feat: record accelerometer to file

This commit is contained in:
2026-03-22 15:16:13 +01:00
parent 7fb3029e8b
commit cc1a2b5b7a
6 changed files with 192 additions and 5 deletions

View File

@@ -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

View File

@@ -6,6 +6,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<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_MEDIA_AUDIO" />

View File

@@ -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<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
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

View File

@@ -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);
}
}
}

View 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]}]}
}
}

View File

@@ -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" }