Skip to content

Commit 291992c

Browse files
risunCodeclaude
andcommitted
refactor(architecture): move business logic from frontend to backend
BREAKING: Activity filtering and launch behavior now handled by backend ## Backend Changes - Add ActivityInfo fields: canLaunchWithoutRoot, hasLauncherIntent - Add AppActivityFilter model for server-side filtering - Implement isHiddenActivity() to filter test/debug/internal activities - Update scanAppActivities() to accept filter parameters - Implement mode-aware launchActivity() (Intent vs shell am start) ## Frontend Changes - Remove all filtering logic from ActivityLauncher component - Delegate filtering to backend via bridge.getActivities(filter) - Add warning badges for non-exported activities in NONE mode - Update TypeScript types to match new backend models ## Benefits - Reduced JSON payload (filtered at source) - No heavy frontend computations - Better separation of concerns (backend = logic, frontend = presentation) - Mode-aware launch behavior (NONE vs ROOT/SHIZUKU) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9c6bcc5 commit 291992c

10 files changed

Lines changed: 509 additions & 228 deletions

File tree

app/src/main/java/com/appcontrolx/bridge/NativeBridge.kt

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import com.appcontrolx.domain.AppScanner
1515
import com.appcontrolx.domain.SafetyValidator
1616
import com.appcontrolx.domain.SystemMonitor
1717
import com.appcontrolx.model.AppAction
18+
import com.appcontrolx.model.AppActivityFilter
1819
import com.appcontrolx.model.BatchComplete
1920
import com.appcontrolx.model.BatchProgress
2021
import com.appcontrolx.model.RealtimeStatus
@@ -75,9 +76,11 @@ class NativeBridge @Inject constructor(
7576
}
7677

