1 Commits

Author SHA1 Message Date
Renovate Bot
e5628190b7 Update plugin com.autonomousapps.dependency-analysis to v2.8.2 2025-02-11 02:00:52 +00:00
46 changed files with 1490 additions and 1949 deletions

View File

@@ -23,9 +23,8 @@ jobs:
contents: write contents: write
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
with:
submodules: true
- name: set up secrets - name: set up secrets
run: | run: |
echo "${{ secrets.RELEASE_KEYSTORE }}" > keystore.asc echo "${{ secrets.RELEASE_KEYSTORE }}" > keystore.asc
@@ -42,7 +41,7 @@ jobs:
run: git checkout -B "$BRANCH" run: git checkout -B "$BRANCH"
- name: set up JDK - name: set up JDK
uses: actions/setup-java@v5 uses: actions/setup-java@v4
with: with:
java-version: 17 java-version: 17
distribution: "temurin" distribution: "temurin"

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "external/KeePassDX"]
path = external/KeePassDX
url = https://github.com/Kunzisoft/KeePassDX.git

View File

@@ -1,55 +1,45 @@
plugins { 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.1.10'
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 36 compileSdk 34
defaultConfig { defaultConfig {
applicationId 'net.helcel.fidelity' applicationId 'net.helcel.fidelity'
versionName "1.0d" resValue "string", "app_name", "Keepass Fidelity"
buildConfigField("String", "APP_NAME", "\"Keepass Fidelity\"")
manifestPlaceholders["APP_NAME"] = "Keepass Fidelity"
minSdk 28 minSdk 28
targetSdk 36 targetSdk 34
} }
signingConfigs { signingConfigs {
create("release") { create("release") {
try { keyAlias keystoreProperties['keyAlias']
def keystorePropertiesFile = rootProject.file("app/keystore.properties") keyPassword keystoreProperties['keyPassword']
def keystoreProperties = new Properties() storeFile file(keystoreProperties['storeFile'])
keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) storePassword keystoreProperties['storePassword']
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 {
initWith(buildTypes.release)
matchingFallbacks = ['release']
signingConfig = signingConfigs.getByName("release") signingConfig = signingConfigs.getByName("release")
} }
} }
@@ -58,15 +48,17 @@ android {
compileOptions { compileOptions {
coreLibraryDesugaringEnabled true coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_21 sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_21 targetCompatibility JavaVersion.VERSION_17
encoding 'utf-8' encoding 'utf-8'
} }
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17
}
buildFeatures { buildFeatures {
viewBinding true viewBinding true
compose true
buildConfig true
} }
dependenciesInfo { dependenciesInfo {
@@ -75,54 +67,18 @@ 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' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.1.4'
implementation 'androidx.compose.material3:material3:1.4.0'
implementation 'androidx.compose.material:material:1.9.4'
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.camera:camera-lifecycle:1.4.1'
implementation "androidx.security:security-crypto:1.1.0" implementation 'androidx.camera:camera-view:1.4.1'
implementation "androidx.datastore:datastore-preferences:1.1.7" runtimeOnly 'androidx.camera:camera-camera2:1.4.1'
implementation "androidx.security:security-crypto:1.1.0"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.1.5' implementation 'com.google.code.gson:gson:2.12.1'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.camera:camera-lifecycle:1.5.1'
implementation 'androidx.camera:camera-view:1.5.1'
runtimeOnly 'androidx.camera:camera-camera2:1.5.1'
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.10.01')
implementation 'androidx.compose.ui:ui-tooling:1.9.3'
implementation 'androidx.compose.ui:ui-tooling-preview'
//Submodule
//noinspection NewerVersionAvailable
implementation 'joda-time:joda-time:2.14.0'
implementation 'org.joda:joda-convert:3.0.1'
} }

View File

@@ -2,6 +2,12 @@
# fields. Proguard removes such information by default, keep it. # fields. Proguard removes such information by default, keep it.
-keepattributes Signature -keepattributes Signature
-keep class org.joda.convert.** { *; } # 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

View File

@@ -1,20 +1,37 @@
<?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="${APP_NAME}" android:label="@string/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>

View File

@@ -1,65 +0,0 @@
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
)
}

