diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d69cd8b4d..38595e850 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -290,9 +290,9 @@ dependencies { implementation(libs.logcat) // Crash reports/analytics - implementation(platform(libs.firebase.bom)) - implementation(libs.firebase.analytics) - implementation(libs.firebase.crashlytics) + "standardImplementation"(platform(libs.firebase.bom)) + "standardImplementation"(libs.firebase.analytics) + "standardImplementation"(libs.firebase.crashlytics) // Shizuku implementation(libs.bundles.shizuku) 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 0fdbf70a5..4745beabb 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,14 +36,19 @@ 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.i18n.kmk.KMR 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) @@ -90,7 +97,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, @@ -106,7 +113,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, @@ -114,7 +121,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, @@ -135,7 +142,7 @@ internal class PermissionStep : OnboardingStep { // no-op. resulting checks is being done on resume }, ) - PermissionItem( + PermissionCheckbox( title = stringResource(KMR.strings.onboarding_permission_external_storage), subtitle = stringResource(KMR.strings.onboarding_permission_external_storage_description), granted = externalStoragePermissionGranted, @@ -148,7 +155,7 @@ internal class PermissionStep : OnboardingStep { // no-op. resulting checks is being done on resume }, ) - PermissionItem( + PermissionCheckbox( title = stringResource(KMR.strings.onboarding_permission_external_storage), subtitle = stringResource(KMR.strings.onboarding_permission_external_storage_description), granted = externalStoragePermissionGranted, @@ -161,7 +168,7 @@ internal class PermissionStep : OnboardingStep { // no-op. resulting checks is being done on resume }, ) - PermissionItem( + PermissionCheckbox( title = stringResource(KMR.strings.onboarding_permission_external_storage), subtitle = stringResource(KMR.strings.onboarding_permission_writing_external_storage_description), granted = externalStoragePermissionGranted, @@ -169,6 +176,30 @@ internal class PermissionStep : OnboardingStep { ) } // KMK <-- + + 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, + ) + */ } } @@ -187,7 +218,7 @@ internal class PermissionStep : OnboardingStep { } @Composable - private fun PermissionItem( + private fun PermissionCheckbox( title: String, subtitle: String, granted: Boolean, @@ -217,4 +248,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 1f4663501..cc6100af4 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 @@ -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 @@ -71,10 +72,20 @@ 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() @@ -82,129 +93,157 @@ object SettingsSecurityScreen : SearchableSettings { val isCbzPasswordSet by remember { CbzCrypto.isPasswordSetState(scope) }.collectAsState() val passwordProtectDownloads by securityPreferences.passwordProtectDownloads().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), + ) + }, + ), + + 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(), + ), + // SY --> + Preference.PreferenceItem.SwitchPreference( + pref = securityPreferences.passwordProtectDownloads(), + title = stringResource(SYMR.strings.password_protect_downloads), + subtitle = stringResource(SYMR.strings.password_protect_downloads_summary), + enabled = isCbzPasswordSet, + ), + Preference.PreferenceItem.ListPreference( + pref = securityPreferences.encryptionType(), + title = stringResource(SYMR.strings.encryption_type), + entries = SecurityPreferences.EncryptionType.entries + .associateWith { stringResource(it.titleRes) } + .toImmutableMap(), + enabled = passwordProtectDownloads, + + ), + kotlin.run { + var dialogOpen by remember { mutableStateOf(false) } + if (dialogOpen) { + PasswordDialog( + onDismissRequest = { dialogOpen = false }, + onReturnPassword = { password -> + dialogOpen = false + + CbzCrypto.deleteKeyCbz() + securityPreferences.cbzPassword().set(CbzCrypto.encryptCbz(password.replace("\n", ""))) + }, + ) } - .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(), - ), - // SY --> - Preference.PreferenceItem.SwitchPreference( - pref = securityPreferences.passwordProtectDownloads(), - title = stringResource(SYMR.strings.password_protect_downloads), - subtitle = stringResource(SYMR.strings.password_protect_downloads_summary), - enabled = isCbzPasswordSet, - ), - Preference.PreferenceItem.ListPreference( - pref = securityPreferences.encryptionType(), - title = stringResource(SYMR.strings.encryption_type), - entries = SecurityPreferences.EncryptionType.entries - .associateWith { stringResource(it.titleRes) } - .toImmutableMap(), - enabled = passwordProtectDownloads, - - ), - kotlin.run { - var dialogOpen by remember { mutableStateOf(false) } - if (dialogOpen) { - PasswordDialog( - onDismissRequest = { dialogOpen = false }, - onReturnPassword = { password -> - dialogOpen = false - - CbzCrypto.deleteKeyCbz() - securityPreferences.cbzPassword().set(CbzCrypto.encryptCbz(password.replace("\n", ""))) + Preference.PreferenceItem.TextPreference( + title = stringResource(SYMR.strings.set_cbz_zip_password), + onClick = { + dialogOpen = true }, ) - } - Preference.PreferenceItem.TextPreference( - title = stringResource(SYMR.strings.set_cbz_zip_password), - onClick = { - dialogOpen = true - }, - ) - }, - Preference.PreferenceItem.TextPreference( - title = stringResource(SYMR.strings.delete_cbz_archive_password), - onClick = { - CbzCrypto.deleteKeyCbz() - securityPreferences.cbzPassword().set("") }, - enabled = isCbzPasswordSet, - ), - kotlin.run { - val navigator = LocalNavigator.currentOrThrow - val count by securityPreferences.authenticatorTimeRanges().collectAsState() Preference.PreferenceItem.TextPreference( - title = stringResource(SYMR.strings.action_edit_biometric_lock_times), - subtitle = pluralStringResource( - SYMR.plurals.num_lock_times, - count.size, - count.size, - ), + title = stringResource(SYMR.strings.delete_cbz_archive_password), onClick = { - navigator.push(BiometricTimesScreen()) + CbzCrypto.deleteKeyCbz() + securityPreferences.cbzPassword().set("") }, - enabled = useAuth, - ) - }, - kotlin.run { - val selection by securityPreferences.authenticatorDays().collectAsState() - var dialogOpen by remember { mutableStateOf(false) } - if (dialogOpen) { - SetLockedDaysDialog( - onDismissRequest = { dialogOpen = false }, - initialSelection = selection, - onDaysSelected = { - dialogOpen = false - securityPreferences.authenticatorDays().set(it) + enabled = isCbzPasswordSet, + ), + kotlin.run { + val navigator = LocalNavigator.currentOrThrow + val count by securityPreferences.authenticatorTimeRanges().collectAsState() + Preference.PreferenceItem.TextPreference( + title = stringResource(SYMR.strings.action_edit_biometric_lock_times), + subtitle = pluralStringResource( + SYMR.plurals.num_lock_times, + count.size, + count.size, + ), + onClick = { + navigator.push(BiometricTimesScreen()) }, + enabled = useAuth, ) - } - Preference.PreferenceItem.TextPreference( - title = stringResource(SYMR.strings.biometric_lock_days), - subtitle = stringResource(SYMR.strings.biometric_lock_days_summary), - onClick = { dialogOpen = true }, - enabled = useAuth, - ) - }, - // SY <-- - Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.secure_screen_summary)), + }, + kotlin.run { + val selection by securityPreferences.authenticatorDays().collectAsState() + var dialogOpen by remember { mutableStateOf(false) } + if (dialogOpen) { + SetLockedDaysDialog( + onDismissRequest = { dialogOpen = false }, + initialSelection = selection, + onDaysSelected = { + dialogOpen = false + securityPreferences.authenticatorDays().set(it) + }, + ) + } + Preference.PreferenceItem.TextPreference( + title = stringResource(SYMR.strings.biometric_lock_days), + subtitle = stringResource(SYMR.strings.biometric_lock_days_summary), + onClick = { dialogOpen = true }, + enabled = useAuth, + ) + }, + // SY <-- + Preference.PreferenceItem.InfoPreference(stringResource(MR.strings.secure_screen_summary)), + ), + ) + } + + @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)), + ), ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index 84224377e..b907ce8ea 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt @@ -28,8 +28,6 @@ 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.Firebase -import com.google.firebase.crashlytics.crashlytics import dev.mihon.injekt.patchInjekt import eu.kanade.domain.DomainModule import eu.kanade.domain.SYDomainModule @@ -37,6 +35,7 @@ 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 @@ -59,9 +58,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.isPreviewBuildType -import eu.kanade.tachiyomi.util.system.isReleaseBuildType import eu.kanade.tachiyomi.util.system.notify import exh.log.CrashlyticsPrinter import exh.log.EHLogLevel @@ -73,6 +69,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 +91,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() @@ -107,11 +105,6 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) // KMK <-- - // SY --> - if (!isDevFlavor) { - Firebase.crashlytics.setCrashlyticsCollectionEnabled(isReleaseBuildType || isPreviewBuildType) - } - // SY <-- GlobalExceptionHandler.initialize(applicationContext, CrashActivity::class.java) // TLS 1.3 support for Android < 10 @@ -136,6 +129,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) 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 f358f8ff5..bcb103f3b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/di/PreferenceModule.kt @@ -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 @@ -40,6 +41,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 bbf15d593..16bc10fa3 100644 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -191,6 +191,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? @@ -243,6 +247,9 @@ App language Security and privacy + Security + Analytics and Crash logs + Require unlock Lock when idle Always @@ -250,6 +257,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