1 Commits

Author SHA1 Message Date
6c9be64030 Update plugin com.android.application to v8.3.1 2024-03-19 01:02:15 +00:00
61 changed files with 1044 additions and 813 deletions

BIN
.github/images/apk.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@ -1,64 +0,0 @@
#file: noinspection SpellCheckingInspection
name: CI-Android APK
env:
main_project_module: app
playstore_name: KeepassFidelity
on:
push:
branches: [ main ]
tags:
- '**'
pull_request:
branches: [ main ]
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: set up secrets
run: |
echo "${{ secrets.RELEASE_KEYSTORE }}" > keystore.asc
echo "${{ secrets.RELEASE_KEY}}" > key.asc
gpg -d --passphrase "${{ secrets.RELEASE_KEYSTORE_PASSWORD }}" --batch keystore.asc > app/keystore.properties
gpg -d --passphrase "${{ secrets.RELEASE_KEYSTORE_PASSWORD }}" --batch key.asc > app/key.jks
- uses: gradle/wrapper-validation-action@v2
- name: create and checkout branch
if: github.event_name == 'pull_request'
env:
BRANCH: ${{ github.head_ref }}
run: git checkout -B "$BRANCH"
- name: set up JDK
uses: actions/setup-java@v4
with:
java-version: 17
distribution: "temurin"
cache: 'gradle'
- name: Build APK
run: ./gradlew assemble
# - name: Upload APK
# uses: actions/upload-artifact@v4
# with:
# name: app.apk
# path: app/build/outputs/apk/release/app-release.apk
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
files: |
app/build/outputs/apk/release/app-release.apk

4
.gitignore vendored
View File

@ -7,11 +7,7 @@ local.properties/
.DS_Store .DS_Store
build/ build/
app/build/ app/build/
app/debug/
app/release/
captures/ captures/
.externalNativeBuild .externalNativeBuild
.cxx .cxx
local.properties local.properties
keystore.properties
key.jks

24
LICENSE
View File

@ -1,24 +0,0 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>

View File

@ -1,69 +0,0 @@
<!--suppress ALL -->
<div align="center">
<h1>Keepass Fidelity</h1>
<p>A minimalist fidelity/loyalty card plugin</p>
<img src="https://forthebadge.com/images/badges/built-for-android.svg" alt="Built for Android">
<img src="https://forthebadge.com/images/badges/built-with-love.svg" alt="Built with love">
<br>
<a href="https://github.com/choelzl/keepass-fidelity/actions/workflows/build.yml">
<img src="https://github.com/choelzl/keepass-fidelity/actions/workflows/build.yml/badge.svg?branch=main" alt="Build Status">
</a>
</div>
## 🌄 Screenshots
<div align="center">
<table>
<tr>
<td style="width: 33%; height: 100px;"><img src=".github/images/launcher.jpg" alt="Launcher" style="width: 100%; height: 100%;"></td>
<td style="width: 33%; height: 100px;"><img src=".github/images/view.jpg" alt="View" style="width: 100%; height: 100%;"></td>
<td style="width: 33%; height: 100px;"><img src=".github/images/edit.jpg" alt="Edit" style="width: 100%; height: 100%;"></td>
</tr>
</table>
</div>
## ⭐ Features
- Search entries in [Keepass2Android](https://github.com/PhilippC/keepass2android/)
- Scan & Create entries
- Recently used history for fast access
- Protect entries from caching
- Minimalist design and features
- Supported Formats:CODE_39, CODE_93, CODE_128, EAN_8, EAN_13, UPC_A, UPC_E, CODE_QR, PDF_417, AZTEC, CODABAR, DATA_MATRIX, ITF
## 📳 Installation
<div style="display: flex; justify-content: center; align-items: center; flex-direction: row;">
<a href="https://github.com/choelzl/keepass-fidelity/releases/latest">
<img width="200" height="84" alt="APK Download" src=".github/images/apk.png">
</a>
</div>
## ⚙️ Permissions
- `CAMERA`: necessary for the scanning of barcodes
## 📝 Contribute
Keepass-Fidelity is a user-driven project. We welcome any contribution, big or small.
- **🖥️ Development:** Fix bugs, implement features, or research issues. Open a PR for review.
- **🍥 Design:** Improve interfaces, including accessibility and usability.
- **📂 Issue Reporting:** Report bugs and edge cases with relevant info.
- **🌍 Localization:** Translate if it doesn't support your language.
## ✏️ Acknowledgements
Thanks to all contributors, the developers of our dependencies, and our users.
## 📝 License
```
Copyright (C) 2024 Helcel
Licensed under the Unlicense
For more information, please refer to <https://unlicense.org>
```

View File

@ -5,79 +5,50 @@ plugins {
} }
def keystorePropertiesFile = rootProject.file("app/keystore.properties")
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
android { android {
namespace 'net.helcel.fidelity' namespace 'net.helcel.fidelity'
compileSdk 34 compileSdk 34
defaultConfig { defaultConfig {
applicationId 'net.helcel.fidelity' applicationId 'net.helcel.fidelity'
resValue "string", "app_name", "Keepass Fidelity"
minSdk 28 minSdk 28
targetSdk 34 targetSdk 34
} versionCode 1
versionName "1.0"
signingConfigs {
create("release") {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
}
} }
buildTypes { buildTypes {
debug {
debuggable true
}
release { release {
minifyEnabled true minifyEnabled false
shrinkResources false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig = signingConfigs.getByName("release")
} }
} }
compileOptions { compileOptions {
coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
encoding 'utf-8'
} }
kotlinOptions { kotlinOptions {
jvmTarget = JavaVersion.VERSION_17 jvmTarget = '1.8'
} }
buildFeatures { buildFeatures {
viewBinding true viewBinding true
} }
dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
// Disables dependency metadata when building Android App Bundles.
includeInBundle = false
}
} }
dependencies { dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.4' 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-lifecycle:1.3.2'
implementation 'androidx.camera:camera-view:1.3.2' implementation 'androidx.camera:camera-view:1.3.2'
runtimeOnly 'androidx.camera:camera-camera2:1.3.2'
implementation 'com.google.code.gson:gson:2.10.1' implementation 'com.google.code.gson:gson:2.10.1'
implementation 'com.google.android.material:material:1.11.0' implementation 'com.google.android.material:material:1.11.0'
implementation 'com.google.zxing:core:3.5.3' implementation 'com.google.zxing:core:3.5.3'
implementation 'com.google.mlkit:barcode-scanning:17.2.0'
} }

View File

@ -1,13 +0,0 @@
# Gson uses generic type information stored in a class file when working with
# fields. Proguard removes such information by default, keep it.
-keepattributes Signature
# This is also needed for R8 in compat mode since multiple
# optimizations will remove the generic signature such as class
# merging and argument removal. See:
# https://r8.googlesource.com/r8/+/refs/heads/main/compatibility-faq.md#troubleshooting-gson-gson
-keep class com.google.gson.reflect.TypeToken { *; }
-keep class * extends com.google.gson.reflect.TypeToken
# Optional. For using GSON @Expose annotation
-keepattributes AnnotationDefault,RuntimeVisibleAnnotations

View File

