diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..efae956
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,13 @@
+*.iml
+.idea/
+node_modules/
+temp/
+.gradle
+local.properties/
+.DS_Store
+build/
+app/build/
+captures/
+.externalNativeBuild
+.cxx
+local.properties
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..c553917
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,54 @@
+plugins {
+ id 'com.android.application'
+ id 'org.jetbrains.kotlin.android'
+ id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.23'
+}
+
+
+android {
+ namespace 'net.helcel.fidelity'
+ compileSdk 34
+
+ defaultConfig {
+ applicationId 'net.helcel.fidelity'
+ minSdk 28
+ targetSdk 34
+ versionCode 1
+ versionName "1.0"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+ buildFeatures {
+ viewBinding true
+ }
+}
+
+
+dependencies {
+ implementation 'androidx.appcompat:appcompat:1.6.1'
+ implementation 'androidx.core:core-ktx:1.12.0'
+ implementation 'androidx.preference:preference-ktx:1.2.1'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
+ implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
+ implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
+ implementation 'androidx.camera:camera-camera2:1.3.2'
+ implementation 'androidx.camera:camera-lifecycle:1.3.2'
+ implementation 'androidx.camera:camera-view:1.3.2'
+ implementation 'com.google.code.gson:gson:2.10.1'
+ implementation 'com.google.android.material:material:1.11.0'
+ implementation 'com.google.zxing:core:3.5.3'
+ implementation 'com.google.mlkit:barcode-scanning:17.2.0'
+
+}
\ No newline at end of file
diff --git a/app/lint.xml b/app/lint.xml
new file mode 100644
index 0000000..8423c0e
--- /dev/null
+++ b/app/lint.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..648a679
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/net/helcel/fidelity/activity/MainActivity.kt b/app/src/main/java/net/helcel/fidelity/activity/MainActivity.kt
new file mode 100644
index 0000000..e06eb18
--- /dev/null
+++ b/app/src/main/java/net/helcel/fidelity/activity/MainActivity.kt
@@ -0,0 +1,48 @@
+package net.helcel.fidelity.activity
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.os.Bundle
+import androidx.activity.addCallback
+import androidx.appcompat.app.AppCompatActivity
+import net.helcel.fidelity.R
+import net.helcel.fidelity.activity.fragment.Launcher
+import net.helcel.fidelity.databinding.ActMainBinding
+import net.helcel.fidelity.tools.CacheManager
+
+class MainActivity : AppCompatActivity() {
+
+ private lateinit var binding: ActMainBinding
+
+ private lateinit var sharedPreferences: SharedPreferences
+
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ sharedPreferences =
+ this.getSharedPreferences(CacheManager.PREF_NAME, Context.MODE_PRIVATE)
+ CacheManager.loadFidelity(sharedPreferences)
+
+
+ binding = ActMainBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ onBackPressedDispatcher.addCallback(this) {
+ if (supportFragmentManager.backStackEntryCount > 0) {
+ supportFragmentManager.popBackStackImmediate()
+ } else {
+ finish()
+ }
+ }
+ if (savedInstanceState == null)
+ loadLauncher()
+
+ }
+
+ private fun loadLauncher() {
+ supportFragmentManager.beginTransaction()
+ .add(R.id.container, Launcher())
+ .commit()
+ }
+}
+
diff --git a/app/src/main/java/net/helcel/fidelity/activity/adapter/FidelityListAdapter.kt b/app/src/main/java/net/helcel/fidelity/activity/adapter/FidelityListAdapter.kt
new file mode 100644
index 0000000..b13c884
--- /dev/null
+++ b/app/src/main/java/net/helcel/fidelity/activity/adapter/FidelityListAdapter.kt
@@ -0,0 +1,50 @@
+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>,
+ private val onItemClicked: (Triple) -> Unit
+) :
+ RecyclerView.Adapter() {
+
+ 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) {
+ val text = "${triple.first}"
+ binding.textView.text = text
+ binding.card.setOnClickListener { onItemClicked(triple) }
+ }
+
+
+ }
+
+}
diff --git a/app/src/main/java/net/helcel/fidelity/activity/fragment/CreateEntry.kt b/app/src/main/java/net/helcel/fidelity/activity/fragment/CreateEntry.kt
new file mode 100644
index 0000000..068156c
--- /dev/null
+++ b/app/src/main/java/net/helcel/fidelity/activity/fragment/CreateEntry.kt
@@ -0,0 +1,143 @@
+package net.helcel.fidelity.activity.fragment
+
+import android.content.ActivityNotFoundException
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ArrayAdapter
+import androidx.core.widget.addTextChangedListener
+import androidx.fragment.app.Fragment
+import com.google.zxing.FormatException
+import net.helcel.fidelity.R
+import net.helcel.fidelity.databinding.FragCreateEntryBinding
+import net.helcel.fidelity.pluginSDK.Kp2aControl
+import net.helcel.fidelity.tools.BarcodeGenerator.generateBarcode
+import net.helcel.fidelity.tools.CacheManager
+import net.helcel.fidelity.tools.ErrorToaster
+import net.helcel.fidelity.tools.KeepassWrapper
+
+private const val DEBOUNCE_DELAY = 500L
+
+class CreateEntry : Fragment() {
+
+ private val handler = Handler(Looper.getMainLooper())
+ private lateinit var binding: FragCreateEntryBinding
+
+ private val resultLauncherAdd = KeepassWrapper.resultLauncherAdd(this) {
+ val r = KeepassWrapper.entryExtract(it)
+ if (!KeepassWrapper.isProtected(it)) {
+ CacheManager.addFidelity(r)
+ }
+ startViewEntry(r.first, r.second, r.third)
+ }
+
+ private var isValid: 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)
+
+ val changeListener = {
+ isValid = false
+ handler.removeCallbacksAndMessages(null)
+ handler.postDelayed({
+ updatePreview()
+ }, DEBOUNCE_DELAY)
+ }
+
+ binding.editTextCode.addTextChangedListener { changeListener() }
+ binding.editTextFormat.addTextChangedListener { changeListener() }
+ binding.editTextFormat.addTextChangedListener { binding.editTextFormat.error = null }
+ binding.btnSave.setOnClickListener {
+ if (!isValid() || !isValid) {
+ ErrorToaster.formIncomplete(requireActivity())
+
+ } else {
+ val kpentry = KeepassWrapper.entryCreate(
+ this,
+ binding.editTextTitle.text.toString(),
+ binding.editTextCode.text.toString(),
+ binding.editTextFormat.text.toString(),
+ binding.checkboxProtected.isChecked,
+ )
+ try {
+ resultLauncherAdd.launch(
+ Kp2aControl.getAddEntryIntent(
+ kpentry.first,
+ kpentry.second
+ )
+ )
+ } catch (e: ActivityNotFoundException) {
+ ErrorToaster.noKP2AFound(requireActivity())
+ }
+ }
+ }
+
+ updatePreview()
+ return binding.root
+ }
+
+ private fun updatePreview() {
+ try {
+ val barcodeBitmap = generateBarcode(
+ binding.editTextCode.text.toString(),
+ binding.editTextFormat.text.toString(),
+ 600
+ )
+ binding.imageViewPreview.setImageBitmap(barcodeBitmap)
+ isValid = true
+ } catch (e: FormatException) {
+ binding.imageViewPreview.setImageBitmap(null)
+ binding.editTextCode.error = "Invalid format"
+ } catch (e: IllegalArgumentException) {
+ binding.imageViewPreview.setImageBitmap(null)
+ binding.editTextCode.error = e.message
+ } catch (e: Exception) {
+ binding.imageViewPreview.setImageBitmap(null)
+ println(e.javaClass)
+ println(e.message)
+ e.printStackTrace()
+ }
+ }
+
+ private fun isValid(): Boolean {
+ var valid = true
+ if (binding.editTextTitle.text!!.isEmpty()) {
+ valid = false
+ binding.editTextTitle.error = "Title cannot be empty"
+ }
+ if (binding.editTextCode.text!!.isEmpty()) {
+ valid = false
+ binding.editTextCode.error = "Code cannot be empty"
+ }
+ if (binding.editTextFormat.text!!.isEmpty()) {
+ valid = false
+ binding.editTextFormat.error = "Format cannot be empty"
+ }
+ return valid
+ }
+
+
+ private fun startViewEntry(title: String?, code: String?, fmt: String?) {
+ val viewEntryFragment = ViewEntry()
+ viewEntryFragment.arguments = KeepassWrapper.bundleCreate(title, code, fmt)
+
+ requireActivity().supportFragmentManager.beginTransaction()
+ .replace(R.id.container, viewEntryFragment).commit()
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/helcel/fidelity/activity/fragment/Launcher.kt b/app/src/main/java/net/helcel/fidelity/activity/fragment/Launcher.kt
new file mode 100644
index 0000000..e02be35
--- /dev/null
+++ b/app/src/main/java/net/helcel/fidelity/activity/fragment/Launcher.kt
@@ -0,0 +1,105 @@
+package net.helcel.fidelity.activity.fragment
+
+import android.content.ActivityNotFoundException
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.recyclerview.widget.LinearLayoutManager
+import net.helcel.fidelity.R
+import net.helcel.fidelity.activity.adapter.FidelityListAdapter
+import net.helcel.fidelity.databinding.FragLauncherBinding
+import net.helcel.fidelity.pluginSDK.Kp2aControl
+import net.helcel.fidelity.tools.CacheManager
+import net.helcel.fidelity.tools.ErrorToaster
+import net.helcel.fidelity.tools.KeepassWrapper
+
+
+class Launcher : Fragment() {
+
+ private lateinit var binding: FragLauncherBinding
+ private lateinit var fidelityListAdapter: FidelityListAdapter
+
+ private val resultLauncherQuery = KeepassWrapper.resultLauncherQuery(this) {
+ val r = KeepassWrapper.entryExtract(it)
+ if (!KeepassWrapper.isProtected(it)) {
+ CacheManager.addFidelity(r)
+ }
+ startViewEntry(r.first, r.second, r.third)
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = FragLauncherBinding.inflate(layoutInflater)
+ binding.btnQuery.setOnClickListener { startGetFromKeepass() }
+ binding.btnAdd.setOnClickListener {
+ if (binding.menuAdd.visibility == View.GONE)
+ showMenuAdd()
+ else
+ hideMenuAdd()
+ }
+
+ hideMenuAdd()
+ binding.btnScan.setOnClickListener {
+ startScanner()
+ hideMenuAdd()
+ }
+
+ binding.btnManual.setOnClickListener {
+ startCreateEntry()
+ hideMenuAdd()
+ }
+
+ binding.fidelityList.layoutManager =
+ LinearLayoutManager(requireContext())
+ fidelityListAdapter = FidelityListAdapter(CacheManager.getFidelity()) {
+ startViewEntry(it.first, it.second, it.third)
+ }
+ binding.fidelityList.adapter = fidelityListAdapter
+ return binding.root
+ }
+
+ private fun hideMenuAdd() {
+ binding.btnAdd.setImageResource(R.drawable.cross)
+ binding.menuAdd.visibility = View.GONE
+
+ }
+
+ private fun showMenuAdd() {
+ binding.btnAdd.setImageResource(R.drawable.minus)
+ binding.menuAdd.visibility = View.VISIBLE
+ }
+
+
+ private fun startGetFromKeepass() {
+ try {
+ this.resultLauncherQuery.launch(Kp2aControl.queryEntryIntentForOwnPackage)
+ } catch (e: ActivityNotFoundException) {
+ ErrorToaster.noKP2AFound(requireActivity())
+ }
+ }
+
+ private fun startFragment(fragment: Fragment) {
+ requireActivity().supportFragmentManager.beginTransaction()
+ .addToBackStack("Launcher")
+ .replace(R.id.container, fragment).commit()
+ }
+
+ private fun startScanner() {
+ startFragment(Scanner())
+ }
+
+ 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)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/helcel/fidelity/activity/fragment/Scanner.kt b/app/src/main/java/net/helcel/fidelity/activity/fragment/Scanner.kt
new file mode 100644
index 0000000..925ae3a
--- /dev/null
+++ b/app/src/main/java/net/helcel/fidelity/activity/fragment/Scanner.kt
@@ -0,0 +1,116 @@
+package net.helcel.fidelity.activity.fragment
+
+import android.Manifest
+import android.content.ContentValues
+import android.content.pm.PackageManager
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.Preview
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import androidx.fragment.app.Fragment
+import net.helcel.fidelity.R
+import net.helcel.fidelity.databinding.FragScannerBinding
+import net.helcel.fidelity.tools.BarcodeScanner.getAnalysisUseCase
+import net.helcel.fidelity.tools.KeepassWrapper
+
+private const val CAMERA_PERMISSION_REQUEST_CODE = 1
+
+class Scanner : Fragment() {
+
+ private lateinit var binding: FragScannerBinding
+
+ private var code: String = ""
+ private var fmt: String = ""
+ private var valid: Boolean = false
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = FragScannerBinding.inflate(layoutInflater)
+ binding.bottomText.setOnClickListener {
+ startCreateEntry()
+ }
+ when (hasCameraPermission()) {
+ true -> bindCameraUseCases()
+ else -> requestPermission()
+ }
+ return binding.root
+ }
+
+ 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 hasCameraPermission() =
+ ActivityCompat.checkSelfPermission(
+ requireContext(),
+ Manifest.permission.CAMERA
+ ) == PackageManager.PERMISSION_GRANTED
+
+ private fun requestPermission() {
+ ActivityCompat.requestPermissions(
+ requireActivity(),
+ arrayOf(Manifest.permission.CAMERA),
+ CAMERA_PERMISSION_REQUEST_CODE
+ )
+ ActivityCompat.OnRequestPermissionsResultCallback { c, p, i ->
+ require(c == CAMERA_PERMISSION_REQUEST_CODE)
+ require(p.contains(Manifest.permission.CAMERA))
+ val el = i[p.indexOf(Manifest.permission.CAMERA)]
+ if (el != PackageManager.PERMISSION_GRANTED) {
+ startCreateEntry()
+ }
+
+ }
+ }
+
+ private fun bindCameraUseCases() {
+ val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
+
+ 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 = getAnalysisUseCase { code, format ->
+ if (code != null && format != null) {
+ this.code = code
+ this.fmt = format
+ this.valid = true
+ } else {
+ this.valid = false
+ }
+ }
+ try {
+ cameraProvider.bindToLifecycle(
+ this,
+ cameraSelector,
+ previewUseCase,
+ analysisUseCase
+ )
+ } catch (illegalStateException: IllegalStateException) {
+ Log.e(ContentValues.TAG, illegalStateException.message.orEmpty())
+ } catch (illegalArgumentException: IllegalArgumentException) {
+ Log.e(ContentValues.TAG, illegalArgumentException.message.orEmpty())
+ }
+ }, ContextCompat.getMainExecutor(requireContext()))
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/helcel/fidelity/activity/fragment/ViewEntry.kt b/app/src/main/java/net/helcel/fidelity/activity/fragment/ViewEntry.kt
new file mode 100644
index 0000000..9659f4a
--- /dev/null
+++ b/app/src/main/java/net/helcel/fidelity/activity/fragment/ViewEntry.kt
@@ -0,0 +1,75 @@
+package net.helcel.fidelity.activity.fragment
+
+import android.content.res.Configuration
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import com.google.zxing.FormatException
+import net.helcel.fidelity.databinding.FragViewEntryBinding
+import net.helcel.fidelity.tools.BarcodeGenerator.generateBarcode
+import net.helcel.fidelity.tools.ErrorToaster
+import net.helcel.fidelity.tools.KeepassWrapper
+
+
+class ViewEntry : Fragment() {
+
+ private lateinit var binding: FragViewEntryBinding
+
+ private var title: String? = null
+ private var code: String? = null
+ private var fmt: String? = null
+
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = FragViewEntryBinding.inflate(layoutInflater)
+ val res = KeepassWrapper.bundleExtract(arguments)
+ title = res.first
+ code = res.second
+ fmt = res.third
+
+ adjustLayout()
+ updatePreview()
+ return binding.root
+ }
+
+ private fun updatePreview() {
+ binding.title.text = title
+ try {
+ val barcodeBitmap = generateBarcode(
+ code!!, fmt!!, 1024
+ )
+ 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)
+ println(e.javaClass)
+ println(e.message)
+ e.printStackTrace()
+ }
+ }
+
+
+ override fun onConfigurationChanged(newConfig: Configuration) {
+ super.onConfigurationChanged(newConfig)
+ adjustLayout()
+ }
+
+ private fun adjustLayout() {
+ if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
+ binding.title.visibility = View.GONE
+ } else {
+ binding.title.visibility = View.VISIBLE
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/helcel/fidelity/pluginSDK/AccessManager.kt b/app/src/main/java/net/helcel/fidelity/pluginSDK/AccessManager.kt
new file mode 100644
index 0000000..2ab93f0
--- /dev/null
+++ b/app/src/main/java/net/helcel/fidelity/pluginSDK/AccessManager.kt
@@ -0,0 +1,179 @@
+package net.helcel.fidelity.pluginSDK
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.content.pm.PackageManager
+import android.text.TextUtils
+import android.util.Log
+import org.json.JSONArray
+import org.json.JSONException
+
+object AccessManager {
+ private const val _tag = "Kp2aPluginSDK"
+ private const val PREF_KEY_SCOPE = "scope"
+ private const val PREF_KEY_TOKEN = "token"
+
+ private fun stringArrayToString(values: ArrayList): String? {
+ val a = JSONArray()
+ for (i in values.indices) {
+ a.put(values[i])
+ }
+ return if (values.isNotEmpty()) {
+ a.toString()
+ } else {
+ null
+ }
+ }
+
+ private fun stringToStringArray(s: String?): ArrayList {
+ val strings = ArrayList()
+ if (!TextUtils.isEmpty(s)) {
+ try {
+ val a = JSONArray(s)
+ for (i in 0 until a.length()) {
+ val url = a.optString(i)
+ strings.add(url)
+ }
+ } catch (e: JSONException) {
+ e.printStackTrace()
+ }
+ }
+ return strings
+ }
+
+ fun storeAccessToken(
+ ctx: Context,
+ hostPackage: String,
+ accessToken: String,
+ scopes: ArrayList
+ ) {
+ 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()
+ Log.d(
+ _tag,
+ "stored access token " + accessToken.substring(
+ 0,
+ 4
+ ) + "... for " + scopes.size + " scopes (" + scopesString + ")."
+ )
+
+ val hostPrefs = ctx.getSharedPreferences("KP2A.PluginAccess.hosts", Context.MODE_PRIVATE)
+ if (!hostPrefs.contains(hostPackage)) {
+ hostPrefs.edit().putString(hostPackage, "").apply()
+ }
+ }
+
+ fun preparePopup(popupMenu: Any) {
+ try {
+ val fields = popupMenu.javaClass.declaredFields
+ for (field in fields) {
+ if ("mPopup" == field.name) {
+ field.isAccessible = true
+ val menuPopupHelper = field[popupMenu]
+ val classPopupHelper = Class.forName(
+ menuPopupHelper
+ .javaClass.name
+ )
+ val setForceIcons = classPopupHelper.getMethod(
+ "setForceShowIcon", Boolean::class.javaPrimitiveType
+ )
+ setForceIcons.invoke(menuPopupHelper, true)
+ break
+ }
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ private fun getPrefsForHost(
+ ctx: Context,
+ hostPackage: String
+ ): SharedPreferences {
+ val prefs = ctx.getSharedPreferences("KP2A.PluginAccess.$hostPackage", Context.MODE_PRIVATE)
+ return prefs
+ }
+
+ fun tryGetAccessToken(ctx: Context, hostPackage: String, scopes: ArrayList): String? {
+ if (TextUtils.isEmpty(hostPackage)) {
+ Log.d(_tag, "hostPackage is empty!")
+ return null
+ }
+ Log.d(_tag, "trying to find prefs for $hostPackage")
+ val prefs = getPrefsForHost(ctx, hostPackage)
+ val scopesString = prefs.getString(PREF_KEY_SCOPE, "")
+ Log.d(_tag, "available scopes: $scopesString")
+ val currentScope = stringToStringArray(scopesString)
+ if (isSubset(scopes, currentScope)) {
+ return prefs.getString(PREF_KEY_TOKEN, null)
+ } else {
+ Log.d(_tag, "looks like scope changed. Access token invalid.")
+ return null
+ }
+ }
+
+ private fun isSubset(
+ requiredScopes: ArrayList,
+ availableScopes: ArrayList
+ ): Boolean {
+ for (r in requiredScopes) {
+ if (availableScopes.indexOf(r) < 0) {
+ Log.d(_tag, "Scope " + r + " not available. " + availableScopes.size)
+ return false
+ }
+ }
+ return true
+ }
+
+ fun removeAccessToken(
+ ctx: Context, hostPackage: String,
+ accessToken: String
+ ) {
+ val prefs = getPrefsForHost(ctx, hostPackage)
+
+ Log.d(_tag, "removing AccessToken.")
+ 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()
+ }
+ }
+
+ fun getAllHostPackages(ctx: Context): Set {
+ val prefs = ctx.getSharedPreferences("KP2A.PluginAccess.hosts", Context.MODE_PRIVATE)
+ val result: MutableSet = HashSet()
+ for (host in prefs.all.keys) {
+ try {
+ val info = ctx.packageManager.getPackageInfo(host, PackageManager.GET_META_DATA)
+ //if we get here, the package is still there
+ result.add(host)
+ } catch (e: PackageManager.NameNotFoundException) {
+ //host gone. ignore.
+ }
+ }
+ return result
+ }
+
+
+ /**
+ * Returns a valid access token or throws PluginAccessException
+ */
+ fun getAccessToken(
+ context: Context, hostPackage: String,
+ scopes: ArrayList
+ ): String {
+ val accessToken = tryGetAccessToken(context, hostPackage, scopes)
+ ?: throw PluginAccessException(hostPackage, scopes)
+ return accessToken
+ }
+}
diff --git a/app/src/main/java/net/helcel/fidelity/pluginSDK/KeepassDefs.kt b/app/src/main/java/net/helcel/fidelity/pluginSDK/KeepassDefs.kt
new file mode 100644
index 0000000..7a0ea22
--- /dev/null
+++ b/app/src/main/java/net/helcel/fidelity/pluginSDK/KeepassDefs.kt
@@ -0,0 +1,45 @@
+package net.helcel.fidelity.pluginSDK
+
+object KeepassDefs {
+ ///
+ /// Default identifier string for the title field. Should not contain
+ /// spaces, tabs or other whitespace.
+ ///
+ var TitleField: String = "Title"
+
+ ///
+ /// Default identifier string for the user name field. Should not contain
+ /// spaces, tabs or other whitespace.
+ ///
+ private var UserNameField: String = "UserName"
+
+ ///
+ /// Default identifier string for the password field. Should not contain
+ /// spaces, tabs or other whitespace.
+ ///
+ private var PasswordField: String = "Password"
+
+ ///
+ /// Default identifier string for the URL field. Should not contain
+ /// spaces, tabs or other whitespace.
+ ///
+ var UrlField: String = "URL"
+
+ ///
+ /// Default identifier string for the notes field. Should not contain
+ /// spaces, tabs or other whitespace.
+ ///
+ private var NotesField: String = "Notes"
+
+
+ fun IsStandardField(strFieldName: String?): Boolean {
+ if (strFieldName == null) return false
+ if (strFieldName == TitleField) return true
+ if (strFieldName == UserNameField) return true
+ if (strFieldName == PasswordField) return true
+ if (strFieldName == UrlField) return true
+ if (strFieldName == NotesField) return true
+
+ return false
+ }
+}
diff --git a/app/src/main/java/net/helcel/fidelity/pluginSDK/Kp2aControl.kt b/app/src/main/java/net/helcel/fidelity/pluginSDK/Kp2aControl.kt
new file mode 100644
index 0000000..54d2bfd
--- /dev/null
+++ b/app/src/main/java/net/helcel/fidelity/pluginSDK/Kp2aControl.kt
@@ -0,0 +1,107 @@
+package net.helcel.fidelity.pluginSDK
+
+import android.content.Intent
+import android.text.TextUtils
+import org.json.JSONException
+import org.json.JSONObject
+
+object Kp2aControl {
+ /**
+ * Creates and returns an intent to launch Keepass2Android for adding an entry with the given fields.
+ * @param fields Key/Value pairs of the field values. See KeepassDefs for standard keys.
+ * @param protectedFields List of keys of the protected fields.
+ * @return Intent to start Keepass2Android.
+ * @throws JSONException
+ */
+ fun getAddEntryIntent(
+ fields: HashMap?,
+ protectedFields: ArrayList?
+ ): Intent {
+ return getAddEntryIntent(JSONObject((fields as Map<*, *>?)!!).toString(), protectedFields)
+ }
+
+ private fun getAddEntryIntent(
+ outputData: String?,
+ protectedFields: ArrayList?
+ ): Intent {
+ 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", "false") //KP2A expects a StringExtra
+ startKp2aIntent.putExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA, outputData)
+ if (protectedFields != null) startKp2aIntent.putStringArrayListExtra(
+ Strings.EXTRA_PROTECTED_FIELDS_LIST,
+ protectedFields
+ )
+
+
+ return startKp2aIntent
+ }
+
+
+ /**
+ * Creates an intent to open a Password Entry matching searchText
+ * @param searchText queryString
+ * @param showUserNotifications if true, the notifications (copy to clipboard, keyboard) are displayed
+ * @param closeAfterOpen if true, the entry is opened and KP2A is immediately closed
+ * @return Intent to start KP2A with
+ */
+ fun getOpenEntryIntent(
+ searchText: String?,
+ showUserNotifications: Boolean,
+ closeAfterOpen: Boolean
+ ): Intent {
+ 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", "SearchUrlTask")
+ startKp2aIntent.putExtra("ShowUserNotifications", showUserNotifications.toString())
+ startKp2aIntent.putExtra("CloseAfterCreate", closeAfterOpen.toString())
+ startKp2aIntent.putExtra("UrlToSearch", searchText)
+ return startKp2aIntent
+ }
+
+ /**
+ * Creates an intent to query a password entry from KP2A. The credentials are returned as Activity result.
+ * @param searchText Text to search for. Should be a URL or "androidapp://com.my.package."
+ * @return an Intent to start KP2A with
+ */
+ fun getQueryEntryIntent(searchText: String?): Intent {
+ val i = Intent(Strings.ACTION_QUERY_CREDENTIALS)
+ if (!TextUtils.isEmpty(searchText)) i.putExtra(Strings.EXTRA_QUERY_STRING, searchText)
+ return i
+ }
+
+ val queryEntryIntentForOwnPackage: Intent
+ /**
+ * Creates an intent to query a password entry from KP2A, matching to the current app's package .
+ * The credentials are returned as Activity result.
+ * This requires SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE.
+ * @return an Intent to start KP2A with
+ */
+ get() = Intent(Strings.ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE)
+
+ /**
+ * Converts the entry fields returned in an intent from a query to a hashmap.
+ * @param intent data received in onActivityResult after getQueryEntryIntent(ForOwnPackage)
+ * @return HashMap with keys = field names (see KeepassDefs for standard keys) and values = values
+ */
+ fun getEntryFieldsFromIntent(intent: Intent): HashMap {
+ val res = HashMap()
+ try {
+ val json = JSONObject(intent.getStringExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA)!!)
+ val iter = json.keys()
+ while (iter.hasNext()) {
+ val key = iter.next()
+ val value = json[key].toString()
+ res[key] = value
+ }
+ } catch (e: JSONException) {
+ e.printStackTrace()
+ } catch (e: NullPointerException) {
+ e.printStackTrace()
+ }
+ return res
+ }
+}
diff --git a/app/src/main/java/net/helcel/fidelity/pluginSDK/PluginAccessBroadcastReceiver.kt b/app/src/main/java/net/helcel/fidelity/pluginSDK/PluginAccessBroadcastReceiver.kt
new file mode 100644
index 0000000..97a1d2b
--- /dev/null
+++ b/app/src/main/java/net/helcel/fidelity/pluginSDK/PluginAccessBroadcastReceiver.kt
@@ -0,0 +1,82 @@
+package net.helcel.fidelity.pluginSDK
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+
+/**
+ * Broadcast flow between Host and Plugin
+ * ======================================
+ *
+ * The host is responsible for deciding when to initiate the session. It
+ * should initiate the session as soon as plugins are required or when a plugin
+ * has been updated through the OS.
+ * It will then send a broadcast to request the currently required scope.
+ * The plugin then sends a broadcast to the app which scope is required. If an
+ * access token is already available, it's sent along with the requset.
+ *
+ * If a previous permission has been revoked (or the app settings cleared or the
+ * permissions have been extended or the token is invalid for any other reason)
+ * the host will answer with a Revoked-Permission broadcast (i.e. the plugin is
+ * unconnected.)
+ *
+ * Unconnected plugins must be permitted by the user (requiring user action).
+ * When the user grants access, the plugin will receive an access token for
+ * the host. This access token is valid for the requested scope. If the scope
+ * changes (e.g after an update of the plugin), the access token becomes invalid.
+ *
+ */
+abstract class PluginAccessBroadcastReceiver : BroadcastReceiver() {
+ override fun onReceive(ctx: Context, intent: Intent) {
+ val action = intent.action
+ Log.d(_tag, "received broadcast with action=$action")
+ if (action == null) 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)
+ Log.d(_tag, "requesting access for " + scopes.size + " tokens.")
+ ctx.sendBroadcast(rpi)
+ }
+
+ /**
+ *
+ * @return the list of required scopes for this plugin.
+ */
+ abstract val scopes: ArrayList
+
+ companion object {
+ private const val _tag = "Kp2aPluginSDK"
+ }
+}
diff --git a/app/src/main/java/net/helcel/fidelity/pluginSDK/PluginAccessException.kt b/app/src/main/java/net/helcel/fidelity/pluginSDK/PluginAccessException.kt
new file mode 100644
index 0000000..571b879
--- /dev/null
+++ b/app/src/main/java/net/helcel/fidelity/pluginSDK/PluginAccessException.kt
@@ -0,0 +1,14 @@
+package net.helcel.fidelity.pluginSDK
+
+class PluginAccessException : Exception {
+ constructor(what: String?) : super(what)
+
+ constructor(hostPackage: String?, scopes: ArrayList)
+
+ companion object {
+ /**
+ *
+ */
+ private const val serialVersionUID = 1L
+ }
+}
diff --git a/app/src/main/java/net/helcel/fidelity/pluginSDK/PluginAccessReceiver.kt b/app/src/main/java/net/helcel/fidelity/pluginSDK/PluginAccessReceiver.kt
new file mode 100644
index 0000000..63895d6
--- /dev/null
+++ b/app/src/main/java/net/helcel/fidelity/pluginSDK/PluginAccessReceiver.kt
@@ -0,0 +1,16 @@
+package net.helcel.fidelity.pluginSDK
+
+import kotlin.collections.ArrayList
+
+
+class PluginAccessReceiver : PluginAccessBroadcastReceiver() {
+
+ override val scopes: ArrayList = ArrayList()
+
+ init {
+ this.scopes.add(Strings.SCOPE_DATABASE_ACTIONS)
+ this.scopes.add(Strings.SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE)
+ }
+
+}
+
diff --git a/app/src/main/java/net/helcel/fidelity/pluginSDK/PluginActionBroadcastReceiver.kt b/app/src/main/java/net/helcel/fidelity/pluginSDK/PluginActionBroadcastReceiver.kt
new file mode 100644
index 0000000..9b2af63
--- /dev/null
+++ b/app/src/main/java/net/helcel/fidelity/pluginSDK/PluginActionBroadcastReceiver.kt
@@ -0,0 +1,224 @@
+package net.helcel.fidelity.pluginSDK
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+import org.json.JSONArray
+import org.json.JSONException
+import org.json.JSONObject
+
+class PluginActionBroadcastReceiver : BroadcastReceiver() {
+ open class PluginActionBase
+ (var context: Context, protected var _intent: Intent) {
+ val hostPackage: String?
+ get() = _intent.getStringExtra(Strings.EXTRA_SENDER)
+ }
+
+ open class PluginEntryActionBase(context: Context, intent: Intent) :
+ PluginActionBase(context, intent) {
+ protected val entryFieldsFromIntent: HashMap
+ get() {
+ val res = HashMap()
+ try {
+ val json = JSONObject(_intent.getStringExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA)!!)
+ val iter = json.keys()
+ while (iter.hasNext()) {
+ val key = iter.next()
+ val value = json[key].toString()
+ res[key] = value
+ }
+ } catch (e: JSONException) {
+ e.printStackTrace()
+ }
+ return res
+ }
+
+ protected val protectedFieldsListFromIntent: Array?
+ get() {
+ try {
+ val json =
+ JSONArray(_intent.getStringExtra(Strings.EXTRA_PROTECTED_FIELDS_LIST))
+ val res = arrayOfNulls(json.length())
+ for (i in 0 until json.length()) res[i] = json.getString(i)
+ return res
+ } catch (e: JSONException) {
+ e.printStackTrace()
+ return null
+ }
+ }
+
+
+ open val entryId: String?
+ get() = _intent.getStringExtra(Strings.EXTRA_ENTRY_ID)
+
+
+ @Throws(PluginAccessException::class)
+ fun setEntryField(fieldId: String?, fieldValue: String?, isProtected: Boolean) {
+ val i = Intent(Strings.ACTION_SET_ENTRY_FIELD)
+ val scope = ArrayList()
+ scope.add(Strings.SCOPE_CURRENT_ENTRY)
+ i.putExtra(
+ Strings.EXTRA_ACCESS_TOKEN, AccessManager.getAccessToken(
+ context, hostPackage!!, scope
+ )
+ )
+ i.setPackage(hostPackage)
+ i.putExtra(Strings.EXTRA_SENDER, context.packageName)
+ i.putExtra(Strings.EXTRA_FIELD_VALUE, fieldValue)
+ i.putExtra(Strings.EXTRA_ENTRY_ID, entryId)
+ i.putExtra(Strings.EXTRA_FIELD_ID, fieldId)
+ i.putExtra(Strings.EXTRA_FIELD_PROTECTED, isProtected)
+
+ context.sendBroadcast(i)
+ }
+ }
+
+ private inner class ActionSelectedAction(ctx: Context, intent: Intent) :
+ PluginEntryActionBase(ctx, intent) {
+ val actionData: Bundle?
+ /**
+ *
+ * @return the Bundle associated with the action. This bundle can be set in OpenEntry.add(Entry)FieldAction
+ */
+ get() = _intent.getBundleExtra(Strings.EXTRA_ACTION_DATA)
+
+ private val fieldId: String?
+ /**
+ *
+ * @return the field id which was selected. null if an entry action (in the options menu) was selected.
+ */
+ get() = _intent.getStringExtra(Strings.EXTRA_FIELD_ID)
+
+ val isEntryAction: Boolean
+ /**
+ *
+ * @return true if an entry action, i.e. an option from the options menu, was selected. False if an option
+ * in a popup menu for a certain field was selected.
+ */
+ get() = fieldId == null
+
+ val entryFields: HashMap
+ /**
+ *
+ * @return a hashmap containing the entry fields in key/value form
+ */
+ get() = entryFieldsFromIntent
+
+ val protectedFieldsList: Array?
+ /**
+ *
+ * @return an array with the keys of all protected fields in the entry
+ */
+ get() = protectedFieldsListFromIntent
+ }
+
+ private inner class CloseEntryViewAction(context: Context, intent: Intent) :
+ PluginEntryActionBase(context, intent) {
+ override val entryId: String?
+ get() = _intent.getStringExtra(Strings.EXTRA_ENTRY_ID)
+ }
+
+ private open inner class OpenEntryAction(context: Context, intent: Intent) :
+ PluginEntryActionBase(context, intent) {
+ val entryFields: HashMap
+ get() = entryFieldsFromIntent
+
+ val protectedFieldsList: Array?
+ /**
+ *
+ * @return an array with the keys of all protected fields in the entry
+ */
+ get() = protectedFieldsListFromIntent
+
+ @Throws(PluginAccessException::class)
+ fun addEntryAction(
+ actionDisplayText: String?,
+ actionIconResourceId: Int,
+ actionData: Bundle?
+ ) {
+ addEntryFieldAction(null, null, actionDisplayText, actionIconResourceId, actionData)
+ }
+
+ @Throws(PluginAccessException::class)
+ fun addEntryFieldAction(
+ actionId: String?,
+ fieldId: String?,
+ actionDisplayText: String?,
+ actionIconResourceId: Int,
+ actionData: Bundle?
+ ) {
+ val i = Intent(Strings.ACTION_ADD_ENTRY_ACTION)
+ val scope = ArrayList()
+ scope.add(Strings.SCOPE_CURRENT_ENTRY)
+ i.putExtra(
+ Strings.EXTRA_ACCESS_TOKEN, AccessManager.getAccessToken(
+ context, hostPackage!!, scope
+ )
+ )
+ i.setPackage(hostPackage)
+ i.putExtra(Strings.EXTRA_SENDER, context.packageName)
+ i.putExtra(Strings.EXTRA_ACTION_DATA, actionData)
+ i.putExtra(Strings.EXTRA_ACTION_DISPLAY_TEXT, actionDisplayText)
+ i.putExtra(Strings.EXTRA_ACTION_ICON_RES_ID, actionIconResourceId)
+ i.putExtra(Strings.EXTRA_ENTRY_ID, entryId)
+ i.putExtra(Strings.EXTRA_FIELD_ID, fieldId)
+ i.putExtra(Strings.EXTRA_ACTION_ID, actionId)
+
+ context.sendBroadcast(i)
+ }
+ }
+
+ private inner class DatabaseAction(context: Context, intent: Intent) :
+ PluginActionBase(context, intent) {
+ val fileDisplayName: String?
+ get() = _intent.getStringExtra(Strings.EXTRA_DATABASE_FILE_DISPLAYNAME)
+
+ val filePath: String?
+ get() = _intent.getStringExtra(Strings.EXTRA_DATABASE_FILEPATH)
+
+ val action: String?
+ get() = _intent.action
+ }
+
+ //EntryOutputModified is very similar to OpenEntry because it receives the same
+ //data (+ the field id which was modified)
+ private inner class EntryOutputModifiedAction(context: Context, intent: Intent) :
+ OpenEntryAction(context, intent) {
+ val modifiedFieldId: String?
+ get() = _intent.getStringExtra(Strings.EXTRA_FIELD_ID)
+ }
+
+ override fun onReceive(ctx: Context, intent: Intent) {
+ val action = intent.action
+ Log.d(
+ "KP2A.pluginsdk",
+ "received broadcast in PluginActionBroadcastReceiver with action=$action"
+ )
+ if (action == null) return
+ if (action == Strings.ACTION_OPEN_ENTRY) {
+ openEntry(OpenEntryAction(ctx, intent))
+ } else if (action == Strings.ACTION_CLOSE_ENTRY_VIEW) {
+ closeEntryView(CloseEntryViewAction(ctx, intent))
+ } else if (action == Strings.ACTION_ENTRY_ACTION_SELECTED) {
+ actionSelected(ActionSelectedAction(ctx, intent))
+ } else if (action == Strings.ACTION_ENTRY_OUTPUT_MODIFIED) {
+ entryOutputModified(EntryOutputModifiedAction(ctx, intent))
+ } else if (action == Strings.ACTION_LOCK_DATABASE || action == Strings.ACTION_UNLOCK_DATABASE || action == Strings.ACTION_OPEN_DATABASE || action == Strings.ACTION_CLOSE_DATABASE) {
+ dbAction(DatabaseAction(ctx, intent))
+ } else {
+ //TODO handle unexpected action
+ }
+ }
+
+ private fun closeEntryView(closeEntryView: CloseEntryViewAction?) {}
+
+ private fun actionSelected(actionSelected: ActionSelectedAction?) {}
+
+ private fun openEntry(oe: OpenEntryAction?) {}
+
+ private fun entryOutputModified(eom: EntryOutputModifiedAction?) {}
+
+ private fun dbAction(db: DatabaseAction?) {}
+}
diff --git a/app/src/main/java/net/helcel/fidelity/pluginSDK/Strings.kt b/app/src/main/java/net/helcel/fidelity/pluginSDK/Strings.kt
new file mode 100644
index 0000000..7b0a46e
--- /dev/null
+++ b/app/src/main/java/net/helcel/fidelity/pluginSDK/Strings.kt
@@ -0,0 +1,195 @@
+package net.helcel.fidelity.pluginSDK
+
+object Strings {
+ /**
+ * Plugin is notified about actions like open/close/update a database.
+ */
+ const val SCOPE_DATABASE_ACTIONS = "keepass2android.SCOPE_DATABASE_ACTIONS"
+
+ /**
+ * Plugin is notified when an entry is opened.
+ */
+ const val SCOPE_CURRENT_ENTRY = "keepass2android.SCOPE_CURRENT_ENTRY"
+
+ /**
+ * Plugin may query credentials for its own package
+ */
+ const val SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE =
+ "keepass2android.SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE"
+
+ /**
+ * Plugin may query credentials for a deliberate package
+ */
+ const val SCOPE_QUERY_CREDENTIALS = "keepass2android.SCOPE_QUERY_CREDENTIALS"
+
+ /**
+ * Extra key to transfer a (json serialized) list of scopes
+ */
+ const val EXTRA_SCOPES = "keepass2android.EXTRA_SCOPES"
+
+
+ const val EXTRA_PLUGIN_PACKAGE = "keepass2android.EXTRA_PLUGIN_PACKAGE"
+
+ /**
+ * Extra key for sending the package name of the sender of a broadcast.
+ * Should be set in every broadcast.
+ */
+ const val EXTRA_SENDER = "keepass2android.EXTRA_SENDER"
+
+ /**
+ * Extra key for sending a request token. The request token is passed from
+ * KP2A to the plugin. It's used in the authorization process.
+ */
+ const val EXTRA_REQUEST_TOKEN = "keepass2android.EXTRA_REQUEST_TOKEN"
+
+ /**
+ * Action to start KP2A with an AppTask
+ */
+ const val ACTION_START_WITH_TASK = "keepass2android.ACTION_START_WITH_TASK"
+
+ /**
+ * Action sent from KP2A to the plugin to indicate that the plugin should request
+ * access (sending it's scopes)
+ */
+ const val ACTION_TRIGGER_REQUEST_ACCESS = "keepass2android.ACTION_TRIGGER_REQUEST_ACCESS"
+
+ /**
+ * Action sent from the plugin to KP2A including the scopes.
+ */
+ const val ACTION_REQUEST_ACCESS = "keepass2android.ACTION_REQUEST_ACCESS"
+
+ /**
+ * Action sent from the KP2A to the plugin when the user grants access.
+ * Will contain an access token.
+ */
+ const val ACTION_RECEIVE_ACCESS = "keepass2android.ACTION_RECEIVE_ACCESS"
+
+ /**
+ * Action sent from KP2A to the plugin to indicate that access is not or no longer valid.
+ */
+ const val ACTION_REVOKE_ACCESS = "keepass2android.ACTION_REVOKE_ACCESS"
+
+
+ /**
+ * Action for startActivity(). Opens an activity in the Plugin Host to edit the plugin settings (i.e. enable it)
+ */
+ const val ACTION_EDIT_PLUGIN_SETTINGS = "keepass2android.ACTION_EDIT_PLUGIN_SETTINGS"
+
+ /**
+ * Action sent from KP2A to the plugin to indicate that an entry was opened.
+ * The Intent contains the full entry data.
+ */
+ const val ACTION_OPEN_ENTRY = "keepass2android.ACTION_OPEN_ENTRY"
+
+ /**
+ * Action sent from KP2A to the plugin to indicate that an entry output field was modified/added.
+ * The Intent contains the full new entry data.
+ */
+ const val ACTION_ENTRY_OUTPUT_MODIFIED = "keepass2android.ACTION_ENTRY_OUTPUT_MODIFIED"
+
+ /**
+ * Action sent from KP2A to the plugin to indicate that an entry activity was closed.
+ */
+ const val ACTION_CLOSE_ENTRY_VIEW = "keepass2android.ACTION_CLOSE_ENTRY_VIEW"
+
+ /**
+ * Extra key for a string containing the GUID of the entry.
+ */
+ const val EXTRA_ENTRY_ID = "keepass2android.EXTRA_ENTRY_DATA"
+
+ /**
+ * Json serialized data of the PwEntry (C# class) representing the opened entry.
+ * currently not implemented.
+ */
+ //const val EXTRA_ENTRY_DATA = "keepass2android.EXTRA_ENTRY_DATA";
+
+ /**
+ * Json serialized list of fields, transformed using the database context (i.e. placeholders are replaced already)
+ */
+ const val EXTRA_ENTRY_OUTPUT_DATA = "keepass2android.EXTRA_ENTRY_OUTPUT_DATA"
+
+ /**
+ * Json serialized lisf of field keys, specifying which field of the EXTRA_ENTRY_OUTPUT_DATA is protected.
+ */
+ const val EXTRA_PROTECTED_FIELDS_LIST = "keepass2android.EXTRA_PROTECTED_FIELDS_LIST"
+
+
+ /**
+ * Extra key for passing the access token (both ways)
+ */
+ const val EXTRA_ACCESS_TOKEN = "keepass2android.EXTRA_ACCESS_TOKEN"
+
+ /**
+ * Action for an intent from the plugin to KP2A to add menu options regarding the currently open entry.
+ * Requires SCOPE_CURRENT_ENTRY.
+ */
+ const val ACTION_ADD_ENTRY_ACTION = "keepass2android.ACTION_ADD_ENTRY_ACTION"
+
+ const val EXTRA_ACTION_DISPLAY_TEXT = "keepass2android.EXTRA_ACTION_DISPLAY_TEXT"
+ const val EXTRA_ACTION_ICON_RES_ID = "keepass2android.EXTRA_ACTION_ICON_RES_ID"
+
+ const val EXTRA_FIELD_ID = "keepass2android.EXTRA_FIELD_ID"
+
+ /**
+ * Used to pass an id for the action. Each actionId may occur only once per field, otherwise the previous
+ * action with same id is replaced by the new action.
+ */
+ const val EXTRA_ACTION_ID = "keepass2android.EXTRA_ACTION_ID"
+
+ /** Extra for ACTION_ADD_ENTRY_ACTION and ACTION_ENTRY_ACTION_SELECTED to pass data specifying the action parameters.*/
+ const val EXTRA_ACTION_DATA = "keepass2android.EXTRA_ACTION_DATA"
+
+ /**
+ * Action for an intent from KP2A to the plugin when an action added with ACTION_ADD_ENTRY_ACTION was selected by the user.
+ *
+ */
+ const val ACTION_ENTRY_ACTION_SELECTED = "keepass2android.ACTION_ENTRY_ACTION_SELECTED"
+
+ /**
+ * Extra key for the string which is used to query the credentials. This should be either a URL for
+ * a web login (google.com or a full URI) or something in the form "androidapp://com.my.package"
+ */
+ const val EXTRA_QUERY_STRING = "keepass2android.EXTRA_QUERY_STRING"
+
+ /**
+ * Action when plugin wants to query credentials for its own package
+ */
+ const val ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE =
+ "keepass2android.ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE"
+
+
+ /**
+ * Action when plugin wants to query credentials for a deliberate package
+ * The query string is passed as intent data
+ */
+ const val ACTION_QUERY_CREDENTIALS = "keepass2android.ACTION_QUERY_CREDENTIALS"
+
+ /**
+ * Action for an intent from the plugin to KP2A to set (i.e. add or update) a field in the entry.
+ * May be used to update existing or add new fields at any time while the entry is opened.
+ */
+ const val ACTION_SET_ENTRY_FIELD = "keepass2android.ACTION_SET_ENTRY_FIELD"
+
+ /** Actions for an intent from KP2A to the plugin to inform that a database was opened, closed, quicklocked or quickunlocked.*/
+ const val ACTION_OPEN_DATABASE = "keepass2android.ACTION_OPEN_DATABASE"
+ const val ACTION_CLOSE_DATABASE = "keepass2android.ACTION_CLOSE_DATABASE"
+ const val ACTION_LOCK_DATABASE = "keepass2android.ACTION_LOCK_DATABASE"
+ const val ACTION_UNLOCK_DATABASE = "keepass2android.ACTION_UNLOCK_DATABASE"
+
+ /** Extra for ACTION_OPEN_DATABASE and ACTION_CLOSE_DATABASE containing a filepath which is used
+ * by KP2A internally to identify the file. Use only where necessary, might contain credentials
+ * for accessing the file (on remote storage).*/
+ const val EXTRA_DATABASE_FILEPATH = "keepass2android.EXTRA_DATABASE_FILEPATH"
+
+ /** Extra for ACTION_OPEN_DATABASE and ACTION_CLOSE_DATABASE containing a filepath which can be
+ * displayed to the user.*/
+ const val EXTRA_DATABASE_FILE_DISPLAYNAME = "keepass2android.EXTRA_DATABASE_FILE_DISPLAYNAME"
+
+
+ const val EXTRA_FIELD_VALUE = "keepass2android.EXTRA_FIELD_VALUE"
+ const val EXTRA_FIELD_PROTECTED = "keepass2android.EXTRA_FIELD_PROTECTED"
+
+ const val PREFIX_STRING = "STRING_"
+ const val PREFIX_BINARY = "BINARY_"
+
+}
diff --git a/app/src/main/java/net/helcel/fidelity/tools/BacodeScanner.kt b/app/src/main/java/net/helcel/fidelity/tools/BacodeScanner.kt
new file mode 100644
index 0000000..28803b3
--- /dev/null
+++ b/app/src/main/java/net/helcel/fidelity/tools/BacodeScanner.kt
@@ -0,0 +1,77 @@
+package net.helcel.fidelity.tools
+
+import android.content.ContentValues
+import android.util.Log
+import androidx.annotation.OptIn
+import androidx.camera.core.ExperimentalGetImage
+import androidx.camera.core.ImageAnalysis
+import androidx.camera.core.ImageProxy
+import com.google.mlkit.vision.barcode.BarcodeScanner
+import com.google.mlkit.vision.barcode.BarcodeScannerOptions
+import com.google.mlkit.vision.barcode.BarcodeScanning
+import com.google.mlkit.vision.barcode.common.Barcode
+import com.google.mlkit.vision.common.InputImage
+import net.helcel.fidelity.tools.BarcodeFormatConverter.formatToString
+import java.util.concurrent.Executors
+
+
+@OptIn(ExperimentalGetImage::class)
+object BarcodeScanner {
+
+ private fun processImageProxy(
+ barcodeScanner: BarcodeScanner,
+ imageProxy: ImageProxy,
+ cb: (String?, String?) -> Unit
+ ) {
+
+ imageProxy.image?.let { image ->
+ val inputImage =
+ InputImage.fromMediaImage(
+ image,
+ imageProxy.imageInfo.rotationDegrees
+ )
+
+ barcodeScanner.process(inputImage)
+ .addOnSuccessListener { barcodeList ->
+ println(barcodeList.map { e -> e.displayValue })
+ println(barcodeList.map { e -> e.format })
+ val barcode =
+ barcodeList.getOrNull(0)
+ if (barcode != null)
+ cb(barcode.displayValue, formatToString(barcode.format))
+ }
+ .addOnFailureListener {
+ Log.e(ContentValues.TAG, it.message.orEmpty())
+ }.addOnCompleteListener {
+ imageProxy.image?.close()
+ imageProxy.close()
+ }
+ }
+ }
+
+ fun getAnalysisUseCase(cb: (String?, String?) -> Unit): ImageAnalysis {
+ val options = BarcodeScannerOptions.Builder().setBarcodeFormats(
+ Barcode.FORMAT_CODE_128,
+ Barcode.FORMAT_CODE_39,
+ Barcode.FORMAT_CODE_93,
+ Barcode.FORMAT_EAN_8,
+ Barcode.FORMAT_EAN_13,
+ Barcode.FORMAT_QR_CODE,
+ Barcode.FORMAT_UPC_A,
+ Barcode.FORMAT_UPC_E,
+ Barcode.FORMAT_PDF417
+ ).build()
+ val scanner = BarcodeScanning.getClient(options)
+ val analysisUseCase = ImageAnalysis.Builder()
+ .build()
+
+ analysisUseCase.setAnalyzer(
+ Executors.newSingleThreadExecutor()
+ ) { imageProxy ->
+ processImageProxy(scanner, imageProxy, cb)
+ }
+ return analysisUseCase
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/helcel/fidelity/tools/BarcodeFormatConverter.kt b/app/src/main/java/net/helcel/fidelity/tools/BarcodeFormatConverter.kt
new file mode 100644
index 0000000..03df33a
--- /dev/null
+++ b/app/src/main/java/net/helcel/fidelity/tools/BarcodeFormatConverter.kt
@@ -0,0 +1,38 @@
+package net.helcel.fidelity.tools
+
+import com.google.mlkit.vision.barcode.common.Barcode
+import com.google.zxing.BarcodeFormat
+
+object BarcodeFormatConverter {
+
+ fun stringToFormat(f: String): BarcodeFormat {
+ return when (f) {
+ "CODE_128" -> BarcodeFormat.CODE_128
+ "CODE_39" -> BarcodeFormat.CODE_39
+ "CODE_93" -> BarcodeFormat.CODE_93
+ "EAN_8" -> BarcodeFormat.EAN_8
+ "EAN_13" -> BarcodeFormat.EAN_13
+ "CODE_QR" -> BarcodeFormat.QR_CODE
+ "UPC_A" -> BarcodeFormat.UPC_A
+ "UPC_E" -> BarcodeFormat.UPC_E
+ "PDF_417" -> BarcodeFormat.PDF_417
+ else -> throw Exception("Unsupported Format: $f")
+ }
+ }
+
+
+ fun formatToString(f: Int): String {
+ return when (f) {
+ Barcode.FORMAT_CODE_128 -> "CODE_128"
+ Barcode.FORMAT_CODE_39 -> "CODE_39"
+ Barcode.FORMAT_CODE_93 -> "CODE_93"
+ Barcode.FORMAT_EAN_8 -> "EAN_8"
+ Barcode.FORMAT_EAN_13 -> "EAN_13"
+ Barcode.FORMAT_QR_CODE -> "CODE_QR"
+ Barcode.FORMAT_UPC_A -> "UPC_A"
+ Barcode.FORMAT_UPC_E -> "UPC_E"
+ Barcode.FORMAT_PDF417 -> "PDF_417"
+ else -> throw Exception("Unsupported Format: $f")
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/helcel/fidelity/tools/BarcodeGenerator.kt b/app/src/main/java/net/helcel/fidelity/tools/BarcodeGenerator.kt
new file mode 100644
index 0000000..d74aa80
--- /dev/null
+++ b/app/src/main/java/net/helcel/fidelity/tools/BarcodeGenerator.kt
@@ -0,0 +1,57 @@
+package net.helcel.fidelity.tools
+
+import android.graphics.Bitmap
+import com.google.zxing.BarcodeFormat
+import com.google.zxing.MultiFormatWriter
+import com.google.zxing.WriterException
+import com.google.zxing.common.BitMatrix
+import net.helcel.fidelity.tools.BarcodeFormatConverter.stringToFormat
+
+object BarcodeGenerator {
+
+ private fun getPixelColor(bitMatrix: BitMatrix, x: Int, y: Int): Int {
+ if (x >= bitMatrix.width || y >= bitMatrix.height)
+ return android.graphics.Color.WHITE
+
+ return if (bitMatrix[x, y])
+ android.graphics.Color.BLACK
+ else
+ android.graphics.Color.WHITE
+ }
+
+ fun generateBarcode(content: String, f: String, width: Int): Bitmap? {
+ if (content.isEmpty() || f.isEmpty()) {
+ return null
+ }
+ try {
+ val format = stringToFormat(f)
+ val writer = MultiFormatWriter()
+ val height = (formatToRatio(format) * width).toInt()
+ val bitMatrix: BitMatrix = writer.encode(content, format, width, height)
+ val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+
+ for (x in 0 until width) {
+ for (y in 0 until height) {
+ bitmap.setPixel(
+ x,
+ y,
+ getPixelColor(bitMatrix, x, y)
+ )
+
+ }
+ }
+ return bitmap
+ } catch (e: WriterException) {
+ e.printStackTrace()
+ }
+ return null
+ }
+
+ private fun formatToRatio(format: BarcodeFormat): Double {
+ return when (format) {
+ BarcodeFormat.QR_CODE -> 1.0
+ BarcodeFormat.PDF_417 -> 0.4
+ else -> 0.5
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/helcel/fidelity/tools/CacheManager.kt b/app/src/main/java/net/helcel/fidelity/tools/CacheManager.kt
new file mode 100644
index 0000000..ccbe2c6
--- /dev/null
+++ b/app/src/main/java/net/helcel/fidelity/tools/CacheManager.kt
@@ -0,0 +1,45 @@
+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> = ArrayList()
+ private var pref: SharedPreferences? = null
+
+ fun addFidelity(item: Triple) {
+ val exists = data.find { it.first == item.first }
+ if (exists != null)
+ data.remove(exists)
+
+ data.add(0, item)
+ 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>>() {}.type
+ data = gson.fromJson(json, type) ?: ArrayList()
+
+ }
+
+ fun getFidelity(): ArrayList> {
+ return data
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/helcel/fidelity/tools/ErrorToaster.kt b/app/src/main/java/net/helcel/fidelity/tools/ErrorToaster.kt
new file mode 100644
index 0000000..8f44ed1
--- /dev/null
+++ b/app/src/main/java/net/helcel/fidelity/tools/ErrorToaster.kt
@@ -0,0 +1,22 @@
+package net.helcel.fidelity.tools
+
+import android.app.Activity
+import android.widget.Toast
+
+object ErrorToaster {
+ private fun helper(activity: Activity, message: String, length: Int) {
+ Toast.makeText(activity, message, length).show()
+ }
+
+ fun noKP2AFound(activity: Activity) {
+ helper(activity, "KeePass2Android Not Installed", Toast.LENGTH_LONG)
+ }
+
+ fun formIncomplete(activity: Activity) {
+ helper(activity, "Form Incomplete", Toast.LENGTH_SHORT)
+ }
+
+ fun invalidFormat(activity: Activity) {
+ helper(activity, "Invalid Format", Toast.LENGTH_SHORT)
+ }
+}
diff --git a/app/src/main/java/net/helcel/fidelity/tools/KeepassWrapper.kt b/app/src/main/java/net/helcel/fidelity/tools/KeepassWrapper.kt
new file mode 100644
index 0000000..8d93062
--- /dev/null
+++ b/app/src/main/java/net/helcel/fidelity/tools/KeepassWrapper.kt
@@ -0,0 +1,103 @@
+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.KeepassDefs
+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, ArrayList> {
+
+ val fields = HashMap()
+ val protected = ArrayList()
+ fields[KeepassDefs.TitleField] = title
+ fields[KeepassDefs.UrlField] =
+ "androidapp://" + fragment.requireActivity().packageName
+ fields[CODE_FIELD] = code
+ fields[FORMAT_FIELD] = format
+ fields[PROTECT_CODE_FIELD] = protectCode.toString()
+
+ if (protectCode) {
+ protected.add(CODE_FIELD)
+ }
+ return Pair(fields, protected)
+ }
+
+
+ fun resultLauncherAdd(
+ fragment: Fragment,
+ callback: (HashMap) -> Unit
+ ): ActivityResultLauncher {
+ return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+ if (result.resultCode == Activity.RESULT_OK) {
+ val data: Intent? = result.data
+ val credentials = Kp2aControl.getEntryFieldsFromIntent(
+ data!!
+ )
+ println(credentials)
+ callback(credentials)
+ }
+ }
+ }
+
+ fun resultLauncherQuery(
+ fragment: Fragment,
+ callback: (HashMap) -> Unit
+ ): ActivityResultLauncher {
+ return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+ if (result.resultCode == Activity.RESULT_OK) {
+ val data: Intent? = result.data
+ val credentials = Kp2aControl.getEntryFieldsFromIntent(
+ data!!
+ )
+ println(credentials)
+ callback(credentials)
+ }
+ }
+ }
+
+ fun entryExtract(map: HashMap): Triple {
+ return Triple(
+ map[KeepassDefs.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 bundleExtract(data: Bundle?): Triple {
+ return Triple(
+ data?.getString("title"),
+ data?.getString("code"),
+ data?.getString("fmt")
+ )
+ }
+
+ fun isProtected(map: HashMap): Boolean {
+ return map[PROTECT_CODE_FIELD].toBoolean()
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/barcode.xml b/app/src/main/res/drawable/barcode.xml
new file mode 100644
index 0000000..2ba4120
--- /dev/null
+++ b/app/src/main/res/drawable/barcode.xml
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bookmark.xml b/app/src/main/res/drawable/bookmark.xml
new file mode 100644
index 0000000..4299d66
--- /dev/null
+++ b/app/src/main/res/drawable/bookmark.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/camera.xml b/app/src/main/res/drawable/camera.xml
new file mode 100644
index 0000000..463d6e2
--- /dev/null
+++ b/app/src/main/res/drawable/camera.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/card.xml b/app/src/main/res/drawable/card.xml
new file mode 100644
index 0000000..39b1967
--- /dev/null
+++ b/app/src/main/res/drawable/card.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/cross.xml b/app/src/main/res/drawable/cross.xml
new file mode 100644
index 0000000..1cce650
--- /dev/null
+++ b/app/src/main/res/drawable/cross.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/edit.xml b/app/src/main/res/drawable/edit.xml
new file mode 100644
index 0000000..2993565
--- /dev/null
+++ b/app/src/main/res/drawable/edit.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/heart.xml b/app/src/main/res/drawable/heart.xml
new file mode 100644
index 0000000..9a4cca7
--- /dev/null
+++ b/app/src/main/res/drawable/heart.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/key.xml b/app/src/main/res/drawable/key.xml
new file mode 100644
index 0000000..0b51384
--- /dev/null
+++ b/app/src/main/res/drawable/key.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/lock_checkbox.xml b/app/src/main/res/drawable/lock_checkbox.xml
new file mode 100644
index 0000000..57ef708
--- /dev/null
+++ b/app/src/main/res/drawable/lock_checkbox.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/locked.xml b/app/src/main/res/drawable/locked.xml
new file mode 100644
index 0000000..21f1db9
--- /dev/null
+++ b/app/src/main/res/drawable/locked.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/locked_fill.xml b/app/src/main/res/drawable/locked_fill.xml
new file mode 100644
index 0000000..7e9298e
--- /dev/null
+++ b/app/src/main/res/drawable/locked_fill.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/logo.xml b/app/src/main/res/drawable/logo.xml
new file mode 100644
index 0000000..b0eaf36
--- /dev/null
+++ b/app/src/main/res/drawable/logo.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/minus.xml b/app/src/main/res/drawable/minus.xml
new file mode 100644
index 0000000..30bf750
--- /dev/null
+++ b/app/src/main/res/drawable/minus.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/save.xml b/app/src/main/res/drawable/save.xml
new file mode 100644
index 0000000..9f33171
--- /dev/null
+++ b/app/src/main/res/drawable/save.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/search.xml b/app/src/main/res/drawable/search.xml
new file mode 100644
index 0000000..40e0395
--- /dev/null
+++ b/app/src/main/res/drawable/search.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/unlocked.xml b/app/src/main/res/drawable/unlocked.xml
new file mode 100644
index 0000000..3e7bf6f
--- /dev/null
+++ b/app/src/main/res/drawable/unlocked.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/act_main.xml b/app/src/main/res/layout/act_main.xml
new file mode 100644
index 0000000..7b75a3e
--- /dev/null
+++ b/app/src/main/res/layout/act_main.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/frag_create_entry.xml b/app/src/main/res/layout/frag_create_entry.xml
new file mode 100644
index 0000000..886d3b4
--- /dev/null
+++ b/app/src/main/res/layout/frag_create_entry.xml
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/frag_launcher.xml b/app/src/main/res/layout/frag_launcher.xml
new file mode 100644
index 0000000..3b6bbae
--- /dev/null
+++ b/app/src/main/res/layout/frag_launcher.xml
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/frag_scanner.xml b/app/src/main/res/layout/frag_scanner.xml
new file mode 100644
index 0000000..fba9927
--- /dev/null
+++ b/app/src/main/res/layout/frag_scanner.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/frag_view_entry.xml b/app/src/main/res/layout/frag_view_entry.xml
new file mode 100644
index 0000000..b12a9dd
--- /dev/null
+++ b/app/src/main/res/layout/frag_view_entry.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/list_item_dropdown.xml b/app/src/main/res/layout/list_item_dropdown.xml
new file mode 100644
index 0000000..606dcfa
--- /dev/null
+++ b/app/src/main/res/layout/list_item_dropdown.xml
@@ -0,0 +1,10 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/list_item_fidelity.xml b/app/src/main/res/layout/list_item_fidelity.xml
new file mode 100644
index 0000000..c744e4b
--- /dev/null
+++ b/app/src/main/res/layout/list_item_fidelity.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..94fd7e0
--- /dev/null
+++ b/app/src/main/res/values/dimens.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..35b495d
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,29 @@
+
+
+ Fidelity
+ Stores and Displays fidelity and other cards
+ [soraefir](soraefir)
+
+ Keepass Fidelity
+
+ barcode preview
+ Expand
+ Manual
+ Scan
+ Query
+ Title
+ Code
+ Format
+ Save
+
+ - CODE_128
+ - CODE_39
+ - CODE_93
+ - EAN_8
+ - EAN_13
+ - CODE_QR
+ - UPC_A
+ - UPC_E
+ - PDF_417
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..f11f745
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..952b68d
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..5cccdb8
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,7 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+plugins {
+ id 'com.android.application' version '8.3.0' apply false
+ id 'com.android.library' version '8.3.0' apply false
+ id 'org.jetbrains.kotlin.android' version '1.9.23' apply false
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..1a982b5
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,24 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+android.enableJetifier=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..8c0fb64
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..52cdcda
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Sat Mar 09 17:54:03 GMT 2024
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..91a7e26
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..8a0b282
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..9377a07
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,18 @@
+pluginManagement {
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ maven { url 'https://jitpack.io' }
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ maven { url 'https://jitpack.io' }
+ }
+}
+rootProject.name = "BeenDroid"
+include ':app'