Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ android {
}

dependencies {
api(project(":core-common"))

implementation(libs.androidx.datastore.preferences)
implementation(libs.androidx.documentfile)
implementation(libs.coroutines.core)
Expand Down
2 changes: 1 addition & 1 deletion sensorservices/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ android {

dependencies {
implementation(project(":core"))
implementation(project(":wearoslib"))
implementation(project(":recording-core"))

implementation(libs.androidx.core.ktx)
implementation(libs.play.services.location)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import com.google.android.gms.location.LocationResult
import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.Priority
import com.motionapps.sensorbox.core.error.AppError
import com.motionapps.sensorbox.core.error.AppErrorCode
import com.motionapps.sensorbox.core.error.AppResult
import com.motionapps.sensorbox.core.error.appResult
import com.motionapps.sensorbox.core.error.flatMap

Expand Down Expand Up @@ -62,12 +64,12 @@ class GPSHandler : LocationCallback() {
callback?.onLastLocationSuccess(location)
}
}.addOnFailureListener { error ->
AppError.from(AppError.Kind.MEASUREMENT, "Read last GPS location", error)
AppError.from(AppErrorCode.MEASUREMENT, "Read last GPS location", error)
callback?.onLastLocationSuccess(null)
}

locationClient.requestLocationUpdates(request, this, Looper.getMainLooper()).addOnFailureListener { error ->
AppError.from(AppError.Kind.MEASUREMENT, "Request GPS updates", error)
AppError.from(AppErrorCode.MEASUREMENT, "Request GPS updates", error)
}
registered = true
}
Expand Down Expand Up @@ -95,14 +97,14 @@ class GPSHandler : LocationCallback() {
}

/** Stops location updates for the active measurement. */
fun gpsOff(): Result<Unit> = appResult(AppError.Kind.MEASUREMENT, "Stop GPS updates") {
fun gpsOff(): AppResult<Unit> = appResult(AppErrorCode.MEASUREMENT, "Stop GPS updates") {
if (registered) {
Log.i(tag, "Logging off location")
locationClient.flushLocations().addOnFailureListener { error ->
AppError.from(AppError.Kind.MEASUREMENT, "Flush GPS updates", error)
AppError.from(AppErrorCode.MEASUREMENT, "Flush GPS updates", error)
}
locationClient.removeLocationUpdates(this).addOnFailureListener { error ->
AppError.from(AppError.Kind.MEASUREMENT, "Remove GPS updates", error)
AppError.from(AppErrorCode.MEASUREMENT, "Remove GPS updates", error)
}
}
registered = false
Expand Down Expand Up @@ -133,9 +135,9 @@ class GPSHandler : LocationCallback() {
* @param gpsCallback - this object will get access to location and updates, previous is forgotten
*
*/
fun addCallback(context: Context, gpsCallback: OnLocationChangedCallback): Result<Unit> =
(if (registered) gpsOff() else Result.success(Unit)).flatMap {
appResult(AppError.Kind.MEASUREMENT, "Register GPS callback") {
fun addCallback(context: Context, gpsCallback: OnLocationChangedCallback): AppResult<Unit> =
(if (registered) gpsOff() else AppResult.success(Unit)).flatMap {
appResult(AppErrorCode.MEASUREMENT, "Register GPS callback") {
callback = gpsCallback
initialize(context)
gpsCallback.onLocationChanged(lastLocation)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.motionapps.sensorservices.handlers
import android.content.Context
import android.content.Intent
import com.motionapps.sensorbox.core.error.AppError
import com.motionapps.sensorbox.core.error.AppErrorCode
import com.motionapps.sensorbox.core.error.AppResult
import com.motionapps.sensorbox.core.error.appResult
import com.motionapps.sensorbox.core.error.flatMap
import com.motionapps.sensorbox.core.storage.NativeDocumentStorage
Expand All @@ -21,15 +23,15 @@ object StorageHandler {
return SimpleDateFormat(format, Locale.getDefault()).format(calendar.time)
}

fun createMainFolder(context: Context, intent: Intent?): Result<Unit> {
fun createMainFolder(context: Context, intent: Intent?): AppResult<Unit> {
val directoryName = context.getString(R.string.app_name)
return if (intent == null) {
NativeDocumentStorage.hasAppDirectory(context, directoryName).flatMap { exists ->
if (exists) {
Result.success(Unit)
AppResult.success(Unit)
} else {
Result.failure(
AppError(AppError.Kind.STORAGE, "Storage directory is not configured"),
AppResult.failure(
AppError(AppErrorCode.STORAGE, "Storage directory is not configured"),
)
}
}
Expand All @@ -38,34 +40,34 @@ object StorageHandler {
}
}

fun isFolder(context: Context): Result<Boolean> = NativeDocumentStorage.hasAppDirectory(
fun isFolder(context: Context): AppResult<Boolean> = NativeDocumentStorage.hasAppDirectory(
context = context,
appDirectoryName = context.getString(R.string.app_name),
)

fun isAccess(context: Context): Result<Boolean> = isFolder(context)
fun isAccess(context: Context): AppResult<Boolean> = isFolder(context)

fun getFolderName(context: Context): Result<String> = NativeDocumentStorage.displayPath(
fun getFolderName(context: Context): AppResult<String> = NativeDocumentStorage.displayPath(
context = context,
appDirectoryName = context.getString(R.string.app_name),
).map { it ?: context.getString(R.string.no_path) }

fun createInternalStorageMeasurementFolder(context: Context, folderName: String): Result<Unit> {
fun createInternalStorageMeasurementFolder(context: Context, folderName: String): AppResult<Unit> {
val directory = internalMeasurementDirectory(context, folderName)
return appResult(AppError.Kind.STORAGE, "Create internal measurement directory") {
return appResult(AppErrorCode.STORAGE, "Create internal measurement directory") {
directory.exists() || directory.mkdirs()
}.flatMap { created ->
if (created) {
Result.success(Unit)
AppResult.success(Unit)
} else {
Result.failure(
AppError(AppError.Kind.STORAGE, "Create internal measurement directory"),
AppResult.failure(
AppError(AppErrorCode.STORAGE, "Create internal measurement directory"),
)
}
}
}

fun createFolderMeasurement(context: Context, folderName: String): Result<Unit> =
fun createFolderMeasurement(context: Context, folderName: String): AppResult<Unit> =
NativeDocumentStorage.createMeasurementDirectory(
context = context,
appDirectoryName = context.getString(R.string.app_name),
Expand All @@ -77,31 +79,31 @@ object StorageHandler {
folderName: String,
mimeOfNewFile: String,
nameOfNewFile: String,
): Result<OutputStream> = NativeDocumentStorage.openMeasurementFile(
): AppResult<OutputStream> = NativeDocumentStorage.openMeasurementFile(
context = context,
appDirectoryName = context.getString(R.string.app_name),
measurementName = folderName,
mimeType = mimeOfNewFile,
fileName = nameOfNewFile,
)

fun createFileInInternalFolder(context: Context, folderName: String, nameOfFile: String): Result<OutputStream> =
appResult(AppError.Kind.STORAGE, "Prepare internal measurement directory") {
fun createFileInInternalFolder(context: Context, folderName: String, nameOfFile: String): AppResult<OutputStream> =
appResult(AppErrorCode.STORAGE, "Prepare internal measurement directory") {
val directory = internalMeasurementDirectory(context, folderName)
directory to (directory.exists() || directory.mkdirs())
}.flatMap { (directory, ready) ->
if (!ready) {
Result.failure(
AppError(AppError.Kind.STORAGE, "Create internal measurement directory"),
AppResult.failure(
AppError(AppErrorCode.STORAGE, "Create internal measurement directory"),
)
} else {
appResult(AppError.Kind.STORAGE, "Open internal measurement file") {
appResult(AppErrorCode.STORAGE, "Open internal measurement file") {
FileOutputStream(File(directory, nameOfFile))
}
}
}

fun deleteByNameOfFolder(context: Context, deleteFolder: String): Result<Unit> =
fun deleteByNameOfFolder(context: Context, deleteFolder: String): AppResult<Unit> =
NativeDocumentStorage.deleteMeasurement(
context = context,
appDirectoryName = context.getString(R.string.app_name),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package com.motionapps.sensorservices.handlers.measurements

import android.Manifest
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import androidx.core.content.ContextCompat
import com.google.android.gms.location.ActivityRecognition
import com.google.android.gms.location.ActivityRecognitionClient
Expand All @@ -18,6 +18,8 @@ import com.google.android.gms.location.ActivityTransitionRequest
import com.google.android.gms.location.ActivityTransitionResult
import com.google.android.gms.location.DetectedActivity
import com.motionapps.sensorbox.core.error.AppError
import com.motionapps.sensorbox.core.error.AppErrorCode
import com.motionapps.sensorbox.core.error.AppResult
import com.motionapps.sensorbox.core.error.appResult
import com.motionapps.sensorbox.core.error.combineAppResults
import com.motionapps.sensorbox.core.error.flatMap
Expand All @@ -26,7 +28,7 @@ import com.motionapps.sensorservices.handlers.StorageHandler
import java.io.OutputStream

/** Records periodic Google Activity Recognition confidence values. */
class ActivityRecognitionMeasurement(private val periodSeconds: Int) : MeasurementInterface {
class ActivityRecognitionMeasurement(private val periodSeconds: Int) {
private var client: ActivityRecognitionClient? = null
private var updatesPendingIntent: PendingIntent? = null
private var transitionsPendingIntent: PendingIntent? = null
Expand Down Expand Up @@ -64,44 +66,38 @@ class ActivityRecognitionMeasurement(private val periodSeconds: Int) : Measureme
}
}

override fun initMeasurement(context: Context, params: Bundle): Result<Unit> {
val folder = params.getString(MeasurementInterface.FOLDER_NAME).orEmpty()
val internal = params.getBoolean(MeasurementInterface.INTERNAL_STORAGE)
val updatesResult = if (internal) {
StorageHandler.createFileInInternalFolder(context, folder, UPDATES_FILE_NAME)
fun prepare(context: Context, folderName: String, useInternalStorage: Boolean): AppResult<Unit> {
val updatesResult = if (useInternalStorage) {
StorageHandler.createFileInInternalFolder(context, folderName, UPDATES_FILE_NAME)
} else {
StorageHandler.createFileInFolder(context, folder, "text/csv", UPDATES_FILE_NAME)
StorageHandler.createFileInFolder(context, folderName, "text/csv", UPDATES_FILE_NAME)
}
return updatesResult.flatMap { updates ->
updatesOutput = updates
val transitionsResult = if (internal) {
StorageHandler.createFileInInternalFolder(context, folder, TRANSITIONS_FILE_NAME)
val transitionsResult = if (useInternalStorage) {
StorageHandler.createFileInInternalFolder(context, folderName, TRANSITIONS_FILE_NAME)
} else {
StorageHandler.createFileInFolder(context, folder, "text/csv", TRANSITIONS_FILE_NAME)
StorageHandler.createFileInFolder(context, folderName, "text/csv", TRANSITIONS_FILE_NAME)
}
transitionsResult.onFailure {
appResult(AppError.Kind.STORAGE, "Close incomplete activity measurement") { updates.close() }
appResult(AppErrorCode.STORAGE, "Close incomplete activity measurement") { updates.close() }
}.flatMap { transitions ->
transitionsOutput = transitions
initializeResources(context)
}
}.withAppError(AppError.Kind.MEASUREMENT, "Initialize activity recognition")
}.withAppError(AppErrorCode.MEASUREMENT, "Initialize activity recognition")
}

private fun initializeResources(context: Context): Result<Unit> = appResult(
AppError.Kind.MEASUREMENT,
private fun initializeResources(context: Context): AppResult<Unit> = appResult(
AppErrorCode.MEASUREMENT,
"Initialize activity recognition resources",
) {
updatesOutput?.write(
"t_elapsed;still;on_foot;walking;running;vehicle;bike;unknown;tilting\n".toByteArray(),
)
transitionsOutput?.write("t_nanos;activity;enter_exit\n".toByteArray())
val filter = IntentFilter(ACTION_UPDATE).apply { addAction(ACTION_TRANSITION) }
if (Build.VERSION.SDK_INT >= 33) {
context.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED)
} else {
context.registerReceiver(receiver, filter)
}
ContextCompat.registerReceiver(context, receiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED)
receiverRegistered = true
client = ActivityRecognition.getClient(context)
updatesPendingIntent = PendingIntent.getBroadcast(
Expand All @@ -118,43 +114,42 @@ class ActivityRecognitionMeasurement(private val periodSeconds: Int) : Measureme
)
}

override fun startMeasurement(context: Context): Result<Unit> {
if (Build.VERSION.SDK_INT >= 29 &&
ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACTIVITY_RECOGNITION,
) != PackageManager.PERMISSION_GRANTED
) {
return Result.failure(AppError(AppError.Kind.PERMISSION, "Start activity recognition"))
@SuppressLint("MissingPermission")
fun start(context: Context): AppResult<Unit> {
if (!hasPermission(context)) {
return AppResult.failure(AppError(AppErrorCode.PERMISSION, "Start activity recognition"))
}
return appResult(AppError.Kind.MEASUREMENT, "Start activity recognition") {
return appResult(AppErrorCode.MEASUREMENT, "Start activity recognition") {
updatesPendingIntent?.let {
client?.requestActivityUpdates(periodSeconds.coerceAtLeast(1) * 1_000L, it)
?.addOnFailureListener { error ->
AppError.from(AppError.Kind.MEASUREMENT, "Request activity updates", error)
AppError.from(AppErrorCode.MEASUREMENT, "Request activity updates", error)
}
}
transitionsPendingIntent?.let {
client?.requestActivityTransitionUpdates(ActivityTransitionRequest(ACTIVITY_TRANSITIONS), it)
?.addOnFailureListener { error ->
AppError.from(AppError.Kind.MEASUREMENT, "Request activity transitions", error)
AppError.from(AppErrorCode.MEASUREMENT, "Request activity transitions", error)
}
}
}
}

override fun pauseMeasurement(context: Context): Result<Unit> = appResult(
AppError.Kind.MEASUREMENT,
@SuppressLint("MissingPermission")
private fun pause(context: Context): AppResult<Unit> = appResult(
AppErrorCode.MEASUREMENT,
"Pause activity recognition",
) {
updatesPendingIntent?.let {
client?.removeActivityUpdates(it)?.addOnFailureListener { error ->
AppError.from(AppError.Kind.MEASUREMENT, "Remove activity updates", error)
if (hasPermission(context)) {
updatesPendingIntent?.let {
client?.removeActivityUpdates(it)?.addOnFailureListener { error ->
AppError.from(AppErrorCode.MEASUREMENT, "Remove activity updates", error)
}
}
}
transitionsPendingIntent?.let {
client?.removeActivityTransitionUpdates(it)?.addOnFailureListener { error ->
AppError.from(AppError.Kind.MEASUREMENT, "Remove activity transitions", error)
transitionsPendingIntent?.let {
client?.removeActivityTransitionUpdates(it)?.addOnFailureListener { error ->
AppError.from(AppErrorCode.MEASUREMENT, "Remove activity transitions", error)
}
}
}
if (receiverRegistered) {
Expand All @@ -163,34 +158,40 @@ class ActivityRecognitionMeasurement(private val periodSeconds: Int) : Measureme
receiverRegistered = false
}

override suspend fun saveMeasurement(context: Context): Result<Unit> {
val results = mutableListOf<Result<*>>()
results += appResult(AppError.Kind.STORAGE, "Close activity updates") {
private suspend fun save(): AppResult<Unit> {
val results = mutableListOf<AppResult<*>>()
results += appResult(AppErrorCode.STORAGE, "Close activity updates") {
updatesOutput?.close()
}
results += appResult(AppError.Kind.STORAGE, "Close activity transitions") {
results += appResult(AppErrorCode.STORAGE, "Close activity transitions") {
transitionsOutput?.close()
}
writeFailure?.let { results += Result.failure<Unit>(it) }
writeFailure?.let { results += AppResult.failure(it) }
updatesOutput = null
transitionsOutput = null
writeFailure = null
return results.combineAppResults(AppError.Kind.MEASUREMENT, "Save activity recognition")
return results.combineAppResults(AppErrorCode.MEASUREMENT, "Save activity recognition")
}

override suspend fun onDestroyMeasurement(context: Context): Result<Unit> {
val results = listOf(pauseMeasurement(context), saveMeasurement(context))
suspend fun stop(context: Context): AppResult<Unit> {
val results = listOf(pause(context), save())
updatesPendingIntent = null
transitionsPendingIntent = null
client = null
return results.combineAppResults(AppError.Kind.MEASUREMENT, "Stop activity recognition")
return results.combineAppResults(AppErrorCode.MEASUREMENT, "Stop activity recognition")
}

private inline fun recordWriteFailure(operation: String, block: () -> Unit) {
if (writeFailure != null) return
appResult(AppError.Kind.STORAGE, operation, block).onFailure { writeFailure = it as AppError }
appResult(AppErrorCode.STORAGE, operation, block).onFailure { writeFailure = it }
}

private fun hasPermission(context: Context): Boolean = Build.VERSION.SDK_INT < 29 ||
ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACTIVITY_RECOGNITION,
) == PackageManager.PERMISSION_GRANTED

private companion object {
const val ACTION_UPDATE = "com.motionapps.sensorbox.ACTIVITY_RECOGNITION_UPDATE"
const val ACTION_TRANSITION = "com.motionapps.sensorbox.ACTIVITY_RECOGNITION_TRANSITION"
Expand Down
Loading