From d4f1509b85418767b973b5eb9d248c750a3f4a69 Mon Sep 17 00:00:00 2001 From: lisonge Date: Sun, 4 Aug 2024 18:48:47 +0800 Subject: [PATCH] feat: activityLog --- app/schemas/li.songe.gkd.db.AppDb/7.json | 341 ++++++++++++++++++ .../kotlin/li/songe/gkd/data/ActivityLog.kt | 68 ++++ app/src/main/kotlin/li/songe/gkd/db/AppDb.kt | 14 +- app/src/main/kotlin/li/songe/gkd/db/DbSet.kt | 2 + .../li/songe/gkd/debug/SnapshotTileService.kt | 5 +- .../kotlin/li/songe/gkd/service/AbState.kt | 47 ++- .../li/songe/gkd/service/GkdAbService.kt | 28 +- .../kotlin/li/songe/gkd/ui/ActivityLogPage.kt | 149 ++++++++ .../kotlin/li/songe/gkd/ui/ActivityLogVm.kt | 22 ++ .../kotlin/li/songe/gkd/ui/ClickLogPage.kt | 28 +- .../kotlin/li/songe/gkd/ui/SnapshotPage.kt | 28 +- .../li/songe/gkd/ui/home/ControlPage.kt | 30 +- .../kotlin/li/songe/gkd/ui/home/HomeVm.kt | 4 - .../main/kotlin/li/songe/gkd/util/TimeExt.kt | 15 +- 14 files changed, 733 insertions(+), 48 deletions(-) create mode 100644 app/schemas/li.songe.gkd.db.AppDb/7.json create mode 100644 app/src/main/kotlin/li/songe/gkd/data/ActivityLog.kt create mode 100644 app/src/main/kotlin/li/songe/gkd/ui/ActivityLogPage.kt create mode 100644 app/src/main/kotlin/li/songe/gkd/ui/ActivityLogVm.kt diff --git a/app/schemas/li.songe.gkd.db.AppDb/7.json b/app/schemas/li.songe.gkd.db.AppDb/7.json new file mode 100644 index 0000000..eca608a --- /dev/null +++ b/app/schemas/li.songe.gkd.db.AppDb/7.json @@ -0,0 +1,341 @@ +{ + "formatVersion": 1, + "database": { + "version": 7, + "identityHash": "9d5e657834ed630ac5cf00753cf24a55", + "entities": [ + { + "tableName": "subs_item", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `update_url` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ctime", + "columnName": "ctime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mtime", + "columnName": "mtime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enableUpdate", + "columnName": "enable_update", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updateUrl", + "columnName": "update_url", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "snapshot", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `app_name` TEXT, `app_version_code` INTEGER, `app_version_name` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `github_asset_id` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "appId", + "columnName": "app_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "activityId", + "columnName": "activity_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "appName", + "columnName": "app_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "appVersionCode", + "columnName": "app_version_code", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "appVersionName", + "columnName": "app_version_name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "screenHeight", + "columnName": "screen_height", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "screenWidth", + "columnName": "screen_width", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isLandscape", + "columnName": "is_landscape", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "githubAssetId", + "columnName": "github_asset_id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "subs_config", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER, `subs_item_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, `exclude` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subsItemId", + "columnName": "subs_item_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "appId", + "columnName": "app_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "groupKey", + "columnName": "group_key", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exclude", + "columnName": "exclude", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "click_log", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `subs_id` INTEGER NOT NULL, `subs_version` INTEGER NOT NULL DEFAULT 0, `group_key` INTEGER NOT NULL, `group_type` INTEGER NOT NULL DEFAULT 2, `rule_index` INTEGER NOT NULL, `rule_key` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "appId", + "columnName": "app_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "activityId", + "columnName": "activity_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "subsId", + "columnName": "subs_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subsVersion", + "columnName": "subs_version", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "groupKey", + "columnName": "group_key", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupType", + "columnName": "group_type", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "2" + }, + { + "fieldPath": "ruleIndex", + "columnName": "rule_index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ruleKey", + "columnName": "rule_key", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "category_config", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER, `subs_item_id` INTEGER NOT NULL, `category_key` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "enable", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subsItemId", + "columnName": "subs_item_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "categoryKey", + "columnName": "category_key", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "activity_log", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `activity_id` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "appId", + "columnName": "app_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activityId", + "columnName": "activity_id", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9d5e657834ed630ac5cf00753cf24a55')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/li/songe/gkd/data/ActivityLog.kt b/app/src/main/kotlin/li/songe/gkd/data/ActivityLog.kt new file mode 100644 index 0000000..3d60ae3 --- /dev/null +++ b/app/src/main/kotlin/li/songe/gkd/data/ActivityLog.kt @@ -0,0 +1,68 @@ +package li.songe.gkd.data + +import androidx.paging.PagingSource +import androidx.room.ColumnInfo +import androidx.room.Dao +import androidx.room.Entity +import androidx.room.Insert +import androidx.room.PrimaryKey +import androidx.room.Query +import kotlinx.coroutines.flow.Flow +import li.songe.gkd.util.format + +@Entity( + tableName = "activity_log", +) +data class ActivityLog( + @PrimaryKey @ColumnInfo(name = "id") val id: Long = System.currentTimeMillis(), + @ColumnInfo(name = "app_id") val appId: String, + @ColumnInfo(name = "activity_id") val activityId: String? = null, +) { + val showActivityId by lazy { + if (activityId != null) { + if (activityId.startsWith( + appId + ) + ) { + activityId.substring(appId.length) + } else { + activityId + } + } else { + null + } + } + val date by lazy { id.format("MM-dd HH:mm:ss SSS") } + + @Dao + interface ActivityLogDao { + @Insert + suspend fun insert(vararg objects: ActivityLog): List + + @Query("DELETE FROM activity_log") + suspend fun deleteAll() + + @Query("SELECT * FROM activity_log ORDER BY id DESC ") + fun pagingSource(): PagingSource + + @Query("SELECT COUNT(*) FROM activity_log") + fun count(): Flow + + @Query( + """ + DELETE FROM activity_log + WHERE ( + SELECT COUNT(*) + FROM activity_log + ) > 1000 + AND id <= ( + SELECT id + FROM activity_log + ORDER BY id DESC + LIMIT 1 OFFSET 1000 + ) + """ + ) + suspend fun deleteKeepLatest(): Int + } +} diff --git a/app/src/main/kotlin/li/songe/gkd/db/AppDb.kt b/app/src/main/kotlin/li/songe/gkd/db/AppDb.kt index 0146838..dc7f813 100644 --- a/app/src/main/kotlin/li/songe/gkd/db/AppDb.kt +++ b/app/src/main/kotlin/li/songe/gkd/db/AppDb.kt @@ -3,6 +3,7 @@ package li.songe.gkd.db import androidx.room.AutoMigration import androidx.room.Database import androidx.room.RoomDatabase +import li.songe.gkd.data.ActivityLog import li.songe.gkd.data.CategoryConfig import li.songe.gkd.data.ClickLog import li.songe.gkd.data.Snapshot @@ -10,14 +11,22 @@ import li.songe.gkd.data.SubsConfig import li.songe.gkd.data.SubsItem @Database( - version = 6, - entities = [SubsItem::class, Snapshot::class, SubsConfig::class, ClickLog::class, CategoryConfig::class], + version = 7, + entities = [ + SubsItem::class, + Snapshot::class, + SubsConfig::class, + ClickLog::class, + CategoryConfig::class, + ActivityLog::class + ], autoMigrations = [ AutoMigration(from = 1, to = 2), AutoMigration(from = 2, to = 3), AutoMigration(from = 3, to = 4), AutoMigration(from = 4, to = 5), AutoMigration(from = 5, to = 6), + AutoMigration(from = 6, to = 7), ] ) abstract class AppDb : RoomDatabase() { @@ -26,4 +35,5 @@ abstract class AppDb : RoomDatabase() { abstract fun subsConfigDao(): SubsConfig.SubsConfigDao abstract fun clickLogDao(): ClickLog.TriggerLogDao abstract fun categoryConfigDao(): CategoryConfig.CategoryConfigDao + abstract fun activityLogDao(): ActivityLog.ActivityLogDao } \ No newline at end of file diff --git a/app/src/main/kotlin/li/songe/gkd/db/DbSet.kt b/app/src/main/kotlin/li/songe/gkd/db/DbSet.kt index e311990..0d5ec9f 100644 --- a/app/src/main/kotlin/li/songe/gkd/db/DbSet.kt +++ b/app/src/main/kotlin/li/songe/gkd/db/DbSet.kt @@ -23,4 +23,6 @@ object DbSet { get() = db.clickLogDao() val categoryConfigDao get() = db.categoryConfigDao() + val activityLogDao + get() = db.activityLogDao() } \ No newline at end of file diff --git a/app/src/main/kotlin/li/songe/gkd/debug/SnapshotTileService.kt b/app/src/main/kotlin/li/songe/gkd/debug/SnapshotTileService.kt index 4b7976e..ac8bade 100644 --- a/app/src/main/kotlin/li/songe/gkd/debug/SnapshotTileService.kt +++ b/app/src/main/kotlin/li/songe/gkd/debug/SnapshotTileService.kt @@ -12,7 +12,7 @@ import li.songe.gkd.service.GkdAbService.Companion.shizukuTopActivityGetter import li.songe.gkd.service.TopActivity import li.songe.gkd.service.getAndUpdateCurrentRules import li.songe.gkd.service.safeActiveWindow -import li.songe.gkd.service.topActivityFlow +import li.songe.gkd.service.updateTopActivity import li.songe.gkd.util.launchTry import li.songe.gkd.util.toast @@ -35,8 +35,9 @@ class SnapshotTileService : TileService() { service.safeActiveWindow?.packageName?.toString() ?: return@launchTry if (latestAppId != oldAppId) { eventExecutor.execute { - topActivityFlow.value = + updateTopActivity( shizukuTopActivityGetter?.invoke() ?: TopActivity(appId = latestAppId) + ) getAndUpdateCurrentRules() appScope.launchTry(Dispatchers.IO) { captureSnapshot() diff --git a/app/src/main/kotlin/li/songe/gkd/service/AbState.kt b/app/src/main/kotlin/li/songe/gkd/service/AbState.kt index af66e7f..3cf590e 100644 --- a/app/src/main/kotlin/li/songe/gkd/service/AbState.kt +++ b/app/src/main/kotlin/li/songe/gkd/service/AbState.kt @@ -1,9 +1,13 @@ package li.songe.gkd.service +import com.blankj.utilcode.util.LogUtils +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import li.songe.gkd.app +import li.songe.gkd.appScope +import li.songe.gkd.data.ActivityLog import li.songe.gkd.data.AppRule import li.songe.gkd.data.ClickLog import li.songe.gkd.data.GlobalRule @@ -13,6 +17,7 @@ import li.songe.gkd.db.DbSet import li.songe.gkd.util.RuleSummary import li.songe.gkd.util.getDefaultLauncherAppId import li.songe.gkd.util.increaseClickCount +import li.songe.gkd.util.launchTry import li.songe.gkd.util.recordStoreFlow import li.songe.gkd.util.ruleSummaryFlow @@ -20,9 +25,47 @@ data class TopActivity( val appId: String = "", val activityId: String? = null, val number: Int = 0 -) +) { + fun format(): String { + return "${appId}/${activityId}/${number}" + } +} val topActivityFlow = MutableStateFlow(TopActivity()) +private val activityLogMutex by lazy { Mutex() } + +private var activityLogCount = 0 +private var lastActivityChangeTime = 0L +fun updateTopActivity(topActivity: TopActivity) { + if (topActivityFlow.value.appId == topActivity.appId && topActivityFlow.value.activityId == topActivity.activityId) { + if (topActivityFlow.value.number == topActivity.number) { + return + } + val t = System.currentTimeMillis() + if (t - lastActivityChangeTime < 1000) { + return + } + } + appScope.launchTry(Dispatchers.IO) { + activityLogMutex.withLock { + DbSet.activityLogDao.insert( + ActivityLog( + appId = topActivity.appId, + activityId = topActivity.activityId + ) + ) + activityLogCount++ + if (activityLogCount % 100 == 0) { + DbSet.activityLogDao.deleteKeepLatest() + } + } + } + LogUtils.d( + "${topActivityFlow.value.format()} -> ${topActivity.format()}" + ) + topActivityFlow.value = topActivity + lastActivityChangeTime = System.currentTimeMillis() +} data class ActivityRule( val appRules: List = emptyList(), @@ -42,7 +85,7 @@ private fun getFixTopActivity(): TopActivity { if (top.activityId == null) { if (lastTopActivity.appId == top.appId) { // 当从通知栏上拉返回应用, 从锁屏返回 等时, activityId 的无障碍事件不会触发, 此时复用上一次获得的 activityId 填充 - topActivityFlow.value = lastTopActivity + updateTopActivity(lastTopActivity) } } else { // 仅保留最近的有 activityId 的单个 TopActivity diff --git a/app/src/main/kotlin/li/songe/gkd/service/GkdAbService.kt b/app/src/main/kotlin/li/songe/gkd/service/GkdAbService.kt index 6950574..62325b0 100644 --- a/app/src/main/kotlin/li/songe/gkd/service/GkdAbService.kt +++ b/app/src/main/kotlin/li/songe/gkd/service/GkdAbService.kt @@ -202,9 +202,9 @@ class GkdAbService : CompositionAbService({ if (topActivityFlow.value.appId != rightAppId) { val shizukuTop = getShizukuTopActivity() if (shizukuTop?.appId == rightAppId) { - topActivityFlow.value = shizukuTop + updateTopActivity(shizukuTop) } else { - topActivityFlow.value = TopActivity(appId = rightAppId) + updateTopActivity(TopActivity(appId = rightAppId)) } getAndUpdateCurrentRules() scope.launch(actionThread) { @@ -308,8 +308,10 @@ class GkdAbService : CompositionAbService({ if (fixedEvent.type == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { // tv.danmaku.bili, com.miui.home, com.miui.home.launcher.Launcher if (isActivity(evAppId, evActivityId)) { - topActivityFlow.value = TopActivity( - evAppId, evActivityId, topActivityFlow.value.number + 1 + updateTopActivity( + TopActivity( + evAppId, evActivityId, topActivityFlow.value.number + 1 + ) ) } } else { @@ -317,11 +319,13 @@ class GkdAbService : CompositionAbService({ val shizukuTop = getShizukuTopActivity() if (shizukuTop != null && shizukuTop.appId == rightAppId) { if (shizukuTop.activityId == evActivityId) { - topActivityFlow.value = TopActivity( - evAppId, evActivityId, topActivityFlow.value.number + 1 + updateTopActivity( + TopActivity( + evAppId, evActivityId, topActivityFlow.value.number + 1 + ) ) } - topActivityFlow.value = shizukuTop + updateTopActivity(shizukuTop) } lastTriggerShizukuTime = fixedEvent.time } @@ -331,9 +335,9 @@ class GkdAbService : CompositionAbService({ // 从 锁屏,下拉通知栏 返回等情况, 应用不会发送事件, 但是系统组件会发送事件 val shizukuTop = getShizukuTopActivity() if (shizukuTop?.appId == rightAppId) { - topActivityFlow.value = shizukuTop + updateTopActivity(shizukuTop) } else { - topActivityFlow.value = TopActivity(rightAppId) + updateTopActivity(TopActivity(rightAppId)) } } @@ -375,14 +379,10 @@ class GkdAbService : CompositionAbService({ scope.launch(Dispatchers.IO) { activityRuleFlow.debounce(300).collect { - if (storeFlow.value.enableService) { + if (storeFlow.value.enableService && it.currentRules.isNotEmpty()) { LogUtils.d(it.topActivity, *it.currentRules.map { r -> r.statusText() }.toTypedArray()) - } else { - LogUtils.d( - it.topActivity - ) } } } diff --git a/app/src/main/kotlin/li/songe/gkd/ui/ActivityLogPage.kt b/app/src/main/kotlin/li/songe/gkd/ui/ActivityLogPage.kt new file mode 100644 index 0000000..34ddfae --- /dev/null +++ b/app/src/main/kotlin/li/songe/gkd/ui/ActivityLogPage.kt @@ -0,0 +1,149 @@ +package li.songe.gkd.ui + +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.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.itemKey +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootNavGraph +import li.songe.gkd.MainActivity +import li.songe.gkd.db.DbSet +import li.songe.gkd.ui.component.StartEllipsisText +import li.songe.gkd.ui.component.waitResult +import li.songe.gkd.ui.style.EmptyHeight +import li.songe.gkd.util.LocalNavController +import li.songe.gkd.util.ProfileTransitions +import li.songe.gkd.util.appInfoCacheFlow +import li.songe.gkd.util.launchAsFn +import li.songe.gkd.util.throttle + +@RootNavGraph +@Destination(style = ProfileTransitions::class) +@Composable +fun ActivityLogPage() { + val context = LocalContext.current as MainActivity + val mainVm = context.mainVm + val vm = hiltViewModel() + val navController = LocalNavController.current + + val logCount by vm.logCountFlow.collectAsState() + val list = vm.pagingDataFlow.collectAsLazyPagingItems() + val appInfoCache by appInfoCacheFlow.collectAsState() + + val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() + Scaffold(modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { + TopAppBar( + scrollBehavior = scrollBehavior, + navigationIcon = { + IconButton(onClick = throttle { + navController.popBackStack() + }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = null, + ) + } + }, + title = { + Text(text = "界面记录") + }, + actions = { + if (logCount > 0) { + IconButton(onClick = throttle(fn = vm.viewModelScope.launchAsFn { + mainVm.dialogFlow.waitResult( + title = "删除记录", + text = "是否删除所有界面记录?", + ) + DbSet.activityLogDao.deleteAll() + })) { + Icon( + imageVector = Icons.Outlined.Delete, + contentDescription = null, + ) + } + } + }) + }) { contentPadding -> + LazyColumn( + modifier = Modifier.padding(contentPadding), + ) { + items( + count = list.itemCount, + key = list.itemKey { it.id } + ) { i -> + val activityLog = list[i] ?: return@items + if (i > 0) { + HorizontalDivider() + } + Column( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp) + ) { + Row { + Text(text = activityLog.date) + Spacer(modifier = Modifier.width(10.dp)) + val appInfo = appInfoCache[activityLog.appId] + val appShowName = appInfo?.name ?: activityLog.appId + Text( + text = appShowName, + style = LocalTextStyle.current.let { + if (appInfo?.isSystem == true) { + it.copy(textDecoration = TextDecoration.Underline) + } else { + it + } + } + ) + } + Spacer(modifier = Modifier.width(10.dp)) + val showActivityId = activityLog.showActivityId + if (showActivityId != null) { + StartEllipsisText(text = showActivityId) + } else { + Text(text = "null", color = LocalContentColor.current.copy(alpha = 0.5f)) + } + } + } + item { + Spacer(modifier = Modifier.height(EmptyHeight)) + if (logCount == 0) { + Text( + text = "暂无记录", + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/li/songe/gkd/ui/ActivityLogVm.kt b/app/src/main/kotlin/li/songe/gkd/ui/ActivityLogVm.kt new file mode 100644 index 0000000..a4e2c20 --- /dev/null +++ b/app/src/main/kotlin/li/songe/gkd/ui/ActivityLogVm.kt @@ -0,0 +1,22 @@ +package li.songe.gkd.ui + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.cachedIn +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn +import li.songe.gkd.db.DbSet +import javax.inject.Inject + +@HiltViewModel +class ActivityLogVm @Inject constructor(stateHandle: SavedStateHandle) : ViewModel() { + val pagingDataFlow = Pager(PagingConfig(pageSize = 100)) { DbSet.activityLogDao.pagingSource() } + .flow.cachedIn(viewModelScope) + + val logCountFlow = + DbSet.activityLogDao.count().stateIn(viewModelScope, SharingStarted.Eagerly, 0) +} \ No newline at end of file diff --git a/app/src/main/kotlin/li/songe/gkd/ui/ClickLogPage.kt b/app/src/main/kotlin/li/songe/gkd/ui/ClickLogPage.kt index e3e7f52..354f478 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/ClickLogPage.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/ClickLogPage.kt @@ -18,6 +18,7 @@ import androidx.compose.material3.Card import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -34,6 +35,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp @@ -51,6 +53,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import li.songe.gkd.MainActivity import li.songe.gkd.data.ClickLog import li.songe.gkd.data.ExcludeData import li.songe.gkd.data.SubsConfig @@ -58,6 +61,7 @@ import li.songe.gkd.data.stringify import li.songe.gkd.data.switch import li.songe.gkd.db.DbSet import li.songe.gkd.ui.component.StartEllipsisText +import li.songe.gkd.ui.component.waitResult import li.songe.gkd.ui.destinations.AppItemPageDestination import li.songe.gkd.ui.destinations.GlobalRulePageDestination import li.songe.gkd.ui.style.EmptyHeight @@ -73,8 +77,9 @@ import li.songe.gkd.util.toast @Destination(style = ProfileTransitions::class) @Composable fun ClickLogPage() { + val context = LocalContext.current as MainActivity + val mainVm = context.mainVm val navController = LocalNavController.current - val vm = hiltViewModel() val clickLogCount by vm.clickLogCountFlow.collectAsState() val clickDataItems = vm.pagingDataFlow.collectAsLazyPagingItems() @@ -111,7 +116,7 @@ fun ClickLogPage() { TopAppBar( scrollBehavior = scrollBehavior, navigationIcon = { - IconButton(onClick = { + IconButton(onClick = throttle { navController.popBackStack() }) { Icon( @@ -120,10 +125,16 @@ fun ClickLogPage() { ) } }, - title = { Text(text = "触发记录" + if (clickLogCount <= 0) "" else ("-$clickLogCount")) }, + title = { Text(text = "触发记录") }, actions = { if (clickLogCount > 0) { - IconButton(onClick = { showDeleteDlg = true }) { + IconButton(onClick = throttle(fn = vm.viewModelScope.launchAsFn { + mainVm.dialogFlow.waitResult( + title = "删除记录", + text = "是否删除所有触发记录?", + ) + DbSet.clickLogDao.deleteAll() + })) { Icon( imageVector = Icons.Outlined.Delete, contentDescription = null, @@ -140,6 +151,9 @@ fun ClickLogPage() { key = clickDataItems.itemKey { c -> c.t0.id } ) { i -> val (clickLog, group, rule) = clickDataItems[i] ?: return@items + if (i > 0) { + HorizontalDivider() + } Column( modifier = Modifier .clickable { @@ -167,8 +181,11 @@ fun ClickLogPage() { } } Spacer(modifier = Modifier.width(10.dp)) - clickLog.showActivityId?.let { showActivityId -> + val showActivityId = clickLog.showActivityId + if (showActivityId != null) { StartEllipsisText(text = showActivityId) + } else { + Text(text = "null", color = LocalContentColor.current.copy(alpha = 0.5f)) } group?.name?.let { name -> Text(text = name) @@ -179,7 +196,6 @@ fun ClickLogPage() { Text(text = (if (clickLog.ruleKey != null) "key=${clickLog.ruleKey}, " else "") + "index=${clickLog.ruleIndex}") } } - HorizontalDivider() } item { Spacer(modifier = Modifier.height(EmptyHeight)) diff --git a/app/src/main/kotlin/li/songe/gkd/ui/SnapshotPage.kt b/app/src/main/kotlin/li/songe/gkd/ui/SnapshotPage.kt index 560634b..d65cab0 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/SnapshotPage.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/SnapshotPage.kt @@ -21,6 +21,7 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -140,18 +141,23 @@ fun SnapshotPage() { maxLines = 1, ) } - if (snapshot.activityId != null) { - val showActivityId = - if (snapshot.appId != null && snapshot.activityId.startsWith( - snapshot.appId - ) - ) { - snapshot.activityId.substring(snapshot.appId.length) - } else { - snapshot.activityId - } - Spacer(modifier = Modifier.width(10.dp)) + Spacer(modifier = Modifier.width(10.dp)) + val showActivityId = if (snapshot.activityId != null) { + if (snapshot.appId != null && snapshot.activityId.startsWith( + snapshot.appId + ) + ) { + snapshot.activityId.substring(snapshot.appId.length) + } else { + snapshot.activityId + } + } else { + null + } + if (showActivityId != null) { StartEllipsisText(text = showActivityId) + } else { + Text(text = "null", color = LocalContentColor.current.copy(alpha = 0.5f)) } } HorizontalDivider() diff --git a/app/src/main/kotlin/li/songe/gkd/ui/home/ControlPage.kt b/app/src/main/kotlin/li/songe/gkd/ui/home/ControlPage.kt index a755a35..17f70a2 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/home/ControlPage.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/home/ControlPage.kt @@ -41,6 +41,7 @@ import li.songe.gkd.service.GkdAbService import li.songe.gkd.service.ManageService import li.songe.gkd.ui.component.AuthCard import li.songe.gkd.ui.component.TextSwitch +import li.songe.gkd.ui.destinations.ActivityLogPageDestination import li.songe.gkd.ui.destinations.ClickLogPageDestination import li.songe.gkd.ui.destinations.SlowGroupPageDestination import li.songe.gkd.ui.style.EmptyHeight @@ -132,7 +133,6 @@ fun useControlPage(): ScaffoldExt { } }) - val clickLogCount by vm.clickLogCountFlow.collectAsState() Row( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, @@ -144,7 +144,7 @@ fun useControlPage(): ScaffoldExt { ) { Column(modifier = Modifier.weight(1f)) { Text( - text = "触发记录" + (if (clickLogCount > 0) "-$clickLogCount" else ""), + text = "触发记录", style = MaterialTheme.typography.bodyLarge, ) Text( @@ -159,6 +159,32 @@ fun useControlPage(): ScaffoldExt { ) } + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clickable(onClick = throttle { + navController.navigate(ActivityLogPageDestination) + }) + .itemPadding(), + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = "界面记录", + style = MaterialTheme.typography.bodyLarge, + ) + Text( + text = "记录打开的应用及界面", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null + ) + } + if (ruleSummary.slowGroupCount > 0) { Row( horizontalArrangement = Arrangement.SpaceBetween, diff --git a/app/src/main/kotlin/li/songe/gkd/ui/home/HomeVm.kt b/app/src/main/kotlin/li/songe/gkd/ui/home/HomeVm.kt index 8786da8..18bb555 100644 --- a/app/src/main/kotlin/li/songe/gkd/ui/home/HomeVm.kt +++ b/app/src/main/kotlin/li/songe/gkd/ui/home/HomeVm.kt @@ -215,9 +215,5 @@ class HomeVm @Inject constructor() : ViewModel() { } }.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) - - val clickLogCountFlow = - DbSet.clickLogDao.count().stateIn(viewModelScope, SharingStarted.Eagerly, 0) - val showShareDataIdsFlow = MutableStateFlow?>(null) } \ No newline at end of file diff --git a/app/src/main/kotlin/li/songe/gkd/util/TimeExt.kt b/app/src/main/kotlin/li/songe/gkd/util/TimeExt.kt index a1abc85..07421d5 100644 --- a/app/src/main/kotlin/li/songe/gkd/util/TimeExt.kt +++ b/app/src/main/kotlin/li/songe/gkd/util/TimeExt.kt @@ -1,5 +1,6 @@ package li.songe.gkd.util +import li.songe.gkd.data.Value import java.text.SimpleDateFormat import java.util.Locale import java.util.concurrent.TimeUnit @@ -36,17 +37,20 @@ fun Long.format(formatStr: String): String { return df.format(this) } -private var globalThrottleLastTriggerTime = 0L +private val defaultThrottleTimer by lazy { + Value(0L) +} private const val defaultThrottleInterval = 1000L fun throttle( interval: Long = defaultThrottleInterval, + timer: Value = defaultThrottleTimer, fn: (() -> Unit), ): (() -> Unit) { return { val t = System.currentTimeMillis() - if (t - globalThrottleLastTriggerTime > interval) { - globalThrottleLastTriggerTime = t + if (t - timer.value > interval) { + timer.value = t fn.invoke() } } @@ -54,12 +58,13 @@ fun throttle( fun throttle( interval: Long = defaultThrottleInterval, + timer: Value = defaultThrottleTimer, fn: ((T) -> Unit), ): ((T) -> Unit) { return { val t = System.currentTimeMillis() - if (t - globalThrottleLastTriggerTime > interval) { - globalThrottleLastTriggerTime = t + if (t - timer.value > interval) { + timer.value = t fn.invoke(it) } }