View File

@@ -1,62 +1,65 @@
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.compose.BackHandler import androidx.activity.addCallback
import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.LaunchedEffect import net.helcel.fidelity.R
import androidx.compose.ui.platform.LocalContext import net.helcel.fidelity.activity.fragment.Launcher
import androidx.fragment.app.FragmentActivity import net.helcel.fidelity.activity.fragment.ViewEntry
import androidx.navigation.compose.NavHost import net.helcel.fidelity.databinding.ActMainBinding
import androidx.navigation.compose.composable import net.helcel.fidelity.pluginSDK.Kp2aControl.getEntryFieldsFromIntent
import androidx.navigation.compose.rememberNavController import net.helcel.fidelity.tools.CacheManager
import net.helcel.fidelity.activity.fragment.CreateEntryScreen import net.helcel.fidelity.tools.KeepassWrapper.bundleCreate
import net.helcel.fidelity.activity.fragment.FileScanner import net.helcel.fidelity.tools.KeepassWrapper.entryExtract
import net.helcel.fidelity.activity.fragment.InitialScreen
import net.helcel.fidelity.activity.fragment.LauncherScreen @SuppressLint("SourceLockedOrientationActivity")
import net.helcel.fidelity.activity.fragment.ScannerScreen class MainActivity : AppCompatActivity() {
import net.helcel.fidelity.activity.fragment.ViewEntryScreen
import net.helcel.fidelity.tools.FidelityRepository.entries private lateinit var binding: ActMainBinding
import net.helcel.fidelity.tools.FidelityRepository.loadEntries private lateinit var sharedPreferences: SharedPreferences
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)
actionBar?.hide() sharedPreferences =
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT this.getSharedPreferences(CacheManager.PREF_NAME, Context.MODE_PRIVATE)
loadEntries(this.baseContext) CacheManager.loadFidelity(sharedPreferences)
setContent { binding = ActMainBinding.inflate(layoutInflater)
SysTheme { setContentView(binding.root)
val navController = rememberNavController() onBackPressedDispatcher.addCallback(this) {
val context = LocalContext.current if (supportFragmentManager.backStackEntryCount > 0) {
supportFragmentManager.popBackStackImmediate()
BackHandler { loadLauncher()
if (!navController.popBackStack()) finish() requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
} } else {
LaunchedEffect(Unit) { finish()
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()
} }
} }

View File

@@ -0,0 +1,49 @@
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) }
}
}
}

View File

