feat: record accelerometer to file
This commit is contained in:
@@ -76,6 +76,7 @@ dependencies {
|
|||||||
implementation libs.oboe
|
implementation libs.oboe
|
||||||
implementation libs.slf4j.api
|
implementation libs.slf4j.api
|
||||||
implementation libs.logback.android
|
implementation libs.logback.android
|
||||||
|
implementation libs.gson
|
||||||
|
|
||||||
implementation libs.androidx.core.ktx
|
implementation libs.androidx.core.ktx
|
||||||
implementation libs.androidx.lifecycle.runtime.ktx
|
implementation libs.androidx.lifecycle.runtime.ktx
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||||
|
|
||||||
|
|||||||
@@ -11,17 +11,18 @@ import android.hardware.SensorEvent;
|
|||||||
import android.hardware.SensorEventListener;
|
import android.hardware.SensorEventListener;
|
||||||
import android.hardware.SensorManager;
|
import android.hardware.SensorManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.Binder;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
import android.os.PowerManager;
|
import android.os.PowerManager;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
import android.os.SystemClock;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.core.app.NotificationCompat;
|
import androidx.core.app.NotificationCompat;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@@ -98,7 +99,7 @@ public class LstForegroundService extends Service implements SensorEventListener
|
|||||||
Toast.makeText(this, "Could not open music file contentUri", Toast.LENGTH_LONG).show();
|
Toast.makeText(this, "Could not open music file contentUri", Toast.LENGTH_LONG).show();
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
startCollection();
|
startCollection(contentUri);
|
||||||
} else if (ACTION_STOP.equals(action)) {
|
} else if (ACTION_STOP.equals(action)) {
|
||||||
stopCollectionAndSelf();
|
stopCollectionAndSelf();
|
||||||
}
|
}
|
||||||
@@ -126,7 +127,7 @@ public class LstForegroundService extends Service implements SensorEventListener
|
|||||||
return fd;
|
return fd;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startCollection() {
|
private void startCollection(String meta) {
|
||||||
if (isCollecting) {
|
if (isCollecting) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -147,6 +148,7 @@ public class LstForegroundService extends Service implements SensorEventListener
|
|||||||
SensorManager.SENSOR_DELAY_GAME
|
SensorManager.SENSOR_DELAY_GAME
|
||||||
);
|
);
|
||||||
isCollecting = true;
|
isCollecting = true;
|
||||||
|
onStartRecording(meta);
|
||||||
} else {
|
} else {
|
||||||
stopCollectionAndSelf();
|
stopCollectionAndSelf();
|
||||||
}
|
}
|
||||||
@@ -156,6 +158,7 @@ public class LstForegroundService extends Service implements SensorEventListener
|
|||||||
if (isCollecting && sensorManager != null) {
|
if (isCollecting && sensorManager != null) {
|
||||||
sensorManager.unregisterListener(this);
|
sensorManager.unregisterListener(this);
|
||||||
isCollecting = false;
|
isCollecting = false;
|
||||||
|
onStopRecording();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wakeLock != null && wakeLock.isHeld()) {
|
if (wakeLock != null && wakeLock.isHeld()) {
|
||||||
@@ -190,15 +193,66 @@ public class LstForegroundService extends Service implements SensorEventListener
|
|||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class LocalBinder extends Binder {
|
||||||
|
LstForegroundService getService() { return LstForegroundService.this; }
|
||||||
|
}
|
||||||
|
private final LocalBinder binder = new LocalBinder();
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public IBinder onBind(Intent intent) {
|
public IBinder onBind(Intent intent) {
|
||||||
return null;
|
return binder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface OnResultListener {
|
||||||
|
void onResult(SensorDataArray recording);
|
||||||
|
}
|
||||||
|
private OnResultListener listener;
|
||||||
|
public void setOnResultListener(OnResultListener listener) { this.listener = listener; }
|
||||||
|
|
||||||
|
/** single sensor sample */
|
||||||
|
public static class SensorData {
|
||||||
|
private long timestamp;
|
||||||
|
private float[] values;
|
||||||
|
public SensorData(SensorEvent event) {
|
||||||
|
timestamp = event.timestamp;
|
||||||
|
values = Arrays.copyOf(event.values, event.values.length);
|
||||||
|
}
|
||||||
|
public SensorData(long timestamp, float[] values) {
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.values = values;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** array of sensor samples */
|
||||||
|
public static class SensorDataArray {
|
||||||
|
private ArrayList<SensorData> data = new ArrayList<SensorData>();
|
||||||
|
private String meta;
|
||||||
|
public void add(SensorEvent event) { data.add(new SensorData(event)); }
|
||||||
|
public void add(SensorData d) { data.add(d); }
|
||||||
|
public void clear() { data.clear(); }
|
||||||
|
public void setMeta(String meta) { this.meta = meta; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private final SensorDataArray recording = new SensorDataArray();
|
||||||
|
private long recordingStartTime = 0;
|
||||||
|
|
||||||
|
private void onStartRecording(String meta) {
|
||||||
|
recordingStartTime = SystemClock.elapsedRealtimeNanos();
|
||||||
|
recording.setMeta(meta);
|
||||||
|
}
|
||||||
|
private void onStopRecording() {
|
||||||
|
if(listener != null) {
|
||||||
|
listener.onResult(recording);
|
||||||
|
}
|
||||||
|
recording.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSensorChanged(SensorEvent event) {
|
public void onSensorChanged(SensorEvent event) {
|
||||||
|
// pass on to C++ filter bank
|
||||||
stepDetector.filter(event.timestamp, event.values);
|
stepDetector.filter(event.timestamp, event.values);
|
||||||
|
// collect accelerometer recording - adjust timebase to 0.0 sec beginning
|
||||||
|
recording.add(new SensorData(event.timestamp - recordingStartTime, event.values));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
package at.lockstep.app;
|
package at.lockstep.app;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.content.ComponentName;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Environment;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.util.Log;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
|
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
@@ -16,7 +21,15 @@ import at.lockstep.saf.SafPickerActivity;
|
|||||||
import at.lockstep.ui.SongPickerActivity;
|
import at.lockstep.ui.SongPickerActivity;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
public class MainActivity extends AppCompatActivity {
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.Writer;
|
||||||
|
|
||||||
|
public class MainActivity extends AppCompatActivity implements LstForegroundService.OnResultListener {
|
||||||
private Button btnStart;
|
private Button btnStart;
|
||||||
private Button btnStop;
|
private Button btnStop;
|
||||||
private Button btnMediaStoreBenchmark;
|
private Button btnMediaStoreBenchmark;
|
||||||
@@ -80,4 +93,74 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
launcher.launch(intent);
|
launcher.launch(intent);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ServiceConnection conn = new ServiceConnection() {
|
||||||
|
@Override
|
||||||
|
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
|
||||||
|
LstForegroundService service = ((LstForegroundService.LocalBinder) iBinder).getService();
|
||||||
|
service.setOnResultListener(MainActivity.this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServiceDisconnected(ComponentName componentName) {
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
// attach ServiceConnection (so we can attach a listener). incidentally, it seems to also create the service. (will currently create a PlaybackEngine, etc.)
|
||||||
|
// TODO: check if this delays starting the application
|
||||||
|
bindService(new Intent(this, LstForegroundService.class), conn, BIND_AUTO_CREATE);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
protected void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
unbindService(conn);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isForeground = false;
|
||||||
|
@Override
|
||||||
|
protected void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
isForeground = false;
|
||||||
|
// TODO: since the Service keeps running, we must signal oboe to stop playing
|
||||||
|
// TODO: signal the pause to the C++ lib
|
||||||
|
//
|
||||||
|
// telltale signs: logcat: "PlaybackEngine - Buffer overrun on output for channel (0.000000)" or (1.000000)
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
isForeground = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
LstForegroundService.SensorDataArray recording;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResult(LstForegroundService.SensorDataArray recording) {
|
||||||
|
if(!isForeground) {
|
||||||
|
Log.i("MainActivity", "ignore onResult() from LstForegroundService due to backgrounded MainActivity");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.recording = recording;
|
||||||
|
|
||||||
|
//
|
||||||
|
// write accelero recording to file
|
||||||
|
//
|
||||||
|
File f = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
|
||||||
|
String dir = f != null ? f.toString() : "/"; // make compiler happy
|
||||||
|
long unixTime = System.currentTimeMillis() / 1000L;
|
||||||
|
String fileName = dir + "/acc_" + unixTime + ".json";
|
||||||
|
Log.i("MainActivity", "written acc rec to " + fileName);
|
||||||
|
try (Writer writer = new FileWriter(fileName)) {
|
||||||
|
Gson gson = new GsonBuilder().create();
|
||||||
|
gson.toJson(recording, writer);
|
||||||
|
} catch (IOException e) {
|
||||||
|
// TODO error handling
|
||||||
|
Log.e("MainActivity", "IOException writing recording: " + e.getMessage());
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
46
app/src/test/java/at/lockstep/GsonUnitTest.java
Normal file
46
app/src/test/java/at/lockstep/GsonUnitTest.java
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package at.lockstep;
|
||||||
|
|
||||||
|
import android.hardware.SensorEvent;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
|
||||||
|
public class GsonUnitTest {
|
||||||
|
|
||||||
|
/** single sensor sample */
|
||||||
|
static class SensorData {
|
||||||
|
private long timestamp;
|
||||||
|
private float[] values;
|
||||||
|
public SensorData(SensorEvent event) {
|
||||||
|
timestamp = event.timestamp;
|
||||||
|
values = Arrays.copyOf(event.values, event.values.length);
|
||||||
|
}
|
||||||
|
public SensorData(long timestamp, float[] values) {
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.values = values;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** array of sensor samples */
|
||||||
|
public static class SensorDataArray {
|
||||||
|
private ArrayList<SensorData> data = new ArrayList<SensorData>();
|
||||||
|
public void add(long timestamp, float[] values) { data.add(new SensorData(timestamp, values)); }
|
||||||
|
public void add(SensorEvent event) { data.add(new SensorData(event)); }
|
||||||
|
public void clear() { data.clear(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGson() {
|
||||||
|
SensorDataArray recording = new SensorDataArray();
|
||||||
|
recording.add(0, new float[]{1, 2, 3});
|
||||||
|
recording.add(1, new float[]{10, 20, 30});
|
||||||
|
Gson gson = new Gson();
|
||||||
|
String json = gson.toJson(recording);
|
||||||
|
System.out.println(json);
|
||||||
|
// {"data":[{"timestamp":0,"values":[1.0,2.0,3.0]},{"timestamp":1,"values":[10.0,20.0,30.0]}]}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ oboe = "1.10.0"
|
|||||||
slf4jApi = "1.7.30"
|
slf4jApi = "1.7.30"
|
||||||
recyclerview = "1.3.1"
|
recyclerview = "1.3.1"
|
||||||
appcompat = "1.7.1"
|
appcompat = "1.7.1"
|
||||||
|
gson = "2.11.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
@@ -34,6 +35,7 @@ oboe = { module = "com.google.oboe:oboe", version.ref = "oboe" }
|
|||||||
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4jApi" }
|
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4jApi" }
|
||||||
androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" }
|
androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" }
|
||||||
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||||
|
gson = { group = "com.google.code.gson", name="gson", version.ref = "gson" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|||||||
Reference in New Issue
Block a user