From 0e65479a858debe17e12af663c1687d49d8e7ac3 Mon Sep 17 00:00:00 2001 From: Yogesh Choudhary Paliyal Date: Sun, 7 May 2023 11:54:49 +0530 Subject: [PATCH] Backup Screen Migration to Compose (#512) --- app/build.gradle | 4 +- .../yogeshpaliyal/keypass/CustomTestRunner.kt | 28 +- .../keypass/ExampleInstrumentedTest.kt | 44 +- .../keypass/ui/nav/DashboardActivityTest.kt | 216 +++--- .../yogeshpaliyal/keypass/MyApplication.kt | 40 +- .../keypass/customViews/MaskedCardView.kt | 47 -- .../keypass/listener/AccountsClickListener.kt | 14 - .../listener/UniversalClickListener.kt | 13 - .../yogeshpaliyal/keypass/ui/CrashActivity.kt | 130 ++-- .../keypass/ui/addTOTP/AddTOTPActivity.kt | 280 +++---- .../keypass/ui/addTOTP/AddTOTPViewModel.kt | 170 ++--- .../keypass/ui/addTOTP/ScannerActivity.kt | 110 +-- .../keypass/ui/auth/AuthScreen.kt | 180 ++--- .../keypass/ui/auth/components/ButtonBar.kt | 154 ++-- .../ui/auth/components/PasswordInputField.kt | 124 +-- .../keypass/ui/backup/BackupActivity.kt | 581 ++++++-------- .../ui/backup/KeyPassBackupDirectoryPick.kt | 19 + .../ui/backup/components/BackupDialogs.kt | 47 ++ .../components/CreateCustomKeyphrase.kt | 75 ++ .../backup/components/SelectKeyphraseType.kt | 32 + .../backup/components/ShowKeyPhraseDialog.kt | 75 ++ .../keypass/ui/detail/DetailActivity.kt | 712 +++++++++--------- .../keypass/ui/detail/DetailViewModel.kt | 131 ++-- .../keypass/ui/detail/QRScanner.kt | 52 +- .../ui/generate/GeneratePasswordActivity.kt | 46 +- .../ui/generate/GeneratePasswordViewModel.kt | 126 ++-- .../ui/generate/GeneratePasswordViewState.kt | 42 +- .../ui/generate/ui/GeneratePasswordContent.kt | 452 +++++------ .../ui/generate/ui/GeneratePasswordScreen.kt | 84 +-- .../ui/components/CheckboxWithLabel.kt | 64 +- .../generate/ui/utils/CopyTextToClipboard.kt | 34 +- .../keypass/ui/home/DashboardViewModel.kt | 110 +-- .../yogeshpaliyal/keypass/ui/home/Homepage.kt | 192 ++--- .../keypass/ui/home/SortingOrders.kt | 40 +- .../ui/home/components/AccountsList.kt | 484 ++++++------ .../keypass/ui/home/components/SearchBar.kt | 138 ++-- .../keypass/ui/home/components/SortingMenu.kt | 90 +-- .../keypass/ui/nav/BottomNavViewModel.kt | 144 ++-- .../ui/nav/DashboardComposeActivity.kt | 617 +++++++-------- .../keypass/ui/nav/NavigationModel.kt | 74 +- .../keypass/ui/nav/NavigationModelItem.kt | 48 +- .../yogeshpaliyal/keypass/ui/redux/Action.kt | 33 - .../keypass/ui/redux/BottomSheetRoutes.kt | 5 + .../keypass/ui/redux/KeyPassRedux.kt | 227 +++--- .../yogeshpaliyal/keypass/ui/redux/Routes.kt | 13 - .../yogeshpaliyal/keypass/ui/redux/State.kt | 37 - .../keypass/ui/redux/actions/Action.kt | 11 + .../ui/redux/actions/BottomSheetAction.kt | 13 + .../ui/redux/actions/IntentNavigation.kt | 8 + .../keypass/ui/redux/actions/UtilityAction.kt | 8 + .../middlewares/IntentNavigationMiddleware.kt | 46 ++ .../ui/redux/states/AccountDetailState.kt | 3 + .../keypass/ui/redux/states/AuthState.kt | 10 + .../ui/redux/states/BackupScreenState.kt | 29 + .../ui/redux/states/BottomSheetState.kt | 9 + .../keypass/ui/redux/states/HomeState.kt | 8 + .../keypass/ui/redux/states/KeyPassState.kt | 17 + .../keypass/ui/redux/states/ScreenState.kt | 3 + .../keypass/ui/redux/states/SettingsState.kt | 3 + .../ui/redux/states/TotpDetailState.kt | 3 + .../keypass/ui/settings/MySettingsFragment.kt | 493 ++++++------ .../ui/settings/MySettingsViewModel.kt | 26 +- .../keypass/ui/settings/OpenKeyPassBackup.kt | 40 +- .../keypass/ui/style/KeyPassTheme.kt | 22 +- .../yogeshpaliyal/keypass/utils/LogHelper.kt | 96 +-- .../keypass/utils/StringDiffUtil.kt | 24 +- .../layout/layout_my_settings_fragment.xml | 8 - app/src/main/res/values/strings.xml | 1 + .../common/ExampleInstrumentedTest.kt | 18 - .../com/yogeshpaliyal/common/AppDatabase.kt | 58 +- .../common/CommonMyApplication.kt | 76 +- .../common/constants/AccountType.kt | 22 +- .../common/constants/IntentKeys.kt | 10 +- .../common/constants/RequestCodes.kt | 10 +- .../yogeshpaliyal/common/data/AccountModel.kt | 136 ++-- .../yogeshpaliyal/common/data/BackupData.kt | 42 +- .../java/com/yogeshpaliyal/common/db/DbDao.kt | 190 ++--- .../common/dbhelper/CryptoException.kt | 26 +- .../common/dbhelper/DbBackupRestore.kt | 146 ++-- .../common/dbhelper/EncryptionHelper.kt | 236 +++--- .../common/di/module/AppModule.kt | 100 +-- .../yogeshpaliyal/common/utils/BackupUtils.kt | 180 ++--- .../common/utils/ContextExtensions.kt | 140 ++-- .../com/yogeshpaliyal/common/utils/Event.kt | 54 +- .../common/utils/FormatCalendar.kt | 38 +- .../common/utils/GetAutoFillService.kt | 36 +- .../common/utils/GetRandomString.kt | 24 +- .../common/utils/IntentHelper.kt | 150 ++-- .../yogeshpaliyal/common/utils/Normalize.kt | 88 +-- .../common/utils/PasswordGenerator.kt | 92 +-- .../common/utils/SharedPreferenceUtils.kt | 236 +++--- .../common/worker/AutoBackupWorker.kt | 78 +- .../common/worker/ExecuteAutoBackup.kt | 36 +- 93 files changed, 4815 insertions(+), 4666 deletions(-) delete mode 100644 app/src/main/java/com/yogeshpaliyal/keypass/customViews/MaskedCardView.kt delete mode 100644 app/src/main/java/com/yogeshpaliyal/keypass/listener/AccountsClickListener.kt delete mode 100644 app/src/main/java/com/yogeshpaliyal/keypass/listener/UniversalClickListener.kt create mode 100644 app/src/main/java/com/yogeshpaliyal/keypass/ui/backup/KeyPassBackupDirectoryPick.kt create mode 100644 app/src/main/java/com/yogeshpaliyal/keypass/ui/backup/components/BackupDialogs.kt create mode 100644 app/src/main/java/com/yogeshpaliyal/keypass/ui/backup/components/CreateCustomKeyphrase.kt create mode 100644 app/src/main/java/com/yogeshpaliyal/keypass/ui/backup/components/SelectKeyphraseType.kt create mode 100644 app/src/main/java/com/yogeshpaliyal/keypass/ui/backup/components/ShowKeyPhraseDialog.kt delete mode 100644 app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/Action.kt create mode 100644 app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/BottomSheetRoutes.kt delete mode 100644 app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/Routes.kt delete mode 100644 app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/State.kt create mode 100644 app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/actions/Action.kt create mode 100644 app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/actions/BottomSheetAction.kt create mode 100644 app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/actions/IntentNavigation.kt create mode 100644 app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/actions/UtilityAction.kt create mode 100644 app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/middlewares/IntentNavigationMiddleware.kt create mode 100644 app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/AccountDetailState.kt create mode 100644 app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/AuthState.kt create mode 100644 app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/BackupScreenState.kt create mode 100644 app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/BottomSheetState.kt create mode 100644 app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/HomeState.kt create mode 100644 app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/KeyPassState.kt create mode 100644 app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/ScreenState.kt create mode 100644 app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/SettingsState.kt create mode 100644 app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/TotpDetailState.kt delete mode 100644 app/src/main/res/layout/layout_my_settings_fragment.xml delete mode 100644 common/src/androidTest/java/com/yogeshpaliyal/common/ExampleInstrumentedTest.kt diff --git a/app/build.gradle b/app/build.gradle index 1e8619f4..73d405b1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -41,8 +41,8 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '17' diff --git a/app/src/androidTest/java/com/yogeshpaliyal/keypass/CustomTestRunner.kt b/app/src/androidTest/java/com/yogeshpaliyal/keypass/CustomTestRunner.kt index f2e487d4..a3f652ae 100644 --- a/app/src/androidTest/java/com/yogeshpaliyal/keypass/CustomTestRunner.kt +++ b/app/src/androidTest/java/com/yogeshpaliyal/keypass/CustomTestRunner.kt @@ -1,14 +1,14 @@ -package com.yogeshpaliyal.keypass - -import android.app.Application -import android.content.Context -import androidx.test.runner.AndroidJUnitRunner -import dagger.hilt.android.testing.HiltTestApplication - -// A custom runner to set up the instrumented application class for tests. -class CustomTestRunner : AndroidJUnitRunner() { - - override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application { - return super.newApplication(cl, HiltTestApplication::class.java.name, context) - } -} +package com.yogeshpaliyal.keypass + +import android.app.Application +import android.content.Context +import androidx.test.runner.AndroidJUnitRunner +import dagger.hilt.android.testing.HiltTestApplication + +// A custom runner to set up the instrumented application class for tests. +class CustomTestRunner : AndroidJUnitRunner() { + + override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application { + return super.newApplication(cl, HiltTestApplication::class.java.name, context) + } +} diff --git a/app/src/androidTest/java/com/yogeshpaliyal/keypass/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/yogeshpaliyal/keypass/ExampleInstrumentedTest.kt index bcdb1cee..5d5fec24 100644 --- a/app/src/androidTest/java/com/yogeshpaliyal/keypass/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/yogeshpaliyal/keypass/ExampleInstrumentedTest.kt @@ -1,22 +1,22 @@ -package com.yogeshpaliyal.keypass - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import org.junit.Assert.assertEquals -import org.junit.Test -import org.junit.runner.RunWith - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.yogeshpaliyal.keypass", appContext.packageName) - } -} +package com.yogeshpaliyal.keypass + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.yogeshpaliyal.keypass", appContext.packageName) + } +} diff --git a/app/src/androidTest/java/com/yogeshpaliyal/keypass/ui/nav/DashboardActivityTest.kt b/app/src/androidTest/java/com/yogeshpaliyal/keypass/ui/nav/DashboardActivityTest.kt index 93c66e5b..f70be8d2 100644 --- a/app/src/androidTest/java/com/yogeshpaliyal/keypass/ui/nav/DashboardActivityTest.kt +++ b/app/src/androidTest/java/com/yogeshpaliyal/keypass/ui/nav/DashboardActivityTest.kt @@ -1,110 +1,106 @@ -package com.yogeshpaliyal.keypass.ui.nav - -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.assertTextEquals -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performTextInput -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.matcher.ViewMatchers.withId -import com.yogeshpaliyal.common.data.AccountModel -import com.yogeshpaliyal.keypass.R -import dagger.hilt.android.testing.HiltAndroidRule -import dagger.hilt.android.testing.HiltAndroidTest -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import javax.inject.Inject - -@HiltAndroidTest -class DashboardActivityTest { - @get:Rule(order = 0) - var hiltRule = HiltAndroidRule(this) - - @get:Rule(order = 1) - var activityScenarioRule = createAndroidComposeRule() - - @Inject - lateinit var appDatabase: com.yogeshpaliyal.common.AppDatabase - - @Before - fun setUp() { - hiltRule.inject() - appDatabase.clearAllTables() - } - - private fun getDummyAccount(): AccountModel { - val accountModel = AccountModel() - accountModel.title = "Github ${System.currentTimeMillis()}" - accountModel.username = "yogeshpaliyal" - accountModel.password = "1234567890" - accountModel.tags = "social" - accountModel.site = "https://yogeshpaliyal.com" - accountModel.notes = "Testing Notes" - return accountModel - } - - @Test - fun addAccountAndDetailAndDeleteTest() { - val accountModel = getDummyAccount() - addAccount(accountModel) - checkAccountDetail(accountModel) - deleteAccount(accountModel) - } - - private fun addAccount(accountModel: AccountModel) { - // Navigate to add screen - onView(withId(R.id.btnAdd)).perform(click()) - - // Fill information on Detail Activity - activityScenarioRule.onNodeWithTag("accountName").performTextInput(accountModel.title ?: "") - - // generate random password - activityScenarioRule.onNodeWithTag("username").performTextInput(accountModel.username ?: "") - - activityScenarioRule.onNodeWithTag("password").performTextInput(accountModel.password ?: "") - - activityScenarioRule.onNodeWithTag("tags").performTextInput(accountModel.tags ?: "") - - activityScenarioRule.onNodeWithTag("website").performTextInput(accountModel.site ?: "") - - activityScenarioRule.onNodeWithTag("notes").performTextInput(accountModel.notes ?: "") - - activityScenarioRule.onNodeWithTag("save").performClick() - - // is showing in listing - activityScenarioRule.onNodeWithText(accountModel.username ?: "").assertIsDisplayed() - } - - private fun checkAccountDetail(accountModel: AccountModel) { - // Navigate to account detail - activityScenarioRule.onNodeWithText(accountModel.username ?: "").performClick() - - // Fill information on Detail Activity - activityScenarioRule.onNodeWithTag("accountName").assertTextEquals(accountModel.title ?: "") - - // generate random password - activityScenarioRule.onNodeWithTag("username").assertTextEquals(accountModel.username ?: "") - - activityScenarioRule.onNodeWithTag("password").assertTextEquals(accountModel.password ?: "") - - activityScenarioRule.onNodeWithTag("tags").assertTextEquals(accountModel.tags ?: "") - - activityScenarioRule.onNodeWithTag("website").assertTextEquals(accountModel.site ?: "") - - activityScenarioRule.onNodeWithTag("notes").assertTextEquals(accountModel.notes ?: "") - } - - private fun deleteAccount(accountModel: AccountModel) { - // delete account - activityScenarioRule.onNodeWithTag("action_delete").performClick() - - activityScenarioRule.onNodeWithTag("delete").performClick() - - // is not showing in listing - activityScenarioRule.onNodeWithText(accountModel.username ?: "").assertDoesNotExist() - } -} +package com.yogeshpaliyal.keypass.ui.nav + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertTextEquals +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput +import com.yogeshpaliyal.common.data.AccountModel +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import javax.inject.Inject + +@HiltAndroidTest +class DashboardActivityTest { + @get:Rule(order = 0) + var hiltRule = HiltAndroidRule(this) + + @get:Rule(order = 1) + var activityScenarioRule = createAndroidComposeRule() + + @Inject + lateinit var appDatabase: com.yogeshpaliyal.common.AppDatabase + + @Before + fun setUp() { + hiltRule.inject() + appDatabase.clearAllTables() + } + + private fun getDummyAccount(): AccountModel { + val accountModel = AccountModel() + accountModel.title = "Github ${System.currentTimeMillis()}" + accountModel.username = "yogeshpaliyal" + accountModel.password = "1234567890" + accountModel.tags = "social" + accountModel.site = "https://yogeshpaliyal.com" + accountModel.notes = "Testing Notes" + return accountModel + } + + @Test + fun addAccountAndDetailAndDeleteTest() { + val accountModel = getDummyAccount() + addAccount(accountModel) + checkAccountDetail(accountModel) + deleteAccount(accountModel) + } + + private fun addAccount(accountModel: AccountModel) { + // Navigate to add screen + activityScenarioRule.onNodeWithTag("btnAdd").performClick() + + // Fill information on Detail Activity + activityScenarioRule.onNodeWithTag("accountName").performTextInput(accountModel.title ?: "") + + // generate random password + activityScenarioRule.onNodeWithTag("username").performTextInput(accountModel.username ?: "") + + activityScenarioRule.onNodeWithTag("password").performTextInput(accountModel.password ?: "") + + activityScenarioRule.onNodeWithTag("tags").performTextInput(accountModel.tags ?: "") + + activityScenarioRule.onNodeWithTag("website").performTextInput(accountModel.site ?: "") + + activityScenarioRule.onNodeWithTag("notes").performTextInput(accountModel.notes ?: "") + + activityScenarioRule.onNodeWithTag("save").performClick() + + // is showing in listing + activityScenarioRule.onNodeWithText(accountModel.username ?: "").assertIsDisplayed() + } + + private fun checkAccountDetail(accountModel: AccountModel) { + // Navigate to account detail + activityScenarioRule.onNodeWithText(accountModel.username ?: "").performClick() + + // Fill information on Detail Activity + activityScenarioRule.onNodeWithTag("accountName").assertTextEquals(accountModel.title ?: "") + + // generate random password + activityScenarioRule.onNodeWithTag("username").assertTextEquals(accountModel.username ?: "") + + activityScenarioRule.onNodeWithTag("password").assertTextEquals(accountModel.password ?: "") + + activityScenarioRule.onNodeWithTag("tags").assertTextEquals(accountModel.tags ?: "") + + activityScenarioRule.onNodeWithTag("website").assertTextEquals(accountModel.site ?: "") + + activityScenarioRule.onNodeWithTag("notes").assertTextEquals(accountModel.notes ?: "") + } + + private fun deleteAccount(accountModel: AccountModel) { + // delete account + activityScenarioRule.onNodeWithTag("action_delete").performClick() + + activityScenarioRule.onNodeWithTag("delete").performClick() + + // is not showing in listing + activityScenarioRule.onNodeWithText(accountModel.username ?: "").assertDoesNotExist() + } +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/MyApplication.kt b/app/src/main/java/com/yogeshpaliyal/keypass/MyApplication.kt index 84c5374c..e34168d3 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/MyApplication.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/MyApplication.kt @@ -1,20 +1,20 @@ -package com.yogeshpaliyal.keypass - -import android.content.Intent -import com.yogeshpaliyal.common.CommonMyApplication -import com.yogeshpaliyal.keypass.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.stackTraceToString()) - } -} +package com.yogeshpaliyal.keypass + +import android.content.Intent +import com.yogeshpaliyal.common.CommonMyApplication +import com.yogeshpaliyal.keypass.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.stackTraceToString()) + } +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/customViews/MaskedCardView.kt b/app/src/main/java/com/yogeshpaliyal/keypass/customViews/MaskedCardView.kt deleted file mode 100644 index eaafb503..00000000 --- a/app/src/main/java/com/yogeshpaliyal/keypass/customViews/MaskedCardView.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.yogeshpaliyal.keypass.customViews - -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.keypass.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/app/src/main/java/com/yogeshpaliyal/keypass/listener/AccountsClickListener.kt b/app/src/main/java/com/yogeshpaliyal/keypass/listener/AccountsClickListener.kt deleted file mode 100644 index 731520b0..00000000 --- a/app/src/main/java/com/yogeshpaliyal/keypass/listener/AccountsClickListener.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.yogeshpaliyal.keypass.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/app/src/main/java/com/yogeshpaliyal/keypass/listener/UniversalClickListener.kt b/app/src/main/java/com/yogeshpaliyal/keypass/listener/UniversalClickListener.kt deleted file mode 100644 index bee9569e..00000000 --- a/app/src/main/java/com/yogeshpaliyal/keypass/listener/UniversalClickListener.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.yogeshpaliyal.keypass.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/app/src/main/java/com/yogeshpaliyal/keypass/ui/CrashActivity.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/CrashActivity.kt index e8293221..c664cba2 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/CrashActivity.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/CrashActivity.kt @@ -1,65 +1,65 @@ -package com.yogeshpaliyal.keypass.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.keypass.BuildConfig -import com.yogeshpaliyal.keypass.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() - deviceInfo.append(binding.txtCrash.text.toString()) - 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, deviceInfo.toString()) - - startActivity(Intent.createChooser(intent, "")) - } - } -} +package com.yogeshpaliyal.keypass.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.keypass.BuildConfig +import com.yogeshpaliyal.keypass.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() + deviceInfo.append(binding.txtCrash.text.toString()) + 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, deviceInfo.toString()) + + startActivity(Intent.createChooser(intent, "")) + } + } +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/addTOTP/AddTOTPActivity.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/addTOTP/AddTOTPActivity.kt index b29841df..37859400 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/addTOTP/AddTOTPActivity.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/addTOTP/AddTOTPActivity.kt @@ -1,140 +1,140 @@ -package com.yogeshpaliyal.keypass.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.keypass.R -import com.yogeshpaliyal.keypass.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() - } - - @Deprecated("Deprecated in Java") - 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) - } - } -} +package com.yogeshpaliyal.keypass.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.keypass.R +import com.yogeshpaliyal.keypass.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() + } + + @Deprecated("Deprecated in Java") + 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/app/src/main/java/com/yogeshpaliyal/keypass/ui/addTOTP/AddTOTPViewModel.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/addTOTP/AddTOTPViewModel.kt index b2177311..f437cfa0 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/addTOTP/AddTOTPViewModel.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/addTOTP/AddTOTPViewModel.kt @@ -1,85 +1,85 @@ -package com.yogeshpaliyal.keypass.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.keypass.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() - } - } -} +package com.yogeshpaliyal.keypass.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.keypass.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/app/src/main/java/com/yogeshpaliyal/keypass/ui/addTOTP/ScannerActivity.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/addTOTP/ScannerActivity.kt index 887c87c3..754f39c3 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/addTOTP/ScannerActivity.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/addTOTP/ScannerActivity.kt @@ -1,55 +1,55 @@ -package com.yogeshpaliyal.keypass.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.keypass.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 } -} +package com.yogeshpaliyal.keypass.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.keypass.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/app/src/main/java/com/yogeshpaliyal/keypass/ui/auth/AuthScreen.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/auth/AuthScreen.kt index c8eb4c15..837433d2 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/auth/AuthScreen.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/auth/AuthScreen.kt @@ -1,90 +1,90 @@ -package com.yogeshpaliyal.keypass.ui.auth - -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.yogeshpaliyal.common.utils.getKeyPassPassword -import com.yogeshpaliyal.keypass.R -import com.yogeshpaliyal.keypass.ui.auth.components.ButtonBar -import com.yogeshpaliyal.keypass.ui.auth.components.PasswordInputField -import com.yogeshpaliyal.keypass.ui.redux.AuthState -import com.yogeshpaliyal.keypass.ui.redux.NavigationAction -import kotlinx.coroutines.launch -import org.reduxkotlin.compose.rememberDispatcher - -@Composable -fun AuthScreen(state: AuthState) { - val context = LocalContext.current - - val dispatchAction = rememberDispatcher() - - val coroutineScope = rememberCoroutineScope() - - val (password, setPassword) = remember(state) { - mutableStateOf("") - } - - val (passwordVisible, setPasswordVisible) = remember(state) { mutableStateOf(false) } - - val (passwordError, setPasswordError) = remember(state, password) { - mutableStateOf(null) - } - - BackHandler(state is AuthState.ConfirmPassword) { - dispatchAction(NavigationAction(AuthState.CreatePassword)) - } - - LaunchedEffect(key1 = Unit, block = { - coroutineScope.launch { - val mPassword = context.getKeyPassPassword() - if (mPassword == null) { - dispatchAction(NavigationAction(AuthState.CreatePassword)) - } - } - }) - - Column( - modifier = Modifier - .padding(32.dp) - .fillMaxSize(1f) - .verticalScroll(rememberScrollState()), - Arrangement.SpaceEvenly, - Alignment.CenterHorizontally - ) { - Image( - painter = painterResource(R.drawable.ic_undraw_unlock_24mb), - contentDescription = "" - ) - - Text(text = stringResource(id = state.title)) - - PasswordInputField( - password, - setPassword, - passwordVisible, - setPasswordVisible, - passwordError - ) - - ButtonBar(state, password, setPasswordError) { - dispatchAction(it) - } - } -} +package com.yogeshpaliyal.keypass.ui.auth + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.yogeshpaliyal.common.utils.getKeyPassPassword +import com.yogeshpaliyal.keypass.R +import com.yogeshpaliyal.keypass.ui.auth.components.ButtonBar +import com.yogeshpaliyal.keypass.ui.auth.components.PasswordInputField +import com.yogeshpaliyal.keypass.ui.redux.actions.NavigationAction +import com.yogeshpaliyal.keypass.ui.redux.states.AuthState +import kotlinx.coroutines.launch +import org.reduxkotlin.compose.rememberDispatcher + +@Composable +fun AuthScreen(state: AuthState) { + val context = LocalContext.current + + val dispatchAction = rememberDispatcher() + + val coroutineScope = rememberCoroutineScope() + + val (password, setPassword) = remember(state) { + mutableStateOf("") + } + + val (passwordVisible, setPasswordVisible) = remember(state) { mutableStateOf(false) } + + val (passwordError, setPasswordError) = remember(state, password) { + mutableStateOf(null) + } + + BackHandler(state is AuthState.ConfirmPassword) { + dispatchAction(NavigationAction(AuthState.CreatePassword)) + } + + LaunchedEffect(key1 = Unit, block = { + coroutineScope.launch { + val mPassword = context.getKeyPassPassword() + if (mPassword == null) { + dispatchAction(NavigationAction(AuthState.CreatePassword)) + } + } + }) + + Column( + modifier = Modifier + .padding(32.dp) + .fillMaxSize(1f) + .verticalScroll(rememberScrollState()), + Arrangement.SpaceEvenly, + Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(R.drawable.ic_undraw_unlock_24mb), + contentDescription = "" + ) + + Text(text = stringResource(id = state.title)) + + PasswordInputField( + password, + setPassword, + passwordVisible, + setPasswordVisible, + passwordError + ) + + ButtonBar(state, password, setPasswordError) { + dispatchAction(it) + } + } +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/auth/components/ButtonBar.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/auth/components/ButtonBar.kt index 241c557a..6a0eea17 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/auth/components/ButtonBar.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/auth/components/ButtonBar.kt @@ -1,77 +1,77 @@ -package com.yogeshpaliyal.keypass.ui.auth.components - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import com.yogeshpaliyal.common.utils.getKeyPassPassword -import com.yogeshpaliyal.common.utils.setKeyPassPassword -import com.yogeshpaliyal.keypass.R -import com.yogeshpaliyal.keypass.ui.redux.AuthState -import com.yogeshpaliyal.keypass.ui.redux.HomeState -import com.yogeshpaliyal.keypass.ui.redux.NavigationAction -import kotlinx.coroutines.launch - -@Composable -fun ButtonBar( - state: AuthState, - password: String, - setPasswordError: (Int?) -> Unit, - dispatchAction: (NavigationAction) -> Unit -) { - val coroutineScope = rememberCoroutineScope() - val context = LocalContext.current - - Row(modifier = Modifier.fillMaxWidth(1f), Arrangement.SpaceEvenly) { - AnimatedVisibility(state is AuthState.ConfirmPassword) { - Button(onClick = { - dispatchAction(NavigationAction(AuthState.CreatePassword)) - }) { - Text(text = stringResource(id = R.string.back)) - } - } - - Button(onClick = { - when (state) { - is AuthState.CreatePassword -> { - if (password.isBlank()) { - setPasswordError(R.string.enter_password) - } else { - dispatchAction(NavigationAction(AuthState.ConfirmPassword(password))) - } - } - - is AuthState.ConfirmPassword -> { - if (state.password == password) { - coroutineScope.launch { - context.setKeyPassPassword(password) - dispatchAction(NavigationAction(HomeState(), true)) - } - } else { - setPasswordError(R.string.password_no_match) - } - } - - is AuthState.Login -> { - coroutineScope.launch { - val savedPassword = context.getKeyPassPassword() - if (savedPassword == password) { - dispatchAction(NavigationAction(HomeState(), true)) - } else { - setPasswordError(R.string.incorrect_password) - } - } - } - } - }) { - Text(text = stringResource(id = R.string.str_continue)) - } - } -} +package com.yogeshpaliyal.keypass.ui.auth.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import com.yogeshpaliyal.common.utils.getKeyPassPassword +import com.yogeshpaliyal.common.utils.setKeyPassPassword +import com.yogeshpaliyal.keypass.R +import com.yogeshpaliyal.keypass.ui.redux.actions.NavigationAction +import com.yogeshpaliyal.keypass.ui.redux.states.AuthState +import com.yogeshpaliyal.keypass.ui.redux.states.HomeState +import kotlinx.coroutines.launch + +@Composable +fun ButtonBar( + state: AuthState, + password: String, + setPasswordError: (Int?) -> Unit, + dispatchAction: (NavigationAction) -> Unit +) { + val coroutineScope = rememberCoroutineScope() + val context = LocalContext.current + + Row(modifier = Modifier.fillMaxWidth(1f), Arrangement.SpaceEvenly) { + AnimatedVisibility(state is AuthState.ConfirmPassword) { + Button(onClick = { + dispatchAction(NavigationAction(AuthState.CreatePassword)) + }) { + Text(text = stringResource(id = R.string.back)) + } + } + + Button(onClick = { + when (state) { + is AuthState.CreatePassword -> { + if (password.isBlank()) { + setPasswordError(R.string.enter_password) + } else { + dispatchAction(NavigationAction(AuthState.ConfirmPassword(password))) + } + } + + is AuthState.ConfirmPassword -> { + if (state.password == password) { + coroutineScope.launch { + context.setKeyPassPassword(password) + dispatchAction(NavigationAction(HomeState(), true)) + } + } else { + setPasswordError(R.string.password_no_match) + } + } + + is AuthState.Login -> { + coroutineScope.launch { + val savedPassword = context.getKeyPassPassword() + if (savedPassword == password) { + dispatchAction(NavigationAction(HomeState(), true)) + } else { + setPasswordError(R.string.incorrect_password) + } + } + } + } + }) { + Text(text = stringResource(id = R.string.str_continue)) + } + } +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/auth/components/PasswordInputField.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/auth/components/PasswordInputField.kt index bd2e2828..0185ddd2 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/auth/components/PasswordInputField.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/auth/components/PasswordInputField.kt @@ -1,62 +1,62 @@ -package com.yogeshpaliyal.keypass.ui.auth.components - -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Visibility -import androidx.compose.material.icons.rounded.VisibilityOff -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.VisualTransformation -import com.yogeshpaliyal.keypass.R - -@Composable -fun PasswordInputField( - password: String, - setPassword: (String) -> Unit, - passwordVisible: Boolean, - setPasswordVisible: (Boolean) -> Unit, - passwordError: Int? -) { - OutlinedTextField( - modifier = Modifier.fillMaxWidth(1f), - value = password, - singleLine = true, - placeholder = { - Text(text = stringResource(id = R.string.enter_password)) - }, - visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), - onValueChange = setPassword, - isError = passwordError != null, - supportingText = { - if (passwordError != null) { - Text( - modifier = Modifier.fillMaxWidth(), - text = stringResource(id = passwordError), - color = MaterialTheme.colorScheme.error - ) - } - }, - trailingIcon = { - val image = if (passwordVisible) { - Icons.Rounded.Visibility - } else Icons.Rounded.VisibilityOff - - // Please provide localized description for accessibility services - val description = if (passwordVisible) "Hide password" else "Show password" - - IconButton(onClick = { setPasswordVisible(!passwordVisible) }) { - Icon(imageVector = image, description) - } - } - ) -} +package com.yogeshpaliyal.keypass.ui.auth.components + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Visibility +import androidx.compose.material.icons.rounded.VisibilityOff +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import com.yogeshpaliyal.keypass.R + +@Composable +fun PasswordInputField( + password: String, + setPassword: (String) -> Unit, + passwordVisible: Boolean, + setPasswordVisible: (Boolean) -> Unit, + passwordError: Int? +) { + OutlinedTextField( + modifier = Modifier.fillMaxWidth(1f), + value = password, + singleLine = true, + placeholder = { + Text(text = stringResource(id = R.string.enter_password)) + }, + visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), + onValueChange = setPassword, + isError = passwordError != null, + supportingText = { + if (passwordError != null) { + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = passwordError), + color = MaterialTheme.colorScheme.error + ) + } + }, + trailingIcon = { + val image = if (passwordVisible) { + Icons.Rounded.Visibility + } else Icons.Rounded.VisibilityOff + + // Please provide localized description for accessibility services + val description = if (passwordVisible) "Hide password" else "Show password" + + IconButton(onClick = { setPasswordVisible(!passwordVisible) }) { + Icon(imageVector = image, description) + } + } + ) +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/backup/BackupActivity.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/backup/BackupActivity.kt index 2291b286..bc965e5a 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/backup/BackupActivity.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/backup/BackupActivity.kt @@ -1,330 +1,251 @@ -package com.yogeshpaliyal.keypass.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.keypass.R -import com.yogeshpaliyal.keypass.databinding.BackupActivityBinding -import com.yogeshpaliyal.keypass.databinding.LayoutBackupKeypharseBinding -import com.yogeshpaliyal.keypass.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() - } - } -} +package com.yogeshpaliyal.keypass.ui.backup + +import android.content.Intent +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ArrowBackIosNew +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +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.isAutoBackupEnabled +import com.yogeshpaliyal.common.utils.isKeyPresent +import com.yogeshpaliyal.common.utils.overrideAutoBackup +import com.yogeshpaliyal.common.utils.setAutoBackupEnabled +import com.yogeshpaliyal.common.utils.setBackupDirectory +import com.yogeshpaliyal.common.utils.setOverrideAutoBackup +import com.yogeshpaliyal.keypass.R +import com.yogeshpaliyal.keypass.ui.backup.components.BackupDialogs +import com.yogeshpaliyal.keypass.ui.redux.actions.Action +import com.yogeshpaliyal.keypass.ui.redux.actions.GoBackAction +import com.yogeshpaliyal.keypass.ui.redux.actions.StateUpdateAction +import com.yogeshpaliyal.keypass.ui.redux.states.BackupScreenState +import com.yogeshpaliyal.keypass.ui.redux.states.SelectKeyphraseType +import com.yogeshpaliyal.keypass.ui.redux.states.ShowKeyphrase +import com.yogeshpaliyal.keypass.ui.settings.PreferenceItem +import kotlinx.coroutines.launch +import org.reduxkotlin.compose.rememberTypedDispatcher + +@Composable +fun BackupScreen(state: BackupScreenState) { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + + val dispatchAction = rememberTypedDispatcher() + + val launcher = rememberLauncherForActivityResult(KeyPassBackupDirectoryPick()) { + if (it == null) { + return@rememberLauncherForActivityResult + } + + context.contentResolver.takePersistableUriPermission( + it, + Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + + coroutineScope.launch { + val dialog = if (context.isKeyPresent()) { + ShowKeyphrase + } else { + SelectKeyphraseType + } + dispatchAction( + StateUpdateAction( + state.copy( + backupDirectory = it, + dialog = dialog + ) + ) + ) + } + } + + LaunchedEffect(key1 = state, block = { + state.backupDirectory?.let { + context.setBackupDirectory(it.toString()) + } + state.isAutoBackupEnabled?.let { + context.setAutoBackupEnabled(it) + } + state.overrideAutoBackup?.let { + context.setOverrideAutoBackup(it) + } + }) + + LaunchedEffect(key1 = state.isBackupEnabled, block = { + state.isBackupEnabled?.let { + if (it.not()) { + context.clearBackupKey() + } + } + }) + + LaunchedEffect(key1 = Unit, block = { + val isBackupEnabled = ( + context.canUserAccessBackupDirectory() && (context.isKeyPresent()) + ) + + val isAutoBackupEnabled = context.isAutoBackupEnabled() + val overrideAutoBackup = context.overrideAutoBackup() + + val lastBackupTime = context.getBackupTime() + val backupDirectory = context.getBackupDirectory() + + dispatchAction( + StateUpdateAction( + state.copy( + isBackupEnabled = isBackupEnabled, + isAutoBackupEnabled = isAutoBackupEnabled, + overrideAutoBackup = overrideAutoBackup, + lastBackupTime = lastBackupTime, + backupDirectory = Uri.parse(backupDirectory) + ) + ) + ) + }) + + Scaffold(bottomBar = { + BottomAppBar { + IconButton(onClick = { + dispatchAction(GoBackAction) + }) { + Icon( + painter = rememberVectorPainter(image = Icons.Rounded.ArrowBackIosNew), + contentDescription = "Go Back", + tint = MaterialTheme.colorScheme.onSurface + ) + } + } + }) { contentPadding -> + Surface(modifier = Modifier.padding(contentPadding)) { + BackSettingOptions(state, updatedState = { + dispatchAction(StateUpdateAction(it)) + }) { + launcher.launch(arrayOf()) + } + } + } + + BackupDialogs(state = state) { + dispatchAction(StateUpdateAction(it)) + } +} + +@Composable +fun BackSettingOptions( + state: BackupScreenState, + updatedState: (BackupScreenState) -> Unit, + launchDirectorySelector: () -> Unit +) { + Column { + PreferenceItem(summary = R.string.backup_desc) + AnimatedVisibility(visible = state.isBackupEnabled == true) { + BackupEnableOptions(state, updatedState, launchDirectorySelector) + } + + AnimatedVisibility(visible = state.isBackupEnabled != true) { + PreferenceItem(title = R.string.turn_on_backup, onClickItem = launchDirectorySelector) + } + } +} + +@Composable +fun BackupEnableOptions( + state: BackupScreenState, + updatedState: (BackupScreenState) -> Unit, + launchDirectorySelector: () -> Unit +) { + Column { + PreferenceItem( + title = R.string.create_backup, + summaryStr = stringResource( + id = R.string.last_backup_date, + state.lastBackupTime?.formatCalendar("dd MMM yyyy hh:mm aa") ?: "" + ) + ) { + updatedState(state.copy(dialog = ShowKeyphrase)) + } + PreferenceItem( + title = R.string.backup_folder, + summaryStr = state.getFormattedBackupDirectory(), + onClickItem = launchDirectorySelector + ) + + AutoBackup(state.isAutoBackupEnabled, state.overrideAutoBackup, { + updatedState(state.copy(isAutoBackupEnabled = it)) + }) { + updatedState(state.copy(overrideAutoBackup = it)) + } + +// PreferenceItem( +// title = R.string.verify_keyphrase, +// summary = R.string.verify_keyphrase_message +// ) + PreferenceItem(title = R.string.turn_off_backup) { + updatedState( + state.copy( + isBackupEnabled = false, + isAutoBackupEnabled = false, + overrideAutoBackup = false, + lastBackupTime = -1, + backupDirectory = null + ) + ) + } + } +} + +@Composable +fun AutoBackup( + isAutoBackupEnabled: Boolean?, + overrideAutoBackup: Boolean?, + setAutoBackupEnabled: (Boolean) -> Unit, + setOverrideAutoBackup: (Boolean) -> Unit +) { + PreferenceItem( + title = R.string.auto_backup, + summary = if (isAutoBackupEnabled == true) R.string.enabled else R.string.disabled + ) { + setAutoBackupEnabled(!(isAutoBackupEnabled ?: false)) + } + + AnimatedVisibility(visible = isAutoBackupEnabled == true) { + Column { + PreferenceItem(title = R.string.auto_backup, isCategory = true) + Divider( + modifier = Modifier + .fillMaxWidth() + .height(1.dp) + ) + PreferenceItem(summary = R.string.auto_backup_desc) + PreferenceItem( + title = R.string.override_auto_backup_file, + summary = if (overrideAutoBackup == true) R.string.enabled else R.string.disabled + ) { + setOverrideAutoBackup(!(overrideAutoBackup ?: false)) + } + } + } +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/backup/KeyPassBackupDirectoryPick.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/backup/KeyPassBackupDirectoryPick.kt new file mode 100644 index 00000000..106cd06f --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/backup/KeyPassBackupDirectoryPick.kt @@ -0,0 +1,19 @@ +package com.yogeshpaliyal.keypass.ui.backup + +import android.content.Context +import android.content.Intent +import androidx.activity.result.contract.ActivityResultContracts + +class KeyPassBackupDirectoryPick : ActivityResultContracts.OpenDocument() { + override fun createIntent(context: Context, input: Array): Intent { + super.createIntent(context, input) + 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 + ) + return intent + } +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/backup/components/BackupDialogs.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/backup/components/BackupDialogs.kt new file mode 100644 index 00000000..5e3c6fd7 --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/backup/components/BackupDialogs.kt @@ -0,0 +1,47 @@ +package com.yogeshpaliyal.keypass.ui.backup.components + +import androidx.compose.runtime.Composable +import com.yogeshpaliyal.keypass.ui.redux.states.BackupScreenState +import com.yogeshpaliyal.keypass.ui.redux.states.CustomKeyphrase +import com.yogeshpaliyal.keypass.ui.redux.states.SelectKeyphraseType +import com.yogeshpaliyal.keypass.ui.redux.states.ShowKeyphrase + +@Composable +fun BackupDialogs(state: BackupScreenState, updateState: (BackupScreenState) -> Unit) { + when (state.dialog) { + is SelectKeyphraseType -> { + SelectKeyphraseType(customKeyphrase = { + updateState(state.copy(dialog = CustomKeyphrase)) + }, generateKeyphrase = { + updateState(state.copy(dialog = ShowKeyphrase)) + }) { + updateState(state.copy(dialog = null)) + } + } + + is CustomKeyphrase -> { + CreateCustomKeyphrase(saveKeyphrase = { + updateState(state.copy(dialog = ShowKeyphrase)) + }) { + updateState(state.copy(dialog = null)) + } + } + + is ShowKeyphrase -> { + ShowKeyPhraseDialog(selectedDirectory = state.backupDirectory, onYesClicked = { + updateState(state.copy(dialog = null, isBackupEnabled = true)) + }) { + updateState( + state.copy( + isBackupEnabled = true, + lastBackupTime = System.currentTimeMillis() + ) + ) + } + } + + null -> { + // Do Noting Do not show dialog + } + } +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/backup/components/CreateCustomKeyphrase.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/backup/components/CreateCustomKeyphrase.kt new file mode 100644 index 00000000..373e621e --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/backup/components/CreateCustomKeyphrase.kt @@ -0,0 +1,75 @@ +package com.yogeshpaliyal.keypass.ui.backup.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.yogeshpaliyal.common.utils.BACKUP_KEY_LENGTH +import com.yogeshpaliyal.common.utils.saveKeyphrase +import com.yogeshpaliyal.keypass.R +import com.yogeshpaliyal.keypass.ui.redux.actions.Action +import com.yogeshpaliyal.keypass.ui.redux.actions.ToastAction +import kotlinx.coroutines.launch +import org.reduxkotlin.compose.rememberTypedDispatcher + +@Composable +fun CreateCustomKeyphrase(saveKeyphrase: () -> Unit, dismissDialog: () -> Unit) { + val (keyphrase, setKeyPhrase) = remember { + mutableStateOf("") + } + + val dispatchAction = rememberTypedDispatcher() + + val context = LocalContext.current + + val coroutineScope = rememberCoroutineScope() + + AlertDialog(onDismissRequest = dismissDialog, title = { + Text(text = stringResource(id = R.string.set_keyphrase)) + }, confirmButton = { + TextButton(onClick = { + if (keyphrase.isEmpty()) { + dispatchAction(ToastAction(R.string.alert_blank_keyphrase)) + return@TextButton + } + + if (keyphrase.length != BACKUP_KEY_LENGTH) { + dispatchAction(ToastAction(R.string.alert_invalid_keyphrase)) + return@TextButton + } + + coroutineScope.launch { + context.saveKeyphrase(keyphrase) + } + + saveKeyphrase.invoke() + }) { + Text(stringResource(id = R.string.yes)) + } + }, text = { + Column { + Text(text = stringResource(id = R.string.set_keyphrase_info)) + Spacer(modifier = Modifier.size(8.dp)) + OutlinedTextField( + modifier = Modifier.fillMaxWidth(1f), + value = keyphrase, + onValueChange = setKeyPhrase, + placeholder = { + Text(text = stringResource(id = R.string.enter_keyphrase)) + } + ) + } + }) +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/backup/components/SelectKeyphraseType.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/backup/components/SelectKeyphraseType.kt new file mode 100644 index 00000000..cd03f1be --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/backup/components/SelectKeyphraseType.kt @@ -0,0 +1,32 @@ +package com.yogeshpaliyal.keypass.ui.backup.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.yogeshpaliyal.keypass.R + +@Composable +fun SelectKeyphraseType( + customKeyphrase: () -> Unit, + generateKeyphrase: () -> Unit, + dismissDialog: () -> Unit +) { + AlertDialog(onDismissRequest = dismissDialog, title = { + Text(text = stringResource(id = R.string.alert)) + }, confirmButton = { + TextButton(onClick = customKeyphrase) { + Text(stringResource(id = R.string.custom_keyphrase)) + } + }, dismissButton = { + TextButton(onClick = generateKeyphrase) { + Text(stringResource(id = R.string.generate_keyphrase)) + } + }, text = { + Column { + Text(text = stringResource(id = R.string.custom_generated_keyphrase_info)) + } + }) +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/backup/components/ShowKeyPhraseDialog.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/backup/components/ShowKeyPhraseDialog.kt new file mode 100644 index 00000000..c0a4ccec --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/backup/components/ShowKeyPhraseDialog.kt @@ -0,0 +1,75 @@ +package com.yogeshpaliyal.keypass.ui.backup.components + +import android.net.Uri +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import com.yogeshpaliyal.common.utils.backupAccounts +import com.yogeshpaliyal.keypass.R +import com.yogeshpaliyal.keypass.ui.redux.actions.Action +import com.yogeshpaliyal.keypass.ui.redux.actions.CopyToClipboard +import com.yogeshpaliyal.keypass.ui.redux.actions.ToastAction +import com.yogeshpaliyal.keypass.ui.settings.MySettingsViewModel +import org.reduxkotlin.compose.rememberTypedDispatcher + +@Composable +fun ShowKeyPhraseDialog( + selectedDirectory: Uri?, + mySettingsViewModel: MySettingsViewModel = hiltViewModel(), + onYesClicked: () -> Unit, + saveKeyphrase: () -> Unit +) { + if (selectedDirectory == null) { + return + } + + val dispatchAction = rememberTypedDispatcher() + + val context = LocalContext.current + val (backupInfo, setBackupInfo) = remember { + mutableStateOf?>(null) + } + + LaunchedEffect(key1 = selectedDirectory, block = { + val localBackupInfo = context.backupAccounts( + mySettingsViewModel.appDb, + selectedDirectory + ) + setBackupInfo(localBackupInfo) + saveKeyphrase() + }) + + if (backupInfo != null) { + val newKeyCreated = backupInfo.first + if (newKeyCreated) { + AlertDialog(onDismissRequest = {}, title = { + Text(text = stringResource(id = R.string.alert)) + }, confirmButton = { + TextButton(onClick = { + onYesClicked() + }) { + Text(stringResource(id = R.string.yes)) + } + }, text = { + Column { + Text(text = stringResource(id = R.string.copy_keypharse_msg)) + TextButton(onClick = { + dispatchAction(CopyToClipboard(backupInfo.second)) + }) { + Text(text = backupInfo.second) + } + } + }) + } else { + dispatchAction(ToastAction(R.string.backup_completed)) + } + } +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/DetailActivity.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/DetailActivity.kt index ae5eee54..81d86f6f 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/DetailActivity.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/DetailActivity.kt @@ -1,356 +1,356 @@ -package com.yogeshpaliyal.keypass.ui.detail - -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.annotation.StringRes -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.ArrowBackIosNew -import androidx.compose.material.icons.rounded.Delete -import androidx.compose.material.icons.rounded.Done -import androidx.compose.material.icons.rounded.Refresh -import androidx.compose.material.icons.rounded.Visibility -import androidx.compose.material.icons.rounded.VisibilityOff -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.BottomAppBar -import androidx.compose.material3.Button -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.rememberVectorPainter -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import com.yogeshpaliyal.common.data.AccountModel -import com.yogeshpaliyal.common.utils.PasswordGenerator -import com.yogeshpaliyal.keypass.R -import com.yogeshpaliyal.keypass.ui.redux.GoBackAction -import org.reduxkotlin.compose.rememberDispatcher - -/* -* @author Yogesh Paliyal -* yogeshpaliyal.foss@gmail.com -* https://techpaliyal.com -* created on 31-01-2021 10:38 -*/ - -@Composable -fun AccountDetailPage( - id: Long?, - viewModel: DetailViewModel = hiltViewModel() -) { - val dispatchAction = rememberDispatcher() - - // task value state - val (accountModel, setAccountModel) = remember { - mutableStateOf( - AccountModel() - ) - } - - // Set initial object - LaunchedEffect(key1 = id) { - viewModel.loadAccount(id) { - setAccountModel(it.copy()) - } - } - - val goBack: () -> Unit = { - dispatchAction(GoBackAction) - } - - val launcher = rememberLauncherForActivityResult(QRScanner()) { - it?.let { - setAccountModel(accountModel.copy(password = it)) - } - } - - Scaffold( - bottomBar = { - BottomBar( - accountModel, - backPressed = goBack, - onDeleteAccount = { - viewModel.deleteAccount(accountModel, goBack) - } - ) { - viewModel.insertOrUpdate(accountModel, goBack) - } - } - ) { paddingValues -> - Surface(modifier = Modifier.padding(paddingValues)) { - Fields( - accountModel = accountModel, - updateAccountModel = { newAccountModel -> - setAccountModel(newAccountModel) - } - ) { - launcher.launch(null) - } - } - } -} - -@Composable -fun Fields( - modifier: Modifier = Modifier, - accountModel: AccountModel, - updateAccountModel: (newAccountModel: AccountModel) -> Unit, - scanClicked: () -> Unit -) { - Column( - modifier = modifier - .fillMaxSize() - .padding(16.dp) - .verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - KeyPassInputField( - modifier = Modifier.testTag("accountName"), - placeholder = R.string.account_name, - value = accountModel.title, - setValue = { - updateAccountModel(accountModel.copy(title = it)) - } - ) - - KeyPassInputField( - modifier = Modifier.testTag("username"), - placeholder = R.string.username_email_phone, - value = accountModel.username, - setValue = { - updateAccountModel(accountModel.copy(username = it)) - } - ) - - Column { - val passwordVisible = rememberSaveable { mutableStateOf(false) } - - val visualTransformation = - if (passwordVisible.value) VisualTransformation.None else PasswordVisualTransformation() - - KeyPassInputField( - modifier = Modifier.testTag("password"), - placeholder = R.string.password, - value = accountModel.password, - setValue = { - updateAccountModel(accountModel.copy(password = it)) - }, - trailingIcon = { - PasswordTrailingIcon(passwordVisible.value) { - passwordVisible.value = it - } - }, - leadingIcon = if (accountModel.id != null) { - null - } else ( - { - IconButton( - onClick = { - updateAccountModel(accountModel.copy(password = PasswordGenerator().generatePassword())) - } - ) { - Icon( - painter = rememberVectorPainter(image = Icons.Rounded.Refresh), - contentDescription = "" - ) - } - } - ), - visualTransformation = visualTransformation - ) - Button(onClick = scanClicked) { - Row { - Icon( - painter = painterResource(id = R.drawable.ic_twotone_qr_code_scanner_24), - contentDescription = "" - ) - Text(text = stringResource(id = R.string.scan_password)) - } - } - } - - KeyPassInputField( - modifier = Modifier.testTag("tags"), - placeholder = R.string.tags_comma_separated_optional, - value = accountModel.tags, - setValue = { - updateAccountModel(accountModel.copy(tags = it)) - } - ) - - KeyPassInputField( - modifier = Modifier.testTag("website"), - placeholder = R.string.website_url_optional, - value = accountModel.site, - setValue = { - updateAccountModel(accountModel.copy(site = it)) - } - ) - - KeyPassInputField( - modifier = Modifier.testTag("notes"), - placeholder = R.string.notes_optional, - value = accountModel.notes, - setValue = { - updateAccountModel(accountModel.copy(notes = it)) - } - ) - } -} - -@Composable -fun DeleteConfirmation( - openDialog: Boolean, - updateDialogVisibility: (Boolean) -> Unit, - onDelete: () -> Unit -) { - if (openDialog) { - AlertDialog( - onDismissRequest = { /*TODO*/ }, - title = { - Text(text = stringResource(id = R.string.delete_account_title)) - }, - confirmButton = { - TextButton( - modifier = Modifier.testTag("delete"), - onClick = { - updateDialogVisibility(false) - onDelete() - } - ) { - Text(text = stringResource(id = R.string.delete)) - } - }, - text = { - Text(text = stringResource(id = R.string.delete_account_msg)) - }, - dismissButton = { - TextButton( - onClick = { - updateDialogVisibility(false) - } - ) { - Text(text = stringResource(id = R.string.cancel)) - } - } - ) - } -} - -@Composable -fun KeyPassInputField( - modifier: Modifier = Modifier, - @StringRes placeholder: Int, - value: String?, - setValue: (String) -> Unit, - leadingIcon: @Composable (() -> Unit)? = null, - trailingIcon: @Composable (() -> Unit)? = null, - visualTransformation: VisualTransformation = VisualTransformation.None -) { - OutlinedTextField( - modifier = modifier.fillMaxWidth(), - value = value ?: "", - label = { - Text(text = stringResource(id = placeholder)) - }, - onValueChange = setValue, - leadingIcon = leadingIcon, - trailingIcon = trailingIcon, - visualTransformation = visualTransformation - ) -} - -@Composable -fun PasswordTrailingIcon( - passwordVisible: Boolean, - changePasswordVisibility: (updatedValue: Boolean) -> Unit -) { - val description = if (passwordVisible) "Hide password" else "Show password" - - val image = if (passwordVisible) { - Icons.Rounded.Visibility - } else { - Icons.Rounded.VisibilityOff - } - - IconButton(onClick = { changePasswordVisibility(!passwordVisible) }) { - Icon( - painter = rememberVectorPainter(image = image), - contentDescription = description - ) - } -} - -@Composable -fun BottomBar( - accountModel: AccountModel, - backPressed: () -> Unit, - onDeleteAccount: () -> Unit, - onSaveClicked: () -> Unit -) { - val openDialog = remember { mutableStateOf(false) } - - BottomAppBar( - actions = { - IconButton(onClick = backPressed) { - Icon( - painter = rememberVectorPainter(image = Icons.Rounded.ArrowBackIosNew), - contentDescription = "Go Back", - tint = MaterialTheme.colorScheme.onSurface - ) - } - - if (accountModel.id != null) { - IconButton( - modifier = Modifier.testTag("action_delete"), - onClick = { openDialog.value = true } - ) { - Icon( - painter = rememberVectorPainter(image = Icons.Rounded.Delete), - contentDescription = "Delete", - tint = MaterialTheme.colorScheme.onSurface - ) - } - } - }, - floatingActionButton = { - FloatingActionButton(modifier = Modifier.testTag("save"), onClick = onSaveClicked) { - Icon( - painter = rememberVectorPainter(image = Icons.Rounded.Done), - contentDescription = "Save Changes" - ) - } - } - ) - - DeleteConfirmation( - openDialog.value, - updateDialogVisibility = { - openDialog.value = it - }, - onDeleteAccount - ) -} +package com.yogeshpaliyal.keypass.ui.detail + +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ArrowBackIosNew +import androidx.compose.material.icons.rounded.Delete +import androidx.compose.material.icons.rounded.Done +import androidx.compose.material.icons.rounded.Refresh +import androidx.compose.material.icons.rounded.Visibility +import androidx.compose.material.icons.rounded.VisibilityOff +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.Button +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.yogeshpaliyal.common.data.AccountModel +import com.yogeshpaliyal.common.utils.PasswordGenerator +import com.yogeshpaliyal.keypass.R +import com.yogeshpaliyal.keypass.ui.redux.actions.GoBackAction +import org.reduxkotlin.compose.rememberDispatcher + +/* +* @author Yogesh Paliyal +* yogeshpaliyal.foss@gmail.com +* https://techpaliyal.com +* created on 31-01-2021 10:38 +*/ + +@Composable +fun AccountDetailPage( + id: Long?, + viewModel: DetailViewModel = hiltViewModel() +) { + val dispatchAction = rememberDispatcher() + + // task value state + val (accountModel, setAccountModel) = remember { + mutableStateOf( + AccountModel() + ) + } + + // Set initial object + LaunchedEffect(key1 = id) { + viewModel.loadAccount(id) { + setAccountModel(it.copy()) + } + } + + val goBack: () -> Unit = { + dispatchAction(GoBackAction) + } + + val launcher = rememberLauncherForActivityResult(QRScanner()) { + it?.let { + setAccountModel(accountModel.copy(password = it)) + } + } + + Scaffold( + bottomBar = { + BottomBar( + accountModel, + backPressed = goBack, + onDeleteAccount = { + viewModel.deleteAccount(accountModel, goBack) + } + ) { + viewModel.insertOrUpdate(accountModel, goBack) + } + } + ) { paddingValues -> + Surface(modifier = Modifier.padding(paddingValues)) { + Fields( + accountModel = accountModel, + updateAccountModel = { newAccountModel -> + setAccountModel(newAccountModel) + } + ) { + launcher.launch(null) + } + } + } +} + +@Composable +fun Fields( + modifier: Modifier = Modifier, + accountModel: AccountModel, + updateAccountModel: (newAccountModel: AccountModel) -> Unit, + scanClicked: () -> Unit +) { + Column( + modifier = modifier + .fillMaxSize() + .padding(16.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + KeyPassInputField( + modifier = Modifier.testTag("accountName"), + placeholder = R.string.account_name, + value = accountModel.title, + setValue = { + updateAccountModel(accountModel.copy(title = it)) + } + ) + + KeyPassInputField( + modifier = Modifier.testTag("username"), + placeholder = R.string.username_email_phone, + value = accountModel.username, + setValue = { + updateAccountModel(accountModel.copy(username = it)) + } + ) + + Column { + val passwordVisible = rememberSaveable { mutableStateOf(false) } + + val visualTransformation = + if (passwordVisible.value) VisualTransformation.None else PasswordVisualTransformation() + + KeyPassInputField( + modifier = Modifier.testTag("password"), + placeholder = R.string.password, + value = accountModel.password, + setValue = { + updateAccountModel(accountModel.copy(password = it)) + }, + trailingIcon = { + PasswordTrailingIcon(passwordVisible.value) { + passwordVisible.value = it + } + }, + leadingIcon = if (accountModel.id != null) { + null + } else ( + { + IconButton( + onClick = { + updateAccountModel(accountModel.copy(password = PasswordGenerator().generatePassword())) + } + ) { + Icon( + painter = rememberVectorPainter(image = Icons.Rounded.Refresh), + contentDescription = "" + ) + } + } + ), + visualTransformation = visualTransformation + ) + Button(onClick = scanClicked) { + Row { + Icon( + painter = painterResource(id = R.drawable.ic_twotone_qr_code_scanner_24), + contentDescription = "" + ) + Text(text = stringResource(id = R.string.scan_password)) + } + } + } + + KeyPassInputField( + modifier = Modifier.testTag("tags"), + placeholder = R.string.tags_comma_separated_optional, + value = accountModel.tags, + setValue = { + updateAccountModel(accountModel.copy(tags = it)) + } + ) + + KeyPassInputField( + modifier = Modifier.testTag("website"), + placeholder = R.string.website_url_optional, + value = accountModel.site, + setValue = { + updateAccountModel(accountModel.copy(site = it)) + } + ) + + KeyPassInputField( + modifier = Modifier.testTag("notes"), + placeholder = R.string.notes_optional, + value = accountModel.notes, + setValue = { + updateAccountModel(accountModel.copy(notes = it)) + } + ) + } +} + +@Composable +fun DeleteConfirmation( + openDialog: Boolean, + updateDialogVisibility: (Boolean) -> Unit, + onDelete: () -> Unit +) { + if (openDialog) { + AlertDialog( + onDismissRequest = { /*TODO*/ }, + title = { + Text(text = stringResource(id = R.string.delete_account_title)) + }, + confirmButton = { + TextButton( + modifier = Modifier.testTag("delete"), + onClick = { + updateDialogVisibility(false) + onDelete() + } + ) { + Text(text = stringResource(id = R.string.delete)) + } + }, + text = { + Text(text = stringResource(id = R.string.delete_account_msg)) + }, + dismissButton = { + TextButton( + onClick = { + updateDialogVisibility(false) + } + ) { + Text(text = stringResource(id = R.string.cancel)) + } + } + ) + } +} + +@Composable +fun KeyPassInputField( + modifier: Modifier = Modifier, + @StringRes placeholder: Int, + value: String?, + setValue: (String) -> Unit, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + visualTransformation: VisualTransformation = VisualTransformation.None +) { + OutlinedTextField( + modifier = modifier.fillMaxWidth(), + value = value ?: "", + label = { + Text(text = stringResource(id = placeholder)) + }, + onValueChange = setValue, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + visualTransformation = visualTransformation + ) +} + +@Composable +fun PasswordTrailingIcon( + passwordVisible: Boolean, + changePasswordVisibility: (updatedValue: Boolean) -> Unit +) { + val description = if (passwordVisible) "Hide password" else "Show password" + + val image = if (passwordVisible) { + Icons.Rounded.Visibility + } else { + Icons.Rounded.VisibilityOff + } + + IconButton(onClick = { changePasswordVisibility(!passwordVisible) }) { + Icon( + painter = rememberVectorPainter(image = image), + contentDescription = description + ) + } +} + +@Composable +fun BottomBar( + accountModel: AccountModel, + backPressed: () -> Unit, + onDeleteAccount: () -> Unit, + onSaveClicked: () -> Unit +) { + val openDialog = remember { mutableStateOf(false) } + + BottomAppBar( + actions = { + IconButton(onClick = backPressed) { + Icon( + painter = rememberVectorPainter(image = Icons.Rounded.ArrowBackIosNew), + contentDescription = "Go Back", + tint = MaterialTheme.colorScheme.onSurface + ) + } + + if (accountModel.id != null) { + IconButton( + modifier = Modifier.testTag("action_delete"), + onClick = { openDialog.value = true } + ) { + Icon( + painter = rememberVectorPainter(image = Icons.Rounded.Delete), + contentDescription = "Delete", + tint = MaterialTheme.colorScheme.onSurface + ) + } + } + }, + floatingActionButton = { + FloatingActionButton(modifier = Modifier.testTag("save"), onClick = onSaveClicked) { + Icon( + painter = rememberVectorPainter(image = Icons.Rounded.Done), + contentDescription = "Save Changes" + ) + } + } + ) + + DeleteConfirmation( + openDialog.value, + updateDialogVisibility = { + openDialog.value = it + }, + onDeleteAccount + ) +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/DetailViewModel.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/DetailViewModel.kt index 0860459b..42eae461 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/DetailViewModel.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/DetailViewModel.kt @@ -1,65 +1,66 @@ -package com.yogeshpaliyal.keypass.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 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( - app: Application, - val appDb: com.yogeshpaliyal.common.AppDatabase -) : AndroidViewModel(app) { - - private val _accountModel by lazy { MutableLiveData() } - val accountModel: LiveData = _accountModel - - fun loadAccount(accountId: Long?, getAccount: (AccountModel) -> Unit) { - viewModelScope.launch(Dispatchers.IO) { - getAccount(appDb.getDao().getAccount(accountId) ?: AccountModel()) - } - } - - fun deleteAccount(accountModel: AccountModel, onExecCompleted: () -> Unit) { - viewModelScope.launch { - accountModel.let { - withContext(Dispatchers.IO) { - appDb.getDao().deleteAccount(it) - } - autoBackup() - onExecCompleted() - } - } - } - - fun insertOrUpdate(accountModel: AccountModel, onExecCompleted: () -> Unit) { - viewModelScope.launch { - accountModel.let { - withContext(Dispatchers.IO) { - appDb.getDao().insertOrUpdateAccount(it) - autoBackup() - } - } - onExecCompleted() - } - } - - private fun autoBackup() { - viewModelScope.launch { - // application.executeAutoBackup() - } - } -} +package com.yogeshpaliyal.keypass.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?, getAccount: (AccountModel) -> Unit) { + viewModelScope.launch(Dispatchers.IO) { + getAccount(appDb.getDao().getAccount(accountId) ?: AccountModel()) + } + } + + fun deleteAccount(accountModel: AccountModel, onExecCompleted: () -> Unit) { + viewModelScope.launch { + accountModel.let { + withContext(Dispatchers.IO) { + appDb.getDao().deleteAccount(it) + } + autoBackup() + onExecCompleted() + } + } + } + + fun insertOrUpdate(accountModel: AccountModel, onExecCompleted: () -> Unit) { + viewModelScope.launch { + accountModel.let { + withContext(Dispatchers.IO) { + appDb.getDao().insertOrUpdateAccount(it) + autoBackup() + } + } + onExecCompleted() + } + } + + private fun autoBackup() { + viewModelScope.launch { + app.executeAutoBackup() + } + } +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/QRScanner.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/QRScanner.kt index f6c05df9..2e15f883 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/QRScanner.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/detail/QRScanner.kt @@ -1,26 +1,26 @@ -package com.yogeshpaliyal.keypass.ui.detail - -import android.app.Activity -import android.content.Context -import android.content.Intent -import androidx.activity.result.contract.ActivityResultContract -import com.google.zxing.integration.android.IntentIntegrator - -class QRScanner : ActivityResultContract() { - override fun createIntent(context: Context, input: Void?): Intent { - val intentIntegration = if (context is Activity) { - IntentIntegrator(context) - } else { - null - } - return intentIntegration?.setPrompt("")?.createScanIntent() ?: Intent() - } - - override fun parseResult(resultCode: Int, result: Intent?): String? { - return IntentIntegrator.parseActivityResult( - IntentIntegrator.REQUEST_CODE, - resultCode, - result - )?.contents - } -} +package com.yogeshpaliyal.keypass.ui.detail + +import android.app.Activity +import android.content.Context +import android.content.Intent +import androidx.activity.result.contract.ActivityResultContract +import com.google.zxing.integration.android.IntentIntegrator + +class QRScanner : ActivityResultContract() { + override fun createIntent(context: Context, input: Void?): Intent { + val intentIntegration = if (context is Activity) { + IntentIntegrator(context) + } else { + null + } + return intentIntegration?.setPrompt("")?.createScanIntent() ?: Intent() + } + + override fun parseResult(resultCode: Int, result: Intent?): String? { + return IntentIntegrator.parseActivityResult( + IntentIntegrator.REQUEST_CODE, + resultCode, + result + )?.contents + } +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/generate/GeneratePasswordActivity.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/generate/GeneratePasswordActivity.kt index 5b15a798..407a7523 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/generate/GeneratePasswordActivity.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/generate/GeneratePasswordActivity.kt @@ -1,23 +1,23 @@ -package com.yogeshpaliyal.keypass.ui.generate - -import android.os.Bundle -import androidx.activity.compose.setContent -import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity -import com.google.accompanist.themeadapter.material3.Mdc3Theme -import com.yogeshpaliyal.keypass.ui.generate.ui.GeneratePasswordScreen -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class GeneratePasswordActivity : AppCompatActivity() { - private val viewModel: GeneratePasswordViewModel by viewModels() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { - Mdc3Theme { - GeneratePasswordScreen(viewModel) - } - } - } -} +package com.yogeshpaliyal.keypass.ui.generate + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import com.google.accompanist.themeadapter.material3.Mdc3Theme +import com.yogeshpaliyal.keypass.ui.generate.ui.GeneratePasswordScreen +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class GeneratePasswordActivity : AppCompatActivity() { + private val viewModel: GeneratePasswordViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + Mdc3Theme { + GeneratePasswordScreen(viewModel) + } + } + } +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/generate/GeneratePasswordViewModel.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/generate/GeneratePasswordViewModel.kt index 130d0c4a..f7a5c166 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/generate/GeneratePasswordViewModel.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/generate/GeneratePasswordViewModel.kt @@ -1,63 +1,63 @@ -package com.yogeshpaliyal.keypass.ui.generate - -import androidx.lifecycle.ViewModel -import com.yogeshpaliyal.common.utils.PasswordGenerator -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import javax.inject.Inject - -@HiltViewModel -class GeneratePasswordViewModel @Inject constructor() : ViewModel() { - - private val _viewState = MutableStateFlow(GeneratePasswordViewState.Initial) - val viewState = _viewState.asStateFlow() - - fun generatePassword() { - val currentViewState = _viewState.value - - val passwordGenerator = PasswordGenerator( - length = currentViewState.length, - includeUpperCaseLetters = currentViewState.includeUppercaseLetters, - includeLowerCaseLetters = currentViewState.includeLowercaseLetters, - includeSymbols = currentViewState.includeSymbols, - includeNumbers = currentViewState.includeNumbers - ) - - _viewState.update { - val newPassword = passwordGenerator.generatePassword() - it.copy(password = newPassword) - } - } - - fun onPasswordLengthSliderChange(value: Float) { - _viewState.update { - it.copy(length = value.toInt()) - } - } - - fun onUppercaseCheckedChange(checked: Boolean) { - _viewState.update { - it.copy(includeUppercaseLetters = checked) - } - } - - fun onLowercaseCheckedChange(checked: Boolean) { - _viewState.update { - it.copy(includeLowercaseLetters = checked) - } - } - - fun onNumbersCheckedChange(checked: Boolean) { - _viewState.update { - it.copy(includeNumbers = checked) - } - } - - fun onSymbolsCheckedChange(checked: Boolean) { - _viewState.update { - it.copy(includeSymbols = checked) - } - } -} +package com.yogeshpaliyal.keypass.ui.generate + +import androidx.lifecycle.ViewModel +import com.yogeshpaliyal.common.utils.PasswordGenerator +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@HiltViewModel +class GeneratePasswordViewModel @Inject constructor() : ViewModel() { + + private val _viewState = MutableStateFlow(GeneratePasswordViewState.Initial) + val viewState = _viewState.asStateFlow() + + fun generatePassword() { + val currentViewState = _viewState.value + + val passwordGenerator = PasswordGenerator( + length = currentViewState.length, + includeUpperCaseLetters = currentViewState.includeUppercaseLetters, + includeLowerCaseLetters = currentViewState.includeLowercaseLetters, + includeSymbols = currentViewState.includeSymbols, + includeNumbers = currentViewState.includeNumbers + ) + + _viewState.update { + val newPassword = passwordGenerator.generatePassword() + it.copy(password = newPassword) + } + } + + fun onPasswordLengthSliderChange(value: Float) { + _viewState.update { + it.copy(length = value.toInt()) + } + } + + fun onUppercaseCheckedChange(checked: Boolean) { + _viewState.update { + it.copy(includeUppercaseLetters = checked) + } + } + + fun onLowercaseCheckedChange(checked: Boolean) { + _viewState.update { + it.copy(includeLowercaseLetters = checked) + } + } + + fun onNumbersCheckedChange(checked: Boolean) { + _viewState.update { + it.copy(includeNumbers = checked) + } + } + + fun onSymbolsCheckedChange(checked: Boolean) { + _viewState.update { + it.copy(includeSymbols = checked) + } + } +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/generate/GeneratePasswordViewState.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/generate/GeneratePasswordViewState.kt index 596d0786..017a5ebb 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/generate/GeneratePasswordViewState.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/generate/GeneratePasswordViewState.kt @@ -1,21 +1,21 @@ -package com.yogeshpaliyal.keypass.ui.generate - -data class GeneratePasswordViewState( - val length: Int, - val includeUppercaseLetters: Boolean, - val includeLowercaseLetters: Boolean, - val includeSymbols: Boolean, - val includeNumbers: Boolean, - val password: String -) { - companion object { - val Initial = GeneratePasswordViewState( - length = 10, - includeUppercaseLetters = true, - includeLowercaseLetters = true, - includeSymbols = true, - includeNumbers = true, - password = "" - ) - } -} +package com.yogeshpaliyal.keypass.ui.generate + +data class GeneratePasswordViewState( + val length: Int, + val includeUppercaseLetters: Boolean, + val includeLowercaseLetters: Boolean, + val includeSymbols: Boolean, + val includeNumbers: Boolean, + val password: String +) { + companion object { + val Initial = GeneratePasswordViewState( + length = 10, + includeUppercaseLetters = true, + includeLowercaseLetters = true, + includeSymbols = true, + includeNumbers = true, + password = "" + ) + } +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/generate/ui/GeneratePasswordContent.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/generate/ui/GeneratePasswordContent.kt index 97224407..f997b2ab 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/generate/ui/GeneratePasswordContent.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/generate/ui/GeneratePasswordContent.kt @@ -1,226 +1,226 @@ -package com.yogeshpaliyal.keypass.ui.generate.ui - -import android.content.res.Configuration -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ContentCopy -import androidx.compose.material.icons.filled.Refresh -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.OutlinedCard -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Slider -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.google.accompanist.themeadapter.material3.Mdc3Theme -import com.yogeshpaliyal.keypass.ui.generate.GeneratePasswordViewState -import com.yogeshpaliyal.keypass.ui.generate.ui.components.CheckboxWithLabel - -@Composable -fun GeneratePasswordContent( - viewState: GeneratePasswordViewState, - onCopyPasswordClick: () -> Unit, - onGeneratePasswordClick: () -> Unit, - onPasswordLengthChange: (Float) -> Unit, - onUppercaseCheckedChange: (Boolean) -> Unit, - onLowercaseCheckedChange: (Boolean) -> Unit, - onNumbersCheckedChange: (Boolean) -> Unit, - onSymbolsCheckedChange: (Boolean) -> Unit -) { - Scaffold( - floatingActionButton = { GeneratePasswordFab(onGeneratePasswordClick) } - ) { innerPadding -> - Column( - modifier = Modifier - .padding(innerPadding) - .fillMaxSize() - .padding(16.dp) - ) { - FormInputCard( - viewState = viewState, - onCopyPasswordClick = onCopyPasswordClick, - onPasswordLengthChange = onPasswordLengthChange, - onUppercaseCheckedChange = onUppercaseCheckedChange, - onLowercaseCheckedChange = onLowercaseCheckedChange, - onNumbersCheckedChange = onNumbersCheckedChange, - onSymbolsCheckedChange = onSymbolsCheckedChange - ) - } - } -} - -@Composable -private fun GeneratePasswordFab(onGeneratePasswordClick: () -> Unit) { - FloatingActionButton( - onClick = onGeneratePasswordClick, - shape = RoundedCornerShape(16.dp) - ) { - Icon( - imageVector = Icons.Default.Refresh, - contentDescription = "" - ) - } -} - -@Composable -private fun FormInputCard( - viewState: GeneratePasswordViewState, - onCopyPasswordClick: () -> Unit, - onPasswordLengthChange: (Float) -> Unit, - onUppercaseCheckedChange: (Boolean) -> Unit, - onLowercaseCheckedChange: (Boolean) -> Unit, - onNumbersCheckedChange: (Boolean) -> Unit, - onSymbolsCheckedChange: (Boolean) -> Unit -) { - OutlinedCard( - colors = CardDefaults.outlinedCardColors(), - shape = RoundedCornerShape(8.dp) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(10.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - PasswordTextField(viewState.password, onCopyPasswordClick) - - // temporary label until we put slider label on the thumb to display current value. - PasswordLengthInput(viewState.length, onPasswordLengthChange) - - UppercaseAlphabetInput(viewState.includeUppercaseLetters, onUppercaseCheckedChange) - - LowercaseAlphabetInput(viewState.includeLowercaseLetters, onLowercaseCheckedChange) - - NumberInput(viewState.includeNumbers, onNumbersCheckedChange) - - SymbolInput(viewState.includeSymbols, onSymbolsCheckedChange) - } - } -} - -@Composable -private fun PasswordTextField( - password: String, - onCopyPasswordClick: () -> Unit -) { - OutlinedTextField( - value = password, - onValueChange = {}, - modifier = Modifier.fillMaxWidth(), - readOnly = true, - trailingIcon = { - IconButton( - onClick = onCopyPasswordClick - ) { - Icon( - imageVector = Icons.Default.ContentCopy, - contentDescription = "Copy password" - ) - } - }, - label = { - Text(text = "Password") - } - ) -} - -@Composable -private fun PasswordLengthInput( - length: Int, - onPasswordLengthChange: (Float) -> Unit -) { - Text(text = "Password Length: $length") - - Slider( - value = length.toFloat(), - onValueChange = onPasswordLengthChange, - valueRange = 7f..50f, - steps = 43 - ) -} - -@Composable -private fun UppercaseAlphabetInput( - includeUppercaseLetters: Boolean, - onUppercaseCheckedChange: (Boolean) -> Unit -) { - CheckboxWithLabel( - label = "Uppercase Alphabets", - checked = includeUppercaseLetters, - onCheckedChange = onUppercaseCheckedChange - ) -} - -@Composable -private fun LowercaseAlphabetInput( - includeLowercaseLetters: Boolean, - onLowercaseCheckedChange: (Boolean) -> Unit -) { - CheckboxWithLabel( - label = "Lowercase Alphabets", - checked = includeLowercaseLetters, - onCheckedChange = onLowercaseCheckedChange - ) -} - -@Composable -private fun NumberInput( - includeNumbers: Boolean, - onNumbersCheckedChange: (Boolean) -> Unit -) { - CheckboxWithLabel( - label = "Numbers", - checked = includeNumbers, - onCheckedChange = onNumbersCheckedChange - ) -} - -@Composable -private fun SymbolInput( - includeSymbols: Boolean, - onSymbolsCheckedChange: (Boolean) -> Unit -) { - CheckboxWithLabel( - label = "Symbols", - checked = includeSymbols, - onCheckedChange = onSymbolsCheckedChange - ) -} - -@Preview( - name = "Night Mode", - uiMode = Configuration.UI_MODE_NIGHT_YES -) -@Preview( - name = "Day Mode", - uiMode = Configuration.UI_MODE_NIGHT_NO -) -@Composable -@Suppress("UnusedPrivateMember", "MagicNumber") -private fun GeneratePasswordContentPreview() { - val viewState = GeneratePasswordViewState.Initial - - Mdc3Theme { - GeneratePasswordContent( - viewState = viewState, - onGeneratePasswordClick = {}, - onCopyPasswordClick = {}, - onPasswordLengthChange = {}, - onUppercaseCheckedChange = {}, - onLowercaseCheckedChange = {}, - onNumbersCheckedChange = {}, - onSymbolsCheckedChange = {} - ) - } -} +package com.yogeshpaliyal.keypass.ui.generate.ui + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ContentCopy +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Slider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.google.accompanist.themeadapter.material3.Mdc3Theme +import com.yogeshpaliyal.keypass.ui.generate.GeneratePasswordViewState +import com.yogeshpaliyal.keypass.ui.generate.ui.components.CheckboxWithLabel + +@Composable +fun GeneratePasswordContent( + viewState: GeneratePasswordViewState, + onCopyPasswordClick: () -> Unit, + onGeneratePasswordClick: () -> Unit, + onPasswordLengthChange: (Float) -> Unit, + onUppercaseCheckedChange: (Boolean) -> Unit, + onLowercaseCheckedChange: (Boolean) -> Unit, + onNumbersCheckedChange: (Boolean) -> Unit, + onSymbolsCheckedChange: (Boolean) -> Unit +) { + Scaffold( + floatingActionButton = { GeneratePasswordFab(onGeneratePasswordClick) } + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + .padding(16.dp) + ) { + FormInputCard( + viewState = viewState, + onCopyPasswordClick = onCopyPasswordClick, + onPasswordLengthChange = onPasswordLengthChange, + onUppercaseCheckedChange = onUppercaseCheckedChange, + onLowercaseCheckedChange = onLowercaseCheckedChange, + onNumbersCheckedChange = onNumbersCheckedChange, + onSymbolsCheckedChange = onSymbolsCheckedChange + ) + } + } +} + +@Composable +private fun GeneratePasswordFab(onGeneratePasswordClick: () -> Unit) { + FloatingActionButton( + onClick = onGeneratePasswordClick, + shape = RoundedCornerShape(16.dp) + ) { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = "" + ) + } +} + +@Composable +private fun FormInputCard( + viewState: GeneratePasswordViewState, + onCopyPasswordClick: () -> Unit, + onPasswordLengthChange: (Float) -> Unit, + onUppercaseCheckedChange: (Boolean) -> Unit, + onLowercaseCheckedChange: (Boolean) -> Unit, + onNumbersCheckedChange: (Boolean) -> Unit, + onSymbolsCheckedChange: (Boolean) -> Unit +) { + OutlinedCard( + colors = CardDefaults.outlinedCardColors(), + shape = RoundedCornerShape(8.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + PasswordTextField(viewState.password, onCopyPasswordClick) + + // temporary label until we put slider label on the thumb to display current value. + PasswordLengthInput(viewState.length, onPasswordLengthChange) + + UppercaseAlphabetInput(viewState.includeUppercaseLetters, onUppercaseCheckedChange) + + LowercaseAlphabetInput(viewState.includeLowercaseLetters, onLowercaseCheckedChange) + + NumberInput(viewState.includeNumbers, onNumbersCheckedChange) + + SymbolInput(viewState.includeSymbols, onSymbolsCheckedChange) + } + } +} + +@Composable +private fun PasswordTextField( + password: String, + onCopyPasswordClick: () -> Unit +) { + OutlinedTextField( + value = password, + onValueChange = {}, + modifier = Modifier.fillMaxWidth(), + readOnly = true, + trailingIcon = { + IconButton( + onClick = onCopyPasswordClick + ) { + Icon( + imageVector = Icons.Default.ContentCopy, + contentDescription = "Copy password" + ) + } + }, + label = { + Text(text = "Password") + } + ) +} + +@Composable +private fun PasswordLengthInput( + length: Int, + onPasswordLengthChange: (Float) -> Unit +) { + Text(text = "Password Length: $length") + + Slider( + value = length.toFloat(), + onValueChange = onPasswordLengthChange, + valueRange = 7f..50f, + steps = 43 + ) +} + +@Composable +private fun UppercaseAlphabetInput( + includeUppercaseLetters: Boolean, + onUppercaseCheckedChange: (Boolean) -> Unit +) { + CheckboxWithLabel( + label = "Uppercase Alphabets", + checked = includeUppercaseLetters, + onCheckedChange = onUppercaseCheckedChange + ) +} + +@Composable +private fun LowercaseAlphabetInput( + includeLowercaseLetters: Boolean, + onLowercaseCheckedChange: (Boolean) -> Unit +) { + CheckboxWithLabel( + label = "Lowercase Alphabets", + checked = includeLowercaseLetters, + onCheckedChange = onLowercaseCheckedChange + ) +} + +@Composable +private fun NumberInput( + includeNumbers: Boolean, + onNumbersCheckedChange: (Boolean) -> Unit +) { + CheckboxWithLabel( + label = "Numbers", + checked = includeNumbers, + onCheckedChange = onNumbersCheckedChange + ) +} + +@Composable +private fun SymbolInput( + includeSymbols: Boolean, + onSymbolsCheckedChange: (Boolean) -> Unit +) { + CheckboxWithLabel( + label = "Symbols", + checked = includeSymbols, + onCheckedChange = onSymbolsCheckedChange + ) +} + +@Preview( + name = "Night Mode", + uiMode = Configuration.UI_MODE_NIGHT_YES +) +@Preview( + name = "Day Mode", + uiMode = Configuration.UI_MODE_NIGHT_NO +) +@Composable +@Suppress("UnusedPrivateMember", "MagicNumber") +private fun GeneratePasswordContentPreview() { + val viewState = GeneratePasswordViewState.Initial + + Mdc3Theme { + GeneratePasswordContent( + viewState = viewState, + onGeneratePasswordClick = {}, + onCopyPasswordClick = {}, + onPasswordLengthChange = {}, + onUppercaseCheckedChange = {}, + onLowercaseCheckedChange = {}, + onNumbersCheckedChange = {}, + onSymbolsCheckedChange = {} + ) + } +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/generate/ui/GeneratePasswordScreen.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/generate/ui/GeneratePasswordScreen.kt index c1d243fe..4a7fa2b6 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/generate/ui/GeneratePasswordScreen.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/generate/ui/GeneratePasswordScreen.kt @@ -1,42 +1,42 @@ -package com.yogeshpaliyal.keypass.ui.generate.ui - -import android.content.Context -import android.widget.Toast -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.platform.LocalContext -import com.yogeshpaliyal.keypass.R -import com.yogeshpaliyal.keypass.ui.generate.GeneratePasswordViewModel -import com.yogeshpaliyal.keypass.ui.generate.ui.utils.copyTextToClipboard - -@Composable -fun GeneratePasswordScreen(viewModel: GeneratePasswordViewModel) { - val context = LocalContext.current - - // replace collectAsState() with collectAsStateWithLifecycle() when compose version and kotlin version are bumped up. - val viewState by viewModel.viewState.collectAsState() - - LaunchedEffect(key1 = Unit) { - viewModel.generatePassword() - } - - GeneratePasswordContent( - viewState = viewState, - onGeneratePasswordClick = viewModel::generatePassword, - onCopyPasswordClick = { onCopyPasswordClick(context, viewState.password) }, - onPasswordLengthChange = viewModel::onPasswordLengthSliderChange, - onUppercaseCheckedChange = viewModel::onUppercaseCheckedChange, - onLowercaseCheckedChange = viewModel::onLowercaseCheckedChange, - onNumbersCheckedChange = viewModel::onNumbersCheckedChange, - onSymbolsCheckedChange = viewModel::onSymbolsCheckedChange - ) -} - -private fun onCopyPasswordClick(context: Context, text: String) { - copyTextToClipboard(context = context, text = text, label = "random_password") - Toast - .makeText(context, context.getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT) - .show() -} +package com.yogeshpaliyal.keypass.ui.generate.ui + +import android.content.Context +import android.widget.Toast +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.LocalContext +import com.yogeshpaliyal.keypass.R +import com.yogeshpaliyal.keypass.ui.generate.GeneratePasswordViewModel +import com.yogeshpaliyal.keypass.ui.generate.ui.utils.copyTextToClipboard + +@Composable +fun GeneratePasswordScreen(viewModel: GeneratePasswordViewModel) { + val context = LocalContext.current + + // replace collectAsState() with collectAsStateWithLifecycle() when compose version and kotlin version are bumped up. + val viewState by viewModel.viewState.collectAsState() + + LaunchedEffect(key1 = Unit) { + viewModel.generatePassword() + } + + GeneratePasswordContent( + viewState = viewState, + onGeneratePasswordClick = viewModel::generatePassword, + onCopyPasswordClick = { onCopyPasswordClick(context, viewState.password) }, + onPasswordLengthChange = viewModel::onPasswordLengthSliderChange, + onUppercaseCheckedChange = viewModel::onUppercaseCheckedChange, + onLowercaseCheckedChange = viewModel::onLowercaseCheckedChange, + onNumbersCheckedChange = viewModel::onNumbersCheckedChange, + onSymbolsCheckedChange = viewModel::onSymbolsCheckedChange + ) +} + +private fun onCopyPasswordClick(context: Context, text: String) { + copyTextToClipboard(context = context, text = text, label = "random_password") + Toast + .makeText(context, context.getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT) + .show() +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/generate/ui/components/CheckboxWithLabel.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/generate/ui/components/CheckboxWithLabel.kt index 9650f93e..ab22ff29 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/generate/ui/components/CheckboxWithLabel.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/generate/ui/components/CheckboxWithLabel.kt @@ -1,32 +1,32 @@ -package com.yogeshpaliyal.keypass.ui.generate.ui.components - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.Checkbox -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier - -@Composable -fun CheckboxWithLabel( - label: String, - checked: Boolean, - onCheckedChange: (Boolean) -> Unit, - modifier: Modifier = Modifier -) { - Row( - modifier = modifier - .fillMaxWidth() - .clickable { onCheckedChange(!checked) }, - verticalAlignment = Alignment.CenterVertically - ) { - Checkbox( - checked = checked, - onCheckedChange = onCheckedChange - ) - - Text(text = label) - } -} +package com.yogeshpaliyal.keypass.ui.generate.ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Checkbox +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +fun CheckboxWithLabel( + label: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .fillMaxWidth() + .clickable { onCheckedChange(!checked) }, + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = checked, + onCheckedChange = onCheckedChange + ) + + Text(text = label) + } +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/generate/ui/utils/CopyTextToClipboard.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/generate/ui/utils/CopyTextToClipboard.kt index af1fda32..8164a73b 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/generate/ui/utils/CopyTextToClipboard.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/generate/ui/utils/CopyTextToClipboard.kt @@ -1,17 +1,17 @@ -package com.yogeshpaliyal.keypass.ui.generate.ui.utils - -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import androidx.appcompat.app.AppCompatActivity - -fun copyTextToClipboard( - context: Context, - text: String, - label: String -) { - val clipboard = - context.getSystemService(AppCompatActivity.CLIPBOARD_SERVICE) as ClipboardManager - val clip = ClipData.newPlainText(label, text) - clipboard.setPrimaryClip(clip) -} +package com.yogeshpaliyal.keypass.ui.generate.ui.utils + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import androidx.appcompat.app.AppCompatActivity + +fun copyTextToClipboard( + context: Context, + text: String, + label: String +) { + val clipboard = + context.getSystemService(AppCompatActivity.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText(label, text) + clipboard.setPrimaryClip(clip) +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/DashboardViewModel.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/DashboardViewModel.kt index 5b9a2ff6..6b38b0f2 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/DashboardViewModel.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/DashboardViewModel.kt @@ -1,55 +1,55 @@ -package com.yogeshpaliyal.keypass.ui.home - -import android.app.Application -import android.content.ContentResolver -import android.net.Uri -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.viewModelScope -import com.yogeshpaliyal.common.data.AccountModel -import com.yogeshpaliyal.common.dbhelper.restoreBackup -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -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) { - - private val appDao = appDb.getDao() - - val mediator = MediatorLiveData>() - - fun queryUpdated( - keyword: String?, - tag: String?, - sortField: String?, - sortAscending: Boolean = true - ) { - viewModelScope.launch(Dispatchers.IO) { - if (sortAscending) { - mediator.postValue(appDao.getAllAccountsAscending(keyword ?: "", tag, sortField)) - } else { - mediator.postValue(appDao.getAllAccountsDescending(keyword ?: "", tag, sortField)) - } - } - } - - suspend fun restoreBackup( - keyphrase: String, - contentResolver: ContentResolver, - fileUri: Uri? - ): Boolean { - return appDb.restoreBackup(keyphrase, contentResolver, fileUri) - } -} +package com.yogeshpaliyal.keypass.ui.home + +import android.app.Application +import android.content.ContentResolver +import android.net.Uri +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.viewModelScope +import com.yogeshpaliyal.common.data.AccountModel +import com.yogeshpaliyal.common.dbhelper.restoreBackup +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +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) { + + private val appDao = appDb.getDao() + + val mediator = MediatorLiveData>() + + fun queryUpdated( + keyword: String?, + tag: String?, + sortField: String?, + sortAscending: Boolean = true + ) { + viewModelScope.launch(Dispatchers.IO) { + if (sortAscending) { + mediator.postValue(appDao.getAllAccountsAscending(keyword ?: "", tag, sortField)) + } else { + mediator.postValue(appDao.getAllAccountsDescending(keyword ?: "", tag, sortField)) + } + } + } + + suspend fun restoreBackup( + keyphrase: String, + contentResolver: ContentResolver, + fileUri: Uri? + ): Boolean { + return appDb.restoreBackup(keyphrase, contentResolver, fileUri) + } +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/Homepage.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/Homepage.kt index 8f92a013..4d4f9963 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/Homepage.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/Homepage.kt @@ -1,96 +1,96 @@ -package com.yogeshpaliyal.keypass.ui.home - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Close -import androidx.compose.material3.AssistChip -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.rememberVectorPainter -import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel -import com.yogeshpaliyal.keypass.ui.home.components.AccountsList -import com.yogeshpaliyal.keypass.ui.home.components.SearchBar -import com.yogeshpaliyal.keypass.ui.redux.HomeState -import com.yogeshpaliyal.keypass.ui.redux.NavigationAction -import com.yogeshpaliyal.keypass.ui.redux.StateUpdateAction -import org.reduxkotlin.compose.rememberDispatcher - -/* -* @author Yogesh Paliyal -* techpaliyal@gmail.com -* https://techpaliyal.com -* created on 31-01-2021 09:25 -*/ - -@Composable() -fun Homepage( - mViewModel: DashboardViewModel = viewModel(), - homeState: HomeState -) { - val tag = homeState.tag - val keyword = homeState.keyword - val sortField = homeState.sortField - val sortAscendingOrder = homeState.sortAscending - - val listOfAccountsLiveData by mViewModel.mediator.observeAsState() - - val dispatchAction = rememberDispatcher() - - LaunchedEffect(tag, keyword, sortField, sortAscendingOrder, block = { - mViewModel.queryUpdated(keyword, tag, sortField, sortAscendingOrder) - }) - - Column(modifier = Modifier.fillMaxSize()) { - SearchBar(keyword, { - dispatchAction(StateUpdateAction(homeState.copy(keyword = it))) - }) { field, order -> - dispatchAction( - - StateUpdateAction( - homeState.copy( - sortField = field.value, - sortAscending = order == SortingOrder.Ascending - ) - ) - - ) - } - - AnimatedVisibility(tag != null) { - LazyRow( - modifier = Modifier.padding(vertical = 8.dp), - contentPadding = PaddingValues(horizontal = 16.dp), - content = { - item { - AssistChip(onClick = { }, label = { - Text(text = tag ?: "") - }, trailingIcon = { - IconButton(onClick = { - dispatchAction(NavigationAction(HomeState(), true)) - }) { - Icon( - painter = rememberVectorPainter(image = Icons.Rounded.Close), - contentDescription = "" - ) - } - }) - } - } - ) - } - - AccountsList(listOfAccountsLiveData) - } -} +package com.yogeshpaliyal.keypass.ui.home + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material3.AssistChip +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.yogeshpaliyal.keypass.ui.home.components.AccountsList +import com.yogeshpaliyal.keypass.ui.home.components.SearchBar +import com.yogeshpaliyal.keypass.ui.redux.actions.NavigationAction +import com.yogeshpaliyal.keypass.ui.redux.actions.StateUpdateAction +import com.yogeshpaliyal.keypass.ui.redux.states.HomeState +import org.reduxkotlin.compose.rememberDispatcher + +/* +* @author Yogesh Paliyal +* techpaliyal@gmail.com +* https://techpaliyal.com +* created on 31-01-2021 09:25 +*/ + +@Composable() +fun Homepage( + mViewModel: DashboardViewModel = viewModel(), + homeState: HomeState +) { + val tag = homeState.tag + val keyword = homeState.keyword + val sortField = homeState.sortField + val sortAscendingOrder = homeState.sortAscending + + val listOfAccountsLiveData by mViewModel.mediator.observeAsState() + + val dispatchAction = rememberDispatcher() + + LaunchedEffect(tag, keyword, sortField, sortAscendingOrder, block = { + mViewModel.queryUpdated(keyword, tag, sortField, sortAscendingOrder) + }) + + Column(modifier = Modifier.fillMaxSize()) { + SearchBar(keyword, { + dispatchAction(StateUpdateAction(homeState.copy(keyword = it))) + }) { field, order -> + dispatchAction( + + StateUpdateAction( + homeState.copy( + sortField = field.value, + sortAscending = order == SortingOrder.Ascending + ) + ) + + ) + } + + AnimatedVisibility(tag != null) { + LazyRow( + modifier = Modifier.padding(vertical = 8.dp), + contentPadding = PaddingValues(horizontal = 16.dp), + content = { + item { + AssistChip(onClick = { }, label = { + Text(text = tag ?: "") + }, trailingIcon = { + IconButton(onClick = { + dispatchAction(NavigationAction(HomeState(), true)) + }) { + Icon( + painter = rememberVectorPainter(image = Icons.Rounded.Close), + contentDescription = "" + ) + } + }) + } + } + ) + } + + AccountsList(listOfAccountsLiveData) + } +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/SortingOrders.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/SortingOrders.kt index a51995d0..e6694f6f 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/SortingOrders.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/SortingOrders.kt @@ -1,20 +1,20 @@ -package com.yogeshpaliyal.keypass.ui.home - -import androidx.annotation.StringRes -import com.yogeshpaliyal.keypass.R - -sealed class SortingOrder(@StringRes val label: Int) { - object Ascending : SortingOrder(R.string.ascending) - object Descending : SortingOrder(R.string.descending) -} - -fun getSortingOrderOptions() = mutableListOf(SortingOrder.Ascending, SortingOrder.Descending) - -sealed class SortingField( - @StringRes val label: Int, - val value: String, - val sortingOrders: List -) { - object Title : SortingField(R.string.account_name, "title", getSortingOrderOptions()) - object Username : SortingField(R.string.username_email_phone, "username", getSortingOrderOptions()) -} +package com.yogeshpaliyal.keypass.ui.home + +import androidx.annotation.StringRes +import com.yogeshpaliyal.keypass.R + +sealed class SortingOrder(@StringRes val label: Int) { + object Ascending : SortingOrder(R.string.ascending) + object Descending : SortingOrder(R.string.descending) +} + +fun getSortingOrderOptions() = mutableListOf(SortingOrder.Ascending, SortingOrder.Descending) + +sealed class SortingField( + @StringRes val label: Int, + val value: String, + val sortingOrders: List +) { + object Title : SortingField(R.string.account_name, "title", getSortingOrderOptions()) + object Username : SortingField(R.string.username_email_phone, "username", getSortingOrderOptions()) +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/components/AccountsList.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/components/AccountsList.kt index f7e85e5f..375d1492 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/components/AccountsList.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/components/AccountsList.kt @@ -1,242 +1,242 @@ -package com.yogeshpaliyal.keypass.ui.home.components - -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.tween -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.twotone.ContentCopy -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.rememberVectorPainter -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.yogeshpaliyal.common.constants.AccountType -import com.yogeshpaliyal.common.data.AccountModel -import com.yogeshpaliyal.keypass.R -import com.yogeshpaliyal.keypass.ui.redux.AccountDetailState -import com.yogeshpaliyal.keypass.ui.redux.CopyToClipboard -import com.yogeshpaliyal.keypass.ui.redux.IntentNavigation -import com.yogeshpaliyal.keypass.ui.redux.NavigationAction -import kotlinx.coroutines.delay -import org.reduxkotlin.compose.rememberDispatcher -import kotlin.time.Duration.Companion.seconds - -@OptIn(ExperimentalFoundationApi::class, ExperimentalAnimationApi::class) -@Composable -fun AccountsList(accounts: List? = null) { - val dispatch = rememberDispatcher() - - if (accounts?.isNotEmpty() == true) { - AnimatedContent(targetState = accounts) { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - items(it) { account -> - Account( - modifier = Modifier.animateItemPlacement(), - account, - onClick = { - if (it.type == AccountType.TOTP) { - dispatch(IntentNavigation.AddTOTP(it.uniqueId)) - } else { - dispatch(NavigationAction(AccountDetailState(it.id))) - } - } - ) - } - item { - Spacer(modifier = Modifier.height(8.dp)) - } - } - } - } else { - NoDataFound() - } -} - -@Composable -fun Account( - modifier: Modifier, - accountModel: AccountModel, - onClick: (AccountModel) -> Unit -) { - val dispatch = rememberDispatcher() - - Card( - modifier = modifier, - elevation = CardDefaults.cardElevation(defaultElevation = 1.dp), - onClick = { onClick(accountModel) } - ) { - Row(modifier = Modifier.padding(12.dp)) { - Box( - modifier = Modifier - .align(Alignment.CenterVertically) - .size(60.dp) - .background( - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.2f), - shape = RoundedCornerShape(50) - ) - ) { - Text( - modifier = Modifier.align(Alignment.Center), - text = accountModel.getInitials(), - textAlign = TextAlign.Center - ) - - if (accountModel.type == AccountType.TOTP) { - WrapWithProgress(accountModel) - } - } - - Column( - modifier = Modifier - .weight(1f) - .align(Alignment.CenterVertically) - .padding(horizontal = 16.dp) - ) { - Text( - text = accountModel.title ?: "", - style = MaterialTheme.typography.headlineSmall.merge( - TextStyle( - fontSize = 16.sp - ) - ) - ) - - RenderUserName(accountModel) - } - - IconButton( - modifier = Modifier.align(Alignment.CenterVertically), - onClick = { dispatch(CopyToClipboard(getPassword(accountModel))) } - ) { - Icon( - painter = rememberVectorPainter(image = Icons.TwoTone.ContentCopy), - contentDescription = "Copy To Clipboard" - ) - } - } - } -} - -private fun getUsernameOrOtp(accountModel: AccountModel): String? { - return if (accountModel.type == AccountType.TOTP) accountModel.getOtp() else accountModel.username -} - -@Composable -fun RenderUserName(accountModel: AccountModel) { - val (username, setUsername) = remember { mutableStateOf("") } - - LaunchedEffect(accountModel) { - if (accountModel.type == AccountType.TOTP) { - while (true) { - setUsername(accountModel.getOtp()) - delay(1.seconds) - } - } else { - setUsername(accountModel.username ?: "") - } - } - - Text( - text = username, - style = MaterialTheme.typography.bodyMedium.merge( - TextStyle( - fontSize = 14.sp - ) - ) - ) -} - -@Composable -fun NoDataFound() { - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.Bottom - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .weight(1f), - verticalArrangement = Arrangement.Center - ) { - Text( - text = stringResource(id = R.string.message_no_accounts), - modifier = Modifier - .padding(32.dp) - .align(alignment = Alignment.CenterHorizontally), - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.Center - ) - } - Image( - painter = painterResource(R.drawable.ic_undraw_empty_street_sfxm), - contentDescription = "" - ) - } -} - -private fun getPassword(model: AccountModel): String { - if (model.type == AccountType.TOTP) { - return model.getOtp() - } - return model.password.orEmpty() -} - -@Composable -fun WrapWithProgress(accountModel: AccountModel) { - if (accountModel.type != AccountType.TOTP) { - return - } - - val infiniteTransition = - rememberInfiniteTransition(accountModel.uniqueId ?: accountModel.title ?: "") - val rotationAnimation = infiniteTransition.animateFloat( - initialValue = 1f - (accountModel.getTOtpProgress().toFloat() / 30), - targetValue = 1f, - animationSpec = infiniteRepeatable(tween(30000, easing = LinearEasing)) - ) - - CircularProgressIndicator( - modifier = Modifier.fillMaxSize(), - progress = rotationAnimation.value - ) -} +package com.yogeshpaliyal.keypass.ui.home.components + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.twotone.ContentCopy +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.yogeshpaliyal.common.constants.AccountType +import com.yogeshpaliyal.common.data.AccountModel +import com.yogeshpaliyal.keypass.R +import com.yogeshpaliyal.keypass.ui.redux.actions.CopyToClipboard +import com.yogeshpaliyal.keypass.ui.redux.actions.IntentNavigation +import com.yogeshpaliyal.keypass.ui.redux.actions.NavigationAction +import com.yogeshpaliyal.keypass.ui.redux.states.AccountDetailState +import kotlinx.coroutines.delay +import org.reduxkotlin.compose.rememberDispatcher +import kotlin.time.Duration.Companion.seconds + +@OptIn(ExperimentalFoundationApi::class, ExperimentalAnimationApi::class) +@Composable +fun AccountsList(accounts: List? = null) { + val dispatch = rememberDispatcher() + + if (accounts?.isNotEmpty() == true) { + AnimatedContent(targetState = accounts) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + items(it) { account -> + Account( + modifier = Modifier.animateItemPlacement(), + account, + onClick = { + if (it.type == AccountType.TOTP) { + dispatch(IntentNavigation.AddTOTP(it.uniqueId)) + } else { + dispatch(NavigationAction(AccountDetailState(it.id))) + } + } + ) + } + item { + Spacer(modifier = Modifier.height(8.dp)) + } + } + } + } else { + NoDataFound() + } +} + +@Composable +fun Account( + modifier: Modifier, + accountModel: AccountModel, + onClick: (AccountModel) -> Unit +) { + val dispatch = rememberDispatcher() + + Card( + modifier = modifier, + elevation = CardDefaults.cardElevation(defaultElevation = 1.dp), + onClick = { onClick(accountModel) } + ) { + Row(modifier = Modifier.padding(12.dp)) { + Box( + modifier = Modifier + .align(Alignment.CenterVertically) + .size(60.dp) + .background( + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.2f), + shape = RoundedCornerShape(50) + ) + ) { + Text( + modifier = Modifier.align(Alignment.Center), + text = accountModel.getInitials(), + textAlign = TextAlign.Center + ) + + if (accountModel.type == AccountType.TOTP) { + WrapWithProgress(accountModel) + } + } + + Column( + modifier = Modifier + .weight(1f) + .align(Alignment.CenterVertically) + .padding(horizontal = 16.dp) + ) { + Text( + text = accountModel.title ?: "", + style = MaterialTheme.typography.headlineSmall.merge( + TextStyle( + fontSize = 16.sp + ) + ) + ) + + RenderUserName(accountModel) + } + + IconButton( + modifier = Modifier.align(Alignment.CenterVertically), + onClick = { dispatch(CopyToClipboard(getPassword(accountModel))) } + ) { + Icon( + painter = rememberVectorPainter(image = Icons.TwoTone.ContentCopy), + contentDescription = "Copy To Clipboard" + ) + } + } + } +} + +private fun getUsernameOrOtp(accountModel: AccountModel): String? { + return if (accountModel.type == AccountType.TOTP) accountModel.getOtp() else accountModel.username +} + +@Composable +fun RenderUserName(accountModel: AccountModel) { + val (username, setUsername) = remember { mutableStateOf("") } + + LaunchedEffect(accountModel) { + if (accountModel.type == AccountType.TOTP) { + while (true) { + setUsername(accountModel.getOtp()) + delay(1.seconds) + } + } else { + setUsername(accountModel.username ?: "") + } + } + + Text( + text = username, + style = MaterialTheme.typography.bodyMedium.merge( + TextStyle( + fontSize = 14.sp + ) + ) + ) +} + +@Composable +fun NoDataFound() { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Bottom + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + verticalArrangement = Arrangement.Center + ) { + Text( + text = stringResource(id = R.string.message_no_accounts), + modifier = Modifier + .padding(32.dp) + .align(alignment = Alignment.CenterHorizontally), + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center + ) + } + Image( + painter = painterResource(R.drawable.ic_undraw_empty_street_sfxm), + contentDescription = "" + ) + } +} + +private fun getPassword(model: AccountModel): String { + if (model.type == AccountType.TOTP) { + return model.getOtp() + } + return model.password.orEmpty() +} + +@Composable +fun WrapWithProgress(accountModel: AccountModel) { + if (accountModel.type != AccountType.TOTP) { + return + } + + val infiniteTransition = + rememberInfiniteTransition(accountModel.uniqueId ?: accountModel.title ?: "") + val rotationAnimation = infiniteTransition.animateFloat( + initialValue = 1f - (accountModel.getTOtpProgress().toFloat() / 30), + targetValue = 1f, + animationSpec = infiniteRepeatable(tween(30000, easing = LinearEasing)) + ) + + CircularProgressIndicator( + modifier = Modifier.fillMaxSize(), + progress = rotationAnimation.value + ) +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/components/SearchBar.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/components/SearchBar.kt index cbbf89d4..dbaca1a3 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/components/SearchBar.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/components/SearchBar.kt @@ -1,69 +1,69 @@ -package com.yogeshpaliyal.keypass.ui.home.components - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Close -import androidx.compose.material.icons.rounded.Sort -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.rememberVectorPainter -import androidx.compose.ui.unit.dp -import com.yogeshpaliyal.keypass.ui.home.SortingField -import com.yogeshpaliyal.keypass.ui.home.SortingOrder - -@Composable -fun SearchBar( - keyword: String?, - updateKeyword: (keyword: String) -> Unit, - updateSorting: (SortingField, SortingOrder) -> Unit -) { - val (isMenuVisible, setMenuVisible) = rememberSaveable { mutableStateOf(false) } - - OutlinedTextField( - modifier = Modifier - .fillMaxWidth(1f) - .padding(16.dp), - value = keyword ?: "", - placeholder = { - Text(text = "Search Account") - }, - onValueChange = { newValue -> - updateKeyword(newValue) - }, - trailingIcon = { - Row { - AnimatedVisibility(keyword.isNullOrBlank().not()) { - IconButton(onClick = { updateKeyword("") }) { - Icon( - painter = rememberVectorPainter(image = Icons.Rounded.Close), - contentDescription = "" - ) - } - } - - IconButton(onClick = { - setMenuVisible(!isMenuVisible) - }) { - Icon( - painter = rememberVectorPainter(image = Icons.Rounded.Sort), - contentDescription = "" - ) - } - } - - SortingMenu(isMenuVisible, setMenuVisible) { sortingField, order -> - updateSorting(sortingField, order) - setMenuVisible(false) - } - } - ) -} +package com.yogeshpaliyal.keypass.ui.home.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material.icons.rounded.Sort +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.unit.dp +import com.yogeshpaliyal.keypass.ui.home.SortingField +import com.yogeshpaliyal.keypass.ui.home.SortingOrder + +@Composable +fun SearchBar( + keyword: String?, + updateKeyword: (keyword: String) -> Unit, + updateSorting: (SortingField, SortingOrder) -> Unit +) { + val (isMenuVisible, setMenuVisible) = rememberSaveable { mutableStateOf(false) } + + OutlinedTextField( + modifier = Modifier + .fillMaxWidth(1f) + .padding(16.dp), + value = keyword ?: "", + placeholder = { + Text(text = "Search Account") + }, + onValueChange = { newValue -> + updateKeyword(newValue) + }, + trailingIcon = { + Row { + AnimatedVisibility(keyword.isNullOrBlank().not()) { + IconButton(onClick = { updateKeyword("") }) { + Icon( + painter = rememberVectorPainter(image = Icons.Rounded.Close), + contentDescription = "" + ) + } + } + + IconButton(onClick = { + setMenuVisible(!isMenuVisible) + }) { + Icon( + painter = rememberVectorPainter(image = Icons.Rounded.Sort), + contentDescription = "" + ) + } + } + + SortingMenu(isMenuVisible, setMenuVisible) { sortingField, order -> + updateSorting(sortingField, order) + setMenuVisible(false) + } + } + ) +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/components/SortingMenu.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/components/SortingMenu.kt index 29141775..981bcd04 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/components/SortingMenu.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/home/components/SortingMenu.kt @@ -1,45 +1,45 @@ -package com.yogeshpaliyal.keypass.ui.home.components - -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.res.stringResource -import com.yogeshpaliyal.keypass.ui.home.SortingField -import com.yogeshpaliyal.keypass.ui.home.SortingOrder -import me.saket.cascade.CascadeDropdownMenu -import me.saket.cascade.rememberCascadeState - -@Composable -fun SortingMenu( - isMenuVisible: Boolean, - setMenuVisible: (Boolean) -> Unit, - onOptionSelected: (SortingField, SortingOrder) -> Unit -) { - val state = rememberCascadeState() - - val sortingOptions = - remember { mutableListOf(SortingField.Title, SortingField.Username) } - - CascadeDropdownMenu( - state = state, - expanded = isMenuVisible, - onDismissRequest = { setMenuVisible(false) } - ) { - sortingOptions.forEach { sortingField -> - DropdownMenuItem( - text = { Text(stringResource(id = sortingField.label)) }, - children = { - sortingField.sortingOrders.forEach { - DropdownMenuItem( - text = { Text(stringResource(id = it.label)) }, - onClick = { - onOptionSelected(sortingField, it) - } - ) - } - } - ) - } - } -} +package com.yogeshpaliyal.keypass.ui.home.components + +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.res.stringResource +import com.yogeshpaliyal.keypass.ui.home.SortingField +import com.yogeshpaliyal.keypass.ui.home.SortingOrder +import me.saket.cascade.CascadeDropdownMenu +import me.saket.cascade.rememberCascadeState + +@Composable +fun SortingMenu( + isMenuVisible: Boolean, + setMenuVisible: (Boolean) -> Unit, + onOptionSelected: (SortingField, SortingOrder) -> Unit +) { + val state = rememberCascadeState() + + val sortingOptions = + remember { mutableListOf(SortingField.Title, SortingField.Username) } + + CascadeDropdownMenu( + state = state, + expanded = isMenuVisible, + onDismissRequest = { setMenuVisible(false) } + ) { + sortingOptions.forEach { sortingField -> + DropdownMenuItem( + text = { Text(stringResource(id = sortingField.label)) }, + children = { + sortingField.sortingOrders.forEach { + DropdownMenuItem( + text = { Text(stringResource(id = it.label)) }, + onClick = { + onOptionSelected(sortingField, it) + } + ) + } + } + ) + } + } +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/BottomNavViewModel.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/BottomNavViewModel.kt index d475d750..a3cb127f 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/BottomNavViewModel.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/BottomNavViewModel.kt @@ -1,72 +1,72 @@ -package com.yogeshpaliyal.keypass.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.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.NavTagItem(it) } ?: listOf() - ) - } else { - NavigationModel.navigationMenuItems - } - - _navigationList.value = newList - } -} +package com.yogeshpaliyal.keypass.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.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.NavTagItem(it) } ?: listOf() + ) + } else { + NavigationModel.navigationMenuItems + } + + _navigationList.value = newList + } +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/DashboardComposeActivity.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/DashboardComposeActivity.kt index e990f1a5..7b8933e3 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/DashboardComposeActivity.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/DashboardComposeActivity.kt @@ -1,305 +1,312 @@ -package com.yogeshpaliyal.keypass.ui.nav - -import android.os.Bundle -import android.view.WindowManager -import androidx.activity.compose.BackHandler -import androidx.activity.compose.setContent -import androidx.appcompat.app.AppCompatActivity -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Add -import androidx.compose.material.icons.rounded.Menu -import androidx.compose.material.icons.rounded.Settings -import androidx.compose.material.ripple.rememberRipple -import androidx.compose.material3.BottomAppBar -import androidx.compose.material3.Divider -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.rememberVectorPainter -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.TextUnit -import androidx.compose.ui.unit.TextUnitType -import androidx.compose.ui.unit.dp -import com.yogeshpaliyal.keypass.BuildConfig -import com.yogeshpaliyal.keypass.ui.auth.AuthScreen -import com.yogeshpaliyal.keypass.ui.detail.AccountDetailPage -import com.yogeshpaliyal.keypass.ui.home.Homepage -import com.yogeshpaliyal.keypass.ui.redux.AccountDetailState -import com.yogeshpaliyal.keypass.ui.redux.AuthState -import com.yogeshpaliyal.keypass.ui.redux.BottomSheetAction -import com.yogeshpaliyal.keypass.ui.redux.BottomSheetState -import com.yogeshpaliyal.keypass.ui.redux.GoBackAction -import com.yogeshpaliyal.keypass.ui.redux.HomeState -import com.yogeshpaliyal.keypass.ui.redux.KeyPassRedux -import com.yogeshpaliyal.keypass.ui.redux.KeyPassState -import com.yogeshpaliyal.keypass.ui.redux.NavigationAction -import com.yogeshpaliyal.keypass.ui.redux.ScreenState -import com.yogeshpaliyal.keypass.ui.redux.SettingsState -import com.yogeshpaliyal.keypass.ui.redux.TotpDetailState -import com.yogeshpaliyal.keypass.ui.redux.UpdateContextAction -import com.yogeshpaliyal.keypass.ui.settings.MySettingCompose -import com.yogeshpaliyal.keypass.ui.style.KeyPassTheme -import dagger.hilt.android.AndroidEntryPoint -import org.reduxkotlin.compose.StoreProvider -import org.reduxkotlin.compose.rememberDispatcher -import org.reduxkotlin.compose.selectState -import java.util.Locale - -@AndroidEntryPoint -class DashboardComposeActivity : AppCompatActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - if (BuildConfig.DEBUG.not()) { - window.setFlags( - WindowManager.LayoutParams.FLAG_SECURE, - WindowManager.LayoutParams.FLAG_SECURE - ) - } - setContent { - KeyPassTheme { - StoreProvider(store = KeyPassRedux.createStore()) { - Dashboard() - } - } - } - } -} - -@Composable -fun Dashboard() { - val systemBackPress by selectState { this.systemBackPress } - - val context = LocalContext.current - val dispatch = rememberDispatcher() - - BackHandler(!systemBackPress) { - dispatch(GoBackAction) - } - - LaunchedEffect(key1 = systemBackPress, block = { - if (systemBackPress) { - (context as? AppCompatActivity)?.onBackPressed() - } - }) - - DisposableEffect(KeyPassRedux, context) { - dispatch(UpdateContextAction(context)) - - onDispose { - dispatch(UpdateContextAction(null)) - } - } - - Scaffold(bottomBar = { - KeyPassBottomBar() - }) { paddingValues -> - Surface(modifier = Modifier.padding(paddingValues)) { - CurrentPage() - - OptionBottomBar() - } - } -} - -@Composable -fun CurrentPage() { - val currentScreen by selectState { this.currentScreen } - - currentScreen.let { - when (it) { - is HomeState -> { - Homepage(homeState = it) - } - - is SettingsState -> { - MySettingCompose() - } - - is AccountDetailState -> { - AccountDetailPage(id = it.accountId) - } - - is AuthState -> { - AuthScreen(it) - } - - is TotpDetailState -> { - } - } - } -} - -@Composable -fun OptionBottomBar( - viewModel: BottomNavViewModel = androidx.lifecycle.viewmodel.compose.viewModel() -) { - val bottomSheetState by selectState { this.bottomSheet } - - if (bottomSheetState?.isBottomSheetOpen != true) { - return - } - - val dispatchAction = rememberDispatcher() - - val navigationItems by viewModel.navigationList.observeAsState() - ModalBottomSheet(onDismissRequest = { - dispatchAction(BottomSheetAction.HomeNavigationMenu(false)) - }) { - LazyColumn( - modifier = Modifier - .fillMaxWidth(1f) - .padding(16.dp) - ) { - if (navigationItems != null) { - items(navigationItems!!) { - when (it) { - is NavigationModelItem.NavDivider -> { - NavItemSection(it) - } - - is NavigationModelItem.NavTagItem -> { - NavMenuFolder(folder = it) { - dispatchAction(NavigationAction(HomeState(tag = it.tag), false)) - dispatchAction(BottomSheetAction.HomeNavigationMenu(false)) - } - } - - is NavigationModelItem.NavMenuItem -> { - NavItem(it) { - dispatchAction(it.action) - dispatchAction(BottomSheetAction.HomeNavigationMenu(false)) - } - } - } - } - } - } - } -} - -@Composable -fun NavMenuFolder(folder: NavigationModelItem.NavTagItem, onClick: () -> Unit) { - Box( - modifier = Modifier - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple( - bounded = true - ), - onClick = onClick - ) - .padding(16.dp) - .fillMaxWidth(1f) - - ) { - Text( - text = folder.tag, - style = MaterialTheme.typography.titleMedium - ) - } -} - -@Composable -fun NavItem(item: NavigationModelItem.NavMenuItem, onClick: () -> Unit) { - Row( - modifier = Modifier - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = true), - onClick = onClick - ) - .padding(16.dp) - .fillMaxWidth(1f) - ) { - Icon( - painter = painterResource(id = item.icon), - contentDescription = "" - ) - Spacer(modifier = Modifier.width(32.dp)) - Text( - text = stringResource(id = item.titleRes), - style = MaterialTheme.typography.titleMedium - ) - } -} - -@Composable -fun NavItemSection(divider: NavigationModelItem.NavDivider) { - Column(modifier = Modifier.padding(16.dp)) { - Divider() - Spacer(modifier = Modifier.height(32.dp)) - Text( - text = divider.title.uppercase(Locale.getDefault()), - style = MaterialTheme.typography.labelMedium, - fontSize = TextUnit(12f, TextUnitType.Sp) - ) - } -} - -@Composable -fun KeyPassBottomBar() { - val showMainBottomAppBar by selectState { this.currentScreen.showMainBottomAppBar } - val dispatchAction = rememberDispatcher() - - if (!showMainBottomAppBar) { - return - } - - BottomAppBar(actions = { - IconButton(onClick = { - dispatchAction(BottomSheetAction.HomeNavigationMenu(true)) - }) { - Icon( - painter = rememberVectorPainter(image = Icons.Rounded.Menu), - contentDescription = "Menu", - tint = MaterialTheme.colorScheme.onSurface - ) - } - - IconButton(onClick = { - dispatchAction(NavigationAction(SettingsState)) - }) { - Icon( - painter = rememberVectorPainter(image = Icons.Rounded.Settings), - contentDescription = "Settings", - tint = MaterialTheme.colorScheme.onSurface - ) - } - }, floatingActionButton = { - FloatingActionButton(onClick = { - dispatchAction(NavigationAction(AccountDetailState())) - }) { - Icon( - painter = rememberVectorPainter(image = Icons.Rounded.Add), - contentDescription = "Add" - ) - } - }) -} +package com.yogeshpaliyal.keypass.ui.nav + +import android.os.Bundle +import android.view.WindowManager +import androidx.activity.compose.BackHandler +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Add +import androidx.compose.material.icons.rounded.Menu +import androidx.compose.material.icons.rounded.Settings +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.Divider +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.TextUnitType +import androidx.compose.ui.unit.dp +import com.yogeshpaliyal.keypass.BuildConfig +import com.yogeshpaliyal.keypass.ui.auth.AuthScreen +import com.yogeshpaliyal.keypass.ui.backup.BackupScreen +import com.yogeshpaliyal.keypass.ui.detail.AccountDetailPage +import com.yogeshpaliyal.keypass.ui.home.Homepage +import com.yogeshpaliyal.keypass.ui.redux.KeyPassRedux +import com.yogeshpaliyal.keypass.ui.redux.actions.BottomSheetAction +import com.yogeshpaliyal.keypass.ui.redux.actions.GoBackAction +import com.yogeshpaliyal.keypass.ui.redux.actions.NavigationAction +import com.yogeshpaliyal.keypass.ui.redux.actions.UpdateContextAction +import com.yogeshpaliyal.keypass.ui.redux.states.AccountDetailState +import com.yogeshpaliyal.keypass.ui.redux.states.AuthState +import com.yogeshpaliyal.keypass.ui.redux.states.BackupScreenState +import com.yogeshpaliyal.keypass.ui.redux.states.BottomSheetState +import com.yogeshpaliyal.keypass.ui.redux.states.HomeState +import com.yogeshpaliyal.keypass.ui.redux.states.KeyPassState +import com.yogeshpaliyal.keypass.ui.redux.states.ScreenState +import com.yogeshpaliyal.keypass.ui.redux.states.SettingsState +import com.yogeshpaliyal.keypass.ui.redux.states.TotpDetailState +import com.yogeshpaliyal.keypass.ui.settings.MySettingCompose +import com.yogeshpaliyal.keypass.ui.style.KeyPassTheme +import dagger.hilt.android.AndroidEntryPoint +import org.reduxkotlin.compose.StoreProvider +import org.reduxkotlin.compose.rememberDispatcher +import org.reduxkotlin.compose.selectState +import java.util.Locale + +@AndroidEntryPoint +class DashboardComposeActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (BuildConfig.DEBUG.not()) { + window.setFlags( + WindowManager.LayoutParams.FLAG_SECURE, + WindowManager.LayoutParams.FLAG_SECURE + ) + } + setContent { + KeyPassTheme { + StoreProvider(store = KeyPassRedux.createStore()) { + Dashboard() + } + } + } + } +} + +@Composable +fun Dashboard() { + val systemBackPress by selectState { this.systemBackPress } + + val context = LocalContext.current + val dispatch = rememberDispatcher() + + BackHandler(!systemBackPress) { + dispatch(GoBackAction) + } + + LaunchedEffect(key1 = systemBackPress, block = { + if (systemBackPress) { + (context as? AppCompatActivity)?.onBackPressed() + } + }) + + DisposableEffect(KeyPassRedux, context) { + dispatch(UpdateContextAction(context)) + + onDispose { + dispatch(UpdateContextAction(null)) + } + } + + Scaffold(bottomBar = { + KeyPassBottomBar() + }) { paddingValues -> + Surface(modifier = Modifier.padding(paddingValues)) { + CurrentPage() + + OptionBottomBar() + } + } +} + +@Composable +fun CurrentPage() { + val currentScreen by selectState { this.currentScreen } + + currentScreen.let { + when (it) { + is HomeState -> { + Homepage(homeState = it) + } + + is SettingsState -> { + MySettingCompose() + } + + is AccountDetailState -> { + AccountDetailPage(id = it.accountId) + } + + is AuthState -> { + AuthScreen(it) + } + + is BackupScreenState -> { + BackupScreen(state = it) + } + + is TotpDetailState -> { + } + } + } +} + +@Composable +fun OptionBottomBar( + viewModel: BottomNavViewModel = androidx.lifecycle.viewmodel.compose.viewModel() +) { + val bottomSheetState by selectState { this.bottomSheet } + + if (bottomSheetState?.isBottomSheetOpen != true) { + return + } + + val dispatchAction = rememberDispatcher() + + val navigationItems by viewModel.navigationList.observeAsState() + ModalBottomSheet(onDismissRequest = { + dispatchAction(BottomSheetAction.HomeNavigationMenu(false)) + }) { + LazyColumn( + modifier = Modifier + .fillMaxWidth(1f) + .padding(16.dp) + ) { + if (navigationItems != null) { + items(navigationItems!!) { + when (it) { + is NavigationModelItem.NavDivider -> { + NavItemSection(it) + } + + is NavigationModelItem.NavTagItem -> { + NavMenuFolder(folder = it) { + dispatchAction(NavigationAction(HomeState(tag = it.tag), false)) + dispatchAction(BottomSheetAction.HomeNavigationMenu(false)) + } + } + + is NavigationModelItem.NavMenuItem -> { + NavItem(it) { + dispatchAction(it.action) + dispatchAction(BottomSheetAction.HomeNavigationMenu(false)) + } + } + } + } + } + } + } +} + +@Composable +fun NavMenuFolder(folder: NavigationModelItem.NavTagItem, onClick: () -> Unit) { + Box( + modifier = Modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple( + bounded = true + ), + onClick = onClick + ) + .padding(16.dp) + .fillMaxWidth(1f) + + ) { + Text( + text = folder.tag, + style = MaterialTheme.typography.titleMedium + ) + } +} + +@Composable +fun NavItem(item: NavigationModelItem.NavMenuItem, onClick: () -> Unit) { + Row( + modifier = Modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = true), + onClick = onClick + ) + .padding(16.dp) + .fillMaxWidth(1f) + ) { + Icon( + painter = painterResource(id = item.icon), + contentDescription = "" + ) + Spacer(modifier = Modifier.width(32.dp)) + Text( + text = stringResource(id = item.titleRes), + style = MaterialTheme.typography.titleMedium + ) + } +} + +@Composable +fun NavItemSection(divider: NavigationModelItem.NavDivider) { + Column(modifier = Modifier.padding(16.dp)) { + Divider() + Spacer(modifier = Modifier.height(32.dp)) + Text( + text = divider.title.uppercase(Locale.getDefault()), + style = MaterialTheme.typography.labelMedium, + fontSize = TextUnit(12f, TextUnitType.Sp) + ) + } +} + +@Composable +fun KeyPassBottomBar() { + val showMainBottomAppBar by selectState { this.currentScreen.showMainBottomAppBar } + val dispatchAction = rememberDispatcher() + + if (!showMainBottomAppBar) { + return + } + + BottomAppBar(actions = { + IconButton(onClick = { + dispatchAction(BottomSheetAction.HomeNavigationMenu(true)) + }) { + Icon( + painter = rememberVectorPainter(image = Icons.Rounded.Menu), + contentDescription = "Menu", + tint = MaterialTheme.colorScheme.onSurface + ) + } + + IconButton(onClick = { + dispatchAction(NavigationAction(SettingsState)) + }) { + Icon( + painter = rememberVectorPainter(image = Icons.Rounded.Settings), + contentDescription = "Settings", + tint = MaterialTheme.colorScheme.onSurface + ) + } + }, floatingActionButton = { + FloatingActionButton(modifier = Modifier.testTag("btnAdd"), onClick = { + dispatchAction(NavigationAction(AccountDetailState())) + }) { + Icon( + painter = rememberVectorPainter(image = Icons.Rounded.Add), + contentDescription = "Add" + ) + } + }) +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/NavigationModel.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/NavigationModel.kt index 7a4b211e..d2e11c9e 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/NavigationModel.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/NavigationModel.kt @@ -1,37 +1,37 @@ -package com.yogeshpaliyal.keypass.ui.nav - -import com.yogeshpaliyal.keypass.R -import com.yogeshpaliyal.keypass.ui.redux.HomeState -import com.yogeshpaliyal.keypass.ui.redux.IntentNavigation -import com.yogeshpaliyal.keypass.ui.redux.NavigationAction - -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, - action = NavigationAction(HomeState(), true) - ), - NavigationModelItem.NavMenuItem( - id = GENERATE_PASSWORD, - icon = R.drawable.ic_twotone_vpn_key_24, - titleRes = R.string.generate_password, - checked = false, - action = IntentNavigation.GeneratePassword - ), - NavigationModelItem.NavMenuItem( - id = ADD_TOPT, - icon = R.drawable.ic_twotone_totp, - titleRes = R.string.add_totp, - checked = false, - action = IntentNavigation.AddTOTP() - ) - ) -} +package com.yogeshpaliyal.keypass.ui.nav + +import com.yogeshpaliyal.keypass.R +import com.yogeshpaliyal.keypass.ui.redux.actions.IntentNavigation +import com.yogeshpaliyal.keypass.ui.redux.actions.NavigationAction +import com.yogeshpaliyal.keypass.ui.redux.states.HomeState + +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, + action = NavigationAction(HomeState(), true) + ), + NavigationModelItem.NavMenuItem( + id = GENERATE_PASSWORD, + icon = R.drawable.ic_twotone_vpn_key_24, + titleRes = R.string.generate_password, + checked = false, + action = IntentNavigation.GeneratePassword + ), + NavigationModelItem.NavMenuItem( + id = ADD_TOPT, + icon = R.drawable.ic_twotone_totp, + titleRes = R.string.add_totp, + checked = false, + action = IntentNavigation.AddTOTP() + ) + ) +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/NavigationModelItem.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/NavigationModelItem.kt index 2e290292..d8f6ecce 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/NavigationModelItem.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/nav/NavigationModelItem.kt @@ -1,24 +1,24 @@ -package com.yogeshpaliyal.keypass.ui.nav - -import androidx.annotation.DrawableRes -import androidx.annotation.StringRes -import com.yogeshpaliyal.keypass.ui.redux.Action - -sealed class NavigationModelItem { - - data class NavMenuItem( - val id: Int, - @DrawableRes val icon: Int, - @StringRes val titleRes: Int, - var checked: Boolean, - var action: Action - ) : 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() - - data class NavTagItem(val tag: String) : NavigationModelItem() -} +package com.yogeshpaliyal.keypass.ui.nav + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import com.yogeshpaliyal.keypass.ui.redux.actions.Action + +sealed class NavigationModelItem { + + data class NavMenuItem( + val id: Int, + @DrawableRes val icon: Int, + @StringRes val titleRes: Int, + var checked: Boolean, + var action: Action + ) : 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() + + data class NavTagItem(val tag: String) : NavigationModelItem() +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/Action.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/Action.kt deleted file mode 100644 index fab02cae..00000000 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/Action.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.yogeshpaliyal.keypass.ui.redux - -import android.content.Context -import android.os.Bundle -import androidx.annotation.StringRes - -sealed interface Action - -class UpdateContextAction(val context: Context?) : Action - -object GoBackAction : Action - -data class NavigationAction(val state: ScreenState, val clearBackStack: Boolean = false) : Action -data class StateUpdateAction(val state: ScreenState) : Action - -sealed interface IntentNavigation : Action { - object GeneratePassword : IntentNavigation - object BackupActivity : IntentNavigation - object ShareApp : IntentNavigation - data class AddTOTP(val accountId: String? = null) : IntentNavigation -} - -data class ToastAction(@StringRes val text: Int) : Action -data class CopyToClipboard(val password: String) : Action - -sealed class BottomSheetAction( - val route: String, - val globalIsBottomSheetOpen: Boolean, - val globalArgs: Bundle? = null -) : Action { - data class HomeNavigationMenu(val isBottomSheetOpen: Boolean, val args: Bundle? = null) : - BottomSheetAction(BottomSheetRoutes.HOME_NAV_MENU, isBottomSheetOpen, args) -} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/BottomSheetRoutes.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/BottomSheetRoutes.kt new file mode 100644 index 00000000..f8802e5c --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/BottomSheetRoutes.kt @@ -0,0 +1,5 @@ +package com.yogeshpaliyal.keypass.ui.redux + +object BottomSheetRoutes { + const val HOME_NAV_MENU = "/home/navMenu" +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/KeyPassRedux.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/KeyPassRedux.kt index 0fb222dc..fda22ba3 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/KeyPassRedux.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/KeyPassRedux.kt @@ -1,124 +1,103 @@ -package com.yogeshpaliyal.keypass.ui.redux - -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Intent -import android.widget.Toast -import androidx.core.content.ContextCompat -import com.yogeshpaliyal.keypass.BuildConfig -import com.yogeshpaliyal.keypass.R -import com.yogeshpaliyal.keypass.ui.addTOTP.AddTOTPActivity -import com.yogeshpaliyal.keypass.ui.backup.BackupActivity -import com.yogeshpaliyal.keypass.ui.generate.GeneratePasswordActivity -import org.reduxkotlin.Reducer -import org.reduxkotlin.applyMiddleware -import org.reduxkotlin.createStore -import org.reduxkotlin.middleware - -object KeyPassRedux { - - private var arrPages = mutableListOf() - - private val reducer: Reducer = { state, action -> - when (action) { - is NavigationAction -> { - if (action.clearBackStack) { - arrPages.clear() - } else { - arrPages.add(state.currentScreen) - } - - state.copy(currentScreen = action.state) - } - - is StateUpdateAction -> { - state.copy(currentScreen = action.state) - } - - is CopyToClipboard -> { - state.context?.let { - val clipboard = ContextCompat.getSystemService( - it, - ClipboardManager::class.java - ) - val clip = ClipData.newPlainText("KeyPass", action.password) - clipboard?.setPrimaryClip(clip) - - Toast.makeText( - it, - R.string.copied_to_clipboard, - Toast.LENGTH_SHORT - ) - .show() - } - state - } - - is UpdateContextAction -> { - state.copy(context = action.context) - } - - is ToastAction -> { - state.context?.let { - Toast.makeText(it, action.text, Toast.LENGTH_SHORT).show() - } - state - } - - is GoBackAction -> { - val lastItem = arrPages.removeLastOrNull() - if (lastItem != null) { - state.copy(currentScreen = lastItem) - } else { - state.copy(systemBackPress = true) - } - } - - is BottomSheetAction -> { - state.copy( - bottomSheet = BottomSheetState( - action.route, - action.globalArgs, - action.globalIsBottomSheetOpen - ) - ) - } - - else -> state - } - } - - private val intentNavigationMiddleware = middleware { store, next, action -> - val state = store.state - - when (action) { - is IntentNavigation.GeneratePassword -> { - val intent = Intent(state.context, GeneratePasswordActivity::class.java) - state.context?.startActivity(intent) - } - - is IntentNavigation.AddTOTP -> { - AddTOTPActivity.start(state.context, action.accountId) - } - - is IntentNavigation.BackupActivity -> { - BackupActivity.start(state.context) - } - - is IntentNavigation.ShareApp -> { - 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" - state.context?.startActivity(Intent.createChooser(sendIntent, state.context.getString(R.string.share_keypass))) - } - } - next(action) - } - - fun createStore() = - createStore(reducer, generateDefaultState(), applyMiddleware(intentNavigationMiddleware)) -} +package com.yogeshpaliyal.keypass.ui.redux + +import android.content.ClipData +import android.content.ClipboardManager +import android.widget.Toast +import androidx.core.content.ContextCompat +import com.yogeshpaliyal.keypass.R +import com.yogeshpaliyal.keypass.ui.redux.actions.BottomSheetAction +import com.yogeshpaliyal.keypass.ui.redux.actions.CopyToClipboard +import com.yogeshpaliyal.keypass.ui.redux.actions.GoBackAction +import com.yogeshpaliyal.keypass.ui.redux.actions.NavigationAction +import com.yogeshpaliyal.keypass.ui.redux.actions.StateUpdateAction +import com.yogeshpaliyal.keypass.ui.redux.actions.ToastAction +import com.yogeshpaliyal.keypass.ui.redux.actions.UpdateContextAction +import com.yogeshpaliyal.keypass.ui.redux.middlewares.intentNavigationMiddleware +import com.yogeshpaliyal.keypass.ui.redux.states.BottomSheetState +import com.yogeshpaliyal.keypass.ui.redux.states.KeyPassState +import com.yogeshpaliyal.keypass.ui.redux.states.ScreenState +import com.yogeshpaliyal.keypass.ui.redux.states.generateDefaultState +import org.reduxkotlin.Reducer +import org.reduxkotlin.applyMiddleware +import org.reduxkotlin.createStore + +object KeyPassRedux { + + private var arrPages = mutableListOf() + + private val reducer: Reducer = { state, action -> + when (action) { + is NavigationAction -> { + if (action.clearBackStack) { + arrPages.clear() + } else { + arrPages.add(state.currentScreen) + } + + state.copy(currentScreen = action.state) + } + + is StateUpdateAction -> { + state.copy(currentScreen = action.state) + } + + is CopyToClipboard -> { + state.context?.let { + val clipboard = ContextCompat.getSystemService( + it, + ClipboardManager::class.java + ) + val clip = ClipData.newPlainText("KeyPass", action.password) + clipboard?.setPrimaryClip(clip) + + Toast.makeText( + it, + R.string.copied_to_clipboard, + Toast.LENGTH_SHORT + ) + .show() + } + state + } + + is UpdateContextAction -> { + state.copy(context = action.context) + } + + is ToastAction -> { + state.context?.let { + Toast.makeText(it, action.text, Toast.LENGTH_SHORT).show() + } + state + } + + is GoBackAction -> { + val lastItem = arrPages.removeLastOrNull() + if (lastItem != null) { + state.copy(currentScreen = lastItem) + } else { + state.copy(systemBackPress = true) + } + } + + is BottomSheetAction -> { + state.copy( + bottomSheet = BottomSheetState( + action.route, + action.globalArgs, + action.globalIsBottomSheetOpen + ) + ) + } + + else -> state + } + } + + fun createStore() = + createStore( + reducer, + generateDefaultState(), + applyMiddleware(intentNavigationMiddleware) + ) +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/Routes.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/Routes.kt deleted file mode 100644 index e0a2460a..00000000 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/Routes.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.yogeshpaliyal.keypass.ui.redux - -object ScreenRoutes { - const val HOME = "home?tag={tag}" - const val SETTINGS = "/settings" - const val ADD_ACCOUNT = "addAccount?accountId={accountId}" - const val GENERATE_PASSWORD = "/generatePassword" - const val ADD_TOTP = "/addTOTP" -} - -object BottomSheetRoutes { - const val HOME_NAV_MENU = "/home/navMenu" -} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/State.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/State.kt deleted file mode 100644 index f38fc668..00000000 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/State.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.yogeshpaliyal.keypass.ui.redux - -import android.content.Context -import android.os.Bundle -import androidx.annotation.StringRes -import com.yogeshpaliyal.keypass.R - -data class KeyPassState( - val context: Context? = null, - val currentScreen: ScreenState, - val bottomSheet: BottomSheetState? = null, - val systemBackPress: Boolean = false -) - -sealed class ScreenState(val showMainBottomAppBar: Boolean = false) - -data class HomeState(val keyword: String? = null, val tag: String? = null, val sortField: String? = null, val sortAscending: Boolean = true) : ScreenState(true) -data class AccountDetailState(val accountId: Long? = null) : ScreenState() -data class TotpDetailState(val accountId: String? = null) : ScreenState() -object SettingsState : ScreenState(true) - -open class AuthState(@StringRes val title: Int) : ScreenState(false) { - object CreatePassword : AuthState(R.string.create_password) - class ConfirmPassword(val password: String) : AuthState(R.string.confirm_password) - object Login : AuthState(R.string.login_to_enter_keypass) -} -data class BottomSheetState( - val path: String, - val args: Bundle? = null, - val isBottomSheetOpen: Boolean = false -) - -fun generateDefaultState(): KeyPassState { - val currentPage = AuthState.Login - val bottomSheet = BottomSheetState(BottomSheetRoutes.HOME_NAV_MENU, isBottomSheetOpen = false) - return KeyPassState(currentScreen = currentPage, bottomSheet = bottomSheet) -} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/actions/Action.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/actions/Action.kt new file mode 100644 index 00000000..cff14af7 --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/actions/Action.kt @@ -0,0 +1,11 @@ +package com.yogeshpaliyal.keypass.ui.redux.actions + +import android.content.Context +import com.yogeshpaliyal.keypass.ui.redux.states.ScreenState + +sealed interface Action + +class UpdateContextAction(val context: Context?) : Action + +data class NavigationAction(val state: ScreenState, val clearBackStack: Boolean = false) : Action +data class StateUpdateAction(val state: ScreenState) : Action diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/actions/BottomSheetAction.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/actions/BottomSheetAction.kt new file mode 100644 index 00000000..4ece8387 --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/actions/BottomSheetAction.kt @@ -0,0 +1,13 @@ +package com.yogeshpaliyal.keypass.ui.redux.actions + +import android.os.Bundle +import com.yogeshpaliyal.keypass.ui.redux.BottomSheetRoutes + +sealed class BottomSheetAction( + val route: String, + val globalIsBottomSheetOpen: Boolean, + val globalArgs: Bundle? = null +) : Action { + data class HomeNavigationMenu(val isBottomSheetOpen: Boolean, val args: Bundle? = null) : + BottomSheetAction(BottomSheetRoutes.HOME_NAV_MENU, isBottomSheetOpen, args) +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/actions/IntentNavigation.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/actions/IntentNavigation.kt new file mode 100644 index 00000000..dcf36c64 --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/actions/IntentNavigation.kt @@ -0,0 +1,8 @@ +package com.yogeshpaliyal.keypass.ui.redux.actions + +sealed interface IntentNavigation : Action { + object GeneratePassword : IntentNavigation + object BackupActivity : IntentNavigation + object ShareApp : IntentNavigation + data class AddTOTP(val accountId: String? = null) : IntentNavigation +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/actions/UtilityAction.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/actions/UtilityAction.kt new file mode 100644 index 00000000..353baae8 --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/actions/UtilityAction.kt @@ -0,0 +1,8 @@ +package com.yogeshpaliyal.keypass.ui.redux.actions + +import androidx.annotation.StringRes + +data class ToastAction(@StringRes val text: Int) : Action +data class CopyToClipboard(val password: String) : Action + +object GoBackAction : Action diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/middlewares/IntentNavigationMiddleware.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/middlewares/IntentNavigationMiddleware.kt new file mode 100644 index 00000000..cf121222 --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/middlewares/IntentNavigationMiddleware.kt @@ -0,0 +1,46 @@ +package com.yogeshpaliyal.keypass.ui.redux.middlewares + +import android.content.Intent +import com.yogeshpaliyal.keypass.BuildConfig +import com.yogeshpaliyal.keypass.R +import com.yogeshpaliyal.keypass.ui.addTOTP.AddTOTPActivity +import com.yogeshpaliyal.keypass.ui.generate.GeneratePasswordActivity +import com.yogeshpaliyal.keypass.ui.redux.actions.IntentNavigation +import com.yogeshpaliyal.keypass.ui.redux.states.KeyPassState +import org.reduxkotlin.middleware + +val intentNavigationMiddleware = middleware { store, next, action -> + val state = store.state + + when (action) { + is IntentNavigation.GeneratePassword -> { + val intent = Intent(state.context, GeneratePasswordActivity::class.java) + state.context?.startActivity(intent) + } + + is IntentNavigation.AddTOTP -> { + AddTOTPActivity.start(state.context, action.accountId) + } + + is IntentNavigation.BackupActivity -> { + // BackupActivity.start(state.context) + } + + is IntentNavigation.ShareApp -> { + 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" + state.context?.startActivity( + Intent.createChooser( + sendIntent, + state.context.getString(R.string.share_keypass) + ) + ) + } + } + next(action) +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/AccountDetailState.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/AccountDetailState.kt new file mode 100644 index 00000000..6cb42962 --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/AccountDetailState.kt @@ -0,0 +1,3 @@ +package com.yogeshpaliyal.keypass.ui.redux.states + +data class AccountDetailState(val accountId: Long? = null) : ScreenState() diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/AuthState.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/AuthState.kt new file mode 100644 index 00000000..3cf7362c --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/AuthState.kt @@ -0,0 +1,10 @@ +package com.yogeshpaliyal.keypass.ui.redux.states + +import androidx.annotation.StringRes +import com.yogeshpaliyal.keypass.R + +open class AuthState(@StringRes val title: Int) : ScreenState(false) { + object CreatePassword : AuthState(R.string.create_password) + class ConfirmPassword(val password: String) : AuthState(R.string.confirm_password) + object Login : AuthState(R.string.login_to_enter_keypass) +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/BackupScreenState.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/BackupScreenState.kt new file mode 100644 index 00000000..d3162757 --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/BackupScreenState.kt @@ -0,0 +1,29 @@ +package com.yogeshpaliyal.keypass.ui.redux.states + +import android.net.Uri +import java.net.URLDecoder + +data class BackupScreenState( + val isBackupEnabled: Boolean? = null, + val isAutoBackupEnabled: Boolean? = null, + val overrideAutoBackup: Boolean? = null, + val lastBackupTime: Long? = null, + val backupDirectory: Uri? = null, + val dialog: BackupDialog? = null +) : ScreenState() { + fun getFormattedBackupDirectory(): String { + if (backupDirectory == null) { + return "" + } + val splittedPath = URLDecoder.decode(backupDirectory.toString(), "utf-8").split("/") + return splittedPath.get(splittedPath.lastIndex) + } +} + +sealed interface BackupDialog + +object SelectKeyphraseType : BackupDialog + +object CustomKeyphrase : BackupDialog + +object ShowKeyphrase : BackupDialog diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/BottomSheetState.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/BottomSheetState.kt new file mode 100644 index 00000000..5fb51145 --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/BottomSheetState.kt @@ -0,0 +1,9 @@ +package com.yogeshpaliyal.keypass.ui.redux.states + +import android.os.Bundle + +data class BottomSheetState( + val path: String, + val args: Bundle? = null, + val isBottomSheetOpen: Boolean = false +) diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/HomeState.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/HomeState.kt new file mode 100644 index 00000000..d992a5fd --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/HomeState.kt @@ -0,0 +1,8 @@ +package com.yogeshpaliyal.keypass.ui.redux.states + +data class HomeState( + val keyword: String? = null, + val tag: String? = null, + val sortField: String? = null, + val sortAscending: Boolean = true +) : ScreenState(true) diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/KeyPassState.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/KeyPassState.kt new file mode 100644 index 00000000..f030d023 --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/KeyPassState.kt @@ -0,0 +1,17 @@ +package com.yogeshpaliyal.keypass.ui.redux.states + +import android.content.Context +import com.yogeshpaliyal.keypass.ui.redux.BottomSheetRoutes + +data class KeyPassState( + val context: Context? = null, + val currentScreen: ScreenState, + val bottomSheet: BottomSheetState? = null, + val systemBackPress: Boolean = false +) + +fun generateDefaultState(): KeyPassState { + val currentPage = AuthState.Login + val bottomSheet = BottomSheetState(BottomSheetRoutes.HOME_NAV_MENU, isBottomSheetOpen = false) + return KeyPassState(currentScreen = currentPage, bottomSheet = bottomSheet) +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/ScreenState.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/ScreenState.kt new file mode 100644 index 00000000..13549ed9 --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/ScreenState.kt @@ -0,0 +1,3 @@ +package com.yogeshpaliyal.keypass.ui.redux.states + +sealed class ScreenState(val showMainBottomAppBar: Boolean = false) diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/SettingsState.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/SettingsState.kt new file mode 100644 index 00000000..73085e2a --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/SettingsState.kt @@ -0,0 +1,3 @@ +package com.yogeshpaliyal.keypass.ui.redux.states + +object SettingsState : ScreenState(true) diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/TotpDetailState.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/TotpDetailState.kt new file mode 100644 index 00000000..beb7bfcb --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/redux/states/TotpDetailState.kt @@ -0,0 +1,3 @@ +package com.yogeshpaliyal.keypass.ui.redux.states + +data class TotpDetailState(val accountId: String? = null) : ScreenState() diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/settings/MySettingsFragment.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/settings/MySettingsFragment.kt index 0373554e..a4ba4683 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/settings/MySettingsFragment.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/settings/MySettingsFragment.kt @@ -1,244 +1,249 @@ -package com.yogeshpaliyal.keypass.ui.settings - -import android.net.Uri -import android.os.Bundle -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.annotation.StringRes -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.widthIn -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Feedback -import androidx.compose.material.icons.rounded.Share -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Divider -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.graphics.vector.rememberVectorPainter -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.preference.PreferenceFragmentCompat -import com.yogeshpaliyal.common.utils.BACKUP_KEY_LENGTH -import com.yogeshpaliyal.common.utils.email -import com.yogeshpaliyal.keypass.R -import com.yogeshpaliyal.keypass.ui.home.DashboardViewModel -import com.yogeshpaliyal.keypass.ui.redux.Action -import com.yogeshpaliyal.keypass.ui.redux.IntentNavigation -import com.yogeshpaliyal.keypass.ui.redux.ToastAction -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch -import org.reduxkotlin.compose.rememberTypedDispatcher -import javax.inject.Inject - -@AndroidEntryPoint -class MySettingsFragment : PreferenceFragmentCompat() { - - @Inject - lateinit var appDb: com.yogeshpaliyal.common.AppDatabase - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - setPreferencesFromResource(R.xml.preferences, rootKey) - } -} - -@Composable -fun RestoreDialog( - selectedFile: Uri, - hideDialog: () -> Unit, - mViewModel: DashboardViewModel = hiltViewModel() -) { - val (keyphrase, setKeyPhrase) = remember { - mutableStateOf("") - } - - val dispatchAction = rememberTypedDispatcher() - - val context = LocalContext.current - - val coroutineScope = rememberCoroutineScope() - - AlertDialog( - onDismissRequest = { - hideDialog() - }, - title = { - Text(text = stringResource(id = R.string.restore)) - }, - confirmButton = { - TextButton(onClick = { - if (keyphrase.isEmpty()) { - dispatchAction(ToastAction(R.string.alert_blank_keyphrase)) - return@TextButton - } - - if (keyphrase.length != BACKUP_KEY_LENGTH) { - dispatchAction(ToastAction(R.string.alert_invalid_keyphrase)) - return@TextButton - } - coroutineScope.launch { - val result = - mViewModel.restoreBackup(keyphrase, context.contentResolver, selectedFile) - - if (result) { - hideDialog() - dispatchAction(ToastAction(R.string.backup_restored)) - } else { - dispatchAction(ToastAction(R.string.invalid_keyphrase)) - } - } - }) { - Text(text = stringResource(id = R.string.restore)) - } - }, - dismissButton = { - TextButton(onClick = hideDialog) { - Text(text = stringResource(id = R.string.cancel)) - } - }, - text = { - Column(modifier = Modifier.fillMaxWidth(1f)) { - Text(text = stringResource(id = R.string.keyphrase_restore_info)) - Spacer(modifier = Modifier.size(8.dp)) - OutlinedTextField( - modifier = Modifier.fillMaxWidth(1f), - value = keyphrase, - onValueChange = setKeyPhrase, - placeholder = { - Text(text = stringResource(id = R.string.enter_keyphrase)) - } - ) - } - } - ) -} - -@Preview(showSystemUi = true) -@Composable -fun MySettingCompose() { - val dispatchAction = rememberTypedDispatcher() - val context = LocalContext.current - - val (result, setResult) = remember { mutableStateOf(null) } - - val launcher = rememberLauncherForActivityResult(OpenKeyPassBackup()) { - setResult(it) - } - - result?.let { - RestoreDialog( - selectedFile = it, - hideDialog = { - setResult(null) - } - ) - } - - Column { - PreferenceItem(title = R.string.security, isCategory = true) - PreferenceItem( - title = R.string.credentials_backups, - summary = R.string.credentials_backups_desc - ) { - dispatchAction(IntentNavigation.BackupActivity) - } - PreferenceItem( - title = R.string.restore_credentials, - summary = R.string.restore_credentials_desc - ) { - launcher.launch(arrayOf()) - } - Divider( - modifier = Modifier - .fillMaxWidth(1f) - .height(1.dp) - ) - PreferenceItem(title = R.string.help, isCategory = true) - PreferenceItem( - title = R.string.send_feedback, - summary = R.string.send_feedback_desc, - icon = Icons.Rounded.Feedback - ) { - context.email( - context.getString(R.string.feedback_to_keypass), - "yogeshpaliyal.foss@gmail.com" - ) - } - PreferenceItem( - title = R.string.share, - summary = R.string.share_desc, - icon = Icons.Rounded.Share - ) { - dispatchAction(IntentNavigation.ShareApp) - } - } -} - -@Composable -fun PreferenceItem( - @StringRes title: Int, - @StringRes summary: Int? = null, - icon: ImageVector? = null, - isCategory: Boolean = false, - onClickItem: (() -> Unit)? = null -) { - val titleColor = if (isCategory) { - MaterialTheme.colorScheme.secondary - } else { - Color.Unspecified - } - - Row( - modifier = Modifier - .fillMaxWidth(1f) - .widthIn(48.dp) - .padding(horizontal = 16.dp) - .clickable(onClickItem != null) { - onClickItem?.invoke() - }, - verticalAlignment = Alignment.CenterVertically - ) { - Box(modifier = Modifier.width(56.dp), Alignment.CenterStart) { - if (icon != null) { - Icon(painter = rememberVectorPainter(image = icon), contentDescription = "") - } - } - Column( - modifier = Modifier - .padding(vertical = 16.dp) - .fillMaxWidth(1f) - ) { - Text( - text = stringResource(id = title), - color = titleColor, - style = TextStyle(fontSize = 16.sp) - ) - if (summary != null) { - Text(text = stringResource(id = summary), style = TextStyle(fontSize = 14.sp)) - } - } - } -} +package com.yogeshpaliyal.keypass.ui.settings + +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.annotation.StringRes +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Feedback +import androidx.compose.material.icons.rounded.Share +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.yogeshpaliyal.common.utils.BACKUP_KEY_LENGTH +import com.yogeshpaliyal.common.utils.email +import com.yogeshpaliyal.keypass.R +import com.yogeshpaliyal.keypass.ui.home.DashboardViewModel +import com.yogeshpaliyal.keypass.ui.redux.actions.Action +import com.yogeshpaliyal.keypass.ui.redux.actions.IntentNavigation +import com.yogeshpaliyal.keypass.ui.redux.actions.NavigationAction +import com.yogeshpaliyal.keypass.ui.redux.actions.ToastAction +import com.yogeshpaliyal.keypass.ui.redux.states.BackupScreenState +import kotlinx.coroutines.launch +import org.reduxkotlin.compose.rememberTypedDispatcher + +@Composable +fun RestoreDialog( + selectedFile: Uri, + hideDialog: () -> Unit, + mViewModel: DashboardViewModel = hiltViewModel() +) { + val (keyphrase, setKeyPhrase) = remember { + mutableStateOf("") + } + + val dispatchAction = rememberTypedDispatcher() + + val context = LocalContext.current + + val coroutineScope = rememberCoroutineScope() + + AlertDialog( + onDismissRequest = { + hideDialog() + }, + title = { + Text(text = stringResource(id = R.string.restore)) + }, + confirmButton = { + TextButton(onClick = { + if (keyphrase.isEmpty()) { + dispatchAction(ToastAction(R.string.alert_blank_keyphrase)) + return@TextButton + } + + if (keyphrase.length != BACKUP_KEY_LENGTH) { + dispatchAction(ToastAction(R.string.alert_invalid_keyphrase)) + return@TextButton + } + coroutineScope.launch { + val result = + mViewModel.restoreBackup(keyphrase, context.contentResolver, selectedFile) + + if (result) { + hideDialog() + dispatchAction(ToastAction(R.string.backup_restored)) + } else { + dispatchAction(ToastAction(R.string.invalid_keyphrase)) + } + } + }) { + Text(text = stringResource(id = R.string.restore)) + } + }, + dismissButton = { + TextButton(onClick = hideDialog) { + Text(text = stringResource(id = R.string.cancel)) + } + }, + text = { + Column(modifier = Modifier.fillMaxWidth(1f)) { + Text(text = stringResource(id = R.string.keyphrase_restore_info)) + Spacer(modifier = Modifier.size(8.dp)) + OutlinedTextField( + modifier = Modifier.fillMaxWidth(1f), + value = keyphrase, + onValueChange = setKeyPhrase, + placeholder = { + Text(text = stringResource(id = R.string.enter_keyphrase)) + } + ) + } + } + ) +} + +@Preview(showSystemUi = true) +@Composable +fun MySettingCompose() { + val dispatchAction = rememberTypedDispatcher() + val context = LocalContext.current + + val (result, setResult) = remember { mutableStateOf(null) } + + val launcher = rememberLauncherForActivityResult(OpenKeyPassBackup()) { + setResult(it) + } + + result?.let { + RestoreDialog( + selectedFile = it, + hideDialog = { + setResult(null) + } + ) + } + + Column { + PreferenceItem(title = R.string.security, isCategory = true) + PreferenceItem( + title = R.string.credentials_backups, + summary = R.string.credentials_backups_desc + ) { + dispatchAction(NavigationAction(BackupScreenState())) + } + PreferenceItem( + title = R.string.restore_credentials, + summary = R.string.restore_credentials_desc + ) { + launcher.launch(arrayOf()) + } + Divider( + modifier = Modifier + .fillMaxWidth(1f) + .height(1.dp) + ) + PreferenceItem(title = R.string.help, isCategory = true) + PreferenceItem( + title = R.string.send_feedback, + summary = R.string.send_feedback_desc, + icon = Icons.Rounded.Feedback + ) { + context.email( + context.getString(R.string.feedback_to_keypass), + "yogeshpaliyal.foss@gmail.com" + ) + } + PreferenceItem( + title = R.string.share, + summary = R.string.share_desc, + icon = Icons.Rounded.Share + ) { + dispatchAction(IntentNavigation.ShareApp) + } + } +} + +@Composable +fun PreferenceItem( + @StringRes title: Int? = null, + @StringRes summary: Int? = null, + summaryStr: String? = null, + icon: ImageVector? = null, + isCategory: Boolean = false, + onClickItem: (() -> Unit)? = null +) { + Row( + modifier = Modifier + .fillMaxWidth(1f) + .widthIn(48.dp) + .padding(horizontal = 16.dp) + .clickable(onClickItem != null) { + onClickItem?.invoke() + }, + verticalAlignment = Alignment.CenterVertically + ) { + Box(modifier = Modifier.width(56.dp), Alignment.CenterStart) { + if (icon != null) { + Icon(painter = rememberVectorPainter(image = icon), contentDescription = "") + } + } + Column( + modifier = Modifier + .padding(vertical = 16.dp) + .fillMaxWidth(1f) + ) { + if (title != null) { + if (isCategory) { + CategoryTitle(title = title) + } else { + PreferenceItemTitle(title = title) + } + } + if (summary != null || summaryStr != null) { + val summaryText = if (summary != null) { + stringResource(id = summary) + } else { + summaryStr + } + if (summaryText != null) { + Text(text = summaryText, style = MaterialTheme.typography.bodyMedium) + } + } + } + } +} + +@Composable +private fun CategoryTitle(title: Int) { + Text( + text = stringResource(id = title), + color = MaterialTheme.colorScheme.tertiary, + style = MaterialTheme.typography.titleMedium + ) +} + +@Composable +private fun PreferenceItemTitle(title: Int) { + Text( + text = stringResource(id = title), + style = MaterialTheme.typography.bodyLarge + ) +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/settings/MySettingsViewModel.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/settings/MySettingsViewModel.kt index 447883ea..0d9387cc 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/settings/MySettingsViewModel.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/settings/MySettingsViewModel.kt @@ -1,14 +1,12 @@ -package com.yogeshpaliyal.keypass.ui.settings - -import android.app.Application -import androidx.lifecycle.AndroidViewModel -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject - -@HiltViewModel -class MySettingsViewModel @Inject constructor( - application: Application, - val appDb: com.yogeshpaliyal.common.AppDatabase -) : AndroidViewModel(application) { - private val appDao = appDb.getDao() -} +package com.yogeshpaliyal.keypass.ui.settings + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class MySettingsViewModel @Inject constructor( + application: Application, + val appDb: com.yogeshpaliyal.common.AppDatabase +) : AndroidViewModel(application) diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/settings/OpenKeyPassBackup.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/settings/OpenKeyPassBackup.kt index 84b06eb5..58457fda 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/settings/OpenKeyPassBackup.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/settings/OpenKeyPassBackup.kt @@ -1,20 +1,20 @@ -package com.yogeshpaliyal.keypass.ui.settings - -import android.content.Context -import android.content.Intent -import androidx.activity.result.contract.ActivityResultContracts - -class OpenKeyPassBackup : ActivityResultContracts.OpenDocument() { - override fun createIntent(context: Context, input: Array): Intent { - super.createIntent(context, input) - 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 - ) - return intent - } -} +package com.yogeshpaliyal.keypass.ui.settings + +import android.content.Context +import android.content.Intent +import androidx.activity.result.contract.ActivityResultContracts + +class OpenKeyPassBackup : ActivityResultContracts.OpenDocument() { + override fun createIntent(context: Context, input: Array): Intent { + super.createIntent(context, input) + 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 + ) + return intent + } +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/ui/style/KeyPassTheme.kt b/app/src/main/java/com/yogeshpaliyal/keypass/ui/style/KeyPassTheme.kt index af068ca7..6c966c6f 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/ui/style/KeyPassTheme.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/ui/style/KeyPassTheme.kt @@ -1,11 +1,11 @@ -package com.yogeshpaliyal.keypass.ui.style - -import androidx.compose.runtime.Composable -import com.google.accompanist.themeadapter.material3.Mdc3Theme - -@Composable -fun KeyPassTheme(content: @Composable () -> Unit) { - Mdc3Theme { - content() - } -} +package com.yogeshpaliyal.keypass.ui.style + +import androidx.compose.runtime.Composable +import com.google.accompanist.themeadapter.material3.Mdc3Theme + +@Composable +fun KeyPassTheme(content: @Composable () -> Unit) { + Mdc3Theme { + content() + } +} diff --git a/app/src/main/java/com/yogeshpaliyal/keypass/utils/LogHelper.kt b/app/src/main/java/com/yogeshpaliyal/keypass/utils/LogHelper.kt index c70ead58..4797004e 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/utils/LogHelper.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/utils/LogHelper.kt @@ -1,48 +1,48 @@ -package com.yogeshpaliyal.keypass.utils - -import android.util.Log -import com.yogeshpaliyal.keypass.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()) -} +package com.yogeshpaliyal.keypass.utils + +import android.util.Log +import com.yogeshpaliyal.keypass.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/app/src/main/java/com/yogeshpaliyal/keypass/utils/StringDiffUtil.kt b/app/src/main/java/com/yogeshpaliyal/keypass/utils/StringDiffUtil.kt index fa6dd23f..1e9e74f2 100644 --- a/app/src/main/java/com/yogeshpaliyal/keypass/utils/StringDiffUtil.kt +++ b/app/src/main/java/com/yogeshpaliyal/keypass/utils/StringDiffUtil.kt @@ -1,12 +1,12 @@ -package com.yogeshpaliyal.keypass.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 -} +package com.yogeshpaliyal.keypass.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/app/src/main/res/layout/layout_my_settings_fragment.xml b/app/src/main/res/layout/layout_my_settings_fragment.xml deleted file mode 100644 index 100bdaee..00000000 --- a/app/src/main/res/layout/layout_my_settings_fragment.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6a6d452a..30df2dd8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -89,5 +89,6 @@ Confirm Password Incorrect Password The password did not match + Your accounts will be backed up whenever account is added or modified diff --git a/common/src/androidTest/java/com/yogeshpaliyal/common/ExampleInstrumentedTest.kt b/common/src/androidTest/java/com/yogeshpaliyal/common/ExampleInstrumentedTest.kt deleted file mode 100644 index f6f3c363..00000000 --- a/common/src/androidTest/java/com/yogeshpaliyal/common/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.yogeshpaliyal.common - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.junit.Test -import org.junit.runner.RunWith - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - } -} diff --git a/common/src/main/java/com/yogeshpaliyal/common/AppDatabase.kt b/common/src/main/java/com/yogeshpaliyal/common/AppDatabase.kt index 13c16d11..42011e55 100644 --- a/common/src/main/java/com/yogeshpaliyal/common/AppDatabase.kt +++ b/common/src/main/java/com/yogeshpaliyal/common/AppDatabase.kt @@ -1,29 +1,29 @@ -package com.yogeshpaliyal.common - -import androidx.room.Database -import androidx.room.RoomDatabase -import com.yogeshpaliyal.common.data.AccountModel -import com.yogeshpaliyal.common.db.DbDao - -/* -* @author Yogesh Paliyal -* yogeshpaliyal.foss@gmail.com -* https://techpaliyal.com -* created on 30-01-2021 20:37 -*/ - -const val DB_VERSION_3 = 3 -const val DB_VERSION_4 = 4 -const val DB_VERSION_5 = 5 - -@Database( - entities = [AccountModel::class], - version = 5, - exportSchema = false -) -abstract class AppDatabase : RoomDatabase() { - - // define DAO start - abstract fun getDao(): DbDao - // define DAO end -} +package com.yogeshpaliyal.common + +import androidx.room.Database +import androidx.room.RoomDatabase +import com.yogeshpaliyal.common.data.AccountModel +import com.yogeshpaliyal.common.db.DbDao + +/* +* @author Yogesh Paliyal +* yogeshpaliyal.foss@gmail.com +* https://techpaliyal.com +* created on 30-01-2021 20:37 +*/ + +const val DB_VERSION_3 = 3 +const val DB_VERSION_4 = 4 +const val DB_VERSION_5 = 5 + +@Database( + entities = [AccountModel::class], + version = 5, + exportSchema = false +) +abstract class AppDatabase : RoomDatabase() { + + // define DAO start + abstract fun getDao(): DbDao + // define DAO end +} diff --git a/common/src/main/java/com/yogeshpaliyal/common/CommonMyApplication.kt b/common/src/main/java/com/yogeshpaliyal/common/CommonMyApplication.kt index d3b98b6e..2aa91b62 100644 --- a/common/src/main/java/com/yogeshpaliyal/common/CommonMyApplication.kt +++ b/common/src/main/java/com/yogeshpaliyal/common/CommonMyApplication.kt @@ -1,38 +1,38 @@ -package com.yogeshpaliyal.common - -import android.app.Application -import android.content.Intent -import android.util.Log -import androidx.hilt.work.HiltWorkerFactory -import androidx.work.Configuration -import com.google.android.material.color.DynamicColors -import javax.inject.Inject -import kotlin.system.exitProcess - -abstract class CommonMyApplication : Application(), Configuration.Provider { - - @Inject - lateinit var workerFactory: HiltWorkerFactory - - abstract fun getCrashActivityIntent(throwable: Throwable): Intent - - override fun onCreate() { - super.onCreate() - Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> - Log.d("MyApplication", "crashed ") - val intent = getCrashActivityIntent(throwable) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - startActivity(intent) - exitProcess(1) - } - - DynamicColors.applyToActivitiesIfAvailable(this) - } - - override fun getWorkManagerConfiguration(): Configuration { - return Configuration.Builder() - .setMinimumLoggingLevel(android.util.Log.INFO) - .setWorkerFactory(workerFactory) - .build() - } -} +package com.yogeshpaliyal.common + +import android.app.Application +import android.content.Intent +import android.util.Log +import androidx.hilt.work.HiltWorkerFactory +import androidx.work.Configuration +import com.google.android.material.color.DynamicColors +import javax.inject.Inject +import kotlin.system.exitProcess + +abstract class CommonMyApplication : Application(), Configuration.Provider { + + @Inject + lateinit var workerFactory: HiltWorkerFactory + + abstract fun getCrashActivityIntent(throwable: Throwable): Intent + + override fun onCreate() { + super.onCreate() + Thread.setDefaultUncaughtExceptionHandler { _, throwable -> + Log.d("MyApplication", "crashed ") + val intent = getCrashActivityIntent(throwable) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + startActivity(intent) + exitProcess(1) + } + + DynamicColors.applyToActivitiesIfAvailable(this) + } + + override fun getWorkManagerConfiguration(): Configuration { + return Configuration.Builder() + .setMinimumLoggingLevel(android.util.Log.INFO) + .setWorkerFactory(workerFactory) + .build() + } +} diff --git a/common/src/main/java/com/yogeshpaliyal/common/constants/AccountType.kt b/common/src/main/java/com/yogeshpaliyal/common/constants/AccountType.kt index 916ff281..2af6a91d 100644 --- a/common/src/main/java/com/yogeshpaliyal/common/constants/AccountType.kt +++ b/common/src/main/java/com/yogeshpaliyal/common/constants/AccountType.kt @@ -1,11 +1,11 @@ -package com.yogeshpaliyal.common.constants - -annotation class AccountType() { - companion object { - const val DEFAULT = 1 // used to store password and user information - const val TOTP = 2 // used to store Time base - One time Password - /* const val HOTP = 3 - const val MOTP = 4 - const val STEAM = 5*/ - } -} +package com.yogeshpaliyal.common.constants + +annotation class AccountType() { + companion object { + const val DEFAULT = 1 // used to store password and user information + const val TOTP = 2 // used to store Time base - One time Password + /* const val HOTP = 3 + const val MOTP = 4 + const val STEAM = 5*/ + } +} diff --git a/common/src/main/java/com/yogeshpaliyal/common/constants/IntentKeys.kt b/common/src/main/java/com/yogeshpaliyal/common/constants/IntentKeys.kt index 93485754..436eb3bd 100644 --- a/common/src/main/java/com/yogeshpaliyal/common/constants/IntentKeys.kt +++ b/common/src/main/java/com/yogeshpaliyal/common/constants/IntentKeys.kt @@ -1,5 +1,5 @@ -package com.yogeshpaliyal.common.constants - -object IntentKeys { - const val SCANNED_TEXT = "scanned_text" -} +package com.yogeshpaliyal.common.constants + +object IntentKeys { + const val SCANNED_TEXT = "scanned_text" +} diff --git a/common/src/main/java/com/yogeshpaliyal/common/constants/RequestCodes.kt b/common/src/main/java/com/yogeshpaliyal/common/constants/RequestCodes.kt index b97ca827..f1e7cf1a 100644 --- a/common/src/main/java/com/yogeshpaliyal/common/constants/RequestCodes.kt +++ b/common/src/main/java/com/yogeshpaliyal/common/constants/RequestCodes.kt @@ -1,5 +1,5 @@ -package com.yogeshpaliyal.common.constants - -object RequestCodes { - const val SCANNER = 342 -} +package com.yogeshpaliyal.common.constants + +object RequestCodes { + const val SCANNER = 342 +} diff --git a/common/src/main/java/com/yogeshpaliyal/common/data/AccountModel.kt b/common/src/main/java/com/yogeshpaliyal/common/data/AccountModel.kt index 61715b86..924b3858 100644 --- a/common/src/main/java/com/yogeshpaliyal/common/data/AccountModel.kt +++ b/common/src/main/java/com/yogeshpaliyal/common/data/AccountModel.kt @@ -1,68 +1,68 @@ -package com.yogeshpaliyal.common.data - -import androidx.annotation.Keep -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey -import com.google.gson.annotations.SerializedName -import com.yogeshpaliyal.common.constants.AccountType -import com.yogeshpaliyal.common.utils.TOTPHelper -import com.yogeshpaliyal.common.utils.getRandomString - -/* -* @author Yogesh Paliyal -* techpaliyal@gmail.com -* https://techpaliyal.com -* created on 30-01-2021 20:38 -*/ -@Keep -@Entity(tableName = "account") -data class AccountModel( - @PrimaryKey(autoGenerate = true) - @ColumnInfo(name = "id") - @SerializedName("id") - var id: Long? = null, - - @ColumnInfo(name = "title") - @SerializedName("title") - var title: String? = null, - - @ColumnInfo(name = "unique_id") - @SerializedName("unique_id") - var uniqueId: String? = getRandomString(), - - @ColumnInfo(name = "username") - @SerializedName("username") - var username: String? = null, - - @ColumnInfo(name = "password") - @SerializedName("password") - var password: String? = null, // TOTP secret when type is TOTP - - @ColumnInfo(name = "site") - @SerializedName("site") - var site: String? = null, - - @ColumnInfo(name = "notes") - @SerializedName("notes") - var notes: String? = null, - - @ColumnInfo(name = "tags") - @SerializedName("tags") - var tags: String? = null, - - @AccountType - @ColumnInfo(name = "type") - @SerializedName("type") - var type: Int? = AccountType.DEFAULT -) { - - fun getInitials() = ( - title?.firstOrNull() ?: username?.firstOrNull() ?: site?.firstOrNull() - ?: notes?.firstOrNull() ?: 'K' - ).toString() - - fun getOtp(): String = TOTPHelper.generate(password) - - fun getTOtpProgress() = TOTPHelper.getProgress().toInt() -} +package com.yogeshpaliyal.common.data + +import androidx.annotation.Keep +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.google.gson.annotations.SerializedName +import com.yogeshpaliyal.common.constants.AccountType +import com.yogeshpaliyal.common.utils.TOTPHelper +import com.yogeshpaliyal.common.utils.getRandomString + +/* +* @author Yogesh Paliyal +* techpaliyal@gmail.com +* https://techpaliyal.com +* created on 30-01-2021 20:38 +*/ +@Keep +@Entity(tableName = "account") +data class AccountModel( + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = "id") + @SerializedName("id") + var id: Long? = null, + + @ColumnInfo(name = "title") + @SerializedName("title") + var title: String? = null, + + @ColumnInfo(name = "unique_id") + @SerializedName("unique_id") + var uniqueId: String? = getRandomString(), + + @ColumnInfo(name = "username") + @SerializedName("username") + var username: String? = null, + + @ColumnInfo(name = "password") + @SerializedName("password") + var password: String? = null, // TOTP secret when type is TOTP + + @ColumnInfo(name = "site") + @SerializedName("site") + var site: String? = null, + + @ColumnInfo(name = "notes") + @SerializedName("notes") + var notes: String? = null, + + @ColumnInfo(name = "tags") + @SerializedName("tags") + var tags: String? = null, + + @AccountType + @ColumnInfo(name = "type") + @SerializedName("type") + var type: Int? = AccountType.DEFAULT +) { + + fun getInitials() = ( + title?.firstOrNull() ?: username?.firstOrNull() ?: site?.firstOrNull() + ?: notes?.firstOrNull() ?: 'K' + ).toString() + + fun getOtp(): String = TOTPHelper.generate(password) + + fun getTOtpProgress() = TOTPHelper.getProgress().toInt() +} diff --git a/common/src/main/java/com/yogeshpaliyal/common/data/BackupData.kt b/common/src/main/java/com/yogeshpaliyal/common/data/BackupData.kt index 9889f0ae..0c5c38be 100644 --- a/common/src/main/java/com/yogeshpaliyal/common/data/BackupData.kt +++ b/common/src/main/java/com/yogeshpaliyal/common/data/BackupData.kt @@ -1,21 +1,21 @@ -package com.yogeshpaliyal.common.data - -import androidx.annotation.Keep -import com.google.gson.annotations.Expose -import com.google.gson.annotations.SerializedName - -/* -* @author Yogesh Paliyal -* techpaliyal@gmail.com -* https://techpaliyal.com -* created on 23-02-2021 20:48 -*/ -@Keep -data class BackupData( - @SerializedName("version") - @Expose - val version: Int, - @SerializedName("data") - @Expose - val data: List -) +package com.yogeshpaliyal.common.data + +import androidx.annotation.Keep +import com.google.gson.annotations.Expose +import com.google.gson.annotations.SerializedName + +/* +* @author Yogesh Paliyal +* techpaliyal@gmail.com +* https://techpaliyal.com +* created on 23-02-2021 20:48 +*/ +@Keep +data class BackupData( + @SerializedName("version") + @Expose + val version: Int, + @SerializedName("data") + @Expose + val data: List +) diff --git a/common/src/main/java/com/yogeshpaliyal/common/db/DbDao.kt b/common/src/main/java/com/yogeshpaliyal/common/db/DbDao.kt index 0afd660d..170f89c3 100644 --- a/common/src/main/java/com/yogeshpaliyal/common/db/DbDao.kt +++ b/common/src/main/java/com/yogeshpaliyal/common/db/DbDao.kt @@ -1,95 +1,95 @@ -package com.yogeshpaliyal.common.db - -import androidx.lifecycle.LiveData -import androidx.room.Dao -import androidx.room.Delete -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import com.yogeshpaliyal.common.data.AccountModel -import kotlinx.coroutines.flow.Flow - -/* -* @author Yogesh Paliyal -* techpaliyal@gmail.com -* https://techpaliyal.com -* created on 30-01-2021 21:43 -*/ - -@Dao -interface DbDao { - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertOrUpdateAccount(vararg accountModel: AccountModel) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertOrUpdateAccount(accountModel: List) - - @Query("SELECT * FROM account ORDER BY title ASC") - fun getAllAccounts(): LiveData> - - @Query("SELECT * FROM account ORDER BY title ASC") - suspend fun getAllAccountsList(): List - - @Query( - "SELECT * FROM account " + - "WHERE " + - "CASE WHEN :tag IS NOT NULL " + - "THEN tags = :tag " + - "ELSE 1 END " + - "AND ((username LIKE '%'||:query||'%' ) " + - "OR (title LIKE '%'||:query||'%' ) " + - "OR (notes LIKE '%'||:query||'%' )) " + - "ORDER BY" + - " CASE" + - " WHEN :sortingField = 'username' THEN username" + - " WHEN :sortingField = 'title' THEN title" + - " WHEN :sortingField = 'notes' THEN notes" + - " END ASC" - ) - fun getAllAccountsAscending( - query: String?, - tag: String?, - sortingField: String? - ): List - - @Query( - "SELECT * FROM account " + - "WHERE " + - "CASE WHEN :tag IS NOT NULL " + - "THEN tags = :tag " + - "ELSE 1 END " + - "AND ((username LIKE '%'||:query||'%' ) " + - "OR (title LIKE '%'||:query||'%' ) " + - "OR (notes LIKE '%'||:query||'%' )) " + - "ORDER BY" + - " CASE" + - " WHEN :sortingField = 'username' THEN username" + - " WHEN :sortingField = 'title' THEN title" + - " WHEN :sortingField = 'notes' THEN notes" + - " END DESC" - ) - fun getAllAccountsDescending( - query: String?, - tag: String?, - sortingField: String? - ): List - - @Query("SELECT * FROM account WHERE id = :id") - suspend fun getAccount(id: Long?): AccountModel? - - @Query("SELECT * FROM account WHERE unique_id = :uniqueId") - suspend fun getAccount(uniqueId: String?): AccountModel? - - @Query("SELECT DISTINCT tags FROM account") - fun getTags(): Flow> - - @Query("DELETE from account WHERE id = :id") - suspend fun deleteAccount(id: Long?) - - @Query("DELETE from account WHERE unique_id = :uniqueId") - suspend fun deleteAccount(uniqueId: String?) - - @Delete - suspend fun deleteAccount(accountModel: AccountModel) -} +package com.yogeshpaliyal.common.db + +import androidx.lifecycle.LiveData +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.yogeshpaliyal.common.data.AccountModel +import kotlinx.coroutines.flow.Flow + +/* +* @author Yogesh Paliyal +* techpaliyal@gmail.com +* https://techpaliyal.com +* created on 30-01-2021 21:43 +*/ + +@Dao +interface DbDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertOrUpdateAccount(vararg accountModel: AccountModel) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertOrUpdateAccount(accountModel: List) + + @Query("SELECT * FROM account ORDER BY title ASC") + fun getAllAccounts(): LiveData> + + @Query("SELECT * FROM account ORDER BY title ASC") + suspend fun getAllAccountsList(): List + + @Query( + "SELECT * FROM account " + + "WHERE " + + "CASE WHEN :tag IS NOT NULL " + + "THEN tags = :tag " + + "ELSE 1 END " + + "AND ((username LIKE '%'||:query||'%' ) " + + "OR (title LIKE '%'||:query||'%' ) " + + "OR (notes LIKE '%'||:query||'%' )) " + + "ORDER BY" + + " CASE" + + " WHEN :sortingField = 'username' THEN username" + + " WHEN :sortingField = 'title' THEN title" + + " WHEN :sortingField = 'notes' THEN notes" + + " END ASC" + ) + fun getAllAccountsAscending( + query: String?, + tag: String?, + sortingField: String? + ): List + + @Query( + "SELECT * FROM account " + + "WHERE " + + "CASE WHEN :tag IS NOT NULL " + + "THEN tags = :tag " + + "ELSE 1 END " + + "AND ((username LIKE '%'||:query||'%' ) " + + "OR (title LIKE '%'||:query||'%' ) " + + "OR (notes LIKE '%'||:query||'%' )) " + + "ORDER BY" + + " CASE" + + " WHEN :sortingField = 'username' THEN username" + + " WHEN :sortingField = 'title' THEN title" + + " WHEN :sortingField = 'notes' THEN notes" + + " END DESC" + ) + fun getAllAccountsDescending( + query: String?, + tag: String?, + sortingField: String? + ): List + + @Query("SELECT * FROM account WHERE id = :id") + suspend fun getAccount(id: Long?): AccountModel? + + @Query("SELECT * FROM account WHERE unique_id = :uniqueId") + suspend fun getAccount(uniqueId: String?): AccountModel? + + @Query("SELECT DISTINCT tags FROM account") + fun getTags(): Flow> + + @Query("DELETE from account WHERE id = :id") + suspend fun deleteAccount(id: Long?) + + @Query("DELETE from account WHERE unique_id = :uniqueId") + suspend fun deleteAccount(uniqueId: String?) + + @Delete + suspend fun deleteAccount(accountModel: AccountModel) +} diff --git a/common/src/main/java/com/yogeshpaliyal/common/dbhelper/CryptoException.kt b/common/src/main/java/com/yogeshpaliyal/common/dbhelper/CryptoException.kt index 2450cf2b..06104cbf 100644 --- a/common/src/main/java/com/yogeshpaliyal/common/dbhelper/CryptoException.kt +++ b/common/src/main/java/com/yogeshpaliyal/common/dbhelper/CryptoException.kt @@ -1,13 +1,13 @@ -package com.yogeshpaliyal.common.dbhelper - -/* -* @author Yogesh Paliyal -* techpaliyal@gmail.com -* https://techpaliyal.com -* created on 07-02-2021 18:52 -*/ - -class CryptoException : Exception { - constructor() - constructor(message: String?, throwable: Throwable?) : super(message, throwable) -} +package com.yogeshpaliyal.common.dbhelper + +/* +* @author Yogesh Paliyal +* techpaliyal@gmail.com +* https://techpaliyal.com +* created on 07-02-2021 18:52 +*/ + +class CryptoException : Exception { + constructor() + constructor(message: String?, throwable: Throwable?) : super(message, throwable) +} diff --git a/common/src/main/java/com/yogeshpaliyal/common/dbhelper/DbBackupRestore.kt b/common/src/main/java/com/yogeshpaliyal/common/dbhelper/DbBackupRestore.kt index d603466e..b1d04565 100644 --- a/common/src/main/java/com/yogeshpaliyal/common/dbhelper/DbBackupRestore.kt +++ b/common/src/main/java/com/yogeshpaliyal/common/dbhelper/DbBackupRestore.kt @@ -1,73 +1,73 @@ -package com.yogeshpaliyal.common.dbhelper - -import android.content.ContentResolver -import android.net.Uri -import androidx.room.withTransaction -import com.google.gson.Gson -import com.yogeshpaliyal.common.AppDatabase -import com.yogeshpaliyal.common.DB_VERSION_3 -import com.yogeshpaliyal.common.DB_VERSION_5 -import com.yogeshpaliyal.common.constants.AccountType -import com.yogeshpaliyal.common.data.BackupData -import com.yogeshpaliyal.common.utils.getRandomString -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -/* -* @author Yogesh Paliyal -* techpaliyal@gmail.com -* https://techpaliyal.com -* created on 20-02-2021 19:31 -*/ - -suspend fun AppDatabase.createBackup( - key: String, - contentResolver: ContentResolver, - fileUri: Uri? -) = - withContext(Dispatchers.IO) { - fileUri ?: return@withContext false - val data = getDao().getAllAccountsList() - - val json = - Gson().toJson(BackupData(this@createBackup.openHelper.readableDatabase.version, data)) - val fileStream = contentResolver.openOutputStream(fileUri) - EncryptionHelper.doCryptoEncrypt(key, json, fileStream) - - return@withContext true - } - -suspend fun AppDatabase.restoreBackup( - key: String, - contentResolver: ContentResolver, - fileUri: Uri? -) = withContext(Dispatchers.IO) { - fileUri ?: return@withContext false - - val restoredFile = try { - EncryptionHelper.doCryptoDecrypt(key, contentResolver.openInputStream(fileUri)) - } catch (e: CryptoException) { - e.printStackTrace() - return@withContext false - } - - return@withContext Gson().fromJson(restoredFile, BackupData::class.java)?.let { data -> - if (data.version == DB_VERSION_3) { - for (datum in data.data) { - getRandomString().also { datum.uniqueId = it } - } - } - if (data.version < DB_VERSION_5) { - for (datum in data.data) { - datum.type = AccountType.DEFAULT - } - } - data.data.forEach { - it.id = null - } - withTransaction { - getDao().insertOrUpdateAccount(data.data) - } - true - } ?: false -} +package com.yogeshpaliyal.common.dbhelper + +import android.content.ContentResolver +import android.net.Uri +import androidx.room.withTransaction +import com.google.gson.Gson +import com.yogeshpaliyal.common.AppDatabase +import com.yogeshpaliyal.common.DB_VERSION_3 +import com.yogeshpaliyal.common.DB_VERSION_5 +import com.yogeshpaliyal.common.constants.AccountType +import com.yogeshpaliyal.common.data.BackupData +import com.yogeshpaliyal.common.utils.getRandomString +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +/* +* @author Yogesh Paliyal +* techpaliyal@gmail.com +* https://techpaliyal.com +* created on 20-02-2021 19:31 +*/ + +suspend fun AppDatabase.createBackup( + key: String, + contentResolver: ContentResolver, + fileUri: Uri? +) = + withContext(Dispatchers.IO) { + fileUri ?: return@withContext false + val data = getDao().getAllAccountsList() + + val json = + Gson().toJson(BackupData(this@createBackup.openHelper.readableDatabase.version, data)) + val fileStream = contentResolver.openOutputStream(fileUri) + EncryptionHelper.doCryptoEncrypt(key, json, fileStream) + + return@withContext true + } + +suspend fun AppDatabase.restoreBackup( + key: String, + contentResolver: ContentResolver, + fileUri: Uri? +) = withContext(Dispatchers.IO) { + fileUri ?: return@withContext false + + val restoredFile = try { + EncryptionHelper.doCryptoDecrypt(key, contentResolver.openInputStream(fileUri)) + } catch (e: CryptoException) { + e.printStackTrace() + return@withContext false + } + + return@withContext Gson().fromJson(restoredFile, BackupData::class.java)?.let { data -> + if (data.version == DB_VERSION_3) { + for (datum in data.data) { + getRandomString().also { datum.uniqueId = it } + } + } + if (data.version < DB_VERSION_5) { + for (datum in data.data) { + datum.type = AccountType.DEFAULT + } + } + data.data.forEach { + it.id = null + } + withTransaction { + getDao().insertOrUpdateAccount(data.data) + } + true + } ?: false +} diff --git a/common/src/main/java/com/yogeshpaliyal/common/dbhelper/EncryptionHelper.kt b/common/src/main/java/com/yogeshpaliyal/common/dbhelper/EncryptionHelper.kt index 6f777411..72c25832 100644 --- a/common/src/main/java/com/yogeshpaliyal/common/dbhelper/EncryptionHelper.kt +++ b/common/src/main/java/com/yogeshpaliyal/common/dbhelper/EncryptionHelper.kt @@ -1,118 +1,118 @@ -package com.yogeshpaliyal.common.dbhelper - -import java.io.IOException -import java.io.InputStream -import java.io.OutputStream -import java.security.InvalidKeyException -import java.security.Key -import java.security.NoSuchAlgorithmException -import javax.crypto.BadPaddingException -import javax.crypto.Cipher -import javax.crypto.CipherInputStream -import javax.crypto.CipherOutputStream -import javax.crypto.IllegalBlockSizeException -import javax.crypto.NoSuchPaddingException -import javax.crypto.spec.IvParameterSpec -import javax.crypto.spec.SecretKeySpec - -/* -* @author Yogesh Paliyal -* techpaliyal@gmail.com -* https://techpaliyal.com -* https://yogeshpaliyal.com -* created on 07-02-2021 18:50 -*/ - -private const val BUFFER_SIZE = 4096 - -object EncryptionHelper { - private const val ALGORITHM = "AES" - private const val TRANSFORMATION = "AES/CBC/PKCS5Padding" - - private val iV = byteArrayOf(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) - - @Throws(CryptoException::class) - fun doCryptoEncrypt( - key: String, - data: String, - outputFile: OutputStream? - ) { - try { - val secretKey: Key = - SecretKeySpec(key.toByteArray(), ALGORITHM) - val cipher = Cipher.getInstance(TRANSFORMATION) - - cipher.init(Cipher.ENCRYPT_MODE, secretKey, IvParameterSpec(iV)) - - data.byteInputStream().use { - val inputStream = it - outputFile?.use { - val outputStream = it - CipherOutputStream(outputStream, cipher).use { - inputStream.copyTo(it, BUFFER_SIZE) - } - } - } - } catch (ex: NoSuchPaddingException) { - // Log.d("TestingEnc","NoSuchPaddingException") - throw CryptoException("Error encrypting/decrypting file", ex) - } catch (ex: NoSuchAlgorithmException) { - // Log.d("TestingEnc","NoSuchAlgorithmException") - throw CryptoException("Error encrypting/decrypting file", ex) - } catch (ex: InvalidKeyException) { - // Log.d("TestingEnc","InvalidKeyException") - throw CryptoException("Error encrypting/decrypting file", ex) - } catch (ex: BadPaddingException) { - // Log.d("TestingEnc","BadPaddingException") - throw CryptoException("Error encrypting/decrypting file", ex) - } catch (ex: IllegalBlockSizeException) { - // Log.d("TestingEnc","IllegalBlockSizeException") - throw CryptoException("Error encrypting/decrypting file", ex) - } catch (ex: IOException) { - // Log.d("TestingEnc","IOException") - throw CryptoException("Error encrypting/decrypting file", ex) - } - } - - @Throws(CryptoException::class) - fun doCryptoDecrypt( - key: String, - inputFile: InputStream? - ): String { - var data = "" - try { - val secretKey: Key = - SecretKeySpec(key.toByteArray(), ALGORITHM) - val cipher = Cipher.getInstance(TRANSFORMATION) - - cipher.init(Cipher.DECRYPT_MODE, secretKey, IvParameterSpec(iV)) - - inputFile.use { - val inputStream = it - CipherInputStream(inputStream, cipher).use { - data = String(it.readBytes()) - } - } - } catch (ex: NoSuchPaddingException) { - // Log.d("TestingEnc","NoSuchPaddingException") - throw CryptoException("Error encrypting/decrypting file", ex) - } catch (ex: NoSuchAlgorithmException) { - // Log.d("TestingEnc","NoSuchAlgorithmException") - throw CryptoException("Error encrypting/decrypting file", ex) - } catch (ex: InvalidKeyException) { - // Log.d("TestingEnc","InvalidKeyException") - throw CryptoException("Error encrypting/decrypting file", ex) - } catch (ex: BadPaddingException) { - // Log.d("TestingEnc","BadPaddingException") - throw CryptoException("Error encrypting/decrypting file", ex) - } catch (ex: IllegalBlockSizeException) { - // Log.d("TestingEnc","IllegalBlockSizeException") - throw CryptoException("Error encrypting/decrypting file", ex) - } catch (ex: IOException) { - // Log.d("TestingEnc","IOException") - throw CryptoException("Error encrypting/decrypting file", ex) - } - - return data - } -} +package com.yogeshpaliyal.common.dbhelper + +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.security.InvalidKeyException +import java.security.Key +import java.security.NoSuchAlgorithmException +import javax.crypto.BadPaddingException +import javax.crypto.Cipher +import javax.crypto.CipherInputStream +import javax.crypto.CipherOutputStream +import javax.crypto.IllegalBlockSizeException +import javax.crypto.NoSuchPaddingException +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +/* +* @author Yogesh Paliyal +* techpaliyal@gmail.com +* https://techpaliyal.com +* https://yogeshpaliyal.com +* created on 07-02-2021 18:50 +*/ + +private const val BUFFER_SIZE = 4096 + +object EncryptionHelper { + private const val ALGORITHM = "AES" + private const val TRANSFORMATION = "AES/CBC/PKCS5Padding" + + private val iV = byteArrayOf(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + + @Throws(CryptoException::class) + fun doCryptoEncrypt( + key: String, + data: String, + outputFile: OutputStream? + ) { + try { + val secretKey: Key = + SecretKeySpec(key.toByteArray(), ALGORITHM) + val cipher = Cipher.getInstance(TRANSFORMATION) + + cipher.init(Cipher.ENCRYPT_MODE, secretKey, IvParameterSpec(iV)) + + data.byteInputStream().use { + val inputStream = it + outputFile?.use { + val outputStream = it + CipherOutputStream(outputStream, cipher).use { + inputStream.copyTo(it, BUFFER_SIZE) + } + } + } + } catch (ex: NoSuchPaddingException) { + // Log.d("TestingEnc","NoSuchPaddingException") + throw CryptoException("Error encrypting/decrypting file", ex) + } catch (ex: NoSuchAlgorithmException) { + // Log.d("TestingEnc","NoSuchAlgorithmException") + throw CryptoException("Error encrypting/decrypting file", ex) + } catch (ex: InvalidKeyException) { + // Log.d("TestingEnc","InvalidKeyException") + throw CryptoException("Error encrypting/decrypting file", ex) + } catch (ex: BadPaddingException) { + // Log.d("TestingEnc","BadPaddingException") + throw CryptoException("Error encrypting/decrypting file", ex) + } catch (ex: IllegalBlockSizeException) { + // Log.d("TestingEnc","IllegalBlockSizeException") + throw CryptoException("Error encrypting/decrypting file", ex) + } catch (ex: IOException) { + // Log.d("TestingEnc","IOException") + throw CryptoException("Error encrypting/decrypting file", ex) + } + } + + @Throws(CryptoException::class) + fun doCryptoDecrypt( + key: String, + inputFile: InputStream? + ): String { + var data: String + try { + val secretKey: Key = + SecretKeySpec(key.toByteArray(), ALGORITHM) + val cipher = Cipher.getInstance(TRANSFORMATION) + + cipher.init(Cipher.DECRYPT_MODE, secretKey, IvParameterSpec(iV)) + + inputFile.use { + val inputStream = it + CipherInputStream(inputStream, cipher).use { + data = String(it.readBytes()) + } + } + } catch (ex: NoSuchPaddingException) { + // Log.d("TestingEnc","NoSuchPaddingException") + throw CryptoException("Error encrypting/decrypting file", ex) + } catch (ex: NoSuchAlgorithmException) { + // Log.d("TestingEnc","NoSuchAlgorithmException") + throw CryptoException("Error encrypting/decrypting file", ex) + } catch (ex: InvalidKeyException) { + // Log.d("TestingEnc","InvalidKeyException") + throw CryptoException("Error encrypting/decrypting file", ex) + } catch (ex: BadPaddingException) { + // Log.d("TestingEnc","BadPaddingException") + throw CryptoException("Error encrypting/decrypting file", ex) + } catch (ex: IllegalBlockSizeException) { + // Log.d("TestingEnc","IllegalBlockSizeException") + throw CryptoException("Error encrypting/decrypting file", ex) + } catch (ex: IOException) { + // Log.d("TestingEnc","IOException") + throw CryptoException("Error encrypting/decrypting file", ex) + } + + return data + } +} diff --git a/common/src/main/java/com/yogeshpaliyal/common/di/module/AppModule.kt b/common/src/main/java/com/yogeshpaliyal/common/di/module/AppModule.kt index 5bfc3ab2..dc5cac13 100644 --- a/common/src/main/java/com/yogeshpaliyal/common/di/module/AppModule.kt +++ b/common/src/main/java/com/yogeshpaliyal/common/di/module/AppModule.kt @@ -1,50 +1,50 @@ -package com.yogeshpaliyal.common.di.module - -import android.content.Context -import androidx.room.Room -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase -import com.yogeshpaliyal.common.AppDatabase -import com.yogeshpaliyal.common.DB_VERSION_3 -import com.yogeshpaliyal.common.DB_VERSION_4 -import com.yogeshpaliyal.common.DB_VERSION_5 -import com.yogeshpaliyal.common.R -import com.yogeshpaliyal.common.utils.getRandomString -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -object AppModule { - - @Provides - @Singleton - fun getDb(@ApplicationContext context: Context): AppDatabase { - return Room.databaseBuilder( - context, - AppDatabase::class.java, - context.getString(R.string.app_name) - ).addMigrations(object : Migration(DB_VERSION_3, DB_VERSION_4) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE `account` ADD COLUMN `unique_id` TEXT") - database.query("select id,unique_id from `account` where unique_id IS NULL") - ?.use { - while (it.moveToNext()) { - val id = it.getInt(0) - val query = "update `account` set `unique_id` = '${getRandomString()}' where `id` = '$id'" - database.execSQL(query) - } - } - } - }).addMigrations(object : Migration(DB_VERSION_4, DB_VERSION_5) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE `account` ADD COLUMN `type` INT DEFAULT 0") - } - }) - .build() - } -} +package com.yogeshpaliyal.common.di.module + +import android.content.Context +import androidx.room.Room +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import com.yogeshpaliyal.common.AppDatabase +import com.yogeshpaliyal.common.DB_VERSION_3 +import com.yogeshpaliyal.common.DB_VERSION_4 +import com.yogeshpaliyal.common.DB_VERSION_5 +import com.yogeshpaliyal.common.R +import com.yogeshpaliyal.common.utils.getRandomString +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object AppModule { + + @Provides + @Singleton + fun getDb(@ApplicationContext context: Context): AppDatabase { + return Room.databaseBuilder( + context, + AppDatabase::class.java, + context.getString(R.string.app_name) + ).addMigrations(object : Migration(DB_VERSION_3, DB_VERSION_4) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE `account` ADD COLUMN `unique_id` TEXT") + database.query("select id,unique_id from `account` where unique_id IS NULL") + ?.use { + while (it.moveToNext()) { + val id = it.getInt(0) + val query = "update `account` set `unique_id` = '${getRandomString()}' where `id` = '$id'" + database.execSQL(query) + } + } + } + }).addMigrations(object : Migration(DB_VERSION_4, DB_VERSION_5) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE `account` ADD COLUMN `type` INT DEFAULT 0") + } + }) + .build() + } +} diff --git a/common/src/main/java/com/yogeshpaliyal/common/utils/BackupUtils.kt b/common/src/main/java/com/yogeshpaliyal/common/utils/BackupUtils.kt index 8125ef37..af609472 100644 --- a/common/src/main/java/com/yogeshpaliyal/common/utils/BackupUtils.kt +++ b/common/src/main/java/com/yogeshpaliyal/common/utils/BackupUtils.kt @@ -1,90 +1,90 @@ -package com.yogeshpaliyal.common.utils - -import android.content.Context -import android.net.Uri -import android.text.TextUtils -import androidx.documentfile.provider.DocumentFile -import com.yogeshpaliyal.common.AppDatabase -import com.yogeshpaliyal.common.dbhelper.createBackup -import java.security.SecureRandom - -/* -* @author Yogesh Paliyal -* techpaliyal@gmail.com -* https://techpaliyal.com -* created on 20-02-2021 22:10 -*/ - -fun getRandomString(sizeOfRandomString: Int): String { - val ALLOWED_CHARACTERS = - "0123456789qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM!@#$%&*_+" - val random = SecureRandom() - val sb = StringBuilder(sizeOfRandomString) - for (i in 0 until sizeOfRandomString) sb.append( - ALLOWED_CHARACTERS[ - random.nextInt( - ALLOWED_CHARACTERS.length - ) - ] - ) - return sb.toString() -} - -suspend fun Context?.canUserAccessBackupDirectory(): Boolean { - if (this != null) { - val backupDirectoryUri = getUri(getBackupDirectory()) - if (backupDirectoryUri != null) { - val backupDirectory = DocumentFile.fromTreeUri(this, backupDirectoryUri) - val listOfConditions = arrayListOf() - listOfConditions.add(backupDirectory != null) - listOfConditions.add(backupDirectory?.exists()) - listOfConditions.add(backupDirectory?.canRead()) - listOfConditions.add(backupDirectory?.canWrite()) - return listOfConditions.all { it == true } - } - } - return false -} - -/** - * @return Pair (Boolean to check if backup is for first time, is backup is for first time show user alert to save encryption key) - * Second Value contains the encryption key - */ -suspend fun Context?.backupAccounts( - appDb: AppDatabase, - selectedDirectory: Uri, - customFileName: String? = null -): Pair? { - this ?: return null - - val keyPair = getOrCreateBackupKey() - - val fileName = (customFileName ?: "key_pass_backup_${System.currentTimeMillis()}") + ".keypass" - - val directory = DocumentFile.fromTreeUri(this, selectedDirectory) - var docFile = directory?.findFile(fileName) - if (docFile == null) { - docFile = DocumentFile.fromTreeUri(this, selectedDirectory)?.createFile( - "*/*", - fileName - ) - } - - appDb.createBackup( - keyPair.second, - contentResolver, - docFile?.uri - ) - setBackupTime(System.currentTimeMillis()) - - return keyPair -} - -private fun getUri(string: String?): Uri? { - val uri = string - return if (TextUtils.isEmpty(uri)) { - null - } else { - Uri.parse(uri) - } -} +package com.yogeshpaliyal.common.utils + +import android.content.Context +import android.net.Uri +import android.text.TextUtils +import androidx.documentfile.provider.DocumentFile +import com.yogeshpaliyal.common.AppDatabase +import com.yogeshpaliyal.common.dbhelper.createBackup +import java.security.SecureRandom + +/* +* @author Yogesh Paliyal +* techpaliyal@gmail.com +* https://techpaliyal.com +* created on 20-02-2021 22:10 +*/ + +fun getRandomString(sizeOfRandomString: Int): String { + val ALLOWED_CHARACTERS = + "0123456789qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM!@#$%&*_+" + val random = SecureRandom() + val sb = StringBuilder(sizeOfRandomString) + for (i in 0 until sizeOfRandomString) sb.append( + ALLOWED_CHARACTERS[ + random.nextInt( + ALLOWED_CHARACTERS.length + ) + ] + ) + return sb.toString() +} + +suspend fun Context?.canUserAccessBackupDirectory(): Boolean { + if (this != null) { + val backupDirectoryUri = getUri(getBackupDirectory()) + if (backupDirectoryUri != null) { + val backupDirectory = DocumentFile.fromTreeUri(this, backupDirectoryUri) + val listOfConditions = arrayListOf() + listOfConditions.add(backupDirectory != null) + listOfConditions.add(backupDirectory?.exists()) + listOfConditions.add(backupDirectory?.canRead()) + listOfConditions.add(backupDirectory?.canWrite()) + return listOfConditions.all { it == true } + } + } + return false +} + +/** + * @return Pair (Boolean to check if backup is for first time, is backup is for first time show user alert to save encryption key) + * Second Value contains the encryption key + */ +suspend fun Context?.backupAccounts( + appDb: AppDatabase, + selectedDirectory: Uri, + customFileName: String? = null +): Pair? { + this ?: return null + + val keyPair = getOrCreateBackupKey() + + val fileName = (customFileName ?: "key_pass_backup_${System.currentTimeMillis()}") + ".keypass" + + val directory = DocumentFile.fromTreeUri(this, selectedDirectory) + var docFile = directory?.findFile(fileName) + if (docFile == null) { + docFile = DocumentFile.fromTreeUri(this, selectedDirectory)?.createFile( + "*/*", + fileName + ) + } + + appDb.createBackup( + keyPair.second, + contentResolver, + docFile?.uri + ) + setBackupTime(System.currentTimeMillis()) + + return keyPair +} + +private fun getUri(string: String?): Uri? { + val uri = string + return if (TextUtils.isEmpty(uri)) { + null + } else { + Uri.parse(uri) + } +} diff --git a/common/src/main/java/com/yogeshpaliyal/common/utils/ContextExtensions.kt b/common/src/main/java/com/yogeshpaliyal/common/utils/ContextExtensions.kt index bb113ab5..87bc4f71 100644 --- a/common/src/main/java/com/yogeshpaliyal/common/utils/ContextExtensions.kt +++ b/common/src/main/java/com/yogeshpaliyal/common/utils/ContextExtensions.kt @@ -1,70 +1,70 @@ -/* - * 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.common.utils - -import android.annotation.SuppressLint -import android.content.Context -import android.graphics.Color -import android.graphics.drawable.Drawable -import android.util.TypedValue -import android.view.animation.AnimationUtils -import android.view.animation.Interpolator -import androidx.annotation.AttrRes -import androidx.annotation.ColorInt -import androidx.annotation.DrawableRes -import androidx.annotation.StyleRes -import androidx.appcompat.content.res.AppCompatResources -import androidx.core.content.res.use - -/** - * Retrieve a color from the current [android.content.res.Resources.Theme]. - */ -@ColorInt -@SuppressLint("Recycle") -fun Context.themeColor( - @AttrRes themeAttrId: Int -): Int { - return obtainStyledAttributes( - intArrayOf(themeAttrId) - ).use { - it.getColor(0, Color.MAGENTA) - } -} - -/** - * Retrieve a style from the current [android.content.res.Resources.Theme]. - */ -@StyleRes -fun Context.themeStyle(@AttrRes attr: Int): Int { - val tv = TypedValue() - theme.resolveAttribute(attr, tv, true) - return tv.data -} - -@SuppressLint("Recycle") -fun Context.themeInterpolator(@AttrRes attr: Int): Interpolator { - return AnimationUtils.loadInterpolator( - this, - obtainStyledAttributes(intArrayOf(attr)).use { - it.getResourceId(0, android.R.interpolator.fast_out_slow_in) - } - ) -} - -fun Context.getDrawableOrNull(@DrawableRes id: Int?): Drawable? { - return if (id == null || id == 0) null else AppCompatResources.getDrawable(this, id) -} +/* + * 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.common.utils + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.Drawable +import android.util.TypedValue +import android.view.animation.AnimationUtils +import android.view.animation.Interpolator +import androidx.annotation.AttrRes +import androidx.annotation.ColorInt +import androidx.annotation.DrawableRes +import androidx.annotation.StyleRes +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.content.res.use + +/** + * Retrieve a color from the current [android.content.res.Resources.Theme]. + */ +@ColorInt +@SuppressLint("Recycle") +fun Context.themeColor( + @AttrRes themeAttrId: Int +): Int { + return obtainStyledAttributes( + intArrayOf(themeAttrId) + ).use { + it.getColor(0, Color.MAGENTA) + } +} + +/** + * Retrieve a style from the current [android.content.res.Resources.Theme]. + */ +@StyleRes +fun Context.themeStyle(@AttrRes attr: Int): Int { + val tv = TypedValue() + theme.resolveAttribute(attr, tv, true) + return tv.data +} + +@SuppressLint("Recycle") +fun Context.themeInterpolator(@AttrRes attr: Int): Interpolator { + return AnimationUtils.loadInterpolator( + this, + obtainStyledAttributes(intArrayOf(attr)).use { + it.getResourceId(0, android.R.interpolator.fast_out_slow_in) + } + ) +} + +fun Context.getDrawableOrNull(@DrawableRes id: Int?): Drawable? { + return if (id == null || id == 0) null else AppCompatResources.getDrawable(this, id) +} diff --git a/common/src/main/java/com/yogeshpaliyal/common/utils/Event.kt b/common/src/main/java/com/yogeshpaliyal/common/utils/Event.kt index 0e8e376d..4905fc69 100644 --- a/common/src/main/java/com/yogeshpaliyal/common/utils/Event.kt +++ b/common/src/main/java/com/yogeshpaliyal/common/utils/Event.kt @@ -1,27 +1,27 @@ -package com.yogeshpaliyal.common.utils - -/*Used as a wrapper for data that is exposed via a LiveData that represents an - event.*/ - -open class Event(private val content: T) { - - var hasBeenHandled = false - private set // Allow external read but not write - - /** - * Returns the content and prevents its use again. - */ - fun getContentIfNotHandled(): T? { - return if (hasBeenHandled) { - null - } else { - hasBeenHandled = true - content - } - } - - /** - * Returns the content, even if it's already been handled. - */ - fun peekContent(): T = content -} +package com.yogeshpaliyal.common.utils + +/*Used as a wrapper for data that is exposed via a LiveData that represents an + event.*/ + +open class Event(private val content: T) { + + var hasBeenHandled = false + private set // Allow external read but not write + + /** + * Returns the content and prevents its use again. + */ + fun getContentIfNotHandled(): T? { + return if (hasBeenHandled) { + null + } else { + hasBeenHandled = true + content + } + } + + /** + * Returns the content, even if it's already been handled. + */ + fun peekContent(): T = content +} diff --git a/common/src/main/java/com/yogeshpaliyal/common/utils/FormatCalendar.kt b/common/src/main/java/com/yogeshpaliyal/common/utils/FormatCalendar.kt index 8c33c9d0..e123f579 100644 --- a/common/src/main/java/com/yogeshpaliyal/common/utils/FormatCalendar.kt +++ b/common/src/main/java/com/yogeshpaliyal/common/utils/FormatCalendar.kt @@ -1,19 +1,19 @@ -package com.yogeshpaliyal.common.utils - -import java.text.SimpleDateFormat -import java.util.Calendar -import java.util.Locale - -/* -* @author Yogesh Paliyal -* techpaliyal@gmail.com -* https://techpaliyal.com -* created on 23-03-2021 22:30 -*/ - -fun Long.formatCalendar(dateTimeFormat: String?): String? { - val calendar: Calendar = Calendar.getInstance() - calendar.timeInMillis = this - val simpleDateFormat = SimpleDateFormat(dateTimeFormat, Locale.US) - return simpleDateFormat.format(calendar.getTime()) -} +package com.yogeshpaliyal.common.utils + +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale + +/* +* @author Yogesh Paliyal +* techpaliyal@gmail.com +* https://techpaliyal.com +* created on 23-03-2021 22:30 +*/ + +fun Long.formatCalendar(dateTimeFormat: String?): String? { + val calendar: Calendar = Calendar.getInstance() + calendar.timeInMillis = this + val simpleDateFormat = SimpleDateFormat(dateTimeFormat, Locale.US) + return simpleDateFormat.format(calendar.getTime()) +} diff --git a/common/src/main/java/com/yogeshpaliyal/common/utils/GetAutoFillService.kt b/common/src/main/java/com/yogeshpaliyal/common/utils/GetAutoFillService.kt index d2da5b97..bb68bd83 100644 --- a/common/src/main/java/com/yogeshpaliyal/common/utils/GetAutoFillService.kt +++ b/common/src/main/java/com/yogeshpaliyal/common/utils/GetAutoFillService.kt @@ -1,18 +1,18 @@ -package com.yogeshpaliyal.common.utils - -import android.content.Context -import android.view.autofill.AutofillManager -import androidx.core.content.ContextCompat.getSystemService - -/* -* @author Yogesh Paliyal -* techpaliyal@gmail.com -* https://techpaliyal.com -* created on 31-01-2021 15:27 -*/ - -fun Context?.getAutoFillService() = if (this != null && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { - getSystemService(this, AutofillManager::class.java) -} else { - null -} +package com.yogeshpaliyal.common.utils + +import android.content.Context +import android.view.autofill.AutofillManager +import androidx.core.content.ContextCompat.getSystemService + +/* +* @author Yogesh Paliyal +* techpaliyal@gmail.com +* https://techpaliyal.com +* created on 31-01-2021 15:27 +*/ + +fun Context?.getAutoFillService() = if (this != null && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + getSystemService(this, AutofillManager::class.java) +} else { + null +} diff --git a/common/src/main/java/com/yogeshpaliyal/common/utils/GetRandomString.kt b/common/src/main/java/com/yogeshpaliyal/common/utils/GetRandomString.kt index 36f87213..ba43a02d 100644 --- a/common/src/main/java/com/yogeshpaliyal/common/utils/GetRandomString.kt +++ b/common/src/main/java/com/yogeshpaliyal/common/utils/GetRandomString.kt @@ -1,12 +1,12 @@ -package com.yogeshpaliyal.common.utils - -import java.util.UUID - -/* -* @author Yogesh Paliyal -* techpaliyal@gmail.com -* https://techpaliyal.com -* created on 22-01-2021 23:14 -*/ - -fun getRandomString() = UUID.randomUUID().toString() +package com.yogeshpaliyal.common.utils + +import java.util.UUID + +/* +* @author Yogesh Paliyal +* techpaliyal@gmail.com +* https://techpaliyal.com +* created on 22-01-2021 23:14 +*/ + +fun getRandomString() = UUID.randomUUID().toString() diff --git a/common/src/main/java/com/yogeshpaliyal/common/utils/IntentHelper.kt b/common/src/main/java/com/yogeshpaliyal/common/utils/IntentHelper.kt index 0fd1831a..c25364c7 100644 --- a/common/src/main/java/com/yogeshpaliyal/common/utils/IntentHelper.kt +++ b/common/src/main/java/com/yogeshpaliyal/common/utils/IntentHelper.kt @@ -1,75 +1,75 @@ -package com.yogeshpaliyal.common.utils - -import android.content.ActivityNotFoundException -import android.content.Context -import android.content.Intent -import android.net.Uri - -/* -* @author Yogesh Paliyal -* techpaliyal@gmail.com -* https://techpaliyal.com -* created on 26-12-2020 19:46 -*/ - -@JvmName("IntentHelper") -fun Context.email( - chooserTitle: String, - email: String = "", - subject: String = "", - text: String = "" -) { - val intent = Intent(Intent.ACTION_SENDTO) - intent.data = Uri.parse("mailto:") - - if (email.isNotEmpty()) { - intent.putExtra(Intent.EXTRA_EMAIL, arrayOf(email)) - } - - if (subject.isNotEmpty()) { - intent.putExtra(Intent.EXTRA_SUBJECT, subject) - } - - if (text.isNotEmpty()) { - intent.putExtra(Intent.EXTRA_TEXT, text) - } - - startActivity(Intent.createChooser(intent, chooserTitle)) -} - -fun Context.makeCall(chooserTitle: String, number: String): Boolean { - try { - val intent = Intent(Intent.ACTION_VIEW) - intent.data = Uri.parse("tel:$number") - startActivity(Intent.createChooser(intent, chooserTitle)) - return true - } catch (e: Exception) { - e.printStackTrace() - return false - } -} - -fun Context.sendSMS(chooserTitle: String, number: String, text: String = ""): Boolean { - try { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse("sms:$number")) - intent.putExtra("sms_body", text) - startActivity(Intent.createChooser(intent, chooserTitle)) - return true - } catch (e: Exception) { - e.printStackTrace() - return false - } -} - -fun Context.share(chooserTitle: String, text: String): Boolean { - try { - val intent = Intent(Intent.ACTION_SEND) - intent.putExtra(Intent.EXTRA_TEXT, text) - intent.type = "text/plain" - startActivity(Intent.createChooser(intent, chooserTitle)) - return true - } catch (e: ActivityNotFoundException) { - e.printStackTrace() - return false - } -} +package com.yogeshpaliyal.common.utils + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.net.Uri + +/* +* @author Yogesh Paliyal +* techpaliyal@gmail.com +* https://techpaliyal.com +* created on 26-12-2020 19:46 +*/ + +@JvmName("IntentHelper") +fun Context.email( + chooserTitle: String, + email: String = "", + subject: String = "", + text: String = "" +) { + val intent = Intent(Intent.ACTION_SENDTO) + intent.data = Uri.parse("mailto:") + + if (email.isNotEmpty()) { + intent.putExtra(Intent.EXTRA_EMAIL, arrayOf(email)) + } + + if (subject.isNotEmpty()) { + intent.putExtra(Intent.EXTRA_SUBJECT, subject) + } + + if (text.isNotEmpty()) { + intent.putExtra(Intent.EXTRA_TEXT, text) + } + + startActivity(Intent.createChooser(intent, chooserTitle)) +} + +fun Context.makeCall(chooserTitle: String, number: String): Boolean { + try { + val intent = Intent(Intent.ACTION_VIEW) + intent.data = Uri.parse("tel:$number") + startActivity(Intent.createChooser(intent, chooserTitle)) + return true + } catch (e: Exception) { + e.printStackTrace() + return false + } +} + +fun Context.sendSMS(chooserTitle: String, number: String, text: String = ""): Boolean { + try { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse("sms:$number")) + intent.putExtra("sms_body", text) + startActivity(Intent.createChooser(intent, chooserTitle)) + return true + } catch (e: Exception) { + e.printStackTrace() + return false + } +} + +fun Context.share(chooserTitle: String, text: String): Boolean { + try { + val intent = Intent(Intent.ACTION_SEND) + intent.putExtra(Intent.EXTRA_TEXT, text) + intent.type = "text/plain" + startActivity(Intent.createChooser(intent, chooserTitle)) + return true + } catch (e: ActivityNotFoundException) { + e.printStackTrace() + return false + } +} diff --git a/common/src/main/java/com/yogeshpaliyal/common/utils/Normalize.kt b/common/src/main/java/com/yogeshpaliyal/common/utils/Normalize.kt index 34f4812b..57eb8f7f 100644 --- a/common/src/main/java/com/yogeshpaliyal/common/utils/Normalize.kt +++ b/common/src/main/java/com/yogeshpaliyal/common/utils/Normalize.kt @@ -1,44 +1,44 @@ -/* - * 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.common.utils - -/** - * Coerce the receiving Float between inputMin and inputMax and linearly interpolate to the - * outputMin to outputMax scale. This function is able to handle ranges which span negative and - * positive numbers. - * - * This differs from [lerp] as the input values are not required to be between 0 and 1. - */ -fun Float.normalize( - inputMin: Float, - inputMax: Float, - outputMin: Float, - outputMax: Float -): Float { - val result: Float? = if (this < inputMin) { - outputMin - } else if (this > inputMax) { - outputMax - } else { - null - } - - return result ?: ( - outputMin * (1 - (this - inputMin) / (inputMax - inputMin)) + - outputMax * ((this - inputMin) / (inputMax - inputMin)) - ) -} +/* + * 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.common.utils + +/** + * Coerce the receiving Float between inputMin and inputMax and linearly interpolate to the + * outputMin to outputMax scale. This function is able to handle ranges which span negative and + * positive numbers. + * + * This differs from [lerp] as the input values are not required to be between 0 and 1. + */ +fun Float.normalize( + inputMin: Float, + inputMax: Float, + outputMin: Float, + outputMax: Float +): Float { + val result: Float? = if (this < inputMin) { + outputMin + } else if (this > inputMax) { + outputMax + } else { + null + } + + return result ?: ( + outputMin * (1 - (this - inputMin) / (inputMax - inputMin)) + + outputMax * ((this - inputMin) / (inputMax - inputMin)) + ) +} diff --git a/common/src/main/java/com/yogeshpaliyal/common/utils/PasswordGenerator.kt b/common/src/main/java/com/yogeshpaliyal/common/utils/PasswordGenerator.kt index ce39101f..5061267f 100644 --- a/common/src/main/java/com/yogeshpaliyal/common/utils/PasswordGenerator.kt +++ b/common/src/main/java/com/yogeshpaliyal/common/utils/PasswordGenerator.kt @@ -1,46 +1,46 @@ -package com.yogeshpaliyal.common.utils - -class PasswordGenerator( - private var length: Int, - private var includeUpperCaseLetters: Boolean, - private var includeLowerCaseLetters: Boolean, - private var includeSymbols: Boolean, - private var includeNumbers: Boolean -) { - - constructor() : this(10, true, true, true, true) - - private val UPPER_CASE = 0 - private val LOWER_CASE = 1 - private val NUMBERS = 2 - private val SYMBOLS = 3 - - public fun generatePassword(): String { - var password = "" - val list = ArrayList() - if (includeUpperCaseLetters) { - list.add(UPPER_CASE) - } - if (includeLowerCaseLetters) { - list.add(LOWER_CASE) - } - if (includeNumbers) { - list.add(NUMBERS) - } - if (includeSymbols) { - list.add(SYMBOLS) - } - - for (i in 1..length) { - if (list.isNotEmpty()) { - when (list.random()) { - UPPER_CASE -> password += ('A'..'Z').random().toString() - LOWER_CASE -> password += ('a'..'z').random().toString() - NUMBERS -> password += ('0'..'9').random().toString() - SYMBOLS -> password += listOf('!', '@', '#', '$', '%', '&', '*', '+', '=', '-', '~', '?', '/', '_').random().toString() - } - } - } - return password - } -} +package com.yogeshpaliyal.common.utils + +class PasswordGenerator( + private var length: Int, + private var includeUpperCaseLetters: Boolean, + private var includeLowerCaseLetters: Boolean, + private var includeSymbols: Boolean, + private var includeNumbers: Boolean +) { + + constructor() : this(10, true, true, true, true) + + private val UPPER_CASE = 0 + private val LOWER_CASE = 1 + private val NUMBERS = 2 + private val SYMBOLS = 3 + + public fun generatePassword(): String { + var password = "" + val list = ArrayList() + if (includeUpperCaseLetters) { + list.add(UPPER_CASE) + } + if (includeLowerCaseLetters) { + list.add(LOWER_CASE) + } + if (includeNumbers) { + list.add(NUMBERS) + } + if (includeSymbols) { + list.add(SYMBOLS) + } + + for (i in 1..length) { + if (list.isNotEmpty()) { + when (list.random()) { + UPPER_CASE -> password += ('A'..'Z').random().toString() + LOWER_CASE -> password += ('a'..'z').random().toString() + NUMBERS -> password += ('0'..'9').random().toString() + SYMBOLS -> password += listOf('!', '@', '#', '$', '%', '&', '*', '+', '=', '-', '~', '?', '/', '_').random().toString() + } + } + } + return password + } +} diff --git a/common/src/main/java/com/yogeshpaliyal/common/utils/SharedPreferenceUtils.kt b/common/src/main/java/com/yogeshpaliyal/common/utils/SharedPreferenceUtils.kt index b845d6f5..8165a97a 100644 --- a/common/src/main/java/com/yogeshpaliyal/common/utils/SharedPreferenceUtils.kt +++ b/common/src/main/java/com/yogeshpaliyal/common/utils/SharedPreferenceUtils.kt @@ -1,118 +1,118 @@ -package com.yogeshpaliyal.common.utils - -import android.content.Context -import androidx.datastore.preferences.core.booleanPreferencesKey -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.longPreferencesKey -import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore -import kotlinx.coroutines.flow.first - -/* -* @author Yogesh Paliyal -* techpaliyal@gmail.com -* https://techpaliyal.com -* created on 21-02-2021 11:18 -*/ - -val Context.dataStore by preferencesDataStore( - name = "settings" -) - -const val BACKUP_KEY_LENGTH = 16 - -/** - * Pair - * 1st => true if key is created now & false if key is created previously - * - */ -suspend fun Context.getOrCreateBackupKey(reset: Boolean = false): Pair { - val sp = dataStore.data.first() - return if (sp.contains(BACKUP_KEY) && reset.not()) { - Pair(false, (sp[BACKUP_KEY]) ?: "") - } else { - val randomKey = getRandomString(BACKUP_KEY_LENGTH) - dataStore.edit { - it[BACKUP_KEY] = randomKey - } - Pair(true, randomKey) - } -} - -suspend fun Context.getKeyPassPassword(): String? { - return dataStore.data.first().get(KEYPASS_PASSWORD) -} - -suspend fun Context.setKeyPassPassword(password: String?) { - dataStore.edit { - if (password == null) { - it.remove(KEYPASS_PASSWORD) - } else { - it[KEYPASS_PASSWORD] = password - } - } -} - -suspend fun Context.isKeyPresent(): Boolean { - val sp = dataStore.data.first() - return sp.contains(BACKUP_KEY) -} - -suspend fun Context.saveKeyphrase(keyphrase: String) { - dataStore.edit { - it[BACKUP_KEY] = keyphrase - } -} - -suspend fun Context?.clearBackupKey() { - this?.dataStore?.edit { - it.remove(BACKUP_KEY) - } -} - -suspend fun Context?.setBackupDirectory(string: String) { - this?.dataStore?.edit { - it[BACKUP_DIRECTORY] = string - } -} - -suspend fun Context?.setBackupTime(time: Long) { - this?.dataStore?.edit { - it[BACKUP_DATE_TIME] = time - } -} - -suspend fun Context?.getBackupDirectory(): String { - return this?.dataStore?.data?.first()?.get(BACKUP_DIRECTORY) ?: "" -} - -suspend fun Context?.isAutoBackupEnabled(): Boolean { - return this?.dataStore?.data?.first()?.get(AUTO_BACKUP) ?: false -} - -suspend fun Context?.overrideAutoBackup(): Boolean { - return this?.dataStore?.data?.first()?.get(OVERRIDE_AUTO_BACKUP) ?: false -} - -suspend fun Context?.setOverrideAutoBackup(value: Boolean) { - this?.dataStore?.edit { - it[OVERRIDE_AUTO_BACKUP] = value - } -} - -suspend fun Context?.setAutoBackupEnabled(value: Boolean) { - this?.dataStore?.edit { - it[AUTO_BACKUP] = value - } -} - -suspend fun Context?.getBackupTime(): Long { - return this?.dataStore?.data?.first()?.get(BACKUP_DATE_TIME) ?: -1 -} - -private val BACKUP_KEY = stringPreferencesKey("backup_key") -private val KEYPASS_PASSWORD = stringPreferencesKey("keypass_password") -private val BACKUP_DIRECTORY = stringPreferencesKey("backup_directory") -private val BACKUP_DATE_TIME = longPreferencesKey("backup_date_time") -private val AUTO_BACKUP = booleanPreferencesKey("auto_backup") -private val OVERRIDE_AUTO_BACKUP = booleanPreferencesKey("override_auto_backup") +package com.yogeshpaliyal.common.utils + +import android.content.Context +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.longPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.first + +/* +* @author Yogesh Paliyal +* techpaliyal@gmail.com +* https://techpaliyal.com +* created on 21-02-2021 11:18 +*/ + +val Context.dataStore by preferencesDataStore( + name = "settings" +) + +const val BACKUP_KEY_LENGTH = 16 + +/** + * Pair + * 1st => true if key is created now & false if key is created previously + * + */ +suspend fun Context.getOrCreateBackupKey(reset: Boolean = false): Pair { + val sp = dataStore.data.first() + return if (sp.contains(BACKUP_KEY) && reset.not()) { + Pair(false, (sp[BACKUP_KEY]) ?: "") + } else { + val randomKey = getRandomString(BACKUP_KEY_LENGTH) + dataStore.edit { + it[BACKUP_KEY] = randomKey + } + Pair(true, randomKey) + } +} + +suspend fun Context.getKeyPassPassword(): String? { + return dataStore.data.first().get(KEYPASS_PASSWORD) +} + +suspend fun Context.setKeyPassPassword(password: String?) { + dataStore.edit { + if (password == null) { + it.remove(KEYPASS_PASSWORD) + } else { + it[KEYPASS_PASSWORD] = password + } + } +} + +suspend fun Context.isKeyPresent(): Boolean { + val sp = dataStore.data.first() + return sp.contains(BACKUP_KEY) +} + +suspend fun Context.saveKeyphrase(keyphrase: String) { + dataStore.edit { + it[BACKUP_KEY] = keyphrase + } +} + +suspend fun Context?.clearBackupKey() { + this?.dataStore?.edit { + it.remove(BACKUP_KEY) + } +} + +suspend fun Context?.setBackupDirectory(string: String) { + this?.dataStore?.edit { + it[BACKUP_DIRECTORY] = string + } +} + +suspend fun Context?.setBackupTime(time: Long) { + this?.dataStore?.edit { + it[BACKUP_DATE_TIME] = time + } +} + +suspend fun Context?.getBackupDirectory(): String { + return this?.dataStore?.data?.first()?.get(BACKUP_DIRECTORY) ?: "" +} + +suspend fun Context?.isAutoBackupEnabled(): Boolean { + return this?.dataStore?.data?.first()?.get(AUTO_BACKUP) ?: false +} + +suspend fun Context?.overrideAutoBackup(): Boolean { + return this?.dataStore?.data?.first()?.get(OVERRIDE_AUTO_BACKUP) ?: false +} + +suspend fun Context?.setOverrideAutoBackup(value: Boolean) { + this?.dataStore?.edit { + it[OVERRIDE_AUTO_BACKUP] = value + } +} + +suspend fun Context?.setAutoBackupEnabled(value: Boolean) { + this?.dataStore?.edit { + it[AUTO_BACKUP] = value + } +} + +suspend fun Context?.getBackupTime(): Long { + return this?.dataStore?.data?.first()?.get(BACKUP_DATE_TIME) ?: -1 +} + +private val BACKUP_KEY = stringPreferencesKey("backup_key") +private val KEYPASS_PASSWORD = stringPreferencesKey("keypass_password") +private val BACKUP_DIRECTORY = stringPreferencesKey("backup_directory") +private val BACKUP_DATE_TIME = longPreferencesKey("backup_date_time") +private val AUTO_BACKUP = booleanPreferencesKey("auto_backup") +private val OVERRIDE_AUTO_BACKUP = booleanPreferencesKey("override_auto_backup") diff --git a/common/src/main/java/com/yogeshpaliyal/common/worker/AutoBackupWorker.kt b/common/src/main/java/com/yogeshpaliyal/common/worker/AutoBackupWorker.kt index 1cc3a5e4..a8241b4d 100644 --- a/common/src/main/java/com/yogeshpaliyal/common/worker/AutoBackupWorker.kt +++ b/common/src/main/java/com/yogeshpaliyal/common/worker/AutoBackupWorker.kt @@ -1,39 +1,39 @@ -package com.yogeshpaliyal.common.worker - -import android.content.Context -import android.net.Uri -import androidx.hilt.work.HiltWorker -import androidx.work.CoroutineWorker -import androidx.work.WorkerParameters -import com.yogeshpaliyal.common.AppDatabase -import com.yogeshpaliyal.common.utils.backupAccounts -import com.yogeshpaliyal.common.utils.canUserAccessBackupDirectory -import com.yogeshpaliyal.common.utils.getBackupDirectory -import com.yogeshpaliyal.common.utils.overrideAutoBackup -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -@HiltWorker -class AutoBackupWorker @AssistedInject constructor( - @Assisted val appContext: Context, - @Assisted params: WorkerParameters, - val appDatabase: AppDatabase -) : - CoroutineWorker(appContext, params) { - override suspend fun doWork(): Result { - return withContext(Dispatchers.IO) { - if (appContext.canUserAccessBackupDirectory()) { - val selectedDirectory = Uri.parse(appContext.getBackupDirectory()) - appContext.backupAccounts( - appDatabase, - selectedDirectory, - if (appContext.overrideAutoBackup()) "key_pass_auto_backup" else null - ) - } - - Result.success() - } - } -} +package com.yogeshpaliyal.common.worker + +import android.content.Context +import android.net.Uri +import androidx.hilt.work.HiltWorker +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.yogeshpaliyal.common.AppDatabase +import com.yogeshpaliyal.common.utils.backupAccounts +import com.yogeshpaliyal.common.utils.canUserAccessBackupDirectory +import com.yogeshpaliyal.common.utils.getBackupDirectory +import com.yogeshpaliyal.common.utils.overrideAutoBackup +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +@HiltWorker +class AutoBackupWorker @AssistedInject constructor( + @Assisted val appContext: Context, + @Assisted params: WorkerParameters, + val appDatabase: AppDatabase +) : + CoroutineWorker(appContext, params) { + override suspend fun doWork(): Result { + return withContext(Dispatchers.IO) { + if (appContext.canUserAccessBackupDirectory()) { + val selectedDirectory = Uri.parse(appContext.getBackupDirectory()) + appContext.backupAccounts( + appDatabase, + selectedDirectory, + if (appContext.overrideAutoBackup()) "key_pass_auto_backup" else null + ) + } + + Result.success() + } + } +} diff --git a/common/src/main/java/com/yogeshpaliyal/common/worker/ExecuteAutoBackup.kt b/common/src/main/java/com/yogeshpaliyal/common/worker/ExecuteAutoBackup.kt index 8a725cf2..d93ba72a 100644 --- a/common/src/main/java/com/yogeshpaliyal/common/worker/ExecuteAutoBackup.kt +++ b/common/src/main/java/com/yogeshpaliyal/common/worker/ExecuteAutoBackup.kt @@ -1,18 +1,18 @@ -package com.yogeshpaliyal.common.worker - -import android.content.Context -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.WorkManager -import com.yogeshpaliyal.common.utils.isAutoBackupEnabled - -suspend fun Context?.executeAutoBackup() { - this ?: return - - if (this.isAutoBackupEnabled()) { - val work = OneTimeWorkRequestBuilder().build() - - WorkManager.getInstance(this.applicationContext) - .enqueueUniqueWork("AutoBackupWorker", ExistingWorkPolicy.KEEP, work) - } -} +package com.yogeshpaliyal.common.worker + +import android.content.Context +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import com.yogeshpaliyal.common.utils.isAutoBackupEnabled + +suspend fun Context?.executeAutoBackup() { + this ?: return + + if (this.isAutoBackupEnabled()) { + val work = OneTimeWorkRequestBuilder().build() + + WorkManager.getInstance(this.applicationContext) + .enqueueUniqueWork("AutoBackupWorker", ExistingWorkPolicy.KEEP, work) + } +}