@@ -1,370 +1,167 @@
package net.helcel.fidelity.activity.fragment package net.helcel.fidelity.activity.fragment
import android.graphics.Bitmap import android.content.ActivityNotFoundException
import androidx.compose.foundation.Image import android.os.Bundle
import androidx.compose.foundation.background import android.os.Handler
import androidx.compose.foundation.clickable import android.os.Looper
import androidx.compose.foundation.interaction.MutableInteractionSource import android.view.LayoutInflater
import androidx.compose.foundation.layout.Arrangement import android.view.View
import androidx.compose.foundation.layout.Box import android.view.ViewGroup
import androidx.compose.foundation.layout.Column import android.view.inputmethod.EditorInfo
import androidx.compose.foundation.layout.Row import android.widget.ArrayAdapter
import androidx.compose.foundation.layout.Spacer import androidx.core.widget.addTextChangedListener
import androidx.compose.foundation.layout.fillMaxSize import androidx.fragment.app.Fragment
import androidx.compose.foundation.layout.fillMaxWidth import com.google.android.material.textfield.TextInputEditText
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.activity.ToastHelper import net.helcel.fidelity.databinding.FragCreateEntryBinding
import net.helcel.fidelity.activity.fragment.CreateEntryEventHandler.onCameraScan import net.helcel.fidelity.pluginSDK.Kp2aControl
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.FidelityEntry import net.helcel.fidelity.tools.CacheManager
import net.helcel.fidelity.tools.FidelityRepository import net.helcel.fidelity.tools.ErrorToaster
import net.helcel.fidelity.tools.FidelityRepository.activeEntry import net.helcel.fidelity.tools.KeepassWrapper
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() }
@Preview updatePreview()
@Composable return binding.root
fun CreateEntryScreen(navController: NavHostController?) { }
var entry by remember { activeEntry }
var errorTitle by remember { mutableStateOf("") }
var errorCode by remember { mutableStateOf("") }
var errorFormat by remember { mutableStateOf("") }
var barcodeBitmap by remember { mutableStateOf<Bitmap?>(null) } private fun updatePreview() {
var isValidBarcode by remember { mutableStateOf(false) }
var showDialog by remember { mutableStateOf(false) }
var isLoading by remember { mutableStateOf(false) }
val ctx = LocalContext.current
val scope = rememberCoroutineScope()
LaunchedEffect(entry) {
isValidBarcode = false
delay(500)
if (entry.code.isEmpty()) return@LaunchedEffect
try { try {
val bmp = generateBarcode(entry.code, entry.format, 600) val barcodeBitmap = generateBarcode(
barcodeBitmap = bmp binding.editTextCode.text.toString(),
binding.editTextFormat.text.toString(),
600
)
binding.imageViewPreview.setImageBitmap(barcodeBitmap)
isValidBarcode = true isValidBarcode = true
errorCode = "" } catch (e: FormatException) {
} catch (_: FormatException) { binding.imageViewPreview.setImageBitmap(null)
barcodeBitmap = null binding.editTextCode.error = "Invalid format"
errorCode = "Invalid Format"
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
barcodeBitmap = null binding.imageViewPreview.setImageBitmap(null)
errorCode = if (e.message == "com.google.zxing.FormatException") "Invalid Format" binding.editTextCode.error = e.message
else e.message ?: "Invalid Argument"
} catch (e: Exception) { } catch (e: Exception) {
barcodeBitmap = null binding.imageViewPreview.setImageBitmap(null)
ToastHelper.show(ctx, e.message ?: e.toString()) e.printStackTrace()
} }
} }
if (showDialog) { private fun isValidForm(): Boolean {
TreeSelectorDialog( var valid = true
onDismiss = { if (binding.editTextFormat.text.isNullOrEmpty()) {
showDialog = false valid = false
if(it!=null){ binding.editTextFormat.error = "Format cannot be empty"
entry = entry.copy(uid = it.nodeId?.id.toString()) }
if(it is Entry){ if (binding.editTextCode.text.isNullOrEmpty()) {
entry = entry.copy(title = it.title) 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
} }
val formats = stringArrayResource(R.array.format_array)
Box(
modifier = Modifier private fun startViewEntry(title: String?, code: String?, fmt: String?) {
.fillMaxSize() val viewEntryFragment = ViewEntry()
.background(MaterialTheme.colors.background) viewEntryFragment.arguments = KeepassWrapper.bundleCreate(title, code, fmt)
) {
Column( requireActivity().supportFragmentManager.beginTransaction()
modifier = Modifier .replace(R.id.container, viewEntryFragment).commit()
.padding(16.dp, 32.dp), }
verticalArrangement = Arrangement.spacedBy(12.dp)
)
{ private fun changeListener() {
OutlinedTextField( isValidBarcode = false
value = entry.title, handler.removeCallbacksAndMessages(null)
enabled = entry.uid!=null, handler.postDelayed({
onValueChange = { updatePreview()
entry = entry.copy(title = it) }, DEBOUNCE_DELAY)
errorTitle = "" }
},
label = { Text(text = "Title") },
isError = errorTitle.isNotEmpty(), private fun TextInputEditText.onDone(callback: () -> Unit) {
modifier = Modifier.fillMaxWidth(), setOnEditorActionListener { _, actionId, _ ->
singleLine = true, if (actionId == EditorInfo.IME_ACTION_DONE) {
colors = TextFieldDefaults.textFieldColors( callback.invoke()
textColor = if(entry.uid!=null)MaterialTheme.colors.onBackground return@setOnEditorActionListener true
else MaterialTheme.colors.secondary }
), false
}
}
private fun submit() {
if (!isValidForm() || !isValidBarcode) {
ErrorToaster.formIncomplete(context)
} else {
val kpEntry = KeepassWrapper.entryCreate(
this,
binding.editTextTitle.text.toString(),
binding.editTextCode.text.toString(),
binding.editTextFormat.text.toString(),
binding.checkboxProtected.isChecked,
) )
if (errorTitle.isNotEmpty()) { try {
Text(errorTitle, color = MaterialTheme.colors.error) resultLauncherAdd.launch(
} Kp2aControl.getAddEntryIntent(
kpEntry.first,
OutlinedTextField( kpEntry.second
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()
) )
Text("Protected", color = MaterialTheme.colors.onBackground) } catch (e: ActivityNotFoundException) {
ErrorToaster.noKP2AFound(context)
Spacer(modifier = Modifier.weight(1f)) } catch (e: Exception) {
Button(onClick = { onCameraScan(navController!!) }) { e.printStackTrace()
Icon(Icons.Default.Camera, contentDescription = null)
}
Spacer(modifier = Modifier.width(8.dp))
Button(onClick = { onFileScan(navController!!) }) {
Icon(Icons.Default.FileOpen, contentDescription = null)
}
} }
if (barcodeBitmap != null) { if (!binding.checkboxProtected.isChecked) {
Image( val r = KeepassWrapper.entryExtract(kpEntry.first)
bitmap = barcodeBitmap!!.asImageBitmap(), CacheManager.addFidelity(r)
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")
}
} }

View File

@@ -0,0 +1,107 @@
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()
}
}
}

View File

@@ -1,345 +1,136 @@
package net.helcel.fidelity.activity.fragment package net.helcel.fidelity.activity.fragment
import android.content.Context import android.content.ActivityNotFoundException
import androidx.compose.foundation.background import android.os.Bundle
import androidx.compose.foundation.clickable import android.view.LayoutInflater
import androidx.compose.foundation.combinedClickable import android.view.View
import androidx.compose.foundation.interaction.MutableInteractionSource import android.view.ViewGroup
import androidx.compose.foundation.layout.Arrangement import androidx.fragment.app.Fragment
import androidx.compose.foundation.layout.Box import androidx.recyclerview.widget.ItemTouchHelper
import androidx.compose.foundation.layout.Row import androidx.recyclerview.widget.LinearLayoutManager
import androidx.compose.foundation.layout.Spacer import androidx.recyclerview.widget.RecyclerView
import androidx.compose.foundation.layout.fillMaxSize import net.helcel.fidelity.R
import androidx.compose.foundation.layout.fillMaxWidth import net.helcel.fidelity.activity.adapter.FidelityListAdapter
import androidx.compose.foundation.layout.padding import net.helcel.fidelity.databinding.FragLauncherBinding
import androidx.compose.foundation.layout.size import net.helcel.fidelity.pluginSDK.Kp2aControl
import androidx.compose.foundation.layout.width import net.helcel.fidelity.tools.CacheManager
import androidx.compose.foundation.lazy.grid.GridCells import net.helcel.fidelity.tools.ErrorToaster
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import net.helcel.fidelity.tools.KeepassWrapper
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
@OptIn(ExperimentalMaterial3Api::class) class Launcher : Fragment() {
@Composable
fun LauncherScreen( private lateinit var binding: FragLauncherBinding
navController: NavHostController?, private lateinit var fidelityListAdapter: FidelityListAdapter
) {
if(navController==null) return private val resultLauncherQuery = KeepassWrapper.resultLauncher(this) {
var isRefreshingState by remember { mutableStateOf(false) } val r = KeepassWrapper.entryExtract(it)
var showHidden by remember { mutableStateOf(false) } if (!KeepassWrapper.isProtected(it)) {
val context = LocalContext.current CacheManager.addFidelity(r)
val scope = rememberCoroutineScope()
val sortedEntries = remember(entries) {
derivedStateOf {
entries.filter{showHidden || !it.hidden}.sortedWith(
compareByDescending<FidelityEntry> { it.pinned }
.thenBy { it.hidden }
.thenByDescending { it.lastUse }
)
} }
startViewEntry(r.first, r.second, r.third)
} }
override fun onCreateView(
Box(modifier = Modifier inflater: LayoutInflater, container: ViewGroup?,
.fillMaxSize() savedInstanceState: Bundle?
.background(MaterialTheme.colors.background)) { ): View {
binding = FragLauncherBinding.inflate(layoutInflater)
PullToRefreshBox( binding.btnQuery.setOnClickListener { startGetFromKeepass() }
onRefresh = { binding.btnAdd.setOnClickListener {
isRefreshingState = true if (binding.menuAdd.visibility == View.GONE)
scope.launch { showMenuAdd()
onRefresh(context, navController) else
isRefreshingState = false hideMenuAdd()
}
},
isRefreshing = isRefreshingState,
modifier = Modifier.fillMaxSize()
) {
LazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier
.fillMaxSize()
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
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) hideMenuAdd()
Box( binding.btnScan.setOnClickListener {
modifier = Modifier startScanner()
.fillMaxSize() hideMenuAdd()
.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( binding.btnOpen.setOnClickListener {
modifier = Modifier, startFileScanner()
expanded = expanded, hideMenuAdd()
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 { binding.btnManual.setOnClickListener {
fun onAdd(navController: NavHostController) { startCreateEntry()
navController.navigate("edit") 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
} }
fun onQuery() { private fun hideMenuAdd() {
//TODO binding.btnAdd.setImageResource(R.drawable.cross)
} binding.menuAdd.visibility = View.GONE
var CRED: CredentialResult.Success? = null
suspend fun onSave(context: Context, navController: NavHostController){ }
private fun showMenuAdd() {
binding.btnAdd.setImageResource(R.drawable.minus)
binding.menuAdd.visibility = View.VISIBLE
}
private fun startGetFromKeepass() {
try { try {
if (CRED == null) { this.resultLauncherQuery.launch(Kp2aControl.getQueryEntryForOwnPackageIntent())
val res = loadCredentials(context) } catch (e: ActivityNotFoundException) {
when (res) { ErrorToaster.noKP2AFound(requireActivity())
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) { private fun startFragment(fragment: Fragment) {
try { requireActivity().supportFragmentManager.beginTransaction()
if (CRED == null) { .addToBackStack("Launcher")
val res = loadCredentials(context) .replace(R.id.container, fragment).commit()
when (res) { }
CredentialResult.AuthFailed, CredentialResult.NoData -> null
is CredentialResult.Success -> CRED = res
} private fun startScanner() {
startFragment(Scanner())
}
private fun startFileScanner() {
startFragment(FileScanner())
}
private fun startCreateEntry() {
startFragment(CreateEntry())
}
private fun startViewEntry(title: String?, code: String?, fmt: String?) {
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(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean = false
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val pos = viewHolder.adapterPosition
CacheManager.rmFidelity(pos)
fidelityListAdapter.notifyItemRemoved(pos)
} }
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")
} }
} }

View File

@@ -1,224 +1,105 @@
@file:Suppress("PreviewAnnotationInFunctionWithParameters",
"PreviewAnnotationInFunctionWithParameters"
)
package net.helcel.fidelity.activity.fragment package net.helcel.fidelity.activity.fragment
import android.Manifest import android.Manifest
import android.graphics.BitmapFactory import android.content.ContentValues
import android.os.Bundle
import android.util.Log import android.util.Log
import android.widget.Toast import android.view.LayoutInflater
import androidx.activity.compose.BackHandler import android.view.View
import androidx.activity.compose.rememberLauncherForActivityResult import android.view.ViewGroup
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.camera.view.PreviewView import androidx.core.content.ContextCompat
import androidx.compose.foundation.Canvas import androidx.fragment.app.Fragment
import androidx.compose.foundation.layout.Box import net.helcel.fidelity.R
import androidx.compose.foundation.layout.fillMaxSize import net.helcel.fidelity.databinding.FragScannerBinding
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.FidelityRepository.activeEntry import net.helcel.fidelity.tools.ErrorToaster
import net.helcel.fidelity.tools.KeepassWrapper
@androidx.compose.ui.tooling.preview.Preview class Scanner : Fragment() {
@Composable
fun ScannerScreen(
navController: NavController
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val scope = rememberCoroutineScope()
val cameraProviderFuture = remember { private lateinit var binding: FragScannerBinding
ProcessCameraProvider.getInstance(context)
}
var camera: Camera? by remember { mutableStateOf(null) }
var torchOn by remember { mutableStateOf(false) }
val done = remember { mutableStateOf(false) } private var code: String = ""
val previewView = remember { PreviewView(context) } private var fmt: String = ""
val permissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission(), private val resultPermissionRequest =
onResult = { granted -> registerForActivityResult(ActivityResultContracts.RequestPermission()) {
if (granted) { if (it) {
val cameraProvider = cameraProviderFuture.get() bindCameraUseCases()
val previewUseCase = Preview.Builder().build().also {
it.surfaceProvider = previewView.surfaceProvider
}
val analysisUseCase = analysisUseCase { detectedCode, detectedFormat ->
if (detectedCode.isNullOrEmpty() || detectedFormat.isNullOrEmpty()) return@analysisUseCase
if(done.value) return@analysisUseCase
scope.launch(Dispatchers.Main) {
activeEntry.value =
activeEntry.value.copy(code = detectedCode, format = detectedFormat)
done.value = true
onResult(navController)
}
return@analysisUseCase
}
try {
cameraProvider.unbindAll()
camera = cameraProvider.bindToLifecycle(
lifecycleOwner,
CameraSelector.DEFAULT_BACK_CAMERA,
previewUseCase,
analysisUseCase
)
} catch (e: Exception) {
Log.e("ScannerScreen", "Camera bind failed: ${e.message}")
}
} else { } else {
Toast.makeText(context, "Camera permission denied", Toast.LENGTH_SHORT).show() parentFragmentManager.popBackStack()
scope.launch(Dispatchers.Main){ ErrorToaster.noPermission(context)
onResult(navController)
}
} }
} }
)
LaunchedEffect(Unit) { override fun onCreateView(
permissionLauncher.launch(Manifest.permission.CAMERA) inflater: LayoutInflater,
} container: ViewGroup?,
savedInstanceState: Bundle?
Box(modifier = Modifier.fillMaxSize()) { ): View {
AndroidView( binding = FragScannerBinding.inflate(layoutInflater)
factory = { previewView }, binding.btnScanDone.setOnClickListener {
modifier = Modifier.fillMaxSize() startCreateEntry()
)
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)
} }
binding.btnScanDone.isEnabled = false
if(!done.value) resultPermissionRequest.launch(Manifest.permission.CAMERA)
CircularProgressIndicator( return binding.root
modifier = Modifier
.align(Alignment.BottomCenter) // same spot as buttons
.padding(bottom =80.dp),
)
} }
}
@Composable private fun startCreateEntry() {
fun ScannerOverlay( val createEntryFragment = CreateEntry()
modifier: Modifier = Modifier createEntryFragment.arguments =
) { KeepassWrapper.bundleCreate(null, this.code, this.fmt)
Canvas(modifier = modifier.fillMaxSize()) { requireActivity().supportFragmentManager.beginTransaction()
val widthF = size.width .replace(R.id.container, createEntryFragment)
val heightF = size.height .commit()
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 private fun scannerResult(code: String?, format: String?) {
fun FileScanner(navController: NavHostController) { if (!code.isNullOrEmpty() && !format.isNullOrEmpty()) {
val context = LocalContext.current this.code = code
this.fmt = format
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 isDone = this.code.isNotEmpty() && this.fmt.isNotEmpty()
val inputStream = context.contentResolver.openInputStream(uri) activity?.runOnUiThread {
val bitmap = BitmapFactory.decodeStream(inputStream) binding.btnScanDone.isEnabled = isDone
BarcodeScanner.bitmapUseCase(bitmap) { code, format -> binding.ScanActive.isEnabled = !isDone
if (!code.isNullOrEmpty() && !format.isNullOrEmpty()) { }
activeEntry.value = activeEntry.value.copy(code=code, format=format) }
onResult(navController)
} else { private fun bindCameraUseCases() {
Toast.makeText(context, "No barcode found", Toast.LENGTH_SHORT).show() val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
onResult(navController)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
val previewUseCase = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(binding.cameraView.surfaceProvider)
} }
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
val analysisUseCase = analysisUseCase { code, format ->
scannerResult(code, format)
} }
} catch (e: Exception) { try {
e.printStackTrace() cameraProvider.bindToLifecycle(
Toast.makeText(context, "Failed to load image", Toast.LENGTH_SHORT).show() this,
onResult(navController) cameraSelector,
} previewUseCase,
analysisUseCase
} )
} catch (illegalStateException: IllegalStateException) {
LaunchedEffect(Unit) { Log.e(ContentValues.TAG, illegalStateException.message.orEmpty())
pickImageLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) } catch (illegalArgumentException: IllegalArgumentException) {
} Log.e(ContentValues.TAG, illegalArgumentException.message.orEmpty())
}
BackHandler { }, ContextCompat.getMainExecutor(requireContext()))
onResult(navController)
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
object ScannerEventHandler {
fun onResult(navController: NavController) {
navController.popBackStack()
} }
} }

View File

@@ -1,144 +0,0 @@
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")
}
}
}
}
)
}

View File

@@ -1,274 +0,0 @@
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
}
}
}

View File

@@ -1,125 +1,86 @@
package net.helcel.fidelity.activity.fragment package net.helcel.fidelity.activity.fragment
import android.app.Activity import android.annotation.SuppressLint
import android.graphics.Bitmap import android.content.pm.ActivityInfo
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 android.widget.Toast import androidx.fragment.app.Fragment
import androidx.activity.compose.BackHandler import com.google.zxing.FormatException
import androidx.compose.foundation.Image import net.helcel.fidelity.databinding.FragViewEntryBinding
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.FidelityEntry import net.helcel.fidelity.tools.ErrorToaster
import kotlin.let import net.helcel.fidelity.tools.KeepassWrapper
import kotlin.math.min
@SuppressLint("SourceLockedOrientationActivity")
class ViewEntry : Fragment() {
@Preview private lateinit var binding: FragViewEntryBinding
@Composable private var title: String? = null
fun PreviewEntryScreen(){ private var code: String? = null
ViewEntryScreen(null, FidelityEntry("Title","AAA","QR")) private var fmt: String? = null
}
@Composable override fun onCreateView(
fun ViewEntryScreen( inflater: LayoutInflater,
navController: NavHostController?, container: ViewGroup?,
entry: FidelityEntry savedInstanceState: Bundle?
) { ): View {
val context = LocalContext.current binding = FragViewEntryBinding.inflate(layoutInflater)
val activity = context as? Activity val res = KeepassWrapper.bundleExtract(arguments)
var isFull by remember { mutableStateOf(false) } title = res.first
var bitmap by remember { mutableStateOf<Bitmap?>(null) } code = res.second
fmt = res.third
SideEffect { updatePreview()
activity?.window?.attributes = activity.window?.attributes?.apply { updateLayout()
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 {
bitmap = generateBarcode(entry.code, entry.format, 1024) val barcodeBitmap = generateBarcode(
} catch (_: Exception) { code, fmt, 1024
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() {
BoxWithConstraints( if (isLandscape()) {
modifier = Modifier binding.title.visibility = View.GONE
.fillMaxSize().padding(8.dp), setScreenBrightness(BRIGHTNESS_OVERRIDE_FULL)
contentAlignment = Alignment.Center } else {
) { binding.title.visibility = View.VISIBLE
bitmap?.let { setScreenBrightness(BRIGHTNESS_OVERRIDE_NONE)
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
}
} }

View File

@@ -0,0 +1,45 @@
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
)
}
}

View File

@@ -0,0 +1,94 @@
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()
}
}
}

View File

@@ -0,0 +1,9 @@
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"
}

View File

@@ -0,0 +1,49 @@
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
}
}

View File

@@ -0,0 +1,51 @@
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,
)
)
}

View File

@@ -0,0 +1,31 @@
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"
}

View File

@@ -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")
} }
} }
} }

