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