feat: activityLog

This commit is contained in:
lisonge 2024-08-04 18:48:47 +08:00
parent 8c12ee172f
commit d4f1509b85
14 changed files with 733 additions and 48 deletions

View File

@ -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')"
]
}
}

View File

@ -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<Long>
@Query("DELETE FROM activity_log")
suspend fun deleteAll()
@Query("SELECT * FROM activity_log ORDER BY id DESC ")
fun pagingSource(): PagingSource<Int, ActivityLog>
@Query("SELECT COUNT(*) FROM activity_log")
fun count(): Flow<Int>
@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
}
}

View File

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

View File

@ -23,4 +23,6 @@ object DbSet {
get() = db.clickLogDao()
val categoryConfigDao
get() = db.categoryConfigDao()
val activityLogDao
get() = db.activityLogDao()
}

View File

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

View File

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

View File

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

View File

@ -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<ActivityLogVm>()
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,
)
}
}
}
}
}

View File

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

View File

@ -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<ClickLogVm>()
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))

View File

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

View File

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

View File

@ -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<Set<Long>?>(null)
}

View File

@ -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<Long> = 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 <T> throttle(
interval: Long = defaultThrottleInterval,
timer: Value<Long> = 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)
}
}