Add option to opt out of Analytics and Crashlytics (#1237)

(cherry picked from commit 7c7af72f8cb12decc06b76c36852dcc54696236d)

# Conflicts:
#	app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSecurityScreen.kt
#	app/src/standard/AndroidManifest.xml
This commit is contained in:
Roshan Varughese 2024-10-12 13:46:28 +13:00 committed by Jobobby04
parent f6d2d0bd48
commit 2bd9a914c1
9 changed files with 276 additions and 132 deletions

View File

@ -0,0 +1,9 @@
package mihon.core.firebase
import android.content.Context
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
import kotlinx.coroutines.CoroutineScope
object Firebase {
fun setup(context: Context, preference: PrivacyPreferences, scope: CoroutineScope) = Unit
}

View File

@ -268,6 +268,14 @@
android:name="android.webkit.WebView.MetricsOptOut"
android:value="true" />
<!-- Disable for manual opt-in -->
<meta-data
android:name="firebase_analytics_collection_enabled"
android:value="false" />
<meta-data
android:name="firebase_crashlytics_collection_enabled"
android:value="false" />
<!-- Disable advertising ID collection for Firebase -->
<meta-data
android:name="google_analytics_adid_collection_enabled"

View File

@ -14,11 +14,13 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
@ -34,13 +36,18 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.LocalLifecycleOwner
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState
import tachiyomi.presentation.core.util.secondaryItemAlpha
import uy.kohesive.injekt.injectLazy
internal class PermissionStep : OnboardingStep {
private val privacyPreferences: PrivacyPreferences by injectLazy()
private var notificationGranted by mutableStateOf(false)
private var batteryGranted by mutableStateOf(false)
@ -73,7 +80,7 @@ internal class PermissionStep : OnboardingStep {
}
Column {
PermissionItem(
PermissionCheckbox(
title = stringResource(MR.strings.onboarding_permission_install_apps),
subtitle = stringResource(MR.strings.onboarding_permission_install_apps_description),
granted = installGranted,
@ -89,7 +96,7 @@ internal class PermissionStep : OnboardingStep {
// no-op. resulting checks is being done on resume
},
)
PermissionItem(
PermissionCheckbox(
title = stringResource(MR.strings.onboarding_permission_notifications),
subtitle = stringResource(MR.strings.onboarding_permission_notifications_description),
granted = notificationGranted,
@ -97,7 +104,7 @@ internal class PermissionStep : OnboardingStep {
)
}
PermissionItem(
PermissionCheckbox(
title = stringResource(MR.strings.onboarding_permission_ignore_battery_opts),
subtitle = stringResource(MR.strings.onboarding_permission_ignore_battery_opts_description),
granted = batteryGranted,
@ -109,6 +116,29 @@ internal class PermissionStep : OnboardingStep {
context.startActivity(intent)
},
)
HorizontalDivider(
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
color = MaterialTheme.colorScheme.onPrimaryContainer,
)
val crashlyticsPref = privacyPreferences.crashlytics()
val crashlytics by crashlyticsPref.collectAsState()
PermissionSwitch(
title = stringResource(MR.strings.onboarding_permission_crashlytics),
subtitle = stringResource(MR.strings.onboarding_permission_crashlytics_description),
granted = crashlytics,
onToggleChange = crashlyticsPref::set,
)
val analyticsPref = privacyPreferences.analytics()
val analytics by analyticsPref.collectAsState()
PermissionSwitch(
title = stringResource(MR.strings.onboarding_permission_analytics),
subtitle = stringResource(MR.strings.onboarding_permission_analytics_description),
granted = analytics,
onToggleChange = analyticsPref::set,
)
}
}
@ -127,7 +157,7 @@ internal class PermissionStep : OnboardingStep {
}
@Composable
private fun PermissionItem(
private fun PermissionCheckbox(
title: String,
subtitle: String,
granted: Boolean,
@ -157,4 +187,26 @@ internal class PermissionStep : OnboardingStep {
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
)
}
@Composable
private fun PermissionSwitch(
title: String,
subtitle: String,
granted: Boolean,
modifier: Modifier = Modifier,
onToggleChange: (Boolean) -> Unit,
) {
ListItem(
modifier = modifier,
headlineContent = { Text(text = title) },
supportingContent = { Text(text = subtitle) },
trailingContent = {
Switch(
checked = granted,
onCheckedChange = onToggleChange,
)
},
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
)
}
}

View File

@ -45,6 +45,7 @@ import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import dev.icerock.moko.resources.StringResource
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.ui.base.delegate.SecureActivityDelegate
import eu.kanade.tachiyomi.ui.category.biometric.BiometricTimesScreen
@ -70,10 +71,20 @@ object SettingsSecurityScreen : SearchableSettings {
@Composable
override fun getPreferences(): List<Preference> {
val context = LocalContext.current
val securityPreferences = remember { Injekt.get<SecurityPreferences>() }
val authSupported = remember { context.isAuthenticationSupported() }
val privacyPreferences = remember { Injekt.get<PrivacyPreferences>() }
return listOf(
getSecurityGroup(securityPreferences),
getFirebaseGroup(privacyPreferences),
)
}
@Composable
private fun getSecurityGroup(
securityPreferences: SecurityPreferences,
): Preference.PreferenceGroup {
val context = LocalContext.current
val authSupported = remember { context.isAuthenticationSupported() }
val useAuthPref = securityPreferences.useAuthenticator()
val useAuth by useAuthPref.collectAsState()
@ -81,7 +92,9 @@ object SettingsSecurityScreen : SearchableSettings {
val isCbzPasswordSet by remember { CbzCrypto.isPasswordSetState(scope) }.collectAsState()
val passwordProtectDownloads by securityPreferences.passwordProtectDownloads().collectAsState()
return listOf(
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_security),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = useAuthPref,
title = stringResource(MR.strings.lock_with_biometrics),
@ -205,6 +218,7 @@ object SettingsSecurityScreen : SearchableSettings {
// SY <--
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.secure_screen_summary)),
)
)
}
// SY -->
@ -361,6 +375,28 @@ object SettingsSecurityScreen : SearchableSettings {
)
}
// SY <--
@Composable
private fun getFirebaseGroup(
privacyPreferences: PrivacyPreferences,
): Preference.PreferenceGroup {
return Preference.PreferenceGroup(
title = stringResource(MR.strings.pref_firebase),
preferenceItems = persistentListOf(
Preference.PreferenceItem.SwitchPreference(
pref = privacyPreferences.crashlytics(),
title = stringResource(MR.strings.onboarding_permission_crashlytics),
subtitle = stringResource(MR.strings.onboarding_permission_crashlytics_description),
),
Preference.PreferenceItem.SwitchPreference(
pref = privacyPreferences.analytics(),
title = stringResource(MR.strings.onboarding_permission_analytics),
subtitle = stringResource(MR.strings.onboarding_permission_analytics_description),
),
Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.firebase_summary)),
),
)
}
}
private val LockAfterValues = persistentListOf(

View File

@ -28,14 +28,13 @@ import com.elvishew.xlog.printer.AndroidPrinter
import com.elvishew.xlog.printer.Printer
import com.elvishew.xlog.printer.file.backup.NeverBackupStrategy
import com.elvishew.xlog.printer.file.naming.DateFileNameGenerator
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase
import eu.kanade.domain.DomainModule
import eu.kanade.domain.SYDomainModule
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.sync.SyncPreferences
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.domain.ui.model.setAppCompatDelegateThemeMode
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
import eu.kanade.tachiyomi.crash.CrashActivity
import eu.kanade.tachiyomi.crash.GlobalExceptionHandler
import eu.kanade.tachiyomi.data.coil.BufferedSourceFetcher
@ -60,8 +59,6 @@ import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.animatorDurationScale
import eu.kanade.tachiyomi.util.system.cancelNotification
import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
import eu.kanade.tachiyomi.util.system.notify
import exh.log.CrashlyticsPrinter
import exh.log.EHLogLevel
@ -74,6 +71,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import logcat.LogPriority
import logcat.LogcatLogger
import mihon.core.firebase.Firebase
import mihon.core.migration.Migrator
import mihon.core.migration.migrations.migrations
import org.conscrypt.Conscrypt
@ -94,6 +92,7 @@ import java.util.Locale
class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factory {
private val basePreferences: BasePreferences by injectLazy()
private val privacyPreferences: PrivacyPreferences by injectLazy()
private val networkPreferences: NetworkPreferences by injectLazy()
private val disableIncognitoReceiver = DisableIncognitoReceiver()
@ -102,11 +101,6 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
override fun onCreate() {
super<Application>.onCreate()
// SY -->
if (!isDevFlavor) {
Firebase.crashlytics.setCrashlyticsCollectionEnabled(isReleaseBuildType)
}
// SY <--
GlobalExceptionHandler.initialize(applicationContext, CrashActivity::class.java)
// TLS 1.3 support for Android < 10
@ -133,6 +127,8 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
setupExhLogging() // EXH logging
LogcatLogger.install(XLogLogcatLogger()) // SY Redirect Logcat to XLog
Firebase.setup(applicationContext, privacyPreferences, ProcessLifecycleOwner.get().lifecycleScope)
setupNotificationChannels()
ProcessLifecycleOwner.get().lifecycle.addObserver(this)

View File

@ -6,6 +6,7 @@ import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.sync.SyncPreferences
import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
@ -37,6 +38,9 @@ class PreferenceModule(val app: Application) : InjektModule {
addSingletonFactory {
SecurityPreferences(get())
}
addSingletonFactory {
PrivacyPreferences(get())
}
addSingletonFactory {
LibraryPreferences(get())
}

View File

@ -0,0 +1,20 @@
package mihon.core.firebase
import android.content.Context
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.crashlytics.FirebaseCrashlytics
import eu.kanade.tachiyomi.core.security.PrivacyPreferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
object Firebase {
fun setup(context: Context, preference: PrivacyPreferences, scope: CoroutineScope) {
preference.analytics().changes().onEach { enabled ->
FirebaseAnalytics.getInstance(context).setAnalyticsCollectionEnabled(enabled)
}.launchIn(scope)
preference.crashlytics().changes().onEach { enabled ->
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(enabled)
}.launchIn(scope)
}
}

View File

@ -0,0 +1,11 @@
package eu.kanade.tachiyomi.core.security
import tachiyomi.core.common.preference.PreferenceStore
class PrivacyPreferences(
private val preferenceStore: PreferenceStore,
) {
fun crashlytics() = preferenceStore.getBoolean("crashlytics", true)
fun analytics() = preferenceStore.getBoolean("analytics", true)
}

View File

@ -190,6 +190,10 @@
<string name="onboarding_permission_notifications_description">Get notified for library updates and more.</string>
<string name="onboarding_permission_ignore_battery_opts">Background battery usage</string>
<string name="onboarding_permission_ignore_battery_opts_description">Avoid interruptions to long-running library updates, downloads, and backup restores.</string>
<string name="onboarding_permission_crashlytics">Send crash logs</string>
<string name="onboarding_permission_crashlytics_description">Send anonymized crash logs to the developers.</string>
<string name="onboarding_permission_analytics">Allow analytics</string>
<string name="onboarding_permission_analytics_description">Send anonymized usage data to improve app features.</string>
<string name="onboarding_permission_action_grant">Grant</string>
<string name="onboarding_guides_new_user">New to %s? We recommend checking out the getting started guide.</string>
<string name="onboarding_guides_returning_user">Reinstalling %s?</string>
@ -242,6 +246,9 @@
<string name="pref_app_language">App language</string>
<string name="pref_category_security">Security and privacy</string>
<string name="pref_security">Security</string>
<string name="pref_firebase">Analytics and Crash logs</string>
<string name="lock_with_biometrics">Require unlock</string>
<string name="lock_when_idle">Lock when idle</string>
<string name="lock_always">Always</string>
@ -249,6 +256,7 @@
<string name="hide_notification_content">Hide notification content</string>
<string name="secure_screen">Secure screen</string>
<string name="secure_screen_summary">Secure screen hides app contents when switching apps and block screenshots</string>
<string name="firebase_summary">Sending crash logs and analytics will allow us to identify and fix issues, improve performance, and make future updates more relevant to your needs</string>
<string name="pref_category_nsfw_content">NSFW (18+) sources</string>
<string name="pref_show_nsfw_source">Show in sources and extensions lists</string>