diff --git a/README.md b/README.md index d19f268a..6f5f8ff8 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ [![Build Status](https://travis-ci.org/MobileRoboticsSkoltech/OpenCamera-Sensors.svg?branch=master)](https://travis-ci.org/MobileRoboticsSkoltech/OpenCamera-Sensors) +OpenCamera Sensors is an Android application for synchronized recording of video and IMU data. It records sensor data (accelerometer, gyroscope, magnetometer) and video with frame timestamps synced to the same clock. + ## Install [Get latest apk](https://github.com/MobileRoboticsSkoltech/OpenCamera-Sensors/releases/latest/download/app-release.apk) @@ -13,7 +15,6 @@ OpenCamera Sensors is an Android application for synchronized recording of video This project is based on [Open Camera](https://opencamera.org.uk/) — a popular open-source camera application with flexibility in camera parameters settings, actively supported by the community. By regular merging of Open Camera updates our app will adapt to new smartphones and APIs — this is an advantage over the other video + IMU recording applications built from scratch for Camera2API. - ## Usage ![screenshot settings](https://imgur.com/BytzCvA.png) @@ -25,10 +26,11 @@ This project is based on [Open Camera](https://opencamera.org.uk/) — a popul - **Record video** - **Get data** from ```DCIM/OpenCamera```: - Video file - - IMU data and frame timestamps in the directory ```{VIDEO_DATE}```: + - Sensor data and frame timestamps in the directory ```{VIDEO_DATE}```: -```{VIDEO_NAME}_gyro.csv```, data format: ```X-data, Y-data, Z-data, timestamp (ns)``` - ```{VIDEO_NAME}_accel.csv```, data format: ```X-data, Y-data, Z-data, timestamp (ns)``` - - ```{VIDEO_NAME}_timestamps.csv```, data format: ```timestamp (ns)``` + - ```{VIDEO_NAME}_magnetic.csv```, data format: ```X-data, Y-data, Z-data, timestamp (ns)``` + - ```{VIDEO_NAME}_timestamps.csv```, data format: ```timestamp (ns)``` ### Remote recording diff --git a/api_client/basic_example.py b/api_client/basic_example.py index 0b345e0b..b3589cb2 100644 --- a/api_client/basic_example.py +++ b/api_client/basic_example.py @@ -1,8 +1,7 @@ import time from src.RemoteControl import RemoteControl -import subprocess -HOST = '192.168.1.100' # The smartphone's IP address +HOST = '192.168.1.75' # The smartphone's IP address def main(): @@ -11,16 +10,16 @@ def main(): remote = RemoteControl(HOST) print("Connected") - accel_data, gyro_data = remote.get_imu(10000, True, False) - print("Accelerometer data length: %d" % len(accel_data)) - with open("accel.csv", "w+") as accel: - accel.writelines(accel_data) + accel_data, gyro_data, magnetic_data = remote.get_imu(10000, True, False, True) + print("Magnetometer data length: %d" % len(magnetic_data)) + with open("magnetic.csv", "w+") as imu_file: + imu_file.writelines(magnetic_data) phase, duration = remote.start_video() print("%d %f" % (phase, duration)) time.sleep(5) remote.stop_video() - + # receives last video (blocks until received) start = time.time() filename = remote.get_video(want_progress_bar=True) diff --git a/api_client/src/RemoteControl.py b/api_client/src/RemoteControl.py index dc2b2734..0fd93a42 100644 --- a/api_client/src/RemoteControl.py +++ b/api_client/src/RemoteControl.py @@ -6,9 +6,9 @@ BUFFER_SIZE = 4096 PROPS_PATH = '../app/src/main/assets/server_config.properties' SUPPORTED_SERVER_VERSIONS = [ - 'v.0.0' + 'v.0.1' ] - +NUM_SENSORS = 3 class RemoteControl: """ @@ -26,24 +26,27 @@ def __init__(self, hostname): self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket.connect((hostname, int(self.props['RPC_PORT']))) - def get_imu(self, duration_ms, want_accel, want_gyro): + def get_imu(self, duration_ms, want_accel, want_gyro, want_magnetic): """ Request IMU data recording :param duration_ms: (int) duration in milliseconds :param want_accel: (boolean) request accelerometer recording :param want_gyro: (boolean) request gyroscope recording - :return: Tuple (accel_data, gyro_data) - csv data strings + :param want_gyro: (boolean) request magnetometer recording + :return: Tuple (accel_data, gyro_data, magnetic_data) - csv data strings If one of the sensors wasn't requested, the corresponding data is None """ accel = int(want_accel) gyro = int(want_gyro) + magnetic = int(want_magnetic) status, socket_file = self._send_and_get_response_status( - 'imu?duration=%d&accel=%d&gyro=%d\n' % (duration_ms, accel, gyro) + 'imu?duration=%d&accel=%d&gyro=%d&magnetic=%d\n' % (duration_ms, accel, gyro, magnetic) ) accel_data = None gyro_data = None + magnetic_data = None - for i in range(2): + for i in range(NUM_SENSORS): # read filename or end marker line = socket_file.readline() msg = line.strip('\n') @@ -62,8 +65,11 @@ def get_imu(self, duration_ms, want_accel, want_gyro): accel_data = data elif msg.endswith("gyro.csv"): gyro_data = data + elif msg.endswith("magnetic.csv"): + magnetic_data = data + socket_file.close() - return accel_data, gyro_data + return accel_data, gyro_data, magnetic_data def start_video(self): """ diff --git a/app/build.gradle b/app/build.gradle index c8fbd211..52bd9002 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,6 +11,7 @@ android { defaultConfig { applicationId "com.opencamera_extended.app" minSdkVersion 19 + // Important! When we decide to change this to 30 or more, we need to add full support for scoped storage targetSdkVersion 29 renderscriptTargetApi 21 diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java index 5e274cf3..d9c930a3 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/MainActivityTest.java @@ -14,6 +14,7 @@ import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; import net.sourceforge.opencamera.LocationSupplier; @@ -31,6 +32,7 @@ import net.sourceforge.opencamera.SaveLocationHistory; import net.sourceforge.opencamera.cameracontroller.CameraController; import net.sourceforge.opencamera.preview.Preview; +import net.sourceforge.opencamera.sensorlogging.RawSensorInfo; import net.sourceforge.opencamera.ui.FolderChooserDialog; import net.sourceforge.opencamera.ui.PopupView; @@ -47,6 +49,7 @@ import android.graphics.Matrix; import android.graphics.Point; import android.graphics.PointF; +import android.hardware.Sensor; import android.hardware.camera2.CameraMetadata; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.params.TonemapCurve; @@ -7838,6 +7841,64 @@ public void testVideoImuInfo() throws InterruptedException { assertEquals(expectedNFiles + 1, nNewFiles); } + /* Test recording video with all sensors enabled + Assumes all of the sensors are supported + */ + public void testVideoAllSensors() throws InterruptedException { + Log.d(TAG, "testVideoAllSensors"); + // check sensor files + Map sensorFilesMap = subTestVideoSensors(true, true, true); + assertSensorRecFileExists(Sensor.TYPE_GYROSCOPE, sensorFilesMap); + assertSensorRecFileExists(Sensor.TYPE_MAGNETIC_FIELD, sensorFilesMap); + assertSensorRecFileExists(Sensor.TYPE_ACCELEROMETER, sensorFilesMap); + } + + public void testVideoMagnetometer() throws InterruptedException { + Log.d(TAG, "testVideoMagnetometer"); + Map sensorFilesMap = subTestVideoSensors(true, false, false); + assertSensorRecFileExists(Sensor.TYPE_MAGNETIC_FIELD, sensorFilesMap); + } + + public void testVideoAccel() throws InterruptedException { + Log.d(TAG, "testVideoAccel"); + Map sensorFilesMap = subTestVideoSensors(false, true, false); + assertSensorRecFileExists(Sensor.TYPE_ACCELEROMETER, sensorFilesMap); + } + + public void testVideoGyro() throws InterruptedException { + Log.d(TAG, "testVideoGyro"); + Map sensorFilesMap = subTestVideoSensors(false, false, true); + assertSensorRecFileExists(Sensor.TYPE_GYROSCOPE, sensorFilesMap); + } + + private void assertSensorRecFileExists(Integer sensorType, Map sensorFilesMap) { + assertTrue( + mActivity.getRawSensorInfoManager().isSensorAvailable(sensorType) && + sensorFilesMap.get(sensorType).canRead() + ); + } + + public Map subTestVideoSensors(boolean wantMagnetic, boolean wantAccel, boolean wantGyro) throws InterruptedException { + setToDefault(); + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mActivity); + SharedPreferences.Editor editor = settings.edit(); + // enable all of the sensors + editor.putBoolean(PreferenceKeys.IMURecordingPreferenceKey, true); + editor.putBoolean(PreferenceKeys.MagnetometerPrefKey, wantMagnetic); + editor.putBoolean(PreferenceKeys.AccelPreferenceKey, wantAccel); + editor.putBoolean(PreferenceKeys.GyroPreferenceKey, wantGyro); + editor.apply(); + updateForSettings(); + + // count initial files in folder + File folder = mActivity.getImageFolder(); + Log.d(TAG, "folder: " + folder); + int expectedNFiles = 1; + int nNewFiles = subTestTakeVideo(false, false, true, false, null, 5000, false, expectedNFiles); + return mActivity.getRawSensorInfoManager() + .getLastSensorFilesMap(); + } + /* Test recording video with raw IMU sensor info */ public void testVideoImuInfoSAF() throws InterruptedException { diff --git a/app/src/androidTest/java/net/sourceforge/opencamera/test/SubsetTests.java b/app/src/androidTest/java/net/sourceforge/opencamera/test/SubsetTests.java index 668593c3..4991fd29 100644 --- a/app/src/androidTest/java/net/sourceforge/opencamera/test/SubsetTests.java +++ b/app/src/androidTest/java/net/sourceforge/opencamera/test/SubsetTests.java @@ -11,6 +11,11 @@ public static Test suite() { TestSuite suite = new TestSuite(MainTests.class.getName()); // Basic video tests suite.addTest(TestSuite.createTest(MainActivityTest.class, "testVideoImuInfo")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testVideoAllSensors")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testVideoGyro")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testVideoAccel")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testVideoMagnetometer")); + suite.addTest(TestSuite.createTest(MainActivityTest.class, "testTakeVideo")); // TODO: update this test for new video rec stop logic, now it relies on synchronous recording stop diff --git a/app/src/main/assets/server_config.properties b/app/src/main/assets/server_config.properties index 6e5ca917..94ad16ed 100644 --- a/app/src/main/assets/server_config.properties +++ b/app/src/main/assets/server_config.properties @@ -1,5 +1,5 @@ RPC_PORT=6969 -SERVER_VERSION=v.0.0 +SERVER_VERSION=v.0.1 VIDEO_START_REQUEST=video_start VIDEO_STOP_REQUEST=video_stop GET_VIDEO_REQUEST=get_video diff --git a/app/src/main/java/net/sourceforge/opencamera/ExtendedAppInterface.java b/app/src/main/java/net/sourceforge/opencamera/ExtendedAppInterface.java index 80c43e96..156834de 100644 --- a/app/src/main/java/net/sourceforge/opencamera/ExtendedAppInterface.java +++ b/app/src/main/java/net/sourceforge/opencamera/ExtendedAppInterface.java @@ -12,6 +12,9 @@ import net.sourceforge.opencamera.sensorlogging.VideoPhaseInfo; import java.io.IOException; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.BlockingQueue; /** @@ -35,7 +38,7 @@ public VideoFrameInfo setupFrameInfo() throws IOException { ExtendedAppInterface(MainActivity mainActivity, Bundle savedInstanceState) { super(mainActivity, savedInstanceState); - mRawSensorInfo = new RawSensorInfo(mainActivity); + mRawSensorInfo = mainActivity.getRawSensorInfoManager(); mMainActivity = mainActivity; mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(mainActivity); // We create it only once here (not during the video) as it is a costly operation @@ -70,6 +73,10 @@ private boolean getGyroPref() { return mSharedPreferences.getBoolean(PreferenceKeys.GyroPreferenceKey, true); } + private boolean getMagneticPref() { + return mSharedPreferences.getBoolean(PreferenceKeys.MagnetometerPrefKey, true); + } + /** * Retrieves gyroscope and accelerometer sample rate preference and converts it to number */ @@ -93,31 +100,45 @@ public boolean getSaveFramesPref() { return mSharedPreferences.getBoolean(PreferenceKeys.saveFramesPreferenceKey, false); } + public void startImu(boolean wantAccel, boolean wantGyro, boolean wantMagnetic, Date currentDate) { + if (wantAccel) { + int accelSampleRate = getSensorSampleRatePref(PreferenceKeys.AccelSampleRatePreferenceKey); + if (!mRawSensorInfo.enableSensor(Sensor.TYPE_ACCELEROMETER, accelSampleRate)) { + mMainActivity.getPreview().showToast(null, "Accelerometer unavailable"); + } + } + if (wantGyro) { + int gyroSampleRate = getSensorSampleRatePref(PreferenceKeys.GyroSampleRatePreferenceKey); + if (!mRawSensorInfo.enableSensor(Sensor.TYPE_GYROSCOPE, gyroSampleRate)) { + mMainActivity.getPreview().showToast(null, "Gyroscope unavailable"); + } + } + if (wantMagnetic) { + int magneticSampleRate = getSensorSampleRatePref(PreferenceKeys.MagneticSampleRatePreferenceKey); + if (!mRawSensorInfo.enableSensor(Sensor.TYPE_MAGNETIC_FIELD, magneticSampleRate)) { + mMainActivity.getPreview().showToast(null, "Magnetometer unavailable"); + } + } + + //mRawSensorInfo.startRecording(mMainActivity, mLastVideoDate, get Pref(), getAccelPref()) + Map wantSensorRecordingMap = new HashMap<>(); + wantSensorRecordingMap.put(Sensor.TYPE_ACCELEROMETER, getAccelPref()); + wantSensorRecordingMap.put(Sensor.TYPE_GYROSCOPE, getGyroPref()); + wantSensorRecordingMap.put(Sensor.TYPE_MAGNETIC_FIELD, getMagneticPref()); + mRawSensorInfo.startRecording(mMainActivity, currentDate, wantSensorRecordingMap); + } + @Override public void startingVideo() { if (MyDebug.LOG) { Log.d(TAG, "starting video"); } - if (getIMURecordingPref() && useCamera2() && (getGyroPref() || getAccelPref())) { + if (getIMURecordingPref() && useCamera2() && (getGyroPref() || getAccelPref() || getMagneticPref())) { // Extracting sample rates from shared preferences try { - if (getAccelPref()) { - int accelSampleRate = getSensorSampleRatePref(PreferenceKeys.AccelSampleRatePreferenceKey); - if (!mRawSensorInfo.enableSensor(Sensor.TYPE_ACCELEROMETER, accelSampleRate)) { - mMainActivity.getPreview().showToast(null, "Accelerometer unavailable"); - } - } - if (getGyroPref()) { - int gyroSampleRate = getSensorSampleRatePref(PreferenceKeys.GyroSampleRatePreferenceKey); - if (!mRawSensorInfo.enableSensor(Sensor.TYPE_GYROSCOPE, gyroSampleRate)) { - mMainActivity.getPreview().showToast(null, "Gyroscope unavailable"); - // TODO: abort recording? - } - } - - mRawSensorInfo.startRecording(mLastVideoDate, getGyroPref(), getAccelPref()); + mMainActivity.getPreview().showToast("Starting video with IMU recording...", true); + startImu(getAccelPref(), getGyroPref(), getMagneticPref(), mLastVideoDate); // TODO: add message to strings.xml - mMainActivity.getPreview().showToast(null, "Starting video with IMU recording"); } catch (NumberFormatException e) { if (MyDebug.LOG) { Log.e(TAG, "Failed to retrieve the sample rate preference value"); @@ -127,7 +148,7 @@ public void startingVideo() { } else if (getIMURecordingPref() && !useCamera2()) { mMainActivity.getPreview().showToast(null, "Not using Camera2API! Can't record in sync with IMU"); mMainActivity.getPreview().stopVideo(false); - } else if (getIMURecordingPref() && !(getGyroPref() || getAccelPref())) { + } else if (getIMURecordingPref() && !(getGyroPref() || getMagneticPref() || getAccelPref())) { mMainActivity.getPreview().showToast(null, "Requested IMU recording but no sensors were enabled"); mMainActivity.getPreview().stopVideo(false); } @@ -144,7 +165,7 @@ public void stoppingVideo() { mRawSensorInfo.disableSensors(); // TODO: add message to strings.xml - mMainActivity.getPreview().showToast(null, "Finished video with IMU recording"); + mMainActivity.getPreview().showToast("Stopping video with IMU recording...", true); } super.stoppingVideo(); diff --git a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java index 187c8256..a15b17b4 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MainActivity.java +++ b/app/src/main/java/net/sourceforge/opencamera/MainActivity.java @@ -69,6 +69,7 @@ import android.widget.ZoomControls; import androidx.annotation.NonNull; +import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.exifinterface.media.ExifInterface; @@ -99,6 +100,8 @@ */ public class MainActivity extends Activity { private static final String TAG = "MainActivity"; + private static final int WRITE_EXTERNAL_STORAGE = 0; + private static final int REQUEST_PERMISSION = 0; private static int activity_count = 0; @@ -211,6 +214,17 @@ public class MainActivity extends Activity { */ @Override protected void onCreate(Bundle savedInstanceState) { + int permissionCheckStorage = ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE); + if (permissionCheckStorage != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions( MainActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, WRITE_EXTERNAL_STORAGE); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + && ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, + REQUEST_PERMISSION); + + return; + } this.mRawSensorInfo = new RawSensorInfo(this); if (MyDebug.LOG) { Log.d(TAG, "Created RawSensorInfo object"); @@ -2314,6 +2328,7 @@ public void openSettings() { Bundle bundle = new Bundle(); bundle.putBoolean(PreferenceKeys.SupportsGyroKey, mRawSensorInfo.isSensorAvailable(Sensor.TYPE_GYROSCOPE)); bundle.putBoolean(PreferenceKeys.SupportsAccelKey, mRawSensorInfo.isSensorAvailable(Sensor.TYPE_ACCELEROMETER)); + bundle.putBoolean(PreferenceKeys.SupportsMagnetometerKey, mRawSensorInfo.isSensorAvailable(Sensor.TYPE_MAGNETIC_FIELD)); bundle.putInt("cameraId", this.preview.getCameraId()); bundle.putInt("nCameras", preview.getCameraControllerManager().getNumberOfCameras()); @@ -3365,7 +3380,10 @@ public void updateGalleryIcon() { protected Bitmap doInBackground(Void... params) { if( MyDebug.LOG ) Log.d(TAG, "doInBackground"); - StorageUtils.Media media = applicationInterface.getStorageUtils().getLatestMedia(); + StorageUtils.Media media = null; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { + media = applicationInterface.getStorageUtils().getLatestMedia(); + } Bitmap thumbnail = null; KeyguardManager keyguard_manager = (KeyguardManager)MainActivity.this.getSystemService(Context.KEYGUARD_SERVICE); boolean is_locked = keyguard_manager != null && keyguard_manager.inKeyguardRestrictedInputMode(); @@ -3589,7 +3607,10 @@ private void openGallery() { if( uri == null ) { if( MyDebug.LOG ) Log.d(TAG, "go to latest media"); - StorageUtils.Media media = applicationInterface.getStorageUtils().getLatestMedia(); + StorageUtils.Media media = null; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { + media = applicationInterface.getStorageUtils().getLatestMedia(); + } if( media != null ) { if( MyDebug.LOG ) Log.d(TAG, "latest uri:" + media.uri); diff --git a/app/src/main/java/net/sourceforge/opencamera/MyPreferenceFragment.java b/app/src/main/java/net/sourceforge/opencamera/MyPreferenceFragment.java index 24f78f79..622760c3 100644 --- a/app/src/main/java/net/sourceforge/opencamera/MyPreferenceFragment.java +++ b/app/src/main/java/net/sourceforge/opencamera/MyPreferenceFragment.java @@ -127,6 +127,13 @@ public void onCreate(Bundle savedInstanceState) { gyroPref.setChecked(false); gyroPref.setEnabled(false); } + + final boolean supports_magnetometer = bundle.getBoolean(PreferenceKeys.SupportsMagnetometerKey); + if (!supports_magnetometer) { + CheckBoxPreference magnetPref = (CheckBoxPreference)findPreference(PreferenceKeys.MagnetometerPrefKey); + magnetPref.setChecked(false); + magnetPref.setEnabled(false); + } final boolean supports_auto_stabilise = bundle.getBoolean("supports_auto_stabilise"); if( MyDebug.LOG ) Log.d(TAG, "supports_auto_stabilise: " + supports_auto_stabilise); diff --git a/app/src/main/java/net/sourceforge/opencamera/PreferenceKeys.java b/app/src/main/java/net/sourceforge/opencamera/PreferenceKeys.java index 7f9d6d76..0e9afbaa 100644 --- a/app/src/main/java/net/sourceforge/opencamera/PreferenceKeys.java +++ b/app/src/main/java/net/sourceforge/opencamera/PreferenceKeys.java @@ -313,6 +313,10 @@ public static String getVideoQualityPreferenceKey(int cameraId, boolean high_spe public static final String GyroPreferenceKey = "preference_gyro"; + public static final String MagnetometerPrefKey = "preference_magnetometer"; + + public static final String SupportsMagnetometerKey = "preference_supports_magnetometer"; + public static final String SupportsAccelKey = "supports_accel"; public static final String SupportsGyroKey = "supports_gyro"; @@ -323,6 +327,8 @@ public static String getVideoQualityPreferenceKey(int cameraId, boolean high_spe public static final String GyroSampleRatePreferenceKey = "preference_gyro_sample_rate"; + public static final String MagneticSampleRatePreferenceKey = "preference_magnetic_sample_rate"; + public static String getVideoFPSPreferenceKey(int cameraId) { // for cameraId==0, we return preference_video_fps instead of preference_video_fps_0, for // backwards compatibility for people upgrading diff --git a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java index bd579259..f8796230 100644 --- a/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java +++ b/app/src/main/java/net/sourceforge/opencamera/cameracontroller/CameraController2.java @@ -1,17 +1,5 @@ package net.sourceforge.opencamera.cameracontroller; -import net.sourceforge.opencamera.MyDebug; -import net.sourceforge.opencamera.preview.VideoProfile; - -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Queue; - import android.app.Activity; import android.content.Context; import android.graphics.ImageFormat; @@ -36,7 +24,6 @@ import android.hardware.camera2.params.TonemapCurve; import android.location.Location; import android.media.AudioManager; -import androidx.exifinterface.media.ExifInterface; import android.media.Image; import android.media.ImageReader; import android.media.MediaActionSound; @@ -44,8 +31,6 @@ import android.os.Build; import android.os.Handler; import android.os.HandlerThread; -import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; import android.util.Log; import android.util.Pair; import android.util.Range; @@ -55,6 +40,22 @@ import android.view.SurfaceHolder; import android.view.TextureView; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.exifinterface.media.ExifInterface; + +import net.sourceforge.opencamera.MyDebug; +import net.sourceforge.opencamera.preview.VideoProfile; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Queue; + /** Provides support using Android 5's Camera 2 API * android.hardware.camera2.*. */ @@ -5106,8 +5107,15 @@ public void closeVideoRecordingSession() { // onClosed() callback to ensure that // mediaRecorder encodes all the frames // from the capture session - captureSession.close(); - captureSession = null; + synchronized( background_camera_lock ) { + if (captureSession != null) { + Log.d(TAG, "Before captureSession.close()"); + captureSession.close(); + Log.d(TAG, "After captureSession.close()"); + + captureSession = null; + } + } } private List getCaptureSessionOutputSurfaces( @@ -7219,11 +7227,9 @@ private void createVideoFrameImageReader( if (MyDebug.LOG) { Log.d(TAG, "Create video frame image reader for capture session"); } - List yuvSizes = Arrays.asList( - characteristics.get( - CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) - .getOutputSizes(ImageFormat.YUV_420_888) - ); + android.util.Size[] yuvSizes = characteristics.get( + CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) + .getOutputSizes(ImageFormat.YUV_420_888); List controllerSizes = new LinkedList<>(); for (android.util.Size yuvSize : yuvSizes) { controllerSizes.add(new CameraController.Size(yuvSize.getWidth(), yuvSize.getHeight())); diff --git a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java index 6939c5e2..42acb8ed 100644 --- a/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java +++ b/app/src/main/java/net/sourceforge/opencamera/preview/Preview.java @@ -962,9 +962,10 @@ private void configureTransform() { cameraSurface.setTransform(matrix); } - private void stopVideoPostPrepare() { + private void stopVideoPostPrepare(boolean from_restart) { applicationInterface.stoppingVideo(); Log.d(TAG, "Stopping video post prepare"); + if( video_recorder != null ) { // check again, just to be safe if( MyDebug.LOG ) Log.d(TAG, "stop video recording"); @@ -1005,7 +1006,6 @@ private void stopVideoPostPrepare() { @TargetApi(Build.VERSION_CODES.LOLLIPOP) public void stopVideo(boolean from_restart) { camera_controller.closeVideoRecordingSession(); - if( MyDebug.LOG ) Log.d(TAG, "stopVideo()"); @@ -1032,12 +1032,13 @@ public void stopVideo(boolean from_restart) { remaining_restart_video = 0; } + /* If camera2api is not used, we should stop video immediately. Otherwise it is done in callback after captureSession is closed */ if (!usingCamera2API()) { - stopVideoPostPrepare(); + stopVideoPostPrepare(from_restart); } } @@ -5625,7 +5626,7 @@ public void onVideoFrameTimestampAvailable(long timestamp) { @Override public void onVideoCaptureSessionClosed() { - stopVideoPostPrepare(); + stopVideoPostPrepare(false); if (mVideoFrameInfoWriter != null) { mVideoFrameInfoWriter.close(); mVideoFrameInfoWriter = null; diff --git a/app/src/main/java/net/sourceforge/opencamera/sensorlogging/RawSensorInfo.java b/app/src/main/java/net/sourceforge/opencamera/sensorlogging/RawSensorInfo.java index 6c11a5b9..9b12f3c8 100644 --- a/app/src/main/java/net/sourceforge/opencamera/sensorlogging/RawSensorInfo.java +++ b/app/src/main/java/net/sourceforge/opencamera/sensorlogging/RawSensorInfo.java @@ -19,46 +19,65 @@ import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; +import java.util.Arrays; +import java.util.Collections; import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; /** * Handles gyroscope and accelerometer raw info recording + * Assumes all the used sensor types are motion or position sensors + * and output [x, y, z] values -- the class should be updated if that changes */ public class RawSensorInfo implements SensorEventListener { private static final String TAG = "RawSensorInfo"; - private static final String SENSOR_TYPE_ACCEL = "accel"; - private static final String SENSOR_TYPE_GYRO = "gyro"; private static final String CSV_SEPARATOR = ","; + private static final List SENSOR_TYPES = Collections.unmodifiableList( + Arrays.asList(Sensor.TYPE_ACCELEROMETER, Sensor.TYPE_GYROSCOPE, Sensor.TYPE_MAGNETIC_FIELD) + ); + private static final Map SENSOR_TYPE_NAMES; + static { + SENSOR_TYPE_NAMES = new HashMap<>(); + SENSOR_TYPE_NAMES.put(Sensor.TYPE_ACCELEROMETER, "accel"); + SENSOR_TYPE_NAMES.put(Sensor.TYPE_GYROSCOPE, "gyro"); + SENSOR_TYPE_NAMES.put(Sensor.TYPE_MAGNETIC_FIELD, "magnetic"); + } final private SensorManager mSensorManager; - final private Sensor mSensorGyro; +/* final private Sensor mSensorGyro; final private Sensor mSensorAccel; + final private Sensor mSensorMagnetic; private PrintWriter mGyroBufferedWriter; - private PrintWriter mAccelBufferedWriter; - private String mLastGyroPath; - private String mLastAccelPath; + private PrintWriter mAccelBufferedWriter;*/ private boolean mIsRecording; - private final MainActivity mContext; + private final Map mUsedSensorMap; + private final Map mSensorWriterMap; + private final Map mLastSensorFilesMap; + + public Map getLastSensorFilesMap() { + return mLastSensorFilesMap; + } public boolean isSensorAvailable(int sensorType) { - if (sensorType == Sensor.TYPE_ACCELEROMETER) { - return mSensorAccel != null; - } else if (sensorType == Sensor.TYPE_GYROSCOPE) { - return mSensorGyro != null; - } else { - if (MyDebug.LOG) { - Log.e(TAG, "Requested unsupported sensor"); - } - throw new IllegalArgumentException(); - } + return mUsedSensorMap.get(sensorType) != null; } public RawSensorInfo(MainActivity context) { mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); - mSensorGyro = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); + mUsedSensorMap = new HashMap<>(); + mSensorWriterMap = new HashMap<>(); + mLastSensorFilesMap = new HashMap<>(); + + for (Integer sensorType : SENSOR_TYPES) { + mUsedSensorMap.put(sensorType, mSensorManager.getDefaultSensor(sensorType)); + } +/* mSensorGyro = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); mSensorAccel = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); mContext = context; + mSensorMagnetic = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); if (MyDebug.LOG) { Log.d(TAG, "RawSensorInfo"); @@ -68,14 +87,13 @@ public RawSensorInfo(MainActivity context) { if (mSensorAccel == null) { Log.d(TAG, "Accelerometer not available"); } - } + }*/ } public int getSensorMinDelay(int sensorType) { - if (sensorType == Sensor.TYPE_ACCELEROMETER) { - return mSensorAccel.getMinDelay(); - } else if (sensorType == Sensor.TYPE_GYROSCOPE) { - return mSensorGyro.getMinDelay(); + Sensor sensor = mUsedSensorMap.get(sensorType); + if (sensor != null) { + return sensor.getMinDelay(); } else { // Unsupported sensorType if (MyDebug.LOG) { @@ -94,11 +112,22 @@ public void onSensorChanged(SensorEvent event) { } sensorData.append(event.timestamp).append("\n"); - if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER && mAccelBufferedWriter != null) { + Sensor sensor = mUsedSensorMap.get(event.sensor.getType()); + if (sensor != null) { + PrintWriter sensorWriter = mSensorWriterMap.get(event.sensor.getType()); + if (sensorWriter != null) { + sensorWriter.write(sensorData.toString()); + } else { + if (MyDebug.LOG) { + Log.d(TAG, "Sensor writer for the requested type wasn't initialized"); + } + } + } + /*if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER && mAccelBufferedWriter != null) { mAccelBufferedWriter.write(sensorData.toString()); } else if (event.sensor.getType() == Sensor.TYPE_GYROSCOPE && mGyroBufferedWriter != null) { mGyroBufferedWriter.write(sensorData.toString()); - } + }*/ } } @@ -107,50 +136,41 @@ public void onAccuracyChanged(Sensor sensor, int accuracy) { // TODO: Add logs for when sensor accuracy decreased } - private void createLastSensorPath(File sensorFile, String sensorType) { - String path = sensorFile.getAbsolutePath(); - switch (sensorType) { - case SENSOR_TYPE_ACCEL: - mLastAccelPath = path; - break; - case SENSOR_TYPE_GYRO: - mLastGyroPath = path; - break; - } - } - /** * Handles sensor info file creation, uses StorageUtils to work both with SAF and standard file * access. */ - private FileWriter getRawSensorInfoFileWriter(MainActivity mainActivity, String sensorType, - Date date) throws IOException { + private FileWriter getRawSensorInfoFileWriter(MainActivity mainActivity, Integer sensorType, String sensorName, + Date lastVideoDate) throws IOException { StorageUtilsWrapper storageUtils = mainActivity.getStorageUtils(); FileWriter fileWriter; try { if (storageUtils.isUsingSAF()) { Uri saveUri = storageUtils.createOutputCaptureInfoFileSAF( - StorageUtils.MEDIA_TYPE_RAW_SENSOR_INFO, sensorType, "csv", date + StorageUtils.MEDIA_TYPE_RAW_SENSOR_INFO, sensorName, "csv", lastVideoDate ); ParcelFileDescriptor rawSensorInfoPfd = mainActivity .getContentResolver() .openFileDescriptor(saveUri, "w"); - fileWriter = new FileWriter(rawSensorInfoPfd.getFileDescriptor()); - File saveFile = storageUtils.getFileFromDocumentUriSAF(saveUri, false); - createLastSensorPath(saveFile, sensorType); - storageUtils.broadcastFile(saveFile, true, false, true); + if (rawSensorInfoPfd != null) { + fileWriter = new FileWriter(rawSensorInfoPfd.getFileDescriptor()); + File saveFile = storageUtils.getFileFromDocumentUriSAF(saveUri, false); + storageUtils.broadcastFile(saveFile, true, false, true); + mLastSensorFilesMap.put(sensorType, saveFile); + } else { + throw new IOException("File descriptor was null"); + } } else { File saveFile = storageUtils.createOutputCaptureInfoFile( - StorageUtils.MEDIA_TYPE_RAW_SENSOR_INFO, sensorType, "csv", date + StorageUtils.MEDIA_TYPE_RAW_SENSOR_INFO, sensorName, "csv", lastVideoDate ); fileWriter = new FileWriter(saveFile); if (MyDebug.LOG) { Log.d(TAG, "save to: " + saveFile.getAbsolutePath()); } - createLastSensorPath(saveFile, sensorType); + mLastSensorFilesMap.put(sensorType, saveFile); storageUtils.broadcastFile(saveFile, false, false, false); } - return fileWriter; } catch (IOException e) { e.printStackTrace(); @@ -161,10 +181,10 @@ private FileWriter getRawSensorInfoFileWriter(MainActivity mainActivity, String } } - private PrintWriter setupRawSensorInfoWriter(String sensorType, - Date date) throws IOException { + private PrintWriter setupRawSensorInfoWriter(MainActivity mainActivity, Integer sensorType, String sensorName, + Date currentVideoDate) throws IOException { FileWriter rawSensorInfoFileWriter = getRawSensorInfoFileWriter( - mContext, sensorType, date + mainActivity, sensorType, sensorName, currentVideoDate ); PrintWriter rawSensorInfoWriter = new PrintWriter( new BufferedWriter(rawSensorInfoFileWriter) @@ -172,24 +192,40 @@ private PrintWriter setupRawSensorInfoWriter(String sensorType, return rawSensorInfoWriter; } - public void startRecording(Date date) { - startRecording(date, true, true); + public void startRecording(MainActivity mainActivity, Date currentVideoDate) { + Map wantSensorRecordingMap = new HashMap<>(); + for (Integer sensorType : SENSOR_TYPES) { + wantSensorRecordingMap.put(sensorType, true); + } + startRecording(mainActivity, currentVideoDate, wantSensorRecordingMap); } - public void startRecording(Date date, boolean wantGyroRecording, boolean wantAccelRecording) { + public void startRecording(MainActivity mainActivity, Date currentVideoDate, Map wantSensorRecordingMap) { + mLastSensorFilesMap.clear(); try { - if (wantGyroRecording && mSensorGyro != null) { +/* if (wantGyroRecording && mSensorGyro != null) { mGyroBufferedWriter = setupRawSensorInfoWriter( - SENSOR_TYPE_GYRO, date + mainActivity, SENSOR_TYPE_GYRO, currentVideoDate ); } if (wantAccelRecording && mSensorAccel != null) { mAccelBufferedWriter = setupRawSensorInfoWriter( - SENSOR_TYPE_ACCEL, date + mainActivity, SENSOR_TYPE_ACCEL, currentVideoDate ); + }*/ + for (Integer sensorType : wantSensorRecordingMap.keySet()) { + Boolean wantRecording = wantSensorRecordingMap.get(sensorType); + if (sensorType != null && + wantRecording != null && + wantRecording == true + ) { + mSensorWriterMap.put( + sensorType, + setupRawSensorInfoWriter(mainActivity, sensorType, SENSOR_TYPE_NAMES.get(sensorType), currentVideoDate) + ); + } } mIsRecording = true; - Log.d(TAG, "thread " + Thread.currentThread().getName()); } catch (IOException e) { e.printStackTrace(); if (MyDebug.LOG) { @@ -202,14 +238,19 @@ public void stopRecording() { if (MyDebug.LOG) { Log.d(TAG, "Close all files"); } - if (mGyroBufferedWriter != null) { + for (PrintWriter sensorWriter : mSensorWriterMap.values()) { + if (sensorWriter != null) { + sensorWriter.close(); + } + } + /*if (mGyroBufferedWriter != null) { mGyroBufferedWriter.flush(); mGyroBufferedWriter.close(); } if (mAccelBufferedWriter != null) { mAccelBufferedWriter.flush(); mAccelBufferedWriter.close(); - } + }*/ mIsRecording = false; } @@ -217,12 +258,24 @@ public boolean isRecording() { return mIsRecording; } - public void enableSensors(int accelSampleRate, int gyroSampleRate) { + public void enableSensors(Map sampleRateMap) { if (MyDebug.LOG) { Log.d(TAG, "enableSensors"); } - enableSensor(Sensor.TYPE_GYROSCOPE, gyroSampleRate); - enableSensor(Sensor.TYPE_ACCELEROMETER, accelSampleRate); + for (Integer sensorType : mUsedSensorMap.keySet()) { + Integer sampleRate = sampleRateMap.get(sensorType); + if (sampleRate == null) { + // Assign default value if not provided + sampleRate = 0; + } + + if (sensorType != null) { + enableSensor(sensorType, sampleRate); + } + + } + /*enableSensor(Sensor.TYPE_GYROSCOPE, gyroSampleRate); + enableSensor(Sensor.TYPE_ACCELEROMETER, accelSampleRate);*/ } @@ -235,7 +288,14 @@ public boolean enableSensor(int sensorType, int sampleRate) { Log.d(TAG, "enableSensor"); } - if (sensorType == Sensor.TYPE_ACCELEROMETER) { + Sensor sensor = mUsedSensorMap.get(sensorType); + if (sensor != null) { + mSensorManager.registerListener(this, sensor, sampleRate); + return true; + } else { + return false; + } + /*if (sensorType == Sensor.TYPE_ACCELEROMETER) { if (mSensorAccel == null) return false; mSensorManager.registerListener(this, mSensorAccel, sampleRate); return true; @@ -245,7 +305,7 @@ public boolean enableSensor(int sensorType, int sampleRate) { return true; } else { return false; - } + }*/ } public void disableSensors() { @@ -254,12 +314,4 @@ public void disableSensors() { } mSensorManager.unregisterListener(this); } - - public String getLastGyroPath() { - return mLastGyroPath; - } - - public String getLastAccelPath() { - return mLastAccelPath; - } } diff --git a/app/src/main/java/net/sourceforge/opencamera/sensorlogging/VideoFrameInfo.java b/app/src/main/java/net/sourceforge/opencamera/sensorlogging/VideoFrameInfo.java index 5a30584e..99812f2b 100644 --- a/app/src/main/java/net/sourceforge/opencamera/sensorlogging/VideoFrameInfo.java +++ b/app/src/main/java/net/sourceforge/opencamera/sensorlogging/VideoFrameInfo.java @@ -191,7 +191,9 @@ public void close() { try { if (mFrameBufferedWriter != null) { + Log.d(TAG, "Before writer close()"); mFrameBufferedWriter.close(); + Log.d(TAG, "After writer close()"); } } catch (IOException e) { Log.d(TAG, "Exception occurred when attempting to close mFrameBufferedWriter"); diff --git a/app/src/main/java/net/sourceforge/opencamera/sensorremote/RemoteRpcRequestHandler.java b/app/src/main/java/net/sourceforge/opencamera/sensorremote/RemoteRpcRequestHandler.java index d2b8a639..757b782b 100644 --- a/app/src/main/java/net/sourceforge/opencamera/sensorremote/RemoteRpcRequestHandler.java +++ b/app/src/main/java/net/sourceforge/opencamera/sensorremote/RemoteRpcRequestHandler.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.io.PrintStream; import java.util.Date; +import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; @@ -65,7 +66,7 @@ RemoteRpcResponse handleInvalidRequest() { return mResponseBuilder.error("Invalid request", mContext); } - RemoteRpcResponse handleImuRequest(long durationMillis, boolean wantAccel, boolean wantGyro) { + RemoteRpcResponse handleImuRequest(long durationMillis, boolean wantAccel, boolean wantGyro, boolean wantMagnetic) { if (mRawSensorInfo != null && !mRawSensorInfo.isRecording()) { // TODO: custom rates? Callable recStartCallable = () -> { @@ -73,11 +74,11 @@ RemoteRpcResponse handleImuRequest(long durationMillis, boolean wantAccel, boole SharedPreferences.Editor prefEditor = sharedPreferences.edit(); prefEditor.putBoolean(PreferenceKeys.AccelPreferenceKey, wantAccel); prefEditor.putBoolean(PreferenceKeys.GyroPreferenceKey, wantGyro); + prefEditor.putBoolean(PreferenceKeys.MagnetometerPrefKey, wantMagnetic); prefEditor.apply(); - mRawSensorInfo.enableSensors(0, 0); Date currentDate = new Date(); - mRawSensorInfo.startRecording(currentDate); + mContext.getApplicationInterface().startImu(wantAccel, wantGyro, wantMagnetic, currentDate); return null; }; @@ -94,7 +95,6 @@ RemoteRpcResponse handleImuRequest(long durationMillis, boolean wantAccel, boole } try { - // Await recording start FutureTask recStartTask = new FutureTask<>(recStartCallable); mContext.runOnUiThread(recStartTask); @@ -107,14 +107,21 @@ RemoteRpcResponse handleImuRequest(long durationMillis, boolean wantAccel, boole recStopTask.get(); StringBuilder msg = new StringBuilder(); try { - if (wantAccel && mRawSensorInfo.getLastAccelPath() != null) { - File imuFile = new File(mRawSensorInfo.getLastAccelPath()); + Map lastSensorFiles = mRawSensorInfo.getLastSensorFilesMap(); + if (wantAccel && lastSensorFiles.get(Sensor.TYPE_ACCELEROMETER) != null) { + File imuFile = lastSensorFiles.get(Sensor.TYPE_ACCELEROMETER); + msg.append(getSensorData(imuFile)); + msg.append(SENSOR_DATA_END_MARKER); + msg.append("\n"); + } + if (wantGyro && lastSensorFiles.get(Sensor.TYPE_GYROSCOPE) != null) { + File imuFile = lastSensorFiles.get(Sensor.TYPE_GYROSCOPE); msg.append(getSensorData(imuFile)); msg.append(SENSOR_DATA_END_MARKER); msg.append("\n"); } - if (wantGyro && mRawSensorInfo.getLastGyroPath() != null) { - File imuFile = new File(mRawSensorInfo.getLastGyroPath()); + if (wantMagnetic && lastSensorFiles.get(Sensor.TYPE_MAGNETIC_FIELD) != null) { + File imuFile = lastSensorFiles.get(Sensor.TYPE_MAGNETIC_FIELD); msg.append(getSensorData(imuFile)); msg.append(SENSOR_DATA_END_MARKER); msg.append("\n"); diff --git a/app/src/main/java/net/sourceforge/opencamera/sensorremote/RemoteRpcServer.java b/app/src/main/java/net/sourceforge/opencamera/sensorremote/RemoteRpcServer.java index 2b2bf593..456ae0c7 100644 --- a/app/src/main/java/net/sourceforge/opencamera/sensorremote/RemoteRpcServer.java +++ b/app/src/main/java/net/sourceforge/opencamera/sensorremote/RemoteRpcServer.java @@ -39,7 +39,7 @@ public class RemoteRpcServer extends Thread { private static final String TAG = "RemoteRpcServer"; private static final int SOCKET_WAIT_TIME_MS = 1000; - private static final String IMU_REQUEST_REGEX = "(imu\\?duration=)(\\d+)(&accel=)(\\d)(&gyro=)(\\d)"; + private static final String IMU_REQUEST_REGEX = "(imu\\?duration=)(\\d+)(&accel=)(\\d)(&gyro=)(\\d)(&magnetic=)(\\d)"; private static final Pattern IMU_REQUEST_PATTERN = Pattern.compile(IMU_REQUEST_REGEX); private final Properties mConfig; @@ -76,11 +76,12 @@ private void handleRequest(String msg, PrintStream outputStream, BufferedOutputS long duration = Long.parseLong(imuRequestMatcher.group(2)); boolean wantAccel = Integer.parseInt(imuRequestMatcher.group(4)) == 1; boolean wantGyro = Integer.parseInt(imuRequestMatcher.group(6)) == 1; + boolean wantMagnetic = Integer.parseInt(imuRequestMatcher.group(8)) == 1; if (MyDebug.LOG) { Log.d(TAG, "received IMU control request, duration = " + duration); } - RemoteRpcResponse imuResponse = mRequestHandler.handleImuRequest(duration, wantAccel, wantGyro); + RemoteRpcResponse imuResponse = mRequestHandler.handleImuRequest(duration, wantAccel, wantGyro, wantMagnetic); outputStream.println(imuResponse.toString()); if (MyDebug.LOG) { diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 360359dd..3bb707b1 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -357,7 +357,7 @@ @string/duration_45m @string/duration_1h - + 20000 10000 5000 @@ -365,13 +365,30 @@ 0 - + 50 Hz 100 Hz 200 Hz 500 Hz Maximum possible + + 1000000 + 100000 + 50000 + 20000 + 10000 + 0 + + + + 1 Hz + 10 Hz + 20 Hz + 50 Hz + 100 Hz + Maximum possible + 0 3 diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 2a71b8c8..aea76ecb 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -83,18 +83,6 @@ android:defaultValue="true" /> - - - - - + - + - + + + + + + + + + + + + + + + + +