diff --git a/app/src/dev/java/mihon/core/firebase/Firebase.kt b/app/src/dev/java/mihon/core/firebase/Firebase.kt new file mode 100644 index 000000000..6108a4c76 --- /dev/null +++ b/app/src/dev/java/mihon/core/firebase/Firebase.kt @@ -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 +} diff --git a/app/src/main/java/eu/kanade/presentation/more/onboarding/PermissionStep.kt b/app/src/main/java/eu/kanade/presentation/more/onboarding/PermissionStep.kt index 8b3d9c07b..6ad493b7e 100644 --- a/app/src/main/java/eu/kanade/presentation/more/onboarding/PermissionStep.kt +++ b/app/src/main/java/eu/kanade/presentation/more/onboarding/PermissionStep.kt @@ -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), + ) + } } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSecurityScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSecurityScreen.kt index 1d0510025..315cc41fb 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSecurityScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsSecurityScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.fragment.app.FragmentActivity 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.util.system.AuthenticatorUtil.authenticate import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.isAuthenticationSupported @@ -28,55 +29,91 @@ object SettingsSecurityScreen : SearchableSettings { @Composable override fun getPreferences(): List { - val context = LocalContext.current val securityPreferences = remember { Injekt.get() } - val authSupported = remember { context.isAuthenticationSupported() } + val privacyPreferences = remember { Injekt.get() } + 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() - return listOf( - Preference.PreferenceItem.SwitchPreference( - pref = useAuthPref, - title = stringResource(MR.strings.lock_with_biometrics), - enabled = authSupported, - onValueChanged = { - (context as FragmentActivity).authenticate( - title = context.stringResource(MR.strings.lock_with_biometrics), - ) - }, - ), - Preference.PreferenceItem.ListPreference( - pref = securityPreferences.lockAppAfter(), - title = stringResource(MR.strings.lock_when_idle), - enabled = authSupported && useAuth, - entries = LockAfterValues - .associateWith { - when (it) { - -1 -> stringResource(MR.strings.lock_never) - 0 -> stringResource(MR.strings.lock_always) - else -> pluralStringResource(MR.plurals.lock_after_mins, count = it, it) + return Preference.PreferenceGroup( + title = stringResource(MR.strings.pref_security), + preferenceItems = persistentListOf( + Preference.PreferenceItem.SwitchPreference( + pref = useAuthPref, + title = stringResource(MR.strings.lock_with_biometrics), + enabled = authSupported, + onValueChanged = { + (context as FragmentActivity).authenticate( + title = context.stringResource(MR.strings.lock_with_biometrics), + ) + }, + ), + Preference.PreferenceItem.ListPreference( + pref = securityPreferences.lockAppAfter(), + title = stringResource(MR.strings.lock_when_idle), + enabled = authSupported && useAuth, + entries = LockAfterValues + .associateWith { + when (it) { + -1 -> stringResource(MR.strings.lock_never) + 0 -> stringResource(MR.strings.lock_always) + else -> pluralStringResource(MR.plurals.lock_after_mins, count = it, it) + } } - } - .toImmutableMap(), - onValueChanged = { - (context as FragmentActivity).authenticate( - title = context.stringResource(MR.strings.lock_when_idle), - ) - }, + .toImmutableMap(), + onValueChanged = { + (context as FragmentActivity).authenticate( + title = context.stringResource(MR.strings.lock_when_idle), + ) + }, + ), + + Preference.PreferenceItem.SwitchPreference( + pref = securityPreferences.hideNotificationContent(), + title = stringResource(MR.strings.hide_notification_content), + ), + Preference.PreferenceItem.ListPreference( + pref = securityPreferences.secureScreen(), + title = stringResource(MR.strings.secure_screen), + entries = SecurityPreferences.SecureScreenMode.entries + .associateWith { stringResource(it.titleRes) } + .toImmutableMap(), + ), + Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.secure_screen_summary)), ), - Preference.PreferenceItem.SwitchPreference( - pref = securityPreferences.hideNotificationContent(), - title = stringResource(MR.strings.hide_notification_content), + ) + } + + @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)), ), - Preference.PreferenceItem.ListPreference( - pref = securityPreferences.secureScreen(), - title = stringResource(MR.strings.secure_screen), - entries = SecurityPreferences.SecureScreenMode.entries - .associateWith { stringResource(it.titleRes) } - .toImmutableMap(), - ), - Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.secure_screen_summary)), ) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index e5b66d838..4b7b03c33 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt @@ -26,6 +26,7 @@ import eu.kanade.domain.DomainModule import eu.kanade.domain.base.BasePreferences 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 @@ -50,6 +51,7 @@ import kotlinx.coroutines.flow.onEach import logcat.AndroidLogcatLogger 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 @@ -67,6 +69,7 @@ import java.security.Security 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() @@ -93,6 +96,8 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor Injekt.importModule(AppModule(this)) Injekt.importModule(DomainModule()) + Firebase.setup(applicationContext, privacyPreferences, ProcessLifecycleOwner.get().lifecycleScope) + setupNotificationChannels() ProcessLifecycleOwner.get().lifecycle.addObserver(this) diff --git a/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt b/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt index b56c16cae..0d79e5608 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt @@ -5,6 +5,7 @@ import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.source.service.SourcePreferences 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 @@ -39,6 +40,9 @@ class PreferenceModule(val app: Application) : InjektModule { addSingletonFactory { SecurityPreferences(get()) } + addSingletonFactory { + PrivacyPreferences(get()) + } addSingletonFactory { LibraryPreferences(get()) } diff --git a/app/src/standard/AndroidManifest.xml b/app/src/standard/AndroidManifest.xml index f6a02077a..9d0be9170 100644 --- a/app/src/standard/AndroidManifest.xml +++ b/app/src/standard/AndroidManifest.xml @@ -20,6 +20,15 @@ tools:node="remove" /> + + + + + + FirebaseAnalytics.getInstance(context).setAnalyticsCollectionEnabled(enabled) + }.launchIn(scope) + preference.crashlytics().changes().onEach { enabled -> + FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(enabled) + }.launchIn(scope) + } +} diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/core/security/PrivacyPreferences.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/core/security/PrivacyPreferences.kt new file mode 100644 index 000000000..ebf5692d8 --- /dev/null +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/core/security/PrivacyPreferences.kt @@ -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) +} diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index 3f1a37d77..da39f34b1 100644 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -190,6 +190,10 @@ Get notified for library updates and more. Background battery usage Avoid interruptions to long-running library updates, downloads, and backup restores. + Send crash logs + Send anonymized crash logs to the developers. + Allow analytics + Send anonymized usage data to improve app features. Grant New to %s? We recommend checking out the getting started guide. Reinstalling %s? @@ -242,6 +246,9 @@ App language Security and privacy + Security + Analytics and Crash logs + Require unlock Lock when idle Always @@ -249,6 +256,7 @@ Hide notification content Secure screen Secure screen hides app contents when switching apps and block screenshots + Sending crash logs and analytics will allow us to identify and fix issues, improve performance, and make future updates more relevant to your needs NSFW (18+) sources Show in sources and extensions lists