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/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2587ddff4..0ab666594 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -268,6 +268,14 @@ android:name="android.webkit.WebView.MetricsOptOut" android:value="true" /> + + + + 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 1508cf8db..a79d37ec0 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 @@ -70,10 +71,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() @@ -81,129 +92,132 @@ 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", ""))) - }, - ) - } - Preference.PreferenceItem.TextPreference( - title = stringResource(SYMR.strings.set_cbz_zip_password), - onClick = { - dialogOpen = true + .toImmutableMap(), + onValueChanged = { + (context as FragmentActivity).authenticate( + title = context.stringResource(MR.strings.lock_when_idle), + ) }, - ) - }, - 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, + ), + 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, + ), - onClick = { - navigator.push(BiometricTimesScreen()) - }, - 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) + 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.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)), + 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, + ), + onClick = { + navigator.push(BiometricTimesScreen()) + }, + 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) + }, + ) + } + 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)), + ) ) } @@ -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( diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index eecbd877a..842a11e03 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt @@ -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.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) 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 5cec2bd39..c482acc47 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 @@ -37,6 +38,9 @@ class PreferenceModule(val app: Application) : InjektModule { addSingletonFactory { SecurityPreferences(get()) } + addSingletonFactory { + PrivacyPreferences(get()) + } addSingletonFactory { LibraryPreferences(get()) } diff --git a/app/src/standard/java/mihon/core/firebase/Firebase.kt b/app/src/standard/java/mihon/core/firebase/Firebase.kt new file mode 100644 index 000000000..7ec88da27 --- /dev/null +++ b/app/src/standard/java/mihon/core/firebase/Firebase.kt @@ -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) + } +} 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 69688c1aa..c298d62f0 100755 --- 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