diff --git a/app/src/staging/res/drawable/ic_twotone_content_copy_24.xml b/app/src/main/res/drawable/ic_twotone_content_copy_24.xml
similarity index 100%
rename from app/src/staging/res/drawable/ic_twotone_content_copy_24.xml
rename to app/src/main/res/drawable/ic_twotone_content_copy_24.xml
diff --git a/app/src/staging/res/drawable/ic_twotone_qr_code_scanner_24.xml b/app/src/main/res/drawable/ic_twotone_qr_code_scanner_24.xml
similarity index 100%
rename from app/src/staging/res/drawable/ic_twotone_qr_code_scanner_24.xml
rename to app/src/main/res/drawable/ic_twotone_qr_code_scanner_24.xml
diff --git a/app/src/staging/res/drawable/ic_twotone_totp.xml b/app/src/main/res/drawable/ic_twotone_totp.xml
similarity index 100%
rename from app/src/staging/res/drawable/ic_twotone_totp.xml
rename to app/src/main/res/drawable/ic_twotone_totp.xml
diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/utils/AnimationUtils.kt b/common/src/main/java/com/yogeshpaliyal/common/utils/AnimationUtils.kt
similarity index 100%
rename from app/src/main/java/com/yogeshpaliyal/keypass/utils/AnimationUtils.kt
rename to common/src/main/java/com/yogeshpaliyal/common/utils/AnimationUtils.kt
diff --git a/keypasscompose/build.gradle b/keypasscompose/build.gradle
index cdae997e..fe821900 100644
--- a/keypasscompose/build.gradle
+++ b/keypasscompose/build.gradle
@@ -1,6 +1,10 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
+ id 'kotlin-kapt'
+ id 'androidx.navigation.safeargs.kotlin'
+ id("dagger.hilt.android.plugin")
+
}
android {
@@ -35,7 +39,27 @@ android {
}
buildFeatures {
compose true
+ viewBinding = true
+ dataBinding = true
}
+
+ flavorDimensions "default"
+ productFlavors {
+ production {
+ }
+
+ staging {
+ applicationIdSuffix ".staging"
+ }
+ }
+ sourceSets {
+ main {
+ res {
+ srcDirs 'src\\main\\res', 'src\\staging\\res'
+ }
+ }
+ }
+
composeOptions {
kotlinCompilerExtensionVersion = "1.2.0-beta01"
}
@@ -67,4 +91,46 @@ dependencies {
debugImplementation "androidx.compose.ui:ui-tooling:1.3.3"
implementation 'androidx.compose.material3:material3:1.1.0-alpha04'
+
+
+ // XML Libraries
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}"
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
+ implementation 'androidx.preference:preference-ktx:1.2.0'
+
+
+ implementation "androidx.navigation:navigation-fragment-ktx:$navigation_version"
+ implementation "androidx.navigation:navigation-ui-ktx:$navigation_version"
+
+
+ kapt "androidx.room:room-compiler:$room_version"
+
+
+ implementation 'com.yogeshpaliyal:universal-adapter:3.0.1'
+
+
+ // dependency injection
+ implementation("com.google.dagger:hilt-android:$hilt_version")
+ kapt("com.google.dagger:hilt-android-compiler:$hilt_version")
+ implementation("androidx.hilt:hilt-work:1.0.0")
+ // When using Kotlin.
+ kapt("androidx.hilt:hilt-compiler:1.0.0")
+
+
+ // zxing library
+ // implementation "com.googl.ezxing:android-core:3.4.1"
+ implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
+
+
+ // For instrumented tests.
+ androidTestImplementation("com.google.dagger:hilt-android-testing:2.44.2")
+ // ...with Kotlin.
+ kaptAndroidTest("com.google.dagger:hilt-android-compiler:$hilt_version")
+
+ // For Robolectric tests.
+ testImplementation("com.google.dagger:hilt-android-testing:2.44")
+ // ...with Kotlin.
+ kaptTest("com.google.dagger:hilt-android-compiler:$hilt_version")
+
+
}
\ No newline at end of file
diff --git a/keypasscompose/src/main/AndroidManifest.xml b/keypasscompose/src/main/AndroidManifest.xml
index be22dea9..37bb7821 100644
--- a/keypasscompose/src/main/AndroidManifest.xml
+++ b/keypasscompose/src/main/AndroidManifest.xml
@@ -1,25 +1,67 @@
+
+
+
+
+ android:name=".ui.CrashActivity"
+ android:exported="false" />
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/MainActivity.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/MainActivity.kt
deleted file mode 100644
index 78e5efb9..00000000
--- a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/MainActivity.kt
+++ /dev/null
@@ -1,89 +0,0 @@
-package com.yogeshpaliyal.keypasscompose
-
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.FabPosition
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Add
-import androidx.compose.material.icons.filled.Menu
-import androidx.compose.material.icons.filled.Star
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.FloatingActionButton
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.tooling.preview.Preview
-import com.yogeshpaliyal.keypasscompose.ui.theme.KeyPassTheme
-import com.yogeshpaliyal.keypasscompose.ui.theme.Material3BottomAppBar
-import com.yogeshpaliyal.keypasscompose.ui.theme.Material3Scaffold
-
-class MainActivity : ComponentActivity() {
- @OptIn(ExperimentalMaterial3Api::class)
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContent {
- KeyPassTheme {
- // A surface container using the 'background' color from the theme
- Surface(color = MaterialTheme.colorScheme.background) {
- Material3Scaffold(
- bottomBar = {
- Material3BottomAppBar(cutoutShape = RoundedCornerShape(50)) {
- IconButton(
- onClick = {
- /* doSomething() */
- }
- ) {
- Icon(Icons.Filled.Menu, "")
- }
-
- Spacer(Modifier.weight(1f, true))
- IconButton(
- onClick = {
- /* doSomething() */
- }
- ) {
- Icon(Icons.Filled.Star, "")
- }
- }
- },
- floatingActionButton = {
- FloatingActionButton(
- onClick = {
- },
- contentColor = Color.White,
- shape = RoundedCornerShape(50)
- ) {
- Icon(Icons.Filled.Add, "")
- }
- },
- floatingActionButtonPosition = FabPosition.Center,
- isFloatingActionButtonDocked = true
- ) {
- }
- Greeting("Android")
- }
- }
- }
- }
-}
-
-@Composable
-fun Greeting(name: String) {
- Text(text = "Hello $name!")
-}
-
-@Preview(showBackground = true)
-@Composable
-fun DefaultPreview() {
- KeyPassTheme {
- Greeting("Android")
- }
-}
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/MyApplication.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/MyApplication.kt
new file mode 100644
index 00000000..a1c5e98b
--- /dev/null
+++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/MyApplication.kt
@@ -0,0 +1,20 @@
+package com.yogeshpaliyal.keypasscompose
+
+import android.content.Intent
+import com.yogeshpaliyal.common.CommonMyApplication
+import com.yogeshpaliyal.keypasscompose.ui.CrashActivity
+import dagger.hilt.android.HiltAndroidApp
+
+/*
+* @author Yogesh Paliyal
+* yogeshpaliyal.foss@gmail.com
+* https://techpaliyal.com
+* created on 22-01-2021 22:41
+*/
+@HiltAndroidApp
+class MyApplication : CommonMyApplication() {
+
+ override fun getCrashActivityIntent(throwable: Throwable): Intent {
+ return CrashActivity.getIntent(this, throwable.localizedMessage)
+ }
+}
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/custom_views/MaskedCardView.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/custom_views/MaskedCardView.kt
new file mode 100644
index 00000000..495c437e
--- /dev/null
+++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/custom_views/MaskedCardView.kt
@@ -0,0 +1,47 @@
+package com.yogeshpaliyal.keypasscompose.custom_views
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Path
+import android.graphics.RectF
+import android.util.AttributeSet
+import com.google.android.material.card.MaterialCardView
+import com.google.android.material.shape.ShapeAppearanceModel
+import com.google.android.material.shape.ShapeAppearancePathProvider
+import com.yogeshpaliyal.keypasscompose.R
+
+/**
+ * A Card view that clips the content of any shape, this should be done upstream in card,
+ * working around it for now.
+ */
+class MaskedCardView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyle: Int = R.attr.materialCardViewStyle,
+) : MaterialCardView(context, attrs, defStyle) {
+ @SuppressLint("RestrictedApi")
+ private val pathProvider = ShapeAppearancePathProvider()
+ private val path: Path = Path()
+ private val shapeAppearance: ShapeAppearanceModel = ShapeAppearanceModel.builder(
+ context,
+ attrs,
+ defStyle,
+ R.style.Widget_MaterialComponents_CardView
+ ).build()
+
+ private val rectF = RectF(0f, 0f, 0f, 0f)
+
+ override fun onDraw(canvas: Canvas) {
+ canvas.clipPath(path)
+ super.onDraw(canvas)
+ }
+
+ @SuppressLint("RestrictedApi")
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+ rectF.right = w.toFloat()
+ rectF.bottom = h.toFloat()
+ pathProvider.calculatePath(shapeAppearance, 1f, rectF, path)
+ super.onSizeChanged(w, h, oldw, oldh)
+ }
+}
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/data/MyAccountModel.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/data/MyAccountModel.kt
new file mode 100644
index 00000000..36b55ac8
--- /dev/null
+++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/data/MyAccountModel.kt
@@ -0,0 +1,36 @@
+package com.yogeshpaliyal.keypasscompose.data
+
+import com.google.gson.Gson
+import com.yogeshpaliyal.common.constants.AccountType
+import com.yogeshpaliyal.common.data.AccountModel
+import com.yogeshpaliyal.keypasscompose.R
+import com.yogeshpaliyal.universalAdapter.listener.UniversalViewType
+import com.yogeshpaliyal.universalAdapter.model.BaseDiffUtil
+
+class MyAccountModel : AccountModel(), BaseDiffUtil, UniversalViewType {
+ override fun getDiffId(): Any? {
+ return id
+ }
+
+ override fun getDiffBody(): Any? {
+ return if (type == AccountType.TOTP) {
+ super.getDiffBody()
+ } else {
+ Gson().toJson(this)
+ }
+ }
+
+ override fun getLayoutId(): Int = if (type == AccountType.TOTP) R.layout.item_totp else R.layout.item_accounts
+
+ fun map(accountModel: AccountModel) {
+ this.id = accountModel.id
+ this.title = accountModel.title
+ this.uniqueId = accountModel.uniqueId
+ this.username = accountModel.username
+ this.password = accountModel.password
+ this.site = accountModel.site
+ this.notes = accountModel.notes
+ this.tags = accountModel.tags
+ this.type = accountModel.type
+ }
+}
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/listener/AccountsClickListener.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/listener/AccountsClickListener.kt
new file mode 100644
index 00000000..4bca61ac
--- /dev/null
+++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/listener/AccountsClickListener.kt
@@ -0,0 +1,14 @@
+package com.yogeshpaliyal.keypasscompose.listener
+
+import android.view.View
+
+/*
+* @author Yogesh Paliyal
+* techpaliyal@gmail.com
+* https://techpaliyal.com
+* created on 31-01-2021 09:00
+*/
+interface AccountsClickListener {
+ fun onItemClick(view: View, model: T)
+ fun onCopyClicked(model: T)
+}
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/listener/UniversalClickListener.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/listener/UniversalClickListener.kt
new file mode 100644
index 00000000..e6df2176
--- /dev/null
+++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/listener/UniversalClickListener.kt
@@ -0,0 +1,13 @@
+package com.yogeshpaliyal.keypasscompose.listener
+
+import android.view.View
+
+/*
+* @author Yogesh Paliyal
+* techpaliyal@gmail.com
+* https://techpaliyal.com
+* created on 31-01-2021 09:00
+*/
+interface UniversalClickListener {
+ fun onItemClick(view: View, model: T)
+}
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/CrashActivity.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/CrashActivity.kt
new file mode 100644
index 00000000..8fe52504
--- /dev/null
+++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/CrashActivity.kt
@@ -0,0 +1,65 @@
+package com.yogeshpaliyal.keypasscompose.ui
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import com.yogeshpaliyal.keypasscompose.BuildConfig
+import com.yogeshpaliyal.keypasscompose.databinding.ActivityCrashBinding
+import dagger.hilt.android.AndroidEntryPoint
+import java.lang.StringBuilder
+
+@AndroidEntryPoint
+class CrashActivity : AppCompatActivity() {
+
+ private lateinit var binding: ActivityCrashBinding
+
+ companion object {
+ private const val ARG_DATA = "arg_data"
+
+ fun getIntent(context: Context, data: String?): Intent {
+ return Intent(context, CrashActivity::class.java).also {
+ it.putExtra(ARG_DATA, data)
+ }
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityCrashBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ binding.txtCrash.text = intent.extras?.getString(ARG_DATA)
+
+ binding.btnSendFeedback.setOnClickListener {
+
+ val deviceInfo = StringBuilder()
+ try {
+ deviceInfo.append("\n")
+ deviceInfo.append("App Version: " + BuildConfig.VERSION_NAME)
+ deviceInfo.append("\n")
+ deviceInfo.append("Brand Name: " + Build.BRAND)
+ deviceInfo.append("\n")
+ deviceInfo.append("Manufacturer Name: " + Build.MANUFACTURER)
+ deviceInfo.append("\n")
+ deviceInfo.append("Device Name: " + Build.MODEL)
+ deviceInfo.append("\n")
+ deviceInfo.append("Device API Version: " + Build.VERSION.SDK_INT)
+ deviceInfo.append("\n")
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+
+ val intent = Intent(Intent.ACTION_SENDTO)
+ intent.data = Uri.parse("mailto:")
+
+ intent.putExtra(Intent.EXTRA_EMAIL, arrayOf("yogeshpaliyal.foss@gmail.com"))
+ intent.putExtra(Intent.EXTRA_SUBJECT, "Crash Report in KeyPass")
+ intent.putExtra(Intent.EXTRA_TEXT, binding.txtCrash.text.toString() + "$deviceInfo")
+
+ startActivity(Intent.createChooser(intent, ""))
+ }
+ }
+}
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/addTOTP/AddTOTPActivity.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/addTOTP/AddTOTPActivity.kt
new file mode 100644
index 00000000..ed1a4eda
--- /dev/null
+++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/addTOTP/AddTOTPActivity.kt
@@ -0,0 +1,139 @@
+package com.yogeshpaliyal.keypasscompose.ui.addTOTP
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.view.Menu
+import android.view.MenuItem
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.isVisible
+import androidx.lifecycle.Observer
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.snackbar.Snackbar
+import com.google.zxing.integration.android.IntentIntegrator
+import com.yogeshpaliyal.common.utils.TOTPHelper
+import com.yogeshpaliyal.keypasscompose.R
+import com.yogeshpaliyal.keypasscompose.databinding.ActivityAddTotpactivityBinding
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class AddTOTPActivity : AppCompatActivity() {
+
+ companion object {
+
+ private const val ARG_ACCOUNT_ID = "account_id"
+
+ @JvmStatic
+ fun start(context: Context?, accountId: String? = null) {
+
+ val starter = Intent(context, AddTOTPActivity::class.java)
+ starter.putExtra(ARG_ACCOUNT_ID, accountId)
+ context?.startActivity(starter)
+ }
+ }
+
+ private lateinit var binding: ActivityAddTotpactivityBinding
+
+ private val mViewModel by viewModels()
+
+ private val accountId by lazy {
+ intent.extras?.getString(ARG_ACCOUNT_ID)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityAddTotpactivityBinding.inflate(layoutInflater)
+ binding.mViewModel = mViewModel
+ binding.lifecycleOwner = this
+ setContentView(binding.root)
+
+ setSupportActionBar(binding.toolbar)
+
+ binding.tilSecretKey.isVisible = accountId == null
+ mViewModel.loadOldAccount(accountId)
+
+ binding.toolbar.setNavigationOnClickListener {
+ onBackPressed()
+ }
+
+ binding.tilSecretKey.setEndIconOnClickListener {
+ // ScannerActivity.start(this)
+ IntentIntegrator(this).setPrompt("").initiateScan()
+ }
+
+ mViewModel.error.observe(
+ this,
+ Observer {
+ it?.getContentIfNotHandled()?.let {
+ Snackbar.make(binding.root, it, Snackbar.LENGTH_SHORT).show()
+ }
+ }
+ )
+
+ mViewModel.goBack.observe(
+ this,
+ Observer {
+ it.getContentIfNotHandled()?.let {
+ onBackPressed()
+ }
+ }
+ )
+
+ binding.btnSave.setOnClickListener {
+ mViewModel.saveAccount(accountId)
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ if (accountId != null)
+ menuInflater.inflate(R.menu.menu_delete, menu)
+ return super.onCreateOptionsMenu(menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ if (item.itemId == R.id.action_delete) {
+ deleteAccount()
+ }
+ return super.onOptionsItemSelected(item)
+ }
+
+ private fun deleteAccount() {
+ MaterialAlertDialogBuilder(this)
+ .setTitle(getString(R.string.delete_account_title))
+ .setMessage(getString(R.string.delete_account_msg))
+ .setPositiveButton(
+ getString(R.string.delete)
+ ) { dialog, which ->
+ dialog?.dismiss()
+
+ if (accountId != null) {
+ mViewModel.deleteAccount(accountId!!) {
+ onBackPressed()
+ }
+ }
+ }
+ .setNegativeButton(getString(R.string.cancel)) { dialog, which ->
+ dialog.dismiss()
+ }.show()
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
+
+ if (result != null) {
+ if (result.contents != null) {
+ try {
+ val totp = TOTPHelper(result.contents)
+ mViewModel.setSecretKey(totp.secret)
+ mViewModel.setAccountName(totp.label)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ } else {
+ super.onActivityResult(requestCode, resultCode, data)
+ }
+ }
+}
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/addTOTP/AddTOTPViewModel.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/addTOTP/AddTOTPViewModel.kt
new file mode 100644
index 00000000..b9247d0b
--- /dev/null
+++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/addTOTP/AddTOTPViewModel.kt
@@ -0,0 +1,85 @@
+package com.yogeshpaliyal.keypasscompose.ui.addTOTP
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.yogeshpaliyal.common.AppDatabase
+import com.yogeshpaliyal.common.constants.AccountType
+import com.yogeshpaliyal.common.data.AccountModel
+import com.yogeshpaliyal.common.utils.Event
+import com.yogeshpaliyal.keypasscompose.R
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@HiltViewModel
+class AddTOTPViewModel @Inject constructor(private val appDatabase: AppDatabase) :
+ ViewModel() {
+
+ private val _goBack = MutableLiveData>()
+ val goBack: LiveData> = _goBack
+
+ private val _error = MutableLiveData>()
+ val error: LiveData> = _error
+
+ val secretKey = MutableLiveData("")
+
+ val accountName = MutableLiveData("")
+
+ fun loadOldAccount(accountId: String?) {
+ accountId ?: return
+
+ viewModelScope.launch(Dispatchers.IO) {
+ appDatabase.getDao().getAccount(accountId)?.let { accountModel ->
+ accountName.postValue(accountModel.title ?: "")
+ }
+ }
+ }
+
+ fun saveAccount(accountId: String?) {
+ viewModelScope.launch {
+ val secretKey = secretKey.value
+ val accountName = accountName.value
+
+ if (accountId == null) {
+ if (secretKey.isNullOrEmpty()) {
+ _error.postValue(Event(R.string.alert_black_secret_key))
+ return@launch
+ }
+ }
+
+ if (accountName.isNullOrEmpty()) {
+ _error.postValue(Event(R.string.alert_black_account_name))
+ return@launch
+ }
+
+ val accountModel = if (accountId == null) {
+ AccountModel(password = secretKey, title = accountName, type = AccountType.TOTP)
+ } else {
+ appDatabase.getDao().getAccount(accountId)?.also {
+ it.title = accountName
+ }
+ }
+
+ accountModel?.let { appDatabase.getDao().insertOrUpdateAccount(it) }
+ _goBack.postValue(Event(Unit))
+ }
+ }
+
+ fun setSecretKey(secretKey: String) {
+ this.secretKey.value = secretKey
+ }
+
+ fun setAccountName(accountName: String) {
+ this.accountName.value = accountName
+ }
+
+ fun deleteAccount(accountId: String, onDeleted: () -> Unit) {
+ viewModelScope.launch {
+ appDatabase.getDao().deleteAccount(accountId)
+ onDeleted()
+ }
+ }
+}
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/addTOTP/ScannerActivity.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/addTOTP/ScannerActivity.kt
new file mode 100644
index 00000000..70a1488a
--- /dev/null
+++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/addTOTP/ScannerActivity.kt
@@ -0,0 +1,55 @@
+package com.yogeshpaliyal.keypasscompose.ui.addTOTP
+
+import android.app.Activity
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import com.yogeshpaliyal.common.constants.RequestCodes
+import com.yogeshpaliyal.keypasscompose.databinding.ActivityScannerBinding
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class ScannerActivity : AppCompatActivity() {
+
+ private lateinit var binding: ActivityScannerBinding
+
+ private val REQUEST_CAM_PERMISSION = 432
+
+ companion object {
+ @JvmStatic
+ fun start(activity: Activity) {
+ val starter = Intent(activity, ScannerActivity::class.java)
+ activity.startActivityForResult(starter, RequestCodes.SCANNER)
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityScannerBinding.inflate(layoutInflater)
+
+ setContentView(binding.root)
+
+ setSupportActionBar(binding.toolbar)
+
+ binding.toolbar.setNavigationOnClickListener {
+ onBackPressed()
+ }
+ }
+
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray
+ ) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ if (requestCode == REQUEST_CAM_PERMISSION) {
+ if (isAllRequestGranted(grantResults)) {
+ // codeScanner?.startPreview()
+ }
+ }
+ }
+
+ private fun isAllRequestGranted(grantResults: IntArray) =
+ grantResults.all { it == PackageManager.PERMISSION_GRANTED }
+}
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/auth/AuthenticationActivity.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/auth/AuthenticationActivity.kt
new file mode 100644
index 00000000..2a153812
--- /dev/null
+++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/auth/AuthenticationActivity.kt
@@ -0,0 +1,133 @@
+package com.yogeshpaliyal.keypasscompose.ui.auth
+
+import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import android.provider.Settings
+import android.util.Log
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import androidx.biometric.BiometricManager
+import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
+import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
+import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
+import androidx.biometric.BiometricPrompt
+import androidx.core.content.ContextCompat
+import com.yogeshpaliyal.keypasscompose.R
+import com.yogeshpaliyal.keypasscompose.databinding.ActivityAuthenticationBinding
+import com.yogeshpaliyal.keypasscompose.ui.nav.DashboardActivity
+import dagger.hilt.android.AndroidEntryPoint
+import java.util.concurrent.Executor
+
+private const val AUTHENTICATION_RESULT = 707
+
+@AndroidEntryPoint
+class AuthenticationActivity : AppCompatActivity() {
+
+ private lateinit var binding: ActivityAuthenticationBinding
+
+ private lateinit var executor: Executor
+ private lateinit var biometricPrompt: BiometricPrompt
+ private lateinit var promptInfo: BiometricPrompt.PromptInfo
+
+ private val biometricManager by lazy {
+ BiometricManager.from(this)
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityAuthenticationBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ executor = ContextCompat.getMainExecutor(this)
+ biometricPrompt = BiometricPrompt(
+ this, executor,
+ object : BiometricPrompt.AuthenticationCallback() {
+ override fun onAuthenticationError(
+ errorCode: Int,
+ errString: CharSequence
+ ) {
+ super.onAuthenticationError(errorCode, errString)
+ Toast.makeText(
+ applicationContext,
+ "Authentication error: $errString", Toast.LENGTH_SHORT
+ )
+ .show()
+ // finish()
+ }
+
+ override fun onAuthenticationSucceeded(
+ result: BiometricPrompt.AuthenticationResult
+ ) {
+ super.onAuthenticationSucceeded(result)
+
+ onAuthenticated()
+ }
+
+ override fun onAuthenticationFailed() {
+ super.onAuthenticationFailed()
+ Toast.makeText(
+ applicationContext, getString(R.string.authentication_failed),
+ Toast.LENGTH_SHORT
+ )
+ .show()
+ }
+ }
+ )
+
+ promptInfo = BiometricPrompt.PromptInfo.Builder()
+ .setTitle(getString(R.string.app_name))
+ .setSubtitle(getString(R.string.login_to_enter_keypass))
+ .setAllowedAuthenticators(DEVICE_CREDENTIAL or BIOMETRIC_WEAK or BIOMETRIC_STRONG)
+ .build()
+
+ // Prompt appears when user clicks "Log in".
+ // Consider integrating with the keystore to unlock cryptographic operations,
+ // if needed by your app.
+
+ biometricPrompt.authenticate(promptInfo)
+
+ binding.btnRetry.setOnClickListener {
+ val allowedAuths = DEVICE_CREDENTIAL or BIOMETRIC_WEAK or BIOMETRIC_STRONG
+ val canAuthentication =
+ biometricManager.canAuthenticate(allowedAuths)
+ when (canAuthentication) {
+ BiometricManager.BIOMETRIC_SUCCESS -> {
+ Log.d("MY_APP_TAG", "App can authenticate using biometrics.")
+ biometricPrompt.authenticate(promptInfo)
+ }
+ BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE,
+ BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE,
+ BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
+ Log.e(
+ "MY_APP_TAG",
+ "$canAuthentication Biometric features are currently unavailable."
+ )
+ // Prompts the user to create credentials that your app accepts.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ val enrollIntent = Intent(Settings.ACTION_BIOMETRIC_ENROLL).apply {
+ putExtra(
+ Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED,
+ BIOMETRIC_STRONG or DEVICE_CREDENTIAL
+ )
+ }
+ startActivityForResult(enrollIntent, AUTHENTICATION_RESULT)
+ } else {
+ Toast.makeText(
+ this,
+ "Please set password for your device first from phone settings",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ }
+ }
+ }
+
+ private fun onAuthenticated() {
+ // binding.passCodeView.isVisible = false
+ val dashboardIntent = Intent(this, DashboardActivity::class.java)
+ startActivity(dashboardIntent)
+ finish()
+ }
+}
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/backup/BackupActivity.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/backup/BackupActivity.kt
new file mode 100644
index 00000000..8d5002e0
--- /dev/null
+++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/backup/BackupActivity.kt
@@ -0,0 +1,332 @@
+package com.yogeshpaliyal.keypasscompose.ui.backup
+
+import android.app.Activity
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.content.DialogInterface
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
+import androidx.lifecycle.lifecycleScope
+import androidx.preference.Preference
+import androidx.preference.PreferenceCategory
+import androidx.preference.PreferenceFragmentCompat
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.yogeshpaliyal.common.utils.BACKUP_KEY_LENGTH
+import com.yogeshpaliyal.common.utils.backupAccounts
+import com.yogeshpaliyal.common.utils.canUserAccessBackupDirectory
+import com.yogeshpaliyal.common.utils.clearBackupKey
+import com.yogeshpaliyal.common.utils.formatCalendar
+import com.yogeshpaliyal.common.utils.getBackupDirectory
+import com.yogeshpaliyal.common.utils.getBackupTime
+import com.yogeshpaliyal.common.utils.getOrCreateBackupKey
+import com.yogeshpaliyal.common.utils.isAutoBackupEnabled
+import com.yogeshpaliyal.common.utils.isKeyPresent
+import com.yogeshpaliyal.common.utils.overrideAutoBackup
+import com.yogeshpaliyal.common.utils.saveKeyphrase
+import com.yogeshpaliyal.common.utils.setAutoBackupEnabled
+import com.yogeshpaliyal.common.utils.setBackupDirectory
+import com.yogeshpaliyal.common.utils.setBackupTime
+import com.yogeshpaliyal.common.utils.setOverrideAutoBackup
+import com.yogeshpaliyal.keypasscompose.R
+import com.yogeshpaliyal.keypasscompose.databinding.BackupActivityBinding
+import com.yogeshpaliyal.keypasscompose.databinding.LayoutBackupKeypharseBinding
+import com.yogeshpaliyal.keypasscompose.databinding.LayoutCustomKeypharseBinding
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.net.URLDecoder
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class BackupActivity : AppCompatActivity() {
+
+ companion object {
+ @JvmStatic
+ fun start(context: Context?) {
+ val starter = Intent(context, BackupActivity::class.java)
+ context?.startActivity(starter)
+ }
+ }
+
+ private lateinit var binding: BackupActivityBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = BackupActivityBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+ setSupportActionBar(binding.toolbar)
+ binding.toolbar.setNavigationOnClickListener {
+ onBackPressed()
+ }
+
+ if (savedInstanceState == null) {
+ supportFragmentManager
+ .beginTransaction()
+ .replace(R.id.settings, SettingsFragment())
+ .commit()
+ }
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ }
+
+ @AndroidEntryPoint
+ class SettingsFragment : PreferenceFragmentCompat() {
+
+ @Inject
+ lateinit var appDb: com.yogeshpaliyal.common.AppDatabase
+
+ private val CHOOSE_BACKUPS_LOCATION_REQUEST_CODE = 26212
+
+ override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+ setPreferencesFromResource(R.xml.backup_preferences, rootKey)
+ updateItems()
+ }
+
+ override fun onPreferenceTreeClick(preference: Preference): Boolean {
+ when (preference.key) {
+ getString(R.string.settings_start_backup) -> {
+ startBackup()
+ }
+ getString(R.string.settings_create_backup) -> {
+ lifecycleScope.launch {
+ if (context.canUserAccessBackupDirectory()) {
+ val selectedDirectory = Uri.parse(context.getBackupDirectory())
+ passwordSelection(selectedDirectory)
+ }
+ }
+ }
+ getString(R.string.settings_backup_folder) -> {
+ changeBackupFolder()
+ }
+ getString(R.string.settings_verify_key_phrase) -> {
+ verifyKeyPhrase()
+ }
+ getString(R.string.settings_stop_backup) -> {
+ lifecycleScope.launch {
+ stopBackup()
+ }
+ }
+ getString(R.string.settings_auto_backup) -> {
+ lifecycleScope.launch {
+ context.setAutoBackupEnabled(context.isAutoBackupEnabled().not())
+ updateItems()
+ }
+ }
+ getString(R.string.settings_override_auto_backup) -> {
+ lifecycleScope.launch {
+ context.setOverrideAutoBackup(context.overrideAutoBackup().not())
+ updateItems()
+ }
+ }
+ }
+ return super.onPreferenceTreeClick(preference)
+ }
+
+ private suspend fun passwordSelection(selectedDirectory: Uri) {
+
+ val isKeyPresent = context?.isKeyPresent() ?: return
+ if (isKeyPresent) {
+ backup(selectedDirectory)
+ return
+ }
+
+ val builder = MaterialAlertDialogBuilder(requireContext())
+ .setTitle(R.string.alert)
+ .setMessage(getString(R.string.custom_generated_keyphrase_info))
+ .setPositiveButton(
+ getString(R.string.custom_keyphrase)
+ ) { dialog, which ->
+ dialog?.dismiss()
+ setCustomKeyphrase(selectedDirectory)
+ }
+ .setNegativeButton(R.string.generate_keyphrase) { dialog, which ->
+ dialog?.dismiss()
+ backup(selectedDirectory)
+ }
+ builder.show()
+ }
+
+ private fun setCustomKeyphrase(selectedDirectory: Uri) {
+ val binding = LayoutCustomKeypharseBinding.inflate(layoutInflater)
+ val dialog = MaterialAlertDialogBuilder(requireContext()).setView(binding.root)
+ .setPositiveButton(
+ getString(R.string.yes)
+ ) { dialog, which ->
+
+ dialog?.dismiss()
+ }.create()
+ dialog.setOnShowListener {
+ val positiveBtn = dialog.getButton(DialogInterface.BUTTON_POSITIVE)
+ positiveBtn.setOnClickListener {
+ val keyphrase = binding.etKeyPhrase.text.toString().trim()
+ if (keyphrase.isEmpty()) {
+ Toast.makeText(context, R.string.alert_blank_keyphrase, Toast.LENGTH_SHORT)
+ .show()
+ return@setOnClickListener
+ }
+
+ if (keyphrase.length != BACKUP_KEY_LENGTH) {
+ Toast.makeText(
+ context,
+ R.string.alert_invalid_keyphrase,
+ Toast.LENGTH_SHORT
+ ).show()
+ return@setOnClickListener
+ }
+ lifecycleScope.launch {
+ context?.saveKeyphrase(keyphrase)
+ backup(selectedDirectory)
+ }
+ dialog.dismiss()
+ }
+ }
+ dialog.show()
+ }
+
+ fun backup(selectedDirectory: Uri) {
+ lifecycleScope.launch {
+ context.backupAccounts(appDb, selectedDirectory)?.let { keyPair ->
+ if (keyPair.first) {
+ val binding = LayoutBackupKeypharseBinding.inflate(layoutInflater)
+ binding.txtCode.text = context?.getOrCreateBackupKey()?.second ?: ""
+ binding.txtCode.setOnClickListener {
+ val clipboard =
+ ContextCompat.getSystemService(
+ requireContext(),
+ ClipboardManager::class.java
+ )
+ val clip = ClipData.newPlainText("KeyPass", binding.txtCode.text)
+ clipboard?.setPrimaryClip(clip)
+ Toast.makeText(
+ context,
+ getString(R.string.copied_to_clipboard),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ MaterialAlertDialogBuilder(requireContext()).setView(binding.root)
+ .setPositiveButton(
+ getString(R.string.yes)
+ ) { dialog, which ->
+ updateItems()
+ dialog?.dismiss()
+ }.show()
+ } else {
+ updateItems()
+ Toast.makeText(
+ context,
+ getString(R.string.backup_completed),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ }
+ }
+
+ private fun updateItems() {
+ lifecycleScope.launch(Dispatchers.IO) {
+ val isBackupEnabled =
+ context.canUserAccessBackupDirectory() && (context?.isKeyPresent() ?: false)
+
+ val isAutoBackupEnabled = context.isAutoBackupEnabled()
+ val overrideAutoBackup = context.overrideAutoBackup()
+
+ val lastBackupTime = context.getBackupTime()
+ val backupDirectory = context.getBackupDirectory()
+
+ withContext(Dispatchers.Main) {
+
+ findPreference(getString(R.string.settings_start_backup))?.isVisible =
+ isBackupEnabled.not()
+ findPreference(getString(R.string.settings_stop_backup))?.isVisible =
+ isBackupEnabled
+
+ findPreference(getString(R.string.settings_auto_backup))?.isVisible =
+ isBackupEnabled
+ findPreference(getString(R.string.settings_auto_backup))?.summary =
+ if (isAutoBackupEnabled) getString(R.string.enabled) else getString(R.string.disabled)
+
+ findPreference(getString(R.string.settings_cat_auto_backup))?.isVisible =
+ isBackupEnabled && isAutoBackupEnabled
+
+ findPreference(getString(R.string.settings_override_auto_backup))?.summary =
+ if (overrideAutoBackup) getString(R.string.enabled) else getString(R.string.disabled)
+
+ findPreference(getString(R.string.settings_create_backup))?.isVisible =
+ isBackupEnabled
+ findPreference(getString(R.string.settings_create_backup))?.summary =
+ getString(
+ R.string.last_backup_date,
+ lastBackupTime.formatCalendar("dd MMM yyyy hh:mm aa")
+ )
+ findPreference(getString(R.string.settings_backup_folder))?.isVisible =
+ isBackupEnabled
+ val directory = URLDecoder.decode(backupDirectory, "utf-8").split("/")
+ val folderName = directory.get(directory.lastIndex)
+ findPreference(getString(R.string.settings_backup_folder))?.summary =
+ folderName
+ findPreference(getString(R.string.settings_verify_key_phrase))?.isVisible =
+ false
+ findPreference(getString(R.string.settings_backup))?.isVisible =
+ isBackupEnabled
+ }
+ }
+ }
+
+ private fun startBackup() {
+ val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
+
+ intent.addFlags(
+ Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
+ Intent.FLAG_GRANT_READ_URI_PERMISSION
+ )
+
+ try {
+ startActivityForResult(intent, CHOOSE_BACKUPS_LOCATION_REQUEST_CODE)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ if (requestCode == CHOOSE_BACKUPS_LOCATION_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
+ val contentResolver = context?.contentResolver
+ val selectedDirectory = data?.data
+ if (contentResolver != null && selectedDirectory != null) {
+ contentResolver.takePersistableUriPermission(
+ selectedDirectory,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ )
+
+ lifecycleScope.launch {
+ context.setBackupDirectory(selectedDirectory.toString())
+ passwordSelection(selectedDirectory)
+ }
+ }
+ }
+ }
+
+ private fun changeBackupFolder() {
+ startBackup()
+ }
+
+ private fun verifyKeyPhrase() {
+ Toast.makeText(context, getString(R.string.coming_soon), Toast.LENGTH_SHORT).show()
+ }
+
+ private suspend fun stopBackup() {
+ context.clearBackupKey()
+ context.setBackupDirectory("")
+ context.setBackupTime(-1)
+ context.setOverrideAutoBackup(false)
+ context.setAutoBackupEnabled(false)
+ updateItems()
+ }
+ }
+}
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/detail/DetailActivity.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/detail/DetailActivity.kt
new file mode 100644
index 00000000..48fc78bd
--- /dev/null
+++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/detail/DetailActivity.kt
@@ -0,0 +1,132 @@
+package com.yogeshpaliyal.keypasscompose.ui.detail
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.view.Menu
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.Observer
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.zxing.integration.android.IntentIntegrator
+import com.yogeshpaliyal.common.utils.PasswordGenerator
+import com.yogeshpaliyal.keypasscompose.R
+import com.yogeshpaliyal.keypasscompose.databinding.FragmentDetailBinding
+import dagger.hilt.android.AndroidEntryPoint
+
+/*
+* @author Yogesh Paliyal
+* yogeshpaliyal.foss@gmail.com
+* https://techpaliyal.com
+* created on 31-01-2021 10:38
+*/
+@AndroidEntryPoint
+class DetailActivity : AppCompatActivity() {
+
+ lateinit var binding: FragmentDetailBinding
+
+ companion object {
+
+ private const val ARG_ACCOUNT_ID = "ARG_ACCOUNT_ID"
+
+ @JvmStatic
+ fun start(context: Context?, accountId: Long? = null) {
+ val starter = Intent(context, DetailActivity::class.java)
+ .putExtra(ARG_ACCOUNT_ID, accountId)
+ context?.startActivity(starter)
+ }
+ }
+
+ private val mViewModel by viewModels()
+
+ private val accountId by lazy {
+ intent?.extras?.getLong(ARG_ACCOUNT_ID) ?: -1
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = FragmentDetailBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+ binding.lifecycleOwner = this
+
+ mViewModel.loadAccount(accountId)
+ mViewModel.accountModel.observe(
+ this,
+ Observer {
+ binding.accountData = it
+ }
+ )
+
+ if (accountId > 0) {
+ binding.bottomAppBar.replaceMenu(R.menu.bottom_app_bar_detail)
+
+ binding.tilPassword.startIconDrawable = null
+ } else {
+ binding.tilPassword.setStartIconDrawable(R.drawable.ic_round_refresh_24)
+
+ binding.tilPassword.setStartIconOnClickListener {
+ binding.etPassword.setText(PasswordGenerator().generatePassword())
+ }
+ }
+
+ binding.bottomAppBar.setNavigationOnClickListener {
+ onBackPressed()
+ }
+ binding.bottomAppBar.setOnMenuItemClickListener { item ->
+ if (item.itemId == R.id.action_delete) {
+ deleteAccount()
+ return@setOnMenuItemClickListener true
+ }
+
+ return@setOnMenuItemClickListener false
+ }
+
+ binding.btnSave.setOnClickListener {
+ mViewModel.insertOrUpdate {
+ onBackPressed()
+ }
+ }
+
+ binding.btnScan.setOnClickListener {
+ IntentIntegrator(this).setPrompt("").initiateScan()
+ }
+ }
+
+ private fun deleteAccount() {
+ MaterialAlertDialogBuilder(this)
+ .setTitle(getString(R.string.delete_account_title))
+ .setMessage(getString(R.string.delete_account_msg))
+ .setPositiveButton(
+ getString(R.string.delete)
+ ) { dialog, which ->
+ dialog?.dismiss()
+
+ if (accountId > 0L) {
+ mViewModel.deleteAccount {
+ onBackPressed()
+ }
+ }
+ }
+ .setNegativeButton(getString(R.string.cancel)) { dialog, which ->
+ dialog.dismiss()
+ }.show()
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ menuInflater.inflate(R.menu.bottom_app_bar_detail, menu)
+ return super.onCreateOptionsMenu(menu)
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
+
+ if (result != null) {
+ if (result.contents != null) {
+ binding.etPassword.setText(result.contents)
+ }
+ } else {
+ super.onActivityResult(requestCode, resultCode, data)
+ }
+ }
+}
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/detail/DetailViewModel.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/detail/DetailViewModel.kt
new file mode 100644
index 00000000..c4bb494b
--- /dev/null
+++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/detail/DetailViewModel.kt
@@ -0,0 +1,68 @@
+package com.yogeshpaliyal.keypasscompose.ui.detail
+
+import android.app.Application
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.viewModelScope
+import com.yogeshpaliyal.common.data.AccountModel
+import com.yogeshpaliyal.common.worker.executeAutoBackup
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+
+/*
+* @author Yogesh Paliyal
+* techpaliyal@gmail.com
+* https://techpaliyal.com
+* created on 31-01-2021 11:52
+*/
+@HiltViewModel
+class DetailViewModel @Inject constructor(
+ val app: Application,
+ val appDb: com.yogeshpaliyal.common.AppDatabase
+) : AndroidViewModel(app) {
+
+ private val _accountModel by lazy { MutableLiveData() }
+ val accountModel: LiveData = _accountModel
+
+ fun loadAccount(accountId: Long?) {
+ viewModelScope.launch(Dispatchers.IO) {
+ _accountModel.postValue(
+ appDb.getDao().getAccount(accountId) ?: AccountModel()
+ )
+ }
+ }
+
+ fun deleteAccount(onExecCompleted: () -> Unit) {
+ viewModelScope.launch {
+ accountModel.value?.let {
+ withContext(Dispatchers.IO) {
+ appDb.getDao().deleteAccount(it)
+ }
+ autoBackup()
+ onExecCompleted()
+ }
+ }
+ }
+
+ fun insertOrUpdate(onExecCompleted: () -> Unit) {
+ viewModelScope.launch {
+ accountModel.value?.let {
+ withContext(Dispatchers.IO) {
+ appDb.getDao().insertOrUpdateAccount(it)
+ autoBackup()
+ }
+ }
+ onExecCompleted()
+ }
+ }
+
+ private fun autoBackup() {
+ viewModelScope.launch {
+ app.executeAutoBackup()
+ }
+ }
+}
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/generate/GeneratePasswordActivity.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/generate/GeneratePasswordActivity.kt
new file mode 100644
index 00000000..3b0be14c
--- /dev/null
+++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/generate/GeneratePasswordActivity.kt
@@ -0,0 +1,47 @@
+package com.yogeshpaliyal.keypasscompose.ui.generate
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.os.Bundle
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import com.yogeshpaliyal.common.utils.PasswordGenerator
+import com.yogeshpaliyal.keypasscompose.R
+import com.yogeshpaliyal.keypasscompose.databinding.ActivityGeneratePasswordBinding
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class GeneratePasswordActivity : AppCompatActivity() {
+ private lateinit var binding: ActivityGeneratePasswordBinding
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityGeneratePasswordBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ generatePassword()
+
+ binding.btnRefresh.setOnClickListener {
+ generatePassword()
+ }
+
+ binding.tilPassword.setEndIconOnClickListener {
+ val clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
+ val clip = ClipData.newPlainText("random_password", binding.etPassword.text)
+ clipboard.setPrimaryClip(clip)
+ Toast.makeText(this, getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ private fun generatePassword() {
+ val password = PasswordGenerator(
+ length = binding.sliderPasswordLength.value.toInt(),
+ includeUpperCaseLetters = binding.cbCapAlphabets.isChecked,
+ includeLowerCaseLetters = binding.cbLowerAlphabets.isChecked,
+ includeSymbols = binding.cbSymbols.isChecked,
+ includeNumbers = binding.cbNumbers.isChecked
+ ).generatePassword()
+
+ binding.etPassword.setText(password)
+ binding.etPassword.setSelection(password.length)
+ }
+}
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/home/DashboardViewModel.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/home/DashboardViewModel.kt
new file mode 100644
index 00000000..5ec08bb2
--- /dev/null
+++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/home/DashboardViewModel.kt
@@ -0,0 +1,57 @@
+package com.yogeshpaliyal.keypasscompose.ui.home
+
+import android.app.Application
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MediatorLiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.viewModelScope
+import com.yogeshpaliyal.common.data.AccountModel
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+/*
+* @author Yogesh Paliyal
+* techpaliyal@gmail.com
+* https://techpaliyal.com
+* created on 30-01-2021 23:02
+*/
+@HiltViewModel
+class DashboardViewModel @Inject constructor(application: Application, val appDb: com.yogeshpaliyal.common.AppDatabase) :
+ AndroidViewModel(application) {
+
+ val keyword by lazy {
+ MutableLiveData("")
+ }
+ val tag by lazy {
+ MutableLiveData()
+ }
+
+ private val appDao = appDb.getDao()
+
+ val mediator = MediatorLiveData>>()
+
+ init {
+ mediator.addSource(keyword) {
+ mediator.postValue(appDao.getAllAccounts(keyword.value, tag.value))
+ }
+ mediator.addSource(tag) {
+ mediator.postValue(appDao.getAllAccounts(keyword.value, tag.value))
+ }
+ mediator.postValue(appDao.getAllAccounts(keyword.value, tag.value))
+
+ reloadData()
+ }
+
+ private fun reloadData() {
+ viewModelScope.launch(Dispatchers.IO) {
+ while (true) {
+ delay(1000)
+ mediator.postValue(appDao.getAllAccounts(keyword.value, tag.value))
+ }
+ }
+ }
+}
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/home/HomeFragment.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/home/HomeFragment.kt
new file mode 100644
index 00000000..d2793ac7
--- /dev/null
+++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/home/HomeFragment.kt
@@ -0,0 +1,121 @@
+package com.yogeshpaliyal.keypasscompose.ui.home
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import androidx.core.content.ContextCompat
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Observer
+import com.yogeshpaliyal.common.constants.AccountType
+import com.yogeshpaliyal.common.data.AccountModel
+import com.yogeshpaliyal.keypasscompose.R
+import com.yogeshpaliyal.keypasscompose.data.MyAccountModel
+import com.yogeshpaliyal.keypasscompose.databinding.FragmentHomeBinding
+import com.yogeshpaliyal.keypasscompose.listener.AccountsClickListener
+import com.yogeshpaliyal.keypasscompose.ui.addTOTP.AddTOTPActivity
+import com.yogeshpaliyal.keypasscompose.ui.detail.DetailActivity
+import com.yogeshpaliyal.universalAdapter.adapter.UniversalAdapterViewType
+import com.yogeshpaliyal.universalAdapter.adapter.UniversalRecyclerAdapter
+import com.yogeshpaliyal.universalAdapter.utils.Resource
+import dagger.hilt.android.AndroidEntryPoint
+
+/*
+* @author Yogesh Paliyal
+* techpaliyal@gmail.com
+* https://techpaliyal.com
+* created on 31-01-2021 09:25
+*/
+@AndroidEntryPoint
+class HomeFragment : Fragment() {
+ private lateinit var binding: FragmentHomeBinding
+
+ private val mViewModel by lazy {
+ activityViewModels().value
+ }
+
+ private val mAdapter by lazy {
+ UniversalRecyclerAdapter.Builder(
+ this,
+ content = UniversalAdapterViewType.Content(
+ R.layout.item_accounts,
+ listener = mListener
+ ),
+ noData = UniversalAdapterViewType.NoData(R.layout.layout_no_accounts)
+ ).build()
+ }
+
+ val mListener = object : AccountsClickListener {
+ override fun onItemClick(view: View, model: AccountModel) {
+ if (model.type == AccountType.TOTP) {
+ AddTOTPActivity.start(context, model.uniqueId)
+ } else {
+ DetailActivity.start(context, model.id)
+ }
+ }
+
+ private fun getPassword(model: AccountModel): String {
+ if (model.type == AccountType.TOTP) {
+ return model.getOtp()
+ }
+ return model.password.orEmpty()
+ }
+
+ override fun onCopyClicked(model: AccountModel) {
+ val clipboard =
+ ContextCompat.getSystemService(
+ requireContext(),
+ ClipboardManager::class.java
+ )
+ val clip = ClipData.newPlainText("KeyPass", getPassword(model))
+ clipboard?.setPrimaryClip(clip)
+ Toast.makeText(context, getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT)
+ .show()
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ binding = FragmentHomeBinding.inflate(layoutInflater, container, false)
+ return binding.root
+ }
+
+ private val observer = Observer> {
+ val newList = it.map { accountModel ->
+ MyAccountModel().also {
+ it.map(accountModel)
+ }
+ }
+ mAdapter.updateData(Resource.success(ArrayList(newList)))
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ binding.recyclerView.adapter = mAdapter.getAdapter()
+
+ mViewModel.mediator.observe(
+ viewLifecycleOwner,
+ Observer {
+ it.removeObserver(observer)
+ it.observe(viewLifecycleOwner, observer)
+ }
+ )
+
+ /* lifecycleScope.launch() {
+ mViewModel.result
+ mViewModel.loadData(args.tag).collect {
+ withContext(Dispatchers.Main) {
+ mAdapter.updateData(Resource.success(ArrayList(it)))
+ }
+ }
+ }*/
+ }
+}
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/BottomNavDrawerFragment.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/BottomNavDrawerFragment.kt
new file mode 100644
index 00000000..195db503
--- /dev/null
+++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/BottomNavDrawerFragment.kt
@@ -0,0 +1,173 @@
+package com.yogeshpaliyal.keypasscompose.ui.nav
+
+import android.content.res.ColorStateList
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.activity.OnBackPressedCallback
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.observe
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED
+import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED
+import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HALF_EXPANDED
+import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN
+import com.google.android.material.bottomsheet.BottomSheetBehavior.from
+import com.google.android.material.shape.MaterialShapeDrawable
+import com.yogeshpaliyal.common.utils.themeColor
+import com.yogeshpaliyal.keypasscompose.R
+import com.yogeshpaliyal.keypasscompose.databinding.FragmentBottomNavDrawerBinding
+import dagger.hilt.android.AndroidEntryPoint
+import kotlin.LazyThreadSafetyMode.NONE
+
+/**
+ * A [Fragment] which acts as a bottom navigation drawer.
+ */
+@AndroidEntryPoint
+class BottomNavDrawerFragment :
+ Fragment(),
+ NavigationAdapter.NavigationAdapterListener {
+
+ private lateinit var binding: FragmentBottomNavDrawerBinding
+
+ private val behavior: BottomSheetBehavior by lazy(NONE) {
+ from(binding.foregroundContainer)
+ }
+
+ private val mViewModel by viewModels()
+
+ private val bottomSheetCallback = BottomNavigationDrawerCallback()
+
+ private val navigationListeners: MutableList =
+ mutableListOf()
+
+ private val foregroundShapeDrawable: MaterialShapeDrawable by lazy(NONE) {
+ val foregroundContext = binding.foregroundContainer.context
+ MaterialShapeDrawable(
+ foregroundContext,
+ null,
+ R.attr.bottomSheetStyle,
+ 0
+ ).apply {
+ fillColor = ColorStateList.valueOf(
+ foregroundContext.themeColor(R.attr.colorSurface)
+ )
+ elevation = resources.getDimension(R.dimen.plane_16)
+ shadowCompatibilityMode = MaterialShapeDrawable.SHADOW_COMPAT_MODE_NEVER
+ initializeElevationOverlay(requireContext())
+ }
+ }
+
+ private val closeDrawerOnBackPressed = object : OnBackPressedCallback(false) {
+ override fun handleOnBackPressed() {
+ close()
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ requireActivity().onBackPressedDispatcher.addCallback(this, closeDrawerOnBackPressed)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = FragmentBottomNavDrawerBinding.inflate(inflater, container, false)
+ binding.foregroundContainer.setOnApplyWindowInsetsListener { view, windowInsets ->
+ // Record the window's top inset so it can be applied when the bottom sheet is slide up
+ // to meet the top edge of the screen.
+ view.setTag(
+ R.id.tag_system_window_inset_top,
+ windowInsets.systemWindowInsetTop
+ )
+ windowInsets
+ }
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ binding.run {
+ // backgroundContainer.background = backgroundShapeDrawable
+ foregroundContainer.background = foregroundShapeDrawable
+
+ scrimView.setOnClickListener { close() }
+
+ bottomSheetCallback.apply {
+ // Scrim view transforms
+ addOnSlideAction(AlphaSlideAction(scrimView))
+ addOnStateChangedAction(VisibilityStateAction(scrimView))
+
+ // Recycler transforms
+ addOnStateChangedAction(ScrollToTopStateAction(navRecyclerView))
+ // Close the sandwiching account picker if open
+ addOnStateChangedAction(object : OnStateChangedAction {
+ override fun onStateChanged(sheet: View, newState: Int) {
+ }
+ })
+ // If the drawer is open, pressing the system back button should close the drawer.
+ addOnStateChangedAction(object : OnStateChangedAction {
+ override fun onStateChanged(sheet: View, newState: Int) {
+ closeDrawerOnBackPressed.isEnabled = newState != STATE_HIDDEN
+ }
+ })
+ }
+
+ behavior.addBottomSheetCallback(bottomSheetCallback)
+ behavior.state = STATE_HIDDEN
+
+ val adapter = NavigationAdapter(this@BottomNavDrawerFragment)
+
+ navRecyclerView.adapter = adapter
+ mViewModel.navigationList.observe(viewLifecycleOwner) {
+ adapter.submitList(it)
+ }
+ mViewModel.setNavigationMenuItemChecked(0)
+ }
+ }
+
+ fun open() {
+ behavior.state = STATE_HALF_EXPANDED
+ }
+
+ fun close() {
+ behavior.state = STATE_HIDDEN
+ }
+
+ fun addOnSlideAction(action: OnSlideAction) {
+ bottomSheetCallback.addOnSlideAction(action)
+ }
+
+ fun addOnStateChangedAction(action: OnStateChangedAction) {
+ bottomSheetCallback.addOnStateChangedAction(action)
+ }
+
+ fun addNavigationListener(listener: NavigationAdapter.NavigationAdapterListener) {
+ navigationListeners.add(listener)
+ }
+
+ override fun onNavMenuItemClicked(item: NavigationModelItem.NavMenuItem) {
+ // mViewModel.setNavigationMenuItemChecked(item.id)
+ close()
+ navigationListeners.forEach { it.onNavMenuItemClicked(item) }
+ }
+
+ override fun onNavEmailFolderClicked(folder: NavigationModelItem.NavEmailFolder) {
+ navigationListeners.forEach { it.onNavEmailFolderClicked(folder) }
+ }
+
+ fun toggle() {
+ when {
+ behavior.state == STATE_HIDDEN -> open()
+ behavior.state == STATE_HIDDEN ||
+ behavior.state == STATE_HALF_EXPANDED ||
+ behavior.state == STATE_EXPANDED
+ || behavior.state == STATE_COLLAPSED -> close()
+ }
+ }
+}
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/BottomNavViewModel.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/BottomNavViewModel.kt
new file mode 100644
index 00000000..4ea30767
--- /dev/null
+++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/BottomNavViewModel.kt
@@ -0,0 +1,67 @@
+package com.yogeshpaliyal.keypasscompose.ui.nav
+
+import android.app.Application
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+/*
+* @author Yogesh Paliyal
+* techpaliyal@gmail.com
+* https://techpaliyal.com
+* created on 31-01-2021 14:11
+*/
+@HiltViewModel
+class BottomNavViewModel @Inject constructor (application: Application, val appDb: com.yogeshpaliyal.common.AppDatabase) : AndroidViewModel(application) {
+ private val _navigationList: MutableLiveData> = MutableLiveData()
+ private val tagsDb = appDb.getDao().getTags()
+
+ private var tagsList: List ? = null
+
+ val navigationList: LiveData>
+ get() = _navigationList
+
+ init {
+ postListUpdate()
+
+ viewModelScope.launch {
+ tagsDb.collect {
+ tagsList = it
+ postListUpdate()
+ }
+ }
+ }
+
+ /**
+ * Set the currently selected menu item.
+ *
+ * @return true if the currently selected item has changed.
+ */
+ fun setNavigationMenuItemChecked(id: Int): Boolean {
+ var updated = false
+ NavigationModel.navigationMenuItems.forEachIndexed { index, item ->
+ val shouldCheck = item.id == id
+ if (item.checked != shouldCheck) {
+ NavigationModel.navigationMenuItems[index] = item.copy(checked = shouldCheck)
+ updated = true
+ }
+ }
+ if (updated) postListUpdate()
+ return updated
+ }
+
+ private fun postListUpdate() {
+ val newList = if (tagsList.isNullOrEmpty().not()) {
+ NavigationModel.navigationMenuItems + NavigationModelItem.NavDivider("Tags") + (tagsList?.filter { it != null }?.map { NavigationModelItem.NavEmailFolder(it) } ?: listOf())
+ } else {
+ NavigationModel.navigationMenuItems
+ }
+
+ _navigationList.value = newList
+ }
+}
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/BottomNavigationDrawerCallback.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/BottomNavigationDrawerCallback.kt
new file mode 100644
index 00000000..402521bf
--- /dev/null
+++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/BottomNavigationDrawerCallback.kt
@@ -0,0 +1,92 @@
+package com.yogeshpaliyal.keypasscompose.ui.nav
+
+import android.annotation.SuppressLint
+import android.view.View
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import com.google.android.material.R
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.yogeshpaliyal.common.utils.normalize
+import kotlin.math.max
+
+/**
+ * A [BottomSheetBehavior.BottomSheetCallback] which helps break apart clients who would like to
+ * react to changed in either the bottom sheet's slide offset or state. Clients can dynamically
+ * add or remove [OnSlideAction]s or [OnStateChangedAction]s which will be run when the
+ * sheet's slideOffset or state are changed.
+ *
+ * This callback's behavior differs slightly in that the slideOffset passed to [OnSlideAction]s
+ * in [onSlide] is corrected to guarantee that the offset 0.0 always be exactly at the
+ * [BottomSheetBehavior.STATE_HALF_EXPANDED] state.
+ */
+class BottomNavigationDrawerCallback : BottomSheetBehavior.BottomSheetCallback() {
+
+ private val onSlideActions: MutableList = mutableListOf()
+ private val onStateChangedActions: MutableList = mutableListOf()
+
+ private var lastSlideOffset = -1.0F
+ private var halfExpandedSlideOffset = Float.MAX_VALUE
+
+ override fun onSlide(sheet: View, slideOffset: Float) {
+ if (halfExpandedSlideOffset == Float.MAX_VALUE)
+ calculateInitialHalfExpandedSlideOffset(sheet)
+
+ lastSlideOffset = slideOffset
+ // Correct for the fact that the slideOffset is not zero when half expanded
+ val trueOffset = if (slideOffset <= halfExpandedSlideOffset) {
+ slideOffset.normalize(-1F, halfExpandedSlideOffset, -1F, 0F)
+ } else {
+ slideOffset.normalize(halfExpandedSlideOffset, 1F, 0F, 1F)
+ }
+
+ onSlideActions.forEach { it.onSlide(sheet, trueOffset) }
+ }
+
+ override fun onStateChanged(sheet: View, newState: Int) {
+ if (newState == BottomSheetBehavior.STATE_HALF_EXPANDED) {
+ halfExpandedSlideOffset = lastSlideOffset
+ onSlide(sheet, lastSlideOffset)
+ }
+
+ onStateChangedActions.forEach { it.onStateChanged(sheet, newState) }
+ }
+
+ /**
+ * Calculate the onSlideOffset which will be given when the bottom sheet is in the
+ * [BottomSheetBehavior.STATE_HALF_EXPANDED] state.
+ *
+ * Recording the correct slide offset for the half expanded state happens in [onStateChanged].
+ * Since the first time the sheet is opened, we haven't yet received a call to [onStateChanged],
+ * this method is used to calculate the initial value manually so we can smoothly normalize
+ * slideOffset values received between -1 and 1.
+ *
+ * See:
+ * [BottomSheetBehavior.calculateCollapsedOffset]
+ * [BottomSheetBehavior.calculateHalfExpandedOffset]
+ * [BottomSheetBehavior.dispatchOnSlide]
+ */
+ @SuppressLint("PrivateResource")
+ private fun calculateInitialHalfExpandedSlideOffset(sheet: View) {
+ val parent = sheet.parent as CoordinatorLayout
+ val behavior = BottomSheetBehavior.from(sheet)
+
+ val halfExpandedOffset = parent.height * (1 - behavior.halfExpandedRatio)
+ val peekHeightMin = parent.resources.getDimensionPixelSize(
+ R.dimen.design_bottom_sheet_peek_height_min
+ )
+ val peek = max(peekHeightMin, parent.height - parent.width * 9 / 16)
+ val collapsedOffset = max(
+ parent.height - peek,
+ max(0, parent.height - sheet.height)
+ )
+ halfExpandedSlideOffset =
+ (collapsedOffset - halfExpandedOffset) / (parent.height - collapsedOffset)
+ }
+
+ fun addOnSlideAction(action: OnSlideAction): Boolean {
+ return onSlideActions.add(action)
+ }
+
+ fun addOnStateChangedAction(action: OnStateChangedAction): Boolean {
+ return onStateChangedActions.add(action)
+ }
+}
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/DashboardActivity.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/DashboardActivity.kt
new file mode 100644
index 00000000..ed900419
--- /dev/null
+++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/DashboardActivity.kt
@@ -0,0 +1,233 @@
+package com.yogeshpaliyal.keypasscompose.ui.nav
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.content.Intent
+import android.os.Bundle
+import android.view.MenuItem
+import android.view.View
+import android.view.WindowManager
+import androidx.activity.viewModels
+import androidx.annotation.MenuRes
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.widget.Toolbar
+import androidx.core.view.isVisible
+import androidx.fragment.app.Fragment
+import androidx.navigation.NavController
+import androidx.navigation.NavDestination
+import androidx.navigation.fragment.NavHostFragment
+import com.google.android.material.transition.MaterialElevationScale
+import com.yogeshpaliyal.keypasscompose.R
+import com.yogeshpaliyal.keypasscompose.databinding.ActivityDashboardBinding
+import com.yogeshpaliyal.keypasscompose.ui.addTOTP.AddTOTPActivity
+import com.yogeshpaliyal.keypasscompose.ui.detail.DetailActivity
+import com.yogeshpaliyal.keypasscompose.ui.generate.GeneratePasswordActivity
+import com.yogeshpaliyal.keypasscompose.ui.home.DashboardViewModel
+import com.yogeshpaliyal.keypasscompose.ui.home.HomeFragmentDirections
+import com.yogeshpaliyal.keypasscompose.ui.settings.MySettingsFragmentDirections
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class DashboardActivity :
+ AppCompatActivity(),
+ Toolbar.OnMenuItemClickListener,
+ NavController.OnDestinationChangedListener,
+ NavigationAdapter.NavigationAdapterListener {
+ lateinit var binding: ActivityDashboardBinding
+
+ private val bottomNavDrawer: BottomNavDrawerFragment by lazy(LazyThreadSafetyMode.NONE) {
+ supportFragmentManager.findFragmentById(R.id.bottom_nav_drawer) as BottomNavDrawerFragment
+ }
+
+ private val mViewModel by viewModels()
+
+ private val currentNavigationFragment: Fragment?
+ get() = supportFragmentManager.findFragmentById(R.id.nav_host_fragment)
+ ?.childFragmentManager
+ ?.fragments
+ ?.first()
+
+ private val navController by lazy {
+ (supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment).navController
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ window.setFlags(
+ WindowManager.LayoutParams.FLAG_SECURE,
+ WindowManager.LayoutParams.FLAG_SECURE
+ )
+ binding = ActivityDashboardBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ // setSupportActionBar(binding.bottomAppBar)
+
+ binding.lifecycleOwner = this
+ binding.viewModel = mViewModel
+
+ navController.addOnDestinationChangedListener(
+ this@DashboardActivity
+ )
+
+ /* val intent = Intent(this, AuthenticationActivity::class.java)
+ startActivity(intent)*/
+
+ /* val autoFillService = getAutoFillService()
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+ if (autoFillService?.isAutofillSupported == true && autoFillService.hasEnabledAutofillServices().not()) {
+ val intent = Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE)
+ intent.data = Uri.parse("package:$packageName")
+ startActivityForResult(intent,777)
+ }
+ }*/
+
+ binding.btnAdd.setOnClickListener {
+ currentNavigationFragment?.apply {
+ exitTransition = MaterialElevationScale(false).apply {
+ duration =
+ resources.getInteger(R.integer.keypass_motion_duration_large).toLong()
+ }
+ reenterTransition = MaterialElevationScale(true).apply {
+ duration =
+ resources.getInteger(R.integer.keypass_motion_duration_large).toLong()
+ }
+ }
+
+ DetailActivity.start(this)
+ }
+
+ bottomNavDrawer.apply {
+ // addOnSlideAction(HalfClockwiseRotateSlideAction(binding.bottomAppBar))
+ // addOnSlideAction(AlphaSlideAction(binding.bottomAppBarTitle, true))
+ addOnStateChangedAction(ShowHideFabStateAction(binding.btnAdd))
+ addOnStateChangedAction(
+ ChangeSettingsMenuStateAction { showSettings ->
+ // Toggle between the current destination's BAB menu and the menu which should
+ // be displayed when the BottomNavigationDrawer is open.
+ binding.bottomAppBar.replaceMenu(
+ if (showSettings) {
+ R.menu.bottom_app_bar_settings_menu
+ } else {
+ getBottomAppBarMenuForDestination()
+ }
+ )
+ }
+ )
+
+ // addOnSandwichSlideAction(HalfCounterClockwiseRotateSlideAction(binding.bottomAppBarChevron))
+ addNavigationListener(this@DashboardActivity)
+ }
+
+ // Set up the BottomAppBar menu
+ binding.bottomAppBar.apply {
+ setNavigationOnClickListener {
+ bottomNavDrawer.toggle()
+ }
+ setOnMenuItemClickListener(this@DashboardActivity)
+ }
+ }
+
+ override fun onMenuItemClick(item: MenuItem?): Boolean {
+ when (item?.itemId) {
+ R.id.action_settings -> {
+ val settingDestination = MySettingsFragmentDirections.actionGlobalSettings()
+ navController.navigate(settingDestination)
+ bottomNavDrawer.close()
+ }
+ }
+ return true
+ }
+
+ override fun onDestinationChanged(
+ controller: NavController,
+ destination: NavDestination,
+ arguments: Bundle?
+ ) {
+
+ binding.searchAppBar.isVisible = destination.id == R.id.homeFragment
+ when (destination.id) {
+ R.id.homeFragment -> {
+ binding.btnAdd.isActivated = false
+ setBottomAppBarForHome(getBottomAppBarMenuForDestination(destination))
+ }
+ }
+ }
+
+ override fun onNavMenuItemClicked(item: NavigationModelItem.NavMenuItem) {
+ // Swap the list of emails for the given mailbox
+ // navigateToHome(item.titleRes, item.mailbox)
+ when (item.id) {
+ NavigationModel.GENERATE_PASSWORD -> {
+ val intent = Intent(this, GeneratePasswordActivity::class.java)
+ startActivity(intent)
+ }
+ NavigationModel.HOME -> {
+ val args = HomeFragmentDirections.actionGlobalHomeFragment()
+ navController.navigate(args)
+ }
+ NavigationModel.ADD_TOPT -> {
+ AddTOTPActivity.start(this)
+ }
+ }
+ }
+
+ override fun onNavEmailFolderClicked(folder: NavigationModelItem.NavEmailFolder) {
+ mViewModel.tag.postValue(folder.category)
+ val destination = HomeFragmentDirections.actionGlobalHomeFragmentTag()
+ navController.navigate(destination)
+ bottomNavDrawer.close()
+ }
+
+ private fun hideBottomAppBar() {
+ binding.run {
+ bottomAppBar.performHide()
+ // Get a handle on the animator that hides the bottom app bar so we can wait to hide
+ // the fab and bottom app bar until after it's exit animation finishes.
+ bottomAppBar.animate().setListener(object : AnimatorListenerAdapter() {
+ var isCanceled = false
+ override fun onAnimationEnd(animation: Animator) {
+ if (isCanceled) return
+
+ // Hide the BottomAppBar to avoid it showing above the keyboard
+ // when composing a new email.
+ bottomAppBar.visibility = View.GONE
+ btnAdd.visibility = View.INVISIBLE
+ }
+
+ override fun onAnimationCancel(animation: Animator) {
+ isCanceled = true
+ }
+ })
+ }
+ }
+
+ /**
+ * Helper function which returns the menu which should be displayed for the current
+ * destination.
+ *
+ * Used both when the destination has changed, centralizing destination-to-menu mapping, as
+ * well as switching between the alternate menu used when the BottomNavigationDrawer is
+ * open and closed.
+ */
+ @MenuRes
+ private fun getBottomAppBarMenuForDestination(destination: NavDestination? = null): Int {
+ val dest = destination ?: navController.currentDestination
+ return when (dest?.id) {
+ R.id.homeFragment -> R.menu.bottom_app_bar_settings_menu
+ // R.id.emailFragment -> R.menu.bottom_app_bar_email_menu
+ else -> R.menu.bottom_app_bar_settings_menu
+ }
+ }
+
+ private fun setBottomAppBarForHome(@MenuRes menuRes: Int) {
+ binding.run {
+ btnAdd.setImageState(intArrayOf(-android.R.attr.state_activated), true)
+ bottomAppBar.visibility = View.VISIBLE
+ bottomAppBar.replaceMenu(menuRes)
+ // btnAdd.contentDescription = getString(R.string.fab_compose_email_content_description)
+ // bottomAppBarTitle.visibility = View.VISIBLE
+ bottomAppBar.performShow()
+ btnAdd.show()
+ }
+ }
+}
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/NavigationAdapter.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/NavigationAdapter.kt
new file mode 100644
index 00000000..9d85f0c7
--- /dev/null
+++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/NavigationAdapter.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.yogeshpaliyal.keypasscompose.ui.nav
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.ListAdapter
+import com.yogeshpaliyal.keypasscompose.databinding.NavDividerItemLayoutBinding
+import com.yogeshpaliyal.keypasscompose.databinding.NavEmailFolderItemLayoutBinding
+import com.yogeshpaliyal.keypasscompose.databinding.NavMenuItemLayoutBinding
+
+private const val VIEW_TYPE_NAV_MENU_ITEM = 4
+private const val VIEW_TYPE_NAV_DIVIDER = 6
+private const val VIEW_TYPE_NAV_EMAIL_FOLDER_ITEM = 5
+
+class NavigationAdapter(
+ private val listener: NavigationAdapterListener
+) : ListAdapter>(
+ NavigationModelItem.NavModelItemDiff
+) {
+
+ interface NavigationAdapterListener {
+ fun onNavMenuItemClicked(item: NavigationModelItem.NavMenuItem)
+ fun onNavEmailFolderClicked(folder: NavigationModelItem.NavEmailFolder)
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ return when (getItem(position)) {
+ is NavigationModelItem.NavMenuItem -> VIEW_TYPE_NAV_MENU_ITEM
+ is NavigationModelItem.NavDivider -> VIEW_TYPE_NAV_DIVIDER
+ is NavigationModelItem.NavEmailFolder -> VIEW_TYPE_NAV_EMAIL_FOLDER_ITEM
+ else -> throw RuntimeException("Unsupported ItemViewType for obj ${getItem(position)}")
+ }
+ }
+
+ @Suppress("unchecked_cast")
+ override fun onCreateViewHolder(
+ parent: ViewGroup,
+ viewType: Int
+ ): NavigationViewHolder {
+ return when (viewType) {
+ VIEW_TYPE_NAV_MENU_ITEM -> NavigationViewHolder.NavMenuItemViewHolder(
+ NavMenuItemLayoutBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ ),
+ listener
+ )
+ VIEW_TYPE_NAV_DIVIDER -> NavigationViewHolder.NavDividerViewHolder(
+ NavDividerItemLayoutBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+ )
+ VIEW_TYPE_NAV_EMAIL_FOLDER_ITEM -> NavigationViewHolder.EmailFolderViewHolder(
+ NavEmailFolderItemLayoutBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ ),
+ listener
+ )
+ else -> throw RuntimeException("Unsupported view holder type")
+ } as NavigationViewHolder
+ }
+
+ override fun onBindViewHolder(
+ holder: NavigationViewHolder,
+ position: Int
+ ) {
+ holder.bind(getItem(position))
+ }
+}
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/NavigationModel.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/NavigationModel.kt
new file mode 100644
index 00000000..e76ccf5e
--- /dev/null
+++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/NavigationModel.kt
@@ -0,0 +1,34 @@
+package com.yogeshpaliyal.keypasscompose.ui.nav
+
+import com.yogeshpaliyal.keypasscompose.R
+
+/**
+ * A class which maintains and generates a navigation list to be displayed by [NavigationAdapter].
+ */
+object NavigationModel {
+
+ const val HOME = 0
+ const val GENERATE_PASSWORD = 1
+ const val ADD_TOPT = 2
+
+ var navigationMenuItems = mutableListOf(
+ NavigationModelItem.NavMenuItem(
+ id = HOME,
+ icon = R.drawable.ic_twotone_home_24,
+ titleRes = R.string.home,
+ checked = false,
+ ),
+ NavigationModelItem.NavMenuItem(
+ id = GENERATE_PASSWORD,
+ icon = R.drawable.ic_twotone_vpn_key_24,
+ titleRes = R.string.generate_password,
+ checked = false,
+ ),
+ NavigationModelItem.NavMenuItem(
+ id = ADD_TOPT,
+ icon = R.drawable.ic_twotone_totp,
+ titleRes = R.string.add_totp,
+ checked = false,
+ )
+ )
+}
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/NavigationModelItem.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/NavigationModelItem.kt
new file mode 100644
index 00000000..a7bd8609
--- /dev/null
+++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/NavigationModelItem.kt
@@ -0,0 +1,63 @@
+package com.yogeshpaliyal.keypasscompose.ui.nav
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import androidx.recyclerview.widget.DiffUtil
+import com.yogeshpaliyal.keypasscompose.utils.StringDiffUtil
+
+/**
+ * A sealed class which encapsulates all objects [NavigationAdapter] is able to display.
+ */
+sealed class NavigationModelItem {
+
+ /**
+ * A class which represents a checkable, navigation destination such as 'Inbox' or 'Sent'.
+ */
+ data class NavMenuItem(
+ val id: Int,
+ @DrawableRes val icon: Int,
+ @StringRes val titleRes: Int,
+ var checked: Boolean
+ ) : NavigationModelItem()
+
+ /**
+ * A class which is used to show a section divider (a subtitle and underline) between
+ * sections of different NavigationModelItem types.
+ */
+ data class NavDivider(val title: String) : NavigationModelItem()
+
+ /**
+ * A class which is used to show an [EmailFolder] in the [NavigationAdapter].
+ */
+ data class NavEmailFolder(val category: String) : NavigationModelItem()
+
+ object NavModelItemDiff : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(
+ oldItem: NavigationModelItem,
+ newItem: NavigationModelItem
+ ): Boolean {
+ return when {
+ oldItem is NavMenuItem && newItem is NavMenuItem ->
+ oldItem.id == newItem.id
+ oldItem is NavEmailFolder && newItem is NavEmailFolder ->
+ StringDiffUtil.areItemsTheSame(oldItem.category, newItem.category)
+ else -> oldItem == newItem
+ }
+ }
+
+ override fun areContentsTheSame(
+ oldItem: NavigationModelItem,
+ newItem: NavigationModelItem
+ ): Boolean {
+ return when {
+ oldItem is NavMenuItem && newItem is NavMenuItem ->
+ oldItem.icon == newItem.icon &&
+ oldItem.titleRes == newItem.titleRes &&
+ oldItem.checked == newItem.checked
+ oldItem is NavEmailFolder && newItem is NavEmailFolder ->
+ StringDiffUtil.areContentsTheSame(oldItem.category, newItem.category)
+ else -> false
+ }
+ }
+ }
+}
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/NavigationViewHolder.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/NavigationViewHolder.kt
new file mode 100644
index 00000000..4183f421
--- /dev/null
+++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/NavigationViewHolder.kt
@@ -0,0 +1,51 @@
+package com.yogeshpaliyal.keypasscompose.ui.nav
+
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+import com.yogeshpaliyal.keypasscompose.databinding.NavDividerItemLayoutBinding
+import com.yogeshpaliyal.keypasscompose.databinding.NavEmailFolderItemLayoutBinding
+import com.yogeshpaliyal.keypasscompose.databinding.NavMenuItemLayoutBinding
+
+sealed class NavigationViewHolder(
+ view: View
+) : RecyclerView.ViewHolder(view) {
+
+ abstract fun bind(navItem: T)
+
+ class NavMenuItemViewHolder(
+ private val binding: NavMenuItemLayoutBinding,
+ private val listener: NavigationAdapter.NavigationAdapterListener
+ ) : NavigationViewHolder(binding.root) {
+
+ override fun bind(navItem: NavigationModelItem.NavMenuItem) {
+ binding.run {
+ navMenuItem = navItem
+ navListener = listener
+ executePendingBindings()
+ }
+ }
+ }
+
+ class NavDividerViewHolder(
+ private val binding: NavDividerItemLayoutBinding
+ ) : NavigationViewHolder(binding.root) {
+ override fun bind(navItem: NavigationModelItem.NavDivider) {
+ binding.navDivider = navItem
+ binding.executePendingBindings()
+ }
+ }
+
+ class EmailFolderViewHolder(
+ private val binding: NavEmailFolderItemLayoutBinding,
+ private val listener: NavigationAdapter.NavigationAdapterListener
+ ) : NavigationViewHolder(binding.root) {
+
+ override fun bind(navItem: NavigationModelItem.NavEmailFolder) {
+ binding.run {
+ navEmailFolder = navItem
+ navListener = listener
+ executePendingBindings()
+ }
+ }
+ }
+}
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/OnSlideAction.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/OnSlideAction.kt
new file mode 100644
index 00000000..c944bee3
--- /dev/null
+++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/OnSlideAction.kt
@@ -0,0 +1,45 @@
+package com.yogeshpaliyal.keypasscompose.ui.nav
+
+import android.view.View
+import androidx.annotation.FloatRange
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.yogeshpaliyal.common.utils.normalize
+
+/**
+ * An action to be performed when a bottom sheet's slide offset is changed.
+ */
+interface OnSlideAction {
+ /**
+ * Called when the bottom sheet's [slideOffset] is changed. [slideOffset] will always be a
+ * value between -1.0 and 1.0. -1.0 is equal to [BottomSheetBehavior.STATE_HIDDEN], 0.0
+ * is equal to [BottomSheetBehavior.STATE_HALF_EXPANDED] and 1.0 is equal to
+ * [BottomSheetBehavior.STATE_EXPANDED].
+ */
+ fun onSlide(
+ sheet: View,
+ @FloatRange(
+ from = -1.0,
+ fromInclusive = true,
+ to = 1.0,
+ toInclusive = true
+ ) slideOffset: Float
+ )
+}
+
+/**
+ * Change the alpha of [view] when a bottom sheet is slid.
+ *
+ * @param reverse Setting reverse to true will cause the view's alpha to approach 0.0 as the sheet
+ * slides up. The default behavior, false, causes the view's alpha to approach 1.0 as the sheet
+ * slides up.
+ */
+class AlphaSlideAction(
+ private val view: View,
+ private val reverse: Boolean = false
+) : OnSlideAction {
+
+ override fun onSlide(sheet: View, slideOffset: Float) {
+ val alpha = slideOffset.normalize(-1F, 0F, 0F, 1F)
+ view.alpha = if (!reverse) alpha else 1F - alpha
+ }
+}
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/OnStateChangedAction.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/OnStateChangedAction.kt
new file mode 100644
index 00000000..1b7c24a7
--- /dev/null
+++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/nav/OnStateChangedAction.kt
@@ -0,0 +1,90 @@
+
+package com.yogeshpaliyal.keypasscompose.ui.nav
+
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.floatingactionbutton.FloatingActionButton
+
+/**
+ * An action to be performed when a bottom sheet's state is changed.
+ */
+interface OnStateChangedAction {
+ fun onStateChanged(sheet: View, newState: Int)
+}
+
+/**
+ * A state change action that tells the calling client when a open-sheet specific menu should be
+ * used.
+ */
+class ChangeSettingsMenuStateAction(
+ private val onShouldShowSettingsMenu: (showSettings: Boolean) -> Unit
+) : OnStateChangedAction {
+
+ private var hasCalledShowSettingsMenu: Boolean = false
+
+ override fun onStateChanged(sheet: View, newState: Int) {
+ if (newState == BottomSheetBehavior.STATE_HIDDEN) {
+ hasCalledShowSettingsMenu = false
+ onShouldShowSettingsMenu(false)
+ } else {
+ if (!hasCalledShowSettingsMenu) {
+ hasCalledShowSettingsMenu = true
+ onShouldShowSettingsMenu(true)
+ }
+ }
+ }
+}
+
+/**
+ * A state change action that handles showing the fab when the sheet is hidden and hiding the fab
+ * when the sheet is not hidden.
+ */
+class ShowHideFabStateAction(
+ private val fab: FloatingActionButton
+) : OnStateChangedAction {
+
+ override fun onStateChanged(sheet: View, newState: Int) {
+ if (newState == BottomSheetBehavior.STATE_HIDDEN) {
+ fab.show()
+ } else {
+ fab.hide()
+ }
+ }
+}
+
+/**
+ * A state change action that sets a view's visibility depending on whether the sheet is hidden
+ * or not.
+ *
+ * By default, the view will be hidden when the sheet is hidden and shown when the sheet is shown
+ * (not hidden). If [reverse] is set to true, the view will be shown when the sheet is hidden and
+ * hidden when the sheet is shown (not hidden).
+ */
+class VisibilityStateAction(
+ private val view: View,
+ private val reverse: Boolean = false
+) : OnStateChangedAction {
+ override fun onStateChanged(sheet: View, newState: Int) {
+ val stateHiddenVisibility = if (!reverse) View.GONE else View.VISIBLE
+ val stateDefaultVisibility = if (!reverse) View.VISIBLE else View.GONE
+ when (newState) {
+ BottomSheetBehavior.STATE_HIDDEN -> view.visibility = stateHiddenVisibility
+ else -> view.visibility = stateDefaultVisibility
+ }
+ }
+}
+
+/**
+ * A state change action which scrolls a [RecyclerView] to the top when the sheet is hidden.
+ *
+ * This is used to make sure the navigation drawer's [RecyclerView] is never half-scrolled when
+ * opened to the half-expanded state, which can happen if the sheet is hidden while scrolled.
+ */
+class ScrollToTopStateAction(
+ private val recyclerView: RecyclerView
+) : OnStateChangedAction {
+ override fun onStateChanged(sheet: View, newState: Int) {
+ if (newState == BottomSheetBehavior.STATE_HIDDEN) recyclerView.scrollToPosition(0)
+ }
+}
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/settings/MySettingsFragment.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/settings/MySettingsFragment.kt
new file mode 100644
index 00000000..cc59ec11
--- /dev/null
+++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/settings/MySettingsFragment.kt
@@ -0,0 +1,203 @@
+package com.yogeshpaliyal.keypasscompose.ui.settings
+
+import android.app.Activity
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.widget.Toast
+import androidx.core.content.ContextCompat.getSystemService
+import androidx.documentfile.provider.DocumentFile
+import androidx.lifecycle.lifecycleScope
+import androidx.preference.Preference
+import androidx.preference.PreferenceFragmentCompat
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.yogeshpaliyal.common.dbhelper.createBackup
+import com.yogeshpaliyal.common.dbhelper.restoreBackup
+import com.yogeshpaliyal.common.utils.email
+import com.yogeshpaliyal.common.utils.getOrCreateBackupKey
+import com.yogeshpaliyal.common.utils.setBackupDirectory
+import com.yogeshpaliyal.keypasscompose.BuildConfig
+import com.yogeshpaliyal.keypasscompose.R
+import com.yogeshpaliyal.keypasscompose.databinding.LayoutBackupKeypharseBinding
+import com.yogeshpaliyal.keypasscompose.databinding.LayoutRestoreKeypharseBinding
+import com.yogeshpaliyal.keypasscompose.ui.backup.BackupActivity
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+private const val CHOOSE_BACKUPS_LOCATION_REQUEST_CODE = 26212
+private const val CHOOSE_RESTORE_FILE_REQUEST_CODE = 26213
+
+@AndroidEntryPoint
+class MySettingsFragment : PreferenceFragmentCompat() {
+
+ @Inject
+ lateinit var appDb: com.yogeshpaliyal.common.AppDatabase
+
+ override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+ setPreferencesFromResource(R.xml.preferences, rootKey)
+ }
+
+ override fun onPreferenceTreeClick(preference: Preference): Boolean {
+ return when (preference.key) {
+ "feedback" -> {
+ context?.email(
+ getString(R.string.feedback_to_keypass),
+ "yogeshpaliyal.foss@gmail.com"
+ )
+ true
+ }
+
+ "backup" -> {
+ BackupActivity.start(context)
+ true
+ }
+
+ getString(R.string.settings_restore_backup) -> {
+ selectRestoreFile()
+ true
+ }
+
+ "share" -> {
+ val sendIntent = Intent()
+ sendIntent.action = Intent.ACTION_SEND
+ sendIntent.putExtra(
+ Intent.EXTRA_TEXT,
+ "KeyPass Password Manager\n Offline, Secure, Open Source https://play.google.com/store/apps/details?id=" + BuildConfig.APPLICATION_ID
+ )
+ sendIntent.type = "text/plain"
+ startActivity(Intent.createChooser(sendIntent, getString(R.string.share_keypass)))
+ true
+ }
+ else -> super.onPreferenceTreeClick(preference)
+ }
+ }
+
+ private fun selectRestoreFile() {
+ val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
+ intent.addCategory(Intent.CATEGORY_OPENABLE)
+ intent.type = "*/*"
+
+ intent.addFlags(
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
+ Intent.FLAG_GRANT_READ_URI_PERMISSION
+ )
+
+ try {
+ startActivityForResult(intent, CHOOSE_RESTORE_FILE_REQUEST_CODE)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ if (requestCode == CHOOSE_BACKUPS_LOCATION_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
+ val contentResolver = context?.contentResolver
+ val selectedDirectory = data?.data
+ if (contentResolver != null && selectedDirectory != null) {
+ contentResolver.takePersistableUriPermission(
+ selectedDirectory,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ )
+
+ lifecycleScope.launch {
+ context?.setBackupDirectory(selectedDirectory.toString())
+
+ backup(selectedDirectory)
+ }
+ }
+ } else if (requestCode == CHOOSE_RESTORE_FILE_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
+ val contentResolver = context?.contentResolver
+ val selectedFile = data?.data
+ if (contentResolver != null && selectedFile != null) {
+
+ val binding = LayoutRestoreKeypharseBinding.inflate(layoutInflater)
+
+ MaterialAlertDialogBuilder(requireContext()).setView(binding.root)
+ .setNegativeButton(
+ "Cancel"
+ ) { dialog, which ->
+ dialog.dismiss()
+ }
+ .setPositiveButton(
+ "Restore"
+ ) { dialog, which ->
+ lifecycleScope.launch {
+ val result = appDb.restoreBackup(
+ binding.etKeyPhrase.text.toString(),
+ contentResolver,
+ selectedFile
+ )
+ if (result) {
+ dialog?.dismiss()
+ Toast.makeText(
+ context,
+ getString(R.string.backup_restored),
+ Toast.LENGTH_SHORT
+ ).show()
+ } else {
+ Toast.makeText(
+ context,
+ getString(R.string.invalid_keyphrase),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ }.show()
+ }
+ }
+ }
+
+ suspend fun backup(selectedDirectory: Uri) {
+
+ val keyPair = requireContext().getOrCreateBackupKey()
+
+ val tempFile = DocumentFile.fromTreeUri(requireContext(), selectedDirectory)?.createFile(
+ "*/*",
+ "key_pass_backup_${System.currentTimeMillis()}.keypass"
+ )
+
+ lifecycleScope.launch {
+ context?.contentResolver?.let {
+ appDb.createBackup(
+ keyPair.second,
+ it,
+ tempFile?.uri
+ )
+ if (keyPair.first) {
+ val binding = LayoutBackupKeypharseBinding.inflate(layoutInflater)
+ binding.txtCode.text = requireContext().getOrCreateBackupKey().second
+ binding.txtCode.setOnClickListener {
+ val clipboard =
+ getSystemService(requireContext(), ClipboardManager::class.java)
+ val clip = ClipData.newPlainText(
+ getString(R.string.app_name),
+ binding.txtCode.text
+ )
+ clipboard?.setPrimaryClip(clip)
+ Toast.makeText(
+ context,
+ getString(R.string.copied_to_clipboard),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ MaterialAlertDialogBuilder(requireContext()).setView(binding.root)
+ .setPositiveButton(
+ "Yes"
+ ) { dialog, which ->
+ dialog?.dismiss()
+ }.show()
+ } else {
+ Toast.makeText(
+ context,
+ getString(R.string.backup_completed),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ }
+ }
+}
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Color.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Color.kt
deleted file mode 100644
index ed07d567..00000000
--- a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Color.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-package com.yogeshpaliyal.keypasscompose.ui.theme
-
-import androidx.compose.ui.graphics.Color
-
-@Suppress
-val KeyPassWhite50 = Color(0xFFffffff)
-
-val KeyPassBlack800 = Color(0xFF121212)
-val KeyPassBlack900 = Color(0xFF000000)
-
-val KeyPassBlue50 = Color(0xFFeef0f2)
-val KeyPassBlue100 = Color(0xFFd2dbe0)
-val KeyPassBlue200 = Color(0xFFadbbc4)
-val KeyPassBlue300 = Color(0xFF8ca2ae)
-val KeyPassBlue600 = Color(0xFF4a6572)
-val KeyPassBlue700 = Color(0xFF344955)
-val KeyPassBlue800 = Color(0xFF232f34)
-val Blue800 = Color(0xFF6c63ff)
-
-val KeyPassOrange300 = Color(0xFFfbd790)
-val KeyPassOrange400 = Color(0xFFf9be64)
-val KeyPassOrange500 = Color(0xFFf9aa33)
-
-val KeyPassRed200 = Color(0xFFcf7779)
-val KeyPassRed400 = Color(0xFFff4c5d)
-
-val KeyPassWhite50alpha060 = Color(0x99ffffff)
-
-val KeyPassBlue50alpha060 = Color(0x99eef0f2)
-
-val KeyPassBlack900alpha020 = Color(0x33000000)
-val KeyPassBlack900alpha087 = Color(0xde000000)
-val KeyPassBlack900alpha060 = Color(0x99000000)
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Material3Components.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Material3Components.kt
deleted file mode 100644
index f43317ec..00000000
--- a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Material3Components.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-package com.yogeshpaliyal.keypasscompose.ui.theme
-
-import androidx.compose.foundation.layout.ColumnScope
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.RowScope
-import androidx.compose.material.AppBarDefaults
-import androidx.compose.material.BottomAppBar
-import androidx.compose.material.DrawerDefaults
-import androidx.compose.material.FabPosition
-import androidx.compose.material.Scaffold
-import androidx.compose.material.ScaffoldState
-import androidx.compose.material.SnackbarHost
-import androidx.compose.material.SnackbarHostState
-import androidx.compose.material.rememberScaffoldState
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.contentColorFor
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.unit.Dp
-
-@Composable
-fun Material3BottomAppBar(
- modifier: Modifier = Modifier,
- backgroundColor: Color = MaterialTheme.colorScheme.surface,
- contentColor: Color = contentColorFor(backgroundColor),
- cutoutShape: Shape? = null,
- elevation: Dp = AppBarDefaults.BottomAppBarElevation,
- contentPadding: PaddingValues = AppBarDefaults.ContentPadding,
- content: @Composable RowScope.() -> Unit
-) {
- BottomAppBar(
- modifier,
- backgroundColor,
- contentColor,
- cutoutShape,
- elevation,
- contentPadding,
- content
- )
-}
-
-@Composable
-fun Material3Scaffold(
- modifier: Modifier = Modifier,
- scaffoldState: ScaffoldState = rememberScaffoldState(),
- topBar: @Composable () -> Unit = {},
- bottomBar: @Composable () -> Unit = {},
- snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
- floatingActionButton: @Composable () -> Unit = {},
- floatingActionButtonPosition: FabPosition = FabPosition.End,
- isFloatingActionButtonDocked: Boolean = false,
- drawerContent: @Composable (ColumnScope.() -> Unit)? = null,
- drawerGesturesEnabled: Boolean = true,
- drawerShape: Shape = androidx.compose.material.MaterialTheme.shapes.large,
- drawerElevation: Dp = DrawerDefaults.Elevation,
- drawerBackgroundColor: Color = MaterialTheme.colorScheme.surface,
- drawerContentColor: Color = contentColorFor(
- drawerBackgroundColor
- ),
- drawerScrimColor: Color = DrawerDefaults.scrimColor,
- backgroundColor: Color = MaterialTheme.colorScheme.background,
- contentColor: Color = contentColorFor(
- backgroundColor
- ),
- content: @Composable (PaddingValues) -> Unit
-) {
- Scaffold(modifier, scaffoldState, topBar, bottomBar, snackbarHost, floatingActionButton, floatingActionButtonPosition, isFloatingActionButtonDocked, drawerContent, drawerGesturesEnabled, drawerShape, drawerElevation, drawerBackgroundColor, drawerContentColor, drawerScrimColor, backgroundColor, contentColor, content)
-}
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Shape.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Shape.kt
deleted file mode 100644
index 8a765e43..00000000
--- a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Shape.kt
+++ /dev/null
@@ -1 +0,0 @@
-package com.yogeshpaliyal.keypasscompose.ui.theme
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Theme.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Theme.kt
deleted file mode 100644
index 54571a3d..00000000
--- a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Theme.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-package com.yogeshpaliyal.keypasscompose.ui.theme
-
-import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.darkColorScheme
-import androidx.compose.material3.lightColorScheme
-import androidx.compose.runtime.Composable
-
-private val DarkColorPalette = darkColorScheme(
- primary = KeyPassBlue700,
- primaryContainer = KeyPassOrange500,
- secondary = KeyPassOrange500
-)
-
-private val LightColorPalette = lightColorScheme(
- primary = KeyPassBlue700,
- primaryContainer = KeyPassOrange500,
- secondary = KeyPassOrange500,
- tertiaryContainer = Blue800,
- background = KeyPassBlue50,
- surface = KeyPassWhite50,
- error = KeyPassRed400,
- onPrimary = KeyPassWhite50,
- onSecondary = KeyPassBlack900,
- onSurface = KeyPassBlack900,
- onError = KeyPassBlack900
-
- /* Other default colors to override
- background = Color.White,
- surface = Color.White,
- onPrimary = Color.White,
- onSecondary = Color.Black,
- onBackground = Color.Black,
- onSurface = Color.Black,
- */
-)
-
-@Composable
-fun KeyPassTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) {
- val colors = if (darkTheme) {
- DarkColorPalette
- } else {
- LightColorPalette
- }
-
- MaterialTheme(
- colorScheme = colors,
- typography = Typography,
- content = content
- )
-}
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Type.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Type.kt
deleted file mode 100644
index b07440d3..00000000
--- a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/ui/theme/Type.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package com.yogeshpaliyal.keypasscompose.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(
- bodyMedium = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Normal,
- fontSize = 16.sp
- )
- /* Other default text styles to override
- button = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.W500,
- fontSize = 14.sp
- ),
- caption = TextStyle(
- fontFamily = FontFamily.Default,
- fontWeight = FontWeight.Normal,
- fontSize = 12.sp
- )
- */
-)
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/BindingAdapter.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/BindingAdapter.kt
new file mode 100644
index 00000000..98d0c38c
--- /dev/null
+++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/BindingAdapter.kt
@@ -0,0 +1,219 @@
+/*
+ * Copyright (c) 2019 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under
+ * the License
+ *
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.yogeshpaliyal.keypasscompose.utils
+
+import android.graphics.drawable.ColorDrawable
+import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import android.view.WindowInsets
+import android.widget.Spinner
+import android.widget.TextView
+import androidx.annotation.DrawableRes
+import androidx.core.view.updateLayoutParams
+import androidx.databinding.BindingAdapter
+import com.google.android.material.elevation.ElevationOverlayProvider
+import com.yogeshpaliyal.common.utils.getDrawableOrNull
+
+@BindingAdapter(
+ "popupElevationOverlay"
+)
+fun Spinner.bindPopupElevationOverlay(popupElevationOverlay: Float) {
+ setPopupBackgroundDrawable(
+ ColorDrawable(
+ ElevationOverlayProvider(context)
+ .compositeOverlayWithThemeSurfaceColorIfNeeded(popupElevationOverlay)
+ )
+ )
+}
+
+@BindingAdapter(
+ "drawableStart",
+ "drawableLeft",
+ "drawableTop",
+ "drawableEnd",
+ "drawableRight",
+ "drawableBottom",
+ requireAll = false
+)
+fun TextView.bindDrawables(
+ @DrawableRes drawableStart: Int? = null,
+ @DrawableRes drawableLeft: Int? = null,
+ @DrawableRes drawableTop: Int? = null,
+ @DrawableRes drawableEnd: Int? = null,
+ @DrawableRes drawableRight: Int? = null,
+ @DrawableRes drawableBottom: Int? = null
+) {
+ setCompoundDrawablesWithIntrinsicBounds(
+ context.getDrawableOrNull(drawableStart ?: drawableLeft),
+ context.getDrawableOrNull(drawableTop),
+ context.getDrawableOrNull(drawableEnd ?: drawableRight),
+ context.getDrawableOrNull(drawableBottom)
+ )
+}
+
+@BindingAdapter("goneIf")
+fun View.bindGoneIf(gone: Boolean) {
+ visibility = if (gone) {
+ GONE
+ } else {
+ VISIBLE
+ }
+}
+
+@BindingAdapter("layoutFullscreen")
+fun View.bindLayoutFullscreen(previousFullscreen: Boolean, fullscreen: Boolean) {
+ if (previousFullscreen != fullscreen && fullscreen) {
+ systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
+ View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
+ View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ }
+}
+
+@BindingAdapter(
+ "paddingLeftSystemWindowInsets",
+ "paddingTopSystemWindowInsets",
+ "paddingRightSystemWindowInsets",
+ "paddingBottomSystemWindowInsets",
+ requireAll = false
+)
+fun View.applySystemWindowInsetsPadding(
+ previousApplyLeft: Boolean,
+ previousApplyTop: Boolean,
+ previousApplyRight: Boolean,
+ previousApplyBottom: Boolean,
+ applyLeft: Boolean,
+ applyTop: Boolean,
+ applyRight: Boolean,
+ applyBottom: Boolean
+) {
+ if (previousApplyLeft == applyLeft &&
+ previousApplyTop == applyTop &&
+ previousApplyRight == applyRight &&
+ previousApplyBottom == applyBottom
+ ) {
+ return
+ }
+
+ doOnApplyWindowInsets { view, insets, padding, _, _ ->
+ val left = if (applyLeft) insets.systemWindowInsetLeft else 0
+ val top = if (applyTop) insets.systemWindowInsetTop else 0
+ val right = if (applyRight) insets.systemWindowInsetRight else 0
+ val bottom = if (applyBottom) insets.systemWindowInsetBottom else 0
+
+ view.setPadding(
+ padding.left + left,
+ padding.top + top,
+ padding.right + right,
+ padding.bottom + bottom
+ )
+ }
+}
+
+@BindingAdapter(
+ "marginLeftSystemWindowInsets",
+ "marginTopSystemWindowInsets",
+ "marginRightSystemWindowInsets",
+ "marginBottomSystemWindowInsets",
+ requireAll = false
+)
+fun View.applySystemWindowInsetsMargin(
+ previousApplyLeft: Boolean,
+ previousApplyTop: Boolean,
+ previousApplyRight: Boolean,
+ previousApplyBottom: Boolean,
+ applyLeft: Boolean,
+ applyTop: Boolean,
+ applyRight: Boolean,
+ applyBottom: Boolean
+) {
+ if (previousApplyLeft == applyLeft &&
+ previousApplyTop == applyTop &&
+ previousApplyRight == applyRight &&
+ previousApplyBottom == applyBottom
+ ) {
+ return
+ }
+
+ doOnApplyWindowInsets { view, insets, _, margin, _ ->
+ val left = if (applyLeft) insets.systemWindowInsetLeft else 0
+ val top = if (applyTop) insets.systemWindowInsetTop else 0
+ val right = if (applyRight) insets.systemWindowInsetRight else 0
+ val bottom = if (applyBottom) insets.systemWindowInsetBottom else 0
+
+ view.updateLayoutParams {
+ leftMargin = margin.left + left
+ topMargin = margin.top + top
+ rightMargin = margin.right + right
+ bottomMargin = margin.bottom + bottom
+ }
+ }
+}
+
+fun View.doOnApplyWindowInsets(
+ block: (View, WindowInsets, InitialPadding, InitialMargin, Int) -> Unit
+) {
+ // Create a snapshot of the view's padding & margin states
+ val initialPadding = recordInitialPaddingForView(this)
+ val initialMargin = recordInitialMarginForView(this)
+ val initialHeight = recordInitialHeightForView(this)
+ // Set an actual OnApplyWindowInsetsListener which proxies to the given
+ // lambda, also passing in the original padding & margin states
+ setOnApplyWindowInsetsListener { v, insets ->
+ block(v, insets, initialPadding, initialMargin, initialHeight)
+ // Always return the insets, so that children can also use them
+ insets
+ }
+ // request some insets
+ requestApplyInsetsWhenAttached()
+}
+
+class InitialPadding(val left: Int, val top: Int, val right: Int, val bottom: Int)
+
+class InitialMargin(val left: Int, val top: Int, val right: Int, val bottom: Int)
+
+private fun recordInitialPaddingForView(view: View) = InitialPadding(
+ view.paddingLeft, view.paddingTop, view.paddingRight, view.paddingBottom
+)
+
+private fun recordInitialMarginForView(view: View): InitialMargin {
+ val lp = view.layoutParams as? ViewGroup.MarginLayoutParams
+ ?: throw IllegalArgumentException("Invalid view layout params")
+ return InitialMargin(lp.leftMargin, lp.topMargin, lp.rightMargin, lp.bottomMargin)
+}
+
+private fun recordInitialHeightForView(view: View): Int {
+ return view.layoutParams.height
+}
+
+fun View.requestApplyInsetsWhenAttached() {
+ if (isAttachedToWindow) {
+ // We're already attached, just request as normal
+ requestApplyInsets()
+ } else {
+ // We're not attached to the hierarchy, add a listener to
+ // request when we are
+ addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
+ override fun onViewAttachedToWindow(v: View) {
+ v.removeOnAttachStateChangeListener(this)
+ v.requestApplyInsets()
+ }
+
+ override fun onViewDetachedFromWindow(v: View) = Unit
+ })
+ }
+}
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/ContentViewBindingDelegate.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/ContentViewBindingDelegate.kt
new file mode 100644
index 00000000..05ae5245
--- /dev/null
+++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/ContentViewBindingDelegate.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.yogeshpaliyal.keypasscompose.utils
+
+import android.app.Activity
+import androidx.annotation.LayoutRes
+import androidx.appcompat.app.AppCompatActivity
+import androidx.databinding.DataBindingUtil
+import androidx.databinding.ViewDataBinding
+import kotlin.reflect.KProperty
+
+/**
+ * A delegate who lazily inflates a data binding layout, calls [Activity.setContentView], sets
+ * the lifecycle owner and returns the binding.
+ */
+class ContentViewBindingDelegate(
+ @LayoutRes private val layoutRes: Int
+) {
+
+ private var binding: T? = null
+
+ operator fun getValue(activity: R, property: KProperty<*>): T {
+ if (binding == null) {
+ binding = DataBindingUtil.setContentView(activity, layoutRes).apply {
+ lifecycleOwner = activity
+ }
+ }
+ return binding!!
+ }
+}
+
+fun contentView(
+ @LayoutRes layoutRes: Int
+): ContentViewBindingDelegate = ContentViewBindingDelegate(layoutRes)
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/LogHelper.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/LogHelper.kt
new file mode 100644
index 00000000..aaf57d60
--- /dev/null
+++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/LogHelper.kt
@@ -0,0 +1,49 @@
+package com.yogeshpaliyal.keypasscompose.utils
+
+import android.util.Log
+import com.yogeshpaliyal.keypasscompose.BuildConfig
+
+/*
+* @author Yogesh Paliyal
+* techpaliyal@gmail.com
+* https://techpaliyal.com
+* created on 26-12-2020 20:21
+*/
+
+@JvmName("LogHelper")
+
+fun Any?.systemOutPrint() {
+ if (BuildConfig.DEBUG) println(this)
+}
+
+fun Any?.systemErrPrint() {
+ if (BuildConfig.DEBUG) System.err.println(this)
+}
+
+fun Exception?.debugPrintStackTrace() {
+ if (BuildConfig.DEBUG) this?.printStackTrace()
+}
+
+fun Throwable?.debugPrintStackTrace() {
+ if (BuildConfig.DEBUG) this?.printStackTrace()
+}
+
+fun Any?.logD(tag: String?) {
+ if (BuildConfig.DEBUG) Log.d(tag, this.toString())
+}
+
+fun Any?.logE(tag: String?) {
+ if (BuildConfig.DEBUG) Log.e(tag, this.toString())
+}
+
+fun Any?.logI(tag: String?) {
+ if (BuildConfig.DEBUG) Log.i(tag, this.toString())
+}
+
+fun Any?.logV(tag: String?) {
+ if (BuildConfig.DEBUG) Log.v(tag, this.toString())
+}
+
+fun Any?.logW(tag: String?) {
+ if (BuildConfig.DEBUG) Log.w(tag, this.toString())
+}
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/StringDiffUtil.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/StringDiffUtil.kt
new file mode 100644
index 00000000..a418c972
--- /dev/null
+++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/StringDiffUtil.kt
@@ -0,0 +1,12 @@
+package com.yogeshpaliyal.keypasscompose.utils
+
+import androidx.recyclerview.widget.DiffUtil
+
+/**
+ * Alias to represent a folder (a String title) into which emails can be placed.
+ */
+
+object StringDiffUtil : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: String, newItem: String) = oldItem == newItem
+ override fun areContentsTheSame(oldItem: String, newItem: String) = oldItem == newItem
+}
diff --git a/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/ViewExtensions.kt b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/ViewExtensions.kt
new file mode 100644
index 00000000..203b8e1d
--- /dev/null
+++ b/keypasscompose/src/main/java/com/yogeshpaliyal/keypasscompose/utils/ViewExtensions.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.yogeshpaliyal.keypasscompose.utils
+
+import android.content.Context
+import android.os.Build
+import android.widget.TextView
+
+@Suppress("DEPRECATION")
+fun TextView.setTextAppearanceCompat(context: Context, resId: Int) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ setTextAppearance(resId)
+ } else {
+ setTextAppearance(context, resId)
+ }
+}
diff --git a/keypasscompose/src/main/res/color/color_navigation_drawer_menu_item.xml b/keypasscompose/src/main/res/color/color_navigation_drawer_menu_item.xml
new file mode 100644
index 00000000..c714257d
--- /dev/null
+++ b/keypasscompose/src/main/res/color/color_navigation_drawer_menu_item.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/color/color_on_primary_surface_divider.xml b/keypasscompose/src/main/res/color/color_on_primary_surface_divider.xml
new file mode 100644
index 00000000..3e4b2b99
--- /dev/null
+++ b/keypasscompose/src/main/res/color/color_on_primary_surface_divider.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/color/color_on_primary_surface_emphasis_medium.xml b/keypasscompose/src/main/res/color/color_on_primary_surface_emphasis_medium.xml
new file mode 100644
index 00000000..cefe6ba4
--- /dev/null
+++ b/keypasscompose/src/main/res/color/color_on_primary_surface_emphasis_medium.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/color/color_on_surface_emphasis_high.xml b/keypasscompose/src/main/res/color/color_on_surface_emphasis_high.xml
new file mode 100644
index 00000000..8e3ec6b9
--- /dev/null
+++ b/keypasscompose/src/main/res/color/color_on_surface_emphasis_high.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/color/color_on_surface_emphasis_medium.xml b/keypasscompose/src/main/res/color/color_on_surface_emphasis_medium.xml
new file mode 100644
index 00000000..bfbf26df
--- /dev/null
+++ b/keypasscompose/src/main/res/color/color_on_surface_emphasis_medium.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/drawable/asl_add_save.xml b/keypasscompose/src/main/res/drawable/asl_add_save.xml
new file mode 100644
index 00000000..7fa37cb0
--- /dev/null
+++ b/keypasscompose/src/main/res/drawable/asl_add_save.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/drawable/avatar_none.xml b/keypasscompose/src/main/res/drawable/avatar_none.xml
new file mode 100644
index 00000000..b09a6b43
--- /dev/null
+++ b/keypasscompose/src/main/res/drawable/avatar_none.xml
@@ -0,0 +1,5 @@
+
+
+
+
diff --git a/keypasscompose/src/main/res/drawable/avd_add_to_save.xml b/keypasscompose/src/main/res/drawable/avd_add_to_save.xml
new file mode 100644
index 00000000..7a51b491
--- /dev/null
+++ b/keypasscompose/src/main/res/drawable/avd_add_to_save.xml
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/keypasscompose/src/main/res/drawable/avd_save_to_add.xml b/keypasscompose/src/main/res/drawable/avd_save_to_add.xml
new file mode 100644
index 00000000..56320f6c
--- /dev/null
+++ b/keypasscompose/src/main/res/drawable/avd_save_to_add.xml
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/keypasscompose/src/main/res/drawable/ic_baseline_arrow_back_ios_24.xml b/keypasscompose/src/main/res/drawable/ic_baseline_arrow_back_ios_24.xml
new file mode 100644
index 00000000..6d147990
--- /dev/null
+++ b/keypasscompose/src/main/res/drawable/ic_baseline_arrow_back_ios_24.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/keypasscompose/src/main/res/drawable/ic_baseline_casino_24.xml b/keypasscompose/src/main/res/drawable/ic_baseline_casino_24.xml
new file mode 100644
index 00000000..fb4b03aa
--- /dev/null
+++ b/keypasscompose/src/main/res/drawable/ic_baseline_casino_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/keypasscompose/src/main/res/drawable/ic_baseline_feedback_24.xml b/keypasscompose/src/main/res/drawable/ic_baseline_feedback_24.xml
new file mode 100644
index 00000000..170a74b1
--- /dev/null
+++ b/keypasscompose/src/main/res/drawable/ic_baseline_feedback_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/keypasscompose/src/main/res/drawable/ic_baseline_refresh_24.xml b/keypasscompose/src/main/res/drawable/ic_baseline_refresh_24.xml
new file mode 100644
index 00000000..f2be45ba
--- /dev/null
+++ b/keypasscompose/src/main/res/drawable/ic_baseline_refresh_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/keypasscompose/src/main/res/drawable/ic_baseline_settings_24.xml b/keypasscompose/src/main/res/drawable/ic_baseline_settings_24.xml
new file mode 100644
index 00000000..41a82ede
--- /dev/null
+++ b/keypasscompose/src/main/res/drawable/ic_baseline_settings_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/keypasscompose/src/main/res/drawable/ic_baseline_share_24.xml b/keypasscompose/src/main/res/drawable/ic_baseline_share_24.xml
new file mode 100644
index 00000000..2f13bb3e
--- /dev/null
+++ b/keypasscompose/src/main/res/drawable/ic_baseline_share_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/keypasscompose/src/main/res/drawable/ic_round_add_24.xml b/keypasscompose/src/main/res/drawable/ic_round_add_24.xml
new file mode 100644
index 00000000..24877ee9
--- /dev/null
+++ b/keypasscompose/src/main/res/drawable/ic_round_add_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/keypasscompose/src/main/res/drawable/ic_round_done_24.xml b/keypasscompose/src/main/res/drawable/ic_round_done_24.xml
new file mode 100644
index 00000000..6a86ea58
--- /dev/null
+++ b/keypasscompose/src/main/res/drawable/ic_round_done_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/keypasscompose/src/main/res/drawable/ic_round_menu_24.xml b/keypasscompose/src/main/res/drawable/ic_round_menu_24.xml
new file mode 100644
index 00000000..ff67e556
--- /dev/null
+++ b/keypasscompose/src/main/res/drawable/ic_round_menu_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/keypasscompose/src/main/res/drawable/ic_round_refresh_24.xml b/keypasscompose/src/main/res/drawable/ic_round_refresh_24.xml
new file mode 100644
index 00000000..5cdbeb07
--- /dev/null
+++ b/keypasscompose/src/main/res/drawable/ic_round_refresh_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/keypasscompose/src/main/res/drawable/ic_twotone_content_copy_24.xml b/keypasscompose/src/main/res/drawable/ic_twotone_content_copy_24.xml
new file mode 100644
index 00000000..86652df4
--- /dev/null
+++ b/keypasscompose/src/main/res/drawable/ic_twotone_content_copy_24.xml
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/keypasscompose/src/main/res/drawable/ic_twotone_delete_24.xml b/keypasscompose/src/main/res/drawable/ic_twotone_delete_24.xml
new file mode 100644
index 00000000..b77afdc9
--- /dev/null
+++ b/keypasscompose/src/main/res/drawable/ic_twotone_delete_24.xml
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/keypasscompose/src/main/res/drawable/ic_twotone_folder.xml b/keypasscompose/src/main/res/drawable/ic_twotone_folder.xml
new file mode 100644
index 00000000..b56127e2
--- /dev/null
+++ b/keypasscompose/src/main/res/drawable/ic_twotone_folder.xml
@@ -0,0 +1,14 @@
+
+
+
+
diff --git a/keypasscompose/src/main/res/drawable/ic_twotone_home_24.xml b/keypasscompose/src/main/res/drawable/ic_twotone_home_24.xml
new file mode 100644
index 00000000..347e5a63
--- /dev/null
+++ b/keypasscompose/src/main/res/drawable/ic_twotone_home_24.xml
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/keypasscompose/src/main/res/drawable/ic_twotone_qr_code_scanner_24.xml b/keypasscompose/src/main/res/drawable/ic_twotone_qr_code_scanner_24.xml
new file mode 100644
index 00000000..597e8d7b
--- /dev/null
+++ b/keypasscompose/src/main/res/drawable/ic_twotone_qr_code_scanner_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/keypasscompose/src/main/res/drawable/ic_twotone_totp.xml b/keypasscompose/src/main/res/drawable/ic_twotone_totp.xml
new file mode 100644
index 00000000..3c6d07f4
--- /dev/null
+++ b/keypasscompose/src/main/res/drawable/ic_twotone_totp.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/keypasscompose/src/main/res/drawable/ic_twotone_vpn_key_24.xml b/keypasscompose/src/main/res/drawable/ic_twotone_vpn_key_24.xml
new file mode 100644
index 00000000..9913dd79
--- /dev/null
+++ b/keypasscompose/src/main/res/drawable/ic_twotone_vpn_key_24.xml
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/keypasscompose/src/main/res/drawable/ic_undraw_empty_street_sfxm.xml b/keypasscompose/src/main/res/drawable/ic_undraw_empty_street_sfxm.xml
new file mode 100644
index 00000000..9dc244ee
--- /dev/null
+++ b/keypasscompose/src/main/res/drawable/ic_undraw_empty_street_sfxm.xml
@@ -0,0 +1,204 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/keypasscompose/src/main/res/drawable/ic_undraw_unlock_24mb.xml b/keypasscompose/src/main/res/drawable/ic_undraw_unlock_24mb.xml
new file mode 100644
index 00000000..437a7ea0
--- /dev/null
+++ b/keypasscompose/src/main/res/drawable/ic_undraw_unlock_24mb.xml
@@ -0,0 +1,194 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/keypasscompose/src/main/res/drawable/nav_divider_top.xml b/keypasscompose/src/main/res/drawable/nav_divider_top.xml
new file mode 100644
index 00000000..2ece804c
--- /dev/null
+++ b/keypasscompose/src/main/res/drawable/nav_divider_top.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/keypasscompose/src/main/res/drawable/white_circle.xml b/keypasscompose/src/main/res/drawable/white_circle.xml
new file mode 100644
index 00000000..0a981d74
--- /dev/null
+++ b/keypasscompose/src/main/res/drawable/white_circle.xml
@@ -0,0 +1,13 @@
+
+
+-
+
+
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/font/work_sans.xml b/keypasscompose/src/main/res/font/work_sans.xml
new file mode 100644
index 00000000..9b5385f8
--- /dev/null
+++ b/keypasscompose/src/main/res/font/work_sans.xml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/keypasscompose/src/main/res/font/work_sans_medium.xml b/keypasscompose/src/main/res/font/work_sans_medium.xml
new file mode 100644
index 00000000..9225d6e9
--- /dev/null
+++ b/keypasscompose/src/main/res/font/work_sans_medium.xml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/keypasscompose/src/main/res/layout/activity_add_totpactivity.xml b/keypasscompose/src/main/res/layout/activity_add_totpactivity.xml
new file mode 100644
index 00000000..9564f274
--- /dev/null
+++ b/keypasscompose/src/main/res/layout/activity_add_totpactivity.xml
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/layout/activity_authentication.xml b/keypasscompose/src/main/res/layout/activity_authentication.xml
new file mode 100644
index 00000000..15f78de2
--- /dev/null
+++ b/keypasscompose/src/main/res/layout/activity_authentication.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/layout/activity_crash.xml b/keypasscompose/src/main/res/layout/activity_crash.xml
new file mode 100644
index 00000000..4d8b0c7e
--- /dev/null
+++ b/keypasscompose/src/main/res/layout/activity_crash.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/layout/activity_dashboard.xml b/keypasscompose/src/main/res/layout/activity_dashboard.xml
new file mode 100644
index 00000000..f06dccc2
--- /dev/null
+++ b/keypasscompose/src/main/res/layout/activity_dashboard.xml
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/layout/activity_generate_password.xml b/keypasscompose/src/main/res/layout/activity_generate_password.xml
new file mode 100644
index 00000000..401dd93c
--- /dev/null
+++ b/keypasscompose/src/main/res/layout/activity_generate_password.xml
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/layout/activity_scanner.xml b/keypasscompose/src/main/res/layout/activity_scanner.xml
new file mode 100644
index 00000000..69c4b788
--- /dev/null
+++ b/keypasscompose/src/main/res/layout/activity_scanner.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/layout/backup_activity.xml b/keypasscompose/src/main/res/layout/backup_activity.xml
new file mode 100644
index 00000000..10af8e65
--- /dev/null
+++ b/keypasscompose/src/main/res/layout/backup_activity.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/layout/fragment_bottom_nav_drawer.xml b/keypasscompose/src/main/res/layout/fragment_bottom_nav_drawer.xml
new file mode 100644
index 00000000..589a9b93
--- /dev/null
+++ b/keypasscompose/src/main/res/layout/fragment_bottom_nav_drawer.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/keypasscompose/src/main/res/layout/fragment_detail.xml b/keypasscompose/src/main/res/layout/fragment_detail.xml
new file mode 100644
index 00000000..ac248174
--- /dev/null
+++ b/keypasscompose/src/main/res/layout/fragment_detail.xml
@@ -0,0 +1,217 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/keypasscompose/src/main/res/layout/fragment_home.xml b/keypasscompose/src/main/res/layout/fragment_home.xml
new file mode 100644
index 00000000..e9f9d408
--- /dev/null
+++ b/keypasscompose/src/main/res/layout/fragment_home.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/layout/item_accounts.xml b/keypasscompose/src/main/res/layout/item_accounts.xml
new file mode 100644
index 00000000..54853f11
--- /dev/null
+++ b/keypasscompose/src/main/res/layout/item_accounts.xml
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/layout/item_totp.xml b/keypasscompose/src/main/res/layout/item_totp.xml
new file mode 100644
index 00000000..284f960e
--- /dev/null
+++ b/keypasscompose/src/main/res/layout/item_totp.xml
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/layout/layout_backup_keypharse.xml b/keypasscompose/src/main/res/layout/layout_backup_keypharse.xml
new file mode 100644
index 00000000..f1371111
--- /dev/null
+++ b/keypasscompose/src/main/res/layout/layout_backup_keypharse.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/layout/layout_custom_keypharse.xml b/keypasscompose/src/main/res/layout/layout_custom_keypharse.xml
new file mode 100644
index 00000000..66387ff2
--- /dev/null
+++ b/keypasscompose/src/main/res/layout/layout_custom_keypharse.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/layout/layout_no_accounts.xml b/keypasscompose/src/main/res/layout/layout_no_accounts.xml
new file mode 100644
index 00000000..75fb794e
--- /dev/null
+++ b/keypasscompose/src/main/res/layout/layout_no_accounts.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/layout/layout_restore_keypharse.xml b/keypasscompose/src/main/res/layout/layout_restore_keypharse.xml
new file mode 100644
index 00000000..28f7adc3
--- /dev/null
+++ b/keypasscompose/src/main/res/layout/layout_restore_keypharse.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/layout/multidataset_service_list_item.xml b/keypasscompose/src/main/res/layout/multidataset_service_list_item.xml
new file mode 100644
index 00000000..7ebc5151
--- /dev/null
+++ b/keypasscompose/src/main/res/layout/multidataset_service_list_item.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/layout/nav_divider_item_layout.xml b/keypasscompose/src/main/res/layout/nav_divider_item_layout.xml
new file mode 100644
index 00000000..179f96c7
--- /dev/null
+++ b/keypasscompose/src/main/res/layout/nav_divider_item_layout.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/keypasscompose/src/main/res/layout/nav_email_folder_item_layout.xml b/keypasscompose/src/main/res/layout/nav_email_folder_item_layout.xml
new file mode 100644
index 00000000..5de56d37
--- /dev/null
+++ b/keypasscompose/src/main/res/layout/nav_email_folder_item_layout.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/keypasscompose/src/main/res/layout/nav_menu_item_layout.xml b/keypasscompose/src/main/res/layout/nav_menu_item_layout.xml
new file mode 100644
index 00000000..332daa58
--- /dev/null
+++ b/keypasscompose/src/main/res/layout/nav_menu_item_layout.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/keypasscompose/src/main/res/menu/bottom_app_bar.xml b/keypasscompose/src/main/res/menu/bottom_app_bar.xml
new file mode 100644
index 00000000..5ae8e2f3
--- /dev/null
+++ b/keypasscompose/src/main/res/menu/bottom_app_bar.xml
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/menu/bottom_app_bar_detail.xml b/keypasscompose/src/main/res/menu/bottom_app_bar_detail.xml
new file mode 100644
index 00000000..d49421bf
--- /dev/null
+++ b/keypasscompose/src/main/res/menu/bottom_app_bar_detail.xml
@@ -0,0 +1,8 @@
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/menu/bottom_app_bar_settings_menu.xml b/keypasscompose/src/main/res/menu/bottom_app_bar_settings_menu.xml
new file mode 100644
index 00000000..3d4e4f9e
--- /dev/null
+++ b/keypasscompose/src/main/res/menu/bottom_app_bar_settings_menu.xml
@@ -0,0 +1,8 @@
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/menu/menu_delete.xml b/keypasscompose/src/main/res/menu/menu_delete.xml
new file mode 100644
index 00000000..87db3e2c
--- /dev/null
+++ b/keypasscompose/src/main/res/menu/menu_delete.xml
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/keypasscompose/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
deleted file mode 100644
index eca70cfe..00000000
--- a/keypasscompose/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/keypasscompose/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
deleted file mode 100644
index eca70cfe..00000000
--- a/keypasscompose/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/mipmap-hdpi/ic_launcher.webp b/keypasscompose/src/main/res/mipmap-hdpi/ic_launcher.webp
deleted file mode 100644
index c209e78e..00000000
Binary files a/keypasscompose/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ
diff --git a/keypasscompose/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/keypasscompose/src/main/res/mipmap-hdpi/ic_launcher_round.webp
deleted file mode 100644
index b2dfe3d1..00000000
Binary files a/keypasscompose/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/keypasscompose/src/main/res/mipmap-mdpi/ic_launcher.webp b/keypasscompose/src/main/res/mipmap-mdpi/ic_launcher.webp
deleted file mode 100644
index 4f0f1d64..00000000
Binary files a/keypasscompose/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ
diff --git a/keypasscompose/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/keypasscompose/src/main/res/mipmap-mdpi/ic_launcher_round.webp
deleted file mode 100644
index 62b611da..00000000
Binary files a/keypasscompose/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/keypasscompose/src/main/res/mipmap-xhdpi/ic_launcher.webp b/keypasscompose/src/main/res/mipmap-xhdpi/ic_launcher.webp
deleted file mode 100644
index 948a3070..00000000
Binary files a/keypasscompose/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ
diff --git a/keypasscompose/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/keypasscompose/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
deleted file mode 100644
index 1b9a6956..00000000
Binary files a/keypasscompose/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/keypasscompose/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/keypasscompose/src/main/res/mipmap-xxhdpi/ic_launcher.webp
deleted file mode 100644
index 28d4b77f..00000000
Binary files a/keypasscompose/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ
diff --git a/keypasscompose/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/keypasscompose/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 9287f508..00000000
Binary files a/keypasscompose/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/keypasscompose/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/keypasscompose/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
deleted file mode 100644
index aa7d6427..00000000
Binary files a/keypasscompose/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ
diff --git a/keypasscompose/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/keypasscompose/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 9126ae37..00000000
Binary files a/keypasscompose/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/keypasscompose/src/main/res/navigation/navigation_graph.xml b/keypasscompose/src/main/res/navigation/navigation_graph.xml
new file mode 100644
index 00000000..d5be9761
--- /dev/null
+++ b/keypasscompose/src/main/res/navigation/navigation_graph.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/values-hi/strings.xml b/keypasscompose/src/main/res/values-hi/strings.xml
new file mode 100644
index 00000000..05d800e5
--- /dev/null
+++ b/keypasscompose/src/main/res/values-hi/strings.xml
@@ -0,0 +1,86 @@
+
+
+
+ पासवर्ड उत्पन्न करें
+ होम
+ हटाएँ
+
+
+ पासवर्ड उत्पन्न करें
+
+ बैकअप रीस्टोर किया गया
+ अमान्य कीफ्रेज़
+
+ बैकअप पूर्ण।
+
+
+ क्रेडेंशियल्स बैकअप
+ बाहरी भंडारण के लिए बैकअप क्रेडेंशियल्स
+ क्रेडेंशियल्स को पुनर्स्थापित करें
+ अपना बैकअप रिस्टोर करें
+ प्रतिक्रिया भेजें
+ तकनीकी मुद्दों की रिपोर्ट करें या नई सुविधाओं का सुझाव दें
+ साझा करें
+ दूसरों के साथ ऐप शेयर करें
+ पासवर्ड की लंबाई
+ पासवर्ड
+ अपरकेस अक्षर
+ लोअरकेस अक्षर
+ प्रतीक
+ संख्याएं
+ क्लिपबोर्ड पर कॉपी किया गया
+ KeyPass में प्रवेश करने के लिए लॉगिन करें
+ प्रमाणीकरण विफल होना
+
+ पिछला बैकअप: %s
+ जल्द आ रहा है
+ क्या आप वाकई ऐसा करना चाहते हैं?
+ क्या आप वास्तव में इस प्रविष्टि को हटाना चाहते हैं, इसे बहाल नहीं किया जा सकता
+ रद्द करें
+ KeyPass को फ़ीडबैक
+ शेयर KeyPass
+ कोई खाता नहीं जोड़ा गया, कृपया नीचे बटन से जोड़ें
+ बैकअप बंद करें
+ बैकअप पासफ़्रेज़ सत्यापित करें
+ अपने बैकअप पासफ्रेज़ का परीक्षण करें और सत्यापित करें कि यह मेल खाता है
+ बैकअप फोल्डर
+ बैकअप
+ बैकअप चालू करें
+ बैकअप पासफ़्रेज़ के साथ एन्क्रिप्ट किए जाते हैं और आपके डिवाइस पर संग्रहीत होते हैं
+ हाँ
+ बैकअप बनायें
+ चेतावनी
+ इस कुंजी वाक्यांश को कॉपी करें इसका उपयोग इस बैकअप को पुनर्प्राप्त करने के लिए किया जाएगा, यह KeyPass अगेन द्वारा प्रदान नहीं किया जाएगा, क्या आपने कॉपी किया है या लिखा है?
+ मदद
+ सुरक्षा
+ खोजें
+ खाता नाम
+ प्रयोक्ता नाम/ईमेल/फोन
+ टैग अल्पविराम से अलग (वैकल्पिक)
+ वेबसाइट यूआरएल (वैकल्पिक)
+ नोट्स (वैकल्पिक)
+
+ ऑटो बैकअप
+ अक्षम
+ सक्रिय
+ ऑटो बैकअप फ़ाइल को ओवरराइड करें
+ स्कैनर
+ कृपया गुप्त कुंजी दर्ज करें
+ कृपया खाता नाम दर्ज करें
+ TOTP . जोड़ें
+ पासवर्ड स्कैन करें
+
+ सहेजें
+
+ कस्टम कीफ़्रेज़
+ कीफ़्रेज़ जनरेट करें
+ कृपया कीफ़्रेज़ दर्ज करें
+ कृपया मान्य कीफ़्रेज़ दर्ज करें
+ कीफ़्रेज़ सेट करें
+ कृपया बैकअप फ़ाइल के लिए कीफ़्रेज़ बनाएं, इसका उपयोग बैकअप फ़ाइल को पुनर्स्थापित करने के लिए किया जाएगा। कीफ़्रेज़ 16 वर्णों का होना चाहिए
+ कीफ़्रेज़ दर्ज करें
+ पुनर्स्थापित करना
+ कृपया वह कीफ़्रेज़ दर्ज करें जो आपके द्वारा बैक अप लेने पर प्राप्त होता है
+ क्या आप बैकअप के लिए अपना खुद का कीफ्रेज बनाना चाहते हैं या मुझे आपके लिए जनरेट करना चाहिए?
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/values-night/themes.xml b/keypasscompose/src/main/res/values-night/themes.xml
index 36861cd7..d54286e8 100644
--- a/keypasscompose/src/main/res/values-night/themes.xml
+++ b/keypasscompose/src/main/res/values-night/themes.xml
@@ -1,16 +1,33 @@
-
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/values-pt-rBR/strings.xml b/keypasscompose/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 00000000..d41c1155
--- /dev/null
+++ b/keypasscompose/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,84 @@
+
+
+
+ Gerar senha
+ Início
+ Deletar
+
+
+ Gerar senha
+
+ Backup restaurado
+ Senha incorreta
+
+ Backup concluído
+
+
+ Backup de credenciais
+ Salva suas senhas em um armazenamento externo
+ Restaurar credenciais
+ Restaura seu backup
+ Enviar feedback
+ Reportar um problema técnico ou sugerir novas funções
+ Compartilhar
+ Divulgar app com outros
+ Comprimento da senha
+ Senha
+ LETRAS MAIÚSCULAS
+ letras minúsculas
+ Símbolos
+ Números
+ Copiado para à área de transferência
+ Autenticação para entrar no KeyPass
+ Falha na autenticação
+
+ Último backup: %s
+ Em breve
+ Tem certeza?
+ Você realmente deseja apagar esses dados? Não será possível restaurá-los.
+ Cancelar
+ Feedback para o KeyPass
+ Compartilhar o KeyPass
+ Nenhuma conta adicionada. Por favor, adicione uma através do botão abaixo
+ Desativar backups
+ Verificar senha do backup
+ Teste a senha do seu backup e verifique se ela está correta
+ Pasta de backup
+ Backup
+ Ativar backups
+ Os backups são criptografados com uma senha e armazenados no seu dispositivo
+ Sim
+ Criar backup
+ Alerta
+ Copie essa senha. Ela será usada para restaurar esse backup. O KeyPass não irá exibi-la novamente. Você copiou ou guardou sua senha?
+ Ajuda
+ Segurança
+ Pesquisar
+ Nome da conta
+ Usuário/E-mail/Telefone
+ Tags separadas por vírgula (opcional)
+ URL do site (opcional)
+ Anotações (opcional)
+ Backup automático
+ Desativado
+ Ativado
+ Sobrescrever o arquivo de backup automático
+ Scanner
+ Por favor insira a chave secreta
+ Por favor insira o nome da conta
+ Adicionar TOTP
+ Escanear senha
+ Salvar
+
+ Frase-chave personalizada
+ Gerar frase-chave
+ Digite a frase-chave
+ Insira uma frase-chave válida
+ Definir frase-chave
+ Crie uma frase-chave para seu arquivo de backup, isso será usado para restaurar o arquivo de backup. a frase-chave deve ter 16 caracteres
+ Digite a frase-chave
+ Restaurar
+ Insira a frase-chave que você obteve ao fazer backup
+ Deseja criar sua própria frase-chave para backups ou devo gerar para você?
+
+
diff --git a/keypasscompose/src/main/res/values-zh-rCN/strings.xml b/keypasscompose/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 00000000..e12778cc
--- /dev/null
+++ b/keypasscompose/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,85 @@
+
+
+
+ 生成密码
+ 主页
+ 删除
+
+
+ 生成密码
+
+ 备份密码
+ 无效密码
+
+ 备份完成
+
+
+ 备份
+ 把密码备份到外部存储
+ 恢复备份
+ 从备份中恢复
+ 发送反馈
+ 报告技术问题或建议新功能
+ 分享
+ 向别人分享该应用
+ 密码长度
+ 密码
+ 大写字母
+ 小写字母
+ 标点符号
+ 数字
+ 已复制到剪切板
+ 输入密码以进入 KeyPass
+ 身份认证失败
+
+ 上次备份: %s
+ 敬请期待
+ 您确定吗?
+ 您确定要删除本条目吗?此操作不可撤销。
+ Cancel
+ 向 KeyPass 发送反馈
+ 分享 KeyPass
+ 这里空空如也。按下下方的加号以新建。
+ 关闭备份
+ 验证备份密码
+ 测试备份密码并验证其匹配
+ 备份文件夹
+ 备份
+ 开启备份
+ 备份使用密码加密并存储在您的设备上
+ 确定
+ 创建备份
+ 警告
+ 请立即复制或在纸上写下下面的密码。KeyPass 将不会再次显示该密码。
+ 帮助
+ 安全
+ 搜索
+ 账户名称
+ 用户名/邮箱/手机号
+ 标签(选填)
+ 网站(选填)
+ 附注(选填)
+
+ 自动备份
+ 已禁用
+ 启用
+ 覆盖自动备份文件
+ 添加 TOTP
+ 请输入账户名
+ 请输入密钥
+ 扫描器
+ 扫描密码
+ 保存
+
+ 自定义关键词
+ 生成关键词
+ 请输入关键词
+ 请输入有效的关键词
+ 设置关键词
+ 请为您的备份文件创建关键字,这将用于恢复备份文件。 关键词应为 16 个字符
+ 输入关键词
+ 恢复
+ 请输入您备份时获得的关键词
+ 您想创建自己的备份关键字还是我应该为您生成?
+
+
diff --git a/keypasscompose/src/main/res/values/arrays.xml b/keypasscompose/src/main/res/values/arrays.xml
new file mode 100644
index 00000000..a082d2e0
--- /dev/null
+++ b/keypasscompose/src/main/res/values/arrays.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/values/attrs.xml b/keypasscompose/src/main/res/values/attrs.xml
new file mode 100644
index 00000000..e52c8315
--- /dev/null
+++ b/keypasscompose/src/main/res/values/attrs.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/values/colors.xml b/keypasscompose/src/main/res/values/colors.xml
index 0d2c4cc4..df9cc0ba 100644
--- a/keypasscompose/src/main/res/values/colors.xml
+++ b/keypasscompose/src/main/res/values/colors.xml
@@ -1,4 +1,41 @@
+ #ffffff
+
+ #121212
+ #000000
+
+ #eef0f2
+ #d2dbe0
+ #adbbc4
+ #8ca2ae
+ #4a6572
+ #344955
+ #232f34
+ #6c63ff
+
+ #fbd790
+ #f9be64
+ #f9aa33
+
+ #cf7779
+ #ff4c5d
+
+ #99ffffff
+
+ #99eef0f2
+
+ #33000000
+ #de000000
+ #99000000
+
+
+
+
+
+
+ @color/keypass_black_900_alpha_020
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/values/dimens.xml b/keypasscompose/src/main/res/values/dimens.xml
new file mode 100644
index 00000000..1b1859e6
--- /dev/null
+++ b/keypasscompose/src/main/res/values/dimens.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+ 56dp
+ 32dp
+ 8dp
+
+
+ 56dp
+
+
+
+ 32dp
+
diff --git a/keypasscompose/src/main/res/values/elevation.xml b/keypasscompose/src/main/res/values/elevation.xml
new file mode 100644
index 00000000..8dc7e990
--- /dev/null
+++ b/keypasscompose/src/main/res/values/elevation.xml
@@ -0,0 +1,19 @@
+
+
+
+
+ 0dp
+ 16dp
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/values/font_certs.xml b/keypasscompose/src/main/res/values/font_certs.xml
new file mode 100644
index 00000000..d2226ac0
--- /dev/null
+++ b/keypasscompose/src/main/res/values/font_certs.xml
@@ -0,0 +1,17 @@
+
+
+
+ - @array/com_google_android_gms_fonts_certs_dev
+ - @array/com_google_android_gms_fonts_certs_prod
+
+
+ -
+ MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAeFw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVyxW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8XW8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexAcKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkwHQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0cxb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrPzgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXclaXjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05aIskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+aayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUWEv9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
+
+
+
+ -
+ MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK
+
+
+
diff --git a/keypasscompose/src/main/res/values/ids.xml b/keypasscompose/src/main/res/values/ids.xml
new file mode 100644
index 00000000..98052dae
--- /dev/null
+++ b/keypasscompose/src/main/res/values/ids.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/values/layout.xml b/keypasscompose/src/main/res/values/layout.xml
new file mode 100644
index 00000000..8eee8a55
--- /dev/null
+++ b/keypasscompose/src/main/res/values/layout.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+ 2dp
+ 4dp
+
+
+ 8dp
+ 16dp
+ 24dp
+ 32dp
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/values/motion.xml b/keypasscompose/src/main/res/values/motion.xml
new file mode 100644
index 00000000..29d608a1
--- /dev/null
+++ b/keypasscompose/src/main/res/values/motion.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+ 300
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/values/preloaded_fonts.xml b/keypasscompose/src/main/res/values/preloaded_fonts.xml
new file mode 100644
index 00000000..374f820a
--- /dev/null
+++ b/keypasscompose/src/main/res/values/preloaded_fonts.xml
@@ -0,0 +1,7 @@
+
+
+
+ - @font/work_sans
+ - @font/work_sans_medium
+
+
diff --git a/keypasscompose/src/main/res/values/shapes.xml b/keypasscompose/src/main/res/values/shapes.xml
new file mode 100644
index 00000000..5c5503f8
--- /dev/null
+++ b/keypasscompose/src/main/res/values/shapes.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 4dp
+ 0dp
+ 12dp
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/values/strings.xml b/keypasscompose/src/main/res/values/strings.xml
index ade25ba3..48a7870f 100644
--- a/keypasscompose/src/main/res/values/strings.xml
+++ b/keypasscompose/src/main/res/values/strings.xml
@@ -1,3 +1,83 @@
- KeyPass Compose
-
\ No newline at end of file
+
+
+ Generate Password
+ Home
+ Delete
+
+
+ Generate Password
+
+ Backup Restored
+ Invalid Keyphrase
+
+ Backup Completed
+
+
+ Credentials Backups
+ Backup Credentials to external storage
+ Restore Credentials
+ Restore your backup
+ Send feedback
+ Report technical issues or suggest new features
+ Share
+ Share app with others
+ Password length
+ Password
+ UPPERCASE ALPHABETS
+ lowercase alphabets
+ Symbols
+ Numbers
+ Copied to clipboard
+ Login to enter KeyPass
+ Authentication failed
+
+ Last backup : %s
+ Coming Soon
+ Are you sure?
+ Do you really want to delete this entry, it can\'t be restored
+ Cancel
+ Feedback to KeyPass
+ Share KeyPass
+ No Accounts added, please add from below button
+ Turn Off Backups
+ Verify backup passphrase
+ Test your backup passphrase and verify that it matches
+ Backup Folder
+ Backup
+ Turn On Backups
+ Backups are encrypted with a passphrase and stored on your device
+ Yes
+ Create Backup
+ Alert
+ Copy This Key Phrase this will be used to recover this backup, this will not be provided by KeyPass Again, have you copied or written down?
+ Help
+ Security
+ Search
+ Account Name
+ Username/Email/Phone
+ Tags comma separated (optional)
+ Website url (optional)
+ Notes (optional)
+ Auto Backup
+ Disabled
+ Enabled
+ Override Auto Backup File
+ Scanner
+ Please enter secret key
+ Please enter account name
+ Add TOTP
+ Scan Password
+ Save
+ Custom Keyphrase
+ Generate Keyphrase
+ Please enter keyphrase
+ Please enter valid keyphrase
+ Set Keyphrase
+ Please create keyphrase for you backup file, this will be used to restore the backup file. keyphrase should be of 16 characters
+ Enter Keyphrase
+ Restore
+ Please enter keyphrase you get when you backed up
+ Do you want to create your own keyphrase for backups or should I generate for you?
+
+
diff --git a/keypasscompose/src/main/res/values/strings_no_translate.xml b/keypasscompose/src/main/res/values/strings_no_translate.xml
new file mode 100644
index 00000000..74cfd201
--- /dev/null
+++ b/keypasscompose/src/main/res/values/strings_no_translate.xml
@@ -0,0 +1,16 @@
+
+
+ backup
+ settings_verify_key_phrase
+ restore_backup
+ feedback
+ share
+ start_backup
+ create_backup
+ backup_folder
+ stop_backup
+ auto_backup
+ cat_auto_backup
+ override_auto_backup
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/values/style_text.xml b/keypasscompose/src/main/res/values/style_text.xml
new file mode 100644
index 00000000..1f3f353d
--- /dev/null
+++ b/keypasscompose/src/main/res/values/style_text.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/values/styles.xml b/keypasscompose/src/main/res/values/styles.xml
new file mode 100644
index 00000000..42f3b05e
--- /dev/null
+++ b/keypasscompose/src/main/res/values/styles.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/values/themes.xml b/keypasscompose/src/main/res/values/themes.xml
index d2c24be0..e90ed4cd 100644
--- a/keypasscompose/src/main/res/values/themes.xml
+++ b/keypasscompose/src/main/res/values/themes.xml
@@ -1,25 +1,67 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/values/type.xml b/keypasscompose/src/main/res/values/type.xml
new file mode 100644
index 00000000..ef84cacd
--- /dev/null
+++ b/keypasscompose/src/main/res/values/type.xml
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/xml/backup_preferences.xml b/keypasscompose/src/main/res/xml/backup_preferences.xml
new file mode 100644
index 00000000..b2ce7ac7
--- /dev/null
+++ b/keypasscompose/src/main/res/xml/backup_preferences.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/xml/preferences.xml b/keypasscompose/src/main/res/xml/preferences.xml
new file mode 100644
index 00000000..6616eab7
--- /dev/null
+++ b/keypasscompose/src/main/res/xml/preferences.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/main/res/xml/shortcuts.xml b/keypasscompose/src/main/res/xml/shortcuts.xml
new file mode 100644
index 00000000..6d864060
--- /dev/null
+++ b/keypasscompose/src/main/res/xml/shortcuts.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/production/res/mipmap-anydpi-v26/ic_launcher.xml b/keypasscompose/src/production/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000..036d09bc
--- /dev/null
+++ b/keypasscompose/src/production/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/production/res/mipmap-anydpi-v26/ic_launcher_round.xml b/keypasscompose/src/production/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000..036d09bc
--- /dev/null
+++ b/keypasscompose/src/production/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/production/res/mipmap-hdpi/ic_launcher.png b/keypasscompose/src/production/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..82b55b70
Binary files /dev/null and b/keypasscompose/src/production/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/keypasscompose/src/production/res/mipmap-hdpi/ic_launcher_foreground.png b/keypasscompose/src/production/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..3f179567
Binary files /dev/null and b/keypasscompose/src/production/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/keypasscompose/src/production/res/mipmap-hdpi/ic_launcher_round.png b/keypasscompose/src/production/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 00000000..33c4a569
Binary files /dev/null and b/keypasscompose/src/production/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/keypasscompose/src/production/res/mipmap-mdpi/ic_launcher.png b/keypasscompose/src/production/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..6b1187d9
Binary files /dev/null and b/keypasscompose/src/production/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/keypasscompose/src/production/res/mipmap-mdpi/ic_launcher_foreground.png b/keypasscompose/src/production/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..206a0f65
Binary files /dev/null and b/keypasscompose/src/production/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/keypasscompose/src/production/res/mipmap-mdpi/ic_launcher_round.png b/keypasscompose/src/production/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 00000000..8b8c3c67
Binary files /dev/null and b/keypasscompose/src/production/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/keypasscompose/src/production/res/mipmap-xhdpi/ic_launcher.png b/keypasscompose/src/production/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..290a993d
Binary files /dev/null and b/keypasscompose/src/production/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/keypasscompose/src/production/res/mipmap-xhdpi/ic_launcher_foreground.png b/keypasscompose/src/production/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..acb214c5
Binary files /dev/null and b/keypasscompose/src/production/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/keypasscompose/src/production/res/mipmap-xhdpi/ic_launcher_round.png b/keypasscompose/src/production/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..a33b22af
Binary files /dev/null and b/keypasscompose/src/production/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/keypasscompose/src/production/res/mipmap-xxhdpi/ic_launcher.png b/keypasscompose/src/production/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..ef9a73a6
Binary files /dev/null and b/keypasscompose/src/production/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/keypasscompose/src/production/res/mipmap-xxhdpi/ic_launcher_foreground.png b/keypasscompose/src/production/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..eab0e7c0
Binary files /dev/null and b/keypasscompose/src/production/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/keypasscompose/src/production/res/mipmap-xxhdpi/ic_launcher_round.png b/keypasscompose/src/production/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..685babd4
Binary files /dev/null and b/keypasscompose/src/production/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/keypasscompose/src/production/res/mipmap-xxxhdpi/ic_launcher.png b/keypasscompose/src/production/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..65571ca6
Binary files /dev/null and b/keypasscompose/src/production/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/keypasscompose/src/production/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/keypasscompose/src/production/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..2956b453
Binary files /dev/null and b/keypasscompose/src/production/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/keypasscompose/src/production/res/mipmap-xxxhdpi/ic_launcher_round.png b/keypasscompose/src/production/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..53595caa
Binary files /dev/null and b/keypasscompose/src/production/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/keypasscompose/src/production/res/values/ic_launcher_background.xml b/keypasscompose/src/production/res/values/ic_launcher_background.xml
new file mode 100644
index 00000000..c5d5899f
--- /dev/null
+++ b/keypasscompose/src/production/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #FFFFFF
+
\ No newline at end of file
diff --git a/keypasscompose/src/production/res/values/strings.xml b/keypasscompose/src/production/res/values/strings.xml
new file mode 100644
index 00000000..cb22809a
--- /dev/null
+++ b/keypasscompose/src/production/res/values/strings.xml
@@ -0,0 +1,5 @@
+
+
+ KeyPass
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/staging/res/mipmap-anydpi-v26/ic_launcher.xml b/keypasscompose/src/staging/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000..036d09bc
--- /dev/null
+++ b/keypasscompose/src/staging/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/staging/res/mipmap-anydpi-v26/ic_launcher_round.xml b/keypasscompose/src/staging/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000..036d09bc
--- /dev/null
+++ b/keypasscompose/src/staging/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/keypasscompose/src/staging/res/mipmap-hdpi/ic_launcher.png b/keypasscompose/src/staging/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..28da6289
Binary files /dev/null and b/keypasscompose/src/staging/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/keypasscompose/src/staging/res/mipmap-hdpi/ic_launcher_foreground.png b/keypasscompose/src/staging/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..3f179567
Binary files /dev/null and b/keypasscompose/src/staging/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/keypasscompose/src/staging/res/mipmap-hdpi/ic_launcher_round.png b/keypasscompose/src/staging/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 00000000..a22e8788
Binary files /dev/null and b/keypasscompose/src/staging/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/keypasscompose/src/staging/res/mipmap-mdpi/ic_launcher.png b/keypasscompose/src/staging/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..aabc5bd4
Binary files /dev/null and b/keypasscompose/src/staging/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/keypasscompose/src/staging/res/mipmap-mdpi/ic_launcher_foreground.png b/keypasscompose/src/staging/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..206a0f65
Binary files /dev/null and b/keypasscompose/src/staging/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/keypasscompose/src/staging/res/mipmap-mdpi/ic_launcher_round.png b/keypasscompose/src/staging/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 00000000..1679c521
Binary files /dev/null and b/keypasscompose/src/staging/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/keypasscompose/src/staging/res/mipmap-xhdpi/ic_launcher.png b/keypasscompose/src/staging/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..4ec733a0
Binary files /dev/null and b/keypasscompose/src/staging/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/keypasscompose/src/staging/res/mipmap-xhdpi/ic_launcher_foreground.png b/keypasscompose/src/staging/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..acb214c5
Binary files /dev/null and b/keypasscompose/src/staging/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/keypasscompose/src/staging/res/mipmap-xhdpi/ic_launcher_round.png b/keypasscompose/src/staging/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..e4ff35ca
Binary files /dev/null and b/keypasscompose/src/staging/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/keypasscompose/src/staging/res/mipmap-xxhdpi/ic_launcher.png b/keypasscompose/src/staging/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..9fbefd50
Binary files /dev/null and b/keypasscompose/src/staging/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/keypasscompose/src/staging/res/mipmap-xxhdpi/ic_launcher_foreground.png b/keypasscompose/src/staging/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..eab0e7c0
Binary files /dev/null and b/keypasscompose/src/staging/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/keypasscompose/src/staging/res/mipmap-xxhdpi/ic_launcher_round.png b/keypasscompose/src/staging/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..387f22a8
Binary files /dev/null and b/keypasscompose/src/staging/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/keypasscompose/src/staging/res/mipmap-xxxhdpi/ic_launcher.png b/keypasscompose/src/staging/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..3bcfc311
Binary files /dev/null and b/keypasscompose/src/staging/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/keypasscompose/src/staging/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/keypasscompose/src/staging/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..2956b453
Binary files /dev/null and b/keypasscompose/src/staging/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/keypasscompose/src/staging/res/mipmap-xxxhdpi/ic_launcher_round.png b/keypasscompose/src/staging/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..48081cb4
Binary files /dev/null and b/keypasscompose/src/staging/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/keypasscompose/src/staging/res/values/ic_launcher_background.xml b/keypasscompose/src/staging/res/values/ic_launcher_background.xml
new file mode 100644
index 00000000..beab31f7
--- /dev/null
+++ b/keypasscompose/src/staging/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #000000
+
\ No newline at end of file
diff --git a/keypasscompose/src/staging/res/values/strings.xml b/keypasscompose/src/staging/res/values/strings.xml
new file mode 100644
index 00000000..f124ba3d
--- /dev/null
+++ b/keypasscompose/src/staging/res/values/strings.xml
@@ -0,0 +1,5 @@
+
+
+ Staging KeyPass
+
+
\ No newline at end of file