Skip to content

Commit c38a61f

Browse files
committed
Phase 1 Part 1: Add Hilt DI, MVVM architecture, Timber logging, Firebase, ProGuard optimization, and unit tests
1 parent 23bb8de commit c38a61f

File tree

12 files changed

+625
-4
lines changed

12 files changed

+625
-4
lines changed

app/build.gradle.kts

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
plugins {
22
id("com.android.application")
33
id("org.jetbrains.kotlin.android")
4+
id("com.google.dagger.hilt.android")
5+
id("com.google.devtools.ksp")
6+
id("com.google.gms.google-services")
7+
id("com.google.firebase.crashlytics")
48
}
59

610
android {
@@ -17,8 +21,24 @@ android {
1721

1822
buildTypes {
1923
release {
20-
isMinifyEnabled = false
21-
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
24+
isMinifyEnabled = true
25+
isShrinkResources = true
26+
proguardFiles(
27+
getDefaultProguardFile("proguard-android-optimize.txt"),
28+
"proguard-rules.pro"
29+
)
30+
}
31+
32+
debug {
33+
applicationIdSuffix = ".debug"
34+
versionNameSuffix = "-DEBUG"
35+
}
36+
}
37+
38+
testOptions {
39+
unitTests {
40+
isIncludeAndroidResources = true
41+
isReturnDefaultValues = true
2242
}
2343
}
2444

@@ -75,4 +95,40 @@ dependencies {
7595

7696
// Coroutines
7797
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
98+
99+
// Hilt - Dependency Injection
100+
implementation("com.google.dagger:hilt-android:2.50")
101+
ksp("com.google.dagger:hilt-compiler:2.50")
102+
103+
// Room Database
104+
implementation("androidx.room:room-runtime:2.6.1")
105+
implementation("androidx.room:room-ktx:2.6.1")
106+
ksp("androidx.room:room-compiler:2.6.1")
107+
108+
// DataStore
109+
implementation("androidx.datastore:datastore-preferences:1.0.0")
110+
111+
// WorkManager
112+
implementation("androidx.work:work-runtime-ktx:2.9.0")
113+
114+
// Firebase
115+
implementation(platform("com.google.firebase:firebase-bom:32.7.0"))
116+
implementation("com.google.firebase:firebase-crashlytics-ktx")
117+
implementation("com.google.firebase:firebase-analytics-ktx")
118+
119+
// Logging - Timber
120+
implementation("com.jakewharton.timber:timber:5.0.1")
121+
122+
// Testing
123+
testImplementation("junit:junit:4.13.2")
124+
testImplementation("org.mockito:mockito-core:5.7.0")
125+
testImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1")
126+
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
127+
testImplementation("androidx.arch.core:core-testing:2.2.0")
128+
testImplementation("app.cash.turbine:turbine:1.0.0")
129+
130+
androidTestImplementation("androidx.test.ext:junit:1.1.5")
131+
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
132+
androidTestImplementation("com.google.dagger:hilt-android-testing:2.50")
133+
kspAndroidTest("com.google.dagger:hilt-compiler:2.50")
78134
}

app/google-services.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"project_info": {
3+
"project_number": "123456789",
4+
"project_id": "appcontrolx-placeholder",
5+
"storage_bucket": "appcontrolx-placeholder.appspot.com"
6+
},
7+
"client": [
8+
{
9+
"client_info": {
10+
"mobilesdk_app_id": "1:123456789:android:abcdef123456",
11+
"android_client_info": {
12+
"package_name": "com.appcontrolx"
13+
}
14+
},
15+
"oauth_client": [],
16+
"api_key": [
17+
{
18+
"current_key": "AIzaSyDummyKeyForPlaceholder123456789"
19+
}
20+
],
21+
"services": {
22+
"appinvite_service": {
23+
"other_platform_oauth_client": []
24+
}
25+
}
26+
}
27+
],
28+
"configuration_version": "1"
29+
}

app/proguard-rules.pro

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# AppControlX ProGuard Rules
22

3+
# Keep attributes for debugging
4+
-keepattributes *Annotation*
5+
-keepattributes SourceFile,LineNumberTable
6+
-keep public class * extends java.lang.Exception
7+
38
# Keep Shizuku
49
-keep class rikka.shizuku.** { *; }
510
-keep class moe.shizuku.** { *; }
@@ -10,3 +15,61 @@
1015
# Keep AIDL
1116
-keep class com.appcontrolx.IShellService { *; }
1217
-keep class com.appcontrolx.IShellService$* { *; }
18+
19+
# Keep Models
20+
-keep class com.appcontrolx.model.** { *; }
21+
-keep class com.appcontrolx.data.local.entity.** { *; }
22+
23+
# Keep ViewModels
24+
-keep class * extends androidx.lifecycle.ViewModel {
25+
<init>();
26+
}
27+
-keep class * extends androidx.lifecycle.AndroidViewModel {
28+
<init>(android.app.Application);
29+
}
30+
31+
# Hilt
32+
-dontwarn com.google.errorprone.annotations.**
33+
-keep class dagger.hilt.** { *; }
34+
-keep class javax.inject.** { *; }
35+
-keep class * extends dagger.hilt.android.internal.managers.ViewComponentManager$FragmentContextWrapper { *; }
36+
37+
# Room
38+
-keep class * extends androidx.room.RoomDatabase
39+
-keep @androidx.room.Entity class *
40+
-dontwarn androidx.room.paging.**
41+
42+
# Gson
43+
-keepattributes Signature
44+
-keepattributes *Annotation*
45+
-dontwarn sun.misc.**
46+
-keep class com.google.gson.** { *; }
47+
-keep class * implements com.google.gson.TypeAdapter
48+
-keep class * implements com.google.gson.TypeAdapterFactory
49+
-keep class * implements com.google.gson.JsonSerializer
50+
-keep class * implements com.google.gson.JsonDeserializer
51+
52+
# Crashlytics
53+
-keepattributes SourceFile,LineNumberTable
54+
-keep public class * extends java.lang.Exception
55+
-keep class com.google.firebase.crashlytics.** { *; }
56+
-dontwarn com.google.firebase.crashlytics.**
57+
58+
# Coroutines
59+
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
60+
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
61+
-keepclassmembernames class kotlinx.** {
62+
volatile <fields>;
63+
}
64+
65+
# Remove logging in release
66+
-assumenosideeffects class android.util.Log {
67+
public static *** d(...);
68+
public static *** v(...);
69+
public static *** i(...);
70+
}
71+
-assumenosideeffects class timber.log.Timber {
72+
public static *** d(...);
73+
public static *** v(...);
74+
public static *** i(...);
75+
}

app/src/main/AndroidManifest.xml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
android:label="@string/app_name"
1717
android:roundIcon="@mipmap/ic_launcher_round"
1818
android:supportsRtl="true"
19-
android:theme="@style/Theme.AppControlX">
19+
android:theme="@style/Theme.AppControlX"
20+
android:fullBackupContent="@xml/backup_rules"
21+
android:dataExtractionRules="@xml/data_extraction_rules">
2022

2123
<!-- Setup Activity (Launcher) -->
2224
<activity

app/src/main/java/com/appcontrolx/App.kt

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,21 @@ package com.appcontrolx
22

33
import android.app.Application
44
import androidx.appcompat.app.AppCompatDelegate
5+
import androidx.hilt.work.HiltWorkerFactory
56
import androidx.preference.PreferenceManager
7+
import androidx.work.Configuration
68
import com.appcontrolx.utils.Constants
9+
import com.google.firebase.crashlytics.FirebaseCrashlytics
710
import com.topjohnwu.superuser.Shell
11+
import dagger.hilt.android.HiltAndroidApp
12+
import timber.log.Timber
13+
import javax.inject.Inject
814

9-
class App : Application() {
15+
@HiltAndroidApp
16+
class App : Application(), Configuration.Provider {
17+
18+
@Inject
19+
lateinit var workerFactory: HiltWorkerFactory
1020

1121
companion object {
1222
init {
@@ -21,12 +31,50 @@ class App : Application() {
2131

2232
override fun onCreate() {
2333
super.onCreate()
34+
initTimber()
35+
initCrashlytics()
2436
applyTheme()
2537
}
2638

39+
private fun initTimber() {
40+
if (BuildConfig.DEBUG) {
41+
Timber.plant(Timber.DebugTree())
42+
} else {
43+
Timber.plant(CrashlyticsTree())
44+
}
45+
Timber.d("App initialized")
46+
}
47+
48+
private fun initCrashlytics() {
49+
FirebaseCrashlytics.getInstance().apply {
50+
setCrashlyticsCollectionEnabled(!BuildConfig.DEBUG)
51+
setCustomKey("app_version", BuildConfig.VERSION_NAME)
52+
setCustomKey("version_code", BuildConfig.VERSION_CODE)
53+
}
54+
}
55+
2756
private fun applyTheme() {
2857
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
2958
val theme = prefs.getInt(Constants.PREFS_THEME, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
3059
AppCompatDelegate.setDefaultNightMode(theme)
3160
}
61+
62+
override val workManagerConfiguration: Configuration
63+
get() = Configuration.Builder()
64+
.setWorkerFactory(workerFactory)
65+
.build()
66+
67+
/**
68+
* Custom Timber tree that logs to Crashlytics in production
69+
*/
70+
private class CrashlyticsTree : Timber.Tree() {
71+
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
72+
if (priority == android.util.Log.ERROR || priority == android.util.Log.WARN) {
73+
FirebaseCrashlytics.getInstance().apply {
74+
log("$tag: $message")
75+
t?.let { recordException(it) }
76+
}
77+
}
78+
}
79+
}
3280
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package com.appcontrolx.data.repository
2+
3+
import com.appcontrolx.model.AppInfo
4+
import com.appcontrolx.service.AppFetcher
5+
import com.appcontrolx.service.BatteryPolicyManager
6+
import kotlinx.coroutines.flow.Flow
7+
import kotlinx.coroutines.flow.flow
8+
import javax.inject.Inject
9+
import javax.inject.Singleton
10+
11+
interface AppRepository {
12+
fun getAllApps(): Flow<Result<List<AppInfo>>>
13+
fun getUserApps(): Flow<Result<List<AppInfo>>>
14+
fun getSystemApps(): Flow<Result<List<AppInfo>>>
15+
suspend fun freezeApp(packageName: String): Result<Unit>
16+
suspend fun unfreezeApp(packageName: String): Result<Unit>
17+
suspend fun uninstallApp(packageName: String): Result<Unit>
18+
suspend fun forceStopApp(packageName: String): Result<Unit>
19+
suspend fun restrictBackground(packageName: String): Result<Unit>
20+
suspend fun allowBackground(packageName: String): Result<Unit>
21+
suspend fun clearCache(packageName: String): Result<Unit>
22+
suspend fun clearData(packageName: String): Result<Unit>
23+
}
24+
25+
@Singleton
26+
class AppRepositoryImpl @Inject constructor(
27+
private val appFetcher: AppFetcher,
28+
private val policyManager: BatteryPolicyManager?
29+
) : AppRepository {
30+
31+
override fun getAllApps(): Flow<Result<List<AppInfo>>> = flow {
32+
try {
33+
val apps = appFetcher.getAllApps()
34+
emit(Result.success(apps))
35+
} catch (e: Exception) {
36+
emit(Result.failure(e))
37+
}
38+
}
39+
40+
override fun getUserApps(): Flow<Result<List<AppInfo>>> = flow {
41+
try {
42+
val apps = appFetcher.getUserApps()
43+
emit(Result.success(apps))
44+
} catch (e: Exception) {
45+
emit(Result.failure(e))
46+
}
47+
}
48+
49+
override fun getSystemApps(): Flow<Result<List<AppInfo>>> = flow {
50+
try {
51+
val apps = appFetcher.getSystemApps()
52+
emit(Result.success(apps))
53+
} catch (e: Exception) {
54+
emit(Result.failure(e))
55+
}
56+
}
57+
58+
override suspend fun freezeApp(packageName: String): Result<Unit> {
59+
return policyManager?.freezeApp(packageName)
60+
?: Result.failure(Exception("Policy manager not available"))
61+
}
62+
63+
override suspend fun unfreezeApp(packageName: String): Result<Unit> {
64+
return policyManager?.unfreezeApp(packageName)
65+
?: Result.failure(Exception("Policy manager not available"))
66+
}
67+
68+
override suspend fun uninstallApp(packageName: String): Result<Unit> {
69+
return policyManager?.uninstallApp(packageName)
70+
?: Result.failure(Exception("Policy manager not available"))
71+
}
72+
73+
override suspend fun forceStopApp(packageName: String): Result<Unit> {
74+
return policyManager?.forceStop(packageName)
75+
?: Result.failure(Exception("Policy manager not available"))
76+
}
77+
78+
override suspend fun restrictBackground(packageName: String): Result<Unit> {
79+
return policyManager?.restrictBackground(packageName)
80+
?: Result.failure(Exception("Policy manager not available"))
81+
}
82+
83+
override suspend fun allowBackground(packageName: String): Result<Unit> {
84+
return policyManager?.allowBackground(packageName)
85+
?: Result.failure(Exception("Policy manager not available"))
86+
}
87+
88+
override suspend fun clearCache(packageName: String): Result<Unit> {
89+
return policyManager?.clearCache(packageName)
90+
?: Result.failure(Exception("Policy manager not available"))
91+
}
92+
93+
override suspend fun clearData(packageName: String): Result<Unit> {
94+
return policyManager?.clearData(packageName)
95+
?: Result.failure(Exception("Policy manager not available"))
96+
}
97+
}

0 commit comments

Comments
 (0)