Skip to content

Commit 45783e9

Browse files
committed
Complete Phase 2-3: Room Database, DataStore, WorkManager, CI/CD Pipeline, Unit Tests, Accessibility, Documentation
1 parent da36987 commit 45783e9

File tree

11 files changed

+482
-4
lines changed

11 files changed

+482
-4
lines changed

.github/workflows/android.yml

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
name: Android CI
2+
3+
on:
4+
push:
5+
branches: [ main, develop ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
build:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- name: Checkout code
15+
uses: actions/checkout@v4
16+
17+
- name: Set up JDK 17
18+
uses: actions/setup-java@v4
19+
with:
20+
java-version: '17'
21+
distribution: 'temurin'
22+
cache: gradle
23+
24+
- name: Grant execute permission for gradlew
25+
run: chmod +x gradlew
26+
27+
- name: Build Debug APK
28+
run: ./gradlew assembleDebug --no-daemon
29+
30+
- name: Run Unit Tests
31+
run: ./gradlew test --no-daemon
32+
33+
- name: Upload Debug APK
34+
uses: actions/upload-artifact@v4
35+
with:
36+
name: app-debug
37+
path: app/build/outputs/apk/debug/app-debug.apk
38+
retention-days: 7
39+
40+
- name: Upload Test Results
41+
uses: actions/upload-artifact@v4
42+
if: always()
43+
with:
44+
name: test-results
45+
path: app/build/reports/tests/
46+
retention-days: 7
47+
48+
lint:
49+
runs-on: ubuntu-latest
50+
51+
steps:
52+
- name: Checkout code
53+
uses: actions/checkout@v4
54+
55+
- name: Set up JDK 17
56+
uses: actions/setup-java@v4
57+
with:
58+
java-version: '17'
59+
distribution: 'temurin'
60+
cache: gradle
61+
62+
- name: Grant execute permission for gradlew
63+
run: chmod +x gradlew
64+
65+
- name: Run Lint
66+
run: ./gradlew lint --no-daemon
67+
68+
- name: Upload Lint Results
69+
uses: actions/upload-artifact@v4
70+
if: always()
71+
with:
72+
name: lint-results
73+
path: app/build/reports/lint-results*.html
74+
retention-days: 7
75+
76+
release:
77+
needs: [build, lint]
78+
runs-on: ubuntu-latest
79+
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
80+
81+
steps:
82+
- name: Checkout code
83+
uses: actions/checkout@v4
84+
85+
- name: Set up JDK 17
86+
uses: actions/setup-java@v4
87+
with:
88+
java-version: '17'
89+
distribution: 'temurin'
90+
cache: gradle
91+
92+
- name: Grant execute permission for gradlew
93+
run: chmod +x gradlew
94+
95+
- name: Build Release APK
96+
run: ./gradlew assembleRelease --no-daemon
97+
98+
- name: Upload Release APK
99+
uses: actions/upload-artifact@v4
100+
with:
101+
name: app-release
102+
path: app/build/outputs/apk/release/app-release-unsigned.apk
103+
retention-days: 30

README.md

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,37 +57,58 @@ Output: `app/build/outputs/apk/debug/app-debug.apk`
5757

5858
## Architecture
5959

60+
The app follows **MVVM (Model-View-ViewModel)** architecture with **Repository pattern** and uses **Hilt** for dependency injection.
61+
6062
```
6163
com.appcontrolx/
64+
├── data/
65+
│ ├── local/ # Room Database, DataStore
66+
│ └── repository/ # Data repositories
67+
├── di/ # Hilt modules
6268
├── executor/ # Command execution (Root/Shizuku)
6369
├── model/ # Data classes
70+
├── presentation/
71+
│ └── viewmodel/ # ViewModels
6472
├── rollback/ # State management & rollback
6573
├── service/ # Business logic
6674
├── ui/ # Activities, Fragments, Adapters
67-
└── utils/ # Helpers & validators
75+
├── utils/ # Helpers & validators
76+
└── worker/ # WorkManager workers
6877
```
6978

7079
### Key Components
7180

7281
| Component | Description |
7382
|-----------|-------------|
83+
| `AppRepository` | Central data access layer |
84+
| `AppListViewModel` | UI state management for app list |
7485
| `PermissionBridge` | Detects available execution mode (Root/Shizuku/None) |
75-
| `RootExecutor` | Executes commands via libsu |
86+
| `RootExecutor` | Executes commands via libsu with security validation |
7687
| `ShizukuExecutor` | Executes commands via Shizuku UserService |
7788
| `BatteryPolicyManager` | Manages appops and battery settings |
78-
| `XiaomiBridge` | Handles MIUI/HyperOS specific features |
7989
| `RollbackManager` | Saves snapshots and restores previous state |
8090
| `SafetyValidator` | Prevents actions on critical system apps |
91+
| `SettingsDataStore` | Persistent settings with DataStore |
92+
| `AppDatabase` | Room database for action logs |
8193

8294
## Tech Stack
8395

84-
- **Language**: Kotlin
96+
- **Language**: Kotlin 1.9
97+
- **Architecture**: MVVM + Repository Pattern
98+
- **DI**: Hilt 2.50
8599
- **UI**: Material 3, ViewBinding
100+
- **Database**: Room 2.6
101+
- **Preferences**: DataStore
102+
- **Async**: Coroutines + Flow
86103
- **Navigation**: Jetpack Navigation Component
104+
- **Background**: WorkManager
87105
- **Root**: [libsu](https://github.com/topjohnwu/libsu) by topjohnwu
88106
- **Shizuku**: [Shizuku-API](https://github.com/RikkaApps/Shizuku-API) by RikkaApps
107+
- **Logging**: Timber
108+
- **Crash Reporting**: Firebase Crashlytics
89109
- **Build**: Gradle 8.2, AGP 8.2.0
90110
- **CI/CD**: GitHub Actions
111+
- **Testing**: JUnit, Mockito, Turbine
91112

92113
## Commands Reference
93114

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.appcontrolx.data.local
2+
3+
import androidx.room.Database
4+
import androidx.room.RoomDatabase
5+
import com.appcontrolx.data.local.dao.ActionLogDao
6+
import com.appcontrolx.data.local.entity.ActionLogEntity
7+
8+
@Database(
9+
entities = [ActionLogEntity::class],
10+
version = 1,
11+
exportSchema = false
12+
)
13+
abstract class AppDatabase : RoomDatabase() {
14+
abstract fun actionLogDao(): ActionLogDao
15+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package com.appcontrolx.data.local
2+
3+
import android.content.Context
4+
import androidx.datastore.core.DataStore
5+
import androidx.datastore.preferences.core.Preferences
6+
import androidx.datastore.preferences.core.booleanPreferencesKey
7+
import androidx.datastore.preferences.core.edit
8+
import androidx.datastore.preferences.core.intPreferencesKey
9+
import androidx.datastore.preferences.core.stringPreferencesKey
10+
import androidx.datastore.preferences.preferencesDataStore
11+
import com.appcontrolx.utils.Constants
12+
import kotlinx.coroutines.flow.Flow
13+
import kotlinx.coroutines.flow.map
14+
import javax.inject.Inject
15+
import javax.inject.Singleton
16+
17+
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
18+
19+
@Singleton
20+
class SettingsDataStore @Inject constructor(
21+
private val context: Context
22+
) {
23+
private object Keys {
24+
val EXECUTION_MODE = stringPreferencesKey(Constants.PREFS_EXECUTION_MODE)
25+
val THEME = intPreferencesKey(Constants.PREFS_THEME)
26+
val CONFIRM_ACTIONS = booleanPreferencesKey(Constants.PREFS_CONFIRM_ACTIONS)
27+
val PROTECT_SYSTEM = booleanPreferencesKey(Constants.PREFS_PROTECT_SYSTEM)
28+
val AUTO_SNAPSHOT = booleanPreferencesKey(Constants.PREFS_AUTO_SNAPSHOT)
29+
val SETUP_COMPLETE = booleanPreferencesKey(Constants.PREFS_SETUP_COMPLETE)
30+
val LAST_VERSION = intPreferencesKey("last_shown_version")
31+
}
32+
33+
val executionMode: Flow<String> = context.dataStore.data
34+
.map { it[Keys.EXECUTION_MODE] ?: Constants.MODE_NONE }
35+
36+
val theme: Flow<Int> = context.dataStore.data
37+
.map { it[Keys.THEME] ?: -1 }
38+
39+
val confirmActions: Flow<Boolean> = context.dataStore.data
40+
.map { it[Keys.CONFIRM_ACTIONS] ?: true }
41+
42+
val protectSystem: Flow<Boolean> = context.dataStore.data
43+
.map { it[Keys.PROTECT_SYSTEM] ?: true }
44+
45+
val autoSnapshot: Flow<Boolean> = context.dataStore.data
46+
.map { it[Keys.AUTO_SNAPSHOT] ?: true }
47+
48+
val setupComplete: Flow<Boolean> = context.dataStore.data
49+
.map { it[Keys.SETUP_COMPLETE] ?: false }
50+
51+
val lastVersion: Flow<Int> = context.dataStore.data
52+
.map { it[Keys.LAST_VERSION] ?: 0 }
53+
54+
suspend fun setExecutionMode(mode: String) {
55+
context.dataStore.edit { it[Keys.EXECUTION_MODE] = mode }
56+
}
57+
58+
suspend fun setTheme(theme: Int) {
59+
context.dataStore.edit { it[Keys.THEME] = theme }
60+
}
61+
62+
suspend fun setConfirmActions(confirm: Boolean) {
63+
context.dataStore.edit { it[Keys.CONFIRM_ACTIONS] = confirm }
64+
}
65+
66+
suspend fun setProtectSystem(protect: Boolean) {
67+
context.dataStore.edit { it[Keys.PROTECT_SYSTEM] = protect }
68+
}
69+
70+
suspend fun setAutoSnapshot(auto: Boolean) {
71+
context.dataStore.edit { it[Keys.AUTO_SNAPSHOT] = auto }
72+
}
73+
74+
suspend fun setSetupComplete(complete: Boolean) {
75+
context.dataStore.edit { it[Keys.SETUP_COMPLETE] = complete }
76+
}
77+
78+
suspend fun setLastVersion(version: Int) {
79+
context.dataStore.edit { it[Keys.LAST_VERSION] = version }
80+
}
81+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.appcontrolx.data.local.dao
2+
3+
import androidx.room.Dao
4+
import androidx.room.Insert
5+
import androidx.room.OnConflictStrategy
6+
import androidx.room.Query
7+
import com.appcontrolx.data.local.entity.ActionLogEntity
8+
import kotlinx.coroutines.flow.Flow
9+
10+
@Dao
11+
interface ActionLogDao {
12+
13+
@Query("SELECT * FROM action_logs ORDER BY timestamp DESC")
14+
fun getAllLogs(): Flow<List<ActionLogEntity>>
15+
16+
@Query("SELECT * FROM action_logs ORDER BY timestamp DESC LIMIT :limit")
17+
fun getRecentLogs(limit: Int): Flow<List<ActionLogEntity>>
18+
19+
@Query("SELECT COUNT(*) FROM action_logs")
20+
suspend fun getLogCount(): Int
21+
22+
@Insert(onConflict = OnConflictStrategy.REPLACE)
23+
suspend fun insertLog(log: ActionLogEntity)
24+
25+
@Query("DELETE FROM action_logs")
26+
suspend fun clearAllLogs()
27+
28+
@Query("DELETE FROM action_logs WHERE id = :id")
29+
suspend fun deleteLog(id: Long)
30+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.appcontrolx.data.local.entity
2+
3+
import androidx.room.Entity
4+
import androidx.room.PrimaryKey
5+
6+
@Entity(tableName = "action_logs")
7+
data class ActionLogEntity(
8+
@PrimaryKey(autoGenerate = true)
9+
val id: Long = 0,
10+
val action: String,
11+
val packages: String, // JSON array of package names
12+
val timestamp: Long,
13+
val success: Boolean,
14+
val message: String? = null
15+
)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.appcontrolx.di
2+
3+
import android.content.Context
4+
import androidx.room.Room
5+
import com.appcontrolx.data.local.AppDatabase
6+
import com.appcontrolx.data.local.SettingsDataStore
7+
import com.appcontrolx.data.local.dao.ActionLogDao
8+
import dagger.Module
9+
import dagger.Provides
10+
import dagger.hilt.InstallIn
11+
import dagger.hilt.android.qualifiers.ApplicationContext
12+
import dagger.hilt.components.SingletonComponent
13+
import javax.inject.Singleton
14+
15+
@Module
16+
@InstallIn(SingletonComponent::class)
17+
object DatabaseModule {
18+
19+
@Provides
20+
@Singleton
21+
fun provideAppDatabase(
22+
@ApplicationContext context: Context
23+
): AppDatabase {
24+
return Room.databaseBuilder(
25+
context,
26+
AppDatabase::class.java,
27+
"appcontrolx_db"
28+
).fallbackToDestructiveMigration()
29+
.build()
30+
}
31+
32+
@Provides
33+
@Singleton
34+
fun provideActionLogDao(database: AppDatabase): ActionLogDao {
35+
return database.actionLogDao()
36+
}
37+
38+
@Provides
39+
@Singleton
40+
fun provideSettingsDataStore(
41+
@ApplicationContext context: Context
42+
): SettingsDataStore {
43+
return SettingsDataStore(context)
44+
}
45+
}

0 commit comments

Comments
 (0)