View File

@@ -6,8 +6,6 @@ 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 {
@@ -33,11 +31,13 @@ object BarcodeGenerator {
val bitMatrix: BitMatrix = writer.encode(content, format, width, height) val bitMatrix: BitMatrix = writer.encode(content, format, width, height)
val bitmap = createBitmap(width, height) val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
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[x, y] = getPixelColor(bitMatrix, x, y) bitmap.setPixel(
x, y, getPixelColor(bitMatrix, x, y)
)
} }
} }
return bitmap return bitmap

View File

@@ -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 (_: NotFoundException) { } catch (e: NotFoundException) {
cb(null, null) cb(null, null)
} catch (_: ReaderException) { } catch (e: ReaderException) {
cb(null, null) cb(null, null)
} }
} }

View File

@@ -1,142 +0,0 @@
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
}

View File

@@ -0,0 +1,50 @@
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
}
}

View File

@@ -0,0 +1,31 @@
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)
}
}

View File

@@ -1,185 +0,0 @@
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)
}
}

View File

@@ -0,0 +1,85 @@
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()
}
}

View File

@@ -0,0 +1,17 @@
<?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>

View File

@@ -0,0 +1,113 @@
<?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>

View File

@@ -0,0 +1,94 @@
<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>

View File

@@ -0,0 +1,37 @@
<?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>

