From 0f1e492bdc2e4fac344e6995c1ef8d3115b59695 Mon Sep 17 00:00:00 2001 From: Francesco Gabbrielli Date: Tue, 8 Aug 2017 13:26:37 +1000 Subject: [PATCH] Permissions - implemented better checking - made all optional in preferences --- app/build.gradle | 4 +- app/src/main/assets/defaults.properties | 8 + .../sensorlogger/CameraHandlerThread.java | 34 ++- .../apps/sensorlogger/FTPUploader.java | 105 +++++---- .../apps/sensorlogger/MainActivity.java | 213 +++++++++++------- .../apps/sensorlogger/RecordingService.java | 29 ++- .../apps/sensorlogger/SettingsActivity.java | 25 +- .../apps/sensorlogger/Util.java | 51 ++++- app/src/main/res/values/strings.xml | 19 +- app/src/main/res/xml/pref_capture.xml | 6 + app/src/main/res/xml/pref_ftp.xml | 16 +- app/src/main/res/xml/pref_logging.xml | 17 +- 12 files changed, 371 insertions(+), 156 deletions(-) create mode 100644 app/src/main/assets/defaults.properties diff --git a/app/build.gradle b/app/build.gradle index e9cc14b..24b8bd2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "it.francescogabbrielli.apps.sensorlogger" minSdkVersion 11 targetSdkVersion 25 - versionCode 3 - versionName "0.2.1" + versionCode 5 + versionName "0.2.3" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/app/src/main/assets/defaults.properties b/app/src/main/assets/defaults.properties new file mode 100644 index 0000000..acd76b9 --- /dev/null +++ b/app/src/main/assets/defaults.properties @@ -0,0 +1,8 @@ +# milliseconds between one sample and the other (frequency in Hz = 1 / (default_logging_rate/1000)) +pref_logging_rate=100 + +# length of file in milliseconds +pref_logging_length=15000 + +# upload file every (ms) +pref_logging_update=1000 \ No newline at end of file diff --git a/app/src/main/java/it/francescogabbrielli/apps/sensorlogger/CameraHandlerThread.java b/app/src/main/java/it/francescogabbrielli/apps/sensorlogger/CameraHandlerThread.java index 12b1c2d..bac0e79 100644 --- a/app/src/main/java/it/francescogabbrielli/apps/sensorlogger/CameraHandlerThread.java +++ b/app/src/main/java/it/francescogabbrielli/apps/sensorlogger/CameraHandlerThread.java @@ -1,9 +1,11 @@ package it.francescogabbrielli.apps.sensorlogger; import android.content.Context; +import android.content.SharedPreferences; import android.hardware.Camera; import android.os.Handler; import android.os.HandlerThread; +import android.preference.PreferenceManager; import android.util.Log; import android.view.SurfaceHolder; @@ -20,15 +22,19 @@ public class CameraHandlerThread extends HandlerThread { private Camera camera; + private boolean on; + public CameraHandlerThread(Context context) { super("CameraHandlerThread"); this.context = context; start(); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + on = prefs.getBoolean(Util.PREF_CAPTURE_CAMERA, false); handler = new Handler(getLooper()); } - public void openCamera(final SurfaceHolder holder) { - handler.post(new Runnable() { + public void openCamera(final SurfaceHolder holder, final Runnable callback) { + handler.postDelayed(new Runnable() { @Override public void run() { try { @@ -40,14 +46,36 @@ public void run() { // Util.setCameraDisplayOrientation(MainActivity.this, 0, camera);XXX: rotation? camera.setPreviewDisplay(holder); camera.startPreview(); + if (callback!=null) + callback.run(); } catch (IOException e) { Log.e("Camera", "Error opening camera", e); } } - }); + }, 100); + } + + public void closeCamera(){ + if (camera!=null) + camera.release(); + camera = null; } public Camera getCamera() { return camera; } + + //XXX + public void restart() { + handler.postDelayed(new Runnable() { + @Override + public void run() { + try { + camera.startPreview(); + } catch (Throwable t) { + Log.e("Camera", "Restarting preview?", t); + } + } + },100); + } } diff --git a/app/src/main/java/it/francescogabbrielli/apps/sensorlogger/FTPUploader.java b/app/src/main/java/it/francescogabbrielli/apps/sensorlogger/FTPUploader.java index e45848d..131a52a 100644 --- a/app/src/main/java/it/francescogabbrielli/apps/sensorlogger/FTPUploader.java +++ b/app/src/main/java/it/francescogabbrielli/apps/sensorlogger/FTPUploader.java @@ -10,6 +10,7 @@ import java.io.OutputStream; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; public class FTPUploader { @@ -21,6 +22,15 @@ public class FTPUploader { private String address, user, password; + /** + * Creates a new FTP uploader, that is a wrapper around Apache Commons FTPClient with threading + * support and configuration linked to application settings + * + * TODO: add a kind of listener interface support instead of single callbacks + * + * @param context + * the application context + */ public FTPUploader(Context context) { client = new FTPClient(); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); @@ -28,18 +38,13 @@ public FTPUploader(Context context) { user = prefs.getString(Util.PREF_FTP_USER, ""); password = prefs.getString(Util.PREF_FTP_PW, ""); exec = Executors.newSingleThreadExecutor(); - execute(null); } - public void execute(final Runnable command) { - - if (client.isConnected()) { - - if(command!=null) - exec.execute(command); - - } else { - + /** + * Connects to the server + */ + public void connect() { + if (!client.isConnected()) exec.execute(new Runnable() { @Override public void run() { @@ -52,27 +57,46 @@ public void run() { } catch (Exception e) { Log.e(TAG, "Can't connect to " + address + ", user: " + user); } - if (command!=null) - command.run(); - } - }); - } + } + }); } + public void execute(final Runnable command) { + if (!client.isConnected()) + connect(); + if (command!=null) + exec.execute(command); + } + + /** + * Closes current connection + */ public void close() { - exec.execute(new Runnable() { - @Override - public void run() { - try { - client.logout(); - client.disconnect(); - Log.d(TAG, "Client disconnected"); - } catch (Exception e) { - Log.e(TAG, "Error finalizing FTP connection", e); + if (client!=null && client.isConnected()) { + exec.execute(new Runnable() { + @Override + public void run() { + try { + client.logout(); + client.disconnect(); + Log.d(TAG, "Client disconnected"); + } catch (Exception e) { + Log.e(TAG, "Error finalizing FTP connection", e); + } } - exec.shutdown(); - } - }); + }); + new Thread() { + @Override + public void run() { + try { + exec.awaitTermination(5, TimeUnit.SECONDS); + exec.shutdown(); + } catch(InterruptedException e) { + Log.e(TAG, "Unexpected interruption", e); + } + } + }.start(); + } } /** @@ -83,7 +107,7 @@ public void run() { * @param filename * the filename to write to */ - public void send(final byte[] data, final String filename) { + public void send(final byte[] data, final String filename, final Runnable callback) { if (client.isConnected()) exec.execute(new Runnable() { @Override @@ -92,27 +116,30 @@ public void run() { try { out = client.storeFileStream(filename); out.write(data); + if (callback!=null) + callback.run(); Log.d(TAG, "Data written to "+filename); } catch (Exception e) { - Log.e(FTPUploader.class.getSimpleName(), "FTP Error", e); + Log.e(FTPUploader.class.getSimpleName(), "Transfer Error", e); } finally { try { out.close(); client.completePendingCommand(); + } catch (Exception e) { + Log.e(FTPUploader.class.getSimpleName(), "Unexpected error", e); } - catch (Exception e) {} } } }); - else { - Log.d(TAG, "Retry"); - execute(new Runnable() { - @Override - public void run() { - send(data, filename); - } - }); - } +// else { +// Log.d(TAG, "Retry"); +// execute(new Runnable() { +// @Override +// public void run() { +// send(data, filename); +// } +// }); +// } } } diff --git a/app/src/main/java/it/francescogabbrielli/apps/sensorlogger/MainActivity.java b/app/src/main/java/it/francescogabbrielli/apps/sensorlogger/MainActivity.java index eb8dda4..1991c30 100644 --- a/app/src/main/java/it/francescogabbrielli/apps/sensorlogger/MainActivity.java +++ b/app/src/main/java/it/francescogabbrielli/apps/sensorlogger/MainActivity.java @@ -9,9 +9,6 @@ import android.content.pm.PackageManager; import android.hardware.Camera; import android.media.AudioManager; -import android.os.Environment; -import android.os.Handler; -import android.os.HandlerThread; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.v4.app.ActivityCompat; @@ -25,10 +22,12 @@ import android.view.SurfaceHolder; import android.widget.CompoundButton; import android.widget.FrameLayout; +import android.widget.Toast; import android.widget.ToggleButton; -import java.io.File; -import java.io.IOException; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; public class MainActivity extends AppCompatActivity implements CompoundButton.OnCheckedChangeListener, @@ -38,47 +37,48 @@ public class MainActivity extends AppCompatActivity implements private final static int REQUEST_EXTERNAL_STORAGE = 1; private final static int REQUEST_CAMERA = 2; + private final static int REQUEST_PERMISSIONS = 3; private BroadcastReceiver receiver; private IntentFilter filter; private CameraHandlerThread cameraHandlerThread; private CameraPreview cameraPreview; - - private byte[] sensorsData; + private Camera.ShutterCallback shutterCallback; private FTPUploader uploader; + private SharedPreferences prefs; + @Override protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + prefs = PreferenceManager.getDefaultSharedPreferences(this); receiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (RecordingService.BROADCAST_SEND_DATA.equals(intent.getAction())) { - sensorsData = intent.getStringExtra(RecordingService.EXTRA_SENSORS_DATA).getBytes(); + final byte[] sensorsData = + intent.getStringExtra(RecordingService.EXTRA_SENSORS_DATA).getBytes(); try { Camera camera = cameraHandlerThread.getCamera(); camera.startPreview(); - camera.takePicture( -// new Camera.ShutterCallback() { -// @Override -// public void onShutter() { -// AudioManager mgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE); -// mgr.playSoundEffect(AudioManager.FLAG_PLAY_SOUND); -// } -// }, - null, null, new Camera.PictureCallback() { + camera.takePicture(shutterCallback, null, new Camera.PictureCallback() { @Override - public void onPictureTaken(byte[] imageData, Camera camera) { - send(imageData, sensorsData); + public void onPictureTaken(final byte[] imageData, final Camera camera) { + send(imageData, sensorsData, new Runnable() { + @Override + public void run() { + camera.startPreview(); + } + }); } }); } catch (Exception e) { @@ -95,18 +95,32 @@ public void onPictureTaken(byte[] imageData, Camera camera) { filter.addAction(RecordingService.BROADCAST_SEND_DATA); filter.addAction(RecordingService.BROADCAST_FTP_ERROR); - verifyStoragePermissions(); - verifyCameraPermissions(); - final ToggleButton b = (ToggleButton) findViewById(R.id.btn_rec); b.setChecked(prefs.getBoolean(Util.PREF_RECORDING, false)); b.setOnCheckedChangeListener(this); - cameraHandlerThread = new CameraHandlerThread(this); - cameraPreview = new CameraPreview(this); - FrameLayout preview = (FrameLayout) findViewById(R.id.recording_preview); - preview.addView(cameraPreview); + } + + /** Setup the camera; can be called anytime */ + private void setupCamera() { + + // Start a handler thread for the camera operation if not already started + if (cameraHandlerThread == null) { + cameraHandlerThread = new CameraHandlerThread(this); + cameraPreview = new CameraPreview(this); + FrameLayout preview = (FrameLayout) findViewById(R.id.recording_preview); + preview.addView(cameraPreview); + } + // Click sound + shutterCallback = prefs.getBoolean(Util.PREF_CAPTURE_SOUND, false) ? + new Camera.ShutterCallback() { + @Override + public void onShutter() { + AudioManager mgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + mgr.playSoundEffect(AudioManager.FLAG_PLAY_SOUND); + } + } : null; } protected void onPause() { @@ -118,10 +132,7 @@ protected void onPause() { @Override protected void onResume() { super.onResume(); -// try { -// } catch(Exception e) { -// Log.e(TAG, "Camera recoonection error", e); -// } + verifyPermissions(); } @Override @@ -150,6 +161,7 @@ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if(isChecked) { LocalBroadcastManager.getInstance(this).registerReceiver(receiver, filter); uploader = new FTPUploader(getApplicationContext()); + uploader.connect(); RecordingService.startRecording(this); } else { LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver); @@ -157,44 +169,48 @@ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { uploader.close(); RecordingService.stopRecording(this); - // restart preview??? - Camera camera = cameraHandlerThread.getCamera(); - if (camera!=null) - camera.startPreview(); + // restart preview ??? + cameraHandlerThread.restart(); } } /** - * Checks if the app has permission to write to device storage + * Checks if the app has the required permissions, as per current setttings. * - * If the app does not has permission then the user will be prompted to grant permissions + * If the app does not has permission then the user will be prompted to grant permissions. */ - private void verifyStoragePermissions() { - // Check if we have write permission - int permission = ActivityCompat.checkSelfPermission(this, - Manifest.permission.WRITE_EXTERNAL_STORAGE); - if (permission != PackageManager.PERMISSION_GRANTED) { - // We don't have permission so prompt the user - ActivityCompat.requestPermissions( - this, - new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, - REQUEST_EXTERNAL_STORAGE - ); + private void verifyPermissions() { + + // List all permissions that are involved in activated features + List requests = new LinkedList<>(); + if(prefs.getBoolean(Util.PREF_CAPTURE_CAMERA, false)) + requests.add(Manifest.permission.CAMERA); + if(prefs.getBoolean(Util.PREF_FTP, false)) + requests.add(Manifest.permission.INTERNET); + if(prefs.getBoolean(Util.PREF_LOGGING_SAVE, false)) + requests.add(Manifest.permission.WRITE_EXTERNAL_STORAGE); + + // Check if we have requested permission + for (ListIterator it = requests.listIterator();it.hasNext();) { + String permission = it.next(); + int check = ActivityCompat.checkSelfPermission(this, permission); + if (check != PackageManager.PERMISSION_GRANTED) { + onPermissionDenied(permission); + } else { + it.remove(); + onPermissionGranted(permission); + } } - } - private void verifyCameraPermissions() { - // Check if we have write permission - int permission = ActivityCompat.checkSelfPermission(this, - Manifest.permission.CAMERA); - if (permission != PackageManager.PERMISSION_GRANTED) { + // Request missing permissions + if (!requests.isEmpty()) { // We don't have permission so prompt the user - ActivityCompat.requestPermissions( - this, - new String[]{Manifest.permission.CAMERA}, - REQUEST_CAMERA + ActivityCompat.requestPermissions(this, + requests.toArray(new String[requests.size()]), + REQUEST_PERMISSIONS ); } + } @@ -202,43 +218,83 @@ private void verifyCameraPermissions() { public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { switch (requestCode) { - case REQUEST_EXTERNAL_STORAGE: - if (grantResults.length > 0 - && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - new File(Environment.getExternalStorageDirectory(), getString(R.string.app_folder)).mkdirs(); - } - break; - case REQUEST_CAMERA: - if (grantResults.length > 0 - && grantResults[0] == PackageManager.PERMISSION_GRANTED) { -// createCamera(); - } +// case REQUEST_EXTERNAL_STORAGE: +// if (grantResults.length > 0 +// && grantResults[0] == PackageManager.PERMISSION_GRANTED) { +// new File(Environment.getExternalStorageDirectory(), getString(R.string.app_folder)).mkdirs(); +// } +// break; +// case REQUEST_CAMERA: +// if (grantResults.length > 0 +// && grantResults[0] == PackageManager.PERMISSION_GRANTED) { +// setupCamera(); +// } +// break; + case REQUEST_PERMISSIONS: + for (int i=0;i target) { } /** - * This method stops fragment injection in malicious applications. + * Th-is method stops fragment injection in malicious applications. * Make sure to deny any unknown fragments here. */ protected boolean isValidFragment(String fragmentName) { @@ -224,7 +243,7 @@ private void addPreferencesFromSensors() { PreferenceScreen screen = getPreferenceManager().createPreferenceScreen(getActivity()); screen.setOrderingAsAdded(true); - List sensors = Util.getSensors((SensorManager) getActivity().getSystemService(SENSOR_SERVICE)); + List sensors = Util.getSensors(getActivity()); for (Sensor s : sensors) { CheckBoxPreference p = new CheckBoxPreference(getActivity()); p.setTitle(Util.getSensorName(s)); diff --git a/app/src/main/java/it/francescogabbrielli/apps/sensorlogger/Util.java b/app/src/main/java/it/francescogabbrielli/apps/sensorlogger/Util.java index 9956c4e..f612672 100644 --- a/app/src/main/java/it/francescogabbrielli/apps/sensorlogger/Util.java +++ b/app/src/main/java/it/francescogabbrielli/apps/sensorlogger/Util.java @@ -2,12 +2,14 @@ import android.app.Activity; +import android.content.Context; import android.hardware.Camera; import android.hardware.Sensor; import android.hardware.SensorManager; import android.os.Build; import android.view.Surface; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -15,27 +17,33 @@ public class Util { - public final static String PREF_RECORDING = "pref_recording"; - public final static String PREF_FTP_ADDRESS = "pref_ftp_address"; - public final static String PREF_FTP_USER = "pref_ftp_user"; - public final static String PREF_FTP_PW = "pref_ftp_pw"; - public final static String PREF_LOGGING_RATE = "pref_logging_rate"; - public final static String PREF_LOGGING_UPDATE = "pref_logging_update"; - public final static String PREF_LOGGING_LENGTH = "pref_logging_length"; + public final static String PREF_RECORDING = "pref_recording"; + + public final static String PREF_FTP = "pref_ftp"; + public final static String PREF_FTP_ADDRESS = "pref_ftp_address"; + public final static String PREF_FTP_USER = "pref_ftp_user"; + public final static String PREF_FTP_PW = "pref_ftp_pw"; + public final static String PREF_LOGGING_RATE = "pref_logging_rate"; + public final static String PREF_LOGGING_UPDATE = "pref_logging_update"; + public final static String PREF_LOGGING_LENGTH = "pref_logging_length"; public final static String PREF_LOGGING_HEADERS = "pref_logging_headers"; - public final static String PREF_LOGGING_TIME = "pref_logging_time"; + public final static String PREF_LOGGING_TIME = "pref_logging_time"; + public final static String PREF_LOGGING_TIMESTAMP = "pref_logging_timestamp"; + public final static String PREF_LOGGING_SAVE = "pref_logging_save"; - public final static String PREF_CAPTURE_CAMERA = "pref_capture_camera"; + public final static String PREF_CAPTURE_CAMERA = "pref_capture_camera"; + public final static String PREF_CAPTURE_SOUND = "pref_capture_sound"; /** * Retrieve all available sensors sorted alphabetically * - * @param sensorManager - * the context Sensor Manager + * @param context + * the context * @return * the list of sensors */ - public final static List getSensors(SensorManager sensorManager) { + public final static List getSensors(Context context) { + SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); List sensors = new ArrayList<>(sensorManager.getSensorList(Sensor.TYPE_ALL)); Collections.sort(sensors, new Comparator() { @Override @@ -58,6 +66,25 @@ public final static String getSensorName(Sensor sensor) { return name; } + public final static int getSensorMaxLength(Sensor sensor) { + Method[] methodList = Sensor.class.getDeclaredMethods(); + int m_count = methodList.length; + for (int j = 0; j < m_count; j++) { + Method method = methodList[j]; + if (!method.getName().equals("getMaxLengthValuesArray")) + continue; + method.setAccessible(true); + try { + int values_length = (Integer) method.invoke(sensor, sensor, Build.VERSION.SDK_INT); +// Log.e(TAG,"value length is "+values_length); + return values_length; + } catch (Exception e) { + e.printStackTrace(); + } + } + return -1; + } + public static void setCameraDisplayOrientation(Activity activity, int cameraId, android.hardware.Camera camera) { android.hardware.Camera.CameraInfo info = diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c9a4111..0acbab7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -8,7 +8,9 @@ FTP - FTP Server + + Enable FTP Upload + If this is enabled, the data will be logged through FTP to a remote server IP address User Password @@ -69,12 +71,18 @@ Log time Print time in the first column of the output file + Filename timestamp + Append timestamp to the output filename (if any) + + Save data to a local file + If this is enabled, the data will be saved in the local filesystem + Sensors Sync frequency - - Video Capture + + Frame Capture Camera Capture frames from the main camera present on the device @@ -82,7 +90,8 @@ Screen Capture device\'s screen + Play sound + Play a shutter-like sound whenever a frame is captured - - Vibrate + WARNING! Denying the permission will set this preference back to its default diff --git a/app/src/main/res/xml/pref_capture.xml b/app/src/main/res/xml/pref_capture.xml index 44f9bba..652a0b8 100644 --- a/app/src/main/res/xml/pref_capture.xml +++ b/app/src/main/res/xml/pref_capture.xml @@ -15,4 +15,10 @@ android:title="@string/pref_capture_screen_title" android:summary="@string/pref_capture_screen_description"/> + + diff --git a/app/src/main/res/xml/pref_ftp.xml b/app/src/main/res/xml/pref_ftp.xml index c797312..88e8156 100644 --- a/app/src/main/res/xml/pref_ftp.xml +++ b/app/src/main/res/xml/pref_ftp.xml @@ -1,20 +1,30 @@ + + diff --git a/app/src/main/res/xml/pref_logging.xml b/app/src/main/res/xml/pref_logging.xml index c1ded6c..0a6e252 100644 --- a/app/src/main/res/xml/pref_logging.xml +++ b/app/src/main/res/xml/pref_logging.xml @@ -1,7 +1,6 @@ + + + +