From 749427f4d8ce8290abcfcd07d604b6e234ca5386 Mon Sep 17 00:00:00 2001 From: longnh15 Date: Tue, 9 Jun 2026 16:27:40 +0700 Subject: [PATCH] initial commit --- .claude/settings.local.json | 8 + .idea/AndroidProjectSystem.xml | 6 + .idea/appInsightsSettings.xml | 26 ++ .idea/compiler.xml | 6 + .idea/deploymentTargetSelector.xml | 15 ++ .idea/deviceManager.xml | 13 + .idea/gradle.xml | 18 ++ .idea/inspectionProfiles/Project_Default.xml | 61 +++++ .idea/misc.xml | 22 ++ .idea/runConfigurations.xml | 17 ++ .idea/vcs.xml | 6 + app/build.gradle.kts | 79 ++++++ app/proguard-rules.pro | 21 ++ .../heimdall/ExampleInstrumentedTest.kt | 24 ++ app/src/main/AndroidManifest.xml | 40 +++ app/src/main/ic_launcher-playstore.png | Bin 0 -> 31906 bytes .../longnh15/heimdall/HeimdallApplication.kt | 36 +++ .../heimdall/HeimdallAutofillService.kt | 83 ++++++ .../com/longnh15/heimdall/MainActivity.kt | 40 +++ .../com/longnh15/heimdall/MainViewModel.kt | 30 +++ .../java/com/longnh15/heimdall/ParsedField.kt | 10 + .../com/longnh15/heimdall/StructureParser.kt | 93 +++++++ .../data/preferences/AppPreferences.kt | 5 + .../data/preferences/PreferenceDataSource.kt | 31 +++ .../repository/CredentialRepositoryImpl.kt | 20 ++ .../data/source/local/CredentialDao.kt | 16 ++ .../data/source/local/HeimdallDatabase.kt | 13 + .../data/source/local/LocalDataSource.kt | 19 ++ .../source/local/entity/CredentialEntity.kt | 13 + .../source/local/entity/CredentialMapper.kt | 21 ++ .../data/vault/VaultSessionManagerImpl.kt | 42 +++ .../com/longnh15/heimdall/di/AppModule.kt | 40 +++ .../longnh15/heimdall/di/DatabaseModule.kt | 30 +++ .../heimdall/domain/model/Credential.kt | 8 + .../domain/repository/CredentialRepository.kt | 9 + .../domain/vault/VaultSessionManager.kt | 12 + .../domain/vault/VaultSessionState.kt | 8 + .../heimdall/navigation/HeimdallNavGraph.kt | 59 +++++ .../longnh15/heimdall/navigation/Screen.kt | 13 + .../ui/common/AutofillNotEnabledDialog.kt | 32 +++ .../heimdall/ui/onboarding/OnboardingBadge.kt | 17 ++ .../heimdall/ui/onboarding/OnboardingEvent.kt | 6 + .../ui/onboarding/OnboardingFinalPage.kt | 62 +++++ .../ui/onboarding/OnboardingIntroducePage.kt | 99 +++++++ .../heimdall/ui/onboarding/OnboardingPage.kt | 134 ++++++++++ .../onboarding/OnboardingRequestEnablePage.kt | 63 +++++ .../ui/onboarding/OnboardingViewModel.kt | 20 ++ .../ui/onboarding/OnboardingWelcomePage.kt | 54 ++++ .../com/longnh15/heimdall/ui/theme/Color.kt | 67 +++++ .../com/longnh15/heimdall/ui/theme/Theme.kt | 49 ++++ .../com/longnh15/heimdall/ui/theme/Type.kt | 34 +++ .../longnh15/heimdall/ui/vault/LockOverlay.kt | 67 +++++ .../heimdall/ui/vault/VaultContent.kt | 43 +++ .../longnh15/heimdall/ui/vault/VaultHeader.kt | 54 ++++ .../longnh15/heimdall/ui/vault/VaultItem.kt | 93 +++++++ .../longnh15/heimdall/ui/vault/VaultScreen.kt | 67 +++++ .../heimdall/ui/vault/VaultViewModel.kt | 32 +++ .../heimdall/ui/vault/edit/AddEditEvent.kt | 6 + .../heimdall/ui/vault/edit/AddEditForm.kt | 146 +++++++++++ .../heimdall/ui/vault/edit/AddEditHeader.kt | 76 ++++++ .../heimdall/ui/vault/edit/AddEditState.kt | 49 ++++ .../ui/vault/edit/AddEditVaultItemScreen.kt | 136 ++++++++++ .../ui/vault/edit/AddEditVaultViewModel.kt | 59 +++++ .../ui/vault/edit/PasswordStrength.kt | 5 + app/src/main/res/drawable/crosshair_icon.xml | 32 +++ .../res/drawable/ic_launcher_background.xml | 170 ++++++++++++ .../res/drawable/ic_launcher_foreground.xml | 29 ++ app/src/main/res/drawable/lock_icon.xml | 29 ++ app/src/main/res/drawable/page_icon.xml | 34 +++ app/src/main/res/drawable/person_icon.xml | 24 ++ app/src/main/res/drawable/shield_icon.xml | 40 +++ app/src/main/res/drawable/tick_icon.xml | 35 +++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 + app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1698 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 3202 bytes app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 1140 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 2024 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 2386 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 4556 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 3676 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 7530 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 5204 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 10366 bytes app/src/main/res/values/colors.xml | 10 + .../res/values/ic_launcher_background.xml | 4 + app/src/main/res/values/strings.xml | 3 + app/src/main/res/values/themes.xml | 5 + app/src/main/res/xml/backup_rules.xml | 13 + .../main/res/xml/data_extraction_rules.xml | 19 ++ .../main/res/xml/service_configuration.xml | 4 + .../com/longnh15/heimdall/ExampleUnitTest.kt | 17 ++ build.gradle.kts | 6 + gradle.properties | 15 ++ gradle/gradle-daemon-jvm.properties | 12 + gradle/libs.versions.toml | 57 ++++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 48462 bytes gradle/wrapper/gradle-wrapper.properties | 9 + gradlew | 248 ++++++++++++++++++ gradlew.bat | 82 ++++++ settings.gradle.kts | 27 ++ 101 files changed, 3419 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 .idea/AndroidProjectSystem.xml create mode 100644 .idea/appInsightsSettings.xml create mode 100644 .idea/compiler.xml create mode 100644 .idea/deploymentTargetSelector.xml create mode 100644 .idea/deviceManager.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 .idea/vcs.xml create mode 100644 app/build.gradle.kts create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/com/longnh15/heimdall/ExampleInstrumentedTest.kt create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/ic_launcher-playstore.png create mode 100644 app/src/main/java/com/longnh15/heimdall/HeimdallApplication.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/HeimdallAutofillService.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/MainActivity.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/MainViewModel.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/ParsedField.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/StructureParser.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/data/preferences/AppPreferences.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/data/preferences/PreferenceDataSource.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/data/repository/CredentialRepositoryImpl.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/data/source/local/CredentialDao.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/data/source/local/HeimdallDatabase.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/data/source/local/LocalDataSource.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/data/source/local/entity/CredentialEntity.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/data/source/local/entity/CredentialMapper.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/data/vault/VaultSessionManagerImpl.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/di/AppModule.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/di/DatabaseModule.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/domain/model/Credential.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/domain/repository/CredentialRepository.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/domain/vault/VaultSessionManager.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/domain/vault/VaultSessionState.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/navigation/HeimdallNavGraph.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/navigation/Screen.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/ui/common/AutofillNotEnabledDialog.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingBadge.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingEvent.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingFinalPage.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingIntroducePage.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingPage.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingRequestEnablePage.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingViewModel.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingWelcomePage.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/ui/theme/Color.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/ui/theme/Theme.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/ui/theme/Type.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/ui/vault/LockOverlay.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/ui/vault/VaultContent.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/ui/vault/VaultHeader.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/ui/vault/VaultItem.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/ui/vault/VaultScreen.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/ui/vault/VaultViewModel.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditEvent.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditForm.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditHeader.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditState.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditVaultItemScreen.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditVaultViewModel.kt create mode 100644 app/src/main/java/com/longnh15/heimdall/ui/vault/edit/PasswordStrength.kt create mode 100644 app/src/main/res/drawable/crosshair_icon.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/lock_icon.xml create mode 100644 app/src/main/res/drawable/page_icon.xml create mode 100644 app/src/main/res/drawable/person_icon.xml create mode 100644 app/src/main/res/drawable/shield_icon.xml create mode 100644 app/src/main/res/drawable/tick_icon.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/ic_launcher_background.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/main/res/xml/backup_rules.xml create mode 100644 app/src/main/res/xml/data_extraction_rules.xml create mode 100644 app/src/main/res/xml/service_configuration.xml create mode 100644 app/src/test/java/com/longnh15/heimdall/ExampleUnitTest.kt create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 gradle/gradle-daemon-jvm.properties create mode 100644 gradle/libs.versions.toml create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle.kts diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..fd49e83 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "mcp__plugin_figma_figma__use_figma", + "mcp__plugin_figma_figma__get_screenshot" + ] + } +} diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml new file mode 100644 index 0000000..371f2e2 --- /dev/null +++ b/.idea/appInsightsSettings.xml @@ -0,0 +1,26 @@ + + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..ca2efc6 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml new file mode 100644 index 0000000..91f9558 --- /dev/null +++ b/.idea/deviceManager.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..02c4aa5 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..7061a0d --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,61 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..f041549 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..fbc9665 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,79 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.devtool.ksp) + alias(libs.plugins.serialization) + alias(libs.plugins.koin.compiler) +} + +android { + namespace = "com.longnh15.heimdall" + compileSdk { + version = release(37) + } + + defaultConfig { + applicationId = "com.longnh15.heimdall" + minSdk = 26 + targetSdk = 37 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + signingConfig = signingConfigs.getByName("debug") + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + buildFeatures { + compose = true + } +} + +dependencies { + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.lifecycle.process) + implementation(libs.icon.pack) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.lifecycle.viewmodel.compose) + testImplementation(libs.junit) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.compose.ui.test.junit4) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(libs.androidx.junit) + debugImplementation(libs.androidx.compose.ui.test.manifest) + debugImplementation(libs.androidx.compose.ui.tooling) + + implementation(libs.navigation.compose) + implementation(libs.androidx.datastore.preferences) + implementation(libs.biometric.compose) + + // Koin + implementation(libs.koin) + implementation(libs.koin.compose) + implementation(libs.koin.annotations) + implementation(libs.koin.compose.viewmodel) + implementation(libs.koin.compose.viewmodel.navigation) + + // Room + implementation(libs.androidx.room) + ksp(libs.androidx.room.compiler) + implementation(libs.androidx.room.ktx) +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/longnh15/heimdall/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/longnh15/heimdall/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..d990fbf --- /dev/null +++ b/app/src/androidTest/java/com/longnh15/heimdall/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.longnh15.heimdall + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.longnh15.heimdall", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..2fde86d --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000000000000000000000000000000000000..3dad8c08173557c6a209451ce7900f05f309a78b GIT binary patch literal 31906 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 q!~1JQL19WMlnAJb%)Y-B=HuWXHe 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