View File

@@ -0,0 +1,39 @@
<?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>

View File

@@ -0,0 +1,8 @@
<?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" />

View File

@@ -0,0 +1,30 @@
<?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>

View File

@@ -0,0 +1,3 @@
<resources>
</resources>

View File

@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="key_theme">App theme</string> <string name="kp2aplugin_title" tools:keep="@string/kp2aplugin_title">Fidelity</string>
<string name="system">System</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="light">Light</string> <string name="kp2aplugin_author" tools:keep="@string/kp2aplugin_author">Soraefir</string>
<string name="dark">Dark</string>
<string name="key_stats">Statistics</string> <string name="app_name">Keepass Fidelity</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>

View File

@@ -0,0 +1,11 @@
<?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>

View File

@@ -1,16 +1,8 @@
// 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.8.0' apply false
id 'com.android.library' version '8.13.0' apply false id 'com.android.library' version '8.8.0' apply false
id 'org.jetbrains.kotlin.android' version '2.2.20' apply false id 'org.jetbrains.kotlin.android' version '2.1.10' apply false
id 'com.autonomousapps.dependency-analysis' version '3.2.0' apply true id 'com.autonomousapps.dependency-analysis' version '2.8.2' apply true
} }

1
external/KeePassDX vendored

Submodule external/KeePassDX deleted from 1b98bd740c

Binary file not shown.

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

9
gradlew vendored
View File

@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
# #
# Copyright © 2015 the original authors. # Copyright © 2015-2021 the original authors.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -114,6 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;; NONSTOP* ) nonstop=true ;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
@@ -171,6 +172,7 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java # For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" ) JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -203,14 +205,15 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command: # Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped. # and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line. # treated as '${Hostname}' itself on the command line.
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ -classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@" "$@"
# Stop when "xargs" is not available. # Stop when "xargs" is not available.

3
gradlew.bat vendored
View File

@@ -70,10 +70,11 @@ goto fail
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell

View File

@@ -14,11 +14,6 @@ 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'