From eacaeb11cd5f432c54a1900569373f0ec1607764 Mon Sep 17 00:00:00 2001 From: sangjun park Date: Thu, 31 Oct 2024 17:52:58 +0900 Subject: [PATCH] ADDED --- build.gradle.kts | 1 - field-smartphone/build.gradle.kts | 2 + .../kaist/iclab/field_tracker/KoinModule.kt | 59 +++-- .../kaist/iclab/field_tracker/MainActivity.kt | 8 + .../iclab/field_tracker/MainApplication.kt | 20 +- .../field_tracker/database/CouchbaseDBImpl.kt | 107 +++++++++ .../database/DatabaseInterface.kt | 24 ++ .../field_tracker}/database/FakeDBImpl.kt | 5 +- .../field_tracker/database/TempDBImpl.kt | 47 ++++ .../field_tracker/ui/AbstractMainViewModel.kt | 9 +- .../iclab/field_tracker/ui/MainScreen.kt | 208 ++++++++++++------ .../field_tracker/ui/MainViewModelFakeImpl.kt | 21 +- .../field_tracker/ui/MainViewModelImpl.kt | 47 ++-- .../iclab/field_tracker/ui/theme/Theme.kt | 31 ++- tracker-library/build.gradle.kts | 3 - tracker-library/src/main/AndroidManifest.xml | 9 +- .../main/java/kaist/iclab/tracker/Tracker.kt | 30 +-- .../collectors/ActivityTransitionCollector.kt | 2 +- .../tracker/collectors/BTScanCollector.kt | 144 ++++++------ .../collectors/OLD/OldAbstractCollector.kt | 84 +++---- .../tracker/collectors/WiFiScanCollector.kt | 5 - .../controller/CollectorController.OLD | 98 --------- .../tracker/controller/CollectorController.kt | 89 ++++++-- .../CollectorControllerInterface.kt | 4 + .../tracker/controller/CollectorService.kt | 75 ------- .../iclab/tracker/database/CouchbaseDBImpl.kt | 107 --------- .../tracker/database/DatabaseInterface.kt | 20 -- 27 files changed, 640 insertions(+), 619 deletions(-) create mode 100644 field-smartphone/src/main/java/kaist/iclab/field_tracker/database/CouchbaseDBImpl.kt create mode 100644 field-smartphone/src/main/java/kaist/iclab/field_tracker/database/DatabaseInterface.kt rename {tracker-library/src/main/java/kaist/iclab/tracker => field-smartphone/src/main/java/kaist/iclab/field_tracker}/database/FakeDBImpl.kt (71%) create mode 100644 field-smartphone/src/main/java/kaist/iclab/field_tracker/database/TempDBImpl.kt delete mode 100644 tracker-library/src/main/java/kaist/iclab/tracker/controller/CollectorController.OLD delete mode 100644 tracker-library/src/main/java/kaist/iclab/tracker/controller/CollectorService.kt delete mode 100644 tracker-library/src/main/java/kaist/iclab/tracker/database/CouchbaseDBImpl.kt delete mode 100644 tracker-library/src/main/java/kaist/iclab/tracker/database/DatabaseInterface.kt diff --git a/build.gradle.kts b/build.gradle.kts index 054151b..9e4ae6c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,5 +3,4 @@ plugins { alias(libs.plugins.androidApplication) apply false alias(libs.plugins.jetbrainsKotlinAndroid) apply false alias(libs.plugins.androidLibrary) apply false -// alias(libs.plugins.realm) apply false } \ No newline at end of file diff --git a/field-smartphone/build.gradle.kts b/field-smartphone/build.gradle.kts index fbc4045..ee001cd 100644 --- a/field-smartphone/build.gradle.kts +++ b/field-smartphone/build.gradle.kts @@ -77,5 +77,7 @@ dependencies { implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material.icons.extended) + implementation(libs.couchbase) + implementation(project(":tracker-library")) } \ No newline at end of file diff --git a/field-smartphone/src/main/java/kaist/iclab/field_tracker/KoinModule.kt b/field-smartphone/src/main/java/kaist/iclab/field_tracker/KoinModule.kt index 8f7be69..8dd963f 100644 --- a/field-smartphone/src/main/java/kaist/iclab/field_tracker/KoinModule.kt +++ b/field-smartphone/src/main/java/kaist/iclab/field_tracker/KoinModule.kt @@ -1,36 +1,63 @@ package kaist.iclab.field_tracker -import kaist.iclab.tracker.collectors.BatteryCollector -import kaist.iclab.tracker.collectors.LocationCollector -import kaist.iclab.tracker.collectors.TestCollector -import kaist.iclab.tracker.controller.CollectorControllerInterface -import kaist.iclab.tracker.database.DatabaseInterface -import kaist.iclab.tracker.permission.PermissionManagerInterface import kaist.iclab.field_tracker.ui.AbstractMainViewModel import kaist.iclab.field_tracker.ui.MainViewModelImpl import kaist.iclab.tracker.Tracker -import org.koin.core.module.dsl.singleOf +import kaist.iclab.tracker.collectors.* +import kaist.iclab.tracker.controller.CollectorControllerInterface +import kaist.iclab.tracker.database.DatabaseInterface +import kaist.iclab.tracker.database.TempDBImpl +import kaist.iclab.tracker.permission.PermissionManagerInterface +import org.koin.android.ext.koin.androidContext import org.koin.core.module.dsl.viewModel -import org.koin.core.module.dsl.viewModelOf +import org.koin.core.module.dsl.singleOf import org.koin.dsl.module val appModule = module { - single { - Tracker.getDatabase() - } single { Tracker.getCollectorController() } single { Tracker.getPermissionManager() } -// singleOf(::TestCollector) -// singleOf(::BatteryCollector) + single { + TempDBImpl(androidContext()) + } + + singleOf(::ActivityTransitionCollector) + singleOf(::AmbientLightCollector) + singleOf(::AppUsageLogCollector) + singleOf(::BatteryCollector) + singleOf(::CallLogCollector) + singleOf(::DataTrafficStatCollector) singleOf(::LocationCollector) + singleOf(::MessageLogCollector) + singleOf(::NotificationCollector) + singleOf(::ScreenCollector) + singleOf(::UserInteractionCollector) + singleOf(::WiFiScanCollector) - viewModelOf(::MainViewModelImpl) - viewModel{ - get() + viewModel { + val collectors = mapOf( + "ActivityTransitionCollector" to get(), + "AmbientLightCollector" to get(), + "AppUsageLogCollector" to get(), + "BatteryCollector" to get(), + "CallLogCollector" to get(), + "DataTrafficStatCollector" to get(), + "LocationCollector" to get(), + "MessageLogCollector" to get(), + "NotificationCollector" to get(), + "ScreenCollector" to get(), + "UserInteractionCollector" to get(), + "WiFiScanCollector" to get() + ) + MainViewModelImpl( + get(), + get(), + get(), + collectors + ) } } diff --git a/field-smartphone/src/main/java/kaist/iclab/field_tracker/MainActivity.kt b/field-smartphone/src/main/java/kaist/iclab/field_tracker/MainActivity.kt index 99ec659..67e0a05 100644 --- a/field-smartphone/src/main/java/kaist/iclab/field_tracker/MainActivity.kt +++ b/field-smartphone/src/main/java/kaist/iclab/field_tracker/MainActivity.kt @@ -1,5 +1,7 @@ package kaist.iclab.field_tracker +import android.Manifest +import android.os.Build import android.os.Bundle import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize @@ -9,6 +11,7 @@ import androidx.compose.ui.Modifier import kaist.iclab.tracker.permission.PermissionActivity import kaist.iclab.field_tracker.ui.MainScreen import kaist.iclab.field_tracker.ui.theme.TrackerTheme +import kaist.iclab.tracker.Tracker import org.koin.androidx.compose.KoinAndroidContext class MainActivity : PermissionActivity() { @@ -27,5 +30,10 @@ class MainActivity : PermissionActivity() { } } } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Tracker.getPermissionManager().request( + arrayOf(Manifest.permission.POST_NOTIFICATIONS) + ) + } } } \ No newline at end of file diff --git a/field-smartphone/src/main/java/kaist/iclab/field_tracker/MainApplication.kt b/field-smartphone/src/main/java/kaist/iclab/field_tracker/MainApplication.kt index 3dd4bde..c6f604e 100644 --- a/field-smartphone/src/main/java/kaist/iclab/field_tracker/MainApplication.kt +++ b/field-smartphone/src/main/java/kaist/iclab/field_tracker/MainApplication.kt @@ -2,11 +2,9 @@ package kaist.iclab.field_tracker import android.app.Application import android.util.Log +import kaist.iclab.tracker.Tracker import kaist.iclab.tracker.collectors.BatteryCollector -import kaist.iclab.tracker.collectors.LocationCollector -import kaist.iclab.tracker.collectors.TestCollector import kaist.iclab.tracker.controller.CollectorControllerInterface -import kaist.iclab.tracker.Tracker import org.koin.android.ext.android.get import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger @@ -27,14 +25,18 @@ class MainApplication: Application(){ fun setupCollector() { val collectorController = get() + get().listener = { data -> + Log.d("BatteryCollector", "Battery data: $data") + // Do something with battery data + } // collectorController.addCollector(get()) // collectorController.addCollector(get()) - val locationCollector = get() - locationCollector.listener = { data -> - Log.d("LocationCollector", "Location data: $data") - } - collectorController.addCollector(locationCollector) - collectorController.enable(locationCollector.NAME, get()) +// val locationCollector = get() +// locationCollector.listener = { data -> +// Log.d("LocationCollector", "Location data: $data") +// } +// collectorController.addCollector(locationCollector) +// collectorController.enable(locationCollector.NAME, get()) // val filter: Filter = { data -> // data + ("custom" to "data") // } diff --git a/field-smartphone/src/main/java/kaist/iclab/field_tracker/database/CouchbaseDBImpl.kt b/field-smartphone/src/main/java/kaist/iclab/field_tracker/database/CouchbaseDBImpl.kt new file mode 100644 index 0000000..5d08958 --- /dev/null +++ b/field-smartphone/src/main/java/kaist/iclab/field_tracker/database/CouchbaseDBImpl.kt @@ -0,0 +1,107 @@ +//package kaist.iclab.tracker.database +// +//import android.content.Context +//import android.util.Log +//import com.couchbase.lite.CouchbaseLite +//import com.couchbase.lite.DataSource +//import com.couchbase.lite.Database +//import com.couchbase.lite.Expression +//import com.couchbase.lite.MutableDocument +//import com.couchbase.lite.Ordering +//import com.couchbase.lite.Query +//import com.couchbase.lite.QueryBuilder +//import com.couchbase.lite.SelectResult +//import com.couchbase.lite.collectionChangeFlow +//import kotlinx.coroutines.flow.Flow +//import kotlinx.coroutines.flow.map +//import kotlinx.coroutines.flow.onStart +// +//class CouchbaseDBImpl( +// context: Context, +//) : DatabaseInterface { +// +// companion object { +// const val TAG = "CouchbaseDBImpl" +// const val LOG_COLLECTION = "LOG" +// const val DB = "tracker" +// } +// +// private val database: Database by lazy { +// Database(DB) +// } +// +// init { +// // Initialize Couchbase Lite +// CouchbaseLite.init(context) +// } +// +// override fun insert(collectionName: String, data: Map): String { +// val document = MutableDocument(data) +// val collection = getCollection(collectionName) +// collection.save(document) +// return document.id +// } +// +// override fun update(collectionName: String, data: Map) { +// val collection = getCollection(collectionName) +// val id = collection.indexes.firstOrNull() +// val document = MutableDocument(id, data) +// collection.save(document) +// Log.d(TAG, "$collectionName Updated: ${document.toMap()}") +// } +// +// override fun sync() { +// TODO("Not yet implemented") +// } +// +// override fun deleteAll() { +// database.delete() +// } +// +// override fun getAllDocs(collectionName: String): List> { +// val collection = getCollection(collectionName) +// val query: Query = QueryBuilder.select(SelectResult.all()) +// .from(DataSource.collection(collection)) +// return query.execute().allResults().map { it.toMap() } +// } +// +// override fun getDocsFlow(collectionName: String): Flow>> { +// val collection = getCollection(collectionName) +// return collection.collectionChangeFlow().map { +// getAllDocs(collectionName) +// }.onStart { +// emit(getAllDocs(collectionName)) +// } +// } +// +// override fun getLastDoc(collectionName: String): Map { +// val collection = getCollection(collectionName) +// val query: Query = QueryBuilder.select(SelectResult.all()) +// .from(DataSource.collection(collection)) +// .orderBy(Ordering.property("timestamp").descending()) +// .limit(Expression.intValue(1)) +// return query.execute().firstOrNull()?.getDictionary(0)?.toMap() ?: mapOf() +// } +// +// override fun getLastDocFlow(collectionName: String): Flow> { +// val collection = getCollection(collectionName) +// return collection.collectionChangeFlow().map { +// getLastDoc(collectionName) +// }.onStart { +// emit(getLastDoc(collectionName)) +// } +// } +// +// override fun log(message: String) { +// insert( +// LOG_COLLECTION, +// mapOf("timestamp" to System.currentTimeMillis(), "message" to message) +// ) +// } +// +// private fun getCollection(collectionName: String): com.couchbase.lite.Collection { +// val collection = database.getCollection(collectionName) +// ?: database.createCollection(collectionName) +// return collection +// } +//} \ No newline at end of file diff --git a/field-smartphone/src/main/java/kaist/iclab/field_tracker/database/DatabaseInterface.kt b/field-smartphone/src/main/java/kaist/iclab/field_tracker/database/DatabaseInterface.kt new file mode 100644 index 0000000..4fe13d9 --- /dev/null +++ b/field-smartphone/src/main/java/kaist/iclab/field_tracker/database/DatabaseInterface.kt @@ -0,0 +1,24 @@ +package kaist.iclab.tracker.database + +import kotlinx.coroutines.flow.Flow + +interface DatabaseInterface { +// fun insert(collectionName: String, data: Map): String +// fun update(collectionName: String, data: Map) +// +// fun sync() +// fun deleteAll() +// +// fun getDocsFlow(collectionName: String): Flow>> +// fun getAllDocs(collectionName: String): List> +// +// fun getLastDocFlow(collectionName: String): Flow> +// fun getLastDoc(collectionName: String): Map +// +// /* Function to Log some messages*/ +// fun log(message: String) + + /* Enable/Disable Collector */ + fun updateConfig(name: String, value: Boolean) + fun getConfigFlow(): Flow> +} \ No newline at end of file diff --git a/tracker-library/src/main/java/kaist/iclab/tracker/database/FakeDBImpl.kt b/field-smartphone/src/main/java/kaist/iclab/field_tracker/database/FakeDBImpl.kt similarity index 71% rename from tracker-library/src/main/java/kaist/iclab/tracker/database/FakeDBImpl.kt rename to field-smartphone/src/main/java/kaist/iclab/field_tracker/database/FakeDBImpl.kt index a2170fc..384be7e 100644 --- a/tracker-library/src/main/java/kaist/iclab/tracker/database/FakeDBImpl.kt +++ b/field-smartphone/src/main/java/kaist/iclab/field_tracker/database/FakeDBImpl.kt @@ -1,9 +1,6 @@ -//package dev.iclab.tracker.database +//package kaist.iclab.tracker.database // //import android.util.Log -//import dev.iclab.tracker.Util -//import dev.iclab.tracker.collectors.AbstractCollector -//import dev.iclab.tracker.collectors.TestCollector // //class FakeDBImpl : DatabaseInterface { // companion object { diff --git a/field-smartphone/src/main/java/kaist/iclab/field_tracker/database/TempDBImpl.kt b/field-smartphone/src/main/java/kaist/iclab/field_tracker/database/TempDBImpl.kt new file mode 100644 index 0000000..7189239 --- /dev/null +++ b/field-smartphone/src/main/java/kaist/iclab/field_tracker/database/TempDBImpl.kt @@ -0,0 +1,47 @@ +package kaist.iclab.tracker.database + +import android.content.Context +import com.couchbase.lite.CouchbaseLite +import com.couchbase.lite.Database +import com.couchbase.lite.MutableDocument +import com.couchbase.lite.collectionChangeFlow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart + +class TempDBImpl(context: Context): DatabaseInterface { + companion object { + const val DB = "CONFIG" + } + private val database: Database by lazy { + Database(DB) + } + + init { + CouchbaseLite.init(context) + } + + override fun updateConfig(name: String, value: Boolean) { + val collection = getCollection() + val document = collection.getDocument(DB)?.toMutable() ?: MutableDocument(DB) + document.setBoolean(name, value) + collection.save(document) + } + + override fun getConfigFlow(): Flow> { + val collection = getCollection() + return collection.collectionChangeFlow().map { + val document = collection.getDocument(DB) + document?.toMap()?.filterValues { it is Boolean }?.mapValues { it.value as Boolean } ?: emptyMap() + }.onStart { + val document = collection.getDocument(DB) + emit(document?.toMap()?.filterValues { it is Boolean }?.mapValues { it.value as Boolean } ?: emptyMap()) + } + } + + private fun getCollection(): com.couchbase.lite.Collection { + val collection = database.getCollection(DB) + ?: database.createCollection(DB) + return collection + } +} \ No newline at end of file diff --git a/field-smartphone/src/main/java/kaist/iclab/field_tracker/ui/AbstractMainViewModel.kt b/field-smartphone/src/main/java/kaist/iclab/field_tracker/ui/AbstractMainViewModel.kt index b3040ab..31e8baf 100644 --- a/field-smartphone/src/main/java/kaist/iclab/field_tracker/ui/AbstractMainViewModel.kt +++ b/field-smartphone/src/main/java/kaist/iclab/field_tracker/ui/AbstractMainViewModel.kt @@ -1,6 +1,7 @@ package kaist.iclab.field_tracker.ui import androidx.lifecycle.ViewModel +import kaist.iclab.tracker.collectors.AbstractCollector import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -10,10 +11,10 @@ abstract class AbstractMainViewModel: ViewModel() { val isRunningState: StateFlow get() = _isRunningState.asStateFlow() - abstract val collectorList: List - abstract val _collectorConfigState: MutableStateFlow> - val collectorConfigState: StateFlow> - get() = _collectorConfigState.asStateFlow() + abstract val collectorMap: Map + abstract val _enabledCollectors: MutableStateFlow> + val enabledCollectors: StateFlow> + get() = _enabledCollectors.asStateFlow() abstract fun start() abstract fun stop() diff --git a/field-smartphone/src/main/java/kaist/iclab/field_tracker/ui/MainScreen.kt b/field-smartphone/src/main/java/kaist/iclab/field_tracker/ui/MainScreen.kt index 3571812..12471cd 100644 --- a/field-smartphone/src/main/java/kaist/iclab/field_tracker/ui/MainScreen.kt +++ b/field-smartphone/src/main/java/kaist/iclab/field_tracker/ui/MainScreen.kt @@ -1,46 +1,66 @@ package kaist.iclab.field_tracker.ui +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.DirectionsRun +import androidx.compose.material.icons.filled.Pause +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.Sync import androidx.compose.material.icons.rounded.Delete +import androidx.compose.material.icons.rounded.ExpandLess import androidx.compose.material.icons.rounded.ExpandMore import androidx.compose.material.icons.rounded.PlayArrow import androidx.compose.material.icons.rounded.Stop import androidx.compose.material.icons.rounded.Upload +import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kaist.iclab.field_tracker.ui.theme.TrackerTheme import org.koin.androidx.compose.koinViewModel @Composable fun MainScreen(viewModel: AbstractMainViewModel = koinViewModel()) { val isRunning = viewModel.isRunningState.collectAsStateWithLifecycle() - val collectorConfig = viewModel.collectorConfigState.collectAsStateWithLifecycle() Column { - Controller(isRunning.value, viewModel) + CollectorControllerUI(isRunning.value, viewModel) LazyColumn( modifier = Modifier.fillMaxWidth() ) { items( - viewModel.collectorList + viewModel.collectorMap.keys.toList() ) { item -> Collector(item, viewModel) } @@ -48,101 +68,143 @@ fun MainScreen(viewModel: AbstractMainViewModel = koinViewModel()) { } } -@Composable -fun Section(name: String){ - Column { - Text(name) - } -} @Composable -fun Collector(name: String, viewModel: AbstractMainViewModel){ +fun Collector(name: String, viewModel: AbstractMainViewModel) { + val enabledCollectors = viewModel.enabledCollectors.collectAsStateWithLifecycle() + val (expanded, setExpand) = remember { mutableStateOf(true) } + Column { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + IconButton(onClick = { setExpand(!expanded) }) { + Icon( - Row(modifier = Modifier - .padding(16.dp) - .fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically) { - IconButton(onClick = {}) { - Icon( - imageVector = Icons.Rounded.ExpandMore, - contentDescription = "expand", - tint = Color.Gray, - modifier = Modifier - .width(32.dp) - .height(32.dp) + imageVector = if (expanded) Icons.Rounded.ExpandLess else Icons.Rounded.ExpandMore, + contentDescription = "expand", + tint = Color.Gray, + modifier = Modifier + .width(32.dp) + .height(32.dp) + ) + } + Icon( + imageVector = Icons.Default.DirectionsRun, + contentDescription = name, + tint = Color.Black, + modifier = Modifier.size(24.dp) + ) + Text(text = name, fontSize = 16.sp) + } + Switch( + checked = enabledCollectors.value[name] ?: false, + onCheckedChange = { + if (it) { + viewModel.enable(name) + } else { + viewModel.disable(name) + } + }, + colors = SwitchDefaults.colors( + checkedThumbColor = Color(0xFF0B57D0), // Blue thumb when checked + checkedTrackColor = Color(0xFFD3E3FD), // Blue track when checked + uncheckedThumbColor = Color.White, // White thumb when unchecked + uncheckedTrackColor = Color(0xFFC0C0C0), // Light blue track when unchecked + uncheckedBorderColor = Color.Transparent, // No border for unchecked state + checkedBorderColor = Color.Transparent // No border for checked state + ), + thumbContent = { + Box( + modifier = Modifier + .size(16.dp) // Increase thumb size + .background(Color.Transparent, CircleShape) // Ensure thumb is circular + ) + } ) } - Text(name) - Switch( - enabled = true, - checked = true, -// enabled = collectorConfig.value[name] != null, -// checked = collectorConfig.value[name]?: false, - onCheckedChange = { - if(it){ - viewModel.enable(name) - }else{ - viewModel.disable(name) - } + if (expanded) { + Row( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + ) { + Text(text = "Hello") } - ) + + } } + + } @Composable -fun Controller(isRunning: Boolean, viewModel: AbstractMainViewModel){ +fun CollectorControllerUI(isRunning: Boolean, viewModel: AbstractMainViewModel) { Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp) ) { - IconButton(onClick = { viewModel.sync() }) { + // Sync Button (left of Play/Pause) + IconButton( + onClick = {}, + modifier = Modifier.size(40.dp) + ) { Icon( - imageVector = Icons.Rounded.Upload, - contentDescription = "upload", + imageVector = Icons.Default.Sync, + contentDescription = "Sync", tint = Color.Gray, - modifier = Modifier - .width(32.dp) - .height(32.dp) + modifier = Modifier.size(24.dp) ) } - if(!isRunning){ - IconButton(onClick = { viewModel.start() }) { - Icon( - imageVector = Icons.Rounded.PlayArrow, - contentDescription = "Starting Button", - tint = Color.Red, - modifier = Modifier - .width(48.dp) - .height(48.dp) - ) - } - }else{ - IconButton(onClick = { viewModel.stop() }) { - Icon( - imageVector = Icons.Rounded.Stop, - contentDescription = "Stopping Button", - tint = Color.Red, - modifier = Modifier - .width(48.dp) - .height(48.dp) - ) - } + + // Play/Pause Button (centered as main action) + IconButton( + onClick = { + if (isRunning) { + viewModel.stop() + } else { + viewModel.start() + } + }, + modifier = Modifier + .size(40.dp) + .background(Color(0xFF0B57D0), CircleShape) + ) { + Icon( + imageVector = if (isRunning) Icons.Default.Pause else Icons.Default.PlayArrow, + contentDescription = if (isRunning) "Pause" else "Start", + tint = Color.White, + modifier = Modifier.size(28.dp) + ) } - IconButton(onClick = { viewModel.delete() }) { + + // Flush Button + IconButton( + onClick = {}, + modifier = Modifier.size(40.dp) + ) { Icon( - imageVector = Icons.Rounded.Delete, - contentDescription = "delete data", + imageVector = Icons.Default.Delete, + contentDescription = "Flush", tint = Color.Gray, - modifier = Modifier - .width(32.dp) - .height(32.dp) + modifier = Modifier.size(24.dp) ) } } } + @Preview(showBackground = true) @Composable fun Preview() { diff --git a/field-smartphone/src/main/java/kaist/iclab/field_tracker/ui/MainViewModelFakeImpl.kt b/field-smartphone/src/main/java/kaist/iclab/field_tracker/ui/MainViewModelFakeImpl.kt index cb7b2c3..4de74de 100644 --- a/field-smartphone/src/main/java/kaist/iclab/field_tracker/ui/MainViewModelFakeImpl.kt +++ b/field-smartphone/src/main/java/kaist/iclab/field_tracker/ui/MainViewModelFakeImpl.kt @@ -1,22 +1,26 @@ package kaist.iclab.field_tracker.ui import android.util.Log +import kaist.iclab.tracker.collectors.AbstractCollector +import kaist.iclab.tracker.collectors.BatteryCollector import kotlinx.coroutines.flow.MutableStateFlow -class MainViewModelFakeImpl() : AbstractMainViewModel() { +class MainViewModelFakeImpl( + +) : AbstractMainViewModel() { companion object{ const val TAG = "MainViewModelFakeImpl" } - override val collectorList = listOf("Battery", "Location", "Test") + override val collectorMap: Map = mapOf() + override val _enabledCollectors: MutableStateFlow> + = MutableStateFlow( + mapOf("Battery" to false) + ) override val _isRunningState = MutableStateFlow(false) - override val _collectorConfigState = MutableStateFlow( - collectorList.associateWith { false }.toMap() - ) - override fun start() { _isRunningState.value = true } @@ -26,13 +30,14 @@ class MainViewModelFakeImpl() : AbstractMainViewModel() { } override fun enable(name: String) { - _collectorConfigState.value = _collectorConfigState.value.toMutableMap().apply { + _enabledCollectors.value = _enabledCollectors.value.toMutableMap().apply { this[name] = true } + } override fun disable(name: String) { - _collectorConfigState.value = _collectorConfigState.value.toMutableMap().apply { + _enabledCollectors.value = _enabledCollectors.value.toMutableMap().apply { this[name] = false } } diff --git a/field-smartphone/src/main/java/kaist/iclab/field_tracker/ui/MainViewModelImpl.kt b/field-smartphone/src/main/java/kaist/iclab/field_tracker/ui/MainViewModelImpl.kt index c714de2..575c7c8 100644 --- a/field-smartphone/src/main/java/kaist/iclab/field_tracker/ui/MainViewModelImpl.kt +++ b/field-smartphone/src/main/java/kaist/iclab/field_tracker/ui/MainViewModelImpl.kt @@ -2,6 +2,7 @@ package kaist.iclab.field_tracker.ui import android.util.Log import androidx.lifecycle.viewModelScope +import kaist.iclab.tracker.collectors.AbstractCollector import kaist.iclab.tracker.controller.CollectorControllerInterface import kaist.iclab.tracker.database.DatabaseInterface import kaist.iclab.tracker.permission.PermissionManagerInterface @@ -11,38 +12,38 @@ import kotlinx.coroutines.launch class MainViewModelImpl( private val collectorController: CollectorControllerInterface, + private val permissionManager: PermissionManagerInterface, private val database: DatabaseInterface, - private val permissionManager: PermissionManagerInterface -): AbstractMainViewModel() { + override val collectorMap: Map +) : AbstractMainViewModel() { - companion object{ + companion object { const val TAG = "MainViewModelImpl" } - override val _isRunningState = MutableStateFlow(false) - override val _collectorConfigState: MutableStateFlow> = MutableStateFlow(mapOf()) + override val _isRunningState: MutableStateFlow = MutableStateFlow(false) + override val _enabledCollectors: MutableStateFlow> = MutableStateFlow( + collectorMap.map { it.key to false }.toMap() + ) + init { Log.d(TAG, "isRunning: ${_isRunningState.value}") - Log.d(TAG, "collectorConfig: ${_collectorConfigState.value}") viewModelScope.launch { - Log.d(TAG,"INITIALIZED") + Log.d(TAG, "INITIALIZED") collectorController.isRunningFlow().collect { Log.d(TAG, "isRunningState: $it") _isRunningState.value = it } } viewModelScope.launch { - Log.d(TAG,"INITIALIZED COLLECTOR CONFIG") - collectorController.getCollectorConfigChange().collect { - Log.d(TAG, "CollectorConfig: $it") - _collectorConfigState.value = it + database.getConfigFlow().collect { + Log.d(TAG, "enabledCollectors: $it") + _enabledCollectors.value = it } } } - override val collectorList = collectorController.getCollectorsList() - override fun start() { collectorController.start() } @@ -52,18 +53,30 @@ class MainViewModelImpl( } override fun enable(name: String) { - collectorController.enable(name, permissionManager) + collectorMap[name]?.let { collector-> + collectorController.add(collector) + collector.enable(permissionManager){ + if(it){ + _enabledCollectors.value = _enabledCollectors.value.toMutableMap().apply { + this[name] = true + } + } + } + + } } override fun disable(name: String) { - collectorController.disable(name) + collectorMap[name]?.let { collector-> + collectorController.remove(collector) + } } override fun sync() { - database.sync() + throw NotImplementedError("Not implemented") } override fun delete() { - database.deleteAll() + throw NotImplementedError("Not implemented") } } \ No newline at end of file diff --git a/field-smartphone/src/main/java/kaist/iclab/field_tracker/ui/theme/Theme.kt b/field-smartphone/src/main/java/kaist/iclab/field_tracker/ui/theme/Theme.kt index 1f83ca7..90535fd 100644 --- a/field-smartphone/src/main/java/kaist/iclab/field_tracker/ui/theme/Theme.kt +++ b/field-smartphone/src/main/java/kaist/iclab/field_tracker/ui/theme/Theme.kt @@ -10,31 +10,28 @@ import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.core.view.WindowCompat -private val DarkColorScheme = darkColorScheme( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80 -) - private val LightColorScheme = lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40 + primary = Color(0xFF2196F3), // Blue primary color for the switch + onPrimary = Color.White, // White on top of primary + secondary = Color(0xFFB0BEC5), // Light gray for secondary/inactive states + background = Color(0xFFF5F5F5), // Light background + surface = Color.White, // Surface color for light theme + error = Color(0xFFD32F2F) // Material recommended error color +) - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), +private val DarkColorScheme = darkColorScheme( + primary = Color(0xFF2962FF), // Blue primary color for dark theme onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ + secondary = Color(0xFFB0BEC5), + background = Color(0xFF121212), // Dark background + surface = Color(0xFF121212), + error = Color(0xFFD32F2F) ) @Composable diff --git a/tracker-library/build.gradle.kts b/tracker-library/build.gradle.kts index 0c35f9a..24b4ec3 100644 --- a/tracker-library/build.gradle.kts +++ b/tracker-library/build.gradle.kts @@ -1,7 +1,6 @@ plugins { alias(libs.plugins.androidLibrary) alias(libs.plugins.jetbrainsKotlinAndroid) - alias(libs.plugins.realm) id("maven-publish") } @@ -42,8 +41,6 @@ dependencies { androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) - implementation(libs.couchbase) - implementation(libs.play.services.location) implementation(libs.android.gms.fitness) diff --git a/tracker-library/src/main/AndroidManifest.xml b/tracker-library/src/main/AndroidManifest.xml index 24faebc..cc88e31 100644 --- a/tracker-library/src/main/AndroidManifest.xml +++ b/tracker-library/src/main/AndroidManifest.xml @@ -16,14 +16,14 @@ @@ -32,7 +32,8 @@ + android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" + android:exported="false"> diff --git a/tracker-library/src/main/java/kaist/iclab/tracker/Tracker.kt b/tracker-library/src/main/java/kaist/iclab/tracker/Tracker.kt index e2508d4..45d93e5 100644 --- a/tracker-library/src/main/java/kaist/iclab/tracker/Tracker.kt +++ b/tracker-library/src/main/java/kaist/iclab/tracker/Tracker.kt @@ -1,11 +1,8 @@ package kaist.iclab.tracker import android.content.Context -import kaist.iclab.tracker.controller.CollectorController +import kaist.iclab.tracker.controller.CollectorControllerImpl import kaist.iclab.tracker.controller.CollectorControllerInterface -import kaist.iclab.tracker.controller.CollectorService -import kaist.iclab.tracker.database.CouchbaseDBImpl -import kaist.iclab.tracker.database.DatabaseInterface import kaist.iclab.tracker.permission.PermissionManagerImpl import kaist.iclab.tracker.permission.PermissionManagerInterface import java.lang.ref.WeakReference @@ -15,40 +12,25 @@ import java.lang.ref.WeakReference // It use volatile and synchronized to ensure that the database and collector controller are initialized only once. // WeakReference is used to avoid memory leaks when the context is passed to the database and collector controller. object Tracker { - @Volatile - private var database: WeakReference? = null @Volatile private var collectorController: WeakReference? = null - @Volatile private var permissionManager: WeakReference? = null @Synchronized - fun initialize(context: Context, database_: DatabaseInterface, permissionManager_: PermissionManagerInterface) { - if (database == null && collectorController?.get() == null) { - database = WeakReference(database_) + fun initialize(context: Context, permissionManager_: PermissionManagerInterface) { + if (collectorController?.get() == null) { permissionManager = WeakReference(permissionManager_) - collectorController = WeakReference(CollectorController(context.applicationContext)) + collectorController = WeakReference(CollectorControllerImpl(context.applicationContext)) /* Add notification channel to show collector is running as a foreground service... */ - CollectorService.createNotificationChannel(context) + CollectorControllerImpl.NotificationHandler.createNotificationChannel(context) } } @Synchronized fun initialize(context: Context){ - if (database == null && collectorController?.get() == null) { - database = WeakReference(CouchbaseDBImpl(context)) - permissionManager = WeakReference(PermissionManagerImpl(context)) - collectorController = WeakReference(CollectorController(context.applicationContext)) - - /* Add notification channel to show collector is running as a foreground service... */ - CollectorService.createNotificationChannel(context) - } - } - - fun getDatabase(): DatabaseInterface { - return database?.get() ?: throw IllegalStateException("TrackerService not initialized") + initialize(context, PermissionManagerImpl(context)) } fun getCollectorController(): CollectorControllerInterface { diff --git a/tracker-library/src/main/java/kaist/iclab/tracker/collectors/ActivityTransitionCollector.kt b/tracker-library/src/main/java/kaist/iclab/tracker/collectors/ActivityTransitionCollector.kt index 85fd118..0f5be4b 100644 --- a/tracker-library/src/main/java/kaist/iclab/tracker/collectors/ActivityTransitionCollector.kt +++ b/tracker-library/src/main/java/kaist/iclab/tracker/collectors/ActivityTransitionCollector.kt @@ -52,7 +52,7 @@ class ActivityTransitionCollector( PendingIntent.getBroadcast( context, CODE, Intent(ACTION), - PendingIntent.FLAG_UPDATE_CURRENT + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) } diff --git a/tracker-library/src/main/java/kaist/iclab/tracker/collectors/BTScanCollector.kt b/tracker-library/src/main/java/kaist/iclab/tracker/collectors/BTScanCollector.kt index 4338551..9892ef5 100644 --- a/tracker-library/src/main/java/kaist/iclab/tracker/collectors/BTScanCollector.kt +++ b/tracker-library/src/main/java/kaist/iclab/tracker/collectors/BTScanCollector.kt @@ -1,72 +1,72 @@ -package kaist.iclab.tracker.collectors - -import android.Manifest -import android.content.Context -import android.hardware.Sensor -import android.hardware.SensorEvent -import android.hardware.SensorEventListener -import android.hardware.SensorManager -import android.net.wifi.WifiManager -import android.os.Build -import kaist.iclab.tracker.triggers.SystemBroadcastTrigger -import java.util.concurrent.TimeUnit - -class BTScanCollector( - override val context: Context -) : AbstractCollector(context) { - -// No Configuration! - - data class DataEntity( - val timestamp: Long, - val bssid: String, - val frequency: Int, - val level: Int - ) : AbstractCollector.DataEntity() - - override val permissions = listOfNotNull( - Manifest.permission.ACCESS_WIFI_STATE, - Manifest.permission.ACCESS_COARSE_LOCATION, - Manifest.permission.ACCESS_FINE_LOCATION, - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) Manifest.permission.ACCESS_BACKGROUND_LOCATION else null - ).toTypedArray() - - override val foregroundServiceTypes: Array = listOfNotNull().toTypedArray() - - - override fun isAvailable(): Boolean { - return wifiManager.isWifiEnabled - } - - private val wifiManager: WifiManager by lazy { - context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager - } - - private val broadcastTrigger = SystemBroadcastTrigger( - context, - arrayOf( - WifiManager.SCAN_RESULTS_AVAILABLE_ACTION - ) - ) { - val results = wifiManager.scanResults - val timestamp = System.currentTimeMillis() - results.forEach { - listener?.invoke( - DataEntity( - timestamp, - it.BSSID, - it.frequency, - it.level, - ) - ) - } - } - - override fun start() { - broadcastTrigger.register() - } - - override fun stop() { - broadcastTrigger.unregister() - } -} \ No newline at end of file +//package kaist.iclab.tracker.collectors +// +//import android.Manifest +//import android.content.Context +//import android.hardware.Sensor +//import android.hardware.SensorEvent +//import android.hardware.SensorEventListener +//import android.hardware.SensorManager +//import android.net.wifi.WifiManager +//import android.os.Build +//import kaist.iclab.tracker.triggers.SystemBroadcastTrigger +//import java.util.concurrent.TimeUnit +// +//class BTScanCollector( +// override val context: Context +//) : AbstractCollector(context) { +// +//// No Configuration! +// +// data class DataEntity( +// val timestamp: Long, +// val bssid: String, +// val frequency: Int, +// val level: Int +// ) : AbstractCollector.DataEntity() +// +// override val permissions = listOfNotNull( +// Manifest.permission.ACCESS_WIFI_STATE, +// Manifest.permission.ACCESS_COARSE_LOCATION, +// Manifest.permission.ACCESS_FINE_LOCATION, +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) Manifest.permission.ACCESS_BACKGROUND_LOCATION else null +// ).toTypedArray() +// +// override val foregroundServiceTypes: Array = listOfNotNull().toTypedArray() +// +// +// override fun isAvailable(): Boolean { +// return wifiManager.isWifiEnabled +// } +// +// private val wifiManager: WifiManager by lazy { +// context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager +// } +// +// private val broadcastTrigger = SystemBroadcastTrigger( +// context, +// arrayOf( +// WifiManager.SCAN_RESULTS_AVAILABLE_ACTION +// ) +// ) { +// val results = wifiManager.scanResults +// val timestamp = System.currentTimeMillis() +// results.forEach { +// listener?.invoke( +// DataEntity( +// timestamp, +// it.BSSID, +// it.frequency, +// it.level, +// ) +// ) +// } +// } +// +// override fun start() { +// broadcastTrigger.register() +// } +// +// override fun stop() { +// broadcastTrigger.unregister() +// } +//} \ No newline at end of file diff --git a/tracker-library/src/main/java/kaist/iclab/tracker/collectors/OLD/OldAbstractCollector.kt b/tracker-library/src/main/java/kaist/iclab/tracker/collectors/OLD/OldAbstractCollector.kt index aaab75c..36b7505 100644 --- a/tracker-library/src/main/java/kaist/iclab/tracker/collectors/OLD/OldAbstractCollector.kt +++ b/tracker-library/src/main/java/kaist/iclab/tracker/collectors/OLD/OldAbstractCollector.kt @@ -1,42 +1,42 @@ -package kaist.iclab.tracker.collectors.OLD - -import android.content.Context -import android.util.Log -import kaist.iclab.tracker.permission.PermissionManagerInterface -import kaist.iclab.tracker.database.DatabaseInterface -import kaist.iclab.tracker.filters.Filter - -abstract class OldAbstractCollector( - open val context: Context, - open val database: DatabaseInterface -) { - abstract val NAME: String - abstract val permissions: Array - open val filters: MutableList = mutableListOf() - - open val TAG: String = this::class.simpleName ?: "UnnamedClass" - - /* Check whether the system allow to collect data - * In case of sensor malfunction or broken, it would not be available.*/ - abstract fun isAvailable(): Boolean - - /* Enable the collector by checking and requesting permissions - * Different with `isAvailable`, `enable` is used to request permissions when - * the collector is available, but does not have permission - * */ - fun enable(permissionManager: PermissionManagerInterface, onResult: (granted: Boolean)-> Unit) { - permissionManager.request(permissions){ - Log.d(TAG, "Permission granted: ${permissions.all { permission -> it[permission] == true }}") - onResult(permissions.all { permission -> it[permission] == true }) - } - } - - /* Start collector to collect data - * */ - abstract fun start() - - /* Stop collector to stop collecting data - * */ - abstract fun stop() - -} \ No newline at end of file +//package kaist.iclab.tracker.collectors.OLD +// +//import android.content.Context +//import android.util.Log +//import kaist.iclab.tracker.permission.PermissionManagerInterface +//import kaist.iclab.tracker.database.DatabaseInterface +//import kaist.iclab.tracker.filters.Filter +// +//abstract class OldAbstractCollector( +// open val context: Context, +// open val database: DatabaseInterface +//) { +// abstract val NAME: String +// abstract val permissions: Array +// open val filters: MutableList = mutableListOf() +// +// open val TAG: String = this::class.simpleName ?: "UnnamedClass" +// +// /* Check whether the system allow to collect data +// * In case of sensor malfunction or broken, it would not be available.*/ +// abstract fun isAvailable(): Boolean +// +// /* Enable the collector by checking and requesting permissions +// * Different with `isAvailable`, `enable` is used to request permissions when +// * the collector is available, but does not have permission +// * */ +// fun enable(permissionManager: PermissionManagerInterface, onResult: (granted: Boolean)-> Unit) { +// permissionManager.request(permissions){ +// Log.d(TAG, "Permission granted: ${permissions.all { permission -> it[permission] == true }}") +// onResult(permissions.all { permission -> it[permission] == true }) +// } +// } +// +// /* Start collector to collect data +// * */ +// abstract fun start() +// +// /* Stop collector to stop collecting data +// * */ +// abstract fun stop() +// +//} \ No newline at end of file diff --git a/tracker-library/src/main/java/kaist/iclab/tracker/collectors/WiFiScanCollector.kt b/tracker-library/src/main/java/kaist/iclab/tracker/collectors/WiFiScanCollector.kt index f9d3e96..3183c3c 100644 --- a/tracker-library/src/main/java/kaist/iclab/tracker/collectors/WiFiScanCollector.kt +++ b/tracker-library/src/main/java/kaist/iclab/tracker/collectors/WiFiScanCollector.kt @@ -2,14 +2,9 @@ package kaist.iclab.tracker.collectors import android.Manifest import android.content.Context -import android.hardware.Sensor -import android.hardware.SensorEvent -import android.hardware.SensorEventListener -import android.hardware.SensorManager import android.net.wifi.WifiManager import android.os.Build import kaist.iclab.tracker.triggers.SystemBroadcastTrigger -import java.util.concurrent.TimeUnit class WiFiScanCollector( override val context: Context diff --git a/tracker-library/src/main/java/kaist/iclab/tracker/controller/CollectorController.OLD b/tracker-library/src/main/java/kaist/iclab/tracker/controller/CollectorController.OLD deleted file mode 100644 index 6019cb7..0000000 --- a/tracker-library/src/main/java/kaist/iclab/tracker/controller/CollectorController.OLD +++ /dev/null @@ -1,98 +0,0 @@ -package kaist.iclab.tracker.controller - -import android.content.Context -import android.content.Intent -import android.os.Build -import android.util.Log -import kaist.iclab.tracker.Tracker -import kaist.iclab.tracker.collectors.AbstractCollector -import kaist.iclab.tracker.permission.PermissionManagerInterface -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map - -class CollectorController( - private val context: Context -) : CollectorControllerInterface { - companion object { - const val TAG = "CollectorController" - const val CONFIG_COLLECTION = "CONFIG" - const val RUNNING_COLLECTION = "RUNNING" - } - - private val database = Tracker.getDatabase() - - private val serviceIntent = Intent(context, CollectorService::class.java) - override val collectors = mutableListOf() - - override fun addCollector(collector: AbstractCollector) { - collectors.add(collector) - if(!(collector.NAME in database.getLastDoc(CONFIG_COLLECTION))) { - database.update( - CONFIG_COLLECTION, database.getLastDoc(CONFIG_COLLECTION) - + mapOf(collector.NAME to false, "timestamp" to System.currentTimeMillis())) - } - } - - override fun removeCollector(collector: AbstractCollector) { - collectors.remove(collector) - database.update(CONFIG_COLLECTION, database.getLastDoc(CONFIG_COLLECTION) - collector.NAME + mapOf("timestamp" to System.currentTimeMillis())) - } - - override fun getCollectorsList(): List { - return collectors.map { it.NAME } - } - - override fun isRunning(): Boolean { - val map = database.getLastDoc(RUNNING_COLLECTION) - return (map["running"] as Boolean?) ?: false - } - - override fun getCollectorConfigChange(): Flow> { - return database.getLastDocFlow(CONFIG_COLLECTION) as Flow> - } - - override fun isRunningFlow(): Flow { - return database.getLastDocFlow(RUNNING_COLLECTION).map { - (it["running"] as Boolean?) ?: false - } - } - - override fun enable(name: String, permissionManager: PermissionManagerInterface) { - collectors.forEach { - if (it.NAME == name) { - Log.d(TAG, "Enabling $name") - it.enable(permissionManager) { enabled -> - if (enabled) { - database.update( - CONFIG_COLLECTION, - database.getLastDoc(CONFIG_COLLECTION) - + mapOf(it.NAME to true, "timestamp" to System.currentTimeMillis())) - } - } - } - } - } - - override fun disable(name: String) { - collectors.forEach { - if (it.NAME == name) { - database.update( - CONFIG_COLLECTION, - database.getLastDoc(CONFIG_COLLECTION) - + mapOf(it.NAME to false, "timestamp" to System.currentTimeMillis())) - } - } - } - - override fun start() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context.startForegroundService(serviceIntent) - } else { - context.startService(serviceIntent) - } - } - - override fun stop() { - context.stopService(serviceIntent) - } -} \ No newline at end of file diff --git a/tracker-library/src/main/java/kaist/iclab/tracker/controller/CollectorController.kt b/tracker-library/src/main/java/kaist/iclab/tracker/controller/CollectorController.kt index 2894bc7..f369e4d 100644 --- a/tracker-library/src/main/java/kaist/iclab/tracker/controller/CollectorController.kt +++ b/tracker-library/src/main/java/kaist/iclab/tracker/controller/CollectorController.kt @@ -1,5 +1,6 @@ package kaist.iclab.tracker.controller +import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.Service @@ -9,21 +10,21 @@ import android.content.pm.ServiceInfo import android.os.Build import android.os.IBinder import android.util.Log +import androidx.core.app.NotificationCompat import androidx.core.app.ServiceCompat import kaist.iclab.tracker.Tracker import kaist.iclab.tracker.collectors.AbstractCollector -import kaist.iclab.tracker.controller.CollectorService.Companion -import kaist.iclab.tracker.permission.PermissionManagerInterface import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow -class CollectorController( +class CollectorControllerImpl( private val context: Context ) : CollectorControllerInterface { - - private val serviceIntent = Intent(context, CollectorService::class.java) - private val collectors = mutableListOf() + override val collectors = mutableListOf() + + override val _stateFlow = MutableSharedFlow() override fun add(collector: AbstractCollector) { collectors.add(collector) @@ -33,10 +34,7 @@ class CollectorController( collectors.remove(collector) } - private val _isRunningFlow = Flow() - override fun isRunningFlow(): Flow { - - } + override fun isRunningFlow(): Flow = _stateFlow.asSharedFlow() override fun start() { @@ -51,27 +49,68 @@ class CollectorController( context.stopService(serviceIntent) } - object CollectorService : Service() { + class CollectorService: Service() { + val collectors = Tracker.getCollectorController().collectors + val _stateFlow = Tracker.getCollectorController()._stateFlow + override fun onBind(intent: Intent?): IBinder? = null override fun onDestroy() { + stop() } + fun run() { + ServiceCompat.startForeground( + this, + NotificationHandler.CHANNEL_NUMBER, + NotificationHandler.createNotification(this), + requiredForegroundServiceType() + ) + _stateFlow.tryEmit(true) + collectors.forEach { collector -> + collector.start() + } + } + fun stop() { + _stateFlow.tryEmit(false) + collectors.forEach { collector -> + collector.stop() + } + stopSelf() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + stopForeground(STOP_FOREGROUND_REMOVE) + } else { + stopForeground(true) + } + } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - ServiceCompat.startForeground(this, 1, notification, requiredForegroundServiceType()) - + try { + run() + } catch (e: Exception) { + Log.e("CollectorService", "ERROR:${e}") + stop() + } + return super.onStartCommand(intent, flags, startId) } fun requiredForegroundServiceType(): Int { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC + val serviceTypes = mutableSetOf() + collectors.forEach { + serviceTypes.addAll(it.foregroundServiceTypes) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + serviceTypes.add(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) + } + return if (serviceTypes.isNotEmpty()) { + serviceTypes.reduce { acc, type -> acc or type } } else { 0 } } } - object CollectorServiceNotificationHandler { + + object NotificationHandler { const val CHANNEL_ID = "COLLECTOR_WORKING" const val CHANNEL_NUMBER = 1 const val NOTF_TITLE = "Collector Working..." @@ -80,8 +119,12 @@ class CollectorController( fun createNotificationChannel(context: Context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val importance = NotificationManager.IMPORTANCE_DEFAULT - val channel = NotificationChannel(kaist.iclab.tracker.controller.CollectorService.CHANNEL_ID, kaist.iclab.tracker.controller.CollectorService.NOTF_TITLE, importance).apply { - description = kaist.iclab.tracker.controller.CollectorService.NOTF_DESCRIPTION + val channel = NotificationChannel( + CHANNEL_ID, + NOTF_TITLE, + importance + ).apply { + description = NOTF_DESCRIPTION } // Register the channel with the system val notificationManager: NotificationManager = @@ -89,5 +132,13 @@ class CollectorController( notificationManager.createNotificationChannel(channel) } } + + fun createNotification(context: Context): Notification { + return NotificationCompat.Builder(context, CHANNEL_ID) + .setContentTitle(NOTF_TITLE) + .setContentText(NOTF_DESCRIPTION) + .setOngoing(true) + .build() + } } } \ No newline at end of file diff --git a/tracker-library/src/main/java/kaist/iclab/tracker/controller/CollectorControllerInterface.kt b/tracker-library/src/main/java/kaist/iclab/tracker/controller/CollectorControllerInterface.kt index 8509ae2..58679ca 100644 --- a/tracker-library/src/main/java/kaist/iclab/tracker/controller/CollectorControllerInterface.kt +++ b/tracker-library/src/main/java/kaist/iclab/tracker/controller/CollectorControllerInterface.kt @@ -3,8 +3,12 @@ package kaist.iclab.tracker.controller import kaist.iclab.tracker.collectors.AbstractCollector import kaist.iclab.tracker.permission.PermissionManagerInterface import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow interface CollectorControllerInterface { + val collectors: MutableList + val _stateFlow: MutableSharedFlow fun add(collector: AbstractCollector) fun remove(collector: AbstractCollector) diff --git a/tracker-library/src/main/java/kaist/iclab/tracker/controller/CollectorService.kt b/tracker-library/src/main/java/kaist/iclab/tracker/controller/CollectorService.kt deleted file mode 100644 index 6df94fa..0000000 --- a/tracker-library/src/main/java/kaist/iclab/tracker/controller/CollectorService.kt +++ /dev/null @@ -1,75 +0,0 @@ -package kaist.iclab.tracker.controller - -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.Service -import android.content.Context -import android.content.Intent -import android.content.pm.ServiceInfo -import android.os.Build -import android.os.IBinder -import android.util.Log -import androidx.core.app.NotificationCompat -import androidx.core.app.ServiceCompat -import kaist.iclab.tracker.Tracker - - -class CollectorService(): Service() { - companion object { - const val TAG = "CollectorService" - const val CHANNEL_ID = "COLLECTOR_WORKING" - const val CHANNEL_NUMBER = 1 - const val NOTF_TITLE = "Collector Working..." - const val NOTF_DESCRIPTION = "Collector is tracking for your daily life!" - - /* Notification Channel Register - * Required once after created - * */ - fun createNotificationChannel(context: Context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val importance = NotificationManager.IMPORTANCE_DEFAULT - val channel = NotificationChannel(CHANNEL_ID, NOTF_TITLE, importance).apply { - description = NOTF_DESCRIPTION - } - // Register the channel with the system - val notificationManager: NotificationManager = - context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.createNotificationChannel(channel) - } - } - } - - override fun onBind(intent: Intent?): IBinder? = null - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - Log.d(TAG, "onStartCommand") - run() - val notification: Notification = - NotificationCompat.Builder(this, CHANNEL_ID) - .setContentTitle(NOTF_TITLE) - .setContentText(NOTF_DESCRIPTION) - .setOngoing(true) - .build() - ServiceCompat.startForeground(this, CHANNEL_NUMBER, notification, - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC - /*TODO: Change dynamically based on enabled collectors*/ - } else { - 0 - } - ) - - return super.onStartCommand(intent, flags, startId) - } - override fun onDestroy() { - super.onDestroy() - Tracker.getCollectorController().start - } - - fun run() { - Log.d(TAG, "run") - Tracker.getCollectorController().collectors.forEach { - it.start() - } - } -} \ No newline at end of file diff --git a/tracker-library/src/main/java/kaist/iclab/tracker/database/CouchbaseDBImpl.kt b/tracker-library/src/main/java/kaist/iclab/tracker/database/CouchbaseDBImpl.kt deleted file mode 100644 index 5ccc80a..0000000 --- a/tracker-library/src/main/java/kaist/iclab/tracker/database/CouchbaseDBImpl.kt +++ /dev/null @@ -1,107 +0,0 @@ -package kaist.iclab.tracker.database - -import android.content.Context -import android.util.Log -import com.couchbase.lite.CouchbaseLite -import com.couchbase.lite.DataSource -import com.couchbase.lite.Database -import com.couchbase.lite.Expression -import com.couchbase.lite.MutableDocument -import com.couchbase.lite.Ordering -import com.couchbase.lite.Query -import com.couchbase.lite.QueryBuilder -import com.couchbase.lite.SelectResult -import com.couchbase.lite.collectionChangeFlow -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart - -class CouchbaseDBImpl( - context: Context, -) : DatabaseInterface { - - companion object { - const val TAG = "CouchbaseDBImpl" - const val LOG_COLLECTION = "LOG" - const val DB = "tracker" - } - - private val database: Database by lazy { - Database(DB) - } - - init { - // Initialize Couchbase Lite - CouchbaseLite.init(context) - } - - override fun insert(collectionName: String, data: Map): String { - val document = MutableDocument(data) - val collection = getCollection(collectionName) - collection.save(document) - return document.id - } - - override fun update(collectionName: String, data: Map) { - val collection = getCollection(collectionName) - val id = collection.indexes.firstOrNull() - val document = MutableDocument(id, data) - collection.save(document) - Log.d(TAG, "$collectionName Updated: ${document.toMap()}") - } - - override fun sync() { - TODO("Not yet implemented") - } - - override fun deleteAll() { - database.delete() - } - - override fun getAllDocs(collectionName: String): List> { - val collection = getCollection(collectionName) - val query: Query = QueryBuilder.select(SelectResult.all()) - .from(DataSource.collection(collection)) - return query.execute().allResults().map { it.toMap() } - } - - override fun getDocsFlow(collectionName: String): Flow>> { - val collection = getCollection(collectionName) - return collection.collectionChangeFlow().map { - getAllDocs(collectionName) - }.onStart { - emit(getAllDocs(collectionName)) - } - } - - override fun getLastDoc(collectionName: String): Map { - val collection = getCollection(collectionName) - val query: Query = QueryBuilder.select(SelectResult.all()) - .from(DataSource.collection(collection)) - .orderBy(Ordering.property("timestamp").descending()) - .limit(Expression.intValue(1)) - return query.execute().firstOrNull()?.getDictionary(0)?.toMap() ?: mapOf() - } - - override fun getLastDocFlow(collectionName: String): Flow> { - val collection = getCollection(collectionName) - return collection.collectionChangeFlow().map { - getLastDoc(collectionName) - }.onStart { - emit(getLastDoc(collectionName)) - } - } - - override fun log(message: String) { - insert( - LOG_COLLECTION, - mapOf("timestamp" to System.currentTimeMillis(), "message" to message) - ) - } - - private fun getCollection(collectionName: String): com.couchbase.lite.Collection { - val collection = database.getCollection(collectionName) - ?: database.createCollection(collectionName) - return collection - } -} \ No newline at end of file diff --git a/tracker-library/src/main/java/kaist/iclab/tracker/database/DatabaseInterface.kt b/tracker-library/src/main/java/kaist/iclab/tracker/database/DatabaseInterface.kt deleted file mode 100644 index 3b4b349..0000000 --- a/tracker-library/src/main/java/kaist/iclab/tracker/database/DatabaseInterface.kt +++ /dev/null @@ -1,20 +0,0 @@ -package kaist.iclab.tracker.database - -import kotlinx.coroutines.flow.Flow - -interface DatabaseInterface { - fun insert(collectionName: String, data: Map): String - fun update(collectionName: String, data: Map) - - fun sync() - fun deleteAll() - - fun getDocsFlow(collectionName: String): Flow>> - fun getAllDocs(collectionName: String): List> - - fun getLastDocFlow(collectionName: String): Flow> - fun getLastDoc(collectionName: String): Map - - /* Function to Log some messages*/ - fun log(message: String) -} \ No newline at end of file