Migrated
This commit is contained in:
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "external/KeePassDX"]
|
||||||
|
path = external/KeePassDX
|
||||||
|
url = https://github.com/Kunzisoft/KeePassDX.git
|
@@ -2,44 +2,55 @@ plugins {
|
|||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
id 'org.jetbrains.kotlin.android'
|
id 'org.jetbrains.kotlin.android'
|
||||||
id 'org.jetbrains.kotlin.plugin.serialization' version '2.2.20'
|
id 'org.jetbrains.kotlin.plugin.serialization' version '2.2.20'
|
||||||
|
id 'org.jetbrains.kotlin.plugin.compose' version '2.2.20'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def keystorePropertiesFile = rootProject.file("app/keystore.properties")
|
|
||||||
def keystoreProperties = new Properties()
|
|
||||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
|
||||||
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'net.helcel.fidelity'
|
namespace 'net.helcel.fidelity'
|
||||||
compileSdk 34
|
compileSdk 36
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId 'net.helcel.fidelity'
|
applicationId 'net.helcel.fidelity'
|
||||||
resValue "string", "app_name", "Keepass Fidelity"
|
versionName "1.0d"
|
||||||
|
buildConfigField("String", "APP_NAME", "\"Keepass Fidelity\"")
|
||||||
|
manifestPlaceholders["APP_NAME"] = "Keepass Fidelity"
|
||||||
minSdk 28
|
minSdk 28
|
||||||
targetSdk 34
|
targetSdk 36
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
create("release") {
|
create("release") {
|
||||||
keyAlias keystoreProperties['keyAlias']
|
try {
|
||||||
keyPassword keystoreProperties['keyPassword']
|
def keystorePropertiesFile = rootProject.file("app/keystore.properties")
|
||||||
storeFile file(keystoreProperties['storeFile'])
|
def keystoreProperties = new Properties()
|
||||||
storePassword keystoreProperties['storePassword']
|
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||||
|
|
||||||
|
keyAlias keystoreProperties['keyAlias']
|
||||||
|
keyPassword keystoreProperties['keyPassword']
|
||||||
|
storeFile file(keystoreProperties['storeFile'])
|
||||||
|
storePassword keystoreProperties['storePassword']
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
println("File not found: ${e.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
debuggable true
|
debuggable true
|
||||||
signingConfig = signingConfigs.getByName("release")
|
|
||||||
}
|
}
|
||||||
release {
|
release {
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
shrinkResources false
|
shrinkResources false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
signedRelease {
|
||||||
|
minifyEnabled true
|
||||||
|
shrinkResources false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
signingConfig = signingConfigs.getByName("release")
|
signingConfig = signingConfigs.getByName("release")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,17 +59,15 @@ android {
|
|||||||
compileOptions {
|
compileOptions {
|
||||||
coreLibraryDesugaringEnabled true
|
coreLibraryDesugaringEnabled true
|
||||||
|
|
||||||
sourceCompatibility JavaVersion.VERSION_17
|
sourceCompatibility JavaVersion.VERSION_21
|
||||||
targetCompatibility JavaVersion.VERSION_17
|
targetCompatibility JavaVersion.VERSION_21
|
||||||
encoding 'utf-8'
|
encoding 'utf-8'
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding true
|
viewBinding true
|
||||||
|
compose true
|
||||||
|
buildConfig true
|
||||||
}
|
}
|
||||||
|
|
||||||
dependenciesInfo {
|
dependenciesInfo {
|
||||||
@@ -67,18 +76,49 @@ android {
|
|||||||
// Disables dependency metadata when building Android App Bundles.
|
// Disables dependency metadata when building Android App Bundles.
|
||||||
includeInBundle = false
|
includeInBundle = false
|
||||||
}
|
}
|
||||||
|
composeOptions {
|
||||||
|
kotlinCompilerExtensionVersion = "2.2.20"
|
||||||
|
}
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(21)
|
||||||
|
}
|
||||||
|
|
||||||
|
lint {
|
||||||
|
disable 'UsingMaterialAndMaterial3Libraries'
|
||||||
|
disable 'PreviewAnnotationInFunctionWithParameters'
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation 'androidx.compose.ui:ui'
|
||||||
|
implementation 'androidx.compose.material3:material3:1.4.0'
|
||||||
|
implementation 'androidx.compose.material:material:1.9.2'
|
||||||
|
implementation 'androidx.compose.material:material-icons-extended:1.7.8'
|
||||||
|
implementation 'androidx.navigation:navigation-compose:2.9.5'
|
||||||
|
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||||
|
|
||||||
|
implementation "androidx.biometric:biometric:1.2.0-alpha05"
|
||||||
|
implementation "androidx.security:security-crypto:1.1.0"
|
||||||
|
implementation "androidx.datastore:datastore-preferences:1.1.7"
|
||||||
|
implementation "androidx.security:security-crypto:1.1.0"
|
||||||
|
|
||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.1.5'
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.1.5'
|
||||||
|
|
||||||
implementation 'androidx.camera:camera-lifecycle:1.5.0'
|
implementation 'androidx.camera:camera-lifecycle:1.5.0'
|
||||||
implementation 'androidx.camera:camera-view:1.5.0'
|
implementation 'androidx.camera:camera-view:1.5.0'
|
||||||
runtimeOnly 'androidx.camera:camera-camera2:1.5.0'
|
runtimeOnly 'androidx.camera:camera-camera2:1.5.0'
|
||||||
|
|
||||||
implementation 'com.google.code.gson:gson:2.13.2'
|
|
||||||
implementation 'com.google.android.material:material:1.13.0'
|
implementation 'com.google.android.material:material:1.13.0'
|
||||||
|
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0'
|
||||||
implementation 'com.google.zxing:core:3.5.3'
|
implementation 'com.google.zxing:core:3.5.3'
|
||||||
|
|
||||||
|
implementation project(":database")
|
||||||
|
implementation project(":crypto")
|
||||||
|
|
||||||
|
implementation platform('androidx.compose:compose-bom:2025.09.01')
|
||||||
|
debugImplementation 'androidx.compose.ui:ui-tooling:1.9.2'
|
||||||
|
debugImplementation 'androidx.compose.ui:ui-tooling-preview'
|
||||||
|
|
||||||
}
|
}
|
7
app/proguard-rules.pro
vendored
7
app/proguard-rules.pro
vendored
@@ -2,12 +2,5 @@
|
|||||||
# fields. Proguard removes such information by default, keep it.
|
# fields. Proguard removes such information by default, keep it.
|
||||||
-keepattributes Signature
|
-keepattributes Signature
|
||||||
|
|
||||||
# This is also needed for R8 in compat mode since multiple
|
|
||||||
# optimizations will remove the generic signature such as class
|
|
||||||
# merging and argument removal. See:
|
|
||||||
# https://r8.googlesource.com/r8/+/refs/heads/main/compatibility-faq.md#troubleshooting-gson-gson
|
|
||||||
-keep class com.google.gson.reflect.TypeToken { *; }
|
|
||||||
-keep class * extends com.google.gson.reflect.TypeToken
|
|
||||||
|
|
||||||
# Optional. For using GSON @Expose annotation
|
# Optional. For using GSON @Expose annotation
|
||||||
-keepattributes AnnotationDefault,RuntimeVisibleAnnotations
|
-keepattributes AnnotationDefault,RuntimeVisibleAnnotations
|
@@ -1,37 +1,20 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:versionCode="8"
|
|
||||||
android:versionName="1.2c">
|
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.camera" />
|
<uses-feature android:name="android.hardware.camera" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
|
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:icon="@mipmap/ic_launcher_round"
|
android:icon="@mipmap/ic_launcher_round"
|
||||||
android:label="@string/app_name"
|
android:label="${APP_NAME}"
|
||||||
android:supportsRtl="true">
|
android:supportsRtl="true">
|
||||||
<activity
|
<activity
|
||||||
android:name=".activity.MainActivity"
|
android:name=".activity.MainActivity"
|
||||||
android:exported="true"
|
android:exported="true">
|
||||||
android:theme="@style/Theme.Fidelity">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<receiver
|
|
||||||
android:name=".pluginSDK.PluginAccessBroadcastReceiver"
|
|
||||||
android:exported="true"
|
|
||||||
tools:ignore="ExportedReceiver">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="keepass2android.ACTION_TRIGGER_REQUEST_ACCESS" />
|
|
||||||
<action android:name="keepass2android.ACTION_RECEIVE_ACCESS" />
|
|
||||||
<action android:name="keepass2android.ACTION_REVOKE_ACCESS" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
65
app/src/main/java/net/helcel/fidelity/activity/Helper.kt
Normal file
65
app/src/main/java/net/helcel/fidelity/activity/Helper.kt
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package net.helcel.fidelity.activity
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.material.Colors
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|
||||||
|
import androidx.compose.material3.darkColorScheme
|
||||||
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
|
import androidx.compose.material3.lightColorScheme
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import net.helcel.fidelity.R
|
||||||
|
|
||||||
|
|
||||||
|
object ToastHelper{
|
||||||
|
fun show(context: Context, message: String, duration: Int = Toast.LENGTH_SHORT) {
|
||||||
|
Toast.makeText(context, message, duration).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SysTheme(
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
val themeKey = prefs.getString(stringResource(R.string.key_theme), stringResource(R.string.system))
|
||||||
|
val darkTheme = when (themeKey) {
|
||||||
|
stringResource(R.string.system) -> isSystemInDarkTheme()
|
||||||
|
stringResource(R.string.light) -> false
|
||||||
|
stringResource(R.string.dark) -> true
|
||||||
|
else -> isSystemInDarkTheme()
|
||||||
|
}
|
||||||
|
val colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
if(darkTheme) dynamicDarkColorScheme(LocalContext.current ) else dynamicLightColorScheme(LocalContext.current )
|
||||||
|
} else {
|
||||||
|
if(darkTheme) darkColorScheme() else lightColorScheme()
|
||||||
|
}
|
||||||
|
val m2colors = Colors(
|
||||||
|
primary = colorScheme.primary,
|
||||||
|
primaryVariant = colorScheme.primaryContainer,
|
||||||
|
secondary = colorScheme.secondary,
|
||||||
|
background = colorScheme.background,
|
||||||
|
surface = colorScheme.surface,
|
||||||
|
onPrimary = colorScheme.onPrimary,
|
||||||
|
onSecondary = colorScheme.onSecondary,
|
||||||
|
onBackground = colorScheme.onBackground,
|
||||||
|
onSurface = colorScheme.onSurface,
|
||||||
|
secondaryVariant = colorScheme.secondary,
|
||||||
|
error = colorScheme.error,
|
||||||
|
onError = colorScheme.onError,
|
||||||
|
isLight = !darkTheme,
|
||||||
|
)
|
||||||
|
|
||||||
|
MaterialTheme(
|
||||||
|
colors = m2colors,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
@@ -1,65 +1,62 @@
|
|||||||
package net.helcel.fidelity.activity
|
package net.helcel.fidelity.activity
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.content.pm.ActivityInfo
|
import android.content.pm.ActivityInfo
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.addCallback
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.activity.compose.setContent
|
||||||
import net.helcel.fidelity.R
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import net.helcel.fidelity.activity.fragment.Launcher
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import net.helcel.fidelity.activity.fragment.ViewEntry
|
import androidx.fragment.app.FragmentActivity
|
||||||
import net.helcel.fidelity.databinding.ActMainBinding
|
import androidx.navigation.compose.NavHost
|
||||||
import net.helcel.fidelity.pluginSDK.Kp2aControl.getEntryFieldsFromIntent
|
import androidx.navigation.compose.composable
|
||||||
import net.helcel.fidelity.tools.CacheManager
|
import androidx.navigation.compose.rememberNavController
|
||||||
import net.helcel.fidelity.tools.KeepassWrapper.bundleCreate
|
import net.helcel.fidelity.activity.fragment.CreateEntryScreen
|
||||||
import net.helcel.fidelity.tools.KeepassWrapper.entryExtract
|
import net.helcel.fidelity.activity.fragment.FileScanner
|
||||||
|
import net.helcel.fidelity.activity.fragment.InitialScreen
|
||||||
@SuppressLint("SourceLockedOrientationActivity")
|
import net.helcel.fidelity.activity.fragment.LauncherScreen
|
||||||
class MainActivity : AppCompatActivity() {
|
import net.helcel.fidelity.activity.fragment.ScannerScreen
|
||||||
|
import net.helcel.fidelity.activity.fragment.ViewEntryScreen
|
||||||
private lateinit var binding: ActMainBinding
|
import net.helcel.fidelity.tools.FidelityRepository.entries
|
||||||
private lateinit var sharedPreferences: SharedPreferences
|
import net.helcel.fidelity.tools.FidelityRepository.loadEntries
|
||||||
|
import net.helcel.fidelity.tools.KeePassStore.hasCredentials
|
||||||
|
|
||||||
|
class MainActivity : FragmentActivity() {
|
||||||
|
|
||||||
|
@SuppressLint("SourceLockedOrientationActivity")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
sharedPreferences =
|
actionBar?.hide()
|
||||||
this.getSharedPreferences(CacheManager.PREF_NAME, Context.MODE_PRIVATE)
|
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||||
CacheManager.loadFidelity(sharedPreferences)
|
loadEntries(this.baseContext)
|
||||||
|
|
||||||
binding = ActMainBinding.inflate(layoutInflater)
|
setContent {
|
||||||
setContentView(binding.root)
|
SysTheme {
|
||||||
onBackPressedDispatcher.addCallback(this) {
|
val navController = rememberNavController()
|
||||||
if (supportFragmentManager.backStackEntryCount > 0) {
|
val context = LocalContext.current
|
||||||
supportFragmentManager.popBackStackImmediate()
|
|
||||||
loadLauncher()
|
BackHandler {
|
||||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
if (!navController.popBackStack()) finish()
|
||||||
} else {
|
}
|
||||||
finish()
|
LaunchedEffect(Unit) {
|
||||||
|
if(!hasCredentials(context)) navController.navigate("init")
|
||||||
|
}
|
||||||
|
NavHost(navController = navController, startDestination = "launcher") {
|
||||||
|
composable("exit") { finish() }
|
||||||
|
composable("launcher") { LauncherScreen(navController) }
|
||||||
|
composable("init"){ InitialScreen (navController)}
|
||||||
|
composable("scanCam") { ScannerScreen(navController) }
|
||||||
|
composable("scanFile") { FileScanner(navController) }
|
||||||
|
composable("edit"){ CreateEntryScreen(navController) }
|
||||||
|
composable("view/{entryId}") { e ->
|
||||||
|
val entry = entries.find {
|
||||||
|
it.uid == (e.arguments?.getString("entryId") ?: "")
|
||||||
|
}
|
||||||
|
if (entry == null) return@composable navController.navigate("launcher")
|
||||||
|
ViewEntryScreen(navController,entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (intent.extras != null)
|
|
||||||
loadViewEntry()
|
|
||||||
else if (savedInstanceState == null)
|
|
||||||
loadLauncher()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadLauncher() {
|
|
||||||
supportFragmentManager.beginTransaction()
|
|
||||||
.replace(R.id.container, Launcher())
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadViewEntry() {
|
|
||||||
val viewEntry = ViewEntry()
|
|
||||||
val data = getEntryFieldsFromIntent(intent)
|
|
||||||
viewEntry.arguments = bundleCreate(entryExtract(data))
|
|
||||||
supportFragmentManager.beginTransaction()
|
|
||||||
.replace(R.id.container, viewEntry)
|
|
||||||
.commit()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,49 +0,0 @@
|
|||||||
package net.helcel.fidelity.activity.adapter
|
|
||||||
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import net.helcel.fidelity.databinding.ListItemFidelityBinding
|
|
||||||
|
|
||||||
class FidelityListAdapter(
|
|
||||||
private val triples: ArrayList<Triple<String?, String?, String?>>,
|
|
||||||
private val onItemClicked: (Triple<String?, String?, String?>) -> Unit
|
|
||||||
) :
|
|
||||||
RecyclerView.Adapter<FidelityListAdapter.FidelityViewHolder>() {
|
|
||||||
|
|
||||||
private lateinit var binding: ListItemFidelityBinding
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FidelityViewHolder {
|
|
||||||
binding = ListItemFidelityBinding.inflate(LayoutInflater.from(parent.context))
|
|
||||||
binding.root.setLayoutParams(
|
|
||||||
LinearLayout.LayoutParams(
|
|
||||||
MATCH_PARENT, WRAP_CONTENT
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return FidelityViewHolder(binding.root)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: FidelityViewHolder, position: Int) {
|
|
||||||
val triple = triples[position]
|
|
||||||
holder.bind(triple)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int = triples.size
|
|
||||||
|
|
||||||
inner class FidelityViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
|
||||||
|
|
||||||
fun bind(triple: Triple<String?, String?, String?>) {
|
|
||||||
val text = "${triple.first}"
|
|
||||||
binding.textView.text = text
|
|
||||||
binding.card.setOnClickListener { onItemClicked(triple) }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,167 +1,370 @@
|
|||||||
package net.helcel.fidelity.activity.fragment
|
package net.helcel.fidelity.activity.fragment
|
||||||
|
|
||||||
import android.content.ActivityNotFoundException
|
import android.graphics.Bitmap
|
||||||
import android.os.Bundle
|
import androidx.compose.foundation.Image
|
||||||
import android.os.Handler
|
import androidx.compose.foundation.background
|
||||||
import android.os.Looper
|
import androidx.compose.foundation.clickable
|
||||||
import android.view.LayoutInflater
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import android.view.View
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import android.view.ViewGroup
|
import androidx.compose.foundation.layout.Box
|
||||||
import android.view.inputmethod.EditorInfo
|
import androidx.compose.foundation.layout.Column
|
||||||
import android.widget.ArrayAdapter
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.core.widget.addTextChangedListener
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
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.material.Button
|
||||||
|
import androidx.compose.material.Checkbox
|
||||||
|
import androidx.compose.material.CheckboxDefaults
|
||||||
|
import androidx.compose.material.CircularProgressIndicator
|
||||||
|
import androidx.compose.material.DropdownMenuItem
|
||||||
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
|
import androidx.compose.material.ExposedDropdownMenuBox
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.OutlinedTextField
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TextFieldDefaults
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||||
|
import androidx.compose.material.icons.filled.Camera
|
||||||
|
import androidx.compose.material.icons.filled.FileOpen
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringArrayResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
import com.google.zxing.FormatException
|
import com.google.zxing.FormatException
|
||||||
|
import com.kunzisoft.keepass.database.element.Entry
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import net.helcel.fidelity.R
|
import net.helcel.fidelity.R
|
||||||
import net.helcel.fidelity.databinding.FragCreateEntryBinding
|
import net.helcel.fidelity.activity.ToastHelper
|
||||||
import net.helcel.fidelity.pluginSDK.Kp2aControl
|
import net.helcel.fidelity.activity.fragment.CreateEntryEventHandler.onCameraScan
|
||||||
|
import net.helcel.fidelity.activity.fragment.CreateEntryEventHandler.onFileScan
|
||||||
|
import net.helcel.fidelity.activity.fragment.CreateEntryEventHandler.onSubmit
|
||||||
|
import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onRefresh
|
||||||
|
import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onSave
|
||||||
import net.helcel.fidelity.tools.BarcodeGenerator.generateBarcode
|
import net.helcel.fidelity.tools.BarcodeGenerator.generateBarcode
|
||||||
import net.helcel.fidelity.tools.CacheManager
|
import net.helcel.fidelity.tools.FidelityEntry
|
||||||
import net.helcel.fidelity.tools.ErrorToaster
|
import net.helcel.fidelity.tools.FidelityRepository
|
||||||
import net.helcel.fidelity.tools.KeepassWrapper
|
import net.helcel.fidelity.tools.FidelityRepository.activeEntry
|
||||||
|
import net.helcel.fidelity.tools.FidelityRepository.addEntry
|
||||||
private const val DEBOUNCE_DELAY = 500L
|
|
||||||
|
|
||||||
class CreateEntry : Fragment() {
|
|
||||||
|
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
|
||||||
private lateinit var binding: FragCreateEntryBinding
|
|
||||||
|
|
||||||
private val resultLauncherAdd = KeepassWrapper.resultLauncher(this) {
|
|
||||||
val r = KeepassWrapper.entryExtract(it)
|
|
||||||
if (!KeepassWrapper.isProtected(it)) {
|
|
||||||
CacheManager.addFidelity(r)
|
|
||||||
}
|
|
||||||
startViewEntry(r.first, r.second, r.third)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var isValidBarcode: Boolean = false
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
binding = FragCreateEntryBinding.inflate(layoutInflater)
|
|
||||||
|
|
||||||
val formats = resources.getStringArray(R.array.format_array)
|
|
||||||
val arrayAdapter = ArrayAdapter(requireContext(), R.layout.list_item_dropdown, formats)
|
|
||||||
binding.editTextFormat.setAdapter(arrayAdapter)
|
|
||||||
|
|
||||||
val res = KeepassWrapper.bundleExtract(arguments)
|
|
||||||
binding.editTextCode.setText(res.second)
|
|
||||||
binding.editTextFormat.setText(res.third, false)
|
|
||||||
|
|
||||||
binding.editTextCode.addTextChangedListener { changeListener() }
|
|
||||||
binding.editTextFormat.addTextChangedListener { changeListener() }
|
|
||||||
binding.editTextFormat.addTextChangedListener { binding.editTextFormat.error = null }
|
|
||||||
binding.btnSave.setOnClickListener { submit() }
|
|
||||||
|
|
||||||
binding.editTextTitle.onDone { submit() }
|
|
||||||
binding.editTextCode.onDone { submit() }
|
|
||||||
|
|
||||||
|
|
||||||
updatePreview()
|
@Preview
|
||||||
return binding.root
|
@Composable
|
||||||
}
|
fun CreateEntryScreen(navController: NavHostController?) {
|
||||||
|
var entry by remember { activeEntry }
|
||||||
|
var errorTitle by remember { mutableStateOf("") }
|
||||||
|
var errorCode by remember { mutableStateOf("") }
|
||||||
|
var errorFormat by remember { mutableStateOf("") }
|
||||||
|
|
||||||
private fun updatePreview() {
|
var barcodeBitmap by remember { mutableStateOf<Bitmap?>(null) }
|
||||||
try {
|
var isValidBarcode by remember { mutableStateOf(false) }
|
||||||
val barcodeBitmap = generateBarcode(
|
var showDialog by remember { mutableStateOf(false) }
|
||||||
binding.editTextCode.text.toString(),
|
var isLoading by remember { mutableStateOf(false) }
|
||||||
binding.editTextFormat.text.toString(),
|
val ctx = LocalContext.current
|
||||||
600
|
val scope = rememberCoroutineScope()
|
||||||
)
|
|
||||||
binding.imageViewPreview.setImageBitmap(barcodeBitmap)
|
|
||||||
isValidBarcode = true
|
|
||||||
} catch (e: FormatException) {
|
|
||||||
binding.imageViewPreview.setImageBitmap(null)
|
|
||||||
binding.editTextCode.error = "Invalid format"
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
binding.imageViewPreview.setImageBitmap(null)
|
|
||||||
binding.editTextCode.error = e.message
|
|
||||||
} catch (e: Exception) {
|
|
||||||
binding.imageViewPreview.setImageBitmap(null)
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isValidForm(): Boolean {
|
LaunchedEffect(entry) {
|
||||||
var valid = true
|
|
||||||
if (binding.editTextFormat.text.isNullOrEmpty()) {
|
|
||||||
valid = false
|
|
||||||
binding.editTextFormat.error = "Format cannot be empty"
|
|
||||||
}
|
|
||||||
if (binding.editTextCode.text.isNullOrEmpty()) {
|
|
||||||
valid = false
|
|
||||||
binding.editTextCode.error = "Code cannot be empty"
|
|
||||||
}
|
|
||||||
if (binding.editTextTitle.text.isNullOrEmpty()) {
|
|
||||||
valid = false
|
|
||||||
binding.editTextTitle.error = "Title cannot be empty"
|
|
||||||
}
|
|
||||||
return valid
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun startViewEntry(title: String?, code: String?, fmt: String?) {
|
|
||||||
val viewEntryFragment = ViewEntry()
|
|
||||||
viewEntryFragment.arguments = KeepassWrapper.bundleCreate(title, code, fmt)
|
|
||||||
|
|
||||||
requireActivity().supportFragmentManager.beginTransaction()
|
|
||||||
.replace(R.id.container, viewEntryFragment).commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun changeListener() {
|
|
||||||
isValidBarcode = false
|
isValidBarcode = false
|
||||||
handler.removeCallbacksAndMessages(null)
|
delay(500)
|
||||||
handler.postDelayed({
|
if (entry.code.isEmpty()) return@LaunchedEffect
|
||||||
updatePreview()
|
try {
|
||||||
}, DEBOUNCE_DELAY)
|
val bmp = generateBarcode(entry.code, entry.format, 600)
|
||||||
}
|
barcodeBitmap = bmp
|
||||||
|
isValidBarcode = true
|
||||||
|
errorCode = ""
|
||||||
private fun TextInputEditText.onDone(callback: () -> Unit) {
|
} catch (_: FormatException) {
|
||||||
setOnEditorActionListener { _, actionId, _ ->
|
barcodeBitmap = null
|
||||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
errorCode = "Invalid Format"
|
||||||
callback.invoke()
|
} catch (e: IllegalArgumentException) {
|
||||||
return@setOnEditorActionListener true
|
barcodeBitmap = null
|
||||||
}
|
errorCode = if (e.message == "com.google.zxing.FormatException") "Invalid Format"
|
||||||
false
|
else e.message ?: "Invalid Argument"
|
||||||
|
} catch (e: Exception) {
|
||||||
|
barcodeBitmap = null
|
||||||
|
ToastHelper.show(ctx, e.message ?: e.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun submit() {
|
if (showDialog) {
|
||||||
if (!isValidForm() || !isValidBarcode) {
|
TreeSelectorDialog(
|
||||||
ErrorToaster.formIncomplete(context)
|
onDismiss = {
|
||||||
} else {
|
showDialog = false
|
||||||
val kpEntry = KeepassWrapper.entryCreate(
|
if(it!=null){
|
||||||
this,
|
entry = entry.copy(uid = it.nodeId?.id.toString())
|
||||||
binding.editTextTitle.text.toString(),
|
if(it is Entry){
|
||||||
binding.editTextCode.text.toString(),
|
entry = entry.copy(title = it.title)
|
||||||
binding.editTextFormat.text.toString(),
|
}
|
||||||
binding.checkboxProtected.isChecked,
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val formats = stringArrayResource(R.array.format_array)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colors.background)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp, 32.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
OutlinedTextField(
|
||||||
|
value = entry.title,
|
||||||
|
enabled = entry.uid!=null,
|
||||||
|
onValueChange = {
|
||||||
|
entry = entry.copy(title = it)
|
||||||
|
errorTitle = ""
|
||||||
|
},
|
||||||
|
label = { Text(text = "Title") },
|
||||||
|
isError = errorTitle.isNotEmpty(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true,
|
||||||
|
colors = TextFieldDefaults.textFieldColors(
|
||||||
|
textColor = if(entry.uid!=null)MaterialTheme.colors.onBackground
|
||||||
|
else MaterialTheme.colors.secondary
|
||||||
|
),
|
||||||
)
|
)
|
||||||
try {
|
if (errorTitle.isNotEmpty()) {
|
||||||
resultLauncherAdd.launch(
|
Text(errorTitle, color = MaterialTheme.colors.error)
|
||||||
Kp2aControl.getAddEntryIntent(
|
}
|
||||||
kpEntry.first,
|
|
||||||
kpEntry.second
|
OutlinedTextField(
|
||||||
)
|
value = entry.code,
|
||||||
|
onValueChange = {
|
||||||
|
entry = entry.copy(code = it)
|
||||||
|
errorCode = ""
|
||||||
|
},
|
||||||
|
colors = TextFieldDefaults.textFieldColors(
|
||||||
|
textColor = MaterialTheme.colors.onBackground
|
||||||
|
),
|
||||||
|
label = { Text("Code") },
|
||||||
|
isError = errorCode.isNotEmpty(),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true
|
||||||
|
)
|
||||||
|
if (errorCode.isNotEmpty()) {
|
||||||
|
Text(errorCode, color = MaterialTheme.colors.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
FormatDropdown(
|
||||||
|
formats,
|
||||||
|
entry.format,
|
||||||
|
errorFormat.ifEmpty { null },
|
||||||
|
) {
|
||||||
|
entry = entry.copy(format = it)
|
||||||
|
errorFormat = ""
|
||||||
|
}
|
||||||
|
if (errorFormat.isNotEmpty()) {
|
||||||
|
Text(errorFormat, color = MaterialTheme.colors.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Checkbox(
|
||||||
|
checked = entry.protected,
|
||||||
|
onCheckedChange = {
|
||||||
|
entry = entry.copy(protected = it)
|
||||||
|
},
|
||||||
|
colors = CheckboxDefaults.colors()
|
||||||
)
|
)
|
||||||
} catch (e: ActivityNotFoundException) {
|
Text("Protected", color = MaterialTheme.colors.onBackground)
|
||||||
ErrorToaster.noKP2AFound(context)
|
|
||||||
} catch (e: Exception) {
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
e.printStackTrace()
|
Button(onClick = { onCameraScan(navController!!) }) {
|
||||||
|
Icon(Icons.Default.Camera, contentDescription = null)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Button(onClick = { onFileScan(navController!!) }) {
|
||||||
|
Icon(Icons.Default.FileOpen, contentDescription = null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!binding.checkboxProtected.isChecked) {
|
if (barcodeBitmap != null) {
|
||||||
val r = KeepassWrapper.entryExtract(kpEntry.first)
|
Image(
|
||||||
CacheManager.addFidelity(r)
|
bitmap = barcodeBitmap!!.asImageBitmap(),
|
||||||
|
contentDescription = "Barcode preview",
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(150.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.padding(48.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
onSubmitIfValid(
|
||||||
|
entry,
|
||||||
|
setErrors = { t, c, f ->
|
||||||
|
errorTitle = t
|
||||||
|
errorCode = c
|
||||||
|
errorFormat = f
|
||||||
|
},
|
||||||
|
isValidBarcode
|
||||||
|
) {
|
||||||
|
if (FidelityRepository.getRoot() == null) {
|
||||||
|
isLoading = true
|
||||||
|
scope.launch {
|
||||||
|
onRefresh(ctx, navController!!)
|
||||||
|
isLoading = false
|
||||||
|
if(entry.uid!=null){
|
||||||
|
addEntry(ctx,entry)
|
||||||
|
isLoading = true
|
||||||
|
onSave(ctx,navController)
|
||||||
|
isLoading = false
|
||||||
|
onSubmit(navController)
|
||||||
|
}else {
|
||||||
|
showDialog = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if(entry.uid!=null){
|
||||||
|
addEntry(ctx,entry)
|
||||||
|
isLoading = true
|
||||||
|
scope.launch {
|
||||||
|
onSave(ctx, navController!!)
|
||||||
|
isLoading = false
|
||||||
|
onSubmit(navController)
|
||||||
|
}
|
||||||
|
}else {
|
||||||
|
showDialog = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = isValidBarcode.and(entry.uid==null || entry.title.isNotEmpty()),
|
||||||
|
) {
|
||||||
|
Text(if(entry.uid==null)"Select Entry" else "Save", style = MaterialTheme.typography.h6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colors.background.copy(alpha = 0.75f))
|
||||||
|
.clickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = null,
|
||||||
|
onClick = { }
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator()
|
||||||
}
|
}
|
||||||
activity?.supportFragmentManager?.popBackStack()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
|
@Composable
|
||||||
|
fun FormatDropdown(
|
||||||
|
formats: Array<String>,
|
||||||
|
format: String,
|
||||||
|
errorFormat: String?,
|
||||||
|
onFormatChange: (String) -> Unit,
|
||||||
|
) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = expanded,
|
||||||
|
onExpandedChange = { expanded = !expanded }
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = format,
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true, // important for dropdown
|
||||||
|
label = { Text("Format", color=MaterialTheme.colors.onBackground) },
|
||||||
|
trailingIcon = {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.ArrowDropDown,
|
||||||
|
contentDescription = "Expand",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = TextFieldDefaults.textFieldColors(
|
||||||
|
textColor = MaterialTheme.colors.onBackground
|
||||||
|
),
|
||||||
|
isError = errorFormat != null,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = { expanded = false }
|
||||||
|
) {
|
||||||
|
formats.forEach { option ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
onClick = {
|
||||||
|
onFormatChange(option)
|
||||||
|
expanded = false
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(option)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private fun onSubmitIfValid(
|
||||||
|
entry: FidelityEntry,
|
||||||
|
setErrors: (String, String, String) -> Unit,
|
||||||
|
isValidBarcode: Boolean,
|
||||||
|
onValid: (FidelityEntry) -> Unit
|
||||||
|
) {
|
||||||
|
var tErr = ""
|
||||||
|
var cErr = ""
|
||||||
|
var fErr = ""
|
||||||
|
if (entry.uid!=null && entry.title.isBlank()) tErr = "Title cannot be empty"
|
||||||
|
if (entry.code.isBlank()) cErr = "Code cannot be empty"
|
||||||
|
if (entry.format.isBlank()) fErr = "Format cannot be empty"
|
||||||
|
|
||||||
|
setErrors(tErr, cErr, fErr)
|
||||||
|
|
||||||
|
if (tErr.isEmpty() && cErr.isEmpty() && fErr.isEmpty() && isValidBarcode) {
|
||||||
|
onValid(entry.copy())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object CreateEntryEventHandler {
|
||||||
|
fun onSubmit(navController: NavHostController){
|
||||||
|
navController.popBackStack()
|
||||||
|
activeEntry.value = activeEntry.value.copy(null,"","","",false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onFileScan(navController: NavHostController){
|
||||||
|
navController.navigate("scanFile")
|
||||||
|
}
|
||||||
|
fun onCameraScan(navController: NavHostController){
|
||||||
|
navController.navigate("scanCam")
|
||||||
|
}
|
||||||
}
|
}
|
@@ -1,107 +0,0 @@
|
|||||||
package net.helcel.fidelity.activity.fragment
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.activity.result.PickVisualMediaRequest
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import net.helcel.fidelity.R
|
|
||||||
import net.helcel.fidelity.tools.BarcodeScanner
|
|
||||||
import net.helcel.fidelity.tools.ErrorToaster
|
|
||||||
import net.helcel.fidelity.tools.KeepassWrapper
|
|
||||||
import java.io.FileNotFoundException
|
|
||||||
|
|
||||||
class FileScanner : Fragment() {
|
|
||||||
|
|
||||||
private var code: String = ""
|
|
||||||
private var fmt: String = ""
|
|
||||||
|
|
||||||
|
|
||||||
private val resultPermission =
|
|
||||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
|
||||||
resultLauncherOpenMediaPick.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
|
|
||||||
}
|
|
||||||
|
|
||||||
private val resultLauncherOpenMediaBase =
|
|
||||||
registerForActivityResult(ActivityResultContracts.GetContent()) {
|
|
||||||
loadUri(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val resultLauncherOpenMediaPick =
|
|
||||||
registerForActivityResult(ActivityResultContracts.PickVisualMedia()) {
|
|
||||||
loadUri(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
println(Build.VERSION.SDK_INT)
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
||||||
resultPermission.launch(Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED)
|
|
||||||
} else {
|
|
||||||
// resultLauncherOpenMediaBase.launch("image/*")
|
|
||||||
resultLauncherOpenMediaPick.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
|
|
||||||
}
|
|
||||||
return View(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startCreateEntry() {
|
|
||||||
val createEntryFragment = CreateEntry()
|
|
||||||
createEntryFragment.arguments =
|
|
||||||
KeepassWrapper.bundleCreate(null, this.code, this.fmt)
|
|
||||||
requireActivity().supportFragmentManager.beginTransaction()
|
|
||||||
.replace(R.id.container, createEntryFragment)
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun scannerResult(code: String?, format: String?) {
|
|
||||||
if (!code.isNullOrEmpty() && !format.isNullOrEmpty()) {
|
|
||||||
this.code = code
|
|
||||||
this.fmt = format
|
|
||||||
}
|
|
||||||
val isDone = this.code.isNotEmpty() && this.fmt.isNotEmpty()
|
|
||||||
requireActivity().runOnUiThread {
|
|
||||||
if (isDone) {
|
|
||||||
startCreateEntry()
|
|
||||||
} else {
|
|
||||||
parentFragmentManager.popBackStack()
|
|
||||||
ErrorToaster.nothingFound(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadUri(it: Uri?) {
|
|
||||||
try {
|
|
||||||
run {
|
|
||||||
require(it != null)
|
|
||||||
|
|
||||||
val file = requireContext().contentResolver.openInputStream(it)
|
|
||||||
val image = BitmapFactory.decodeStream(file)
|
|
||||||
BarcodeScanner.bitmapUseCase(image) { code, format ->
|
|
||||||
scannerResult(code, format)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: FileNotFoundException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
println(e.message)
|
|
||||||
println(it)
|
|
||||||
ErrorToaster.noPermission(context)
|
|
||||||
parentFragmentManager.popBackStack()
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
ErrorToaster.nothingFound(context)
|
|
||||||
parentFragmentManager.popBackStack()
|
|
||||||
} catch (e: SecurityException) {
|
|
||||||
ErrorToaster.noPermission(context)
|
|
||||||
parentFragmentManager.popBackStack()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,136 +1,345 @@
|
|||||||
package net.helcel.fidelity.activity.fragment
|
package net.helcel.fidelity.activity.fragment
|
||||||
|
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import androidx.compose.foundation.background
|
||||||
import android.view.LayoutInflater
|
import androidx.compose.foundation.clickable
|
||||||
import android.view.View
|
import androidx.compose.foundation.combinedClickable
|
||||||
import android.view.ViewGroup
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import net.helcel.fidelity.R
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import net.helcel.fidelity.activity.adapter.FidelityListAdapter
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import net.helcel.fidelity.databinding.FragLauncherBinding
|
import androidx.compose.foundation.layout.padding
|
||||||
import net.helcel.fidelity.pluginSDK.Kp2aControl
|
import androidx.compose.foundation.layout.size
|
||||||
import net.helcel.fidelity.tools.CacheManager
|
import androidx.compose.foundation.layout.width
|
||||||
import net.helcel.fidelity.tools.ErrorToaster
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
import net.helcel.fidelity.tools.KeepassWrapper
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.DropdownMenu
|
||||||
|
import androidx.compose.material.DropdownMenuItem
|
||||||
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
|
import androidx.compose.material.FloatingActionButton
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
import androidx.compose.material.icons.filled.HideSource
|
||||||
|
import androidx.compose.material.icons.filled.PushPin
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onAdd
|
||||||
|
import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onEdit
|
||||||
|
import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onHide
|
||||||
|
import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onPin
|
||||||
|
import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onQuery
|
||||||
|
import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onRefresh
|
||||||
|
import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onView
|
||||||
|
import net.helcel.fidelity.tools.CredentialResult
|
||||||
|
import net.helcel.fidelity.tools.FidelityEntry
|
||||||
|
import net.helcel.fidelity.tools.FidelityRepository.activeEntry
|
||||||
|
import net.helcel.fidelity.tools.FidelityRepository.end
|
||||||
|
import net.helcel.fidelity.tools.FidelityRepository.entries
|
||||||
|
import net.helcel.fidelity.tools.FidelityRepository.genCredentials
|
||||||
|
import net.helcel.fidelity.tools.FidelityRepository.importDB
|
||||||
|
import net.helcel.fidelity.tools.FidelityRepository.start
|
||||||
|
import net.helcel.fidelity.tools.KeePassStore.loadCredentials
|
||||||
|
|
||||||
|
@Preview
|
||||||
class Launcher : Fragment() {
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
private lateinit var binding: FragLauncherBinding
|
fun LauncherScreen(
|
||||||
private lateinit var fidelityListAdapter: FidelityListAdapter
|
navController: NavHostController?,
|
||||||
|
) {
|
||||||
private val resultLauncherQuery = KeepassWrapper.resultLauncher(this) {
|
if(navController==null) return
|
||||||
val r = KeepassWrapper.entryExtract(it)
|
var isRefreshingState by remember { mutableStateOf(false) }
|
||||||
if (!KeepassWrapper.isProtected(it)) {
|
var showHidden by remember { mutableStateOf(false) }
|
||||||
CacheManager.addFidelity(r)
|
val context = LocalContext.current
|
||||||
}
|
val scope = rememberCoroutineScope()
|
||||||
startViewEntry(r.first, r.second, r.third)
|
val sortedEntries = remember(entries) {
|
||||||
}
|
derivedStateOf {
|
||||||
|
entries.filter{showHidden || !it.hidden}.sortedWith(
|
||||||
override fun onCreateView(
|
compareByDescending<FidelityEntry> { it.pinned }
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
.thenBy { it.hidden }
|
||||||
savedInstanceState: Bundle?
|
.thenByDescending { it.lastUse }
|
||||||
): View {
|
)
|
||||||
binding = FragLauncherBinding.inflate(layoutInflater)
|
|
||||||
binding.btnQuery.setOnClickListener { startGetFromKeepass() }
|
|
||||||
binding.btnAdd.setOnClickListener {
|
|
||||||
if (binding.menuAdd.visibility == View.GONE)
|
|
||||||
showMenuAdd()
|
|
||||||
else
|
|
||||||
hideMenuAdd()
|
|
||||||
}
|
|
||||||
|
|
||||||
hideMenuAdd()
|
|
||||||
binding.btnScan.setOnClickListener {
|
|
||||||
startScanner()
|
|
||||||
hideMenuAdd()
|
|
||||||
}
|
|
||||||
binding.btnOpen.setOnClickListener {
|
|
||||||
startFileScanner()
|
|
||||||
hideMenuAdd()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
binding.btnManual.setOnClickListener {
|
|
||||||
startCreateEntry()
|
|
||||||
hideMenuAdd()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.fidelityList.layoutManager =
|
|
||||||
LinearLayoutManager(requireContext())
|
|
||||||
fidelityListAdapter = FidelityListAdapter(CacheManager.getFidelity()) {
|
|
||||||
startViewEntry(it.first, it.second, it.third)
|
|
||||||
}
|
|
||||||
binding.fidelityList.adapter = fidelityListAdapter
|
|
||||||
|
|
||||||
recyclerSlideHelper().attachToRecyclerView(binding.fidelityList)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hideMenuAdd() {
|
|
||||||
binding.btnAdd.setImageResource(R.drawable.cross)
|
|
||||||
binding.menuAdd.visibility = View.GONE
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showMenuAdd() {
|
|
||||||
binding.btnAdd.setImageResource(R.drawable.minus)
|
|
||||||
binding.menuAdd.visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun startGetFromKeepass() {
|
|
||||||
try {
|
|
||||||
this.resultLauncherQuery.launch(Kp2aControl.getQueryEntryForOwnPackageIntent())
|
|
||||||
} catch (e: ActivityNotFoundException) {
|
|
||||||
ErrorToaster.noKP2AFound(requireActivity())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startFragment(fragment: Fragment) {
|
|
||||||
requireActivity().supportFragmentManager.beginTransaction()
|
|
||||||
.addToBackStack("Launcher")
|
|
||||||
.replace(R.id.container, fragment).commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startScanner() {
|
Box(modifier = Modifier
|
||||||
startFragment(Scanner())
|
.fillMaxSize()
|
||||||
}
|
.background(MaterialTheme.colors.background)) {
|
||||||
|
|
||||||
private fun startFileScanner() {
|
PullToRefreshBox(
|
||||||
startFragment(FileScanner())
|
onRefresh = {
|
||||||
}
|
isRefreshingState = true
|
||||||
|
scope.launch {
|
||||||
private fun startCreateEntry() {
|
onRefresh(context, navController)
|
||||||
startFragment(CreateEntry())
|
isRefreshingState = false
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
isRefreshing = isRefreshingState,
|
||||||
private fun startViewEntry(title: String?, code: String?, fmt: String?) {
|
modifier = Modifier.fillMaxSize()
|
||||||
val viewEntryFragment = ViewEntry()
|
|
||||||
viewEntryFragment.arguments = KeepassWrapper.bundleCreate(title, code, fmt)
|
|
||||||
startFragment(viewEntryFragment)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun recyclerSlideHelper(): ItemTouchHelper {
|
|
||||||
return ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(
|
|
||||||
0, ItemTouchHelper.LEFT
|
|
||||||
) {
|
) {
|
||||||
override fun onMove(
|
LazyVerticalGrid(
|
||||||
recyclerView: RecyclerView,
|
columns = GridCells.Fixed(2),
|
||||||
viewHolder: RecyclerView.ViewHolder,
|
modifier = Modifier
|
||||||
target: RecyclerView.ViewHolder
|
.fillMaxSize()
|
||||||
): Boolean = false
|
.fillMaxSize()
|
||||||
|
.padding(16.dp),
|
||||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
val pos = viewHolder.adapterPosition
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
CacheManager.rmFidelity(pos)
|
) {
|
||||||
fidelityListAdapter.notifyItemRemoved(pos)
|
items(sortedEntries.value) { entry ->
|
||||||
|
FidelityRow(navController, entry)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
FloatingActionButton(
|
||||||
|
onClick = { onQuery() },
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.padding(bottom = 16.dp),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Search,
|
||||||
|
contentDescription = "Query",
|
||||||
|
modifier = Modifier.size(32.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = { onAdd(navController) }, modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Add, contentDescription = "Add")
|
||||||
|
}
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
showHidden=!showHidden
|
||||||
|
}, modifier = Modifier
|
||||||
|
.align(Alignment.BottomStart)
|
||||||
|
.padding(16.dp).size(24.dp),
|
||||||
|
backgroundColor = if(showHidden) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary,
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.HideSource,
|
||||||
|
tint= if(showHidden) MaterialTheme.colors.background else MaterialTheme.colors.onSecondary,
|
||||||
|
contentDescription = "Show Hidden")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRefreshingState)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colors.background.copy(alpha = 0.75f))
|
||||||
|
.clickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = null,
|
||||||
|
onClick = { }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
|
@Composable
|
||||||
|
fun FidelityRow(
|
||||||
|
navController: NavHostController,
|
||||||
|
e: FidelityEntry
|
||||||
|
) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Box(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(2.dp)
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = { onView(navController, e) },
|
||||||
|
onLongClick = { expanded = true },
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colors.primary,
|
||||||
|
contentColor = MaterialTheme.colors.background
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize().padding(2.dp)) {
|
||||||
|
Row(modifier = Modifier.padding(14.dp)) {
|
||||||
|
Text(
|
||||||
|
text = e.title,
|
||||||
|
style = MaterialTheme.typography.h6,
|
||||||
|
color = MaterialTheme.colors.onPrimary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(modifier = Modifier.align(Alignment.TopEnd)) {
|
||||||
|
if (e.hidden)
|
||||||
|
Icon(
|
||||||
|
Icons.Default.HideSource, contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = MaterialTheme.colors.onPrimary
|
||||||
|
)
|
||||||
|
if (e.hidden && e.pinned)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
if (e.pinned)
|
||||||
|
Icon(
|
||||||
|
Icons.Default.PushPin, contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = MaterialTheme.colors.onPrimary
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DropdownMenu(
|
||||||
|
modifier = Modifier,
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = { expanded = false }
|
||||||
|
) {
|
||||||
|
DropdownMenuItem(onClick = {
|
||||||
|
expanded = false
|
||||||
|
onEdit(navController, e)
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Edit,
|
||||||
|
contentDescription = "edit",
|
||||||
|
)
|
||||||
|
Spacer(modifier= Modifier.width(8.dp))
|
||||||
|
Text("Edit")
|
||||||
|
}
|
||||||
|
DropdownMenuItem(onClick = {
|
||||||
|
expanded = false
|
||||||
|
onPin(e)
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.PushPin,
|
||||||
|
contentDescription = "pin",
|
||||||
|
)
|
||||||
|
Spacer(modifier= Modifier.width(8.dp))
|
||||||
|
if(e.pinned) Text("Unpin")
|
||||||
|
else Text("Pin")
|
||||||
|
}
|
||||||
|
DropdownMenuItem(onClick = {
|
||||||
|
expanded = false
|
||||||
|
onHide(e)
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.HideSource,
|
||||||
|
contentDescription = "hide",
|
||||||
|
)
|
||||||
|
Spacer(modifier= Modifier.width(8.dp))
|
||||||
|
if(e.hidden) Text("Unhide")
|
||||||
|
else Text("Hide")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
object LauncherEventHandlers {
|
||||||
|
fun onAdd(navController: NavHostController) {
|
||||||
|
navController.navigate("edit")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onQuery() {
|
||||||
|
//TODO
|
||||||
|
}
|
||||||
|
var CRED: CredentialResult.Success? = null
|
||||||
|
|
||||||
|
suspend fun onSave(context: Context, navController: NavHostController){
|
||||||
|
try {
|
||||||
|
if (CRED == null) {
|
||||||
|
val res = loadCredentials(context)
|
||||||
|
when (res) {
|
||||||
|
CredentialResult.AuthFailed, CredentialResult.NoData -> null
|
||||||
|
is CredentialResult.Success -> CRED = res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CRED!!
|
||||||
|
val cred = withContext(Dispatchers.IO) {
|
||||||
|
genCredentials(context, CRED!!)
|
||||||
|
}
|
||||||
|
if (withContext(Dispatchers.IO) {
|
||||||
|
end(context, CRED!!.db, cred)
|
||||||
|
})
|
||||||
|
throw Exception("Error in saving")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println(e.toString())
|
||||||
|
navController.navigate("init")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun onRefresh(context: Context, navController: NavHostController) {
|
||||||
|
try {
|
||||||
|
if (CRED == null) {
|
||||||
|
val res = loadCredentials(context)
|
||||||
|
when (res) {
|
||||||
|
CredentialResult.AuthFailed, CredentialResult.NoData -> null
|
||||||
|
is CredentialResult.Success -> CRED = res
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CRED!!
|
||||||
|
val cred = withContext(Dispatchers.IO) {
|
||||||
|
genCredentials(context, CRED!!)
|
||||||
|
}
|
||||||
|
if (withContext(Dispatchers.IO) {
|
||||||
|
start(context, CRED!!.db, cred)
|
||||||
|
})
|
||||||
|
importDB(context)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println(e.toString())
|
||||||
|
navController.navigate("init")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onView(navController: NavHostController, entry: FidelityEntry) {
|
||||||
|
navController.navigate("view/${entry.uid}")
|
||||||
|
val index = entries.indexOfFirst { it.uid == entry.uid }
|
||||||
|
if (index != -1)
|
||||||
|
entries[index] = entry.copy(lastUse = System.currentTimeMillis().toInt())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPin(entry: FidelityEntry){
|
||||||
|
val index = entries.indexOfFirst { it.uid == entry.uid }
|
||||||
|
if (index != -1)
|
||||||
|
entries[index] = entry.copy(pinned = !entry.pinned)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onHide(entry: FidelityEntry){
|
||||||
|
val index = entries.indexOfFirst { it.uid == entry.uid }
|
||||||
|
if (index != -1)
|
||||||
|
entries[index] = entry.copy(hidden = !entry.hidden)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEdit(navController: NavHostController, entry: FidelityEntry){
|
||||||
|
activeEntry.value = entry
|
||||||
|
navController.navigate("edit")
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,105 +1,224 @@
|
|||||||
|
@file:Suppress("PreviewAnnotationInFunctionWithParameters",
|
||||||
|
"PreviewAnnotationInFunctionWithParameters"
|
||||||
|
)
|
||||||
|
|
||||||
package net.helcel.fidelity.activity.fragment
|
package net.helcel.fidelity.activity.fragment
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.ContentValues
|
import android.graphics.BitmapFactory
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.widget.Toast
|
||||||
import android.view.View
|
import androidx.activity.compose.BackHandler
|
||||||
import android.view.ViewGroup
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.PickVisualMediaRequest
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.camera.core.Camera
|
||||||
import androidx.camera.core.CameraSelector
|
import androidx.camera.core.CameraSelector
|
||||||
import androidx.camera.core.Preview
|
import androidx.camera.core.Preview
|
||||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.camera.view.PreviewView
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.compose.foundation.Canvas
|
||||||
import net.helcel.fidelity.R
|
import androidx.compose.foundation.layout.Box
|
||||||
import net.helcel.fidelity.databinding.FragScannerBinding
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.Button
|
||||||
|
import androidx.compose.material.CircularProgressIndicator
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.FlashOn
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.BlendMode
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import net.helcel.fidelity.activity.fragment.ScannerEventHandler.onResult
|
||||||
|
import net.helcel.fidelity.tools.BarcodeScanner
|
||||||
import net.helcel.fidelity.tools.BarcodeScanner.analysisUseCase
|
import net.helcel.fidelity.tools.BarcodeScanner.analysisUseCase
|
||||||
import net.helcel.fidelity.tools.ErrorToaster
|
import net.helcel.fidelity.tools.FidelityRepository.activeEntry
|
||||||
import net.helcel.fidelity.tools.KeepassWrapper
|
|
||||||
|
|
||||||
class Scanner : Fragment() {
|
@androidx.compose.ui.tooling.preview.Preview
|
||||||
|
@Composable
|
||||||
|
fun ScannerScreen(
|
||||||
|
navController: NavController
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
private lateinit var binding: FragScannerBinding
|
val cameraProviderFuture = remember {
|
||||||
|
ProcessCameraProvider.getInstance(context)
|
||||||
private var code: String = ""
|
|
||||||
private var fmt: String = ""
|
|
||||||
|
|
||||||
|
|
||||||
private val resultPermissionRequest =
|
|
||||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
|
||||||
if (it) {
|
|
||||||
bindCameraUseCases()
|
|
||||||
} else {
|
|
||||||
parentFragmentManager.popBackStack()
|
|
||||||
ErrorToaster.noPermission(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
binding = FragScannerBinding.inflate(layoutInflater)
|
|
||||||
binding.btnScanDone.setOnClickListener {
|
|
||||||
startCreateEntry()
|
|
||||||
}
|
|
||||||
binding.btnScanDone.isEnabled = false
|
|
||||||
resultPermissionRequest.launch(Manifest.permission.CAMERA)
|
|
||||||
return binding.root
|
|
||||||
}
|
}
|
||||||
|
var camera: Camera? by remember { mutableStateOf(null) }
|
||||||
|
var torchOn by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val done = remember { mutableStateOf(false) }
|
||||||
|
val previewView = remember { PreviewView(context) }
|
||||||
|
|
||||||
private fun startCreateEntry() {
|
val permissionLauncher = rememberLauncherForActivityResult(
|
||||||
val createEntryFragment = CreateEntry()
|
contract = ActivityResultContracts.RequestPermission(),
|
||||||
createEntryFragment.arguments =
|
onResult = { granted ->
|
||||||
KeepassWrapper.bundleCreate(null, this.code, this.fmt)
|
if (granted) {
|
||||||
requireActivity().supportFragmentManager.beginTransaction()
|
val cameraProvider = cameraProviderFuture.get()
|
||||||
.replace(R.id.container, createEntryFragment)
|
val previewUseCase = Preview.Builder().build().also {
|
||||||
.commit()
|
it.surfaceProvider = previewView.surfaceProvider
|
||||||
}
|
}
|
||||||
|
val analysisUseCase = analysisUseCase { detectedCode, detectedFormat ->
|
||||||
|
if (detectedCode.isNullOrEmpty() || detectedFormat.isNullOrEmpty()) return@analysisUseCase
|
||||||
private fun scannerResult(code: String?, format: String?) {
|
if(done.value) return@analysisUseCase
|
||||||
if (!code.isNullOrEmpty() && !format.isNullOrEmpty()) {
|
scope.launch(Dispatchers.Main) {
|
||||||
this.code = code
|
activeEntry.value =
|
||||||
this.fmt = format
|
activeEntry.value.copy(code = detectedCode, format = detectedFormat)
|
||||||
}
|
done.value = true
|
||||||
val isDone = this.code.isNotEmpty() && this.fmt.isNotEmpty()
|
onResult(navController)
|
||||||
activity?.runOnUiThread {
|
}
|
||||||
binding.btnScanDone.isEnabled = isDone
|
return@analysisUseCase
|
||||||
binding.ScanActive.isEnabled = !isDone
|
}
|
||||||
}
|
try {
|
||||||
}
|
cameraProvider.unbindAll()
|
||||||
|
camera = cameraProvider.bindToLifecycle(
|
||||||
private fun bindCameraUseCases() {
|
lifecycleOwner,
|
||||||
val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
|
CameraSelector.DEFAULT_BACK_CAMERA,
|
||||||
|
previewUseCase,
|
||||||
cameraProviderFuture.addListener({
|
analysisUseCase
|
||||||
val cameraProvider = cameraProviderFuture.get()
|
)
|
||||||
val previewUseCase = Preview.Builder()
|
} catch (e: Exception) {
|
||||||
.build()
|
Log.e("ScannerScreen", "Camera bind failed: ${e.message}")
|
||||||
.also {
|
}
|
||||||
it.setSurfaceProvider(binding.cameraView.surfaceProvider)
|
} else {
|
||||||
|
Toast.makeText(context, "Camera permission denied", Toast.LENGTH_SHORT).show()
|
||||||
|
scope.launch(Dispatchers.Main){
|
||||||
|
onResult(navController)
|
||||||
}
|
}
|
||||||
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
|
||||||
val analysisUseCase = analysisUseCase { code, format ->
|
|
||||||
scannerResult(code, format)
|
|
||||||
}
|
}
|
||||||
try {
|
}
|
||||||
cameraProvider.bindToLifecycle(
|
)
|
||||||
this,
|
|
||||||
cameraSelector,
|
LaunchedEffect(Unit) {
|
||||||
previewUseCase,
|
permissionLauncher.launch(Manifest.permission.CAMERA)
|
||||||
analysisUseCase
|
|
||||||
)
|
|
||||||
} catch (illegalStateException: IllegalStateException) {
|
|
||||||
Log.e(ContentValues.TAG, illegalStateException.message.orEmpty())
|
|
||||||
} catch (illegalArgumentException: IllegalArgumentException) {
|
|
||||||
Log.e(ContentValues.TAG, illegalArgumentException.message.orEmpty())
|
|
||||||
}
|
|
||||||
}, ContextCompat.getMainExecutor(requireContext()))
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
AndroidView(
|
||||||
|
factory = { previewView },
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
ScannerOverlay(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
Button(onClick = {
|
||||||
|
torchOn = !torchOn
|
||||||
|
camera?.cameraControl?.enableTorch(torchOn)
|
||||||
|
}, modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(16.dp),
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.FlashOn, contentDescription = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!done.value)
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter) // same spot as buttons
|
||||||
|
.padding(bottom =80.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ScannerOverlay(
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Canvas(modifier = modifier.fillMaxSize()) {
|
||||||
|
val widthF = size.width
|
||||||
|
val heightF = size.height
|
||||||
|
|
||||||
|
drawRect(
|
||||||
|
color = Color(0x80000000), // semi-transparent black
|
||||||
|
size = size
|
||||||
|
)
|
||||||
|
|
||||||
|
val squareSize = 0.75f * minOf(widthF, heightF)
|
||||||
|
val left = (widthF - squareSize) / 2
|
||||||
|
val top = (heightF - squareSize) / 2
|
||||||
|
|
||||||
|
drawRect(
|
||||||
|
color = Color.Transparent,
|
||||||
|
topLeft = Offset(left, top),
|
||||||
|
size = Size(squareSize, squareSize),
|
||||||
|
blendMode = BlendMode.Clear
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FileScanner(navController: NavHostController) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
rememberCoroutineScope()
|
||||||
|
val pickImageLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.PickVisualMedia()
|
||||||
|
) { uri ->
|
||||||
|
if (uri == null) {
|
||||||
|
Toast.makeText(context, "No file selected", Toast.LENGTH_SHORT).show()
|
||||||
|
onResult(navController)
|
||||||
|
return@rememberLauncherForActivityResult
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
val inputStream = context.contentResolver.openInputStream(uri)
|
||||||
|
val bitmap = BitmapFactory.decodeStream(inputStream)
|
||||||
|
BarcodeScanner.bitmapUseCase(bitmap) { code, format ->
|
||||||
|
if (!code.isNullOrEmpty() && !format.isNullOrEmpty()) {
|
||||||
|
activeEntry.value = activeEntry.value.copy(code=code, format=format)
|
||||||
|
onResult(navController)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, "No barcode found", Toast.LENGTH_SHORT).show()
|
||||||
|
onResult(navController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
Toast.makeText(context, "Failed to load image", Toast.LENGTH_SHORT).show()
|
||||||
|
onResult(navController)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
pickImageLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
|
||||||
|
}
|
||||||
|
|
||||||
|
BackHandler {
|
||||||
|
onResult(navController)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object ScannerEventHandler {
|
||||||
|
fun onResult(navController: NavController) {
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -0,0 +1,144 @@
|
|||||||
|
package net.helcel.fidelity.activity.fragment
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
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.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.Button
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.Undo
|
||||||
|
import androidx.compose.material.icons.filled.ExpandMore
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
|
import com.kunzisoft.keepass.database.element.node.Node
|
||||||
|
import net.helcel.fidelity.tools.FidelityRepository
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun TreeSelectorDialog(onDismiss: (Node?) -> Unit = {}) {
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = {onDismiss(null)},
|
||||||
|
content = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth().background(
|
||||||
|
MaterialTheme.colors.background,
|
||||||
|
RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
var currentRoot by remember { mutableStateOf(FidelityRepository.getRoot()) }
|
||||||
|
var selection by remember { mutableStateOf<Node?>(FidelityRepository.getRoot()) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(8.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
selection = currentRoot
|
||||||
|
currentRoot = currentRoot?.parent
|
||||||
|
},
|
||||||
|
enabled = currentRoot?.parent != null
|
||||||
|
) {
|
||||||
|
Icon(Icons.AutoMirrored.Filled.Undo, contentDescription = "up")
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
currentRoot?.title ?: "?",
|
||||||
|
color = MaterialTheme.colors.onBackground,
|
||||||
|
style = MaterialTheme.typography.h6
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
LazyColumn(modifier = Modifier.fillMaxHeight(0.75f)) {
|
||||||
|
items(currentRoot?.getChildGroups() ?: emptyList()) { entry ->
|
||||||
|
val isSel = (entry.nodeId == selection?.nodeId)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(color = if (isSel) MaterialTheme.colors.primary else MaterialTheme.colors.background)
|
||||||
|
.clickable {
|
||||||
|
if (entry.getChildEntries().isNotEmpty()) {
|
||||||
|
currentRoot = entry
|
||||||
|
selection = entry
|
||||||
|
} else if (entry.getChildGroups().isNotEmpty()) {
|
||||||
|
currentRoot = entry
|
||||||
|
selection = entry
|
||||||
|
} else {
|
||||||
|
selection = entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
if (entry.getChildEntries().isNotEmpty() || entry.getChildGroups()
|
||||||
|
.isNotEmpty()
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.ExpandMore,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = if (isSel) MaterialTheme.colors.onPrimary else MaterialTheme.colors.onBackground
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
entry.title,
|
||||||
|
modifier = Modifier.padding(start = 8.dp),
|
||||||
|
color = if (isSel) MaterialTheme.colors.onPrimary else MaterialTheme.colors.onBackground
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items(currentRoot?.getChildEntries() ?: emptyList()) { entry ->
|
||||||
|
val isSel = (entry.nodeId == selection?.nodeId)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(color = if (isSel) MaterialTheme.colors.primary else MaterialTheme.colors.background)
|
||||||
|
.clickable {
|
||||||
|
selection = entry
|
||||||
|
}
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
entry.title,
|
||||||
|
modifier = Modifier.padding(start = 8.dp),
|
||||||
|
color = if (isSel) MaterialTheme.colors.onPrimary else MaterialTheme.colors.onBackground
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||||
|
enabled = selection != null,
|
||||||
|
onClick = {
|
||||||
|
onDismiss(selection)
|
||||||
|
}) {
|
||||||
|
Text("Select " + if (selection is Group) "Group" else "Entry")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
274
app/src/main/java/net/helcel/fidelity/activity/fragment/Setup.kt
Normal file
274
app/src/main/java/net/helcel/fidelity/activity/fragment/Setup.kt
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
package net.helcel.fidelity.activity.fragment
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts.OpenDocument
|
||||||
|
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.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.Button
|
||||||
|
import androidx.compose.material.Checkbox
|
||||||
|
import androidx.compose.material.CheckboxDefaults
|
||||||
|
import androidx.compose.material.CircularProgressIndicator
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.OutlinedTextField
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TextFieldDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import net.helcel.fidelity.activity.ToastHelper
|
||||||
|
import net.helcel.fidelity.activity.fragment.SetupEventHandlers.onOpen
|
||||||
|
import net.helcel.fidelity.tools.CredentialResult
|
||||||
|
import net.helcel.fidelity.tools.FidelityRepository.genCredentials
|
||||||
|
import net.helcel.fidelity.tools.FidelityRepository.start
|
||||||
|
import net.helcel.fidelity.tools.KeePassStore.loadCredentials
|
||||||
|
import net.helcel.fidelity.tools.KeePassStore.packCredentials
|
||||||
|
import net.helcel.fidelity.tools.KeePassStore.saveCredentials
|
||||||
|
|
||||||
|
|
||||||
|
class GetPersistentContent : OpenDocument() {
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
override fun createIntent(context: Context, input: Array<String>): Intent {
|
||||||
|
return super.createIntent(context, input).apply {
|
||||||
|
addCategory(Intent.CATEGORY_DEFAULT)
|
||||||
|
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun InitialScreen(
|
||||||
|
navController: NavHostController?
|
||||||
|
) {
|
||||||
|
var loading by remember { mutableStateOf(false) }
|
||||||
|
var dbFile by remember { mutableStateOf<Uri?>(null) }
|
||||||
|
var password by remember { mutableStateOf("") }
|
||||||
|
var keyFile by remember { mutableStateOf<Uri?>(null) }
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val dbFilePickerLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = GetPersistentContent(),
|
||||||
|
) {
|
||||||
|
if(it!=null) {
|
||||||
|
dbFile = it
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
context.contentResolver.takePersistableUriPermission(
|
||||||
|
it,
|
||||||
|
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val keyFilePickerLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = GetPersistentContent()
|
||||||
|
) {
|
||||||
|
if(it!=null) {
|
||||||
|
keyFile = it
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
context.contentResolver.takePersistableUriPermission(
|
||||||
|
it,
|
||||||
|
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BackHandler {
|
||||||
|
navController!!.navigate("exit")
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
scope.launch(Dispatchers.Main) {
|
||||||
|
when(val res = loadCredentials(context)) {
|
||||||
|
CredentialResult.AuthFailed -> null
|
||||||
|
CredentialResult.NoData -> null
|
||||||
|
is CredentialResult.Success -> {
|
||||||
|
if (res.db != null) dbFile = res.db
|
||||||
|
if (res.key != null) keyFile = res.key
|
||||||
|
if (res.password != "" && password == "") password = res.password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colors.background)) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp)
|
||||||
|
.background(MaterialTheme.colors.background),
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Keypass Database Setup",
|
||||||
|
style = MaterialTheme.typography.h5,
|
||||||
|
color = MaterialTheme.colors.onBackground
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text("KDBX Database:", color = MaterialTheme.colors.onBackground)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Checkbox(
|
||||||
|
enabled = !loading,
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colors.primary,
|
||||||
|
RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.size(32.dp),
|
||||||
|
checked = dbFile != null,
|
||||||
|
onCheckedChange = { dbFilePickerLauncher.launch(arrayOf("*/*")) },
|
||||||
|
colors = CheckboxDefaults.colors(
|
||||||
|
uncheckedColor = MaterialTheme.colors.primary,
|
||||||
|
checkedColor = MaterialTheme.colors.primary,
|
||||||
|
checkmarkColor = MaterialTheme.colors.onPrimary
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
enabled = !loading,
|
||||||
|
value = password,
|
||||||
|
onValueChange = { password = it },
|
||||||
|
label = { Text("Password") },
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
capitalization = KeyboardCapitalization.Unspecified,
|
||||||
|
autoCorrectEnabled = false,
|
||||||
|
keyboardType = KeyboardType.Password,
|
||||||
|
imeAction = ImeAction.Done
|
||||||
|
),
|
||||||
|
colors = TextFieldDefaults.textFieldColors(
|
||||||
|
textColor = MaterialTheme.colors.onBackground
|
||||||
|
),
|
||||||
|
visualTransformation = PasswordVisualTransformation(),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text("KDBX Key File:", color = MaterialTheme.colors.onBackground)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Checkbox(
|
||||||
|
enabled = !loading,
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colors.primary,
|
||||||
|
RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
.size(32.dp),
|
||||||
|
checked = keyFile != null,
|
||||||
|
onCheckedChange = { keyFilePickerLauncher.launch(arrayOf("*/*")) },
|
||||||
|
colors = CheckboxDefaults.colors(
|
||||||
|
uncheckedColor = MaterialTheme.colors.primary,
|
||||||
|
checkedColor = MaterialTheme.colors.primary,
|
||||||
|
checkmarkColor = MaterialTheme.colors.onPrimary
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Button(
|
||||||
|
enabled = !loading && password.isNotBlank() && dbFile != null ,
|
||||||
|
onClick = {
|
||||||
|
loading = true
|
||||||
|
scope.launch {
|
||||||
|
if(onOpen(context, dbFile!!, password, keyFile)){
|
||||||
|
navController!!.popBackStack()
|
||||||
|
navController.navigate("init")
|
||||||
|
}else{
|
||||||
|
ToastHelper.show(context, "Auth failed...")
|
||||||
|
navController!!.popBackStack()
|
||||||
|
navController.navigate("exit")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text("Continue")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(contentAlignment = Alignment.BottomCenter, modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(32.dp)){
|
||||||
|
|
||||||
|
if(loading )
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter) // same spot as buttons
|
||||||
|
.padding(bottom = 80.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object SetupEventHandlers {
|
||||||
|
suspend fun onOpen(context: Context, db: Uri, p: String, key: Uri?): Boolean {
|
||||||
|
try {
|
||||||
|
val packCred = packCredentials(db, p, key)
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
start(context, db, genCredentials(context, packCred)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val res = withContext(Dispatchers.Main) {
|
||||||
|
saveCredentials(context, packCred)
|
||||||
|
}
|
||||||
|
return when (res) {
|
||||||
|
CredentialResult.AuthFailed, CredentialResult.NoData -> false
|
||||||
|
is CredentialResult.Success -> true
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
ToastHelper.show(context, e.message.toString())
|
||||||
|
println("Err${e.toString()}")
|
||||||
|
println(e.message)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,86 +1,125 @@
|
|||||||
package net.helcel.fidelity.activity.fragment
|
package net.helcel.fidelity.activity.fragment
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.app.Activity
|
||||||
import android.content.pm.ActivityInfo
|
import android.graphics.Bitmap
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL
|
|
||||||
import android.view.WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
|
import android.view.WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
|
||||||
import androidx.fragment.app.Fragment
|
import android.widget.Toast
|
||||||
import com.google.zxing.FormatException
|
import androidx.activity.compose.BackHandler
|
||||||
import net.helcel.fidelity.databinding.FragViewEntryBinding
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material.CircularProgressIndicator
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.rotate
|
||||||
|
import androidx.compose.ui.draw.scale
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
import net.helcel.fidelity.tools.BarcodeGenerator.generateBarcode
|
import net.helcel.fidelity.tools.BarcodeGenerator.generateBarcode
|
||||||
import net.helcel.fidelity.tools.ErrorToaster
|
import net.helcel.fidelity.tools.FidelityEntry
|
||||||
import net.helcel.fidelity.tools.KeepassWrapper
|
import kotlin.let
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
@SuppressLint("SourceLockedOrientationActivity")
|
|
||||||
class ViewEntry : Fragment() {
|
|
||||||
|
|
||||||
private lateinit var binding: FragViewEntryBinding
|
@Preview
|
||||||
private var title: String? = null
|
@Composable
|
||||||
private var code: String? = null
|
fun PreviewEntryScreen(){
|
||||||
private var fmt: String? = null
|
ViewEntryScreen(null, FidelityEntry("Title","AAA","QR"))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateView(
|
@Composable
|
||||||
inflater: LayoutInflater,
|
fun ViewEntryScreen(
|
||||||
container: ViewGroup?,
|
navController: NavHostController?,
|
||||||
savedInstanceState: Bundle?
|
entry: FidelityEntry
|
||||||
): View {
|
) {
|
||||||
binding = FragViewEntryBinding.inflate(layoutInflater)
|
val context = LocalContext.current
|
||||||
val res = KeepassWrapper.bundleExtract(arguments)
|
val activity = context as? Activity
|
||||||
title = res.first
|
var isFull by remember { mutableStateOf(false) }
|
||||||
code = res.second
|
var bitmap by remember { mutableStateOf<Bitmap?>(null) }
|
||||||
fmt = res.third
|
|
||||||
|
|
||||||
updatePreview()
|
SideEffect {
|
||||||
updateLayout()
|
activity?.window?.attributes = activity.window?.attributes?.apply {
|
||||||
|
screenBrightness = if (isFull) 1f else BRIGHTNESS_OVERRIDE_NONE
|
||||||
binding.imageViewPreview.setOnClickListener {
|
|
||||||
requireActivity().requestedOrientation =
|
|
||||||
if (isLandscape()) ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
|
||||||
else ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updatePreview() {
|
|
||||||
binding.title.text = title
|
|
||||||
try {
|
try {
|
||||||
val barcodeBitmap = generateBarcode(
|
bitmap = generateBarcode(entry.code, entry.format, 1024)
|
||||||
code, fmt, 1024
|
} catch (_: Exception) {
|
||||||
|
bitmap = null
|
||||||
|
Toast.makeText(context, "Invalid barcode format", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BackHandler {
|
||||||
|
isFull=false
|
||||||
|
navController!!.popBackStack()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black)
|
||||||
|
.clickable(
|
||||||
|
onClick = { isFull = !isFull },
|
||||||
|
indication = null, // remove ripple effect
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.TopCenter
|
||||||
|
) {
|
||||||
|
if (!isFull) {
|
||||||
|
Text(
|
||||||
|
text = entry.title,
|
||||||
|
color = Color.White,
|
||||||
|
style = MaterialTheme.typography.h4,
|
||||||
|
modifier = Modifier.padding(32.dp)
|
||||||
)
|
)
|
||||||
binding.imageViewPreview.setImageBitmap(barcodeBitmap)
|
|
||||||
} catch (e: FormatException) {
|
|
||||||
ErrorToaster.invalidFormat(requireActivity())
|
|
||||||
binding.imageViewPreview.setImageBitmap(null)
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
binding.imageViewPreview.setImageBitmap(null)
|
|
||||||
ErrorToaster.invalidFormat(requireActivity())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
binding.imageViewPreview.setImageBitmap(null)
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateLayout() {
|
|
||||||
if (isLandscape()) {
|
BoxWithConstraints(
|
||||||
binding.title.visibility = View.GONE
|
modifier = Modifier
|
||||||
setScreenBrightness(BRIGHTNESS_OVERRIDE_FULL)
|
.fillMaxSize().padding(8.dp),
|
||||||
} else {
|
contentAlignment = Alignment.Center
|
||||||
binding.title.visibility = View.VISIBLE
|
) {
|
||||||
setScreenBrightness(BRIGHTNESS_OVERRIDE_NONE)
|
bitmap?.let {
|
||||||
|
|
||||||
|
|
||||||
|
val modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.width(maxWidth)
|
||||||
|
.height(maxHeight)
|
||||||
|
.padding(16.dp)
|
||||||
|
.aspectRatio(it.width.toFloat()/it.height.toFloat())
|
||||||
|
.rotate(if (isFull) 90f else 0f)
|
||||||
|
.scale(if(isFull) min(it.width.dp/maxHeight,it.height.dp/maxWidth) else 1f)
|
||||||
|
|
||||||
|
Image(
|
||||||
|
bitmap = it.asImageBitmap(),
|
||||||
|
contentDescription = "Barcode",
|
||||||
|
modifier = modifier,
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
)
|
||||||
|
} ?: CircularProgressIndicator(color = Color.White)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun isLandscape(): Boolean {
|
|
||||||
return (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setScreenBrightness(brightness: Float) {
|
|
||||||
requireActivity().window?.attributes?.screenBrightness = brightness
|
|
||||||
}
|
|
||||||
}
|
}
|
@@ -1,45 +0,0 @@
|
|||||||
package net.helcel.fidelity.activity.view
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.graphics.Paint
|
|
||||||
import android.graphics.PorterDuff
|
|
||||||
import android.graphics.PorterDuffXfermode
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.View
|
|
||||||
|
|
||||||
class ScannerView : View {
|
|
||||||
|
|
||||||
private val overlayPaint = Paint().apply {
|
|
||||||
color = Color.parseColor("#80000000") // Semi-transparent black
|
|
||||||
style = Paint.Style.FILL
|
|
||||||
}
|
|
||||||
|
|
||||||
private val clearPaint = Paint().apply {
|
|
||||||
xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(context: Context) : super(context)
|
|
||||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
|
||||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
|
|
||||||
context,
|
|
||||||
attrs,
|
|
||||||
defStyleAttr
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun onDraw(canvas: Canvas) {
|
|
||||||
super.onDraw(canvas)
|
|
||||||
|
|
||||||
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), overlayPaint)
|
|
||||||
|
|
||||||
val centerX = width / 2f
|
|
||||||
val centerY = height / 2f
|
|
||||||
val squareSize = 0.75f * width.coerceAtMost(height)
|
|
||||||
canvas.drawRect(
|
|
||||||
centerX - squareSize / 2, centerY - squareSize / 2,
|
|
||||||
centerX + squareSize / 2, centerY + squareSize / 2, clearPaint
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@@ -1,94 +0,0 @@
|
|||||||
package net.helcel.fidelity.pluginSDK
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import org.json.JSONArray
|
|
||||||
import org.json.JSONException
|
|
||||||
|
|
||||||
|
|
||||||
object AccessManager {
|
|
||||||
private const val PREF_KEY_SCOPE = "scope"
|
|
||||||
private const val PREF_KEY_TOKEN = "token"
|
|
||||||
|
|
||||||
private fun stringArrayToString(values: ArrayList<String?>): String? {
|
|
||||||
if (values.isEmpty()) return null
|
|
||||||
val a = JSONArray()
|
|
||||||
values.forEach { a.put(it) }
|
|
||||||
return a.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stringToStringArray(s: String?): ArrayList<String> {
|
|
||||||
val strings = ArrayList<String>()
|
|
||||||
if (s.isNullOrEmpty()) return strings
|
|
||||||
|
|
||||||
try {
|
|
||||||
val a = JSONArray(s)
|
|
||||||
for (i in 0 until a.length())
|
|
||||||
strings.add(a.optString(i))
|
|
||||||
} catch (e: JSONException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
return strings
|
|
||||||
}
|
|
||||||
|
|
||||||
fun storeAccessToken(
|
|
||||||
ctx: Context,
|
|
||||||
hostPackage: String?,
|
|
||||||
accessToken: String?,
|
|
||||||
scopes: ArrayList<String?>
|
|
||||||
) {
|
|
||||||
val prefs = getPrefsForHost(ctx, hostPackage)
|
|
||||||
val edit = prefs.edit()
|
|
||||||
edit.putString(PREF_KEY_TOKEN, accessToken)
|
|
||||||
val scopesString = stringArrayToString(scopes)
|
|
||||||
edit.putString(PREF_KEY_SCOPE, scopesString)
|
|
||||||
edit.apply()
|
|
||||||
|
|
||||||
val hostPrefs = ctx.getSharedPreferences("KP2A.PluginAccess.hosts", Context.MODE_PRIVATE)
|
|
||||||
if (!hostPrefs.contains(hostPackage))
|
|
||||||
hostPrefs.edit().putString(hostPackage, "").apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getPrefsForHost(
|
|
||||||
ctx: Context,
|
|
||||||
hostPackage: String?
|
|
||||||
): SharedPreferences {
|
|
||||||
return ctx.getSharedPreferences("KP2A.PluginAccess.$hostPackage", Context.MODE_PRIVATE)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun tryGetAccessToken(ctx: Context, hostPackage: String?, scopes: ArrayList<String?>): String? {
|
|
||||||
if (hostPackage.isNullOrEmpty()) return null
|
|
||||||
|
|
||||||
val prefs = getPrefsForHost(ctx, hostPackage)
|
|
||||||
val scopesString = prefs.getString(PREF_KEY_SCOPE, "")
|
|
||||||
val currentScope = stringToStringArray(scopesString)
|
|
||||||
if (!isSubset(scopes, currentScope))
|
|
||||||
return null
|
|
||||||
return prefs.getString(PREF_KEY_TOKEN, null)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isSubset(
|
|
||||||
requiredScopes: ArrayList<String?>,
|
|
||||||
availableScopes: ArrayList<String>
|
|
||||||
): Boolean {
|
|
||||||
return availableScopes.containsAll(requiredScopes)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeAccessToken(
|
|
||||||
ctx: Context, hostPackage: String?,
|
|
||||||
accessToken: String?
|
|
||||||
) {
|
|
||||||
val prefs = getPrefsForHost(ctx, hostPackage)
|
|
||||||
if (prefs.getString(PREF_KEY_TOKEN, "") == accessToken) {
|
|
||||||
val edit = prefs.edit()
|
|
||||||
edit.clear()
|
|
||||||
edit.apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
val hostPrefs = ctx.getSharedPreferences("KP2A.PluginAccess.hosts", Context.MODE_PRIVATE)
|
|
||||||
if (hostPrefs.contains(hostPackage)) {
|
|
||||||
hostPrefs.edit().remove(hostPackage).apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,9 +0,0 @@
|
|||||||
package net.helcel.fidelity.pluginSDK
|
|
||||||
|
|
||||||
@Suppress("unused")
|
|
||||||
object KeepassDef {
|
|
||||||
var TitleField: String = "Title"
|
|
||||||
var UserNameField: String = "UserName"
|
|
||||||
var PasswordField: String = "Password"
|
|
||||||
var UrlField: String = "URL"
|
|
||||||
}
|
|
@@ -1,49 +0,0 @@
|
|||||||
package net.helcel.fidelity.pluginSDK
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import org.json.JSONException
|
|
||||||
import org.json.JSONObject
|
|
||||||
|
|
||||||
object Kp2aControl {
|
|
||||||
|
|
||||||
fun getAddEntryIntent(
|
|
||||||
fields: HashMap<String, String>,
|
|
||||||
protectedFields: ArrayList<String>?
|
|
||||||
): Intent {
|
|
||||||
val outputData = JSONObject((fields as Map<*, *>)).toString()
|
|
||||||
val startKp2aIntent = Intent(Strings.ACTION_START_WITH_TASK)
|
|
||||||
startKp2aIntent.addCategory(Intent.CATEGORY_DEFAULT)
|
|
||||||
startKp2aIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
|
||||||
startKp2aIntent.putExtra("KP2A_APPTASK", "CreateEntryThenCloseTask")
|
|
||||||
startKp2aIntent.putExtra("ShowUserNotifications", "true")
|
|
||||||
startKp2aIntent.putExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA, outputData)
|
|
||||||
if (protectedFields != null)
|
|
||||||
startKp2aIntent.putStringArrayListExtra(
|
|
||||||
Strings.EXTRA_PROTECTED_FIELDS_LIST,
|
|
||||||
protectedFields
|
|
||||||
)
|
|
||||||
return startKp2aIntent
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getQueryEntryForOwnPackageIntent(): Intent {
|
|
||||||
return Intent(Strings.ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getEntryFieldsFromIntent(intent: Intent?): HashMap<String, String> {
|
|
||||||
val res = HashMap<String, String>()
|
|
||||||
try {
|
|
||||||
val json = JSONObject(intent?.getStringExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA) ?: "")
|
|
||||||
val itr = json.keys()
|
|
||||||
while (itr.hasNext()) {
|
|
||||||
val key = itr.next()
|
|
||||||
val value = json[key].toString()
|
|
||||||
res[key] = value
|
|
||||||
}
|
|
||||||
} catch (e: JSONException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
} catch (e: NullPointerException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,51 +0,0 @@
|
|||||||
package net.helcel.fidelity.pluginSDK
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
|
|
||||||
class PluginAccessBroadcastReceiver : BroadcastReceiver() {
|
|
||||||
override fun onReceive(ctx: Context, intent: Intent) {
|
|
||||||
val action = intent.action ?: return
|
|
||||||
when (action) {
|
|
||||||
Strings.ACTION_TRIGGER_REQUEST_ACCESS -> requestAccess(ctx, intent)
|
|
||||||
Strings.ACTION_RECEIVE_ACCESS -> receiveAccess(ctx, intent)
|
|
||||||
Strings.ACTION_REVOKE_ACCESS -> revokeAccess(ctx, intent)
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun revokeAccess(ctx: Context, intent: Intent) {
|
|
||||||
val senderPackage = intent.getStringExtra(Strings.EXTRA_SENDER)
|
|
||||||
val accessToken = intent.getStringExtra(Strings.EXTRA_ACCESS_TOKEN)
|
|
||||||
AccessManager.removeAccessToken(ctx, senderPackage, accessToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun receiveAccess(ctx: Context, intent: Intent) {
|
|
||||||
val senderPackage = intent.getStringExtra(Strings.EXTRA_SENDER)
|
|
||||||
val accessToken = intent.getStringExtra(Strings.EXTRA_ACCESS_TOKEN)
|
|
||||||
AccessManager.storeAccessToken(ctx, senderPackage, accessToken, scopes)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun requestAccess(ctx: Context, intent: Intent) {
|
|
||||||
val senderPackage = intent.getStringExtra(Strings.EXTRA_SENDER)
|
|
||||||
val requestToken = intent.getStringExtra(Strings.EXTRA_REQUEST_TOKEN)
|
|
||||||
val rpi = Intent(Strings.ACTION_REQUEST_ACCESS)
|
|
||||||
rpi.setPackage(senderPackage)
|
|
||||||
rpi.putExtra(Strings.EXTRA_SENDER, ctx.packageName)
|
|
||||||
rpi.putExtra(Strings.EXTRA_REQUEST_TOKEN, requestToken)
|
|
||||||
|
|
||||||
val token: String? = AccessManager.tryGetAccessToken(ctx, senderPackage, scopes)
|
|
||||||
rpi.putExtra(Strings.EXTRA_ACCESS_TOKEN, token)
|
|
||||||
|
|
||||||
rpi.putStringArrayListExtra(Strings.EXTRA_SCOPES, scopes)
|
|
||||||
ctx.sendBroadcast(rpi)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val scopes: ArrayList<String?> = ArrayList(
|
|
||||||
listOf(
|
|
||||||
Strings.SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
@@ -1,31 +0,0 @@
|
|||||||
package net.helcel.fidelity.pluginSDK
|
|
||||||
|
|
||||||
@Suppress("unused")
|
|
||||||
object Strings {
|
|
||||||
|
|
||||||
const val SCOPE_DATABASE_ACTIONS = "keepass2android.SCOPE_DATABASE_ACTIONS"
|
|
||||||
const val SCOPE_CURRENT_ENTRY = "keepass2android.SCOPE_CURRENT_ENTRY"
|
|
||||||
const val SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE =
|
|
||||||
"keepass2android.SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE"
|
|
||||||
const val SCOPE_QUERY_CREDENTIALS = "keepass2android.SCOPE_QUERY_CREDENTIALS"
|
|
||||||
|
|
||||||
const val EXTRA_SCOPES = "keepass2android.EXTRA_SCOPES"
|
|
||||||
const val EXTRA_PLUGIN_PACKAGE = "keepass2android.EXTRA_PLUGIN_PACKAGE"
|
|
||||||
|
|
||||||
const val EXTRA_SENDER = "keepass2android.EXTRA_SENDER"
|
|
||||||
const val EXTRA_REQUEST_TOKEN = "keepass2android.EXTRA_REQUEST_TOKEN"
|
|
||||||
const val ACTION_START_WITH_TASK = "keepass2android.ACTION_START_WITH_TASK"
|
|
||||||
|
|
||||||
const val ACTION_TRIGGER_REQUEST_ACCESS = "keepass2android.ACTION_TRIGGER_REQUEST_ACCESS"
|
|
||||||
const val ACTION_REQUEST_ACCESS = "keepass2android.ACTION_REQUEST_ACCESS"
|
|
||||||
const val ACTION_RECEIVE_ACCESS = "keepass2android.ACTION_RECEIVE_ACCESS"
|
|
||||||
const val ACTION_REVOKE_ACCESS = "keepass2android.ACTION_REVOKE_ACCESS"
|
|
||||||
|
|
||||||
|
|
||||||
const val EXTRA_ENTRY_OUTPUT_DATA = "keepass2android.EXTRA_ENTRY_OUTPUT_DATA"
|
|
||||||
const val EXTRA_PROTECTED_FIELDS_LIST = "keepass2android.EXTRA_PROTECTED_FIELDS_LIST"
|
|
||||||
const val EXTRA_ACCESS_TOKEN = "keepass2android.EXTRA_ACCESS_TOKEN"
|
|
||||||
|
|
||||||
const val ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE =
|
|
||||||
"keepass2android.ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE"
|
|
||||||
}
|
|
@@ -48,7 +48,7 @@ object BarcodeFormatConverter {
|
|||||||
BarcodeFormat.RSS_14 -> "RSS_14"
|
BarcodeFormat.RSS_14 -> "RSS_14"
|
||||||
BarcodeFormat.RSS_EXPANDED -> "RSS_EXPANDED"
|
BarcodeFormat.RSS_EXPANDED -> "RSS_EXPANDED"
|
||||||
BarcodeFormat.UPC_EAN_EXTENSION -> "UPC_EAN"
|
BarcodeFormat.UPC_EAN_EXTENSION -> "UPC_EAN"
|
||||||
else -> throw Exception("Unsupported Format: $f")
|
//else -> throw Exception("Unsupported Format: $f")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,6 +6,8 @@ import com.google.zxing.MultiFormatWriter
|
|||||||
import com.google.zxing.WriterException
|
import com.google.zxing.WriterException
|
||||||
import com.google.zxing.common.BitMatrix
|
import com.google.zxing.common.BitMatrix
|
||||||
import net.helcel.fidelity.tools.BarcodeFormatConverter.stringToFormat
|
import net.helcel.fidelity.tools.BarcodeFormatConverter.stringToFormat
|
||||||
|
import androidx.core.graphics.set
|
||||||
|
import androidx.core.graphics.createBitmap
|
||||||
|
|
||||||
object BarcodeGenerator {
|
object BarcodeGenerator {
|
||||||
|
|
||||||
@@ -31,13 +33,11 @@ object BarcodeGenerator {
|
|||||||
|
|
||||||
|
|
||||||
val bitMatrix: BitMatrix = writer.encode(content, format, width, height)
|
val bitMatrix: BitMatrix = writer.encode(content, format, width, height)
|
||||||
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
val bitmap = createBitmap(width, height)
|
||||||
|
|
||||||
for (x in 0 until width) {
|
for (x in 0 until width) {
|
||||||
for (y in 0 until height) {
|
for (y in 0 until height) {
|
||||||
bitmap.setPixel(
|
bitmap[x, y] = getPixelColor(bitMatrix, x, y)
|
||||||
x, y, getPixelColor(bitMatrix, x, y)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return bitmap
|
return bitmap
|
||||||
|
@@ -26,9 +26,9 @@ object BarcodeScanner {
|
|||||||
try {
|
try {
|
||||||
val result = reader.decode(binaryBitmap)
|
val result = reader.decode(binaryBitmap)
|
||||||
cb(result.text, formatToString(result.barcodeFormat))
|
cb(result.text, formatToString(result.barcodeFormat))
|
||||||
} catch (e: NotFoundException) {
|
} catch (_: NotFoundException) {
|
||||||
cb(null, null)
|
cb(null, null)
|
||||||
} catch (e: ReaderException) {
|
} catch (_: ReaderException) {
|
||||||
cb(null, null)
|
cb(null, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
142
app/src/main/java/net/helcel/fidelity/tools/BiometricStore.kt
Normal file
142
app/src/main/java/net/helcel/fidelity/tools/BiometricStore.kt
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
package net.helcel.fidelity.tools
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.security.keystore.KeyGenParameterSpec
|
||||||
|
import android.security.keystore.KeyProperties
|
||||||
|
import android.util.Base64
|
||||||
|
import androidx.biometric.BiometricPrompt
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import androidx.datastore.preferences.core.*
|
||||||
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||||
|
import com.kunzisoft.keepass.utils.parseUri
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import java.security.KeyStore
|
||||||
|
import javax.crypto.KeyGenerator
|
||||||
|
import javax.crypto.SecretKey
|
||||||
|
|
||||||
|
val Context.securePrefs by preferencesDataStore("keepass_prefs")
|
||||||
|
object KeePassKeys {
|
||||||
|
val DB_FILE_PATH = stringPreferencesKey("db_file_path")
|
||||||
|
val PASSWORD = stringPreferencesKey("password_enc")
|
||||||
|
val KEY_FILE_PATH = stringPreferencesKey("key_file_path")
|
||||||
|
val IV = stringPreferencesKey("iv")
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class CredentialResult {
|
||||||
|
data class Success(val db: Uri?, val password: String, val key: Uri?) : CredentialResult()
|
||||||
|
object NoData : CredentialResult()
|
||||||
|
object AuthFailed : CredentialResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val KEY_ALIAS = "keepass_bio_key"
|
||||||
|
|
||||||
|
fun getOrCreateBiometricKey(): SecretKey {
|
||||||
|
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
|
||||||
|
keyStore.getKey(KEY_ALIAS, null)?.let { return it as SecretKey }
|
||||||
|
val keyGenerator =
|
||||||
|
KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
|
||||||
|
val spec = KeyGenParameterSpec.Builder(
|
||||||
|
KEY_ALIAS,
|
||||||
|
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
|
||||||
|
).apply {
|
||||||
|
setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
||||||
|
setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||||
|
setUserAuthenticationRequired(true)
|
||||||
|
setInvalidatedByBiometricEnrollment(true)
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
keyGenerator.init(spec)
|
||||||
|
return keyGenerator.generateKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCipherForDecryption(key: SecretKey, iv: ByteArray?): Cipher {
|
||||||
|
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
||||||
|
if(iv==null) cipher.init(Cipher.ENCRYPT_MODE, key)
|
||||||
|
else cipher.init(Cipher.DECRYPT_MODE, key, javax.crypto.spec.GCMParameterSpec(128, iv))
|
||||||
|
return cipher
|
||||||
|
}
|
||||||
|
object KeePassStore {
|
||||||
|
suspend fun saveCredentials(
|
||||||
|
context: Context, cred: CredentialResult.Success
|
||||||
|
): CredentialResult {
|
||||||
|
val cipher = showBiometricPrompt(context as FragmentActivity, true)
|
||||||
|
?: return CredentialResult.AuthFailed
|
||||||
|
val encPasswordB = cipher.doFinal(cred.password.toByteArray(Charsets.UTF_8))
|
||||||
|
context.securePrefs.edit { prefs ->
|
||||||
|
prefs[KeePassKeys.DB_FILE_PATH] = cred.db.toString()
|
||||||
|
prefs[KeePassKeys.PASSWORD] = Base64.encodeToString(encPasswordB, Base64.DEFAULT)
|
||||||
|
prefs[KeePassKeys.IV] = Base64.encodeToString(cipher.iv, Base64.DEFAULT)
|
||||||
|
cred.key?.let { prefs[KeePassKeys.KEY_FILE_PATH] = it.toString() }
|
||||||
|
}
|
||||||
|
return cred
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun hasCredentials(context: Context): Boolean {
|
||||||
|
val prefs = context.securePrefs.data.first()
|
||||||
|
return prefs[KeePassKeys.DB_FILE_PATH] != null &&
|
||||||
|
prefs[KeePassKeys.PASSWORD] != null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun packCredentials(dbFilePath:Uri?, password: String, keyFilePath: Uri?): CredentialResult.Success {
|
||||||
|
return CredentialResult.Success(dbFilePath, password, keyFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun loadCredentials(context: Context): CredentialResult {
|
||||||
|
val prefs = context.securePrefs.data.first { true }
|
||||||
|
val dbFilePath = prefs[KeePassKeys.DB_FILE_PATH] ?: return CredentialResult.NoData
|
||||||
|
val encryptedBase64 = prefs[KeePassKeys.PASSWORD] ?: return CredentialResult.NoData
|
||||||
|
val keyFilePath = prefs[KeePassKeys.KEY_FILE_PATH]
|
||||||
|
val cipher = showBiometricPrompt(context as FragmentActivity, false)
|
||||||
|
?: return CredentialResult.AuthFailed
|
||||||
|
val decrypted = cipher.doFinal(Base64.decode(encryptedBase64, Base64.DEFAULT))
|
||||||
|
return packCredentials(
|
||||||
|
dbFilePath.parseUri(),
|
||||||
|
String(decrypted, Charsets.UTF_8),
|
||||||
|
keyFilePath?.parseUri()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
suspend fun showBiometricPrompt(activity: FragmentActivity, enc: Boolean): Cipher? {
|
||||||
|
val prefs = activity.securePrefs.data.first()
|
||||||
|
return suspendCancellableCoroutine { cont ->
|
||||||
|
val executor = ContextCompat.getMainExecutor(activity)
|
||||||
|
val biometricPrompt = BiometricPrompt(
|
||||||
|
activity,
|
||||||
|
executor,
|
||||||
|
object : BiometricPrompt.AuthenticationCallback() {
|
||||||
|
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { cont.resume(result.cryptoObject?.cipher) {} }
|
||||||
|
override fun onAuthenticationError(code: Int, msg: CharSequence) { cont.resume(null) {} }
|
||||||
|
override fun onAuthenticationFailed() { cont.resume(null) {} }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val iv = if(enc) null else prefs[KeePassKeys.IV]?.let { Base64.decode(it, Base64.DEFAULT) }
|
||||||
|
if (!enc && iv == null) { cont.resume(null) {} }
|
||||||
|
val cipher = getCipherForDecryption(getOrCreateBiometricKey(), iv)
|
||||||
|
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
||||||
|
.setTitle("Unlock KeePass")
|
||||||
|
.setSubtitle("Authenticate to access your KeePass database")
|
||||||
|
.setNegativeButtonText("Cancel")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun retrieveResponseFromChallenge(
|
||||||
|
hardwareKey: HardwareKey,
|
||||||
|
seed: ByteArray?,
|
||||||
|
): ByteArray {
|
||||||
|
val response: ByteArray = "".toByteArray()
|
||||||
|
return response
|
||||||
|
}
|
@@ -1,50 +0,0 @@
|
|||||||
package net.helcel.fidelity.tools
|
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.reflect.TypeToken
|
|
||||||
|
|
||||||
|
|
||||||
object CacheManager {
|
|
||||||
|
|
||||||
const val PREF_NAME = "FIDELITY"
|
|
||||||
private const val ENTRY_KEY = "FIDELITY"
|
|
||||||
private var data: ArrayList<Triple<String?, String?, String?>> = ArrayList()
|
|
||||||
private var pref: SharedPreferences? = null
|
|
||||||
|
|
||||||
fun addFidelity(item: Triple<String?, String?, String?>) {
|
|
||||||
val exists = data.find { it.first == item.first }
|
|
||||||
if (exists != null)
|
|
||||||
data.remove(exists)
|
|
||||||
|
|
||||||
data.add(0, item)
|
|
||||||
saveFidelity()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun rmFidelity(idx: Int) {
|
|
||||||
data.removeAt(idx)
|
|
||||||
saveFidelity()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveFidelity() {
|
|
||||||
val editor = pref?.edit()
|
|
||||||
val gson = Gson()
|
|
||||||
val json = gson.toJson(data)
|
|
||||||
editor?.putString(ENTRY_KEY, json)
|
|
||||||
editor?.apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun loadFidelity(pref: SharedPreferences) {
|
|
||||||
this.pref = pref
|
|
||||||
val gson = Gson()
|
|
||||||
val json = pref.getString(ENTRY_KEY, null)
|
|
||||||
val type = object : TypeToken<List<Triple<String, String, Int>>>() {}.type
|
|
||||||
data = gson.fromJson(json, type) ?: ArrayList()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getFidelity(): ArrayList<Triple<String?, String?, String?>> {
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,31 +0,0 @@
|
|||||||
package net.helcel.fidelity.tools
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.widget.Toast
|
|
||||||
|
|
||||||
object ErrorToaster {
|
|
||||||
private fun helper(activity: Context?, message: String, length: Int) {
|
|
||||||
if (activity != null)
|
|
||||||
Toast.makeText(activity, message, length).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun noKP2AFound(activity: Context?) {
|
|
||||||
helper(activity, "KeePass2Android Not Installed", Toast.LENGTH_LONG)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun formIncomplete(activity: Context?) {
|
|
||||||
helper(activity, "Form Incomplete", Toast.LENGTH_SHORT)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun invalidFormat(activity: Context?) {
|
|
||||||
helper(activity, "Invalid Format", Toast.LENGTH_SHORT)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun nothingFound(activity: Context?) {
|
|
||||||
helper(activity, "Nothing Found", Toast.LENGTH_SHORT)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun noPermission(activity: Context?) {
|
|
||||||
helper(activity, "Missing Permission", Toast.LENGTH_LONG)
|
|
||||||
}
|
|
||||||
}
|
|
185
app/src/main/java/net/helcel/fidelity/tools/Keepass.kt
Normal file
185
app/src/main/java/net/helcel/fidelity/tools/Keepass.kt
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
package net.helcel.fidelity.tools
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import com.kunzisoft.keepass.database.element.Database
|
||||||
|
import com.kunzisoft.keepass.database.element.Field
|
||||||
|
import com.kunzisoft.keepass.database.element.Group
|
||||||
|
import com.kunzisoft.keepass.database.element.MasterCredential
|
||||||
|
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
||||||
|
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
||||||
|
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||||
|
import com.kunzisoft.keepass.hardware.HardwareKey
|
||||||
|
import com.kunzisoft.keepass.utils.getBinaryDir
|
||||||
|
import kotlinx.serialization.builtins.ListSerializer
|
||||||
|
import java.io.File
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
object FidelityKeepassFields {
|
||||||
|
const val FIDELITYFORMAT = "FidelityFormat"
|
||||||
|
const val FIDELITYCODE = "FidelityCode"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class FidelityEntry(
|
||||||
|
val uid: String? = null,
|
||||||
|
val title: String = "",
|
||||||
|
val code: String = "",
|
||||||
|
val format: String = "",
|
||||||
|
val protected: Boolean = false,
|
||||||
|
|
||||||
|
val hidden: Boolean = false,
|
||||||
|
val pinned: Boolean = false,
|
||||||
|
val lastUse: Int = 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
object FidelityRepository {
|
||||||
|
private var db: Database = Database()
|
||||||
|
private var binaryDir: File? = null
|
||||||
|
val entries = mutableStateListOf<FidelityEntry>()
|
||||||
|
val activeEntry = mutableStateOf(FidelityEntry())
|
||||||
|
|
||||||
|
|
||||||
|
fun getRoot(): Group? {
|
||||||
|
return db.rootGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
fun start(ctx: Context, uri: Uri?, c: MasterCredential): Boolean {
|
||||||
|
if (binaryDir == null) binaryDir = ctx.getBinaryDir()
|
||||||
|
if (uri == null) return false
|
||||||
|
try {
|
||||||
|
val bitStream =
|
||||||
|
ByteArrayInputStream(ctx.contentResolver.openInputStream(uri)?.readBytes())
|
||||||
|
db.loadData(
|
||||||
|
bitStream, c,
|
||||||
|
{ hardwareKey, seed -> retrieveResponseFromChallenge(hardwareKey, seed) },
|
||||||
|
false, binaryDir!!,
|
||||||
|
{ BinaryData.canMemoryBeAllocatedInRAM(ctx, it) },
|
||||||
|
false, null
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println(e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun end(ctx: Context, uri: Uri?, c: MasterCredential): Boolean {
|
||||||
|
if (uri == null) return false
|
||||||
|
db.saveData(
|
||||||
|
File(binaryDir, db.binaryCache.hashCode().toString()),{ ctx.contentResolver.openOutputStream(uri) },
|
||||||
|
false, c, { hardwareKey, seed -> retrieveResponseFromChallenge(hardwareKey, seed) })
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun genCredentials(
|
||||||
|
ctx: Context,
|
||||||
|
cred: CredentialResult.Success,
|
||||||
|
hardwareKey: HardwareKey? = null
|
||||||
|
): MasterCredential {
|
||||||
|
return MasterCredential(
|
||||||
|
cred.password,
|
||||||
|
cred.key?.let { ctx.contentResolver.openInputStream(cred.key)?.readBytes() },
|
||||||
|
hardwareKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun importDB(context: Context) {
|
||||||
|
val seenID= arrayListOf<String>()
|
||||||
|
fun importDBRec(group: Group) {
|
||||||
|
group.getChildEntries().forEach {
|
||||||
|
val fields = it.getExtraFields()
|
||||||
|
val code = fields.firstOrNull { e -> e.name == FidelityKeepassFields.FIDELITYCODE }
|
||||||
|
val format =
|
||||||
|
fields.firstOrNull { e -> e.name == FidelityKeepassFields.FIDELITYFORMAT }
|
||||||
|
if (code == null || format == null) return@forEach
|
||||||
|
|
||||||
|
val newEntry = FidelityEntry(
|
||||||
|
uid=it.nodeId.id.toString(),
|
||||||
|
title=it.title,
|
||||||
|
code=code.protectedValue.stringValue,
|
||||||
|
format=format.protectedValue.stringValue,
|
||||||
|
protected=code.protectedValue.isProtected,
|
||||||
|
)
|
||||||
|
val idx = entries.indexOfFirst { e -> e.uid == newEntry.uid }
|
||||||
|
seenID.add(newEntry.uid!!)
|
||||||
|
if (idx >= 0) {
|
||||||
|
val oldEntry = entries[idx]
|
||||||
|
entries[idx] = newEntry.copy(
|
||||||
|
pinned = oldEntry.pinned,
|
||||||
|
hidden = oldEntry.hidden,
|
||||||
|
lastUse = oldEntry.lastUse
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
entries.add(newEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
group.getChildGroups().forEach { importDBRec(it) }
|
||||||
|
}
|
||||||
|
if (db.rootGroup != null)
|
||||||
|
importDBRec(db.rootGroup!!)
|
||||||
|
entries.removeAll { !seenID.contains(it.uid)}
|
||||||
|
val distinct = entries.distinctBy { it.uid }
|
||||||
|
entries.clear()
|
||||||
|
entries.addAll(distinct)
|
||||||
|
saveEntries(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveEntries(context: Context) {
|
||||||
|
val prefs = context.getSharedPreferences("fidelity_prefs", Context.MODE_PRIVATE)
|
||||||
|
prefs.edit { putString("entries", Json.encodeToString(
|
||||||
|
ListSerializer(FidelityEntry.serializer()),
|
||||||
|
entries
|
||||||
|
)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadEntries(context: Context) {
|
||||||
|
val prefs = context.getSharedPreferences("fidelity_prefs", Context.MODE_PRIVATE)
|
||||||
|
try {
|
||||||
|
val json = prefs.getString("entries", null) ?: return
|
||||||
|
val list = Json.decodeFromString(
|
||||||
|
ListSerializer(FidelityEntry.serializer()),
|
||||||
|
json
|
||||||
|
)
|
||||||
|
|
||||||
|
entries.clear()
|
||||||
|
entries.addAll(list)
|
||||||
|
}catch(_: Exception){
|
||||||
|
prefs.edit{ putString("entries",Json.encodeToString(
|
||||||
|
ListSerializer(FidelityEntry.serializer()),emptyList()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addEntry(ctx: Context, entry: FidelityEntry) {
|
||||||
|
val dbEntry = db.getEntryById(NodeIdUUID(UUID.fromString(entry.uid))) ?: db.createEntry()
|
||||||
|
val dbParent = db.getGroupById(NodeIdUUID(UUID.fromString(entry.uid)))
|
||||||
|
dbEntry?.apply {
|
||||||
|
putExtraField(
|
||||||
|
Field(
|
||||||
|
FidelityKeepassFields.FIDELITYCODE,
|
||||||
|
ProtectedString(entry.protected, entry.code)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
putExtraField(
|
||||||
|
Field(
|
||||||
|
FidelityKeepassFields.FIDELITYFORMAT,
|
||||||
|
ProtectedString(string= entry.format)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if(dbParent!=null) title = entry.title
|
||||||
|
dbParent?.addChildEntry(dbEntry)
|
||||||
|
}
|
||||||
|
entries.removeIf {it.uid == entry.uid}
|
||||||
|
entries.add(entry.copy(uid=dbEntry?.nodeId?.id.toString()))
|
||||||
|
saveEntries(ctx)
|
||||||
|
}
|
||||||
|
}
|
@@ -1,85 +0,0 @@
|
|||||||
package net.helcel.fidelity.tools
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import net.helcel.fidelity.pluginSDK.KeepassDef
|
|
||||||
import net.helcel.fidelity.pluginSDK.Kp2aControl
|
|
||||||
|
|
||||||
object KeepassWrapper {
|
|
||||||
|
|
||||||
private const val CODE_FIELD: String = "FidelityCode"
|
|
||||||
private const val FORMAT_FIELD: String = "FidelityFormat"
|
|
||||||
private const val PROTECT_CODE_FIELD: String = "FidelityProtectedCode"
|
|
||||||
|
|
||||||
fun entryCreate(
|
|
||||||
fragment: Fragment,
|
|
||||||
title: String,
|
|
||||||
code: String,
|
|
||||||
format: String,
|
|
||||||
protectCode: Boolean,
|
|
||||||
): Pair<HashMap<String, String>, ArrayList<String>> {
|
|
||||||
|
|
||||||
val fields = HashMap<String, String>()
|
|
||||||
val protected = ArrayList<String>()
|
|
||||||
fields[KeepassDef.TitleField] = title
|
|
||||||
fields[KeepassDef.UrlField] =
|
|
||||||
"androidapp://" + fragment.requireActivity().packageName
|
|
||||||
fields[CODE_FIELD] = code
|
|
||||||
fields[FORMAT_FIELD] = format
|
|
||||||
fields[PROTECT_CODE_FIELD] = protectCode.toString()
|
|
||||||
protected.add(CODE_FIELD)
|
|
||||||
|
|
||||||
return Pair(fields, protected)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun resultLauncher(
|
|
||||||
fragment: Fragment,
|
|
||||||
callback: (HashMap<String, String>) -> Unit
|
|
||||||
): ActivityResultLauncher<Intent> {
|
|
||||||
return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
|
||||||
if (result.resultCode == Activity.RESULT_OK) {
|
|
||||||
val credentials = Kp2aControl.getEntryFieldsFromIntent(result.data)
|
|
||||||
callback(credentials)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun entryExtract(map: HashMap<String, String>): Triple<String?, String?, String?> {
|
|
||||||
return Triple(
|
|
||||||
map[KeepassDef.TitleField],
|
|
||||||
map[CODE_FIELD],
|
|
||||||
map[FORMAT_FIELD]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun bundleCreate(title: String?, code: String?, fmt: String?): Bundle {
|
|
||||||
val data = Bundle()
|
|
||||||
data.putString("title", title)
|
|
||||||
data.putString("code", code)
|
|
||||||
data.putString("fmt", fmt)
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
fun bundleCreate(triple: Triple<String?, String?, String?>): Bundle {
|
|
||||||
return bundleCreate(triple.first, triple.second, triple.third)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun bundleExtract(data: Bundle?): Triple<String?, String?, String?> {
|
|
||||||
return Triple(
|
|
||||||
data?.getString("title"),
|
|
||||||
data?.getString("code"),
|
|
||||||
data?.getString("fmt")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isProtected(map: HashMap<String, String>): Boolean {
|
|
||||||
return map[PROTECT_CODE_FIELD].toBoolean()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@@ -1,17 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:fitsSystemWindows="true"
|
|
||||||
android:orientation="vertical"
|
|
||||||
tools:context=".activity.MainActivity">
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:id="@+id/container"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:background="@color/black"
|
|
||||||
tools:ignore="MergeRootFrame" />
|
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@@ -1,113 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical"
|
|
||||||
tools:context=".activity.fragment.CreateEntry">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="16dp">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:hint="@string/title">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
|
||||||
android:id="@+id/editTextTitle"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:imeOptions="actionNext"
|
|
||||||
android:inputType="text"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:minLines="1" />
|
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
|
||||||
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="16dp">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
|
||||||
android:id="@+id/codeInputLayout"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="@string/code"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/checkboxProtected"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
|
||||||
android:id="@+id/editTextCode"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:imeOptions="actionDone"
|
|
||||||
android:inputType="text"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:minLines="1" />
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
|
||||||
|
|
||||||
|
|
||||||
<com.google.android.material.checkbox.MaterialCheckBox
|
|
||||||
android:id="@+id/checkboxProtected"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:button="@drawable/lock_checkbox"
|
|
||||||
android:scaleX="0.40"
|
|
||||||
android:scaleY="0.40"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@+id/codeInputLayout"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
|
||||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:hint="@string/format"
|
|
||||||
android:labelFor="@id/edit_text_format">
|
|
||||||
|
|
||||||
<AutoCompleteTextView
|
|
||||||
android:id="@+id/edit_text_format"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:focusable="false"
|
|
||||||
android:inputType="none" />
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
|
||||||
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/imageViewPreview"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:contentDescription="@string/barcode_preview"
|
|
||||||
android:scaleType="fitCenter" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:id="@+id/btnSave"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentEnd="true"
|
|
||||||
android:layout_alignParentBottom="true"
|
|
||||||
android:layout_margin="24dp"
|
|
||||||
android:contentDescription="@string/save"
|
|
||||||
app:fabCustomSize="46dp"
|
|
||||||
app:maxImageSize="32dp"
|
|
||||||
app:srcCompat="@drawable/save" />
|
|
||||||
</RelativeLayout>
|
|
@@ -1,94 +0,0 @@
|
|||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:background="@color/black"
|
|
||||||
android:orientation="vertical"
|
|
||||||
tools:context=".activity.fragment.Launcher">
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/fidelityList"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_margin="24dp" />
|
|
||||||
|
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:id="@+id/btnQuery"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentBottom="true"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:layout_margin="24dp"
|
|
||||||
android:contentDescription="@string/query"
|
|
||||||
app:fabCustomSize="46dp"
|
|
||||||
app:maxImageSize="32dp"
|
|
||||||
app:srcCompat="@drawable/search" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentEnd="true"
|
|
||||||
android:layout_alignParentBottom="true"
|
|
||||||
android:layout_margin="16dp"
|
|
||||||
android:background="@android:color/transparent"
|
|
||||||
android:orientation="vertical"
|
|
||||||
tools:ignore="RelativeOverlap">
|
|
||||||
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/menuAdd"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="@android:color/transparent"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:visibility="visible">
|
|
||||||
|
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:id="@+id/btnScan"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="8dp"
|
|
||||||
android:contentDescription="@string/scan"
|
|
||||||
app:fabCustomSize="46dp"
|
|
||||||
app:maxImageSize="32dp"
|
|
||||||
app:srcCompat="@drawable/camera" />
|
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:id="@+id/btnOpen"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="8dp"
|
|
||||||
android:contentDescription="@string/open"
|
|
||||||
app:fabCustomSize="46dp"
|
|
||||||
app:maxImageSize="32dp"
|
|
||||||
app:srcCompat="@drawable/open" />
|
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:id="@+id/btnManual"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="8dp"
|
|
||||||
android:contentDescription="@string/manual"
|
|
||||||
app:fabCustomSize="46dp"
|
|
||||||
app:maxImageSize="32dp"
|
|
||||||
app:srcCompat="@drawable/edit" />
|
|
||||||
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:id="@+id/btnAdd"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="bottom|end"
|
|
||||||
android:layout_margin="8dp"
|
|
||||||
android:contentDescription="@string/expand"
|
|
||||||
app:fabCustomSize="46dp"
|
|
||||||
app:maxImageSize="32dp" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
@@ -1,37 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical"
|
|
||||||
tools:context=".activity.fragment.Scanner">
|
|
||||||
|
|
||||||
<androidx.camera.view.PreviewView
|
|
||||||
android:id="@+id/cameraView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent" />
|
|
||||||
|
|
||||||
<net.helcel.fidelity.activity.view.ScannerView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent" />
|
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:id="@+id/btnScanDone"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentBottom="true"
|
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:layout_margin="24dp"
|
|
||||||
android:contentDescription="@string/manual" />
|
|
||||||
|
|
||||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
|
||||||
android:id="@+id/ScanActive"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentBottom="true"
|
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:layout_margin="28dp"
|
|
||||||
android:indeterminate="true" />
|
|
||||||
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
@@ -1,39 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="16dp"
|
|
||||||
tools:context=".activity.fragment.ViewEntry">
|
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
|
||||||
android:id="@+id/title"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="16dp"
|
|
||||||
android:layout_marginEnd="16dp"
|
|
||||||
android:hint="@string/title"
|
|
||||||
android:textAlignment="center"
|
|
||||||
android:textSize="42sp"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/imageViewPreview"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintVertical_bias="0.0" />
|
|
||||||
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/imageViewPreview"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:contentDescription="@string/barcode_preview"
|
|
||||||
android:scaleType="fitCenter"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/title" />
|
|
||||||
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:padding="15dp"
|
|
||||||
android:text=""
|
|
||||||
android:textSize="18sp"
|
|
||||||
android:textStyle="bold" />
|
|
@@ -1,30 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:id="@+id/card"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="16dp"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:layout_marginEnd="16dp"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
app:cardCornerRadius="8dp"
|
|
||||||
app:cardElevation="4dp"
|
|
||||||
app:cardMaxElevation="4dp"
|
|
||||||
app:cardPreventCornerOverlap="false"
|
|
||||||
app:cardUseCompatPadding="true">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="16dp">
|
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
|
||||||
android:id="@+id/textView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textSize="18sp"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
</LinearLayout>
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
|
@@ -1,3 +0,0 @@
|
|||||||
<resources>
|
|
||||||
|
|
||||||
</resources>
|
|
@@ -1,10 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources>
|
||||||
<string name="kp2aplugin_title" tools:keep="@string/kp2aplugin_title">Fidelity</string>
|
<string name="key_theme">App theme</string>
|
||||||
<string name="kp2aplugin_shortdesc" tools:keep="@string/kp2aplugin_shortdesc">Fidelity adds an interface to manage fidelity cards and other barcodes to Keepass2Android</string>
|
<string name="system">System</string>
|
||||||
<string name="kp2aplugin_author" tools:keep="@string/kp2aplugin_author">Soraefir</string>
|
<string name="light">Light</string>
|
||||||
|
<string name="dark">Dark</string>
|
||||||
<string name="app_name">Keepass Fidelity</string>
|
<string name="key_stats">Statistics</string>
|
||||||
|
|
||||||
<string name="barcode_preview">barcode preview</string>
|
<string name="barcode_preview">barcode preview</string>
|
||||||
<string name="expand">Expand</string>
|
<string name="expand">Expand</string>
|
||||||
|
@@ -1,11 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<style name="Theme.Fidelity" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
|
||||||
<item name="colorPrimary">@color/blue</item>
|
|
||||||
<item name="colorPrimaryVariant">@color/blue</item>
|
|
||||||
<item name="colorSecondary">@color/blue</item>
|
|
||||||
<item name="colorSecondaryVariant">@color/blue</item>
|
|
||||||
<item name="colorOnPrimary">@color/darkgray</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
@@ -1,5 +1,13 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
// ext.kotlin_version = '1.8.20'
|
||||||
|
// ext.android_core_version = '1.10.1'
|
||||||
|
// ext.android_appcompat_version = '1.6.1'
|
||||||
|
// ext.android_material_version = '1.9.0'
|
||||||
|
ext.android_test_version = '1.5.2'
|
||||||
|
}
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application' version '8.13.0' apply false
|
id 'com.android.application' version '8.13.0' apply false
|
||||||
id 'com.android.library' version '8.13.0' apply false
|
id 'com.android.library' version '8.13.0' apply false
|
||||||
|
1
external/KeePassDX
vendored
Submodule
1
external/KeePassDX
vendored
Submodule
Submodule external/KeePassDX added at a7d0467127
@@ -14,6 +14,11 @@ dependencyResolutionManagement {
|
|||||||
maven { url 'https://jitpack.io' }
|
maven { url 'https://jitpack.io' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
include(":database")
|
||||||
|
project(":database").projectDir = file("external/KeePassDX/database")
|
||||||
|
|
||||||
|
include(":crypto")
|
||||||
|
project(":crypto").projectDir = file("external/KeePassDX/crypto")
|
||||||
|
|
||||||
rootProject.name = "Fidelity"
|
rootProject.name = "Fidelity"
|
||||||
include ':app'
|
include ':app'
|
||||||
|
Reference in New Issue
Block a user