diff --git a/.github/workflows/decrypt-and-validate-data.sh b/.github/workflows/decrypt-and-validate-data.sh index c0702dbb..f4954798 100755 --- a/.github/workflows/decrypt-and-validate-data.sh +++ b/.github/workflows/decrypt-and-validate-data.sh @@ -1,15 +1,15 @@ #!/bin/bash if [ -n "$GPG_PASSPHRASE" ]; then - gpg --passphrase $GPG_PASSPHRASE --pinentry-mode loopback -o ./keys/debug.keystore -d ./keys/debug.keystore.gpg - gpg --passphrase $GPG_PASSPHRASE --pinentry-mode loopback -o ./keys/release.keystore -d ./keys/release.keystore.gpg - gpg --passphrase $GPG_PASSPHRASE --pinentry-mode loopback -o ./keys/google-cloud-key.json -d ./keys/google-cloud-key.json.gpg - gpg --passphrase $GPG_PASSPHRASE --pinentry-mode loopback -o ./ci.properties -d ./ci.properties.gpg + gpg --passphrase $GPG_PASSPHRASE --yes --pinentry-mode loopback -o ./keys/debug.keystore -d ./keys/debug.keystore.gpg + gpg --passphrase $GPG_PASSPHRASE --yes --pinentry-mode loopback -o ./keys/release.keystore -d ./keys/release.keystore.gpg + gpg --passphrase $GPG_PASSPHRASE --yes --pinentry-mode loopback -o ./keys/google-cloud-key.json -d ./keys/google-cloud-key.json.gpg + gpg --passphrase $GPG_PASSPHRASE --yes --pinentry-mode loopback -o ./ci.properties -d ./ci.properties.gpg echo "3a15a7065ed4a62a747af2e3477b0a3e1940a7bc2946df638b902bcb186998e7 *keys/debug.keystore" | shasum --algorithm 256 --check echo "36a3bb8b7bda141b414c2df7fac7dcd09a2775769c561e562d2b43f7b246bfa5 *keys/release.keystore" | shasum --algorithm 256 --check echo "eac44fec5848bb6718b23ad433e73650352611953ae7f98c8e502c0491154da5 *keys/google-cloud-key.json" | shasum --algorithm 256 --check - echo "b009011968e80d5b99eaa181141e6c94178a538c3dac0e413c43d5f95f50f557 *ci.properties" | shasum --algorithm 256 --check + echo "43278a8837886a8e485f021e5bb4f0df14a057f796d0230cf2ea0e88709c98fa *ci.properties" | shasum --algorithm 256 --check else echo "Unable to decrypt secret data" exit 1 diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 16b4591e..89c15929 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -56,10 +56,6 @@ jobs: strategy: matrix: include: - - command: assembleGplayDebug - fileName: app-gplay-debug.apk - filePath: ./app/build/outputs/apk/gplay/debug/app-gplay-debug.apk - - command: assembleGplayRelease fileName: app-gplay-release.apk filePath: ./app/build/outputs/apk/gplay/release/app-gplay-release.apk @@ -72,9 +68,9 @@ jobs: fileName: app-fdroid-release.apk filePath: ./app/build/outputs/apk/fdroid/release/app-fdroid-release.apk - - command: assembleFdroidAutomation - fileName: app-fdroid-automation.apk - filePath: ./app/build/outputs/apk/fdroid/automation/app-fdroid-automation.apk + - command: assembleAutomationDebug + fileName: app-automation-debug.apk + filePath: ./app/build/outputs/apk/automation/debug/app-automation-debug.apk steps: - name: Checkout uses: actions/checkout@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 263522d8..4f677578 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,13 +26,12 @@ jobs: - name: Build apk's run: | - ./gradlew app:assembleFdroidDebug app:assembleFdroidRelease app:assembleFdroidAutomation + app:assembleFdroidRelease app:assembleAutomationDebug - name: Rename files run: | - cp ./app/build/outputs/apk/fdroid/debug/app-fdroid-debug.apk ./keepassvault-debug.apk cp ./app/build/outputs/apk/fdroid/release/app-fdroid-release.apk ./keepassvault-release.apk - cp ./app/build/outputs/apk/fdroid/automation/app-fdroid-automation.apk ./keepassvault-test.apk + cp ./app/build/outputs/apk/automation/debug/app-automation-debug.apk ./keepassvault-test.apk - name: Publish Github Release uses: softprops/action-gh-release@v2 @@ -40,7 +39,6 @@ jobs: generate_release_notes: true prerelease: true files: | - ./keepassvault-debug.apk ./keepassvault-release.apk ./keepassvault-test.apk diff --git a/PRIVACY-POLICY.md b/PRIVACY-POLICY.md index 6a17efca..2a3f4e69 100644 --- a/PRIVACY-POLICY.md +++ b/PRIVACY-POLICY.md @@ -14,7 +14,7 @@ For a better experience, while using our Service, I may require you to provide u **Log Data** -I want to inform you that whenever you use my Service, in a case of an error in the app I collect data and information (through third-party products) on your phone called Log Data. This Log Data may include information such as your device Internet Protocol (“IP”) address, device name, operating system version, the configuration of the app when utilizing my Service, the time and date of your use of the Service, and other statistics. +If you enable crash reporting in the application settings, then in a case of an error in the app I collect data and information (through third-party products) on your phone called Log Data. This Log Data may include information such as your device Internet Protocol (“IP”) address, device name, operating system version, the configuration of the app when utilizing my Service, the time and date of your use of the Service, and other statistics. Crash reporting is disabled by default. **Cookies** @@ -49,7 +49,7 @@ I do not knowingly collect personally identifiable information from children. I I may update our Privacy Policy from time to time. Thus, you are advised to review this page periodically for any changes. I will notify you of any changes by posting the new Privacy Policy on this page. -This policy is effective as of 2022-10-22 +This policy is effective as of 2026-07-06 **Contact Us** diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a9cb8b62..26e3a300 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -29,6 +29,7 @@ val ciPropertiesFile = file("../ci.properties") val propertyKeystorePassword = "KEYSTORE_PASSWORD" val propertyKeystoreAlias = "KEYSTORE_ALIAS" +val propertySentryDsn = "SENTRY_DSN" val debugKeyPath = "../keys/debug.keystore" val releaseKeyPath = "../keys/release.keystore" @@ -51,6 +52,10 @@ fun getCustomProperty(key: String): String { return customProps.getProperty(key) ?: System.getenv(key).orEmpty() } +fun String.toBuildConfigString(): String { + return "\"${replace("\\", "\\\\").replace("\"", "\\\"")}\"" +} + val generateVersionPropertiesFile by tasks.registering { description = "Generates version.properties for F-Droid." @@ -91,6 +96,11 @@ android { versionCode = formattedVersionCode.toInt() versionName = formattedVersionName testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + buildConfigField( + "String", + "SENTRY_DSN", + getCustomProperty(propertySentryDsn).toBuildConfigString() + ) vectorDrawables { useSupportLibrary = true @@ -157,9 +167,15 @@ android { productFlavors { create("fdroid") { dimension = "default" + buildConfigField("boolean", "IS_AUTOMATION_BUILD", "false") + } + create("automation") { + applicationIdSuffix = ".automation" + buildConfigField("boolean", "IS_AUTOMATION_BUILD", "true") } create("gplay") { dimension = "default" + buildConfigField("boolean", "IS_AUTOMATION_BUILD", "false") } } @@ -170,12 +186,6 @@ android { versionNameSuffix = "d" signingConfig = signingConfigs.getByName("debug") } - create("automation") { - isDebuggable = true - isMinifyEnabled = false - signingConfig = signingConfigs.getByName("debug") - applicationIdSuffix = ".automation" - } release { isDebuggable = false isMinifyEnabled = false @@ -310,6 +320,7 @@ dependencies { // Logging implementation(libs.timber) implementation(libs.treessence) + implementation(libs.sentry.android) // KeePass implementation(libs.kotpass) diff --git a/app/src/automation/kotlin/com/ivanovsky/passnotes/injection/DebugModuleBuilder.kt b/app/src/automation/kotlin/com/ivanovsky/passnotes/injection/DebugModuleBuilder.kt deleted file mode 100644 index 1feb89bc..00000000 --- a/app/src/automation/kotlin/com/ivanovsky/passnotes/injection/DebugModuleBuilder.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.ivanovsky.passnotes.injection - -import android.content.Context -import com.ivanovsky.passnotes.data.repository.settings.Settings -import com.ivanovsky.passnotes.domain.logger.LoggerInteractor -import com.ivanovsky.passnotes.injection.modules.CoreModule -import com.ivanovsky.passnotes.injection.modules.UiModule -import com.ivanovsky.passnotes.injection.modules.UseCaseModule -import com.ivanovsky.passnotes.injection.modules.debug.DebugBiometricModule -import com.ivanovsky.passnotes.injection.modules.debug.DebugFileSystemProvidersModule -import com.ivanovsky.passnotes.injection.modules.debug.DebugModule -import org.koin.core.module.Module - -// Is loaded via reflection in App.kt -class DebugModuleBuilder( - private val context: Context, - private val loggerInteractor: LoggerInteractor, - private val settings: Settings -) : DIModuleBuilder { - - override var isExternalStorageAccessEnabled: Boolean = false - - override fun buildModules(): List { - val isLoadFakeBiometricModule = settings.testToggles?.isFakeBiometricEnabled ?: false - val isFakeFileSystemEnabled = settings.testToggles?.isFakeFileSystemEnabled ?: false - - return listOf( - CoreModule.build(loggerInteractor), - DebugModule.build(), - DebugFileSystemProvidersModule.build( - isExternalStorageAccessEnabled = isExternalStorageAccessEnabled, - isFakeFileSystemEnabled = isFakeFileSystemEnabled - ), - DebugBiometricModule.build(), - UseCaseModule.build(), - UiModule.build() - ) - } -} \ No newline at end of file diff --git a/app/src/debug/kotlin/com/ivanovsky/passnotes/injection/DebugModuleBuilder.kt b/app/src/debug/kotlin/com/ivanovsky/passnotes/injection/DebugModuleBuilder.kt index c25c02a0..e2f5e8d3 100644 --- a/app/src/debug/kotlin/com/ivanovsky/passnotes/injection/DebugModuleBuilder.kt +++ b/app/src/debug/kotlin/com/ivanovsky/passnotes/injection/DebugModuleBuilder.kt @@ -1,8 +1,6 @@ package com.ivanovsky.passnotes.injection -import android.content.Context -import com.ivanovsky.passnotes.data.repository.settings.Settings -import com.ivanovsky.passnotes.domain.logger.LoggerInteractor +import com.ivanovsky.passnotes.BuildConfig import com.ivanovsky.passnotes.injection.modules.CoreModule import com.ivanovsky.passnotes.injection.modules.UiModule import com.ivanovsky.passnotes.injection.modules.UseCaseModule @@ -11,20 +9,18 @@ import com.ivanovsky.passnotes.injection.modules.debug.DebugFileSystemProvidersM import com.ivanovsky.passnotes.injection.modules.debug.DebugModule import org.koin.core.module.Module -// Is loaded via reflection in App.kt class DebugModuleBuilder( - private val context: Context, - private val loggerInteractor: LoggerInteractor, - private val settings: Settings + private val startDeps: AppStartDependencies ) : DIModuleBuilder { override var isExternalStorageAccessEnabled: Boolean = false override fun buildModules(): List { - val isFakeFileSystemEnabled = settings.testToggles?.isFakeFileSystemEnabled ?: false + val isFakeFileSystemEnabled = startDeps.settings.testToggles?.isFakeFileSystemEnabled + ?: BuildConfig.IS_AUTOMATION_BUILD return listOf( - CoreModule.build(loggerInteractor), + CoreModule.build(startDeps), DebugModule.build(), DebugFileSystemProvidersModule.build( isExternalStorageAccessEnabled = isExternalStorageAccessEnabled, diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8ced7136..3a5f79b2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,6 +20,10 @@ android:theme="@style/AppTheme" android:networkSecurityConfig="@xml/network_security_config"> + + - \ No newline at end of file + diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/App.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/App.kt index 51cb4fce..119c72d2 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/App.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/App.kt @@ -1,11 +1,12 @@ package com.ivanovsky.passnotes import android.app.Application -import android.content.Context -import com.ivanovsky.passnotes.data.repository.settings.Settings import com.ivanovsky.passnotes.data.repository.settings.SettingsImpl -import com.ivanovsky.passnotes.domain.logger.LoggerInteractor +import com.ivanovsky.passnotes.domain.loggingAndReporting.CrashReporterInteractor +import com.ivanovsky.passnotes.domain.loggingAndReporting.LoggerInteractor +import com.ivanovsky.passnotes.injection.AppStartDependencies import com.ivanovsky.passnotes.injection.DIModuleBuilder +import com.ivanovsky.passnotes.injection.DebugModuleBuilder import com.ivanovsky.passnotes.injection.DefaultModuleBuilder import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger @@ -15,29 +16,33 @@ open class App : Application() { open fun configureModuleBuilder(builder: DIModuleBuilder) { // implementation should be flavor specific + if (BuildConfig.IS_AUTOMATION_BUILD) { + builder.isExternalStorageAccessEnabled = true + } } override fun onCreate() { super.onCreate() val settings = SettingsImpl(context = this) - val loggerInteractor = LoggerInteractor(context = this, settings) - .apply { - initialize() - } - - val moduleBuilder = if (BuildConfig.DEBUG) { - val type = Class.forName("com.ivanovsky.passnotes.injection.DebugModuleBuilder") - val constructor = type.getConstructor( - Context::class.java, - LoggerInteractor::class.java, - Settings::class.java - ) + val deps = AppStartDependencies( + context = this, + settings = settings, + loggerInteractor = LoggerInteractor(context = this, settings) + .apply { + initialize() + }, + crashReporterInteractor = CrashReporterInteractor(context = this) + .apply { + initialize(settings) + } + ) - constructor.newInstance(this, loggerInteractor, settings) as DIModuleBuilder + val moduleBuilder = if (BuildConfig.DEBUG) { + DebugModuleBuilder(deps) } else { - DefaultModuleBuilder(loggerInteractor) + DefaultModuleBuilder(deps) } configureModuleBuilder(moduleBuilder) diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/settings/Settings.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/settings/Settings.kt index 4ac46a5b..0383e5c8 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/settings/Settings.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/settings/Settings.kt @@ -18,6 +18,7 @@ interface Settings { var isLockNotificationDialogEnabled: Boolean var isLockDatabaseOnBack: Boolean var isFileLogEnabled: Boolean + var isCrashReportingEnabled: Boolean var searchOptions: SearchOptions var isActivateSearchOnStart: Boolean var sortType: SortType diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/settings/SettingsImpl.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/settings/SettingsImpl.kt index d3a111d1..6aec4dc1 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/settings/SettingsImpl.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/settings/SettingsImpl.kt @@ -16,6 +16,7 @@ import com.ivanovsky.passnotes.data.repository.settings.SettingsImpl.Pref.GIT_US import com.ivanovsky.passnotes.data.repository.settings.SettingsImpl.Pref.GIT_USER_NAME import com.ivanovsky.passnotes.data.repository.settings.SettingsImpl.Pref.IS_ACTIVATE_SEARCH_ON_START import com.ivanovsky.passnotes.data.repository.settings.SettingsImpl.Pref.IS_BIOMETRIC_UNLOCK_ENABLED +import com.ivanovsky.passnotes.data.repository.settings.SettingsImpl.Pref.IS_CRASH_REPORTING_ENABLED import com.ivanovsky.passnotes.data.repository.settings.SettingsImpl.Pref.IS_EXTERNAL_STORAGE_CACHE_ENABLED import com.ivanovsky.passnotes.data.repository.settings.SettingsImpl.Pref.IS_FILE_LOG_ENABLED import com.ivanovsky.passnotes.data.repository.settings.SettingsImpl.Pref.IS_GROUPS_AT_START_ENABLED @@ -101,6 +102,12 @@ class SettingsImpl(private val context: Context) : Settings { putBoolean(IS_FILE_LOG_ENABLED, value) } + override var isCrashReportingEnabled: Boolean + get() = getBoolean(IS_CRASH_REPORTING_ENABLED) + set(value) { + putBoolean(IS_CRASH_REPORTING_ENABLED, value) + } + override var isBiometricUnlockEnabled: Boolean get() = getBoolean(IS_BIOMETRIC_UNLOCK_ENABLED) set(value) { @@ -318,6 +325,11 @@ class SettingsImpl(private val context: Context) : Settings { type = BOOLEAN, defaultValue = false ), + IS_CRASH_REPORTING_ENABLED( + keyId = R.string.pref_is_crash_reporting_enabled, + type = BOOLEAN, + defaultValue = false + ), IS_POSTPONED_SYNC_ENABLED( keyId = R.string.pref_is_postponed_sync_enabled, type = BOOLEAN, diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/domain/interactor/settings/app/AppSettingsInteractor.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/domain/interactor/settings/app/AppSettingsInteractor.kt index ad4e4c32..6b2a3210 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/domain/interactor/settings/app/AppSettingsInteractor.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/domain/interactor/settings/app/AppSettingsInteractor.kt @@ -6,7 +6,8 @@ import com.ivanovsky.passnotes.data.entity.OperationError.newGenericIOError import com.ivanovsky.passnotes.data.entity.OperationResult import com.ivanovsky.passnotes.domain.DispatcherProvider import com.ivanovsky.passnotes.domain.entity.exception.Stacktrace -import com.ivanovsky.passnotes.domain.logger.LoggerInteractor +import com.ivanovsky.passnotes.domain.loggingAndReporting.CrashReporterInteractor +import com.ivanovsky.passnotes.domain.loggingAndReporting.LoggerInteractor import com.ivanovsky.passnotes.domain.usecases.LockDatabaseUseCase import com.ivanovsky.passnotes.domain.usecases.RemoveBiometricDataUseCase import java.io.File @@ -16,13 +17,21 @@ class AppSettingsInteractor( private val loggerInteractor: LoggerInteractor, private val dispatchers: DispatcherProvider, private val lockUseCase: LockDatabaseUseCase, - private val removeBiometricDataUseCase: RemoveBiometricDataUseCase + private val removeBiometricDataUseCase: RemoveBiometricDataUseCase, + private val crashReporterInteractor: CrashReporterInteractor ) { + fun isCrashReportingAvailable(): Boolean = + crashReporterInteractor.isAvailable + fun reInitializeLogging() { loggerInteractor.initialize() } + fun setCrashReportingEnabled(isEnabled: Boolean) { + crashReporterInteractor.setEnabled(isEnabled) + } + fun lockDatabase() { lockUseCase.lockIfNeed() } diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/domain/loggingAndReporting/CrashReporterInteractor.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/domain/loggingAndReporting/CrashReporterInteractor.kt new file mode 100644 index 00000000..fe9522d2 --- /dev/null +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/domain/loggingAndReporting/CrashReporterInteractor.kt @@ -0,0 +1,45 @@ +package com.ivanovsky.passnotes.domain.loggingAndReporting + +import android.content.Context +import com.ivanovsky.passnotes.BuildConfig +import com.ivanovsky.passnotes.data.repository.settings.Settings +import io.sentry.Sentry +import io.sentry.android.core.SentryAndroid + +class CrashReporterInteractor( + private val context: Context +) { + + val isAvailable: Boolean = BuildConfig.SENTRY_DSN.isNotBlank() + + fun initialize(settings: Settings) { + setEnabled(settings.isCrashReportingEnabled) + } + + fun setEnabled(isEnabled: Boolean) { + if (!isEnabled || !isAvailable) { + Sentry.close() + return + } + + if (Sentry.isEnabled()) { + return + } + + SentryAndroid.init(context.applicationContext) { options -> + options.setDsn(BuildConfig.SENTRY_DSN) + options.dist = BuildConfig.VERSION_CODE.toString() + options.environment = "${BuildConfig.FLAVOR}-${BuildConfig.BUILD_TYPE}" + options.release = buildReleaseName() + } + } + + private fun buildReleaseName(): String = + buildString { + append(BuildConfig.APPLICATION_ID) + append('@') + append(BuildConfig.VERSION_NAME) + append('+') + append(BuildConfig.VERSION_CODE) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/domain/logger/FileLogger.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/domain/loggingAndReporting/FileLogger.kt similarity index 99% rename from app/src/main/kotlin/com/ivanovsky/passnotes/domain/logger/FileLogger.kt rename to app/src/main/kotlin/com/ivanovsky/passnotes/domain/loggingAndReporting/FileLogger.kt index a8bab948..a1c66c26 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/domain/logger/FileLogger.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/domain/loggingAndReporting/FileLogger.kt @@ -1,4 +1,4 @@ -package com.ivanovsky.passnotes.domain.logger +package com.ivanovsky.passnotes.domain.loggingAndReporting import android.util.Log import java.io.File diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/domain/logger/FileLoggerTree.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/domain/loggingAndReporting/FileLoggerTree.kt similarity index 96% rename from app/src/main/kotlin/com/ivanovsky/passnotes/domain/logger/FileLoggerTree.kt rename to app/src/main/kotlin/com/ivanovsky/passnotes/domain/loggingAndReporting/FileLoggerTree.kt index 7210e10f..a9747667 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/domain/logger/FileLoggerTree.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/domain/loggingAndReporting/FileLoggerTree.kt @@ -1,4 +1,4 @@ -package com.ivanovsky.passnotes.domain.logger +package com.ivanovsky.passnotes.domain.loggingAndReporting import android.util.Log import java.io.File diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/domain/logger/FileSystemFacade.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/domain/loggingAndReporting/FileSystemFacade.kt similarity index 97% rename from app/src/main/kotlin/com/ivanovsky/passnotes/domain/logger/FileSystemFacade.kt rename to app/src/main/kotlin/com/ivanovsky/passnotes/domain/loggingAndReporting/FileSystemFacade.kt index ea1adcea..d4429cff 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/domain/logger/FileSystemFacade.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/domain/loggingAndReporting/FileSystemFacade.kt @@ -1,4 +1,4 @@ -package com.ivanovsky.passnotes.domain.logger +package com.ivanovsky.passnotes.domain.loggingAndReporting import java.io.BufferedWriter import java.io.File diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/domain/logger/LoggerInteractor.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/domain/loggingAndReporting/LoggerInteractor.kt similarity index 98% rename from app/src/main/kotlin/com/ivanovsky/passnotes/domain/logger/LoggerInteractor.kt rename to app/src/main/kotlin/com/ivanovsky/passnotes/domain/loggingAndReporting/LoggerInteractor.kt index 3dbc4b65..575216f7 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/domain/logger/LoggerInteractor.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/domain/loggingAndReporting/LoggerInteractor.kt @@ -1,4 +1,4 @@ -package com.ivanovsky.passnotes.domain.logger +package com.ivanovsky.passnotes.domain.loggingAndReporting import android.content.Context import android.util.Log diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/injection/AppStartDependencies.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/injection/AppStartDependencies.kt new file mode 100644 index 00000000..1281f682 --- /dev/null +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/injection/AppStartDependencies.kt @@ -0,0 +1,13 @@ +package com.ivanovsky.passnotes.injection + +import android.content.Context +import com.ivanovsky.passnotes.data.repository.settings.Settings +import com.ivanovsky.passnotes.domain.loggingAndReporting.CrashReporterInteractor +import com.ivanovsky.passnotes.domain.loggingAndReporting.LoggerInteractor + +data class AppStartDependencies( + val context: Context, + val loggerInteractor: LoggerInteractor, + val crashReporterInteractor: CrashReporterInteractor, + val settings: Settings +) \ No newline at end of file diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/injection/DefaultModuleBuilder.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/injection/DefaultModuleBuilder.kt index 3b2f54bc..28795047 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/injection/DefaultModuleBuilder.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/injection/DefaultModuleBuilder.kt @@ -1,6 +1,5 @@ package com.ivanovsky.passnotes.injection -import com.ivanovsky.passnotes.domain.logger.LoggerInteractor import com.ivanovsky.passnotes.injection.modules.BiometricModule import com.ivanovsky.passnotes.injection.modules.CoreModule import com.ivanovsky.passnotes.injection.modules.FileSystemProvidersModule @@ -9,14 +8,14 @@ import com.ivanovsky.passnotes.injection.modules.UseCaseModule import org.koin.core.module.Module class DefaultModuleBuilder( - private val loggerInteractor: LoggerInteractor + private val appDeps: AppStartDependencies ) : DIModuleBuilder { override var isExternalStorageAccessEnabled: Boolean = false override fun buildModules(): List { return listOf( - CoreModule.build(loggerInteractor), + CoreModule.build(appDeps), FileSystemProvidersModule.build( isExternalStorageAccessEnabled = isExternalStorageAccessEnabled ), diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/injection/modules/CoreModule.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/injection/modules/CoreModule.kt index c682baa8..750e573c 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/injection/modules/CoreModule.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/injection/modules/CoreModule.kt @@ -11,7 +11,6 @@ import com.ivanovsky.passnotes.data.repository.file.saf.SAFHelper import com.ivanovsky.passnotes.data.repository.keepass.DatabaseSyncStateProvider import com.ivanovsky.passnotes.data.repository.keepass.KeepassDatabaseRepository import com.ivanovsky.passnotes.data.repository.settings.Settings -import com.ivanovsky.passnotes.data.repository.settings.SettingsImpl import com.ivanovsky.passnotes.domain.DateFormatProvider import com.ivanovsky.passnotes.domain.DateFormatter import com.ivanovsky.passnotes.domain.DispatcherProvider @@ -21,42 +20,44 @@ import com.ivanovsky.passnotes.domain.NoteDiffer import com.ivanovsky.passnotes.domain.PermissionHelper import com.ivanovsky.passnotes.domain.ResourceProvider import com.ivanovsky.passnotes.domain.interactor.SelectionHolder -import com.ivanovsky.passnotes.domain.logger.LoggerInteractor +import com.ivanovsky.passnotes.injection.AppStartDependencies import com.ivanovsky.passnotes.presentation.core.ThemeProvider import org.koin.dsl.module object CoreModule { - fun build(loggerInteractor: LoggerInteractor) = - module { - single { loggerInteractor } - single { ThemeProvider(get()) } - single { ResourceProvider(get(), get()) } - single { PermissionHelper(get()) } - single { LocaleProvider(get()) } - single { DispatcherProvider() } - single { ObserverBus() } - single { DateFormatProvider(get(), get()) } - single { DateFormatter(get()) } - single { NoteDiffer() } - single { SelectionHolder() } - single { SettingsImpl(get()) } - single { DataCipherProviderImpl(get()) } - single { FileHelper(get(), get()) } - single { SAFHelper(get()) } + fun build( + appStartDeps: AppStartDependencies + ) = module { + single { appStartDeps.loggerInteractor } + single { ThemeProvider(get()) } + single { ResourceProvider(get(), get()) } + single { PermissionHelper(get()) } + single { LocaleProvider(get()) } + single { DispatcherProvider() } + single { ObserverBus() } + single { DateFormatProvider(get(), get()) } + single { DateFormatter(get()) } + single { NoteDiffer() } + single { SelectionHolder() } + single { appStartDeps.settings } + single { appStartDeps.crashReporterInteractor } + single { DataCipherProviderImpl(get()) } + single { FileHelper(get(), get()) } + single { SAFHelper(get()) } - // Database - single { AppDatabase.buildDatabase(get(), get()) } - single { provideRemoteFileRepository(get()) } - single { provideUsedFileRepository(get(), get()) } - single { provideGitRootDao(get()) } + // Database + single { AppDatabase.buildDatabase(get(), get()) } + single { provideRemoteFileRepository(get()) } + single { provideUsedFileRepository(get(), get()) } + single { provideGitRootDao(get()) } - // Files, Keepass - single { DatabaseSyncStateProvider(get(), get(), get()) } - single { - KeepassDatabaseRepository(get(), get(), get(), get()) - } + // Files, Keepass + single { DatabaseSyncStateProvider(get(), get(), get()) } + single { + KeepassDatabaseRepository(get(), get(), get(), get()) } + } private fun provideRemoteFileRepository(database: AppDatabase) = RemoteFileRepository(database.remoteFileDao) diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/injection/modules/UiModule.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/injection/modules/UiModule.kt index 58228fd3..c6cf5070 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/injection/modules/UiModule.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/injection/modules/UiModule.kt @@ -168,7 +168,7 @@ object UiModule { single { ServerLoginInteractor(get(), get(), get()) } single { MainSettingsInteractor(get()) } single { DatabaseSettingsInteractor(get(), get()) } - single { AppSettingsInteractor(get(), get(), get(), get()) } + single { AppSettingsInteractor(get(), get(), get(), get(), get()) } single { AutofillInteractor(get(), get()) } single { MainInteractor(get()) } single { LockServiceInteractor(get(), get(), get(), get(), get()) } diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/about/AboutScreen.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/about/AboutScreen.kt index 2731c566..48cbfa42 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/about/AboutScreen.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/about/AboutScreen.kt @@ -194,7 +194,7 @@ fun DarkPreview() { ) { AboutScreen( version = BuildConfig.VERSION_NAME, - buildType = BuildConfig.BUILD_TYPE + buildType = "${BuildConfig.BUILD_TYPE} / ${BuildConfig.FLAVOR}" ) } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/about/AboutViewModel.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/about/AboutViewModel.kt index b67a6110..5fa20e2e 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/about/AboutViewModel.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/about/AboutViewModel.kt @@ -12,7 +12,7 @@ class AboutViewModel( ) : ViewModel() { val appVersion = BuildConfig.VERSION_NAME - val appBuildType = BuildConfig.BUILD_TYPE + val appBuildType = "${BuildConfig.BUILD_TYPE} / ${BuildConfig.FLAVOR}" val theme = themeFlow(themeProvider) fun onBackClicked() = router.exit() diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/settings/app/AppSettingsFragment.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/settings/app/AppSettingsFragment.kt index 4afc5e09..938076ae 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/settings/app/AppSettingsFragment.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/settings/app/AppSettingsFragment.kt @@ -16,6 +16,7 @@ import com.ivanovsky.passnotes.data.repository.settings.SettingsImpl.Pref.AUTO_C import com.ivanovsky.passnotes.data.repository.settings.SettingsImpl.Pref.AUTO_LOCK_DELAY_IN_MS import com.ivanovsky.passnotes.data.repository.settings.SettingsImpl.Pref.IS_ACTIVATE_SEARCH_ON_START import com.ivanovsky.passnotes.data.repository.settings.SettingsImpl.Pref.IS_BIOMETRIC_UNLOCK_ENABLED +import com.ivanovsky.passnotes.data.repository.settings.SettingsImpl.Pref.IS_CRASH_REPORTING_ENABLED import com.ivanovsky.passnotes.data.repository.settings.SettingsImpl.Pref.IS_FILE_LOG_ENABLED import com.ivanovsky.passnotes.data.repository.settings.SettingsImpl.Pref.IS_LOCK_DATABASE_ON_BACK import com.ivanovsky.passnotes.data.repository.settings.SettingsImpl.Pref.IS_LOCK_NOTIFICATION_VISIBLE @@ -42,6 +43,7 @@ class AppSettingsFragment : BasePreferenceFragment(), PermissionRequestResultRec private val viewModel: AppSettingsViewModel by viewModel() private lateinit var isFileLogEnabledPref: SwitchPreferenceCompat + private lateinit var isCrashReportingEnabledPref: SwitchPreferenceCompat private lateinit var isPostponedSyncEnabledPref: SwitchPreferenceCompat private lateinit var isBiometricUnlockEnabledPref: SwitchPreferenceCompat private lateinit var sendLogFilePref: Preference @@ -83,6 +85,7 @@ class AppSettingsFragment : BasePreferenceFragment(), PermissionRequestResultRec AUTO_LOCK_DELAY_IN_MS, AUTO_CLEAR_CLIPBOARD_DELAY_IN_MS, IS_FILE_LOG_ENABLED, + IS_CRASH_REPORTING_ENABLED, IS_POSTPONED_SYNC_ENABLED, IS_BIOMETRIC_UNLOCK_ENABLED, IS_ACTIVATE_SEARCH_ON_START @@ -100,6 +103,11 @@ class AppSettingsFragment : BasePreferenceFragment(), PermissionRequestResultRec isFileLogEnabledPref = findPreference(getString(R.string.pref_is_file_log_enabled)) ?: throwPreferenceNotFound(R.string.pref_is_file_log_enabled) + isCrashReportingEnabledPref = findPreference( + getString(R.string.pref_is_crash_reporting_enabled) + ) + ?: throwPreferenceNotFound(R.string.pref_is_crash_reporting_enabled) + isPostponedSyncEnabledPref = findPreference( getString(R.string.pref_is_postponed_sync_enabled) ) @@ -129,14 +137,19 @@ class AppSettingsFragment : BasePreferenceFragment(), PermissionRequestResultRec categoryNotifications = findPreference(getString(R.string.pref_category_notifications)) ?: throwPreferenceNotFound(R.string.pref_category_notifications) - categoryLogging = findPreference(getString(R.string.pref_category_logging)) - ?: throwPreferenceNotFound(R.string.pref_category_logging) + categoryLogging = findPreference(getString(R.string.pref_category_reporting)) + ?: throwPreferenceNotFound(R.string.pref_category_reporting) isFileLogEnabledPref.setOnPreferenceChangeListener { _, newValue -> viewModel.onFileLogEnabledChanged(newValue as Boolean) true } + isCrashReportingEnabledPref.setOnPreferenceChangeListener { _, newValue -> + viewModel.onCrashReportingEnabledChanged(newValue as Boolean) + true + } + isPostponedSyncEnabledPref.setOnPreferenceChangeListener { _, newValue -> viewModel.onPostponedSyncEnabledChanged(newValue as Boolean) true @@ -148,6 +161,12 @@ class AppSettingsFragment : BasePreferenceFragment(), PermissionRequestResultRec } categoryBiometric.isVisible = viewModel.isBiometricUnlockAvailable() + isCrashReportingEnabledPref.isEnabled = viewModel.isCrashReportingAvailable() + if (!viewModel.isCrashReportingAvailable()) { + isCrashReportingEnabledPref.summary = getString( + R.string.pref_is_crash_reporting_enabled_not_configured_summary + ) + } enableNotificationPermissionPref.isVisible = viewModel.isEnableNotificationPermissionVisible.value ?: false } diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/settings/app/AppSettingsViewModel.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/settings/app/AppSettingsViewModel.kt index 6c3b4809..9238f804 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/settings/app/AppSettingsViewModel.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/settings/app/AppSettingsViewModel.kt @@ -74,6 +74,14 @@ class AppSettingsViewModel( interactor.reInitializeLogging() } + fun isCrashReportingAvailable(): Boolean = + interactor.isCrashReportingAvailable() + + fun onCrashReportingEnabledChanged(isEnabled: Boolean) { + settings.isCrashReportingEnabled = isEnabled + interactor.setCrashReportingEnabled(isEnabled) + } + fun onPostponedSyncEnabledChanged(isEnabled: Boolean) { interactor.lockDatabase() } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c3bb4c62..42405c40 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -364,6 +364,9 @@ Lock database when pressing \'Back\' from the root group screen Write log to file Application will write all logs to file that can be attached to issue at Github page + Send crash reports + Send crash reports to Sentry to help diagnose application failures + Crash reporting is not configured for this build Send log file Clear logs Postpone uploading @@ -382,7 +385,7 @@ Open the search field automatically after the database is opened General Notifications - Logging + Reporting Biometrics Master key Synchronization with server diff --git a/app/src/main/res/values/untranslatable-strings.xml b/app/src/main/res/values/untranslatable-strings.xml index 6be17a16..5f464556 100644 --- a/app/src/main/res/values/untranslatable-strings.xml +++ b/app/src/main/res/values/untranslatable-strings.xml @@ -12,6 +12,7 @@ isExternalStorageCacheEnabled isSslCertificateValidationEnabled isLogToFileEnabled + isCrashReportingEnabled isPostponedSyncEnabled gitUserName gitUserEmail @@ -35,7 +36,7 @@ categoryGeneral categoryBiometrics categoryNotifications - categoryLogging + categoryLogging categorySearch categorySynchronization diff --git a/app/src/main/res/xml/application_settings.xml b/app/src/main/res/xml/application_settings.xml index 21904906..d947caad 100644 --- a/app/src/main/res/xml/application_settings.xml +++ b/app/src/main/res/xml/application_settings.xml @@ -86,8 +86,13 @@ + app:key="@string/pref_category_reporting" + app:title="@string/reporting"> + +