File Scanner & Metadata
@ -1,7 +1,7 @@
|
||||
<!--suppress ALL -->
|
||||
<div align="center">
|
||||
<h1>Keepass Fidelity</h1>
|
||||
<img src="./app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp" alt="Logo">
|
||||
<img style="width: 120px;" src="./metadata/en-US/images/icon.png" alt="Logo">
|
||||
|
||||
<p>A minimalist fidelity/loyalty card plugin</p>
|
||||
|
||||
@ -18,9 +18,9 @@
|
||||
<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>
|
||||
<td style="width: 33%; height: 100px;"><img src="./metadata/en-US/images/phoneScreenshots/launcher.jpg" alt="Launcher" style="width: 100%; height: 100%;"></td>
|
||||
<td style="width: 33%; height: 100px;"><img src="./metadata/en-US/images/phoneScreenshots/view.jpg" alt="View" style="width: 100%; height: 100%;"></td>
|
||||
<td style="width: 33%; height: 100px;"><img src="./metadata/en-US/images/phoneScreenshots/edit.jpg" alt="Edit" style="width: 100%; height: 100%;"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -5,7 +5,10 @@
|
||||
android:versionName="1.1c">
|
||||
|
||||
<uses-feature android:name="android.hardware.camera" />
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
|
||||
|
||||
<application
|
||||
android:icon="@mipmap/ic_launcher_round"
|
||||
android:label="@string/app_name"
|
||||
|
@ -0,0 +1,107 @@
|
||||
package net.helcel.fidelity.activity.fragment
|
||||
|
||||
import android.Manifest
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.result.PickVisualMediaRequest
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.fragment.app.Fragment
|
||||
import net.helcel.fidelity.R
|
||||
import net.helcel.fidelity.tools.BarcodeScanner
|
||||
import net.helcel.fidelity.tools.ErrorToaster
|
||||
import net.helcel.fidelity.tools.KeepassWrapper
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
class FileScanner : Fragment() {
|
||||
|
||||
private var code: String = ""
|
||||
private var fmt: String = ""
|
||||
|
||||
|
||||
private val resultPermission =
|
||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
||||
resultLauncherOpenMediaPick.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
|
||||
}
|
||||
|
||||
private val resultLauncherOpenMediaBase =
|
||||
registerForActivityResult(ActivityResultContracts.GetContent()) {
|
||||
loadUri(it)
|
||||
}
|
||||
|
||||
private val resultLauncherOpenMediaPick =
|
||||
registerForActivityResult(ActivityResultContracts.PickVisualMedia()) {
|
||||
loadUri(it)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
println(Build.VERSION.SDK_INT)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
resultPermission.launch(Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED)
|
||||
} else {
|
||||
// resultLauncherOpenMediaBase.launch("image/*")
|
||||
resultLauncherOpenMediaPick.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
|
||||
}
|
||||
return View(context)
|
||||
}
|
||||
|
||||
private fun startCreateEntry() {
|
||||
val createEntryFragment = CreateEntry()
|
||||
createEntryFragment.arguments =
|
||||
KeepassWrapper.bundleCreate(null, this.code, this.fmt)
|
||||
requireActivity().supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.container, createEntryFragment)
|
||||
.commit()
|
||||
}
|
||||
|
||||
private fun scannerResult(code: String?, format: String?) {
|
||||
if (!code.isNullOrEmpty() && !format.isNullOrEmpty()) {
|
||||
this.code = code
|
||||
this.fmt = format
|
||||
}
|
||||
val isDone = this.code.isNotEmpty() && this.fmt.isNotEmpty()
|
||||
requireActivity().runOnUiThread {
|
||||
if (isDone) {
|
||||
startCreateEntry()
|
||||
} else {
|
||||
parentFragmentManager.popBackStack()
|
||||
ErrorToaster.nothingFound(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadUri(it: Uri?) {
|
||||
try {
|
||||
run {
|
||||
require(it != null)
|
||||
|
||||
val file = requireContext().contentResolver.openInputStream(it)
|
||||
val image = BitmapFactory.decodeStream(file)
|
||||
BarcodeScanner.bitmapUseCase(image) { code, format ->
|
||||
scannerResult(code, format)
|
||||
}
|
||||
}
|
||||
} catch (e: FileNotFoundException) {
|
||||
e.printStackTrace()
|
||||
println(e.message)
|
||||
println(it)
|
||||
ErrorToaster.noPermission(context)
|
||||
parentFragmentManager.popBackStack()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
ErrorToaster.nothingFound(context)
|
||||
parentFragmentManager.popBackStack()
|
||||
} catch (e: SecurityException) {
|
||||
ErrorToaster.noPermission(context)
|
||||
parentFragmentManager.popBackStack()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -49,6 +49,11 @@ class Launcher : Fragment() {
|
||||
startScanner()
|
||||
hideMenuAdd()
|
||||
}
|
||||
binding.btnOpen.setOnClickListener {
|
||||
startFileScanner()
|
||||
hideMenuAdd()
|
||||
}
|
||||
|
||||
|
||||
binding.btnManual.setOnClickListener {
|
||||
startCreateEntry()
|
||||
@ -96,6 +101,10 @@ class Launcher : Fragment() {
|
||||
startFragment(Scanner())
|
||||
}
|
||||
|
||||
private fun startFileScanner() {
|
||||
startFragment(FileScanner())
|
||||
}
|
||||
|
||||
private fun startCreateEntry() {
|
||||
startFragment(CreateEntry())
|
||||
}
|
||||
|
@ -2,25 +2,23 @@ package net.helcel.fidelity.activity.fragment
|
||||
|
||||
import android.Manifest
|
||||
import android.content.ContentValues
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.core.Preview
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import net.helcel.fidelity.R
|
||||
import net.helcel.fidelity.databinding.FragScannerBinding
|
||||
import net.helcel.fidelity.tools.BarcodeScanner.getAnalysisUseCase
|
||||
import net.helcel.fidelity.tools.BarcodeScanner.analysisUseCase
|
||||
import net.helcel.fidelity.tools.ErrorToaster
|
||||
import net.helcel.fidelity.tools.KeepassWrapper
|
||||
|
||||
private const val CAMERA_PERMISSION_REQUEST_CODE = 1
|
||||
|
||||
class Scanner : Fragment() {
|
||||
|
||||
private lateinit var binding: FragScannerBinding
|
||||
@ -28,6 +26,17 @@ class Scanner : Fragment() {
|
||||
private var code: String = ""
|
||||
private var fmt: String = ""
|
||||
|
||||
|
||||
private val resultPermissionRequest =
|
||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
||||
if (it) {
|
||||
bindCameraUseCases()
|
||||
} else {
|
||||
parentFragmentManager.popBackStack()
|
||||
ErrorToaster.noPermission(context)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
@ -37,11 +46,8 @@ class Scanner : Fragment() {
|
||||
binding.btnScanDone.setOnClickListener {
|
||||
startCreateEntry()
|
||||
}
|
||||
when (hasCameraPermission()) {
|
||||
true -> bindCameraUseCases()
|
||||
else -> requestPermission()
|
||||
}
|
||||
binding.btnScanDone.isEnabled = false
|
||||
resultPermissionRequest.launch(Manifest.permission.CAMERA)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@ -55,26 +61,16 @@ class Scanner : Fragment() {
|
||||
.commit()
|
||||
}
|
||||
|
||||
private fun hasCameraPermission() =
|
||||
ActivityCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.CAMERA
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
|
||||
private fun requestPermission() {
|
||||
ActivityCompat.requestPermissions(
|
||||
requireActivity(),
|
||||
arrayOf(Manifest.permission.CAMERA),
|
||||
CAMERA_PERMISSION_REQUEST_CODE
|
||||
)
|
||||
ActivityCompat.OnRequestPermissionsResultCallback { c, p, i ->
|
||||
require(c == CAMERA_PERMISSION_REQUEST_CODE)
|
||||
require(p.contains(Manifest.permission.CAMERA))
|
||||
val el = i[p.indexOf(Manifest.permission.CAMERA)]
|
||||
if (el != PackageManager.PERMISSION_GRANTED) {
|
||||
startCreateEntry()
|
||||
}
|
||||
|
||||
private fun scannerResult(code: String?, format: String?) {
|
||||
if (!code.isNullOrEmpty() && !format.isNullOrEmpty()) {
|
||||
this.code = code
|
||||
this.fmt = format
|
||||
}
|
||||
val isDone = this.code.isNotEmpty() && this.fmt.isNotEmpty()
|
||||
requireActivity().runOnUiThread {
|
||||
binding.btnScanDone.isEnabled = isDone
|
||||
binding.ScanActive.isEnabled = !isDone
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,16 +85,8 @@ class Scanner : Fragment() {
|
||||
it.setSurfaceProvider(binding.cameraView.surfaceProvider)
|
||||
}
|
||||
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
||||
val analysisUseCase = getAnalysisUseCase { code, format ->
|
||||
if (!code.isNullOrEmpty() && !format.isNullOrEmpty()) {
|
||||
this.code = code
|
||||
this.fmt = format
|
||||
}
|
||||
val isDone = this.code.isNotEmpty() && this.fmt.isNotEmpty()
|
||||
requireActivity().runOnUiThread {
|
||||
binding.btnScanDone.isEnabled = isDone
|
||||
binding.ScanActive.isEnabled = !isDone
|
||||
}
|
||||
val analysisUseCase = analysisUseCase { code, format ->
|
||||
scannerResult(code, format)
|
||||
}
|
||||
try {
|
||||
cameraProvider.bindToLifecycle(
|
||||
|
@ -4,7 +4,6 @@ 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
|
||||
@ -15,14 +14,13 @@ import net.helcel.fidelity.tools.BarcodeFormatConverter.formatToString
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
|
||||
@OptIn(ExperimentalGetImage::class)
|
||||
object BarcodeScanner {
|
||||
|
||||
@OptIn(ExperimentalGetImage::class)
|
||||
private fun processImageProxy(
|
||||
imageProxy: ImageProxy,
|
||||
private fun processImage(
|
||||
bitmap: Bitmap,
|
||||
cb: (String?, String?) -> Unit
|
||||
) {
|
||||
val bitmap = imageProxy.toBitmap() // Convert ImageProxy to Bitmap
|
||||
val binaryBitmap = createBinaryBitmap(bitmap)
|
||||
val reader = MultiFormatReader()
|
||||
try {
|
||||
@ -32,8 +30,6 @@ object BarcodeScanner {
|
||||
cb(null, null)
|
||||
} catch (e: ReaderException) {
|
||||
cb(null, null)
|
||||
} finally {
|
||||
imageProxy.close()
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,13 +41,21 @@ object BarcodeScanner {
|
||||
return BinaryBitmap(HybridBinarizer(source))
|
||||
}
|
||||
|
||||
fun getAnalysisUseCase(cb: (String?, String?) -> Unit): ImageAnalysis {
|
||||
fun analysisUseCase(cb: (String?, String?) -> Unit): ImageAnalysis {
|
||||
val analysisUseCase = ImageAnalysis.Builder().build()
|
||||
analysisUseCase.setAnalyzer(
|
||||
Executors.newSingleThreadExecutor()
|
||||
) { imageProxy ->
|
||||
processImageProxy(imageProxy, cb)
|
||||
val bitmap = imageProxy.toBitmap()
|
||||
imageProxy.close()
|
||||
bitmapUseCase(bitmap, cb)
|
||||
}
|
||||
return analysisUseCase
|
||||
}
|
||||
|
||||
fun bitmapUseCase(bitmap: Bitmap, cb: (String?, String?) -> Unit) {
|
||||
processImage(bitmap, cb)
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -20,4 +20,12 @@ object ErrorToaster {
|
||||
fun invalidFormat(activity: Context?) {
|
||||
helper(activity, "Invalid Format", Toast.LENGTH_SHORT)
|
||||
}
|
||||
|
||||
fun nothingFound(activity: Context?) {
|
||||
helper(activity, "Nothing Found", Toast.LENGTH_SHORT)
|
||||
}
|
||||
|
||||
fun noPermission(activity: Context?) {
|
||||
helper(activity, "Missing Permission", Toast.LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
|
22
app/src/main/res/drawable/open.xml
Normal file
@ -0,0 +1,22 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="72dp"
|
||||
android:height="72dp"
|
||||
android:viewportWidth="58"
|
||||
android:viewportHeight="58">
|
||||
<group android:translateX="-10" android:translateY="-8">
|
||||
<path
|
||||
android:pathData="m57.008,20.304v-3.356l-27.338,-0.002c-0.198,0 -0.359,-0.165 -0.359,-0.368l-0.069,-1.517c-0.116,-1.788 -1.34,-3.003 -2.997,-3.003h-11.287c-1.657,0 -3,1.343 -3,3v40.943"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="m17.027,55.568c-0.59,1.954 -2.972,4.139 -4.646,4.394l44.665,0.011c1.657,0 2.323,-0.439 3,-3s7,-31.657 7,-31.657c0,-0.552 -0.448,-1 -1,-1H24.965c-0.552,0 -1,0.448 -1,1 0,0 -6.348,28.299 -6.938,30.253Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"
|
||||
android:strokeLineCap="round"/>
|
||||
</group>
|
||||
</vector>
|
@ -56,6 +56,16 @@
|
||||
app:maxImageSize="32dp"
|
||||
app:srcCompat="@drawable/camera" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/btnOpen"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:contentDescription="@string/open"
|
||||
app:fabCustomSize="46dp"
|
||||
app:maxImageSize="32dp"
|
||||
app:srcCompat="@drawable/open" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/btnManual"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -15,6 +15,7 @@
|
||||
<string name="code">Code</string>
|
||||
<string name="format">Format</string>
|
||||
<string name="save">Save</string>
|
||||
<string name="open">Open</string>
|
||||
<string-array name="format_array">
|
||||
<item>CODE_39</item>
|
||||
<item>CODE_93</item>
|
||||
|
1
metadata/en-US/full_description.txt
Normal file
@ -0,0 +1 @@
|
||||
<p><i>Keepass-Fidelity</i> adds an interface to view/save barcodes (QR included) to Keepass through the plugin interface of the Keepass2Android app.</p><p><br></p><ul><li><b>Launcher:</b> view and launch recent entries (a per entry flag can disable this behaviour)</li><li><b>View:</b> view entries from the history or queried from Keepass2Android</li><li><b>Create:</b> add entries from the camera, an image of by filling out a form. The entry is then created in the Keepass2Android app</li><li><b>Data:</b> the app uses the following data Title (entry name), barcode type (QR, UPC, ...), barcode content (number/text content) and a "secure" flag (enable/disable caching the entry).</li></ul>
|
BIN
metadata/en-US/images/icon.png
Normal file
After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
1
metadata/en-US/short_description.txt
Normal file
@ -0,0 +1 @@
|
||||
Fidelity (Membership/Loyalty) Card plugin for Keepass2Android
|