7778
@JavascriptInterface
78-
fun getAppList(@Suppress("UNUSED_PARAMETER") filterJson: String): String {
79+
fun getAppList(filterJson: String): String {
7980
return runBlocking {
80-
val apps = appScanner.scanAllApps(includeIcons = false) // Fast load without icons
81+
// TODO: Implement AppFilter parsing and filtering in AppScanner
82+
// For now, just return all apps
83+
val apps = appScanner.scanAllApps(includeIcons = false)
8184
json.encodeToString(apps)
8285
}
8386
}
@@ -259,15 +262,38 @@ class NativeBridge @Inject constructor(
259262
}
260263

261264
@JavascriptInterface
262-
fun getActivities(): String {
265+
fun getActivities(filterJson: String): String {
263266
return runBlocking {
264-
val activities = appScanner.scanAppActivities()
267+
val filter = try {
268+
json.decodeFromString<AppActivityFilter>(filterJson)
269+
} catch (e: Exception) {
270+
AppActivityFilter()
271+
}
272+
273+
val activities = appScanner.scanAppActivities(filter)
265274
json.encodeToString(activities)
266275
}
267276
}
268277

269278
@JavascriptInterface
270279
fun launchActivity(packageName: String, activityName: String): Boolean {
280+
val mode = shellManager.getMode()
281+
282+
return try {
283+
if (mode == com.appcontrolx.model.ExecutionMode.NONE) {
284+
// Standard Intent launch (only works if exported)
285+
launchViaIntent(packageName, activityName)
286+
} else {
287+
// Shell launch (can launch non-exported activities)
288+
val result = shellManager.execute("am start -n $packageName/$activityName")
289+
result.isSuccess
290+
}
291+
} catch (e: Exception) {
292+
false
293+
}
294+
}
295+
296+
private fun launchViaIntent(packageName: String, activityName: String): Boolean {
271297
return try {
272298
val intent = Intent().apply {
273299
component = ComponentName(packageName, activityName)
@@ -310,6 +336,28 @@ class NativeBridge @Inject constructor(
310336
}
311337
}
312338

339+
@JavascriptInterface
340+
fun clearCache(packageName: String): String {
341+
return runBlocking {
342+
val result = appManager.executeAction(packageName, AppAction.CLEAR_CACHE)
343+
if (result.success) {
344+
appScanner.invalidateCache()
345+
}
346+
json.encodeToString(result)
347+
}
348+
}
349+
350+
@JavascriptInterface
351+
fun clearData(packageName: String): String {
352+
return runBlocking {
353+
val result = appManager.executeAction(packageName, AppAction.CLEAR_DATA)
354+
if (result.success) {
355+
appScanner.invalidateCache()
356+
}
357+
json.encodeToString(result)
358+
}
359+
}
360+
313361
private fun sendCallback(callbackId: String, data: String) {
314362
val escapedData = data.replace("'", "\\'")
315363
mainHandler.post {

app/src/main/java/com/appcontrolx/domain/AppScanner.kt

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import android.os.Build
1111
import android.util.Base64
1212
import com.appcontrolx.core.ShellManager
1313
import com.appcontrolx.model.AppActivities
14+
import com.appcontrolx.model.AppActivityFilter
1415
import com.appcontrolx.model.AppInfo
1516
import com.appcontrolx.model.ExecutionMode
1617
import kotlinx.coroutines.Dispatchers
@@ -61,7 +62,11 @@ class AppScanner @Inject constructor(
6162
isBackgroundRestricted = false,
6263
size = getAppSize(appInfo),
6364
uid = appInfo.uid,
64-
safetyLevel = safetyValidator.getSafetyLevel(pkg.packageName)
65+
safetyLevel = safetyValidator.getSafetyLevel(pkg.packageName),
66+
installPath = appInfo.sourceDir,
67+
targetSdk = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) appInfo.targetSdkVersion else null,
68+
minSdk = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) appInfo.minSdkVersion else null,
69+
permissions = pkg.requestedPermissions?.size
6570
)
6671
} catch (e: Exception) {
6772
null
@@ -82,29 +87,78 @@ class AppScanner @Inject constructor(
8287
}
8388
}
8489

85-
suspend fun scanAppActivities(): List<AppActivities> = withContext(Dispatchers.IO) {
90+
suspend fun scanAppActivities(filter: AppActivityFilter = AppActivityFilter()): List<AppActivities> = withContext(Dispatchers.IO) {
8691
val packages = packageManager.getInstalledPackages(PackageManager.GET_ACTIVITIES)
8792
packages.mapNotNull { pkg ->
8893
try {
89-
val activities = pkg.activities?.map { it.name } ?: return@mapNotNull null
90-
if (activities.isEmpty()) return@mapNotNull null
91-
94+
// Filter by app type first
9295
val appInfo = pkg.applicationInfo
9396
val isSystemApp = (appInfo.flags and ApplicationInfo.FLAG_SYSTEM) != 0
9497

98+
when (filter.type) {
99+
"user" -> if (isSystemApp) return@mapNotNull null
100+
"system" -> if (!isSystemApp) return@mapNotNull null
101+
else -> {} // "all" - include everything
102+
}
103+
104+
// Filter by search query (app name or package name)
105+
if (filter.search.isNotEmpty()) {
106+
val appName = appInfo.loadLabel(packageManager).toString()
107+
val searchLower = filter.search.lowercase()
108+
val matchesApp = appName.lowercase().contains(searchLower) ||
109+
pkg.packageName.lowercase().contains(searchLower)
110+
111+
if (!matchesApp) {
112+
// Check if any activity matches
113+
val matchesActivity = pkg.activities?.any { activityInfo ->
114+
activityInfo.name.lowercase().contains(searchLower)
115+
} ?: false
116+
117+
if (!matchesActivity) return@mapNotNull null
118+
}
119+
}
120+
121+
val activities = pkg.activities?.mapNotNull { activityInfo ->
122+
try {
123+
// Filter hidden/test activities
124+
if (isHiddenActivity(activityInfo.name)) return@mapNotNull null
125+
126+
com.appcontrolx.model.ActivityInfo(
127+
activityName = activityInfo.name,
128+
isExported = activityInfo.exported,
129+
canLaunchWithoutRoot = activityInfo.exported,
130+
hasLauncherIntent = activityInfo.labelRes != 0
131+
)
132+
} catch (e: Exception) {
133+
null
134+
}
135+
} ?: return@mapNotNull null
136+
137+
if (activities.isEmpty()) return@mapNotNull null
138+
95139
AppActivities(
96140
packageName = pkg.packageName,
97141
appName = appInfo.loadLabel(packageManager).toString(),
98142
iconBase64 = getIconBase64(appInfo),
99143
isSystem = isSystemApp,
100-
activities = activities.sorted()
144+
activities = activities.sortedBy { it.activityName }
101145
)
102146
} catch (e: Exception) {
103147
null
104148
}
105149
}.sortedBy { it.appName.lowercase() }
106150
}
107151

152+
private fun isHiddenActivity(name: String): Boolean {
153+
val lowerName = name.lowercase()
154+
return lowerName.contains("test") ||
155+
lowerName.contains("debug") ||
156+
lowerName.contains("internal") ||
157+
lowerName.endsWith("receiver") ||
158+
lowerName.endsWith("service") ||
159+
lowerName.endsWith("provider")
160+
}
161+
108162
fun invalidateCache() {
109163
cachedApps = null
110164
cacheTimestamp = 0

app/src/main/java/com/appcontrolx/model/AppModels.kt

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,15 @@ data class AppActivities(
4545
val appName: String,
4646
val iconBase64: String?,
4747
val isSystem: Boolean,
48-
val activities: List<String>
48+
val activities: List<ActivityInfo>
49+
)
50+
51+
@Serializable
52+
data class ActivityInfo(
53+
val activityName: String,
54+
val isExported: Boolean,
55+
val canLaunchWithoutRoot: Boolean, // true if exported
56+
val hasLauncherIntent: Boolean // true if has label resource
4957
)
5058

5159
@Serializable
@@ -61,7 +69,11 @@ data class AppInfo(
6169
val isBackgroundRestricted: Boolean,
6270
val size: Long,
6371
val uid: Int,
64-
val safetyLevel: SafetyLevel
72+
val safetyLevel: SafetyLevel,
73+
val installPath: String? = null,
74+
val targetSdk: Int? = null,
75+
val minSdk: Int? = null,
76+
val permissions: Int? = null
6577
)
6678

6779
@Serializable
@@ -87,3 +99,9 @@ data class BatchComplete(
8799
val successCount: Int,
88100
val failureCount: Int
89101
)
102+
103+
@Serializable
104+
data class AppActivityFilter(
105+
val type: String = "all", // "all"|"user"|"system"
106+
val search: String = ""
107+
)

web/src/api/bridge.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import type {
77
RealtimeStatus,
88
ActionLog,
99
AppFilter,
10-
DeviceInfo
10+
DeviceInfo,
11+
AppActivityFilter,
12+
AppActivities
1113
} from './types'
1214

1315
// Check if running in development mode (browser)
@@ -243,9 +245,10 @@ export const bridge = {
243245
},
244246

245247
// Get all activities from installed apps
246-
getActivities: (): { packageName: string; appName: string; isSystem: boolean; activities: string[] }[] => {
248+
getActivities: (filter?: AppActivityFilter): AppActivities[] => {
247249
if (!isNativeBridgeAvailable()) return []
248-
return callNative(() => window.NativeBridge.getActivities())
250+
const filterJson = filter ? JSON.stringify(filter) : '{}'
251+
return callNative(() => window.NativeBridge.getActivities(filterJson))
249252
},
250253

251254
// Launch a specific activity
@@ -276,5 +279,35 @@ export const bridge = {
276279
} catch {
277280
return false
278281
}
282+
},
283+
284+
// Clear app cache
285+
clearCache: (packageName: string): ActionResult => {
286+
if (!isNativeBridgeAvailable()) {
287+
return {
288+
success: false,
289+
message: 'Native bridge not available',
290+
packageName,
291+
action: 'CLEAR_CACHE'
292+
}
293+
}
294+
return callNative<ActionResult>(() =>
295+
window.NativeBridge.clearCache(packageName)
296+
)
297+
},
298+
299+
// Clear app data
300+
clearData: (packageName: string): ActionResult => {
301+
if (!isNativeBridgeAvailable()) {
302+
return {
303+
success: false,
304+
message: 'Native bridge not available',
305+
packageName,
306+
action: 'CLEAR_DATA'
307+
}
308+
}
309+
return callNative<ActionResult>(() =>
310+
window.NativeBridge.clearData(packageName)
311+
)
279312
}
280313
}

web/src/api/types.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ export interface AppInfo {
2424
size: number
2525
uid: number
2626
safetyLevel: SafetyLevel
27+
installPath?: string
28+
targetSdk?: number
29+
minSdk?: number
30+
permissions?: number
31+
backgroundOps?: {
32+
runInBackground: string
33+
runAnyInBackground: string
34+
}
2735
}
2836

2937
export interface SystemStats {
@@ -150,3 +158,23 @@ export interface AppFilter {
150158
showDisabledOnly: boolean
151159
searchQuery: string
152160
}
161+
162+
export interface AppActivityFilter {
163+
type: 'all' | 'user' | 'system'
164+
search: string
165+
}
166+
167+
export interface ActivityInfo {
168+
activityName: string
169+
isExported: boolean
170+
canLaunchWithoutRoot: boolean
171+
hasLauncherIntent: boolean
172+
}
173+
174+
export interface AppActivities {
175+
packageName: string
176+
appName: string
177+
iconBase64?: string
178+
isSystem: boolean
179+
activities: ActivityInfo[]
180+
}

0 commit comments

Comments
 (0)