@ -1,13 +1,12 @@
<?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="1"
android:versionCode="5" android:versionName="1.0">
android:versionName="1.1c">
<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" />
<application <application
android:icon="@mipmap/ic_launcher_round" android:icon="@drawable/logo"
android:label="@string/app_name" android:label="@string/app_name"
android:supportsRtl="true"> android:supportsRtl="true">
<activity <activity
@ -21,14 +20,30 @@
</activity> </activity>
<receiver <receiver
android:name=".pluginSDK.PluginAccessBroadcastReceiver" android:name=".pluginSDK.PluginAccessReceiver"
android:exported="true" android:exported="true">
tools:ignore="ExportedReceiver">
<intent-filter> <intent-filter>
<action android:name="keepass2android.ACTION_TRIGGER_REQUEST_ACCESS" /> <action android:name="keepass2android.ACTION_TRIGGER_REQUEST_ACCESS" />
<action android:name="keepass2android.ACTION_RECEIVE_ACCESS" /> <action android:name="keepass2android.ACTION_RECEIVE_ACCESS" />
<action android:name="keepass2android.ACTION_REVOKE_ACCESS" /> <action android:name="keepass2android.ACTION_REVOKE_ACCESS" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver
android:name=".pluginSDK.PluginActionBroadcastReceiver"
android:exported="true">
<intent-filter>
<action android:name="keepass2android.ACTION_OPEN_ENTRY" />
<action android:name="keepass2android.ACTION_CLOSE_ENTRY_VIEW" />
<action android:name="keepass2android.ACTION_ENTRY_ACTION_SELECTED" />
<action android:name="keepass2android.ACTION_LOCK_DATABASE" />
<action android:name="keepass2android.ACTION_UNLOCK_DATABASE" />
<action android:name="keepass2android.ACTION_CLOSE_DATABASE" />
<action android:name="keepass2android.ACTION_OPEN_DATABASE" />
</intent-filter>
</receiver>
</application> </application>
</manifest> </manifest>

View File

@ -1,25 +1,19 @@
package net.helcel.fidelity.activity package net.helcel.fidelity.activity
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.pm.ActivityInfo
import android.os.Bundle import android.os.Bundle
import androidx.activity.addCallback import androidx.activity.addCallback
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import net.helcel.fidelity.R import net.helcel.fidelity.R
import net.helcel.fidelity.activity.fragment.Launcher import net.helcel.fidelity.activity.fragment.Launcher
import net.helcel.fidelity.activity.fragment.ViewEntry
import net.helcel.fidelity.databinding.ActMainBinding import net.helcel.fidelity.databinding.ActMainBinding
import net.helcel.fidelity.pluginSDK.Kp2aControl.getEntryFieldsFromIntent
import net.helcel.fidelity.tools.CacheManager import net.helcel.fidelity.tools.CacheManager
import net.helcel.fidelity.tools.KeepassWrapper.bundleCreate
import net.helcel.fidelity.tools.KeepassWrapper.entryExtract
@SuppressLint("SourceLockedOrientationActivity")
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private lateinit var binding: ActMainBinding private lateinit var binding: ActMainBinding
private lateinit var sharedPreferences: SharedPreferences private lateinit var sharedPreferences: SharedPreferences
@ -29,37 +23,25 @@ class MainActivity : AppCompatActivity() {
this.getSharedPreferences(CacheManager.PREF_NAME, Context.MODE_PRIVATE) this.getSharedPreferences(CacheManager.PREF_NAME, Context.MODE_PRIVATE)
CacheManager.loadFidelity(sharedPreferences) CacheManager.loadFidelity(sharedPreferences)
binding = ActMainBinding.inflate(layoutInflater) binding = ActMainBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
onBackPressedDispatcher.addCallback(this) { onBackPressedDispatcher.addCallback(this) {
if (supportFragmentManager.backStackEntryCount > 0) { if (supportFragmentManager.backStackEntryCount > 0) {
supportFragmentManager.popBackStackImmediate() supportFragmentManager.popBackStackImmediate()
loadLauncher()
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
} else { } else {
finish() finish()
} }
} }
if (savedInstanceState == null)
if (intent.extras != null)
loadViewEntry()
else if (savedInstanceState == null)
loadLauncher() loadLauncher()
} }
private fun loadLauncher() { private fun loadLauncher() {
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.replace(R.id.container, Launcher()) .add(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() .commit()
} }
} }

View File