R?0jn>&g|(Y_;*p$1sS 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 SH4XzaK`IJQpOng3g#AC{a}^u_ literal 0 HcmV?d00001 diff --git a/app/src/main/java/com/longnh15/heimdall/HeimdallApplication.kt b/app/src/main/java/com/longnh15/heimdall/HeimdallApplication.kt new file mode 100644 index 0000000..a59eda3 --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/HeimdallApplication.kt @@ -0,0 +1,36 @@ +package com.longnh15.heimdall + +import android.app.Application +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ProcessLifecycleOwner +import com.longnh15.heimdall.di.appModule +import com.longnh15.heimdall.domain.vault.VaultSessionManager +import org.koin.android.ext.android.inject +import org.koin.android.ext.koin.androidContext +import org.koin.android.ext.koin.androidLogger +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.KoinApplication +import org.koin.plugin.module.dsl.startKoin + +//@KoinApplication(modules = [DatabaseModule::class]) +//@ComponentScan +class HeimdallApplication: Application() { + private val vaultSessionManager: VaultSessionManager by inject() + private val vaultSessionLifecycleObserver = object : DefaultLifecycleObserver { + override fun onStart(owner: LifecycleOwner) { + vaultSessionManager.refreshSession() + } + } + + override fun onCreate() { + super.onCreate() + startKoin { + androidLogger() + androidContext(this@HeimdallApplication) + modules(appModule) + } + + ProcessLifecycleOwner.get().lifecycle.addObserver(vaultSessionLifecycleObserver) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/HeimdallAutofillService.kt b/app/src/main/java/com/longnh15/heimdall/HeimdallAutofillService.kt new file mode 100644 index 0000000..b98d19e --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/HeimdallAutofillService.kt @@ -0,0 +1,83 @@ +package com.longnh15.heimdall + +import android.app.assist.AssistStructure +import android.os.CancellationSignal +import android.service.autofill.AutofillService +import android.service.autofill.Dataset +import android.service.autofill.FillCallback +import android.service.autofill.FillContext +import android.service.autofill.FillRequest +import android.service.autofill.FillResponse +import android.service.autofill.SaveCallback +import android.service.autofill.SaveRequest +import android.util.Log +import android.view.autofill.AutofillValue +import android.widget.RemoteViews + +class HeimdallAutofillService : AutofillService() { + + private fun buildFillResponse( + parsedFields: ParsedField + ): FillResponse { + val responseBuilder = FillResponse.Builder() + val datasetBuilder = Dataset.Builder() + + parsedFields.usernameId?.let { id -> + datasetBuilder.setValue( + id, + AutofillValue.forText("+84826552595"), + RemoteViews(packageName, android.R.layout.simple_list_item_1).also { + it.setTextViewText(android.R.id.text1, "Long Nguyễn") + } + ) + } + + parsedFields.passwordId?.let { id -> + datasetBuilder.setValue( + id, + AutofillValue.forText("123123123"), + RemoteViews(packageName, android.R.layout.simple_list_item_1).also { + it.setTextViewText(android.R.id.text1, "••••••••") + } + ) + } + + responseBuilder.addDataset(datasetBuilder.build()) + + return responseBuilder.build() + } + + override fun onFillRequest( + request: FillRequest, + cancellationSignal: CancellationSignal, + callback: FillCallback + ) { + // Get the structure from the request + val context: List = request.fillContexts + val structure: AssistStructure = context[context.size - 1].structure + + Log.d("Heimdall", "Package: ${structure.activityComponent.packageName}") + Log.d("Heimdall", "Window nodes: ${structure.windowNodeCount}") + + // Traverse the structure looking for nodes to fill out + val parser = StructureParser(structure) + val parsedFields = parser.parse() + Log.d("Heimdall", "Parsed fields: $parsedFields") + if (parsedFields.isEmpty()) { + callback.onSuccess(null) + return + } + + + val fillResponse = buildFillResponse(parsedFields) + callback.onSuccess(fillResponse) + } + + override fun onSaveRequest( + p0: SaveRequest, + p1: SaveCallback + ) { + // Empty for now + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/MainActivity.kt b/app/src/main/java/com/longnh15/heimdall/MainActivity.kt new file mode 100644 index 0000000..7d6ac3d --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/MainActivity.kt @@ -0,0 +1,40 @@ +package com.longnh15.heimdall + +import android.os.Bundle +import android.view.autofill.AutofillManager +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.longnh15.heimdall.navigation.HeimdallNavGraph +import com.longnh15.heimdall.navigation.Screen +import com.longnh15.heimdall.ui.theme.HeimdallTheme +import org.koin.androidx.viewmodel.ext.android.viewModel + +class MainActivity : ComponentActivity() { + private val viewModel: MainViewModel by viewModel() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + HeimdallTheme { + val isFirstLaunch by viewModel.isFirstLaunch.collectAsStateWithLifecycle() + HeimdallNavGraph( + modifier = Modifier.fillMaxSize(), + startDestination = if (isFirstLaunch != false) Screen.Onboarding else Screen.Vault, + isAutofillServiceEnabled = ::isAutofillServiceEnabled, + onCompleteOnboarding = viewModel::onCompleteOnboarding, + ) + } + } + } + + fun isAutofillServiceEnabled(): Boolean { + val afm = getSystemService(AutofillManager::class.java) + return afm?.hasEnabledAutofillServices() ?: false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/MainViewModel.kt b/app/src/main/java/com/longnh15/heimdall/MainViewModel.kt new file mode 100644 index 0000000..500ec83 --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/MainViewModel.kt @@ -0,0 +1,30 @@ +package com.longnh15.heimdall + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.longnh15.heimdall.data.preferences.PreferenceDataSource +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import org.koin.core.annotation.KoinViewModel + +@KoinViewModel +class MainViewModel ( + private val preferenceDataSource: PreferenceDataSource +) : ViewModel() { + val isFirstLaunch: StateFlow = preferenceDataSource.preferences + .map { it.isFirstLaunch } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = null + ) + + fun onCompleteOnboarding() { + viewModelScope.launch { + preferenceDataSource.setFirstLaunchDone() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ParsedField.kt b/app/src/main/java/com/longnh15/heimdall/ParsedField.kt new file mode 100644 index 0000000..1642dce --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/ParsedField.kt @@ -0,0 +1,10 @@ +package com.longnh15.heimdall + +import android.view.autofill.AutofillId + +data class ParsedField( + val usernameId: AutofillId?, + val passwordId: AutofillId? +) { + fun isEmpty() = usernameId == null && passwordId == null +} diff --git a/app/src/main/java/com/longnh15/heimdall/StructureParser.kt b/app/src/main/java/com/longnh15/heimdall/StructureParser.kt new file mode 100644 index 0000000..eb90185 --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/StructureParser.kt @@ -0,0 +1,93 @@ +package com.longnh15.heimdall + +import android.app.assist.AssistStructure +import android.text.InputType +import android.util.Log +import android.view.View +import android.view.autofill.AutofillId + +class StructureParser(private val structure: AssistStructure) { + + fun parse(): ParsedField { + var usernameId: AutofillId? = null + var passwordId: AutofillId? = null + + for (i in 0 until structure.windowNodeCount) { + val windowNode = structure.getWindowNodeAt(i) + Log.d("Heimdall", "Root children: ${windowNode.rootViewNode.childCount}") + traverseNode(windowNode.rootViewNode) { node -> + when (classifyNode(node)) { + FieldType.USERNAME -> if (usernameId == null) usernameId = node.autofillId + FieldType.PASSWORD -> if (passwordId == null) passwordId = node.autofillId + else -> {} + } + } + } + + return ParsedField(usernameId, passwordId) + } + + fun traverseNode(node: AssistStructure.ViewNode, visitor: (AssistStructure.ViewNode) -> Unit) { + visitor(node) + for (i in 0 until node.childCount) { + traverseNode(node.getChildAt(i), visitor) + } + } + + private enum class FieldType { USERNAME, PASSWORD, NONE } + + private fun classifyNode(node: AssistStructure.ViewNode): FieldType { + // Must be an autofill-able text field + if (node.autofillId == null) return FieldType.NONE + if (node.visibility != View.VISIBLE) return FieldType.NONE + + // --- Heuristic 1: autofillHints (most reliable, respect first) --- + node.autofillHints?.forEach { hint -> + when (hint) { + View.AUTOFILL_HINT_USERNAME, + View.AUTOFILL_HINT_EMAIL_ADDRESS -> return FieldType.USERNAME + + View.AUTOFILL_HINT_PASSWORD -> return FieldType.PASSWORD + } + } + + // --- Heuristic 2: inputType flags --- + val inputType = node.inputType + if (inputType and InputType.TYPE_TEXT_VARIATION_PASSWORD != 0 || + inputType and InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD != 0 || + inputType and InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD != 0 + ) return FieldType.PASSWORD + + if (inputType and InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS != 0 || + inputType and InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS != 0 || + inputType and InputType.TYPE_CLASS_PHONE != 0 + ) return FieldType.USERNAME + + // --- Heuristic 3: htmlInfo for web views --- + val htmlAttrs = node.htmlInfo?.attributes + htmlAttrs?.forEach { pair -> + if (pair.first.equals("type", ignoreCase = true)) { + when (pair.second.lowercase()) { + "password" -> return FieldType.PASSWORD + "email" -> return FieldType.USERNAME // "text" is a weak signal, see note + } + } + } + + // --- Heuristic 4: hint / idEntry / contentDescription text matching --- + val textSignals = listOfNotNull( + node.hint, + node.idEntry, + node.contentDescription?.toString(), + node.text?.toString() + ).map { it.lowercase() } + + val usernameKeywords = listOf("user", "email", "login", "account", "phone", "username") + val passwordKeywords = listOf("pass", "pwd", "secret", "pin") + + if (textSignals.any { s -> passwordKeywords.any { s.contains(it) } }) return FieldType.PASSWORD + if (textSignals.any { s -> usernameKeywords.any { s.contains(it) } }) return FieldType.USERNAME + + return FieldType.NONE + } +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/data/preferences/AppPreferences.kt b/app/src/main/java/com/longnh15/heimdall/data/preferences/AppPreferences.kt new file mode 100644 index 0000000..bf21743 --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/data/preferences/AppPreferences.kt @@ -0,0 +1,5 @@ +package com.longnh15.heimdall.data.preferences + +data class AppPreferences( + val isFirstLaunch: Boolean = true, +) \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/data/preferences/PreferenceDataSource.kt b/app/src/main/java/com/longnh15/heimdall/data/preferences/PreferenceDataSource.kt new file mode 100644 index 0000000..5cf933e --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/data/preferences/PreferenceDataSource.kt @@ -0,0 +1,31 @@ +package com.longnh15.heimdall.data.preferences + +import android.content.Context +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import org.koin.core.annotation.Singleton + +private val Context.dataStore by preferencesDataStore("settings") + +@Singleton +class PreferenceDataSource( + appContext: Context +) { + private object Keys { + val IS_FIRST_LAUNCH = booleanPreferencesKey("is_first_launch") + } + + private val dataStore = appContext.dataStore + val preferences: Flow = dataStore.data.map { prefs -> + AppPreferences( + isFirstLaunch = prefs[Keys.IS_FIRST_LAUNCH] ?: true + ) + } + + suspend fun setFirstLaunchDone() { + dataStore.edit { it[Keys.IS_FIRST_LAUNCH] = false } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/data/repository/CredentialRepositoryImpl.kt b/app/src/main/java/com/longnh15/heimdall/data/repository/CredentialRepositoryImpl.kt new file mode 100644 index 0000000..ce3b724 --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/data/repository/CredentialRepositoryImpl.kt @@ -0,0 +1,20 @@ +package com.longnh15.heimdall.data.repository + +import com.longnh15.heimdall.data.source.local.LocalDataSource +import com.longnh15.heimdall.domain.model.Credential +import com.longnh15.heimdall.domain.repository.CredentialRepository +import kotlinx.coroutines.flow.Flow +import org.koin.core.annotation.Singleton + +@Singleton +class CredentialRepositoryImpl( + val localDataSource: LocalDataSource +) : CredentialRepository { + override suspend fun save(credential: Credential) { + localDataSource.save(credential) + } + + override fun getAll(): Flow> { + return localDataSource.getAllCredentials() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/data/source/local/CredentialDao.kt b/app/src/main/java/com/longnh15/heimdall/data/source/local/CredentialDao.kt new file mode 100644 index 0000000..82e50ff --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/data/source/local/CredentialDao.kt @@ -0,0 +1,16 @@ +package com.longnh15.heimdall.data.source.local + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import com.longnh15.heimdall.data.source.local.entity.CredentialEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface CredentialDao { + @Query("SELECT * FROM credential") + fun getAll(): Flow> + + @Insert + suspend fun insert(credential: CredentialEntity) +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/data/source/local/HeimdallDatabase.kt b/app/src/main/java/com/longnh15/heimdall/data/source/local/HeimdallDatabase.kt new file mode 100644 index 0000000..c2fde77 --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/data/source/local/HeimdallDatabase.kt @@ -0,0 +1,13 @@ +package com.longnh15.heimdall.data.source.local + +import androidx.room.Database +import androidx.room.RoomDatabase +import com.longnh15.heimdall.data.source.local.entity.CredentialEntity + +@Database( + entities = [CredentialEntity::class], + version = 1, +) +abstract class HeimdallDatabase: RoomDatabase() { + abstract fun credentialDao(): CredentialDao +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/data/source/local/LocalDataSource.kt b/app/src/main/java/com/longnh15/heimdall/data/source/local/LocalDataSource.kt new file mode 100644 index 0000000..511a0f0 --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/data/source/local/LocalDataSource.kt @@ -0,0 +1,19 @@ +package com.longnh15.heimdall.data.source.local + +import com.longnh15.heimdall.data.source.local.entity.toDomain +import com.longnh15.heimdall.data.source.local.entity.toEntity +import com.longnh15.heimdall.domain.model.Credential +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class LocalDataSource( + private val credentialDao: CredentialDao +) { + fun getAllCredentials(): Flow> { + return credentialDao.getAll().map { entities -> entities.map { it.toDomain() } } + } + + suspend fun save(credential: Credential) { + credentialDao.insert(credential.toEntity()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/data/source/local/entity/CredentialEntity.kt b/app/src/main/java/com/longnh15/heimdall/data/source/local/entity/CredentialEntity.kt new file mode 100644 index 0000000..34ac833 --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/data/source/local/entity/CredentialEntity.kt @@ -0,0 +1,13 @@ +package com.longnh15.heimdall.data.source.local.entity + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "credential") +data class CredentialEntity( + @PrimaryKey(autoGenerate = true) val id: Long = 0L, + val name: String, + val url: String?, + val username: String, + val password: String +) \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/data/source/local/entity/CredentialMapper.kt b/app/src/main/java/com/longnh15/heimdall/data/source/local/entity/CredentialMapper.kt new file mode 100644 index 0000000..d11491e --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/data/source/local/entity/CredentialMapper.kt @@ -0,0 +1,21 @@ +package com.longnh15.heimdall.data.source.local.entity + +import com.longnh15.heimdall.domain.model.Credential + +fun Credential.toEntity(): CredentialEntity { + return CredentialEntity( + name = name, + url = url, + username = username, + password = password + ) +} + +fun CredentialEntity.toDomain(): Credential { + return Credential( + name = name, + url = url.orEmpty(), + username = username, + password = password + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/data/vault/VaultSessionManagerImpl.kt b/app/src/main/java/com/longnh15/heimdall/data/vault/VaultSessionManagerImpl.kt new file mode 100644 index 0000000..9f950ab --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/data/vault/VaultSessionManagerImpl.kt @@ -0,0 +1,42 @@ +package com.longnh15.heimdall.data.vault + +import com.longnh15.heimdall.domain.vault.VaultSessionManager +import com.longnh15.heimdall.domain.vault.VaultSessionState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import org.koin.core.annotation.Singleton +import kotlin.time.Duration.Companion.seconds +import kotlin.time.TimeSource + +@Singleton +class VaultSessionManagerImpl: VaultSessionManager { + private companion object { + const val TIMEOUT_DURATION = 5 // 5 sec + } + + private val _sessionState = MutableStateFlow(VaultSessionState.Locked) + override val sessionState: StateFlow + get() = _sessionState + + override fun unlock() { + val now = TimeSource.Monotonic.markNow() + _sessionState.update { VaultSessionState.Unlocked(now + TIMEOUT_DURATION.seconds) } + } + + override fun lock() { + _sessionState.update { VaultSessionState.Locked } + } + + override fun refreshSession() { + val currentState = sessionState.value + if (currentState is VaultSessionState.Unlocked) { + val now = TimeSource.Monotonic.markNow() + if (currentState.expiredAt <= now) { + _sessionState.update { VaultSessionState.Locked } + } else { + _sessionState.update { VaultSessionState.Unlocked(now + TIMEOUT_DURATION.seconds) } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/di/AppModule.kt b/app/src/main/java/com/longnh15/heimdall/di/AppModule.kt new file mode 100644 index 0000000..fae9d2a --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/di/AppModule.kt @@ -0,0 +1,40 @@ +package com.longnh15.heimdall.di + +import com.longnh15.heimdall.MainViewModel +import com.longnh15.heimdall.data.preferences.PreferenceDataSource +import com.longnh15.heimdall.data.repository.CredentialRepositoryImpl +import com.longnh15.heimdall.data.source.local.LocalDataSource +import com.longnh15.heimdall.data.vault.VaultSessionManagerImpl +import com.longnh15.heimdall.domain.repository.CredentialRepository +import com.longnh15.heimdall.domain.vault.VaultSessionManager +import com.longnh15.heimdall.ui.onboarding.OnboardingViewModel +import com.longnh15.heimdall.ui.vault.VaultViewModel +import com.longnh15.heimdall.ui.vault.edit.AddEditVaultViewModel +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single +import org.koin.dsl.bind +import org.koin.dsl.module +import org.koin.plugin.module.dsl.single +import org.koin.plugin.module.dsl.viewModel + +//@Module +//class AppModule { +// @Single +// fun provideVaultSessionManager(): VaultSessionManager = VaultSessionManagerImpl() + +// @Single +// fun bindCredentialRepository( +// localDataSource: LocalDataSource +// ): CredentialRepository = CredentialRepositoryImpl(localDataSource) +//} + +val appModule = module { + single() bind VaultSessionManager::class + single() bind CredentialRepository::class + single() + + viewModel() + viewModel() + viewModel() + viewModel() +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/di/DatabaseModule.kt b/app/src/main/java/com/longnh15/heimdall/di/DatabaseModule.kt new file mode 100644 index 0000000..6ee555c --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/di/DatabaseModule.kt @@ -0,0 +1,30 @@ +package com.longnh15.heimdall.di + +import android.content.Context +import androidx.room.Room +import com.longnh15.heimdall.data.source.local.CredentialDao +import com.longnh15.heimdall.data.source.local.HeimdallDatabase +import com.longnh15.heimdall.data.source.local.LocalDataSource +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single + +@Module +class DatabaseModule { + @Single + fun provideDatabase(context: Context): HeimdallDatabase = + Room.databaseBuilder( + context, + HeimdallDatabase::class.java, + "heimdall.db" + ).build() + + @Single + fun provideCredentialDao(db: HeimdallDatabase): CredentialDao { + return db.credentialDao() + } + + @Single + fun provideLocalDataSource(credentialDao: CredentialDao): LocalDataSource { + return LocalDataSource(credentialDao) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/domain/model/Credential.kt b/app/src/main/java/com/longnh15/heimdall/domain/model/Credential.kt new file mode 100644 index 0000000..869f365 --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/domain/model/Credential.kt @@ -0,0 +1,8 @@ +package com.longnh15.heimdall.domain.model + +data class Credential( + val name: String, + val url: String, + val username: String, + val password: String, +) diff --git a/app/src/main/java/com/longnh15/heimdall/domain/repository/CredentialRepository.kt b/app/src/main/java/com/longnh15/heimdall/domain/repository/CredentialRepository.kt new file mode 100644 index 0000000..b454a1e --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/domain/repository/CredentialRepository.kt @@ -0,0 +1,9 @@ +package com.longnh15.heimdall.domain.repository + +import com.longnh15.heimdall.domain.model.Credential +import kotlinx.coroutines.flow.Flow + +interface CredentialRepository { + fun getAll(): Flow> + suspend fun save(credential: Credential) +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/domain/vault/VaultSessionManager.kt b/app/src/main/java/com/longnh15/heimdall/domain/vault/VaultSessionManager.kt new file mode 100644 index 0000000..62b54f9 --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/domain/vault/VaultSessionManager.kt @@ -0,0 +1,12 @@ +package com.longnh15.heimdall.domain.vault + +import kotlinx.coroutines.flow.StateFlow +import org.koin.core.annotation.Singleton + +interface VaultSessionManager { + val sessionState: StateFlow + + fun unlock() + fun lock() + fun refreshSession() +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/domain/vault/VaultSessionState.kt b/app/src/main/java/com/longnh15/heimdall/domain/vault/VaultSessionState.kt new file mode 100644 index 0000000..9967d40 --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/domain/vault/VaultSessionState.kt @@ -0,0 +1,8 @@ +package com.longnh15.heimdall.domain.vault + +import kotlin.time.TimeSource + +sealed class VaultSessionState { + data object Locked: VaultSessionState() + data class Unlocked(val expiredAt: TimeSource.Monotonic.ValueTimeMark): VaultSessionState() +} diff --git a/app/src/main/java/com/longnh15/heimdall/navigation/HeimdallNavGraph.kt b/app/src/main/java/com/longnh15/heimdall/navigation/HeimdallNavGraph.kt new file mode 100644 index 0000000..ebb5683 --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/navigation/HeimdallNavGraph.kt @@ -0,0 +1,59 @@ +package com.longnh15.heimdall.navigation + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.longnh15.heimdall.ui.onboarding.OnboardingPage +import com.longnh15.heimdall.ui.onboarding.OnboardingViewModel +import com.longnh15.heimdall.ui.vault.VaultScreen +import com.longnh15.heimdall.ui.vault.VaultViewModel +import com.longnh15.heimdall.ui.vault.edit.AddEditVaultItemScreen +import com.longnh15.heimdall.ui.vault.edit.AddEditVaultViewModel +import org.koin.compose.viewmodel.koinViewModel + +@Composable +fun HeimdallNavGraph( + modifier: Modifier = Modifier, + navController: NavHostController = rememberNavController(), + startDestination: Screen = Screen.Onboarding, + isAutofillServiceEnabled: () -> Boolean, + onCompleteOnboarding: () -> Unit, +) { + NavHost( + navController = navController, + startDestination = startDestination, + modifier = modifier + ) { + composable { + OnboardingPage( + viewModel = koinViewModel(), + isAutofillEnabled = isAutofillServiceEnabled, + navigateToVault = { + onCompleteOnboarding() + navController.navigate(Screen.Vault) { + popUpTo(Screen.Onboarding) { inclusive = true } + } + } + ) + } + composable { + VaultScreen( + viewModel = koinViewModel(), + navigateToAddScreen = { + navController.navigate(Screen.NewVaultItem) + } + ) + } + composable { + AddEditVaultItemScreen( + viewModel = koinViewModel(), + popScreen = { + navController.navigateUp() + } + ) + } + } +} diff --git a/app/src/main/java/com/longnh15/heimdall/navigation/Screen.kt b/app/src/main/java/com/longnh15/heimdall/navigation/Screen.kt new file mode 100644 index 0000000..a47cb80 --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/navigation/Screen.kt @@ -0,0 +1,13 @@ +package com.longnh15.heimdall.navigation + +import kotlinx.serialization.Serializable + +@Serializable +sealed class Screen { + @Serializable + data object Onboarding: Screen() + @Serializable + data object Vault: Screen() + @Serializable + data object NewVaultItem: Screen() +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ui/common/AutofillNotEnabledDialog.kt b/app/src/main/java/com/longnh15/heimdall/ui/common/AutofillNotEnabledDialog.kt new file mode 100644 index 0000000..a69f107 --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/ui/common/AutofillNotEnabledDialog.kt @@ -0,0 +1,32 @@ +package com.longnh15.heimdall.ui.common + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Warning +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable + +@Composable +fun AutofillNotEnabledDialog( + onRetry: () -> Unit, + onDismiss: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + icon = { + Icon(Icons.Outlined.Warning, contentDescription = null) + }, + title = { Text("Autofill not enabled") }, + text = { + Text("Heimdall wasn't selected as your autofill provider. Open settings and tap Heimdall to enable it.") + }, + confirmButton = { + TextButton(onClick = onRetry) { Text("Open settings") } + }, + dismissButton = { + TextButton(onClick = onDismiss) { Text("Skip for now") } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingBadge.kt b/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingBadge.kt new file mode 100644 index 0000000..7d16c6e --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingBadge.kt @@ -0,0 +1,17 @@ +package com.longnh15.heimdall.ui.onboarding + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material3.Badge +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun OnboardingBadge(modifier: Modifier = Modifier, content: @Composable (RowScope.() -> Unit)? = null) { + Badge( + modifier = modifier, + containerColor = MaterialTheme.colorScheme.surfaceVariant, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingEvent.kt b/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingEvent.kt new file mode 100644 index 0000000..f1d2050 --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingEvent.kt @@ -0,0 +1,6 @@ +package com.longnh15.heimdall.ui.onboarding + +sealed class OnboardingEvent { + data object OpenAutofillSettings : OnboardingEvent() + data object NavigateToVault : OnboardingEvent() +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingFinalPage.kt b/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingFinalPage.kt new file mode 100644 index 0000000..f743b92 --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingFinalPage.kt @@ -0,0 +1,62 @@ +package com.longnh15.heimdall.ui.onboarding + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.longnh15.heimdall.R + +@Composable +fun OnboardingFinalPage( + modifier: Modifier = Modifier, + navigateToVault: () -> Unit +) { + Column( + modifier = modifier.padding(48.dp, 32.dp).fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(32.dp) + ) { + OnboardingBadge { + Text("You're all set") + } + Box( + modifier = Modifier + .size(72.dp) + .background(Color(0xFF0A2E1A), shape = RoundedCornerShape(20.dp)), + contentAlignment = Alignment.Center + ) { + Image( + painterResource(R.drawable.tick_icon), + "Page icon", + ) + } + + Text("Heimdall is watching", style = MaterialTheme.typography.headlineMedium) + Text( + text = "Your vault is ready. Start adding passwords or import from your browser to get going.", + textAlign = TextAlign.Center + ) + + Spacer(Modifier.weight(1f)) + + Button(onClick = navigateToVault) { + Text("Enter my vault ↗") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingIntroducePage.kt b/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingIntroducePage.kt new file mode 100644 index 0000000..33170af --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingIntroducePage.kt @@ -0,0 +1,99 @@ +package com.longnh15.heimdall.ui.onboarding + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.longnh15.heimdall.R + +@Composable +fun OnboardingIntroducePage(modifier: Modifier = Modifier) { + Column( + modifier = modifier.padding(48.dp, 32.dp).fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(32.dp) + ) { + OnboardingBadge { + Text("What you get") + } + Text("Everything in one vault", style = MaterialTheme.typography.headlineMedium) + Text( + text = "Heimdall keeps your passwords safe and fills them in automatically", + textAlign = TextAlign.Center + ) + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + FeatureItem( + iconDrawableRes = R.drawable.lock_icon, + text = "End-to-end encrypted", + sub = "Only you can see your data" + ) + FeatureItem( + iconDrawableRes = R.drawable.crosshair_icon, + text = "Autofill anywhere", + sub = "Works across all your apps" + ) + FeatureItem( + iconDrawableRes = R.drawable.person_icon, + text = "Biometric unlock", + sub = "Fingerprint or face to open" + ) + } + } +} + +@Composable +fun FeatureItem( + modifier: Modifier = Modifier, + @DrawableRes iconDrawableRes: Int, + text: String, + sub: String +) { + Row( + modifier = modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.secondary, shape = RoundedCornerShape(8.dp)) + .border( + 0.5.dp, + MaterialTheme.colorScheme.tertiary, + RoundedCornerShape(8.dp) + ) + .padding(12.dp, 14.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Box( + modifier = Modifier + .size(32.dp) + .background(Color(0xFFEEEDFE), RoundedCornerShape(8.dp)), + contentAlignment = Alignment.Center + ) { + Image(painterResource(iconDrawableRes), "Icon") + } + Column { + Text(text = text, fontSize = 13.sp, fontWeight = FontWeight(500), color = MaterialTheme.colorScheme.onPrimary) + Text(text = sub, fontSize = 12.sp, color = MaterialTheme.colorScheme.onSecondary) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingPage.kt b/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingPage.kt new file mode 100644 index 0000000..f5816a1 --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingPage.kt @@ -0,0 +1,134 @@ +package com.longnh15.heimdall.ui.onboarding + +import android.content.Intent +import android.provider.Settings +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.core.net.toUri +import com.longnh15.heimdall.ui.common.AutofillNotEnabledDialog + +private sealed class OnboardingPages { + data object Welcome : OnboardingPages() + data object Introduce : OnboardingPages() + data object EnableAutofill : OnboardingPages() + data object Final : OnboardingPages() +} + +private val onboardingPages = listOf( + OnboardingPages.Welcome, + OnboardingPages.Introduce, + OnboardingPages.EnableAutofill, + OnboardingPages.Final +) + +@Composable +fun OnboardingPage( + modifier: Modifier = Modifier, + viewModel: OnboardingViewModel = viewModel(), + isAutofillEnabled: () -> Boolean, + navigateToVault: () -> Unit +) { + val pagerState = rememberPagerState { onboardingPages.size } + var showDialog by remember { mutableStateOf(false) } + val context = LocalContext.current + val launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult() + ) { + if (isAutofillEnabled()) { + pagerState.requestScrollToPage(3) + } else { + showDialog = true + } + } + + LaunchedEffect(Unit) { + viewModel.uiEvent.collect { event -> + when (event) { + OnboardingEvent.OpenAutofillSettings -> { + val intent = Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE).apply { + data = "package:${context.packageName}".toUri() + } + launcher.launch(intent) + } + OnboardingEvent.NavigateToVault -> navigateToVault() + } + } + } + + Scaffold(Modifier.fillMaxSize()) { paddingValues -> + Column(modifier.padding(paddingValues)) { + HorizontalPager( + modifier = Modifier.weight(1f), + state = pagerState + ) { pageState -> + when (onboardingPages[pageState]) { + OnboardingPages.Welcome -> OnboardingWelcomePage() + OnboardingPages.Introduce -> OnboardingIntroducePage() + OnboardingPages.EnableAutofill -> OnboardingRequestEnablePage(onOpenAutofillSettings = viewModel::onOpenAutofillSettings) + OnboardingPages.Final -> OnboardingFinalPage(navigateToVault = navigateToVault) + } + } + + // Indicator + Row( + Modifier.wrapContentHeight().padding(bottom = 32.dp).align(Alignment.CenterHorizontally), + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + repeat(pagerState.pageCount) { iteration -> + val isSelected = pagerState.currentPage == iteration + val width by animateDpAsState(targetValue = if (isSelected) 24.dp else 8.dp) + val color by animateColorAsState(targetValue = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.inversePrimary) + + Box( + modifier = Modifier + .clip(RoundedCornerShape(3.dp)) + .background(color) + .width(width).height(6.dp) + ) + } + } + } + } + + if (showDialog) { + AutofillNotEnabledDialog( + onRetry = { + showDialog = false + val intent = Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE).apply { + data = "package:${context.packageName}".toUri() + } + launcher.launch(intent) + }, + onDismiss = { showDialog = false } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingRequestEnablePage.kt b/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingRequestEnablePage.kt new file mode 100644 index 0000000..8e95612 --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingRequestEnablePage.kt @@ -0,0 +1,63 @@ +package com.longnh15.heimdall.ui.onboarding + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Badge +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.longnh15.heimdall.R + +@Composable +fun OnboardingRequestEnablePage( + modifier: Modifier = Modifier, + onOpenAutofillSettings: () -> Unit +) { + Column( + modifier = modifier.padding(48.dp, 32.dp).fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(32.dp) + ) { + OnboardingBadge { + Text("Setup · 1 of 2") + } + Box( + modifier = Modifier + .size(72.dp) + .background(Color(0xFF1A1A2E), shape = RoundedCornerShape(20.dp)), + contentAlignment = Alignment.Center + ) { + Image( + painterResource(R.drawable.page_icon), + "Page icon", + ) + } + + Text("Enable autofill", style = MaterialTheme.typography.headlineMedium) + Text( + text = "Let Heimdall fill your passwords automatically. You'll be taken to Android settings — tap Heimdall and you're done.", + textAlign = TextAlign.Center + ) + + Spacer(Modifier.weight(1f)) + + Button(onClick = onOpenAutofillSettings) { + Text("Open settings") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingViewModel.kt b/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingViewModel.kt new file mode 100644 index 0000000..3f52c1e --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingViewModel.kt @@ -0,0 +1,20 @@ +package com.longnh15.heimdall.ui.onboarding + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import org.koin.core.annotation.KoinViewModel + +@KoinViewModel +class OnboardingViewModel : ViewModel() { + private val _uiEvent = Channel() + val uiEvent = _uiEvent.receiveAsFlow() + + fun onOpenAutofillSettings() { + viewModelScope.launch { + _uiEvent.send(OnboardingEvent.OpenAutofillSettings) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingWelcomePage.kt b/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingWelcomePage.kt new file mode 100644 index 0000000..16e7100 --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/ui/onboarding/OnboardingWelcomePage.kt @@ -0,0 +1,54 @@ +package com.longnh15.heimdall.ui.onboarding + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.longnh15.heimdall.R + +@Composable +fun OnboardingWelcomePage(modifier: Modifier = Modifier) { + Column( + modifier = modifier.padding(48.dp, 32.dp).fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(32.dp) + ) { + Box( + modifier = Modifier + .size(72.dp) + .background(Color(0xFF1A1A2E), shape = RoundedCornerShape(20.dp)), + contentAlignment = Alignment.Center + ) { + Image( + painterResource(R.drawable.shield_icon), + "Heimdall Logo", + ) + } + Text("Welcome to Heimdall", style = MaterialTheme.typography.headlineMedium) + Text( + text = "Your all-seeing guardian for passwords. Secure, private, and always watching over your credentials", + textAlign = TextAlign.Center + ) + } +} + +@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) +@Composable +fun OnboardingWelcomePagePreview() { + OnboardingWelcomePage(Modifier.background(Color.White)) +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ui/theme/Color.kt b/app/src/main/java/com/longnh15/heimdall/ui/theme/Color.kt new file mode 100644 index 0000000..a5f03d9 --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/ui/theme/Color.kt @@ -0,0 +1,67 @@ +package com.longnh15.heimdall.ui.theme + +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.graphics.Color + +val HeimdallLightColors = lightColorScheme( + primary = Color(0xFF534AB7), + onPrimary = Color(0xFFFFFFFF), + primaryContainer = Color(0xFFEEEDFE), + onPrimaryContainer = Color(0xFF26215C), + + secondary = Color(0xFF1D9E75), + onSecondary = Color(0xFFFFFFFF), + secondaryContainer = Color(0xFFE1F5EE), + onSecondaryContainer = Color(0xFF04342C), + + tertiary = Color(0xFF7F77DD), + onTertiary = Color(0xFFFFFFFF), + tertiaryContainer = Color(0xFFCECBF6), + onTertiaryContainer = Color(0xFF26215C), + + error = Color(0xFFE24B4A), + onError = Color(0xFFFFFFFF), + errorContainer = Color(0xFFFCEBEB), + onErrorContainer = Color(0xFF501313), + + background = Color(0xFFF8F8FC), + onBackground = Color(0xFF1A1A2E), + surface = Color(0xFFFFFFFF), + onSurface = Color(0xFF1A1A2E), + surfaceVariant = Color(0xFFEEEDFE), + onSurfaceVariant = Color(0xFF534AB7), + outline = Color(0xFFAFA9EC), + outlineVariant = Color(0xFFCECBF6), +) + +val HeimdallDarkColors = darkColorScheme( + primary = Color(0xFF7F77DD), + onPrimary = Color(0xFF26215C), + primaryContainer = Color(0xFF3C3489), + onPrimaryContainer = Color(0xFFCECBF6), + + secondary = Color(0xFF5DCAA5), + onSecondary = Color(0xFF04342C), + secondaryContainer = Color(0xFF0F6E56), + onSecondaryContainer = Color(0xFF9FE1CB), + + tertiary = Color(0xFFAFA9EC), + onTertiary = Color(0xFF26215C), + tertiaryContainer = Color(0xFF534AB7), + onTertiaryContainer = Color(0xFFEEEDFE), + + error = Color(0xFFF09595), + onError = Color(0xFF501313), + errorContainer = Color(0xFFA32D2D), + onErrorContainer = Color(0xFFF7C1C1), + + background = Color(0xFF1A1A2E), + onBackground = Color(0xFFEEEDFE), + surface = Color(0xFF26215C), + onSurface = Color(0xFFEEEDFE), + surfaceVariant = Color(0xFF3C3489), + onSurfaceVariant = Color(0xFFAFA9EC), + outline = Color(0xFF7F77DD), + outlineVariant = Color(0xFF534AB7), +) \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ui/theme/Theme.kt b/app/src/main/java/com/longnh15/heimdall/ui/theme/Theme.kt new file mode 100644 index 0000000..77ec77d --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/ui/theme/Theme.kt @@ -0,0 +1,49 @@ +package com.longnh15.heimdall.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Typography +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +@Composable +fun HeimdallTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + val colorScheme = if (darkTheme) { + HeimdallDarkColors + } else { + HeimdallLightColors + } + + MaterialTheme( + colorScheme = colorScheme, + typography = HeimdallTypography, + content = content + ) +} + +// ui/theme/Type.kt +val HeimdallTypography = Typography( + headlineLarge = TextStyle( + fontWeight = FontWeight.SemiBold, + fontSize = 28.sp + ), + titleMedium = TextStyle( + fontWeight = FontWeight.Medium, + fontSize = 16.sp + ), + bodyMedium = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 22.sp + ), + labelSmall = TextStyle( + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + letterSpacing = 0.5.sp + ) +) \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ui/theme/Type.kt b/app/src/main/java/com/longnh15/heimdall/ui/theme/Type.kt new file mode 100644 index 0000000..6fff4c5 --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package com.longnh15.heimdall.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ui/vault/LockOverlay.kt b/app/src/main/java/com/longnh15/heimdall/ui/vault/LockOverlay.kt new file mode 100644 index 0000000..d8f6d60 --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/ui/vault/LockOverlay.kt @@ -0,0 +1,67 @@ +package com.longnh15.heimdall.ui.vault + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Fingerprint +import androidx.compose.material.icons.outlined.Lock +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun LockOverlay( + modifier: Modifier = Modifier, + requestBiometric: () -> Unit, +) { + Box( + modifier = modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Icon( + imageVector = Icons.Outlined.Lock, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(48.dp) + ) + Text( + text = "Vault is locked", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onBackground + ) + Text( + text = "Authenticate to access your credentials", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(8.dp)) + Button(onClick = requestBiometric) { + Icon( + Icons.Outlined.Fingerprint, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("Unlock with biometrics") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ui/vault/VaultContent.kt b/app/src/main/java/com/longnh15/heimdall/ui/vault/VaultContent.kt new file mode 100644 index 0000000..5b0f8dd --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/ui/vault/VaultContent.kt @@ -0,0 +1,43 @@ +package com.longnh15.heimdall.ui.vault + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.longnh15.heimdall.domain.model.Credential + +@Composable +fun VaultContent( + credentials: List, + navigateToAddScreen: () -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + topBar = { VaultHeader() }, + floatingActionButton = { + FloatingActionButton(onClick = navigateToAddScreen) { + Icon(Icons.Filled.Add, null) + } + }, + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .padding(paddingValues) + .padding(14.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + items(credentials) { credential -> + VaultItem(credential) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ui/vault/VaultHeader.kt b/app/src/main/java/com/longnh15/heimdall/ui/vault/VaultHeader.kt new file mode 100644 index 0000000..769be45 --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/ui/vault/VaultHeader.kt @@ -0,0 +1,54 @@ +package com.longnh15.heimdall.ui.vault + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.AccessTime +import androidx.compose.material.icons.outlined.Person +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun VaultHeader() { + TopAppBar( + title = { + Text("My vault") + }, + colors = TopAppBarDefaults.topAppBarColors().copy( + containerColor = MaterialTheme.colorScheme.background + ), + actions = { + IconButton( + colors = IconButtonDefaults.iconButtonColors().copy( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer + ), + onClick = {} + ) { + Icon(Icons.Outlined.Person, null) + } + + IconButton( + colors = IconButtonDefaults.iconButtonColors().copy( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer + ), + onClick = {} + ) { + Icon(Icons.Outlined.AccessTime, null) + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ui/vault/VaultItem.kt b/app/src/main/java/com/longnh15/heimdall/ui/vault/VaultItem.kt new file mode 100644 index 0000000..78ddf15 --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/ui/vault/VaultItem.kt @@ -0,0 +1,93 @@ +package com.longnh15.heimdall.ui.vault + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ContentCopy +import androidx.compose.material.icons.outlined.Lock +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.longnh15.heimdall.domain.model.Credential + +@Composable +fun VaultItem( + credential: Credential, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .clip(RoundedCornerShape(12.dp)) + .background(MaterialTheme.colorScheme.onPrimary) + .border(0.5.dp, MaterialTheme.colorScheme.primaryContainer, shape = RoundedCornerShape(12.dp)) + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + Box( + modifier = Modifier + .size(36.dp) + .clip(RoundedCornerShape(10.dp)) + .background(Color(0xFF4A1B0C)), + contentAlignment = Alignment.Center + ) { + Text(credential.name.first().uppercase(), color = Color.White) + } + + Column(modifier = Modifier) { + Text( + text = credential.name, + fontSize = 13.sp, + color = MaterialTheme.colorScheme.onTertiaryContainer, + fontWeight = FontWeight(500) + ) + Text( + text = credential.username, + fontSize = 11.sp, + color = MaterialTheme.colorScheme.outline + ) + } + + Spacer(Modifier.weight(1f)) + + VaultItemAction(icon = Icons.Outlined.ContentCopy, onClick = {}) + VaultItemAction(icon = Icons.Outlined.Lock, onClick = {}) + } +} + +@Composable +fun VaultItemAction( + icon: ImageVector, + onClick: () -> Unit +) { + IconButton( + modifier = Modifier.size(32.dp), + onClick = onClick, + shape = RoundedCornerShape(8.dp), + colors = IconButtonDefaults.iconButtonColors().copy( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) { + Icon(icon, null, modifier = Modifier.size(16.dp)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ui/vault/VaultScreen.kt b/app/src/main/java/com/longnh15/heimdall/ui/vault/VaultScreen.kt new file mode 100644 index 0000000..c6826a2 --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/ui/vault/VaultScreen.kt @@ -0,0 +1,67 @@ +package com.longnh15.heimdall.ui.vault + +import androidx.biometric.AuthenticationRequest +import androidx.biometric.AuthenticationRequest.Companion.biometricRequest +import androidx.biometric.AuthenticationResult +import androidx.biometric.AuthenticationResultCallback +import androidx.biometric.compose.rememberAuthenticationLauncher +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel + +@Composable +fun VaultScreen( + modifier: Modifier = Modifier, + viewModel: VaultViewModel = viewModel(), + navigateToAddScreen: () -> Unit, +) { + val isUnlocked by viewModel.isUnlocked.collectAsStateWithLifecycle() + val credentials by viewModel.credentials.collectAsStateWithLifecycle() + val launcher = rememberAuthenticationLauncher( + resultCallback = object : AuthenticationResultCallback { + override fun onAuthResult(result: AuthenticationResult) { + viewModel.requestUnlock() + } + override fun onAuthAttemptFailed() { + } + } + ) + val requestBiometric = remember { { + val request = biometricRequest( + title = "Unlock Heimdall", + AuthenticationRequest.Biometric.Fallback.DeviceCredential, + ) { + setSubtitle("Confirm your identity to access your vault") + } + launcher.launch(request) + } } + + LaunchedEffect(isUnlocked) { + if (!isUnlocked) { + requestBiometric() + } + } + + VaultContent( + credentials = credentials, + navigateToAddScreen = navigateToAddScreen + ) + + AnimatedVisibility( + visible = !isUnlocked, + enter = fadeIn(), + exit = fadeOut() + ) { + LockOverlay( + modifier = modifier, + requestBiometric = requestBiometric + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ui/vault/VaultViewModel.kt b/app/src/main/java/com/longnh15/heimdall/ui/vault/VaultViewModel.kt new file mode 100644 index 0000000..f7ae029 --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/ui/vault/VaultViewModel.kt @@ -0,0 +1,32 @@ +package com.longnh15.heimdall.ui.vault + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.longnh15.heimdall.domain.repository.CredentialRepository +import com.longnh15.heimdall.domain.vault.VaultSessionManager +import com.longnh15.heimdall.domain.vault.VaultSessionState +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import org.koin.core.annotation.KoinViewModel + +@KoinViewModel +class VaultViewModel ( + private val vaultSessionManager: VaultSessionManager, + private val credentialRepository: CredentialRepository, +): ViewModel() { + val isUnlocked: StateFlow = vaultSessionManager.sessionState + .map { it is VaultSessionState.Unlocked } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false) + val credentials = credentialRepository.getAll() + .stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(), + listOf() + ) + + fun requestUnlock() { + vaultSessionManager.unlock() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditEvent.kt b/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditEvent.kt new file mode 100644 index 0000000..9ac9efe --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditEvent.kt @@ -0,0 +1,6 @@ +package com.longnh15.heimdall.ui.vault.edit + +sealed class AddEditEvent { + data object SaveSuccess: AddEditEvent() + data class SaveError(val message: String): AddEditEvent() +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditForm.kt b/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditForm.kt new file mode 100644 index 0000000..f26d12d --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditForm.kt @@ -0,0 +1,146 @@ +package com.longnh15.heimdall.ui.vault.edit + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.AndroidUiModes.UI_MODE_NIGHT_YES +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.longnh15.heimdall.ui.theme.HeimdallTheme + +@Composable +fun AddEditForm( + modifier: Modifier = Modifier, + content: @Composable ColumnScope.() -> Unit +) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .background(MaterialTheme.colorScheme.onPrimary) + .border(0.5.dp, MaterialTheme.colorScheme.outline, RoundedCornerShape(12.dp)), + content = content + ) +} + +@Composable +fun AddEditFormTextField( + value: String, + onValueChange: (String) -> Unit, + label: String, + placeholder: String, + isHideContent: Boolean = false, + isLastField: Boolean = false, + autoCorrectEnabled: Boolean = true, + actions: @Composable RowScope.() -> Unit = {}, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = label, + modifier = Modifier.width(100.dp).padding(vertical = 12.dp), + color = MaterialTheme.colorScheme.primary, + fontSize = 14.sp + ) + BasicTextField( + value = value, + onValueChange = onValueChange, + modifier = Modifier.weight(1f), + textStyle = TextStyle( + color = MaterialTheme.colorScheme.onSurface, + fontSize = 16.sp + ), + keyboardOptions = if (isLastField) + KeyboardOptions.Default.copy( + autoCorrectEnabled = autoCorrectEnabled, + imeAction = ImeAction.Done + ) + else + KeyboardOptions.Default.copy( + autoCorrectEnabled = autoCorrectEnabled, + imeAction = ImeAction.Next + ), + singleLine = true, + visualTransformation = if (isHideContent) PasswordVisualTransformation() else VisualTransformation.None, + decorationBox = { innerTextField -> + if (value.isEmpty()) { + Text( + text = placeholder, + fontSize = 16.sp, + color = MaterialTheme.colorScheme.surfaceVariant + ) + } + innerTextField() + } + ) + CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.primary) { + actions() + } + } +} + +@Composable +fun AddEditFormHeader(text: String) { + Text( + text = text.uppercase(), + fontSize = 13.sp, + fontWeight = FontWeight(500), + color = MaterialTheme.colorScheme.primary, + letterSpacing = 0.8.sp, + ) +} + +@Composable +fun AddEditDivider() { + HorizontalDivider(thickness = 0.5.dp, color = MaterialTheme.colorScheme.outline) +} + +@Preview(uiMode = UI_MODE_NIGHT_YES) +@Composable +fun AddEditFormPreview() { + HeimdallTheme { + AddEditForm { + AddEditFormTextField( + value = "", + onValueChange = {}, + label = "Name", + placeholder = "e.g. Google" + ) + HorizontalDivider(thickness = 0.5.dp, color = MaterialTheme.colorScheme.outline) + AddEditFormTextField( + value = "abc", + onValueChange = {}, + label = "URL", + placeholder = "google.com" + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditHeader.kt b/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditHeader.kt new file mode 100644 index 0000000..30e890c --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditHeader.kt @@ -0,0 +1,76 @@ +package com.longnh15.heimdall.ui.vault.edit + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AddEditHeader( + isSaveEnable: Boolean, + isSaving: Boolean, + popScreen: () -> Unit, + onSaveClick: () -> Unit, +) { + TopAppBar( + title = { + Text("New item") + }, + modifier = Modifier.padding(end = 16.dp), + colors = TopAppBarDefaults.topAppBarColors().copy( + containerColor = MaterialTheme.colorScheme.background + ), + navigationIcon = { IconButton(onClick = popScreen) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, null) + }}, + actions = { + Button( + onClick = onSaveClick, + contentPadding = PaddingValues(14.dp, 6.dp), + colors = ButtonDefaults.buttonColors().copy( + containerColor = MaterialTheme.colorScheme.tertiaryContainer, + contentColor = MaterialTheme.colorScheme.onTertiaryContainer, + disabledContainerColor = MaterialTheme.colorScheme.tertiaryContainer, + disabledContentColor = MaterialTheme.colorScheme.onTertiaryContainer, + ), + enabled = isSaveEnable, + modifier = Modifier.alpha(if (isSaveEnable) 1f else 0.4f) + ) { + if (isSaving) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp, + ) + Spacer(Modifier.width(8.dp)) + } + Text( + text = if (isSaving) "Saving…" else "Save", + fontSize = 13.sp, + fontWeight = FontWeight(500), + color = MaterialTheme.colorScheme.onTertiaryContainer + ) + } + } + ) + +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditState.kt b/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditState.kt new file mode 100644 index 0000000..940b18f --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditState.kt @@ -0,0 +1,49 @@ +package com.longnh15.heimdall.ui.vault.edit + +import com.longnh15.heimdall.domain.model.Credential + +data class AddEditState( + val name: String = "", + val url: String = "", + val username: String = "", + val password: String = "", + + val nameTouched: Boolean = false, + val urlTouched: Boolean = false, + val usernameTouched: Boolean = false, + val passwordTouched: Boolean = false, + + val isShowPassword: Boolean = false, + val isSaving: Boolean = false, +) { + val nameError: String? + get() = if (nameTouched && name.isBlank()) "Name is required" else null + val usernameError: String? + get() = if (urlTouched && url.isBlank()) "Url is required" else null + val passwordError: String? + get() = if (passwordTouched && password.isBlank()) "Password is required" else null + + val isFormValid: Boolean + get() = name.isNotBlank() && username.isNotBlank() && password.isNotBlank() + + val passwordStrength: PasswordStrength get() { + if (password.isBlank()) return PasswordStrength.NONE + var score = 0 + if (password.length >= 8) score++ + if (password.length >= 14) score++ + if (password.any { it.isUpperCase() } && password.any { it.isDigit() }) score++ + if (password.any { !it.isLetterOrDigit() }) score++ + return when (score) { + 1 -> PasswordStrength.WEAK + 2 -> PasswordStrength.FAIR + 3 -> PasswordStrength.GOOD + else -> PasswordStrength.STRONG + } + } + + fun toCredential(): Credential { + return Credential( + name, url, username, password + ) + } +} diff --git a/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditVaultItemScreen.kt b/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditVaultItemScreen.kt new file mode 100644 index 0000000..b2139f7 --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditVaultItemScreen.kt @@ -0,0 +1,136 @@ +package com.longnh15.heimdall.ui.vault.edit + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.AccessTime +import androidx.compose.material.icons.outlined.Visibility +import androidx.compose.material.icons.outlined.VisibilityOff +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.focusProperties +import androidx.compose.ui.tooling.preview.AndroidUiModes.UI_MODE_NIGHT_YES +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.longnh15.heimdall.ui.theme.HeimdallTheme + +@Composable +fun AddEditVaultItemScreen( + modifier: Modifier = Modifier, + viewModel: AddEditVaultViewModel = viewModel(), + popScreen: () -> Unit, +) { + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(Unit) { + viewModel.uiEvent.collect { event -> + when (event) { + AddEditEvent.SaveSuccess -> popScreen() + is AddEditEvent.SaveError -> snackbarHostState.showSnackbar(event.message) + } + } + } + + Scaffold( + modifier = Modifier + .fillMaxSize(), + topBar = { + AddEditHeader( + isSaveEnable = with(viewModel.state) { isFormValid && !isSaving }, + isSaving = viewModel.state.isSaving, + popScreen = popScreen, + onSaveClick = viewModel::save, + ) + }, + snackbarHost = { + SnackbarHost(snackbarHostState) + } + ) { paddingValues -> + Column( + modifier.padding(paddingValues).padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + AddEditFormHeader("details") + AddEditForm { + AddEditFormTextField( + value = viewModel.state.name, + onValueChange = viewModel::onNameChanged, + label = "Name", + placeholder = "e.g. Google" + ) + AddEditDivider() + AddEditFormTextField( + value = viewModel.state.url, + onValueChange = viewModel::onUrlChanged, + label = "URL", + placeholder = "google.com", + actions = { + IconButton( + onClick = {}, + modifier = Modifier.focusProperties { canFocus = false } + ) { + Icon(Icons.Outlined.AccessTime, null) + } + } + ) + } + AddEditFormHeader("credentials") + AddEditForm { + AddEditFormTextField( + viewModel.state.username, + onValueChange = viewModel::onUsernameChanged, + label = "Username", + placeholder = "you@email.com" + ) + AddEditDivider() + AddEditFormTextField( + value = viewModel.state.password, + onValueChange = viewModel::onPasswordChanged, + label = "Password", + placeholder = "••••••••", + isHideContent = viewModel.state.isShowPassword.not(), + isLastField = true, + autoCorrectEnabled = false, + actions = { + IconButton( + onClick = viewModel::toggleShowPassword, + modifier = Modifier.focusProperties { canFocus = false } + ) { + Icon( + imageVector = if (viewModel.state.isShowPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility, + null + ) + } + } + ) + } + } + } +} + +@Preview( + showSystemUi = true, + uiMode = UI_MODE_NIGHT_YES +) +@Composable +fun AddEditVaultItemScreenPreview() { + HeimdallTheme { + AddEditVaultItemScreen(popScreen = {}) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditVaultViewModel.kt b/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditVaultViewModel.kt new file mode 100644 index 0000000..588e489 --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/AddEditVaultViewModel.kt @@ -0,0 +1,59 @@ +package com.longnh15.heimdall.ui.vault.edit + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.longnh15.heimdall.domain.repository.CredentialRepository +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import org.koin.core.annotation.KoinViewModel + +@KoinViewModel +class AddEditVaultViewModel( + private val credentialRepository: CredentialRepository +): ViewModel() { + var state by mutableStateOf(AddEditState()) + private set + private val _uiEvent = Channel() + val uiEvent = _uiEvent.receiveAsFlow() + + fun onNameChanged(value: String) { + state = state.copy(name = value, nameTouched = true) + } + + fun onUrlChanged(value: String) { + state = state.copy(url = value, urlTouched = true) + } + + fun onUsernameChanged(value: String) { + state = state.copy(username = value, usernameTouched = true) + } + + fun onPasswordChanged(value: String) { + state = state.copy(password = value, passwordTouched = true) + } + + fun toggleShowPassword() { + state = state.copy(isShowPassword = !state.isShowPassword) + } + + fun save() { + if (!state.isFormValid && state.isSaving) { + // Just defensive, UI must "blog" this action already + return + } + viewModelScope.launch { + state = state.copy(isSaving = true) + try { + credentialRepository.save(state.toCredential()) + _uiEvent.send(AddEditEvent.SaveSuccess) + } catch (e: Exception) { + state = state.copy(isSaving = false) + _uiEvent.send(AddEditEvent.SaveError(e.message ?: "Something went wrong.")) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/PasswordStrength.kt b/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/PasswordStrength.kt new file mode 100644 index 0000000..297c532 --- /dev/null +++ b/app/src/main/java/com/longnh15/heimdall/ui/vault/edit/PasswordStrength.kt @@ -0,0 +1,5 @@ +package com.longnh15.heimdall.ui.vault.edit + +enum class PasswordStrength { + NONE, WEAK, FAIR, GOOD, STRONG +} diff --git a/app/src/main/res/drawable/crosshair_icon.xml b/app/src/main/res/drawable/crosshair_icon.xml new file mode 100644 index 0000000..01905b7 --- /dev/null +++ b/app/src/main/res/drawable/crosshair_icon.xml @@ -0,0 +1,32 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..9c3a5da --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,29 @@ + + + + + + diff --git a/app/src/main/res/drawable/lock_icon.xml b/app/src/main/res/drawable/lock_icon.xml new file mode 100644 index 0000000..7374098 --- /dev/null +++ b/app/src/main/res/drawable/lock_icon.xml @@ -0,0 +1,29 @@ + + + + + diff --git a/app/src/main/res/drawable/page_icon.xml b/app/src/main/res/drawable/page_icon.xml new file mode 100644 index 0000000..e456c04 --- /dev/null +++ b/app/src/main/res/drawable/page_icon.xml @@ -0,0 +1,34 @@ + + + + + diff --git a/app/src/main/res/drawable/person_icon.xml b/app/src/main/res/drawable/person_icon.xml new file mode 100644 index 0000000..82f0049 --- /dev/null +++ b/app/src/main/res/drawable/person_icon.xml @@ -0,0 +1,24 @@ + + + + diff --git a/app/src/main/res/drawable/shield_icon.xml b/app/src/main/res/drawable/shield_icon.xml new file mode 100644 index 0000000..ba30eb0 --- /dev/null +++ b/app/src/main/res/drawable/shield_icon.xml @@ -0,0 +1,40 @@ + + + + + + + diff --git a/app/src/main/res/drawable/tick_icon.xml b/app/src/main/res/drawable/tick_icon.xml new file mode 100644 index 0000000..a4b5fc1 --- /dev/null +++ b/app/src/main/res/drawable/tick_icon.xml @@ -0,0 +1,35 @@ + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..ef49c99 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..ef49c99 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..f69df9f2c7e3d6f4eca532218b106001176f176e GIT binary patch literal 1698 zcmV;T23`45Nk&GR1^@t8MM6+kP&iDE1^@srN5Byf6^DYhZJ2~V?EN8#hzX#r8vG0d zx^SayZ=`(TWz4PlwL|~4N@9KZRq_{shYDOoNXBdh#Oj2IkjxdZCc;IGTDTULoNXjY zP6dsR2Jh~*SOOpc7`JUBN&S~Qk;mEN;k4SeW%=FRt^P@X4J>kqWI8KQNiNAwE(cl!w zLZ@hE>PB{pG3{bbr_?X^?@9)Tz2vt3q>&$)fuyy}oz*BKjmnrQq-6)AU6AguDubrAi(m&Jq&lif)HHXwI!Jfy#q4kj zb(O;!ia4qf@+!J*zE9@E6ANIFh)8{ktjzk&lu=VA8*=h7#(Tp`rvFg zWLU=i8!J;8BP$kGk>X*oBBh%gS?96UAOS1gYubx&)?h&xV5{s-k*aRTqQCg4M^&?H zse`@Sl!&Uh1G3GNY;DH{LXIjDZ=E|WP|>e4Xm94ipJ|8$BV^D5>#11Fp8X1;bL-ga zp?)l6=pze#C1d9f77y-i=7*Dl<_B`k1MUDxyA5pRNG)?(nzuNd6t>*Gy*iE|wk-27 zuDhErY{DRw7pPAue%zWsso+IZ;!;syIm6&*Pr6JMz z;&8Ac<<;yY*;?ipA;sTYT&B5=uyG8K#oLRWp6bChL>A>NadGy#g@g8*%p9o-;*mcS zmwIiPN14cccUDrZE%RWixT)a1CHY#c6b%FZ%I-{kF2>aFwe0dCUm8|^6j+;>#qMG( z3|Ao%=%~rcTmd|0@^{4+WLO4*nplh+XMhdBEn@-v`x_B0u+n|YEyph)A`fq@P6faK zX&QjBCV#N;j#`6=ZxyP@XBCKZ>)6ifgWdHj{uwAa<}+f(-0W2!>>7-xIyiC9+ zm*P4&XBsC3mEG!MX@ZsY>!?P7I2vHX1%14gZJxK&c*ihTp4i&_v;mUtbEH{EA6tWIU1s!B94Ov-31WU(S}^}*4N#CfNfH$K zsLN~En@>dUax|Ro)Dwwl{uB3yJBuV@;>R`WpaOBs4<}k+=EF7%h<@JrpJ+#sROqEd zTdzTQ*m1;b`a`Ove@nRcrP7IYwA&mztx4r67oUfcUsQAFCP&Ppr1MyV>8?=&o6P6? z2^FGi>Q48Vbdqj8HsRJYg=Qw$d+fu~aagf7(fytD3+-BOx(6H=NQZK#87MhKP-w8= z*L#wL>@@tR0TTM>JX;M?qBwgg5ffUIDEd((DGLL2>}?&7nx?K?9;!0vzf0b3zba9av=f&!rSciI3L0eyd$!dTAEeC)zyWSnbJti&O>$p= z;B2(v89>>re>SOoNf$%`E&o%J``#>n;culqfB|@b%=T$*Yr9WEPKoD`gd9u#yu4mM s9s*c8jsOne075_nh)X};0c-$2x{v@8Cki$UtXQSLy8WQnv4ZSV0UQxgvH$=8 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..16997ff76e00393e4fe5b3a6b177b78049106a03 GIT binary patch literal 3202 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..fa35a46a042b8260096d2d9ebf5bef7a841868e3 GIT binary patch literal 1140 zcmV-)1dIDpNk&F&1ONb6MM6+kP&iCr1ONapFTe{B^#(z<&FZiHJ4D0;v|-&s8*a93 zYpSop9sYdC{*%V7e{qLC%mA%_1JGuLe*idss$6U%Nph-b(b|6fsNFAo5+DiKwvFUS z{TKS_nb*wQz-<5kWE0!AjVsc%@A(7cDrc#&)?;XUpJMGwJ`F~Yx=f{aFJMS0{B-v_WP2}*2?qUvzA4M1dW&D4yuqpL_0uYz< z1JeiM`?oU!kRX5}4g?rHTkc=O>vOn1udaaD2>R}zy4nM#dzKg9<#_}F$c}Z6xcCpG zQlB$GJ2rL|Q}eSVvS0oJXUkW6``u2xcYHPG+7>V=Q;oK=-wH+}Ez=Iac`k zBp!wHvI=zc92$#AV34xYR>bf8uEEm4lK^Kk>ZS!C>^3AEah*jZfR09Ng6s{TyFS|w zL^x^~pbQ}3(JZujRtW@9AEfc62#pW*odA@JYtFz1Vh%Nc`am)2U)BQxm`QDnU=VH_ z0B8cI(;-5qXJa@32)Z|eJi~IvbGxl2;-`v}b6Dxj%SCdKX#9$Rl63sw7E7y0X6->w zeZbd+BrrzU|IxZVo!k{^os{K=U&qM}-HyOVkK@LgdBi|FdzU4%@nDcVpfTJ{U%yO!GzAxWEkRY`>jHV~F$u8i&DMNW5mKv?}xO{|5G% zOGbf@Uh^A^n}M2xj-KP$-O8$k)zXTQn9jUrD@ms$oL+x87?kW`Ag4WDs!ymnGchW zJx>p%7NEMog86H{Ni4_+ap*>sz=ptZ>%1wR_FhBF7|Sb_cq`mG2g}AS6St0&A%fVD z-3C>r|IhN@N|6E2?r5|p#xi}>wK3XJIWSH6>B?_zd5u_+<5n%L5=_s>)mlBB-e_xu zjrLm|^nOj;d0JTQCwAoIuBLDyw^8W*DfhKW3ARsD&@GY!xvJG1F3{1$4S4iX7ZoIr zRiU@}q|=j#w0jOz7it@yXUQbW`c_s7)W=l&l1R5_<-pQ$W6UEH&viDGjpQkv*um(t zOw9A=-&=`i&m(i0kn38ltazGRxvU_`SYF*ILHGVeL@#$JrfFGgyS6`zmm6s_oit5a zEBb-aikHK;XU+7<@|*dbPPB35Qa4Xz1p*KOqH|`(@~;DEl>FPt6#-IUMF7tAZ{5z_ zs=HUkK9YZ5lmBHqc9-kk_Fw}^3512^?@HU#_r0+Ey;CZ%7XlaongHkf{w#3(K`*H9 GABamG3Pgzj literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..c543f27638a31b0f8006e09092712a9c7a6ea7c8 GIT binary patch literal 2024 zcmV6xi2+-(J9972aRz=m`SunlK3u!cw0T|IMm@XPvK3GsfO;x3s(pF>PujfCzU z<2ErjLh6RdWLO=tUyQpK(pcs&0mMd<6v@i!?wS9Wd3L+2dY;jNcVZznlB7sV{|!BU z*qyBIK87pVwrT4c&$f-oMrPZ#mD&6N+b_0l=2!8vjeX_>$j!Fx_@6Z#+qSLBwr$(C zZQHhO+qPAG&-*SBC#9`1GQt0e?JgN01K4P7_u0V7)5bQwBXuf$$Ctxl0DxtiZEUAT zxG4;Pr1AfIBNt%*Cjfm5pvw+Z{$RJTwq+YI!2%fa4-0AahD`6JG)>+N&wAo^q+>G? zO$5+;zME~tm1dM46KnshFg9)<3M9|q>og*M=BxNYc=}2B51ke$~09wFkcQWBIX(Z7(f1bI0?a4DP2im?` zz=WkH{HGt1V6flNuA6= zgUD{Ud`AyR(t9JJE#-)o5*ulA9CA#O8%rEgJa7U)KN#4CTuL|k8X$e`r*bF6az35T~(>2c4q%s5NO4HT5_XDTIW$0oW|HWc-bCWChbK~Q&g64{7FJbt=)((dr|5MA zDkTFE0no|{b@+ucsnl3m%x;x5CA^699`3yxR8$4`^KzAx_L$57vuj&U6@o{A##1YGt zkqWT&#P<05z~}?7J$5~+KwDShLd2?ETM^IBWN6RLhTRwlph3@@v5}7!LlrH_A@+cm z%=zV7!k8rmXPWTL#;oIPUxVH_y&Sd zTT)CalO-Zn;nFm~>U3HI$uN4~Sjgd%@_kVZuy+&coQ8P}L#{m>gkbm*p>r3M%&pm& zI^f=-*bZBEbSE)1bGt!3#=u`M*(6VAb-*qng;j-53R7dT$tQn zF`weij}l9nl67xX0|aojyKkdos~S?pjk6@FU;kV6h8HW2!>5{2BQCQ0WX!a^Q25_F zjoZHP;)SWt94(DRte)jqfE3~%$}*gTv<_smtj-lBbzAJv>jd5U_uG%9q)Q6AJ1do2 zIq=ru)@ea5Lh7BFEwu~~%Sd!nO$cDeGIUcF(n!i#=w)fCe44vogj(C@(T^KauXB7? z6M7x+c|AL9?#A?--IY&maVhN`#B?U4w1Upi6`^dgVHF##ywFHuBdz^a=1S+*#+T7! zYnZV8W?iN!ec@H*w(_OMiRf`GluYAUL1-$mJr(P9{i2-_0|dw@uTAQlzZ_(;{7OO_ zHOxo!t0lQs#CF~!+7vZ)MIqw_#96bq^^O2}1jIkOQxz?DE(cw$&3!^;f|Tbo)Npwj zsmvE*?V4QL1}LYSEIM6R{1Jf$YoT=&OGp#`=Q;Mjv~=*bB_bjz*xF-_*ME#>NM#YN zi7`L}APV3mV^=PpbUCiMzuLoDz2IpWPoQq1CyGgvROjPxlUl+0)1$790MI~yX3y)K zbqW0GaR>br-6v;9(=U)tD}Gtlt)u% z2V(_`&fN0?j7FCruk0|-5MZ37nJQSj4DOm5GpWrfc3uN!x|+p90!+?`$RE!TV32gv zq3Jg#YnRIJ>Pc$%JL#tLxicdAZ3J-6U#ED+^-|6YPS)P#alW2p(OYu9&eJpD{M#-; z|3Tj12v8JYxnVzzT^nIRa-5T7@xX@tGzqYJdNyF7gK_X=2leAOgN&ve&1zE)YTwCDcS)z`jo>)fdVEdCe3 zFB?o?6GsB%{(tToIm2$@EnaIpyuClT`1fm``~SvKY%swg&d!eJ1V{^zeRjp%Xts<2 GeFFfKfZdS* literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..94ad62436794efe73af8d98388c04008419914f0 GIT binary patch literal 2386 zcmV-Y39a^0Nk&FW2><|BMM6+kP&iCI2><{uU%(d-HHU(>ZJ3rn?7K0DhzY;abaNjvMt_`G#Fl^h#k@g?BNm3ermjm8#dj|z=+c4vgcir#W{|S&;)&Ma4fioD7 zM+m}V2-ffeA%ZW+Q7!`FE+gNij-Ph5A~LWPr2c|3L^Ik`Cb=egB6&P_`tr4R&UP;) zSQ*F=)oD+UBw$i2J+c^NU?Ip@d(gC}?}@&&=R@+#;PJIr?4V4T5^NBY^~(Du`u8jz zs#D*Z0qt>e7GgmFt(B_pdM3NRWz$3oL1L32)bCP~TTj6s7yn*Q?wX}K2}S_WMjHd^ zyl|q-Yqc2Xty$b?Vq`^GF4baOu^9loUdPPJb~PJM)NwlBP?@4qG{)2#i&7M3i0RPr zM%~1M_^Fi8ZtEb@8dH3S;v7^V{Bv_xb)h|GZPXe?7R9Wv-_eBqh-MaQqS6vmODxk@ z@hE}M=22djQJ1nJdw?`RaUEz98Zv4EloV2t;CmjcXYr`O!dV67Yl@j!697^JrBp|g z+K>?l07(aGN>xa{sVgZ!W6E+|`Q?Z33jh)A>3UObO0;elif#@7e9n_!eKGOM6G$hy zAw3Z4#;S`5Ag7|ey-M=;?*}bl*rMX??6opLEmU<9L2{s^>LlX@_S*cJS5ZoW}U=b2#b+IT=QuVawxK@uzZ9#)3M2*Dg{5nlA^k^MtnytLy%#b2cPjVaSklID-1I&Bg$w zgcM{D%qyn*8*ltHFG1sFxqy+&PY{1P63m+nj-Xzw;^GM)pQMTuJki}%BskqV@o?$j zv+JKQ9k54-dn?N!s)N0JQTagF)CB}bkYLv|ngrluo_eqdpaT0VVwtN83z*hH?1Zrg z0m2)*1;Ug-PUqoKh(fqcJdlXh@y82ihbX35+3d9yp&jjg zcQxz<8^8jnflAw(9+m;{J+Fx?U98=~Zp<1)c7yA+4L=#g55Q^q-mIA#zy;S0vY)-Z z+2p_+U-Lo^{Q<%*3y?)qUO{NPP%kk%N3Z+F49vEd*j!S>@E1TZzX}R+DFWQ*FHAS! zeHjiW2^QHs?sW8Z&bO`#+>mbHNDkKswgc7@)m{Hvy%8O}^tH~md51n*QuC;0X(+>A zASoRE90!vIORvbQo{nI5rhM|@f_7yUWCGFydR|6sbJJJVNS~o1dt-N9@=!g*`kzK> zh10DYK%%oN>U%PhT{^4&%9uo0Nse;-=}>MJ=g6QBK5tb{L0Wijm9Xyiel?qZ?%ipD zYEww7h-{YT#++_kOIOd<|1|P4CZkL9SCj*j3ag1xfr}eB?6}DW(mUMWNk?4BqNz59 z*zRi#d2MH_0XK}@9`?6vY6c?7)cYb}?!0Fx`KVi6oEb5)Z=YLf%ws|?tH;nX~(j07iM?VGqXVT2B zpN8J79mP|T;&XmJ-f5fnwh(4u=(Xx5_RePPcJ1%Q!9$-n2mnBNE?qI%nB#D(&4hiv z!GJ=Rt`hcx3h?w!x88igfq58X%r@|3%~Sw?YKzIk7~^^WsFkXhwWi0EfJJq0X7iI5 zWxzwP3=qjK8#8v%+YU~*7Wn~F1fJg0GW_MZo@c~QgSP1)s)(>4B9eNUpF_Uyq#4)J!BgKD1tBB zKbp=0|2CD(yBMSZy4V`+oD>6Z_CC+NRDz{7d?oZD+K2Bv^#Z08>~&dgN;pBJlmoe* zm&Oz$wV@>c*#wYg8$(KUs=+podO{Fs@y#&haFW0Ypa=}ZqaM6`^l4FERGtG<5MFc^ zgsPa58p9KTvRtgx>sVOXuH@sj@kMmUiJG#nCAFKy5%w@O)AC&|y1q5Gj}81NG7)@1 z0+8E%^F)F6yovsKIPB{nXx1$(uZS4{p}2h=?J48cKFQBNgU9{VX5VS%fCqqxD=0Gn zC{hRj{POXMw}wu&2|SBi&AzkJ@lAgsfp}{s00$lO@jq_j8t;{Y3 E0N!tnV*mgE literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..157762ea5df2719833d245dba5cccd0d3497d704 GIT binary patch literal 4556 zcmV;-5i{;mNk&G*5dZ*JMM6+kP&iDu5dZ)$U%(d-HHU$;ZCd}fxBU_#VgmTI7;kof z=7~alpelF2(*G^8)&JiMneBT`&vf^F;~W5|z&UW-oYtD2x$o(Woyan%JHVY+NUb8Y z6;eCGHS2gat~+|m7A&G_Y9|sSNm3+PS{}plV8ixQHDvYSqXMtQOKc;_k)-h7zjHy0 z7nPY+wcQKIf+X3rE!(zj+qP}nwr$&dw*9`YZQHi@9SD*nw{2z*J^*(JPK}x=p^AV1WBy;iW>P2x#>AkSD$Q?-<9aN5{9l_t0DGa|MhMnDw3@#60PSThIY-tx)O=H7RY85x)QOj zB!z)3CG3IKCezy!1y767I9-LOlbhND8!Uq>bJGvI^ zWS~RufZd!LKCspZaaLd1CuA6;SytwBk1$Qc^e}td$z{Anp(|@$4AHh>Qv3H&ETok{=uBZ=w3}BNy|bEdtiMR7L_=m6AiOKe(M29*X&r#kxbPAwE)XWyuOa) z?Zk+q?sMkGn|@}}SjR|SKf100tpXtZtkoW=(vDK*RmLZ4D^|-&@sTGbGVs` z%+vvpJYSWY_{yPLV3tCM6{SeK44OSUL_Vd)e2I5ZDq(4a0Mar$buRppa>J60-)!Z( zdQx#Kl5s$+WW0h^Lx|B}XpAr;IRne4UQd{${=>n51O>rTl}=O+mZLXW9KTrsR>_31 z;{cE(NI8h>lT`-EY~F}pn?Y&M}c$Hbcu0oxSJ2|@wu6J3f|)D6EcXdos}}|qPct#aB522PSP(( znW;O6-*dT&2bs`&izhL&X3LKM$@hR@S`a}*U~*~`f;9(wLq0MM06Zkrk!X5JKL_nEX` zP^6=GeSVs!XQ&U9#E7l-bLM2DW?OT^g1MvvwlXOTaa-H)CTX&k*B(Ee138JK$(O$} zyQwETEBFK}X|&6lDurI??WhG9O~ES{G0&lN%pwJZi&Y?l(urkOng0MFVCl%d7Vx=+Mpu8s<>pW^59Oi-Q7X0$%lqD553 z3ez$r@)@*F1vRaat-8aia zq|1-L{tni}g$9YqW>^AB0l+ZnMu}r;RnX{=jyYag{0?+Y&c-Yqcj=nW`;|&$0>EPQ zPcg566?b>>^?AMv49hrui^(z6|7*}KN5KpMqetZ_m8wnRz~%KSMGHH~>G7_>sUmNE z-*FzV+)LYpB0pzWhD03pRe67Y9_ghg;Ru<^A%?2y0ySqXi}7?7p+L@fkWEEMK~w7( zZm=OyKdV#$B>{j`8M0R18DgvWl_GGL-TfKQS?a? z=xSX=c^Q-WJZm@Nt=h@SX4F27&JlAh#G-{bbuPwUj*Rp{HYvIFdsem3fL#^s&?a3C zKtCudi4dp;nvbjIhOBhVcFks1UzDvaZ6T*c3rq0X^yKGnx*`j8z=C;+S5ORNrAT!9 z7R{(&m1lv>)!uzG`4VmC;!w7S{xN|bb zBF?}ZCLhJ>U zj%lt&A?qVKw_DR?BX2~EZLvhzM8I6;OH>I><{4f_EAI(5Gkd@3@({Mtcz}VoS`kkq z#Q!BzV#vnsRyvvnSVn*wVEN%dIK^wDP0Zf3m6Xv6e0GcD%!PLma> z*p(yRFY$K+`9fQ~}cV z!BXuBq-j>U-U^XWB~)^5HBC3va9qucDXIxr+##M$!`^xL$=g|PWS~#6BD%k`OR0fl z1&0DRJ+Wqp#mNJJm%#)8aAIj}iF}e#)A{hn4o;r9dO=9(=@qA!kBCjPm#=4F{m;7x zUW>N1hR+Acjh}=PDP24%;sQl8&gS&ZcJSbnf`?D63Yz=lS#P&I73;_N8)+3TYH7B?X<<>oUMvJz-B?y=|GIpLL&w|UxpiP8x% zpotdT=b{)5vDBR>y?|Ce(YA3WDoCR4?;DkeC9Sgm8*4378ljE>p|7L4YJmZ&vnwpc z&*KSQ-TNPHtFcT~a&gx3gh3w zb^u7UI8KgqQwWtPu`7p$ME!IuWU_FJz_Z$h6Mw7{_k){5@2w&PciXInX72dCT^>nl z0>FbO*je!uc_go1-jWjYD`XR9?}Wm1P?wM~MG}6bs(=ns%DGI^HFNL?Z%uL=5dwJg z>epmD={&w&KkPnjF4FxIGFPzl)aOBLEy>y(fvmC#9ak_fZuZ9WgH}x#iF?aEg zttsW&kQ_AHU)7VoMoj5of)D^hJar+XAr1T;l^=hD4WUz?sCqHY#pPn2ct^weaSpA7 z>(EqR@>=Xq?dR)=D0Eu@pou&jNmI4#)X7?Ik{Bl9)(on1o{L<>b5|rME?MpV z;mSU7p^|wj3MWVbFf|{?#jJjip3vQ$l|!`|a~qbJ+qSubo4dFl(I|dsG9B+pH_M@n z?uR{8061cP@>mtf=KPtdS`@@d7hkn~WMJ-3?n&C(CFi%iU-E6qwNC)xiKTg-Ur6%s z)WxR~t6M4?t4H2~WHv)I%$p;gs8O=djaL9b4?r-!<>yA8oQ?JQrYto9ixIm|zTL~m zV*dpo)L{h|WtH1(QUD@x>26uOxEYp0*XQ1~ZTkR-CnZUCb%Z^X09bB}r|!uwBsakG zRmD-#6~pNMO1^t6#F{`$+YdnMlJz?(OvCPP34j%VsFvneYbrSse3P&rx+q)oTstM2 zMvLz3Ji`KjxAFZ^&|<&;!0|oXDvswnMhR%4KQ_9*O07PBJ5lVg8~}WH zfe`@bAKI{Cddlp9Qf6vfOJ*u%Kv6X#|qn~ygxh3=haU~fN-PxtJ-|NVOp3D{hA>|b6{p8 ztO^FX%Sq)2X>ArG|IxP$Jy{(1X#o7ypU;xD-d|Kqr3L^wyueUd3qT+Ogd;$*$?Maw zY}jCJ>B4B-7qPAX#id6K@uH#LGD`N$?8E5~AOHQ4HjBo6uDLBN8#ZY5nV;UTw(_h& zY9wklF9!holLLm`=KLe}`y&EGB0&7cMz~51pV3^s$;&k!Y4z(H;VRzv@P2;)5IExe zZH7JIlj(D+;y>=var!I(QrJPW_~(`Ft80%qqE)L_N7Pogul#)RUrx$S-!b_EFfBl6 qyKA$f&j)}4c2NIPu3284Z}jZ7+og=PAR*Nk&Fg4gdgGMM6+kP&iCT4gdfzkH8}kRfmGMZJ3ll>~1_!uFa z4qk`|grEQ^sMrt@XxK{!XMPH~9lVhBfF#oKKunII`wq#;Nm~FCRs;19aRBM{B;aDx zF3gOP_Rv*hgA4-`t`kz%kz}i?s(J42_?2@WReX{FNdPI5BuA3-N4WrYPi9t)dcd)5 z?b!3%wr$(i5bVE7WsSh8lDTB1{QI!9o!e#Y3gZQHhO+qP}nwr$(C zJySi;bKUpz&2|5PVKuRB+o^2dQRQ-GJwavHM)jeKx>WuJ&q}At701(uFWdF%J>V<#WVIK5eYe15?CedXQk&Kzwyk3me{#&chUt|6KV~F|L zyxz{tStJIW)#ZPvHG5inqcxve^RG33F+Y)azxVygco2aB*Clx^Inx>-^I_emzyo6g zR_}Ja*BV5fMYIId5}d%t)6?E7ZJ!5*GJfy9I{WX*RN8;9q@YY|oEZ_u7gcG`{c}&~ ze;>NJ!VQzfL?&AP$3FF>rpg6^$4pDhgagP*yYHuoQCbLIGXW^pfJivKxmt7QAJg*K zho(=f>PSTz=WOlzXVsD~KTgZIubF(8*ZQq(TsExU<4)-b-5|wn2{kk{{2SlaCc%c1Vl2)uXy<Pt+qZF7D*t$VvMe09c$(qnCT{7$^#i4NTEK9xyiiB57S2VfPD4&??X>Km4q}m9#lOH}RK^M9I9**t08p}e z7vH#irCg#^%~T`R#RJGuh9m&2Gtc3Gas~a#;GI{^^EU^eVQkRMeBL~ULsg6kZzD=l zVX9$5PUGb?orc!j#ydRc8Z$t5oy6pE2AqP#4J<{!@}?M${(HXJsh$d~MFV{+AhG?( zC{aCFt77s$1Qt@hS-nh&pwONFCd^4 z5pX9UUvo~l6G#e|0s@POz{LdAg*6xMd=~<@6oHW66o9C&=8ebQ3xO9B)mb(`tVavU zYdDoNF7_bg?l!>agp@7WJZCN_3%RDMH{(-v+S6Q*OW&HW!f*9i5^(Q01sP^z|lW@OjEP=O#rT5h>gIFMlt6$v(sI?5c6fVEnEnLka4+Qk;H<>ZroI^PLQUa&!palnmCrMs$}`hTgd1=uEoM zbi7;3EP9t?+bhn!pj&ao6<=zUp~ztQno<-27`oL`7cnv9`mwY$h;5|+EWN8ZFT+F+ z#S@E6m(9;SL*2wax7a%Y@{AXz(P5QbB9tcCx`xde1Gv+d!h7oIy5w1AI#;~bnE~a zQljg~g5-qPo&q4J>SmV;PzB?)!hO;v%f*dcA+H;@1vu4)HJf5OS^+BK3Or?6c4QO^ z|L7+OsuwHhm?f!)vdUm$?g3b7*QOqzRsI){`*2=2C%#aC2^Dd|ya#hF4Imq$n)`S% zkUw1W>DcYIaCnbCubxs+l}ze+U4FyY5{1A64I^bSFYU;nWmZ$Li4I*5Z|E1bQbP?? znJilsmj>S4{9Xvx-nX6pyNJT4#78^oU# z&DDU%o@|^Nn8Px*R6P^$`54bOeTk;MP63bBOVcvNc68mufXW#+=vnuZ1Hhe1R*dPW zYAJEE-KiX+WgHu*gmYR`RRG{k_Tt&cCNPmm?joYmor&qB5K-6WKYx!Mv^b_kHGmDk=O<3d`rZ$)sR*wm`t-G#X5>u+(NT{M+$_n^WJdvD zNX>0fF)smmiX?uc?KLd|`&vlvGJtXfeJrrIVF2AOmdHgFTnt-X2&s`@B?DL;eY#>H z0Dp~rui=)e2=w4-cWzODDXOCc3N;+jP0azUc^oU61pvggw~74DF$f(yeK}bqx;6kM z;dRuuN|z6yW@?*lYpTFw{mDW6qL5X?FyNj#$n?)n#2n&C3W z<~svi_OlGr_4flbgsqS5kaZk;SILN(M0d8w9W-%E8Te2fd#z}n2UscYRJ{K8s2ST` zDV&D$B=OC-yAR>P=`KTU6<}V$rIHC_GRAQB<^c?NUO!S_KG!ju&9L`snE+JI72%!D zPCkQWUBYGd)7qtU?HKT{XwFhOM;>?o?#-Zlx7llr?&#Rm|D8~%Z@v@29iDm(BJwv! zUgnMt-09iVCNCRy?-DlqacgfD3Kk#NRwm*db(i6?D}`Z*ruLIu#~e3H-EF7je~Kx{ z06IKEr@O0I5J0yl-wBPV83TUmXc?v(t`vX0j}zh_b(hn)cf#VEGn(SwdhxKicA~wU z&j8>|#i>^SxIDnMh~9fCJR-WY$k?1Y+u)H&wHtIZf6#5s^2|v!3_C z(0;R7oREm9pR3%S)G(er4sgj6k~EOT$$`gSjA(MC)$PY+Ee#kE}U zRUJOnXidiX+9Ii;*_{YV-sW1-CeVwH87n9V-nSAYJ+z&{Ldj}glL)2*C>WO?C5WK$ zE@L~zs|Pl0A;_X1pfJNRFXKl91(Pb2CulAe2?4uQ1*mfcDi!iKgD!YCg6`Odm(A5% z0ex5rIDBF7zyj34tU1iLD02CK3i%C@LVuDHTgMP#A6CMmKgvwP$TX=n4@QXNlN(9O=+Su%FX)n2CTXL2pHP7H*+}Xd?dyL^ zmkFixgl>?+yjc^&yu8-`nV!(}yD)AlPa0?Q73HY3n zOt9+$0ziEP@GM4PF+$7Yr?jfR>MQHJ9s8@QtMP3y!ZUY|{GIDMHYzrs15|(oDG%VA zpCNF5{^EoEbGa2WEH(#tfE3UGM!*g@FUJ1y(bFI9jpbI@v{--!u*RBzY!*;n-yiGO uug@N90{qEqR@l6VO}bAf0d}V!>z&Tz({5r#IFcgHSpuCC;Rp+Xytz{5y$ap{ literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..11a004e4b4c1756f635657efa92804b916eb6d4b GIT binary patch literal 7530 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 eTyPqy-!d9|@_Pp;i0 z8IT}5C0cfGAb><^hR&(adS*@K1@vNT*c9*bvXbM}vFBR>UZh%e)?QXHOnw4L6fK{b za(lD>k{ap#YFYp{CRjp~o!iR$vohFW)Gg5uRnH6S0`yTbojJ0kY0Kn3C;=oSfHZo? zV**JGFW~n|b^C1acWl(SmDK?!tT8odRh%P$v`amxsh=qc`5{iW^ix*wi2TlH`#j~L ziv=(lfb+Qn_`4=}!^CgARlM}JWhp*8l-}w`x}mCPGtR==MCBnpk`O>@%^Rb`xlpDv zcab*guAQavPGIcSV*7A9`vdU*Lq6v%eUQvDroWc|t#?rSvFM+&Ihz*cQNPvGS7uhv%^&`01j_eCKqZ zS^7pA+mxARw@CuXpfRy7oQLW4z7clK%FDxa=EuXPPsaVpZL8Si>`ZJ5O#~1zXEg+n zQDgitm(pTm##bI-1@?8}T=pl;8z%@Lb2S7IKf=hSw6O;qv~&pq$f7fLv8EU+DpoJI z#?D#rSb)PdI<65w_7bHJ8en&tITTxDFzJzu0MewZJFm{#Js#!ZN7gP@M%1qvai}xb zn0T13?gFd2{a{Lv;+W9rS>{UHqo~&TqnbEuSchj#^v*o0R{1|^JCw*pI;4yaH3ab~ zQP~U3(x;r(`dtnaJXr=M2GhP-Xepjd9M)frjt7tKGs^(Z3}!FjAk#nrUU6!iJ_*~J<5Cypf|nRG}+0O`{JvbpOxq)*1r zp)|#LG~PRTmbpR2d!?T~8uvGkr2*vlfXydIL~;Gk4o-VmMDfl0qyc1c)3H9)swymN ztb|CO+}V_dp)MG=r#7bnWcAs6@rW?H(`@Ekk~f(v4Ip#oCnp~L;#A})K6|Ck``^r0 zPIKkjWU_JPUE&HXl(a}s0GZP7+d^yX-|z)Etlp;QSU{{+`_uH>YwOw}0VFFBM_Yfn zPRJfj0Eu>3)&02XJb0@DON@7^G1jYgf8$8ndJ0!XT4cx(o!w*x_ij`u0YqOaX_0Of z`Q6SKr*rl-+)qRXtlprP>KCqc{x$QNlk%@(hfDPhWviSP4&N8 zuHwY43M8smPqI4HonN))iBsvstLB;>0i?=VC^AX;A5JSerxHM<CmFT7Q=Lh+#oh-3tLENzP+*4)wwk7DoWlw^O1jS6Y~Y?lP9^ynT+v z7_SPjI$|(fXYI4woh#GA6n0g$s_pp3h0&|mq=-CdamkoJ{Ww+JrEk+)`x%R>hzwY{ z8?4q;6~n7>e{x5<=~XpYiyt)7G>4ZO10tfgb3CL)raukJFm^q+CwA$rzU_7Aeo|`A zxffuKP0>i-o#EFFg8sD3@oXnLBEZz@<;JYZIaWm)WG)tzZhF<+?^v$2{!|}jUg&V} zGFU@IWWa%ooAzAYuUL_8c-4c7)xN6I$ysHsUYjy|q2i@;P47>GDrk^D^Hn3XubtC5 zecP)*f(})&%NpWP9q1eTKsRyE6*MRi^rzFyRl0bg?3A3|!k82g7yNKVR#oOt^QQr1 z&oFe|Oi#_#YQL)U`mxt}Av)AlDOCPCwpyjwRMmmFxY%d-p6|{ubiG>g|7n?36?wSe zxPTT!5;osi~P6Nmp=)8Zr)xCz#%a_`<`p#?SvD03p#>Rt<{&R+7I#<}CbS*(ryid06 z%&rh6!W(wNLi0pFT#k}HWs12ZR~kT`^utuixOT@(SB=g5)Ka%6cDm_2tucO6W8<;b z+Ebmg*Sg^EblyLF%Zc5;*4a~yjYk^iH_vt-N0;g9F{fUl0zlqB3(D^*R>r4fj)#lR z8X9KO^PU}BRJpjU(GR!Og%UvOdnr@qPYt1eR?CHbZ(5k56?Q07p+Ncc-K&^y@t1v1 z-tzYjCIg@=Qiy_{)=lu<#5eE$}Q2eKtT6&?`N>)=MS1uo) zT)9eAQ!?8^OE2}Oix&t!XhbO8k|h{yNl^ij+2isYG}t$~!0eKXOG=(yfoR|0gXW3L z9vKjoWTRQK9G4WD6crXwE;2IINRg4{0>Yw_q*%HyOAD3K8Dx`W^aPwvuZ}eX07xgE A@&Et; literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..663d0bfb55093363aa9bb3302276049677b1196b GIT binary patch literal 5204 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 OEQjZ{1kkZ-T?7DA>;OOj literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..f9f24789649a0b11f169005ceb4eafbde6dd5827 GIT binary patch literal 10366 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 + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..ed14948 --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #3D170B + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..0b3f407 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Heimdall + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..c3725c6 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +