Skip to content

Commit 54a36f8

Browse files
committed
ad firebase
1 parent c4d0eca commit 54a36f8

39 files changed

Lines changed: 1731 additions & 1906 deletions

File tree

.idea/appInsightsSettings.xml

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/gradle.xml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/misc.xml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/build.gradle.kts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ plugins {
2121
val firebaseConfig = file("src/play/google-services.json")
2222
val hasFirebaseConfig = firebaseConfig.exists()
2323

24+
// The module root is also on the plugin's search path, and a file there applies to *every*
25+
// flavor — the `opensource` APK silently gains the Firebase app id and API key as string
26+
// resources. Android Studio's Firebase assistant puts one here given half a chance, and it is
27+
// gitignored, so nothing else would ever tell us. Fail loudly instead.
28+
val strayFirebaseConfig = file("google-services.json")
29+
require(!strayFirebaseConfig.exists()) {
30+
"$strayFirebaseConfig applies to every flavor, including the open-source build. " +
31+
"Move it to ${firebaseConfig} — that is the only location the `play` flavor reads."
32+
}
33+
2434
if (hasFirebaseConfig) {
2535
apply(plugin = "com.google.gms.google-services")
2636
apply(plugin = "com.google.firebase.crashlytics")

app/src/main/java/app/pwhs/universalinstaller/Application.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import app.pwhs.universalinstaller.di.appModule
77
import app.pwhs.universalinstaller.di.flavorModule
88
import app.pwhs.universalinstaller.presentation.install.controller.BackendSelfHeal
99
import app.pwhs.universalinstaller.presentation.install.controller.InstallerBackendFactory
10+
import app.pwhs.core.data.local.SharedPrefsKeys
11+
import app.pwhs.core.data.local.dataStore
1012
import app.pwhs.universalinstaller.telemetry.Telemetry
1113
import app.pwhs.universalinstaller.telemetry.createTelemetrySink
1214
import app.pwhs.universalinstaller.util.ApkFileIconFetcher
@@ -16,6 +18,7 @@ import com.topjohnwu.superuser.Shell
1618
import kotlinx.coroutines.CoroutineScope
1719
import kotlinx.coroutines.Dispatchers
1820
import kotlinx.coroutines.SupervisorJob
21+
import kotlinx.coroutines.flow.first
1922
import kotlinx.coroutines.launch
2023
import org.koin.android.ext.android.inject
2124
import org.koin.android.ext.koin.androidContext
@@ -101,10 +104,29 @@ class App : Application(), SingletonImageLoader.Factory {
101104
// Self-heal stale install-method prefs (Root revoked, Shizuku not running). Runs
102105
// once per process on a background dispatcher; never blocks app start.
103106
CoroutineScope(SupervisorJob() + Dispatchers.Default).launch {
107+
applyTelemetryPreference()
104108
BackendSelfHeal.runOnce(this@App, backendFactory)
105109
}
106110
}
107111

112+
/**
113+
* Hands the user's choice to the reporting sink at every start.
114+
*
115+
* Firebase remembers the flag on its own, so this is not what makes the setting stick — it
116+
* is what makes the *preference* authoritative, including after a restore onto a new device
117+
* where the preference travelled but Firebase's own state did not. Absent means on.
118+
*
119+
* Runs before [BackendSelfHeal] in the same coroutine deliberately: self-heal is the first
120+
* thing that reports, and it must not report on a build the user opted out of.
121+
*/
122+
private suspend fun applyTelemetryPreference() {
123+
if (!Telemetry.isCollecting) return
124+
val enabled = runCatching { dataStore.data.first()[SharedPrefsKeys.ANALYTICS_ENABLED] }
125+
.onFailure { Timber.w(it, "Could not read the analytics preference; leaving it on") }
126+
.getOrNull() ?: true
127+
Telemetry.setCollectionEnabled(enabled)
128+
}
129+
108130
override fun newImageLoader(context: android.content.Context): ImageLoader {
109131
return ImageLoader.Builder(context)
110132
.components {

app/src/main/java/app/pwhs/universalinstaller/MainActivity.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import androidx.compose.runtime.remember
2525
import androidx.compose.runtime.setValue
2626
import app.pwhs.core.domain.ThemeMode
2727
import app.pwhs.core.presentation.onboarding.OnboardingScreen
28+
import app.pwhs.universalinstaller.telemetry.Telemetry
2829
import app.pwhs.core.util.DeviceCompat
2930
import app.pwhs.core.data.local.dataStore
3031
import app.pwhs.universalinstaller.presentation.splash.SplashActivity
@@ -113,6 +114,9 @@ class MainActivity : ComponentActivity() {
113114
onFinish = { currentRoute = AppRoute.Main },
114115
showXiaomiTip = DeviceCompat.isXiaomi,
115116
showVirusTotalTip = true,
117+
// Only the build that has reporting asks about it. On `opensource`
118+
// there is no sink, so the page would be a question about nothing.
119+
showAnalyticsConsent = Telemetry.isCollecting,
116120
)
117121
AppRoute.Main -> {
118122
LaunchedEffect(Unit) {

app/src/main/java/app/pwhs/universalinstaller/presentation/install/InstallViewModel.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ import app.pwhs.universalinstaller.util.SignatureCheck
4444
import app.pwhs.universalinstaller.presentation.setting.PreferencesKeys
4545
import app.pwhs.core.util.RootShell
4646
import app.pwhs.core.data.local.dataStore
47+
import app.pwhs.universalinstaller.telemetry.Telemetry
48+
import app.pwhs.universalinstaller.telemetry.TelemetryEvents
4749
import app.pwhs.universalinstaller.util.extension.getDisplayName
4850
import kotlinx.coroutines.Dispatchers
4951
import kotlinx.coroutines.Job
@@ -324,6 +326,7 @@ class InstallViewModel(
324326
}
325327

326328
fun applyProfile(profile: InstallerProfile) {
329+
Telemetry.feature(TelemetryEvents.FEATURE_INSTALLER_PROFILE)
327330
_selectedProfileId.value = profile.id
328331
}
329332

@@ -763,6 +766,7 @@ class InstallViewModel(
763766
val zipTotal = entries.sumOf { it.sizeBytes.coerceAtLeast(0L) }
764767
val attachedTotal = attached.sumOf { it.sizeBytes.coerceAtLeast(0L) }
765768
val combinedTotal = zipTotal + attachedTotal
769+
Telemetry.feature(TelemetryEvents.FEATURE_OBB_COPY)
766770
_obbCopyState.value = ObbCopyState.Running(appName, packageName, 0L, combinedTotal)
767771
pendingObbCopyJob = ObbCopyJob(sourceUri, entries, attached, packageName, appName)
768772

@@ -1177,6 +1181,9 @@ class InstallViewModel(
11771181
val picked = ready.entries.filter { it.selected && it.splitUris.isNotEmpty() }
11781182
_batchState.value = BatchInstallState.Idle
11791183
if (picked.isEmpty()) return
1184+
// Reported once for the batch, not once per entry — the per-entry installs already
1185+
// arrive as their own install_started events from the controller.
1186+
Telemetry.feature(TelemetryEvents.FEATURE_BATCH_INSTALL)
11801187

11811188
viewModelScope.launch {
11821189
val deleteAfterInstall = readDeleteApkPref()
@@ -1359,6 +1366,7 @@ class InstallViewModel(
13591366
return
13601367
}
13611368
downloadJob?.cancel()
1369+
Telemetry.feature(TelemetryEvents.FEATURE_URL_DOWNLOAD)
13621370
val displayName = trimmed.substringAfterLast('/').substringBefore('?')
13631371
.ifBlank { "download_${System.currentTimeMillis()}" }
13641372
_downloadState.value = DownloadState.Running(url = trimmed, bytesRead = 0L, totalBytes = -1L)
@@ -1564,6 +1572,7 @@ class InstallViewModel(
15641572
val current = _pendingApkInfo.value ?: return
15651573

15661574
cancelActiveScan()
1575+
Telemetry.feature(TelemetryEvents.FEATURE_VIRUSTOTAL)
15671576
scanJob = viewModelScope.launch {
15681577
val apiKey = readVirusTotalApiKey()
15691578
val sizeBytes = current.fileSizeBytes

app/src/main/java/app/pwhs/universalinstaller/presentation/install/controller/BackendSelfHeal.kt

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import android.content.pm.PackageManager
55
import androidx.datastore.preferences.core.edit
66
import app.pwhs.universalinstaller.presentation.setting.PreferencesKeys
77
import app.pwhs.core.data.local.dataStore
8+
import app.pwhs.universalinstaller.telemetry.Telemetry
9+
import app.pwhs.universalinstaller.telemetry.TelemetryEvents
810
import kotlinx.coroutines.delay
911
import kotlinx.coroutines.flow.first
1012
import rikka.shizuku.Shizuku
@@ -45,7 +47,11 @@ object BackendSelfHeal {
4547
Timber.w(t, "BackendSelfHeal: root probe threw")
4648
RootState.UNAVAILABLE
4749
}
48-
if (state == RootState.UNAVAILABLE || state == RootState.NOT_ROOTED || state == RootState.DENIED) {
50+
val healthy = state != RootState.UNAVAILABLE &&
51+
state != RootState.NOT_ROOTED &&
52+
state != RootState.DENIED
53+
reportHealth("root", healthy)
54+
if (!healthy) {
4955
Timber.i("BackendSelfHeal: USE_ROOT on but root definitively unavailable ($state) — disabling pref")
5056
runCatching {
5157
application.dataStore.edit { it[PreferencesKeys.USE_ROOT] = false }
@@ -54,14 +60,31 @@ object BackendSelfHeal {
5460
}
5561

5662
val useShizuku = prefs[PreferencesKeys.USE_SHIZUKU] ?: false
57-
if (useShizuku && !awaitShizukuReady()) {
58-
Timber.i("BackendSelfHeal: USE_SHIZUKU on but Shizuku not ready — disabling pref")
59-
runCatching {
60-
application.dataStore.edit { it[PreferencesKeys.USE_SHIZUKU] = false }
63+
if (useShizuku) {
64+
val healthy = awaitShizukuReady()
65+
reportHealth("shizuku", healthy)
66+
if (!healthy) {
67+
Timber.i("BackendSelfHeal: USE_SHIZUKU on but Shizuku not ready — disabling pref")
68+
runCatching {
69+
application.dataStore.edit { it[PreferencesKeys.USE_SHIZUKU] = false }
70+
}
6171
}
6272
}
6373
}
6474

75+
/**
76+
* Only backends the user actually turned on are reported, so the denominator is "people who
77+
* chose this backend" — the population whose setup we can hope to fix. Cold starts where
78+
* nothing privileged is enabled say nothing and send nothing.
79+
*/
80+
private fun reportHealth(method: String, healthy: Boolean) {
81+
Telemetry.event(
82+
TelemetryEvents.BACKEND_HEALTH,
83+
TelemetryEvents.PARAM_METHOD to method,
84+
TelemetryEvents.PARAM_HEALTHY to healthy,
85+
)
86+
}
87+
6588
// Shizuku's binder is bound asynchronously after the app starts, so an immediate
6689
// pingBinder() right at process launch can return false even when Shizuku is healthy.
6790
// Poll briefly (~2s) to give the binder a chance to land before we conclude it's gone.

app/src/main/java/app/pwhs/universalinstaller/presentation/manage/ManageViewModel.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import app.pwhs.universalinstaller.presentation.install.controller.SystemAppMeth
2020
import app.pwhs.universalinstaller.presentation.setting.PreferencesKeys
2121
import app.pwhs.core.data.local.dataStore
2222
import app.pwhs.universalinstaller.domain.manager.InstallBlacklist
23+
import app.pwhs.universalinstaller.telemetry.Telemetry
24+
import app.pwhs.universalinstaller.telemetry.TelemetryEvents
2325
import androidx.datastore.preferences.core.edit
2426
import kotlinx.coroutines.Dispatchers
2527
import kotlinx.coroutines.flow.MutableStateFlow
@@ -346,6 +348,9 @@ class ManageViewModel(
346348
) {
347349
if (_extractState.value is ExtractState.Running) return
348350
if (_batchExtractState.value is BatchExtractState.Running) return
351+
// Only a real backup counts. Share / Server / Reinstall run the same extractor but are
352+
// steps inside other flows, not the feature a user would say they used.
353+
if (mode == ExtractMode.Backup) Telemetry.feature(TelemetryEvents.FEATURE_APK_BACKUP)
349354
extractJob?.cancel()
350355
_extractState.value = ExtractState.Running(packageName, appName, 0L, 1L, mode)
351356
extractJob = viewModelScope.launch {
@@ -983,6 +988,7 @@ class ManageViewModel(
983988
}
984989

985990
fun uninstallApp(packageName: String) {
991+
Telemetry.feature(TelemetryEvents.FEATURE_UNINSTALL)
986992
val app = _apps.value.firstOrNull { it.packageName == packageName }
987993
if (app != null && app.isSystemApp) {
988994
viewModelScope.launch {
@@ -1072,6 +1078,7 @@ class ManageViewModel(
10721078
}
10731079

10741080
private suspend fun runBatchUninstall(packages: List<String>) {
1081+
Telemetry.feature(TelemetryEvents.FEATURE_UNINSTALL)
10751082
val opts = readUninstallOptions()
10761083
val total = packages.size
10771084
val notifId = notifier.notifyBatchStart(total)

0 commit comments

Comments
 (0)