@ -37,6 +37,7 @@ class FidelityListAdapter(
inner class FidelityViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { inner class FidelityViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(triple: Triple<String?, String?, String?>) { fun bind(triple: Triple<String?, String?, String?>) {
val text = "${triple.first}" val text = "${triple.first}"
binding.textView.text = text binding.textView.text = text

View File

@ -7,11 +7,9 @@ import android.os.Looper
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import androidx.core.widget.addTextChangedListener import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.google.android.material.textfield.TextInputEditText
import com.google.zxing.FormatException import com.google.zxing.FormatException
import net.helcel.fidelity.R import net.helcel.fidelity.R
import net.helcel.fidelity.databinding.FragCreateEntryBinding import net.helcel.fidelity.databinding.FragCreateEntryBinding
@ -28,7 +26,7 @@ class CreateEntry : Fragment() {
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
private lateinit var binding: FragCreateEntryBinding private lateinit var binding: FragCreateEntryBinding
private val resultLauncherAdd = KeepassWrapper.resultLauncher(this) { private val resultLauncherAdd = KeepassWrapper.resultLauncherAdd(this) {
val r = KeepassWrapper.entryExtract(it) val r = KeepassWrapper.entryExtract(it)
if (!KeepassWrapper.isProtected(it)) { if (!KeepassWrapper.isProtected(it)) {
CacheManager.addFidelity(r) CacheManager.addFidelity(r)
@ -36,7 +34,7 @@ class CreateEntry : Fragment() {
startViewEntry(r.first, r.second, r.third) startViewEntry(r.first, r.second, r.third)
} }
private var isValidBarcode: Boolean = false private var isValid: Boolean = false
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -53,14 +51,41 @@ class CreateEntry : Fragment() {
binding.editTextCode.setText(res.second) binding.editTextCode.setText(res.second)
binding.editTextFormat.setText(res.third, false) binding.editTextFormat.setText(res.third, false)
val changeListener = {
isValid = false
handler.removeCallbacksAndMessages(null)
handler.postDelayed({
updatePreview()
}, DEBOUNCE_DELAY)
}
binding.editTextCode.addTextChangedListener { changeListener() } binding.editTextCode.addTextChangedListener { changeListener() }
binding.editTextFormat.addTextChangedListener { changeListener() } binding.editTextFormat.addTextChangedListener { changeListener() }
binding.editTextFormat.addTextChangedListener { binding.editTextFormat.error = null } binding.editTextFormat.addTextChangedListener { binding.editTextFormat.error = null }
binding.btnSave.setOnClickListener { submit() } binding.btnSave.setOnClickListener {
if (!isValid() || !isValid) {
binding.editTextTitle.onDone { submit() } ErrorToaster.formIncomplete(requireActivity())
binding.editTextCode.onDone { submit() }
} 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() updatePreview()
return binding.root return binding.root
@ -74,7 +99,7 @@ class CreateEntry : Fragment() {
600 600
) )
binding.imageViewPreview.setImageBitmap(barcodeBitmap) binding.imageViewPreview.setImageBitmap(barcodeBitmap)
isValidBarcode = true isValid = true
} catch (e: FormatException) { } catch (e: FormatException) {
binding.imageViewPreview.setImageBitmap(null) binding.imageViewPreview.setImageBitmap(null)
binding.editTextCode.error = "Invalid format" binding.editTextCode.error = "Invalid format"
@ -83,23 +108,25 @@ class CreateEntry : Fragment() {
binding.editTextCode.error = e.message binding.editTextCode.error = e.message
} catch (e: Exception) { } catch (e: Exception) {
binding.imageViewPreview.setImageBitmap(null) binding.imageViewPreview.setImageBitmap(null)
println(e.javaClass)
println(e.message)
e.printStackTrace() e.printStackTrace()
} }
} }
private fun isValidForm(): Boolean { private fun isValid(): Boolean {
var valid = true var valid = true
if (binding.editTextFormat.text.isNullOrEmpty()) { if (binding.editTextTitle.text!!.isEmpty()) {
valid = false valid = false
binding.editTextFormat.error = "Format cannot be empty" binding.editTextTitle.error = "Title cannot be empty"
} }
if (binding.editTextCode.text.isNullOrEmpty()) { if (binding.editTextCode.text!!.isEmpty()) {
valid = false valid = false
binding.editTextCode.error = "Code cannot be empty" binding.editTextCode.error = "Code cannot be empty"
} }
if (binding.editTextTitle.text.isNullOrEmpty()) { if (binding.editTextFormat.text!!.isEmpty()) {
valid = false valid = false
binding.editTextTitle.error = "Title cannot be empty" binding.editTextFormat.error = "Format cannot be empty"
} }
return valid return valid
} }
@ -113,50 +140,4 @@ class CreateEntry : Fragment() {
.replace(R.id.container, viewEntryFragment).commit() .replace(R.id.container, viewEntryFragment).commit()
} }
private fun changeListener() {
isValidBarcode = false
handler.removeCallbacksAndMessages(null)
handler.postDelayed({
updatePreview()
}, DEBOUNCE_DELAY)
}
private fun TextInputEditText.onDone(callback: () -> Unit) {
setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
callback.invoke()
return@setOnEditorActionListener true
}
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,
)
try {
resultLauncherAdd.launch(
Kp2aControl.getAddEntryIntent(
kpEntry.first,
kpEntry.second
)
)
} catch (e: ActivityNotFoundException) {
ErrorToaster.noKP2AFound(context)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
} }

View File

@ -23,7 +23,7 @@ class Launcher : Fragment() {
private lateinit var binding: FragLauncherBinding private lateinit var binding: FragLauncherBinding
private lateinit var fidelityListAdapter: FidelityListAdapter private lateinit var fidelityListAdapter: FidelityListAdapter
private val resultLauncherQuery = KeepassWrapper.resultLauncher(this) { private val resultLauncherQuery = KeepassWrapper.resultLauncherQuery(this) {
val r = KeepassWrapper.entryExtract(it) val r = KeepassWrapper.entryExtract(it)
if (!KeepassWrapper.isProtected(it)) { if (!KeepassWrapper.isProtected(it)) {
CacheManager.addFidelity(r) CacheManager.addFidelity(r)
@ -80,7 +80,7 @@ class Launcher : Fragment() {
private fun startGetFromKeepass() { private fun startGetFromKeepass() {
try { try {
this.resultLauncherQuery.launch(Kp2aControl.getQueryEntryForOwnPackageIntent()) this.resultLauncherQuery.launch(Kp2aControl.queryEntryIntentForOwnPackage)
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
ErrorToaster.noKP2AFound(requireActivity()) ErrorToaster.noKP2AFound(requireActivity())
} }

View File

@ -27,6 +27,7 @@ class Scanner : Fragment() {
private var code: String = "" private var code: String = ""
private var fmt: String = "" private var fmt: String = ""
private var valid: Boolean = false
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -34,18 +35,16 @@ class Scanner : Fragment() {
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
binding = FragScannerBinding.inflate(layoutInflater) binding = FragScannerBinding.inflate(layoutInflater)
binding.btnScanDone.setOnClickListener { binding.bottomText.setOnClickListener {
startCreateEntry() startCreateEntry()
} }
when (hasCameraPermission()) { when (hasCameraPermission()) {
true -> bindCameraUseCases() true -> bindCameraUseCases()
else -> requestPermission() else -> requestPermission()
} }
binding.btnScanDone.isEnabled = false
return binding.root return binding.root
} }
private fun startCreateEntry() { private fun startCreateEntry() {
val createEntryFragment = CreateEntry() val createEntryFragment = CreateEntry()
createEntryFragment.arguments = createEntryFragment.arguments =
@ -90,14 +89,12 @@ class Scanner : Fragment() {
} }
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
val analysisUseCase = getAnalysisUseCase { code, format -> val analysisUseCase = getAnalysisUseCase { code, format ->
if (!code.isNullOrEmpty() && !format.isNullOrEmpty()) { if (code != null && format != null) {
this.code = code this.code = code
this.fmt = format this.fmt = format
} this.valid = true
val isDone = this.code.isNotEmpty() && this.fmt.isNotEmpty() } else {
requireActivity().runOnUiThread { this.valid = false
binding.btnScanDone.isEnabled = isDone
binding.ScanActive.isEnabled = !isDone
} }
} }
try { try {
@ -114,4 +111,6 @@ class Scanner : Fragment() {
} }
}, ContextCompat.getMainExecutor(requireContext())) }, ContextCompat.getMainExecutor(requireContext()))
} }
} }

View File

@ -1,14 +1,10 @@
package net.helcel.fidelity.activity.fragment package net.helcel.fidelity.activity.fragment
import android.annotation.SuppressLint
import android.content.pm.ActivityInfo
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL
import android.view.WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.google.zxing.FormatException import com.google.zxing.FormatException
import net.helcel.fidelity.databinding.FragViewEntryBinding import net.helcel.fidelity.databinding.FragViewEntryBinding
@ -16,14 +12,16 @@ import net.helcel.fidelity.tools.BarcodeGenerator.generateBarcode
import net.helcel.fidelity.tools.ErrorToaster import net.helcel.fidelity.tools.ErrorToaster
import net.helcel.fidelity.tools.KeepassWrapper import net.helcel.fidelity.tools.KeepassWrapper
@SuppressLint("SourceLockedOrientationActivity")
class ViewEntry : Fragment() { class ViewEntry : Fragment() {
private lateinit var binding: FragViewEntryBinding private lateinit var binding: FragViewEntryBinding
private var title: String? = null private var title: String? = null
private var code: String? = null private var code: String? = null
private var fmt: String? = null private var fmt: String? = null
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -35,15 +33,8 @@ class ViewEntry : Fragment() {
code = res.second code = res.second
fmt = res.third fmt = res.third
adjustLayout()
updatePreview() updatePreview()
updateLayout()
binding.imageViewPreview.setOnClickListener {
requireActivity().requestedOrientation =
if (isLandscape()) ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
else ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}
return binding.root return binding.root
} }
@ -51,7 +42,7 @@ class ViewEntry : Fragment() {
binding.title.text = title binding.title.text = title
try { try {
val barcodeBitmap = generateBarcode( val barcodeBitmap = generateBarcode(
code, fmt, 1024 code!!, fmt!!, 1024
) )
binding.imageViewPreview.setImageBitmap(barcodeBitmap) binding.imageViewPreview.setImageBitmap(barcodeBitmap)
} catch (e: FormatException) { } catch (e: FormatException) {
@ -62,25 +53,23 @@ class ViewEntry : Fragment() {
ErrorToaster.invalidFormat(requireActivity()) ErrorToaster.invalidFormat(requireActivity())
} catch (e: Exception) { } catch (e: Exception) {
binding.imageViewPreview.setImageBitmap(null) binding.imageViewPreview.setImageBitmap(null)
println(e.javaClass)
println(e.message)
e.printStackTrace() e.printStackTrace()
} }
} }
private fun updateLayout() {
if (isLandscape()) { override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
adjustLayout()
}
private fun adjustLayout() {
if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
binding.title.visibility = View.GONE binding.title.visibility = View.GONE
setScreenBrightness(BRIGHTNESS_OVERRIDE_FULL)
} else { } else {
binding.title.visibility = View.VISIBLE binding.title.visibility = View.VISIBLE
setScreenBrightness(BRIGHTNESS_OVERRIDE_NONE)
} }
} }
private fun isLandscape(): Boolean {
return (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE)
}
private fun setScreenBrightness(brightness: Float?) {
requireActivity().window?.attributes?.screenBrightness = brightness
}
} }

View File

@ -1,45 +0,0 @@
package net.helcel.fidelity.activity.view
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.util.AttributeSet
import android.view.View
class ScannerView : View {
private val overlayPaint = Paint().apply {
color = Color.parseColor("#80000000") // Semi-transparent black
style = Paint.Style.FILL
}
private val clearPaint = Paint().apply {
xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
}
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), overlayPaint)
val centerX = width / 2f
val centerY = height / 2f
val squareSize = 0.75f * width.coerceAtMost(height)
canvas.drawRect(
centerX - squareSize / 2, centerY - squareSize / 2,
centerX + squareSize / 2, centerY + squareSize / 2, clearPaint
)
}
}

View File

@ -2,84 +2,141 @@ package net.helcel.fidelity.pluginSDK
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.text.TextUtils
import android.util.Log
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONException import org.json.JSONException
object AccessManager { object AccessManager {
private const val _tag = "Kp2aPluginSDK"
private const val PREF_KEY_SCOPE = "scope" private const val PREF_KEY_SCOPE = "scope"
private const val PREF_KEY_TOKEN = "token" private const val PREF_KEY_TOKEN = "token"
private fun stringArrayToString(values: ArrayList<String?>): String? { private fun stringArrayToString(values: ArrayList<String?>): String? {
if (values.isEmpty()) return null
val a = JSONArray() val a = JSONArray()
values.forEach { a.put(it) } for (i in values.indices) {
return a.toString() a.put(values[i])
}
return if (values.isNotEmpty()) {
a.toString()
} else {
null
}
} }
private fun stringToStringArray(s: String?): ArrayList<String> { private fun stringToStringArray(s: String?): ArrayList<String> {
val strings = ArrayList<String>() val strings = ArrayList<String>()
if (s.isNullOrEmpty()) return strings if (!TextUtils.isEmpty(s)) {
try { try {
val a = JSONArray(s) val a = JSONArray(s)
for (i in 0 until a.length()) for (i in 0 until a.length()) {
strings.add(a.optString(i)) val url = a.optString(i)
strings.add(url)
}
} catch (e: JSONException) { } catch (e: JSONException) {
e.printStackTrace() e.printStackTrace()
} }
}
return strings return strings
} }
fun storeAccessToken( fun storeAccessToken(
ctx: Context, ctx: Context,
hostPackage: String?, hostPackage: String,
accessToken: String?, accessToken: String,
scopes: ArrayList<String?> scopes: ArrayList<String?>
) { ) {
val prefs = getPrefsForHost(ctx, hostPackage) val prefs = getPrefsForHost(ctx, hostPackage)
val edit = prefs.edit() val edit = prefs.edit()
edit.putString(PREF_KEY_TOKEN, accessToken) edit.putString(PREF_KEY_TOKEN, accessToken)
val scopesString = stringArrayToString(scopes) val scopesString = stringArrayToString(scopes)
edit.putString(PREF_KEY_SCOPE, scopesString) edit.putString(PREF_KEY_SCOPE, scopesString)
edit.apply() 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) val hostPrefs = ctx.getSharedPreferences("KP2A.PluginAccess.hosts", Context.MODE_PRIVATE)
if (!hostPrefs.contains(hostPackage)) if (!hostPrefs.contains(hostPackage)) {
hostPrefs.edit().putString(hostPackage, "").apply() 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( private fun getPrefsForHost(
ctx: Context, ctx: Context,
hostPackage: String? hostPackage: String
): SharedPreferences { ): SharedPreferences {
return ctx.getSharedPreferences("KP2A.PluginAccess.$hostPackage", Context.MODE_PRIVATE) val prefs = ctx.getSharedPreferences("KP2A.PluginAccess.$hostPackage", Context.MODE_PRIVATE)
return prefs
} }
fun tryGetAccessToken(ctx: Context, hostPackage: String?, scopes: ArrayList<String?>): String? { fun tryGetAccessToken(ctx: Context, hostPackage: String, scopes: ArrayList<String?>): String? {
if (hostPackage.isNullOrEmpty()) return null 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 prefs = getPrefsForHost(ctx, hostPackage)
val scopesString = prefs.getString(PREF_KEY_SCOPE, "") val scopesString = prefs.getString(PREF_KEY_SCOPE, "")
Log.d(_tag, "available scopes: $scopesString")
val currentScope = stringToStringArray(scopesString) val currentScope = stringToStringArray(scopesString)
if (!isSubset(scopes, currentScope)) if (isSubset(scopes, currentScope)) {
return null
return prefs.getString(PREF_KEY_TOKEN, null) return prefs.getString(PREF_KEY_TOKEN, null)
} else {
Log.d(_tag, "looks like scope changed. Access token invalid.")
return null
}
} }
private fun isSubset( private fun isSubset(
requiredScopes: ArrayList<String?>, requiredScopes: ArrayList<String?>,
availableScopes: ArrayList<String> availableScopes: ArrayList<String>
): Boolean { ): Boolean {
return availableScopes.containsAll(requiredScopes) for (r in requiredScopes) {
if (availableScopes.indexOf(r) < 0) {
Log.d(_tag, "Scope " + r + " not available. " + availableScopes.size)
return false
}
}
return true
} }
fun removeAccessToken( fun removeAccessToken(
ctx: Context, hostPackage: String?, ctx: Context, hostPackage: String,
accessToken: String? accessToken: String
) { ) {
val prefs = getPrefsForHost(ctx, hostPackage) val prefs = getPrefsForHost(ctx, hostPackage)
Log.d(_tag, "removing AccessToken.")
if (prefs.getString(PREF_KEY_TOKEN, "") == accessToken) { if (prefs.getString(PREF_KEY_TOKEN, "") == accessToken) {
val edit = prefs.edit() val edit = prefs.edit()
edit.clear() edit.clear()
@ -91,4 +148,32 @@ object AccessManager {
hostPrefs.edit().remove(hostPackage).apply() hostPrefs.edit().remove(hostPackage).apply()
} }
} }
fun getAllHostPackages(ctx: Context): Set<String> {
val prefs = ctx.getSharedPreferences("KP2A.PluginAccess.hosts", Context.MODE_PRIVATE)
val result: MutableSet<String> = 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?>
): String {
val accessToken = tryGetAccessToken(context, hostPackage, scopes)
?: throw PluginAccessException(hostPackage, scopes)
return accessToken
}
} }

View File

@ -1,9 +0,0 @@
package net.helcel.fidelity.pluginSDK
@Suppress("unused")
object KeepassDef {
var TitleField: String = "Title"
var UserNameField: String = "UserName"
var PasswordField: String = "Password"
var UrlField: String = "URL"
}

View File

@ -0,0 +1,45 @@
package net.helcel.fidelity.pluginSDK
object KeepassDefs {
/// <summary>
/// Default identifier string for the title field. Should not contain
/// spaces, tabs or other whitespace.
/// </summary>
var TitleField: String = "Title"
/// <summary>
/// Default identifier string for the user name field. Should not contain
/// spaces, tabs or other whitespace.
/// </summary>
private var UserNameField: String = "UserName"
/// <summary>
/// Default identifier string for the password field. Should not contain
/// spaces, tabs or other whitespace.
/// </summary>
private var PasswordField: String = "Password"
/// <summary>
/// Default identifier string for the URL field. Should not contain
/// spaces, tabs or other whitespace.
/// </summary>
var UrlField: String = "URL"
/// <summary>
/// Default identifier string for the notes field. Should not contain
/// spaces, tabs or other whitespace.
/// </summary>
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
}
}

View File

@ -1,41 +1,99 @@
package net.helcel.fidelity.pluginSDK package net.helcel.fidelity.pluginSDK
import android.content.Intent import android.content.Intent
import android.text.TextUtils
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
object Kp2aControl { 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( fun getAddEntryIntent(
fields: HashMap<String?, String?>, fields: HashMap<String?, String?>?,
protectedFields: ArrayList<String?>?
): Intent {
return getAddEntryIntent(JSONObject((fields as Map<*, *>?)!!).toString(), protectedFields)
}
private fun getAddEntryIntent(
outputData: String?,
protectedFields: ArrayList<String?>? protectedFields: ArrayList<String?>?
): Intent { ): Intent {
val outputData = JSONObject((fields as Map<*, *>)).toString()
val startKp2aIntent = Intent(Strings.ACTION_START_WITH_TASK) val startKp2aIntent = Intent(Strings.ACTION_START_WITH_TASK)
startKp2aIntent.addCategory(Intent.CATEGORY_DEFAULT) startKp2aIntent.addCategory(Intent.CATEGORY_DEFAULT)
startKp2aIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) startKp2aIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
startKp2aIntent.putExtra("KP2A_APPTASK", "CreateEntryThenCloseTask") startKp2aIntent.putExtra("KP2A_APPTASK", "CreateEntryThenCloseTask")
startKp2aIntent.putExtra("ShowUserNotifications", "false") startKp2aIntent.putExtra("ShowUserNotifications", "false") //KP2A expects a StringExtra
startKp2aIntent.putExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA, outputData) startKp2aIntent.putExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA, outputData)
if (protectedFields != null) if (protectedFields != null) startKp2aIntent.putStringArrayListExtra(
startKp2aIntent.putStringArrayListExtra(
Strings.EXTRA_PROTECTED_FIELDS_LIST, Strings.EXTRA_PROTECTED_FIELDS_LIST,
protectedFields protectedFields
) )
return startKp2aIntent return startKp2aIntent
} }
fun getQueryEntryForOwnPackageIntent(): Intent {
return Intent(Strings.ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE) /**
* 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
} }
fun getEntryFieldsFromIntent(intent: Intent?): HashMap<String, String> { /**
* 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<String, String> {
val res = HashMap<String, String>() val res = HashMap<String, String>()
try { try {
val json = JSONObject(intent?.getStringExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA) ?: "") val json = JSONObject(intent.getStringExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA)!!)
val itr = json.keys() val iter = json.keys()
while (itr.hasNext()) { while (iter.hasNext()) {
val key = itr.next() val key = iter.next()
val value = json[key].toString() val value = json[key].toString()
res[key] = value res[key] = value
} }

View File

@ -3,10 +3,35 @@ package net.helcel.fidelity.pluginSDK
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.util.Log
class PluginAccessBroadcastReceiver : BroadcastReceiver() { /**
* 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) { override fun onReceive(ctx: Context, intent: Intent) {
val action = intent.action ?: return val action = intent.action
Log.d(_tag, "received broadcast with action=$action")
if (action == null) return
when (action) { when (action) {
Strings.ACTION_TRIGGER_REQUEST_ACCESS -> requestAccess(ctx, intent) Strings.ACTION_TRIGGER_REQUEST_ACCESS -> requestAccess(ctx, intent)
Strings.ACTION_RECEIVE_ACCESS -> receiveAccess(ctx, intent) Strings.ACTION_RECEIVE_ACCESS -> receiveAccess(ctx, intent)
@ -15,17 +40,18 @@ class PluginAccessBroadcastReceiver : BroadcastReceiver() {
} }
} }
private fun revokeAccess(ctx: Context, intent: Intent) { private fun revokeAccess(ctx: Context, intent: Intent) {
val senderPackage = intent.getStringExtra(Strings.EXTRA_SENDER) val senderPackage = intent.getStringExtra(Strings.EXTRA_SENDER)
val accessToken = intent.getStringExtra(Strings.EXTRA_ACCESS_TOKEN) val accessToken = intent.getStringExtra(Strings.EXTRA_ACCESS_TOKEN)
AccessManager.removeAccessToken(ctx, senderPackage, accessToken) AccessManager.removeAccessToken(ctx, senderPackage!!, accessToken!!)
} }
private fun receiveAccess(ctx: Context, intent: Intent) { private fun receiveAccess(ctx: Context, intent: Intent) {
val senderPackage = intent.getStringExtra(Strings.EXTRA_SENDER) val senderPackage = intent.getStringExtra(Strings.EXTRA_SENDER)
val accessToken = intent.getStringExtra(Strings.EXTRA_ACCESS_TOKEN) val accessToken = intent.getStringExtra(Strings.EXTRA_ACCESS_TOKEN)
AccessManager.storeAccessToken(ctx, senderPackage, accessToken, scopes) AccessManager.storeAccessToken(ctx, senderPackage!!, accessToken!!, scopes)
} }
private fun requestAccess(ctx: Context, intent: Intent) { private fun requestAccess(ctx: Context, intent: Intent) {
@ -36,16 +62,21 @@ class PluginAccessBroadcastReceiver : BroadcastReceiver() {
rpi.putExtra(Strings.EXTRA_SENDER, ctx.packageName) rpi.putExtra(Strings.EXTRA_SENDER, ctx.packageName)
rpi.putExtra(Strings.EXTRA_REQUEST_TOKEN, requestToken) rpi.putExtra(Strings.EXTRA_REQUEST_TOKEN, requestToken)
val token: String? = AccessManager.tryGetAccessToken(ctx, senderPackage, scopes) val token: String? = AccessManager.tryGetAccessToken(ctx, senderPackage!!, scopes)
rpi.putExtra(Strings.EXTRA_ACCESS_TOKEN, token) rpi.putExtra(Strings.EXTRA_ACCESS_TOKEN, token)
rpi.putStringArrayListExtra(Strings.EXTRA_SCOPES, scopes) rpi.putStringArrayListExtra(Strings.EXTRA_SCOPES, scopes)
Log.d(_tag, "requesting access for " + scopes.size + " tokens.")
ctx.sendBroadcast(rpi) ctx.sendBroadcast(rpi)
} }
private val scopes: ArrayList<String?> = ArrayList( /**
listOf( *
Strings.SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE, * @return the list of required scopes for this plugin.
) */
) abstract val scopes: ArrayList<String?>
companion object {
private const val _tag = "Kp2aPluginSDK"
}
} }

View File

@ -0,0 +1,14 @@
package net.helcel.fidelity.pluginSDK
class PluginAccessException : Exception {
constructor(what: String?) : super(what)
constructor(hostPackage: String?, scopes: ArrayList<String?>)
companion object {
/**
*
*/
private const val serialVersionUID = 1L
}
}

View File

@ -0,0 +1,16 @@
package net.helcel.fidelity.pluginSDK
import kotlin.collections.ArrayList
class PluginAccessReceiver : PluginAccessBroadcastReceiver() {
override val scopes: ArrayList<String?> = ArrayList()
init {
this.scopes.add(Strings.SCOPE_DATABASE_ACTIONS)
this.scopes.add(Strings.SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE)
}
}

View File

@ -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<String, String>
get() {
val res = HashMap<String, String>()
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<String?>?
get() {
try {
val json =
JSONArray(_intent.getStringExtra(Strings.EXTRA_PROTECTED_FIELDS_LIST))
val res = arrayOfNulls<String>(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<String?>()
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<String, String>
/**
*
* @return a hashmap containing the entry fields in key/value form
*/
get() = entryFieldsFromIntent
val protectedFieldsList: Array<String?>?
/**
*
* @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<String, String>
get() = entryFieldsFromIntent
val protectedFieldsList: Array<String?>?
/**
*
* @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<String?>()
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?) {}
}

View File

@ -1,30 +1,195 @@
package net.helcel.fidelity.pluginSDK package net.helcel.fidelity.pluginSDK
@Suppress("unused")
object Strings { object Strings {
/**
* Plugin is notified about actions like open/close/update a database.
*/
const val SCOPE_DATABASE_ACTIONS = "keepass2android.SCOPE_DATABASE_ACTIONS" 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" 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 = const val SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE =
"keepass2android.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_SCOPES = "keepass2android.EXTRA_SCOPES"
const val EXTRA_PLUGIN_PACKAGE = "keepass2android.EXTRA_PLUGIN_PACKAGE" 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" 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" 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" 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" 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" 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" 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" 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" 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" 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" 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 = const val ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE =
"keepass2android.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_"
} }

View File

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

View File

@ -1,5 +1,6 @@
package net.helcel.fidelity.tools package net.helcel.fidelity.tools
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.zxing.BarcodeFormat import com.google.zxing.BarcodeFormat
object BarcodeFormatConverter { object BarcodeFormatConverter {
@ -15,39 +16,22 @@ object BarcodeFormatConverter {
"UPC_A" -> BarcodeFormat.UPC_A "UPC_A" -> BarcodeFormat.UPC_A
"UPC_E" -> BarcodeFormat.UPC_E "UPC_E" -> BarcodeFormat.UPC_E
"PDF_417" -> BarcodeFormat.PDF_417 "PDF_417" -> BarcodeFormat.PDF_417
"AZTEC" -> BarcodeFormat.AZTEC
"CODABAR" -> BarcodeFormat.CODABAR
"MAXICODE" -> BarcodeFormat.MAXICODE
"DATA_MATRIX" -> BarcodeFormat.DATA_MATRIX
"ITF" -> BarcodeFormat.ITF
"RSS_14" -> BarcodeFormat.RSS_14
"RSS_EXPANDED" -> BarcodeFormat.RSS_EXPANDED
"UPC_EAN" -> BarcodeFormat.UPC_EAN_EXTENSION
else -> throw Exception("Unsupported Format: $f") else -> throw Exception("Unsupported Format: $f")
} }
} }
fun formatToString(f: BarcodeFormat): String {
fun formatToString(f: Int): String {
return when (f) { return when (f) {
BarcodeFormat.CODE_39 -> "CODE_39" Barcode.FORMAT_CODE_128 -> "CODE_128"
BarcodeFormat.CODE_93 -> "CODE_93" Barcode.FORMAT_CODE_39 -> "CODE_39"
BarcodeFormat.CODE_128 -> "CODE_128" Barcode.FORMAT_CODE_93 -> "CODE_93"
BarcodeFormat.EAN_8 -> "EAN_8" Barcode.FORMAT_EAN_8 -> "EAN_8"
BarcodeFormat.EAN_13 -> "EAN_13" Barcode.FORMAT_EAN_13 -> "EAN_13"
BarcodeFormat.QR_CODE -> "CODE_QR" Barcode.FORMAT_QR_CODE -> "CODE_QR"
BarcodeFormat.UPC_A -> "UPC_A" Barcode.FORMAT_UPC_A -> "UPC_A"
BarcodeFormat.UPC_E -> "UPC_E" Barcode.FORMAT_UPC_E -> "UPC_E"
BarcodeFormat.PDF_417 -> "PDF_417" Barcode.FORMAT_PDF417 -> "PDF_417"
BarcodeFormat.AZTEC -> "AZTEC"
BarcodeFormat.CODABAR -> "CODABAR"
BarcodeFormat.MAXICODE -> "MAXICODE"
BarcodeFormat.DATA_MATRIX -> "DATA_MATRIX"
BarcodeFormat.ITF -> "ITF"
BarcodeFormat.RSS_14 -> "RSS_14"
BarcodeFormat.RSS_EXPANDED -> "RSS_EXPANDED"
BarcodeFormat.UPC_EAN_EXTENSION -> "UPC_EAN"
else -> throw Exception("Unsupported Format: $f") else -> throw Exception("Unsupported Format: $f")
} }
} }

View File

@ -19,25 +19,25 @@ object BarcodeGenerator {
android.graphics.Color.WHITE android.graphics.Color.WHITE
} }
fun generateBarcode(content: String?, f: String?, w: Int): Bitmap? { fun generateBarcode(content: String, f: String, width: Int): Bitmap? {
if (content.isNullOrEmpty() || f.isNullOrEmpty()) { if (content.isEmpty() || f.isEmpty()) {
return null return null
} }
try { try {
val format = stringToFormat(f) val format = stringToFormat(f)
val writer = MultiFormatWriter() val writer = MultiFormatWriter()
val height = (w * formatToRatio(format)).toInt() val height = (formatToRatio(format) * width).toInt()
val width = (w * 1.0f).toInt()
val bitMatrix: BitMatrix = writer.encode(content, format, width, height) val bitMatrix: BitMatrix = writer.encode(content, format, width, height)
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) val bitmap = 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.setPixel( bitmap.setPixel(
x, y, getPixelColor(bitMatrix, x, y) x,
y,
getPixelColor(bitMatrix, x, y)
) )
} }
} }
return bitmap return bitmap

View File

@ -1,57 +0,0 @@
package net.helcel.fidelity.tools
import android.graphics.Bitmap
import androidx.annotation.OptIn
import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import com.google.zxing.BinaryBitmap
import com.google.zxing.MultiFormatReader
import com.google.zxing.NotFoundException
import com.google.zxing.RGBLuminanceSource
import com.google.zxing.ReaderException
import com.google.zxing.common.HybridBinarizer
import net.helcel.fidelity.tools.BarcodeFormatConverter.formatToString
import java.util.concurrent.Executors
object BarcodeScanner {
@OptIn(ExperimentalGetImage::class)
private fun processImageProxy(
imageProxy: ImageProxy,
cb: (String?, String?) -> Unit
) {
val bitmap = imageProxy.toBitmap() // Convert ImageProxy to Bitmap
val binaryBitmap = createBinaryBitmap(bitmap)
val reader = MultiFormatReader()
try {
val result = reader.decode(binaryBitmap)
cb(result.text, formatToString(result.barcodeFormat))
} catch (e: NotFoundException) {
cb(null, null)
} catch (e: ReaderException) {
cb(null, null)
} finally {
imageProxy.close()
}
}
private fun createBinaryBitmap(bitmap: Bitmap): BinaryBitmap {
val pixels = IntArray(bitmap.width * bitmap.height)
bitmap.getPixels(pixels, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height)
val source =
RGBLuminanceSource(bitmap.width, bitmap.height, pixels)
return BinaryBitmap(HybridBinarizer(source))
}
fun getAnalysisUseCase(cb: (String?, String?) -> Unit): ImageAnalysis {
val analysisUseCase = ImageAnalysis.Builder().build()
analysisUseCase.setAnalyzer(
Executors.newSingleThreadExecutor()
) { imageProxy ->
processImageProxy(imageProxy, cb)
}
return analysisUseCase
}
}

View File

@ -1,23 +1,22 @@
package net.helcel.fidelity.tools package net.helcel.fidelity.tools
import android.content.Context import android.app.Activity
import android.widget.Toast import android.widget.Toast
object ErrorToaster { object ErrorToaster {
private fun helper(activity: Context?, message: String, length: Int) { private fun helper(activity: Activity, message: String, length: Int) {
if (activity != null)
Toast.makeText(activity, message, length).show() Toast.makeText(activity, message, length).show()
} }
fun noKP2AFound(activity: Context?) { fun noKP2AFound(activity: Activity) {
helper(activity, "KeePass2Android Not Installed", Toast.LENGTH_LONG) helper(activity, "KeePass2Android Not Installed", Toast.LENGTH_LONG)
} }
fun formIncomplete(activity: Context?) { fun formIncomplete(activity: Activity) {
helper(activity, "Form Incomplete", Toast.LENGTH_SHORT) helper(activity, "Form Incomplete", Toast.LENGTH_SHORT)
} }
fun invalidFormat(activity: Context?) { fun invalidFormat(activity: Activity) {
helper(activity, "Invalid Format", Toast.LENGTH_SHORT) helper(activity, "Invalid Format", Toast.LENGTH_SHORT)
} }
} }

View File

@ -6,7 +6,7 @@ import android.os.Bundle
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import net.helcel.fidelity.pluginSDK.KeepassDef import net.helcel.fidelity.pluginSDK.KeepassDefs
import net.helcel.fidelity.pluginSDK.Kp2aControl import net.helcel.fidelity.pluginSDK.Kp2aControl
object KeepassWrapper { object KeepassWrapper {
@ -25,8 +25,8 @@ object KeepassWrapper {
val fields = HashMap<String?, String?>() val fields = HashMap<String?, String?>()
val protected = ArrayList<String?>() val protected = ArrayList<String?>()
fields[KeepassDef.TitleField] = title fields[KeepassDefs.TitleField] = title
fields[KeepassDef.UrlField] = fields[KeepassDefs.UrlField] =
"androidapp://" + fragment.requireActivity().packageName "androidapp://" + fragment.requireActivity().packageName
fields[CODE_FIELD] = code fields[CODE_FIELD] = code
fields[FORMAT_FIELD] = format fields[FORMAT_FIELD] = format
@ -37,13 +37,33 @@ object KeepassWrapper {
} }
fun resultLauncher( fun resultLauncherAdd(
fragment: Fragment, fragment: Fragment,
callback: (HashMap<String, String>) -> Unit callback: (HashMap<String, String>) -> Unit
): ActivityResultLauncher<Intent> { ): ActivityResultLauncher<Intent> {
return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) { if (result.resultCode == Activity.RESULT_OK) {
val credentials = Kp2aControl.getEntryFieldsFromIntent(result.data) val data: Intent? = result.data
val credentials = Kp2aControl.getEntryFieldsFromIntent(
data!!
)
println(credentials)
callback(credentials)
}
}
}
fun resultLauncherQuery(
fragment: Fragment,
callback: (HashMap<String, String>) -> Unit
): ActivityResultLauncher<Intent> {
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) callback(credentials)
} }
} }
@ -51,7 +71,7 @@ object KeepassWrapper {
fun entryExtract(map: HashMap<String, String>): Triple<String?, String?, String?> { fun entryExtract(map: HashMap<String, String>): Triple<String?, String?, String?> {
return Triple( return Triple(
map[KeepassDef.TitleField], map[KeepassDefs.TitleField],
map[CODE_FIELD], map[CODE_FIELD],
map[FORMAT_FIELD] map[FORMAT_FIELD]
) )
@ -65,10 +85,6 @@ object KeepassWrapper {
return data 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?> { fun bundleExtract(data: Bundle?): Triple<String?, String?, String?> {
return Triple( return Triple(
data?.getString("title"), data?.getString("title"),

View File

@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="72dp"
android:height="72dp"
android:viewportWidth="52"
android:viewportHeight="52">
<group
android:translateX="-10"
android:translateY="-10">
<path
android:fillColor="#EA5A47"
android:pathData="M60.7,26.2c0,-7.2 -5.9,-13.1 -13.1,-13.1c-5,0 -9.3,2.8 -11.5,6.9c-2.2,-4.1 -6.6,-6.9 -11.5,-6.9c-7.2,0 -13.1,5.9 -13.1,13.1c0,3.1 1.1,6 2.9,8.2l0,0l21.8,27l21.8,-27l0,0C59.6,32.2 60.7,29.4 60.7,26.2z" />
<path
android:fillColor="#00000000"
android:pathData="M60.7,26.2c0,-7.2 -5.9,-13.1 -13.1,-13.1c-5,0 -9.3,2.8 -11.5,6.9c-2.2,-4.1 -6.6,-6.9 -11.5,-6.9c-7.2,0 -13.1,5.9 -13.1,13.1c0,3.1 1.1,6 2.9,8.2l0,0l21.8,27l21.8,-27l0,0C59.6,32.2 60.7,29.4 60.7,26.2z"
android:strokeWidth="2"
android:strokeColor="#000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</group>
</vector>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/logo_g"/>
</adaptive-icon>

View File

@ -0,0 +1,31 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="72dp"
android:height="72dp"
android:viewportWidth="52"
android:viewportHeight="52">
<group
android:translateX="-10"
android:translateY="-10">
<path
android:fillColor="#00000000"
android:pathData="M30.735,34.656l-16.432,16.026l0,7.24l7.565,0l0,-4.637l5.125,0l0,-5.857l5.098,0l2.404,-2.404l0,-4.358l2.015,0"
android:strokeWidth="2"
android:strokeColor="#000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M48.52,23.998m-3.952,0a3.952,3.952 0,1 1,7.904 0a3.952,3.952 0,1 1,-7.904 0"
android:strokeWidth="2"
android:strokeColor="#000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M34.226,31.178c-1.43,-4.238 -0.347,-9.221 3.18,-12.695c4.845,-4.772 12.465,-4.889 17.022,-0.263s4.322,12.244 -0.522,17.016c-3.917,3.858 -9.648,4.674 -14.108,2.4"
android:strokeWidth="2"
android:strokeColor="#000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</group>
</vector>

View File

@ -0,0 +1,31 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="72dp"
android:height="72dp"
android:viewportWidth="52"
android:viewportHeight="52">
<group
android:translateX="-10"
android:translateY="-10">
<path
android:pathData="M53,32.25l1.875,0l0,26.875l-38,0l0,-26.875l1.875,0z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M21.375,28.915c0,-8.379 6.415,-16.274 14.318,-16.523c7.97,-0.251 15.41,7.285 14.742,16.523"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M25.548,28.915c0,-6.335 4.576,-12.305 10.212,-12.493c5.684,-0.19 10.991,5.508 10.514,12.493"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
</group>
</vector>

View File

@ -1,13 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:width="256dp"
android:height="256dp"
android:viewportWidth="256"
android:viewportHeight="256"
android:gravity="center"
>
<layer-list>
<item <item
android:width="128dp" android:width="128dp"
android:height="128dp" android:height="128dp"
@ -26,5 +18,4 @@
android:gravity="center" android:gravity="center"
android:left="72dp" android:left="72dp"
android:bottom="20dp" /> android:bottom="20dp" />
</layer-list></item>
</layer-list> </layer-list>

View File

@ -1,167 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="128"
android:viewportHeight="128">
<group
android:translateX="28"
android:translateY="28">
<group>
<path
android:fillColor="#92D3F5"
android:pathData="M59.959,52.794H12.041c-0.552,0 -1,-0.448 -1,-1v-29.547c0,-0.552 0.448,-1 1,-1h47.918c0.552,0 1,0.448 1,1v29.547C60.959,52.347 60.511,52.794 59.959,52.794z"
android:strokeWidth="2"
android:strokeColor="#000000" />
</group>
<group
android:scaleX="0.5"
android:scaleY="0.5"
android:translateX="32"
android:translateY="16">
<path
android:fillColor="#EA5A47"
android:pathData="M46.5,56l-10,-11.151l-10,11.151l0,-45.042l20,0z"
android:strokeWidth="2"
android:strokeColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#D22F27"
android:pathData="M41.864,12.03l0,37.854l4.523,5.044l0,-42.898z"
android:strokeColor="#00000000" />
<path
android:fillColor="#00000000"
android:pathData="M46.5,56l-10,-11.151l-10,11.151l0,-45.042l20,0z"
android:strokeWidth="2"
android:strokeColor="#000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M46.5,56l-10,-11.151l-10,11.151l0,-45.042l20,0z"
android:strokeWidth="2"
android:strokeColor="#000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</group>
<group
android:scaleX="0.5"
android:scaleY="0.5"
android:translateX="10"
android:translateY="18">
<path
android:fillColor="#00000000"
android:pathData="M9,21V52"
android:strokeWidth="2"
android:strokeColor="#000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M12,21V52"
android:strokeWidth="2"
android:strokeColor="#000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M20,21V50"
android:strokeWidth="2"
android:strokeColor="#000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M28,21V50"
android:strokeWidth="2"
android:strokeColor="#000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#000"
android:pathData="M15,50V21H17V50H15Z"
android:strokeWidth="2"
android:strokeColor="#000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#000"
android:pathData="M23,50V21H25V50H23Z"
android:strokeWidth="2"
android:strokeColor="#000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#000"
android:pathData="M31,50V21H32V50H31Z"
android:strokeWidth="2"
android:strokeColor="#000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M46,21V50"
android:strokeWidth="2"
android:strokeColor="#000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M49,21V50"
android:strokeWidth="2"
android:strokeColor="#000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M57,21V50"
android:strokeWidth="2"
android:strokeColor="#000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#000"
android:pathData="M41,50V21H43V50H41Z"
android:strokeWidth="2"
android:strokeColor="#000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#000"
android:pathData="M52,50V21H54V50H52Z"
android:strokeWidth="2"
android:strokeColor="#000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M60,21V52"
android:strokeWidth="2"
android:strokeColor="#000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M63,21V52"
android:strokeWidth="2"
android:strokeColor="#000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M35,21V52"
android:strokeWidth="2"
android:strokeColor="#000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M38,21V52"
android:strokeWidth="2"
android:strokeColor="#000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</group>
</group>
</vector>

View File

@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/coordinator"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:fitsSystemWindows="true" android:fitsSystemWindows="true"

View File

@ -14,6 +14,7 @@
android:padding="16dp"> android:padding="16dp">
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/nameInputLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
@ -22,11 +23,7 @@
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/editTextTitle" android:id="@+id/editTextTitle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content" />
android:imeOptions="actionNext"
android:inputType="text"
android:maxLines="1"
android:minLines="1" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
@ -49,11 +46,7 @@
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/editTextCode" android:id="@+id/editTextCode"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" 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.textfield.TextInputLayout>
@ -71,6 +64,7 @@
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/formatInputLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu" style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -83,7 +77,6 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_weight="1" android:layout_weight="1"
android:focusable="false"
android:inputType="none" /> android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>

View File

@ -9,8 +9,7 @@
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/fidelityList" android:id="@+id/fidelityList"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent" />
android:layout_margin="24dp" />
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
@ -26,6 +25,7 @@
app:srcCompat="@drawable/search" /> app:srcCompat="@drawable/search" />
<LinearLayout <LinearLayout
android:id="@+id/expandedMenuContainer"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"

View File

@ -11,27 +11,12 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />
<net.helcel.fidelity.activity.view.ScannerView <com.google.android.material.textview.MaterialTextView
android:id="@+id/bottomText"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="64dp"
<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_alignParentBottom="true"
android:layout_centerHorizontal="true" android:background="#ffffff"
android:layout_margin="24dp" android:textSize="24sp" />
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> </RelativeLayout>

View File

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android" <TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/textViewFeelings"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="15dp" android:padding="15dp"

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#393939</color>
</resources>

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="kp2aplugin_title" tools:keep="@string/kp2aplugin_title">Fidelity</string> <string name="kp2aplugin_title" tools:keep="@string/kp2aplugin_title">Fidelity</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="kp2aplugin_shortdesc">Stores and Displays fidelity and other cards</string>
<string name="kp2aplugin_author" tools:keep="@string/kp2aplugin_author">Soraefir</string> <string name="kp2aplugin_author" tools:keep="@string/kp2aplugin_author">[soraefir](soraefir)</string>
<string name="app_name">Keepass Fidelity</string> <string name="app_name">Keepass Fidelity</string>
@ -16,18 +16,14 @@
<string name="format">Format</string> <string name="format">Format</string>
<string name="save">Save</string> <string name="save">Save</string>
<string-array name="format_array"> <string-array name="format_array">
<item>CODE_128</item>
<item>CODE_39</item> <item>CODE_39</item>
<item>CODE_93</item> <item>CODE_93</item>
<item>CODE_128</item>
<item>EAN_8</item> <item>EAN_8</item>
<item>EAN_13</item> <item>EAN_13</item>
<item>CODE_QR</item> <item>CODE_QR</item>
<item>UPC_A</item> <item>UPC_A</item>
<item>UPC_E</item> <item>UPC_E</item>
<item>PDF_417</item> <item>PDF_417</item>
<item>AZTEC</item>
<item>CODABAR</item>
<item>DATA_MATRIX</item>
<item>ITF</item>
</string-array> </string-array>
</resources> </resources>

View File

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

View File

@ -1,12 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Theme.Fidelity" parent="Theme.MaterialComponents.DayNight.NoActionBar"> <style name="Theme.Fidelity" parent="Theme.Material3.DayNight.NoActionBar">
<item name="colorPrimary">?attr/colorAccent</item>
<item name="colorPrimary">#7DB9F5</item>
<item name="colorPrimaryVariant">#7DB9F5</item>
<item name="colorSecondary">#7DB9F5</item>
<item name="colorSecondaryVariant">#7DB9F5</item>
<item name="colorOnPrimary">#030B12</item>
</style> </style>
</resources> </resources>

View File

@ -2,7 +2,6 @@
plugins { plugins {
id 'com.android.application' version '8.3.1' apply false id 'com.android.application' version '8.3.1' apply false
id 'com.android.library' version '8.3.1' apply false id 'com.android.library' version '8.3.0' apply false
id 'org.jetbrains.kotlin.android' version '1.9.23' apply false id 'org.jetbrains.kotlin.android' version '1.9.23' apply false
id 'com.autonomousapps.dependency-analysis' version '1.30.0' apply true
} }

View File

@ -15,7 +15,7 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# Android operating system, and which are packaged with your app's APK # Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn # https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=false android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete": # Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the # Enables namespacing of each library's R class so that its R class includes only the

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-8.7-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View File

@ -14,6 +14,5 @@ dependencyResolutionManagement {
maven { url 'https://jitpack.io' } maven { url 'https://jitpack.io' }
} }
} }
rootProject.name = "BeenDroid"
rootProject.name = "Fidelity"
include ':app' include ':app'