mirror of
https://github.com/yogeshpaliyal/KeyPass.git
synced 2026-01-07 00:49:46 -06:00
Backup Screen Migration to Compose (#512)
This commit is contained in:
committed by
GitHub
parent
b3c94f1b04
commit
0e65479a85
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<DashboardActivity>()
|
||||
|
||||
@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<DashboardComposeActivity>()
|
||||
|
||||
@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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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<T> {
|
||||
fun onItemClick(view: View, model: T)
|
||||
fun onCopyClicked(model: T)
|
||||
}
|
||||
@@ -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<T> {
|
||||
fun onItemClick(view: View, model: T)
|
||||
}
|
||||
@@ -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, ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<AddTOTPViewModel>()
|
||||
|
||||
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<AddTOTPViewModel>()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Event<Unit>>()
|
||||
val goBack: LiveData<Event<Unit>> = _goBack
|
||||
|
||||
private val _error = MutableLiveData<Event<Int>>()
|
||||
val error: LiveData<Event<Int>> = _error
|
||||
|
||||
val secretKey = MutableLiveData<String>("")
|
||||
|
||||
val accountName = MutableLiveData<String>("")
|
||||
|
||||
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<Event<Unit>>()
|
||||
val goBack: LiveData<Event<Unit>> = _goBack
|
||||
|
||||
private val _error = MutableLiveData<Event<Int>>()
|
||||
val error: LiveData<Event<Int>> = _error
|
||||
|
||||
val secretKey = MutableLiveData<String>("")
|
||||
|
||||
val accountName = MutableLiveData<String>("")
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<out String>,
|
||||
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<out String>,
|
||||
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 }
|
||||
}
|
||||
|
||||
@@ -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<Int?>(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<Int?>(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<Preference>(getString(R.string.settings_start_backup))?.isVisible =
|
||||
isBackupEnabled.not()
|
||||
findPreference<Preference>(getString(R.string.settings_stop_backup))?.isVisible =
|
||||
isBackupEnabled
|
||||
|
||||
findPreference<Preference>(getString(R.string.settings_auto_backup))?.isVisible =
|
||||
isBackupEnabled
|
||||
findPreference<Preference>(getString(R.string.settings_auto_backup))?.summary =
|
||||
if (isAutoBackupEnabled) getString(R.string.enabled) else getString(R.string.disabled)
|
||||
|
||||
findPreference<PreferenceCategory>(getString(R.string.settings_cat_auto_backup))?.isVisible =
|
||||
isBackupEnabled && isAutoBackupEnabled
|
||||
|
||||
findPreference<Preference>(getString(R.string.settings_override_auto_backup))?.summary =
|
||||
if (overrideAutoBackup) getString(R.string.enabled) else getString(R.string.disabled)
|
||||
|
||||
findPreference<Preference>(getString(R.string.settings_create_backup))?.isVisible =
|
||||
isBackupEnabled
|
||||
findPreference<Preference>(getString(R.string.settings_create_backup))?.summary =
|
||||
getString(
|
||||
R.string.last_backup_date,
|
||||
lastBackupTime.formatCalendar("dd MMM yyyy hh:mm aa")
|
||||
)
|
||||
findPreference<Preference>(getString(R.string.settings_backup_folder))?.isVisible =
|
||||
isBackupEnabled
|
||||
val directory = URLDecoder.decode(backupDirectory, "utf-8").split("/")
|
||||
val folderName = directory.get(directory.lastIndex)
|
||||
findPreference<Preference>(getString(R.string.settings_backup_folder))?.summary =
|
||||
folderName
|
||||
findPreference<Preference>(getString(R.string.settings_verify_key_phrase))?.isVisible =
|
||||
false
|
||||
findPreference<Preference>(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<Action>()
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String>): 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Action>()
|
||||
|
||||
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))
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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<Action>()
|
||||
|
||||
val context = LocalContext.current
|
||||
val (backupInfo, setBackupInfo) = remember {
|
||||
mutableStateOf<Pair<Boolean, String>?>(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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<AccountModel>() }
|
||||
val accountModel: LiveData<AccountModel> = _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<AccountModel>() }
|
||||
val accountModel: LiveData<AccountModel> = _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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Void?, String?>() {
|
||||
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<Void?, String?>() {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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<List<AccountModel>>()
|
||||
|
||||
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<List<AccountModel>>()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<SortingOrder>
|
||||
) {
|
||||
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<SortingOrder>
|
||||
) {
|
||||
object Title : SortingField(R.string.account_name, "title", getSortingOrderOptions())
|
||||
object Username : SortingField(R.string.username_email_phone, "username", getSortingOrderOptions())
|
||||
}
|
||||
|
||||
@@ -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<AccountModel>? = 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<AccountModel>? = 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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<List<NavigationModelItem>> = MutableLiveData()
|
||||
private val tagsDb = appDb.getDao().getTags()
|
||||
|
||||
private var tagsList: List<String>? = null
|
||||
|
||||
val navigationList: LiveData<List<NavigationModelItem>>
|
||||
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<List<NavigationModelItem>> = MutableLiveData()
|
||||
private val tagsDb = appDb.getDao().getTags()
|
||||
|
||||
private var tagsList: List<String>? = null
|
||||
|
||||
val navigationList: LiveData<List<NavigationModelItem>>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<KeyPassState, Boolean> { 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<KeyPassState, ScreenState> { 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<KeyPassState, BottomSheetState?> { 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<KeyPassState, Boolean> { 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<KeyPassState, Boolean> { 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<KeyPassState, ScreenState> { 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<KeyPassState, BottomSheetState?> { 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<KeyPassState, Boolean> { 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"
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.yogeshpaliyal.keypass.ui.redux
|
||||
|
||||
object BottomSheetRoutes {
|
||||
const val HOME_NAV_MENU = "/home/navMenu"
|
||||
}
|
||||
@@ -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<ScreenState>()
|
||||
|
||||
private val reducer: Reducer<KeyPassState> = { 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<KeyPassState> { 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<ScreenState>()
|
||||
|
||||
private val reducer: Reducer<KeyPassState> = { 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)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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<KeyPassState> { 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)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.yogeshpaliyal.keypass.ui.redux.states
|
||||
|
||||
data class AccountDetailState(val accountId: Long? = null) : ScreenState()
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.yogeshpaliyal.keypass.ui.redux.states
|
||||
|
||||
sealed class ScreenState(val showMainBottomAppBar: Boolean = false)
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.yogeshpaliyal.keypass.ui.redux.states
|
||||
|
||||
object SettingsState : ScreenState(true)
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.yogeshpaliyal.keypass.ui.redux.states
|
||||
|
||||
data class TotpDetailState(val accountId: String? = null) : ScreenState()
|
||||
@@ -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<Action>()
|
||||
|
||||
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<Action>()
|
||||
val context = LocalContext.current
|
||||
|
||||
val (result, setResult) = remember { mutableStateOf<Uri?>(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<Action>()
|
||||
|
||||
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<Action>()
|
||||
val context = LocalContext.current
|
||||
|
||||
val (result, setResult) = remember { mutableStateOf<Uri?>(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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<String>): 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<String>): 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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<String>() {
|
||||
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<String>() {
|
||||
override fun areItemsTheSame(oldItem: String, newItem: String) = oldItem == newItem
|
||||
override fun areContentsTheSame(oldItem: String, newItem: String) = oldItem == newItem
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout>
|
||||
<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/fragment_container_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:name="com.yogeshpaliyal.keypass.ui.settings.MySettingsFragment" />
|
||||
</layout>
|
||||
@@ -89,5 +89,6 @@
|
||||
<string name="confirm_password">Confirm Password</string>
|
||||
<string name="incorrect_password">Incorrect Password</string>
|
||||
<string name="password_no_match">The password did not match</string>
|
||||
<string name="auto_backup_desc">Your accounts will be backed up whenever account is added or modified</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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*/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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<AccountModel>
|
||||
)
|
||||
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<AccountModel>
|
||||
)
|
||||
|
||||
@@ -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<AccountModel>)
|
||||
|
||||
@Query("SELECT * FROM account ORDER BY title ASC")
|
||||
fun getAllAccounts(): LiveData<List<AccountModel>>
|
||||
|
||||
@Query("SELECT * FROM account ORDER BY title ASC")
|
||||
suspend fun getAllAccountsList(): List<AccountModel>
|
||||
|
||||
@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<AccountModel>
|
||||
|
||||
@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<AccountModel>
|
||||
|
||||
@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<List<String>>
|
||||
|
||||
@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<AccountModel>)
|
||||
|
||||
@Query("SELECT * FROM account ORDER BY title ASC")
|
||||
fun getAllAccounts(): LiveData<List<AccountModel>>
|
||||
|
||||
@Query("SELECT * FROM account ORDER BY title ASC")
|
||||
suspend fun getAllAccountsList(): List<AccountModel>
|
||||
|
||||
@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<AccountModel>
|
||||
|
||||
@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<AccountModel>
|
||||
|
||||
@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<List<String>>
|
||||
|
||||
@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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Boolean?>()
|
||||
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<Boolean, String>? {
|
||||
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<Boolean?>()
|
||||
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<Boolean, String>? {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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<out T>(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<out T>(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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<Int>()
|
||||
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<Int>()
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Boolean, String> {
|
||||
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<Boolean, String> {
|
||||
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")
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<AutoBackupWorker>().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<AutoBackupWorker>().build()
|
||||
|
||||
WorkManager.getInstance(this.applicationContext)
|
||||
.enqueueUniqueWork("AutoBackupWorker", ExistingWorkPolicy.KEEP, work)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user