diff --git a/app/build.gradle b/app/build.gradle
index 7e1ab67..283dd4b 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -76,6 +76,7 @@ dependencies {
implementation libs.oboe
implementation libs.slf4j.api
implementation libs.logback.android
+ implementation libs.gson
implementation libs.androidx.core.ktx
implementation libs.androidx.lifecycle.runtime.ktx
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8b4de8c..1770bf5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,6 +6,7 @@
+
diff --git a/app/src/main/java/at/lockstep/app/LstForegroundService.java b/app/src/main/java/at/lockstep/app/LstForegroundService.java
index 56cd573..ff84897 100644
--- a/app/src/main/java/at/lockstep/app/LstForegroundService.java
+++ b/app/src/main/java/at/lockstep/app/LstForegroundService.java
@@ -11,17 +11,18 @@ import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.net.Uri;
+import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.PowerManager;
import android.util.Log;
import android.widget.Toast;
+import android.os.SystemClock;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
-import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
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();
throw new RuntimeException(e);
}
- startCollection();
+ startCollection(contentUri);
} else if (ACTION_STOP.equals(action)) {
stopCollectionAndSelf();
}
@@ -126,7 +127,7 @@ public class LstForegroundService extends Service implements SensorEventListener
return fd;
}
- private void startCollection() {
+ private void startCollection(String meta) {
if (isCollecting) {
return;
}
@@ -147,6 +148,7 @@ public class LstForegroundService extends Service implements SensorEventListener
SensorManager.SENSOR_DELAY_GAME
);
isCollecting = true;
+ onStartRecording(meta);
} else {
stopCollectionAndSelf();
}
@@ -156,6 +158,7 @@ public class LstForegroundService extends Service implements SensorEventListener
if (isCollecting && sensorManager != null) {
sensorManager.unregisterListener(this);
isCollecting = false;
+ onStopRecording();
}
if (wakeLock != null && wakeLock.isHeld()) {
@@ -190,15 +193,66 @@ public class LstForegroundService extends Service implements SensorEventListener
super.onDestroy();
}
+ public class LocalBinder extends Binder {
+ LstForegroundService getService() { return LstForegroundService.this; }
+ }
+ private final LocalBinder binder = new LocalBinder();
@Nullable
@Override
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 data = new ArrayList();
+ 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
public void onSensorChanged(SensorEvent event) {
+ // pass on to C++ filter bank
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
diff --git a/app/src/main/java/at/lockstep/app/MainActivity.java b/app/src/main/java/at/lockstep/app/MainActivity.java
index 7fcd77b..83563c9 100644
--- a/app/src/main/java/at/lockstep/app/MainActivity.java
+++ b/app/src/main/java/at/lockstep/app/MainActivity.java
@@ -1,8 +1,13 @@
package at.lockstep.app;
import android.app.Activity;
+import android.content.ComponentName;
import android.content.Intent;
+import android.content.ServiceConnection;
import android.os.Bundle;
+import android.os.Environment;
+import android.os.IBinder;
+import android.util.Log;
import android.widget.Button;
import androidx.activity.result.ActivityResultLauncher;
@@ -16,7 +21,15 @@ import at.lockstep.saf.SafPickerActivity;
import at.lockstep.ui.SongPickerActivity;
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 btnStop;
private Button btnMediaStoreBenchmark;
@@ -80,4 +93,74 @@ public class MainActivity extends AppCompatActivity {
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);
+ }
+ }
}
diff --git a/app/src/test/java/at/lockstep/GsonUnitTest.java b/app/src/test/java/at/lockstep/GsonUnitTest.java
new file mode 100644
index 0000000..cdd7760
--- /dev/null
+++ b/app/src/test/java/at/lockstep/GsonUnitTest.java
@@ -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 data = new ArrayList();
+ 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]}]}
+ }
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index bb63fed..2a628ba 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -13,6 +13,7 @@ oboe = "1.10.0"
slf4jApi = "1.7.30"
recyclerview = "1.3.1"
appcompat = "1.7.1"
+gson = "2.11.0"
[libraries]
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" }
androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+gson = { group = "com.google.code.gson", name="gson", version.ref = "gson" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }