gitignore & init
This commit is contained in:
parent
75dfdafebf
commit
7dc43c0682
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
*.iml
|
||||
.idea/
|
||||
node_modules/
|
||||
temp/
|
||||
.gradle
|
||||
local.properties/
|
||||
.DS_Store
|
||||
build/
|
||||
app/build/
|
||||
captures/
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
54
app/build.gradle
Normal file
54
app/build.gradle
Normal file
@ -0,0 +1,54 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.23'
|
||||
}
|
||||
|
||||
|
||||
android {
|
||||
namespace 'net.helcel.fidelity'
|
||||
compileSdk 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId 'net.helcel.fidelity'
|
||||
minSdk 28
|
||||
targetSdk 34
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
|
||||
implementation 'androidx.camera:camera-camera2:1.3.2'
|
||||
implementation 'androidx.camera:camera-lifecycle:1.3.2'
|
||||
implementation 'androidx.camera:camera-view:1.3.2'
|
||||
implementation 'com.google.code.gson:gson:2.10.1'
|
||||
implementation 'com.google.android.material:material:1.11.0'
|
||||
implementation 'com.google.zxing:core:3.5.3'
|
||||
implementation 'com.google.mlkit:barcode-scanning:17.2.0'
|
||||
|
||||
}
|
3
app/lint.xml
Normal file
3
app/lint.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<lint>
|
||||
</lint>
|
49
app/src/main/AndroidManifest.xml
Normal file
49
app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:versionCode="1"
|
||||
android:versionName="1.0">
|
||||
|
||||
<uses-feature android:name="android.hardware.camera" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<application
|
||||
android:icon="@drawable/logo"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true">
|
||||
<activity
|
||||
android:name=".activity.MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.Fidelity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<receiver
|
||||
android:name=".pluginSDK.PluginAccessReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="keepass2android.ACTION_TRIGGER_REQUEST_ACCESS" />
|
||||
<action android:name="keepass2android.ACTION_RECEIVE_ACCESS" />
|
||||
<action android:name="keepass2android.ACTION_REVOKE_ACCESS" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<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>
|
||||
|
||||
</manifest>
|
@ -0,0 +1,48 @@
|
||||
package net.helcel.fidelity.activity
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import androidx.activity.addCallback
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import net.helcel.fidelity.R
|
||||
import net.helcel.fidelity.activity.fragment.Launcher
|
||||
import net.helcel.fidelity.databinding.ActMainBinding
|
||||
import net.helcel.fidelity.tools.CacheManager
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActMainBinding
|
||||
|
||||
private lateinit var sharedPreferences: SharedPreferences
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
sharedPreferences =
|
||||
this.getSharedPreferences(CacheManager.PREF_NAME, Context.MODE_PRIVATE)
|
||||
CacheManager.loadFidelity(sharedPreferences)
|
||||
|
||||
|
||||
binding = ActMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
if (supportFragmentManager.backStackEntryCount > 0) {
|
||||
supportFragmentManager.popBackStackImmediate()
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
if (savedInstanceState == null)
|
||||
loadLauncher()
|
||||
|
||||
}
|
||||
|
||||
private fun loadLauncher() {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.add(R.id.container, Launcher())
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,50 @@
|
||||
package net.helcel.fidelity.activity.adapter
|
||||
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.widget.LinearLayout
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import net.helcel.fidelity.databinding.ListItemFidelityBinding
|
||||
|
||||
class FidelityListAdapter(
|
||||
private val triples: ArrayList<Triple<String?, String?, String?>>,
|
||||
private val onItemClicked: (Triple<String?, String?, String?>) -> Unit
|
||||
) :
|
||||
RecyclerView.Adapter<FidelityListAdapter.FidelityViewHolder>() {
|
||||
|
||||
private lateinit var binding: ListItemFidelityBinding
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FidelityViewHolder {
|
||||
binding = ListItemFidelityBinding.inflate(LayoutInflater.from(parent.context))
|
||||
binding.root.setLayoutParams(
|
||||
LinearLayout.LayoutParams(
|
||||
MATCH_PARENT, WRAP_CONTENT
|
||||
)
|
||||
)
|
||||
return FidelityViewHolder(binding.root)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: FidelityViewHolder, position: Int) {
|
||||
val triple = triples[position]
|
||||
holder.bind(triple)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = triples.size
|
||||
|
||||
inner class FidelityViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
|
||||
fun bind(triple: Triple<String?, String?, String?>) {
|
||||
val text = "${triple.first}"
|
||||
binding.textView.text = text
|
||||
binding.card.setOnClickListener { onItemClicked(triple) }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,143 @@
|
||||
package net.helcel.fidelity.activity.fragment
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.zxing.FormatException
|
||||
import net.helcel.fidelity.R
|
||||
import net.helcel.fidelity.databinding.FragCreateEntryBinding
|
||||
import net.helcel.fidelity.pluginSDK.Kp2aControl
|
||||
import net.helcel.fidelity.tools.BarcodeGenerator.generateBarcode
|
||||
import net.helcel.fidelity.tools.CacheManager
|
||||
import net.helcel.fidelity.tools.ErrorToaster
|
||||
import net.helcel.fidelity.tools.KeepassWrapper
|
||||
|
||||
private const val DEBOUNCE_DELAY = 500L
|
||||
|
||||
class CreateEntry : Fragment() {
|
||||
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private lateinit var binding: FragCreateEntryBinding
|
||||
|
||||
private val resultLauncherAdd = KeepassWrapper.resultLauncherAdd(this) {
|
||||
val r = KeepassWrapper.entryExtract(it)
|
||||
if (!KeepassWrapper.isProtected(it)) {
|
||||
CacheManager.addFidelity(r)
|
||||
}
|
||||
startViewEntry(r.first, r.second, r.third)
|
||||
}
|
||||
|
||||
private var isValid: Boolean = false
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragCreateEntryBinding.inflate(layoutInflater)
|
||||
|
||||
val formats = resources.getStringArray(R.array.format_array)
|
||||
val arrayAdapter = ArrayAdapter(requireContext(), R.layout.list_item_dropdown, formats)
|
||||
binding.editTextFormat.setAdapter(arrayAdapter)
|
||||
|
||||
val res = KeepassWrapper.bundleExtract(arguments)
|
||||
binding.editTextCode.setText(res.second)
|
||||
binding.editTextFormat.setText(res.third, false)
|
||||
|
||||
val changeListener = {
|
||||
isValid = false
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
handler.postDelayed({
|
||||
updatePreview()
|
||||
}, DEBOUNCE_DELAY)
|
||||
}
|
||||
|
||||
binding.editTextCode.addTextChangedListener { changeListener() }
|
||||
binding.editTextFormat.addTextChangedListener { changeListener() }
|
||||
binding.editTextFormat.addTextChangedListener { binding.editTextFormat.error = null }
|
||||
binding.btnSave.setOnClickListener {
|
||||
if (!isValid() || !isValid) {
|
||||
ErrorToaster.formIncomplete(requireActivity())
|
||||
|
||||
} else {
|
||||
val kpentry = KeepassWrapper.entryCreate(
|
||||
this,
|
||||
binding.editTextTitle.text.toString(),
|
||||
binding.editTextCode.text.toString(),
|
||||
binding.editTextFormat.text.toString(),
|
||||
binding.checkboxProtected.isChecked,
|
||||
)
|
||||
try {
|
||||
resultLauncherAdd.launch(
|
||||
Kp2aControl.getAddEntryIntent(
|
||||
kpentry.first,
|
||||
kpentry.second
|
||||
)
|
||||
)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
ErrorToaster.noKP2AFound(requireActivity())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updatePreview()
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun updatePreview() {
|
||||
try {
|
||||
val barcodeBitmap = generateBarcode(
|
||||
binding.editTextCode.text.toString(),
|
||||
binding.editTextFormat.text.toString(),
|
||||
600
|
||||
)
|
||||
binding.imageViewPreview.setImageBitmap(barcodeBitmap)
|
||||
isValid = true
|
||||
} catch (e: FormatException) {
|
||||
binding.imageViewPreview.setImageBitmap(null)
|
||||
binding.editTextCode.error = "Invalid format"
|
||||
} catch (e: IllegalArgumentException) {
|
||||
binding.imageViewPreview.setImageBitmap(null)
|
||||
binding.editTextCode.error = e.message
|
||||
} catch (e: Exception) {
|
||||
binding.imageViewPreview.setImageBitmap(null)
|
||||
println(e.javaClass)
|
||||
println(e.message)
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun isValid(): Boolean {
|
||||
var valid = true
|
||||
if (binding.editTextTitle.text!!.isEmpty()) {
|
||||
valid = false
|
||||
binding.editTextTitle.error = "Title cannot be empty"
|
||||
}
|
||||
if (binding.editTextCode.text!!.isEmpty()) {
|
||||
valid = false
|
||||
binding.editTextCode.error = "Code cannot be empty"
|
||||
}
|
||||
if (binding.editTextFormat.text!!.isEmpty()) {
|
||||
valid = false
|
||||
binding.editTextFormat.error = "Format cannot be empty"
|
||||
}
|
||||
return valid
|
||||
}
|
||||
|
||||
|
||||
private fun startViewEntry(title: String?, code: String?, fmt: String?) {
|
||||
val viewEntryFragment = ViewEntry()
|
||||
viewEntryFragment.arguments = KeepassWrapper.bundleCreate(title, code, fmt)
|
||||
|
||||
requireActivity().supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.container, viewEntryFragment).commit()
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
package net.helcel.fidelity.activity.fragment
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import net.helcel.fidelity.R
|
||||
import net.helcel.fidelity.activity.adapter.FidelityListAdapter
|
||||
import net.helcel.fidelity.databinding.FragLauncherBinding
|
||||
import net.helcel.fidelity.pluginSDK.Kp2aControl
|
||||
import net.helcel.fidelity.tools.CacheManager
|
||||
import net.helcel.fidelity.tools.ErrorToaster
|
||||
import net.helcel.fidelity.tools.KeepassWrapper
|
||||
|
||||
|
||||
class Launcher : Fragment() {
|
||||
|
||||
private lateinit var binding: FragLauncherBinding
|
||||
private lateinit var fidelityListAdapter: FidelityListAdapter
|
||||
|
||||
private val resultLauncherQuery = KeepassWrapper.resultLauncherQuery(this) {
|
||||
val r = KeepassWrapper.entryExtract(it)
|
||||
if (!KeepassWrapper.isProtected(it)) {
|
||||
CacheManager.addFidelity(r)
|
||||
}
|
||||
startViewEntry(r.first, r.second, r.third)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragLauncherBinding.inflate(layoutInflater)
|
||||
binding.btnQuery.setOnClickListener { startGetFromKeepass() }
|
||||
binding.btnAdd.setOnClickListener {
|
||||
if (binding.menuAdd.visibility == View.GONE)
|
||||
showMenuAdd()
|
||||
else
|
||||
hideMenuAdd()
|
||||
}
|
||||
|
||||
hideMenuAdd()
|
||||
binding.btnScan.setOnClickListener {
|
||||
startScanner()
|
||||
hideMenuAdd()
|
||||
}
|
||||
|
||||
binding.btnManual.setOnClickListener {
|
||||
startCreateEntry()
|
||||
hideMenuAdd()
|
||||
}
|
||||
|
||||
binding.fidelityList.layoutManager =
|
||||
LinearLayoutManager(requireContext())
|
||||
fidelityListAdapter = FidelityListAdapter(CacheManager.getFidelity()) {
|
||||
startViewEntry(it.first, it.second, it.third)
|
||||
}
|
||||
binding.fidelityList.adapter = fidelityListAdapter
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun hideMenuAdd() {
|
||||
binding.btnAdd.setImageResource(R.drawable.cross)
|
||||
binding.menuAdd.visibility = View.GONE
|
||||
|
||||
}
|
||||
|
||||
private fun showMenuAdd() {
|
||||
binding.btnAdd.setImageResource(R.drawable.minus)
|
||||
binding.menuAdd.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
|
||||
private fun startGetFromKeepass() {
|
||||
try {
|
||||
this.resultLauncherQuery.launch(Kp2aControl.queryEntryIntentForOwnPackage)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
ErrorToaster.noKP2AFound(requireActivity())
|
||||
}
|
||||
}
|
||||
|
||||
private fun startFragment(fragment: Fragment) {
|
||||
requireActivity().supportFragmentManager.beginTransaction()
|
||||
.addToBackStack("Launcher")
|
||||
.replace(R.id.container, fragment).commit()
|
||||
}
|
||||
|
||||
private fun startScanner() {
|
||||
startFragment(Scanner())
|
||||
}
|
||||
|
||||
private fun startCreateEntry() {
|
||||
startFragment(CreateEntry())
|
||||
}
|
||||
|
||||
|
||||
private fun startViewEntry(title: String?, code: String?, fmt: String?) {
|
||||
val viewEntryFragment = ViewEntry()
|
||||
viewEntryFragment.arguments = KeepassWrapper.bundleCreate(title, code, fmt)
|
||||
startFragment(viewEntryFragment)
|
||||
}
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
package net.helcel.fidelity.activity.fragment
|
||||
|
||||
import android.Manifest
|
||||
import android.content.ContentValues
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.core.Preview
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import net.helcel.fidelity.R
|
||||
import net.helcel.fidelity.databinding.FragScannerBinding
|
||||
import net.helcel.fidelity.tools.BarcodeScanner.getAnalysisUseCase
|
||||
import net.helcel.fidelity.tools.KeepassWrapper
|
||||
|
||||
private const val CAMERA_PERMISSION_REQUEST_CODE = 1
|
||||
|
||||
class Scanner : Fragment() {
|
||||
|
||||
private lateinit var binding: FragScannerBinding
|
||||
|
||||
private var code: String = ""
|
||||
private var fmt: String = ""
|
||||
private var valid: Boolean = false
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragScannerBinding.inflate(layoutInflater)
|
||||
binding.bottomText.setOnClickListener {
|
||||
startCreateEntry()
|
||||
}
|
||||
when (hasCameraPermission()) {
|
||||
true -> bindCameraUseCases()
|
||||
else -> requestPermission()
|
||||
}
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun startCreateEntry() {
|
||||
val createEntryFragment = CreateEntry()
|
||||
createEntryFragment.arguments =
|
||||
KeepassWrapper.bundleCreate(null, this.code, this.fmt)
|
||||
requireActivity().supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.container, createEntryFragment)
|
||||
.commit()
|
||||
}
|
||||
|
||||
private fun hasCameraPermission() =
|
||||
ActivityCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.CAMERA
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
|
||||
private fun requestPermission() {
|
||||
ActivityCompat.requestPermissions(
|
||||
requireActivity(),
|
||||
arrayOf(Manifest.permission.CAMERA),
|
||||
CAMERA_PERMISSION_REQUEST_CODE
|
||||
)
|
||||
ActivityCompat.OnRequestPermissionsResultCallback { c, p, i ->
|
||||
require(c == CAMERA_PERMISSION_REQUEST_CODE)
|
||||
require(p.contains(Manifest.permission.CAMERA))
|
||||
val el = i[p.indexOf(Manifest.permission.CAMERA)]
|
||||
if (el != PackageManager.PERMISSION_GRANTED) {
|
||||
startCreateEntry()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindCameraUseCases() {
|
||||
val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
|
||||
|
||||
cameraProviderFuture.addListener({
|
||||
val cameraProvider = cameraProviderFuture.get()
|
||||
val previewUseCase = Preview.Builder()
|
||||
.build()
|
||||
.also {
|
||||
it.setSurfaceProvider(binding.cameraView.surfaceProvider)
|
||||
}
|
||||
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
||||
val analysisUseCase = getAnalysisUseCase { code, format ->
|
||||
if (code != null && format != null) {
|
||||
this.code = code
|
||||
this.fmt = format
|
||||
this.valid = true
|
||||
} else {
|
||||
this.valid = false
|
||||
}
|
||||
}
|
||||
try {
|
||||
cameraProvider.bindToLifecycle(
|
||||
this,
|
||||
cameraSelector,
|
||||
previewUseCase,
|
||||
analysisUseCase
|
||||
)
|
||||
} catch (illegalStateException: IllegalStateException) {
|
||||
Log.e(ContentValues.TAG, illegalStateException.message.orEmpty())
|
||||
} catch (illegalArgumentException: IllegalArgumentException) {
|
||||
Log.e(ContentValues.TAG, illegalArgumentException.message.orEmpty())
|
||||
}
|
||||
}, ContextCompat.getMainExecutor(requireContext()))
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
package net.helcel.fidelity.activity.fragment
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.zxing.FormatException
|
||||
import net.helcel.fidelity.databinding.FragViewEntryBinding
|
||||
import net.helcel.fidelity.tools.BarcodeGenerator.generateBarcode
|
||||
import net.helcel.fidelity.tools.ErrorToaster
|
||||
import net.helcel.fidelity.tools.KeepassWrapper
|
||||
|
||||
|
||||
class ViewEntry : Fragment() {
|
||||
|
||||
private lateinit var binding: FragViewEntryBinding
|
||||
|
||||
private var title: String? = null
|
||||
private var code: String? = null
|
||||
private var fmt: String? = null
|
||||
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragViewEntryBinding.inflate(layoutInflater)
|
||||
val res = KeepassWrapper.bundleExtract(arguments)
|
||||
title = res.first
|
||||
code = res.second
|
||||
fmt = res.third
|
||||
|
||||
adjustLayout()
|
||||
updatePreview()
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun updatePreview() {
|
||||
binding.title.text = title
|
||||
try {
|
||||
val barcodeBitmap = generateBarcode(
|
||||
code!!, fmt!!, 1024
|
||||
)
|
||||
binding.imageViewPreview.setImageBitmap(barcodeBitmap)
|
||||
} catch (e: FormatException) {
|
||||
ErrorToaster.invalidFormat(requireActivity())
|
||||
binding.imageViewPreview.setImageBitmap(null)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
binding.imageViewPreview.setImageBitmap(null)
|
||||
ErrorToaster.invalidFormat(requireActivity())
|
||||
} catch (e: Exception) {
|
||||
binding.imageViewPreview.setImageBitmap(null)
|
||||
println(e.javaClass)
|
||||
println(e.message)
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
adjustLayout()
|
||||
}
|
||||
|
||||
private fun adjustLayout() {
|
||||
if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
binding.title.visibility = View.GONE
|
||||
} else {
|
||||
binding.title.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
179
app/src/main/java/net/helcel/fidelity/pluginSDK/AccessManager.kt
Normal file
179
app/src/main/java/net/helcel/fidelity/pluginSDK/AccessManager.kt
Normal file
@ -0,0 +1,179 @@
|
||||
package net.helcel.fidelity.pluginSDK
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
|
||||
object AccessManager {
|
||||
private const val _tag = "Kp2aPluginSDK"
|
||||
private const val PREF_KEY_SCOPE = "scope"
|
||||
private const val PREF_KEY_TOKEN = "token"
|
||||
|
||||
private fun stringArrayToString(values: ArrayList<String?>): String? {
|
||||
val a = JSONArray()
|
||||
for (i in values.indices) {
|
||||
a.put(values[i])
|
||||
}
|
||||
return if (values.isNotEmpty()) {
|
||||
a.toString()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun stringToStringArray(s: String?): ArrayList<String> {
|
||||
val strings = ArrayList<String>()
|
||||
if (!TextUtils.isEmpty(s)) {
|
||||
try {
|
||||
val a = JSONArray(s)
|
||||
for (i in 0 until a.length()) {
|
||||
val url = a.optString(i)
|
||||
strings.add(url)
|
||||
}
|
||||
} catch (e: JSONException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
return strings
|
||||
}
|
||||
|
||||
fun storeAccessToken(
|
||||
ctx: Context,
|
||||
hostPackage: String,
|
||||
accessToken: String,
|
||||
scopes: ArrayList<String?>
|
||||
) {
|
||||
val prefs = getPrefsForHost(ctx, hostPackage)
|
||||
|
||||
val edit = prefs.edit()
|
||||
edit.putString(PREF_KEY_TOKEN, accessToken)
|
||||
val scopesString = stringArrayToString(scopes)
|
||||
edit.putString(PREF_KEY_SCOPE, scopesString)
|
||||
edit.apply()
|
||||
Log.d(
|
||||
_tag,
|
||||
"stored access token " + accessToken.substring(
|
||||
0,
|
||||
4
|
||||
) + "... for " + scopes.size + " scopes (" + scopesString + ")."
|
||||
)
|
||||
|
||||
val hostPrefs = ctx.getSharedPreferences("KP2A.PluginAccess.hosts", Context.MODE_PRIVATE)
|
||||
if (!hostPrefs.contains(hostPackage)) {
|
||||
hostPrefs.edit().putString(hostPackage, "").apply()
|
||||
}
|
||||
}
|
||||
|
||||
fun preparePopup(popupMenu: Any) {
|
||||
try {
|
||||
val fields = popupMenu.javaClass.declaredFields
|
||||
for (field in fields) {
|
||||
if ("mPopup" == field.name) {
|
||||
field.isAccessible = true
|
||||
val menuPopupHelper = field[popupMenu]
|
||||
val classPopupHelper = Class.forName(
|
||||
menuPopupHelper
|
||||
.javaClass.name
|
||||
)
|
||||
val setForceIcons = classPopupHelper.getMethod(
|
||||
"setForceShowIcon", Boolean::class.javaPrimitiveType
|
||||
)
|
||||
setForceIcons.invoke(menuPopupHelper, true)
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPrefsForHost(
|
||||
ctx: Context,
|
||||
hostPackage: String
|
||||
): SharedPreferences {
|
||||
val prefs = ctx.getSharedPreferences("KP2A.PluginAccess.$hostPackage", Context.MODE_PRIVATE)
|
||||
return prefs
|
||||
}
|
||||
|
||||
fun tryGetAccessToken(ctx: Context, hostPackage: String, scopes: ArrayList<String?>): String? {
|
||||
if (TextUtils.isEmpty(hostPackage)) {
|
||||
Log.d(_tag, "hostPackage is empty!")
|
||||
return null
|
||||
}
|
||||
Log.d(_tag, "trying to find prefs for $hostPackage")
|
||||
val prefs = getPrefsForHost(ctx, hostPackage)
|
||||
val scopesString = prefs.getString(PREF_KEY_SCOPE, "")
|
||||
Log.d(_tag, "available scopes: $scopesString")
|
||||
val currentScope = stringToStringArray(scopesString)
|
||||
if (isSubset(scopes, currentScope)) {
|
||||
return prefs.getString(PREF_KEY_TOKEN, null)
|
||||
} else {
|
||||
Log.d(_tag, "looks like scope changed. Access token invalid.")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun isSubset(
|
||||
requiredScopes: ArrayList<String?>,
|
||||
availableScopes: ArrayList<String>
|
||||
): Boolean {
|
||||
for (r in requiredScopes) {
|
||||
if (availableScopes.indexOf(r) < 0) {
|
||||
Log.d(_tag, "Scope " + r + " not available. " + availableScopes.size)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun removeAccessToken(
|
||||
ctx: Context, hostPackage: String,
|
||||
accessToken: String
|
||||
) {
|
||||
val prefs = getPrefsForHost(ctx, hostPackage)
|
||||
|
||||
Log.d(_tag, "removing AccessToken.")
|
||||
if (prefs.getString(PREF_KEY_TOKEN, "") == accessToken) {
|
||||
val edit = prefs.edit()
|
||||
edit.clear()
|
||||
edit.apply()
|
||||
}
|
||||
|
||||
val hostPrefs = ctx.getSharedPreferences("KP2A.PluginAccess.hosts", Context.MODE_PRIVATE)
|
||||
if (hostPrefs.contains(hostPackage)) {
|
||||
hostPrefs.edit().remove(hostPackage).apply()
|
||||
}
|
||||
}
|
||||
|
||||
fun getAllHostPackages(ctx: Context): Set<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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
107
app/src/main/java/net/helcel/fidelity/pluginSDK/Kp2aControl.kt
Normal file
107
app/src/main/java/net/helcel/fidelity/pluginSDK/Kp2aControl.kt
Normal file
@ -0,0 +1,107 @@
|
||||
package net.helcel.fidelity.pluginSDK
|
||||
|
||||
import android.content.Intent
|
||||
import android.text.TextUtils
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
object Kp2aControl {
|
||||
/**
|
||||
* Creates and returns an intent to launch Keepass2Android for adding an entry with the given fields.
|
||||
* @param fields Key/Value pairs of the field values. See KeepassDefs for standard keys.
|
||||
* @param protectedFields List of keys of the protected fields.
|
||||
* @return Intent to start Keepass2Android.
|
||||
* @throws JSONException
|
||||
*/
|
||||
fun getAddEntryIntent(
|
||||
fields: HashMap<String?, String?>?,
|
||||
protectedFields: ArrayList<String?>?
|
||||
): Intent {
|
||||
return getAddEntryIntent(JSONObject((fields as Map<*, *>?)!!).toString(), protectedFields)
|
||||
}
|
||||
|
||||
private fun getAddEntryIntent(
|
||||
outputData: String?,
|
||||
protectedFields: ArrayList<String?>?
|
||||
): Intent {
|
||||
val startKp2aIntent = Intent(Strings.ACTION_START_WITH_TASK)
|
||||
startKp2aIntent.addCategory(Intent.CATEGORY_DEFAULT)
|
||||
startKp2aIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
startKp2aIntent.putExtra("KP2A_APPTASK", "CreateEntryThenCloseTask")
|
||||
startKp2aIntent.putExtra("ShowUserNotifications", "false") //KP2A expects a StringExtra
|
||||
startKp2aIntent.putExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA, outputData)
|
||||
if (protectedFields != null) startKp2aIntent.putStringArrayListExtra(
|
||||
Strings.EXTRA_PROTECTED_FIELDS_LIST,
|
||||
protectedFields
|
||||
)
|
||||
|
||||
|
||||
return startKp2aIntent
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates an intent to open a Password Entry matching searchText
|
||||
* @param searchText queryString
|
||||
* @param showUserNotifications if true, the notifications (copy to clipboard, keyboard) are displayed
|
||||
* @param closeAfterOpen if true, the entry is opened and KP2A is immediately closed
|
||||
* @return Intent to start KP2A with
|
||||
*/
|
||||
fun getOpenEntryIntent(
|
||||
searchText: String?,
|
||||
showUserNotifications: Boolean,
|
||||
closeAfterOpen: Boolean
|
||||
): Intent {
|
||||
val startKp2aIntent = Intent(Strings.ACTION_START_WITH_TASK)
|
||||
startKp2aIntent.addCategory(Intent.CATEGORY_DEFAULT)
|
||||
startKp2aIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
startKp2aIntent.putExtra("KP2A_APPTASK", "SearchUrlTask")
|
||||
startKp2aIntent.putExtra("ShowUserNotifications", showUserNotifications.toString())
|
||||
startKp2aIntent.putExtra("CloseAfterCreate", closeAfterOpen.toString())
|
||||
startKp2aIntent.putExtra("UrlToSearch", searchText)
|
||||
return startKp2aIntent
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an intent to query a password entry from KP2A. The credentials are returned as Activity result.
|
||||
* @param searchText Text to search for. Should be a URL or "androidapp://com.my.package."
|
||||
* @return an Intent to start KP2A with
|
||||
*/
|
||||
fun getQueryEntryIntent(searchText: String?): Intent {
|
||||
val i = Intent(Strings.ACTION_QUERY_CREDENTIALS)
|
||||
if (!TextUtils.isEmpty(searchText)) i.putExtra(Strings.EXTRA_QUERY_STRING, searchText)
|
||||
return i
|
||||
}
|
||||
|
||||
val queryEntryIntentForOwnPackage: Intent
|
||||
/**
|
||||
* Creates an intent to query a password entry from KP2A, matching to the current app's package .
|
||||
* The credentials are returned as Activity result.
|
||||
* This requires SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE.
|
||||
* @return an Intent to start KP2A with
|
||||
*/
|
||||
get() = Intent(Strings.ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE)
|
||||
|
||||
/**
|
||||
* Converts the entry fields returned in an intent from a query to a hashmap.
|
||||
* @param intent data received in onActivityResult after getQueryEntryIntent(ForOwnPackage)
|
||||
* @return HashMap with keys = field names (see KeepassDefs for standard keys) and values = values
|
||||
*/
|
||||
fun getEntryFieldsFromIntent(intent: Intent): HashMap<String, String> {
|
||||
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()
|
||||
} catch (e: NullPointerException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return res
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
package net.helcel.fidelity.pluginSDK
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
|
||||
/**
|
||||
* Broadcast flow between Host and Plugin
|
||||
* ======================================
|
||||
*
|
||||
* The host is responsible for deciding when to initiate the session. It
|
||||
* should initiate the session as soon as plugins are required or when a plugin
|
||||
* has been updated through the OS.
|
||||
* It will then send a broadcast to request the currently required scope.
|
||||
* The plugin then sends a broadcast to the app which scope is required. If an
|
||||
* access token is already available, it's sent along with the requset.
|
||||
*
|
||||
* If a previous permission has been revoked (or the app settings cleared or the
|
||||
* permissions have been extended or the token is invalid for any other reason)
|
||||
* the host will answer with a Revoked-Permission broadcast (i.e. the plugin is
|
||||
* unconnected.)
|
||||
*
|
||||
* Unconnected plugins must be permitted by the user (requiring user action).
|
||||
* When the user grants access, the plugin will receive an access token for
|
||||
* the host. This access token is valid for the requested scope. If the scope
|
||||
* changes (e.g after an update of the plugin), the access token becomes invalid.
|
||||
*
|
||||
*/
|
||||
abstract class PluginAccessBroadcastReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(ctx: Context, intent: Intent) {
|
||||
val action = intent.action
|
||||
Log.d(_tag, "received broadcast with action=$action")
|
||||
if (action == null) return
|
||||
when (action) {
|
||||
Strings.ACTION_TRIGGER_REQUEST_ACCESS -> requestAccess(ctx, intent)
|
||||
Strings.ACTION_RECEIVE_ACCESS -> receiveAccess(ctx, intent)
|
||||
Strings.ACTION_REVOKE_ACCESS -> revokeAccess(ctx, intent)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun revokeAccess(ctx: Context, intent: Intent) {
|
||||
val senderPackage = intent.getStringExtra(Strings.EXTRA_SENDER)
|
||||
val accessToken = intent.getStringExtra(Strings.EXTRA_ACCESS_TOKEN)
|
||||
AccessManager.removeAccessToken(ctx, senderPackage!!, accessToken!!)
|
||||
}
|
||||
|
||||
|
||||
private fun receiveAccess(ctx: Context, intent: Intent) {
|
||||
val senderPackage = intent.getStringExtra(Strings.EXTRA_SENDER)
|
||||
val accessToken = intent.getStringExtra(Strings.EXTRA_ACCESS_TOKEN)
|
||||
AccessManager.storeAccessToken(ctx, senderPackage!!, accessToken!!, scopes)
|
||||
}
|
||||
|
||||
private fun requestAccess(ctx: Context, intent: Intent) {
|
||||
val senderPackage = intent.getStringExtra(Strings.EXTRA_SENDER)
|
||||
val requestToken = intent.getStringExtra(Strings.EXTRA_REQUEST_TOKEN)
|
||||
val rpi = Intent(Strings.ACTION_REQUEST_ACCESS)
|
||||
rpi.setPackage(senderPackage)
|
||||
rpi.putExtra(Strings.EXTRA_SENDER, ctx.packageName)
|
||||
rpi.putExtra(Strings.EXTRA_REQUEST_TOKEN, requestToken)
|
||||
|
||||
val token: String? = AccessManager.tryGetAccessToken(ctx, senderPackage!!, scopes)
|
||||
rpi.putExtra(Strings.EXTRA_ACCESS_TOKEN, token)
|
||||
|
||||
rpi.putStringArrayListExtra(Strings.EXTRA_SCOPES, scopes)
|
||||
Log.d(_tag, "requesting access for " + scopes.size + " tokens.")
|
||||
ctx.sendBroadcast(rpi)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return the list of required scopes for this plugin.
|
||||
*/
|
||||
abstract val scopes: ArrayList<String?>
|
||||
|
||||
companion object {
|
||||
private const val _tag = "Kp2aPluginSDK"
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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?) {}
|
||||
}
|
195
app/src/main/java/net/helcel/fidelity/pluginSDK/Strings.kt
Normal file
195
app/src/main/java/net/helcel/fidelity/pluginSDK/Strings.kt
Normal file
@ -0,0 +1,195 @@
|
||||
package net.helcel.fidelity.pluginSDK
|
||||
|
||||
object Strings {
|
||||
/**
|
||||
* Plugin is notified about actions like open/close/update a database.
|
||||
*/
|
||||
const val SCOPE_DATABASE_ACTIONS = "keepass2android.SCOPE_DATABASE_ACTIONS"
|
||||
|
||||
/**
|
||||
* Plugin is notified when an entry is opened.
|
||||
*/
|
||||
const val SCOPE_CURRENT_ENTRY = "keepass2android.SCOPE_CURRENT_ENTRY"
|
||||
|
||||
/**
|
||||
* Plugin may query credentials for its own package
|
||||
*/
|
||||
const val SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE =
|
||||
"keepass2android.SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE"
|
||||
|
||||
/**
|
||||
* Plugin may query credentials for a deliberate package
|
||||
*/
|
||||
const val SCOPE_QUERY_CREDENTIALS = "keepass2android.SCOPE_QUERY_CREDENTIALS"
|
||||
|
||||
/**
|
||||
* Extra key to transfer a (json serialized) list of scopes
|
||||
*/
|
||||
const val EXTRA_SCOPES = "keepass2android.EXTRA_SCOPES"
|
||||
|
||||
|
||||
const val EXTRA_PLUGIN_PACKAGE = "keepass2android.EXTRA_PLUGIN_PACKAGE"
|
||||
|
||||
/**
|
||||
* Extra key for sending the package name of the sender of a broadcast.
|
||||
* Should be set in every broadcast.
|
||||
*/
|
||||
const val EXTRA_SENDER = "keepass2android.EXTRA_SENDER"
|
||||
|
||||
/**
|
||||
* Extra key for sending a request token. The request token is passed from
|
||||
* KP2A to the plugin. It's used in the authorization process.
|
||||
*/
|
||||
const val EXTRA_REQUEST_TOKEN = "keepass2android.EXTRA_REQUEST_TOKEN"
|
||||
|
||||
/**
|
||||
* Action to start KP2A with an AppTask
|
||||
*/
|
||||
const val ACTION_START_WITH_TASK = "keepass2android.ACTION_START_WITH_TASK"
|
||||
|
||||
/**
|
||||
* Action sent from KP2A to the plugin to indicate that the plugin should request
|
||||
* access (sending it's scopes)
|
||||
*/
|
||||
const val ACTION_TRIGGER_REQUEST_ACCESS = "keepass2android.ACTION_TRIGGER_REQUEST_ACCESS"
|
||||
|
||||
/**
|
||||
* Action sent from the plugin to KP2A including the scopes.
|
||||
*/
|
||||
const val ACTION_REQUEST_ACCESS = "keepass2android.ACTION_REQUEST_ACCESS"
|
||||
|
||||
/**
|
||||
* Action sent from the KP2A to the plugin when the user grants access.
|
||||
* Will contain an access token.
|
||||
*/
|
||||
const val ACTION_RECEIVE_ACCESS = "keepass2android.ACTION_RECEIVE_ACCESS"
|
||||
|
||||
/**
|
||||
* Action sent from KP2A to the plugin to indicate that access is not or no longer valid.
|
||||
*/
|
||||
const val ACTION_REVOKE_ACCESS = "keepass2android.ACTION_REVOKE_ACCESS"
|
||||
|
||||
|
||||
/**
|
||||
* Action for startActivity(). Opens an activity in the Plugin Host to edit the plugin settings (i.e. enable it)
|
||||
*/
|
||||
const val ACTION_EDIT_PLUGIN_SETTINGS = "keepass2android.ACTION_EDIT_PLUGIN_SETTINGS"
|
||||
|
||||
/**
|
||||
* Action sent from KP2A to the plugin to indicate that an entry was opened.
|
||||
* The Intent contains the full entry data.
|
||||
*/
|
||||
const val ACTION_OPEN_ENTRY = "keepass2android.ACTION_OPEN_ENTRY"
|
||||
|
||||
/**
|
||||
* Action sent from KP2A to the plugin to indicate that an entry output field was modified/added.
|
||||
* The Intent contains the full new entry data.
|
||||
*/
|
||||
const val ACTION_ENTRY_OUTPUT_MODIFIED = "keepass2android.ACTION_ENTRY_OUTPUT_MODIFIED"
|
||||
|
||||
/**
|
||||
* Action sent from KP2A to the plugin to indicate that an entry activity was closed.
|
||||
*/
|
||||
const val ACTION_CLOSE_ENTRY_VIEW = "keepass2android.ACTION_CLOSE_ENTRY_VIEW"
|
||||
|
||||
/**
|
||||
* Extra key for a string containing the GUID of the entry.
|
||||
*/
|
||||
const val EXTRA_ENTRY_ID = "keepass2android.EXTRA_ENTRY_DATA"
|
||||
|
||||
/**
|
||||
* Json serialized data of the PwEntry (C# class) representing the opened entry.
|
||||
* currently not implemented.
|
||||
*/
|
||||
//const val EXTRA_ENTRY_DATA = "keepass2android.EXTRA_ENTRY_DATA";
|
||||
|
||||
/**
|
||||
* Json serialized list of fields, transformed using the database context (i.e. placeholders are replaced already)
|
||||
*/
|
||||
const val EXTRA_ENTRY_OUTPUT_DATA = "keepass2android.EXTRA_ENTRY_OUTPUT_DATA"
|
||||
|
||||
/**
|
||||
* Json serialized lisf of field keys, specifying which field of the EXTRA_ENTRY_OUTPUT_DATA is protected.
|
||||
*/
|
||||
const val EXTRA_PROTECTED_FIELDS_LIST = "keepass2android.EXTRA_PROTECTED_FIELDS_LIST"
|
||||
|
||||
|
||||
/**
|
||||
* Extra key for passing the access token (both ways)
|
||||
*/
|
||||
const val EXTRA_ACCESS_TOKEN = "keepass2android.EXTRA_ACCESS_TOKEN"
|
||||
|
||||
/**
|
||||
* Action for an intent from the plugin to KP2A to add menu options regarding the currently open entry.
|
||||
* Requires SCOPE_CURRENT_ENTRY.
|
||||
*/
|
||||
const val ACTION_ADD_ENTRY_ACTION = "keepass2android.ACTION_ADD_ENTRY_ACTION"
|
||||
|
||||
const val EXTRA_ACTION_DISPLAY_TEXT = "keepass2android.EXTRA_ACTION_DISPLAY_TEXT"
|
||||
const val EXTRA_ACTION_ICON_RES_ID = "keepass2android.EXTRA_ACTION_ICON_RES_ID"
|
||||
|
||||
const val EXTRA_FIELD_ID = "keepass2android.EXTRA_FIELD_ID"
|
||||
|
||||
/**
|
||||
* Used to pass an id for the action. Each actionId may occur only once per field, otherwise the previous
|
||||
* action with same id is replaced by the new action.
|
||||
*/
|
||||
const val EXTRA_ACTION_ID = "keepass2android.EXTRA_ACTION_ID"
|
||||
|
||||
/** Extra for ACTION_ADD_ENTRY_ACTION and ACTION_ENTRY_ACTION_SELECTED to pass data specifying the action parameters.*/
|
||||
const val EXTRA_ACTION_DATA = "keepass2android.EXTRA_ACTION_DATA"
|
||||
|
||||
/**
|
||||
* Action for an intent from KP2A to the plugin when an action added with ACTION_ADD_ENTRY_ACTION was selected by the user.
|
||||
*
|
||||
*/
|
||||
const val ACTION_ENTRY_ACTION_SELECTED = "keepass2android.ACTION_ENTRY_ACTION_SELECTED"
|
||||
|
||||
/**
|
||||
* Extra key for the string which is used to query the credentials. This should be either a URL for
|
||||
* a web login (google.com or a full URI) or something in the form "androidapp://com.my.package"
|
||||
*/
|
||||
const val EXTRA_QUERY_STRING = "keepass2android.EXTRA_QUERY_STRING"
|
||||
|
||||
/**
|
||||
* Action when plugin wants to query credentials for its own package
|
||||
*/
|
||||
const val ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE =
|
||||
"keepass2android.ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE"
|
||||
|
||||
|
||||
/**
|
||||
* Action when plugin wants to query credentials for a deliberate package
|
||||
* The query string is passed as intent data
|
||||
*/
|
||||
const val ACTION_QUERY_CREDENTIALS = "keepass2android.ACTION_QUERY_CREDENTIALS"
|
||||
|
||||
/**
|
||||
* Action for an intent from the plugin to KP2A to set (i.e. add or update) a field in the entry.
|
||||
* May be used to update existing or add new fields at any time while the entry is opened.
|
||||
*/
|
||||
const val ACTION_SET_ENTRY_FIELD = "keepass2android.ACTION_SET_ENTRY_FIELD"
|
||||
|
||||
/** Actions for an intent from KP2A to the plugin to inform that a database was opened, closed, quicklocked or quickunlocked.*/
|
||||
const val ACTION_OPEN_DATABASE = "keepass2android.ACTION_OPEN_DATABASE"
|
||||
const val ACTION_CLOSE_DATABASE = "keepass2android.ACTION_CLOSE_DATABASE"
|
||||
const val ACTION_LOCK_DATABASE = "keepass2android.ACTION_LOCK_DATABASE"
|
||||
const val ACTION_UNLOCK_DATABASE = "keepass2android.ACTION_UNLOCK_DATABASE"
|
||||
|
||||
/** Extra for ACTION_OPEN_DATABASE and ACTION_CLOSE_DATABASE containing a filepath which is used
|
||||
* by KP2A internally to identify the file. Use only where necessary, might contain credentials
|
||||
* for accessing the file (on remote storage).*/
|
||||
const val EXTRA_DATABASE_FILEPATH = "keepass2android.EXTRA_DATABASE_FILEPATH"
|
||||
|
||||
/** Extra for ACTION_OPEN_DATABASE and ACTION_CLOSE_DATABASE containing a filepath which can be
|
||||
* displayed to the user.*/
|
||||
const val EXTRA_DATABASE_FILE_DISPLAYNAME = "keepass2android.EXTRA_DATABASE_FILE_DISPLAYNAME"
|
||||
|
||||
|
||||
const val EXTRA_FIELD_VALUE = "keepass2android.EXTRA_FIELD_VALUE"
|
||||
const val EXTRA_FIELD_PROTECTED = "keepass2android.EXTRA_FIELD_PROTECTED"
|
||||
|
||||
const val PREFIX_STRING = "STRING_"
|
||||
const val PREFIX_BINARY = "BINARY_"
|
||||
|
||||
}
|
77
app/src/main/java/net/helcel/fidelity/tools/BacodeScanner.kt
Normal file
77
app/src/main/java/net/helcel/fidelity/tools/BacodeScanner.kt
Normal 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
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package net.helcel.fidelity.tools
|
||||
|
||||
import com.google.mlkit.vision.barcode.common.Barcode
|
||||
import com.google.zxing.BarcodeFormat
|
||||
|
||||
object BarcodeFormatConverter {
|
||||
|
||||
fun stringToFormat(f: String): BarcodeFormat {
|
||||
return when (f) {
|
||||
"CODE_128" -> BarcodeFormat.CODE_128
|
||||
"CODE_39" -> BarcodeFormat.CODE_39
|
||||
"CODE_93" -> BarcodeFormat.CODE_93
|
||||
"EAN_8" -> BarcodeFormat.EAN_8
|
||||
"EAN_13" -> BarcodeFormat.EAN_13
|
||||
"CODE_QR" -> BarcodeFormat.QR_CODE
|
||||
"UPC_A" -> BarcodeFormat.UPC_A
|
||||
"UPC_E" -> BarcodeFormat.UPC_E
|
||||
"PDF_417" -> BarcodeFormat.PDF_417
|
||||
else -> throw Exception("Unsupported Format: $f")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun formatToString(f: Int): String {
|
||||
return when (f) {
|
||||
Barcode.FORMAT_CODE_128 -> "CODE_128"
|
||||
Barcode.FORMAT_CODE_39 -> "CODE_39"
|
||||
Barcode.FORMAT_CODE_93 -> "CODE_93"
|
||||
Barcode.FORMAT_EAN_8 -> "EAN_8"
|
||||
Barcode.FORMAT_EAN_13 -> "EAN_13"
|
||||
Barcode.FORMAT_QR_CODE -> "CODE_QR"
|
||||
Barcode.FORMAT_UPC_A -> "UPC_A"
|
||||
Barcode.FORMAT_UPC_E -> "UPC_E"
|
||||
Barcode.FORMAT_PDF417 -> "PDF_417"
|
||||
else -> throw Exception("Unsupported Format: $f")
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
package net.helcel.fidelity.tools
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.MultiFormatWriter
|
||||
import com.google.zxing.WriterException
|
||||
import com.google.zxing.common.BitMatrix
|
||||
import net.helcel.fidelity.tools.BarcodeFormatConverter.stringToFormat
|
||||
|
||||
object BarcodeGenerator {
|
||||
|
||||
private fun getPixelColor(bitMatrix: BitMatrix, x: Int, y: Int): Int {
|
||||
if (x >= bitMatrix.width || y >= bitMatrix.height)
|
||||
return android.graphics.Color.WHITE
|
||||
|
||||
return if (bitMatrix[x, y])
|
||||
android.graphics.Color.BLACK
|
||||
else
|
||||
android.graphics.Color.WHITE
|
||||
}
|
||||
|
||||
fun generateBarcode(content: String, f: String, width: Int): Bitmap? {
|
||||
if (content.isEmpty() || f.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
val format = stringToFormat(f)
|
||||
val writer = MultiFormatWriter()
|
||||
val height = (formatToRatio(format) * width).toInt()
|
||||
val bitMatrix: BitMatrix = writer.encode(content, format, width, height)
|
||||
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
|
||||
for (x in 0 until width) {
|
||||
for (y in 0 until height) {
|
||||
bitmap.setPixel(
|
||||
x,
|
||||
y,
|
||||
getPixelColor(bitMatrix, x, y)
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
return bitmap
|
||||
} catch (e: WriterException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun formatToRatio(format: BarcodeFormat): Double {
|
||||
return when (format) {
|
||||
BarcodeFormat.QR_CODE -> 1.0
|
||||
BarcodeFormat.PDF_417 -> 0.4
|
||||
else -> 0.5
|
||||
}
|
||||
}
|
||||
}
|
45
app/src/main/java/net/helcel/fidelity/tools/CacheManager.kt
Normal file
45
app/src/main/java/net/helcel/fidelity/tools/CacheManager.kt
Normal file
@ -0,0 +1,45 @@
|
||||
package net.helcel.fidelity.tools
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
|
||||
|
||||
object CacheManager {
|
||||
|
||||
const val PREF_NAME = "FIDELITY"
|
||||
private const val ENTRY_KEY = "FIDELITY"
|
||||
private var data: ArrayList<Triple<String?, String?, String?>> = ArrayList()
|
||||
private var pref: SharedPreferences? = null
|
||||
|
||||
fun addFidelity(item: Triple<String?, String?, String?>) {
|
||||
val exists = data.find { it.first == item.first }
|
||||
if (exists != null)
|
||||
data.remove(exists)
|
||||
|
||||
data.add(0, item)
|
||||
saveFidelity()
|
||||
}
|
||||
|
||||
private fun saveFidelity() {
|
||||
val editor = pref?.edit()
|
||||
val gson = Gson()
|
||||
val json = gson.toJson(data)
|
||||
editor?.putString(ENTRY_KEY, json)
|
||||
editor?.apply()
|
||||
}
|
||||
|
||||
fun loadFidelity(pref: SharedPreferences) {
|
||||
this.pref = pref
|
||||
val gson = Gson()
|
||||
val json = pref.getString(ENTRY_KEY, null)
|
||||
val type = object : TypeToken<List<Triple<String, String, Int>>>() {}.type
|
||||
data = gson.fromJson(json, type) ?: ArrayList()
|
||||
|
||||
}
|
||||
|
||||
fun getFidelity(): ArrayList<Triple<String?, String?, String?>> {
|
||||
return data
|
||||
}
|
||||
|
||||
}
|
22
app/src/main/java/net/helcel/fidelity/tools/ErrorToaster.kt
Normal file
22
app/src/main/java/net/helcel/fidelity/tools/ErrorToaster.kt
Normal file
@ -0,0 +1,22 @@
|
||||
package net.helcel.fidelity.tools
|
||||
|
||||
import android.app.Activity
|
||||
import android.widget.Toast
|
||||
|
||||
object ErrorToaster {
|
||||
private fun helper(activity: Activity, message: String, length: Int) {
|
||||
Toast.makeText(activity, message, length).show()
|
||||
}
|
||||
|
||||
fun noKP2AFound(activity: Activity) {
|
||||
helper(activity, "KeePass2Android Not Installed", Toast.LENGTH_LONG)
|
||||
}
|
||||
|
||||
fun formIncomplete(activity: Activity) {
|
||||
helper(activity, "Form Incomplete", Toast.LENGTH_SHORT)
|
||||
}
|
||||
|
||||
fun invalidFormat(activity: Activity) {
|
||||
helper(activity, "Invalid Format", Toast.LENGTH_SHORT)
|
||||
}
|
||||
}
|
103
app/src/main/java/net/helcel/fidelity/tools/KeepassWrapper.kt
Normal file
103
app/src/main/java/net/helcel/fidelity/tools/KeepassWrapper.kt
Normal file
@ -0,0 +1,103 @@
|
||||
package net.helcel.fidelity.tools
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.fragment.app.Fragment
|
||||
import net.helcel.fidelity.pluginSDK.KeepassDefs
|
||||
import net.helcel.fidelity.pluginSDK.Kp2aControl
|
||||
|
||||
object KeepassWrapper {
|
||||
|
||||
private const val CODE_FIELD: String = "FidelityCode"
|
||||
private const val FORMAT_FIELD: String = "FidelityFormat"
|
||||
private const val PROTECT_CODE_FIELD: String = "FidelityProtectedCode"
|
||||
|
||||
fun entryCreate(
|
||||
fragment: Fragment,
|
||||
title: String,
|
||||
code: String,
|
||||
format: String,
|
||||
protectCode: Boolean,
|
||||
): Pair<HashMap<String?, String?>, ArrayList<String?>> {
|
||||
|
||||
val fields = HashMap<String?, String?>()
|
||||
val protected = ArrayList<String?>()
|
||||
fields[KeepassDefs.TitleField] = title
|
||||
fields[KeepassDefs.UrlField] =
|
||||
"androidapp://" + fragment.requireActivity().packageName
|
||||
fields[CODE_FIELD] = code
|
||||
fields[FORMAT_FIELD] = format
|
||||
fields[PROTECT_CODE_FIELD] = protectCode.toString()
|
||||
|
||||
if (protectCode) {
|
||||
protected.add(CODE_FIELD)
|
||||
}
|
||||
return Pair(fields, protected)
|
||||
}
|
||||
|
||||
|
||||
fun resultLauncherAdd(
|
||||
fragment: Fragment,
|
||||
callback: (HashMap<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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun entryExtract(map: HashMap<String, String>): Triple<String?, String?, String?> {
|
||||
return Triple(
|
||||
map[KeepassDefs.TitleField],
|
||||
map[CODE_FIELD],
|
||||
map[FORMAT_FIELD]
|
||||
)
|
||||
}
|
||||
|
||||
fun bundleCreate(title: String?, code: String?, fmt: String?): Bundle {
|
||||
val data = Bundle()
|
||||
data.putString("title", title)
|
||||
data.putString("code", code)
|
||||
data.putString("fmt", fmt)
|
||||
return data
|
||||
}
|
||||
|
||||
fun bundleExtract(data: Bundle?): Triple<String?, String?, String?> {
|
||||
return Triple(
|
||||
data?.getString("title"),
|
||||
data?.getString("code"),
|
||||
data?.getString("fmt")
|
||||
)
|
||||
}
|
||||
|
||||
fun isProtected(map: HashMap<String, String>): Boolean {
|
||||
return map[PROTECT_CODE_FIELD].toBoolean()
|
||||
}
|
||||
|
||||
|
||||
}
|
120
app/src/main/res/drawable/barcode.xml
Normal file
120
app/src/main/res/drawable/barcode.xml
Normal file
@ -0,0 +1,120 @@
|
||||
<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="M9,21V52"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M12,21V52"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M20,21V50"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M28,21V50"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M15,50V21H17V50H15Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#000"
|
||||
android:strokeColor="#000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M23,50V21H25V50H23Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#000"
|
||||
android:strokeColor="#000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M31,50V21H32V50H31Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#000"
|
||||
android:strokeColor="#000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M46,21V50"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M49,21V50"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M57,21V50"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M41,50V21H43V50H41Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#000"
|
||||
android:strokeColor="#000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M52,50V21H54V50H52Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#000"
|
||||
android:strokeColor="#000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M60,21V52"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M63,21V52"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M35,21V52"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M38,21V52"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"
|
||||
android:strokeLineCap="round"/>
|
||||
</group>
|
||||
</vector>
|
35
app/src/main/res/drawable/bookmark.xml
Normal file
35
app/src/main/res/drawable/bookmark.xml
Normal file
@ -0,0 +1,35 @@
|
||||
<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="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>
|
||||
</vector>
|
50
app/src/main/res/drawable/camera.xml
Normal file
50
app/src/main/res/drawable/camera.xml
Normal file
@ -0,0 +1,50 @@
|
||||
<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="m15.27,22.918c-1.737,0 -3.144,1.371 -3.144,3.062v23.221c0,1.691 1.408,3.062 3.144,3.062h41.661c1.737,0 3.144,-1.371 3.144,-3.062v-23.221c0,-1.691 -1.408,-3.062 -3.144,-3.062 0,0 -41.661,0 -41.661,0Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M47.152,33.197L59.618,33.197"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M12.582,33.197L20.629,33.197"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M33.852,37.591m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M33.852,37.591m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="m27.831,19.12v-1.078c0,-0.564 -0.469,-1.021 -1.048,-1.021h-7.677c-0.579,0 -1.048,0.457 -1.048,1.021v1.078"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"
|
||||
android:strokeLineCap="round"/>
|
||||
</group>
|
||||
</vector>
|
15
app/src/main/res/drawable/card.xml
Normal file
15
app/src/main/res/drawable/card.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<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="#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>
|
||||
</vector>
|
15
app/src/main/res/drawable/cross.xml
Normal file
15
app/src/main/res/drawable/cross.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<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="M31,31l0,-18l10,0l0,18l18,0l0,10l-18,0l0,18l-10,0l0,-18l-18,0l0,-10z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
</group>
|
||||
</vector>
|
44
app/src/main/res/drawable/edit.xml
Normal file
44
app/src/main/res/drawable/edit.xml
Normal file
@ -0,0 +1,44 @@
|
||||
<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="M18.63,56.82l9.197,-3.526l25.993,-25.993l-9.9,-9.899l-25.993,25.993l-3.538,9.209z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M47.335,13.987l3.503,-3.503l9.9,9.899l-3.474,3.474"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M18.556,42.766L28.456,52.666"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M14.398,52.582l-2.491,6.733l6.748,-2.506"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M36.91,25.007L46.423,34.52"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
</group>
|
||||
</vector>
|
21
app/src/main/res/drawable/heart.xml
Normal file
21
app/src/main/res/drawable/heart.xml
Normal 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>
|
31
app/src/main/res/drawable/key.xml
Normal file
31
app/src/main/res/drawable/key.xml
Normal 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>
|
7
app/src/main/res/drawable/lock_checkbox.xml
Normal file
7
app/src/main/res/drawable/lock_checkbox.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item android:drawable="@drawable/unlocked" android:state_checked="false" />
|
||||
<item android:drawable="@drawable/locked_fill" android:state_checked="true" />
|
||||
|
||||
</selector>
|
31
app/src/main/res/drawable/locked.xml
Normal file
31
app/src/main/res/drawable/locked.xml
Normal 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>
|
46
app/src/main/res/drawable/locked_fill.xml
Normal file
46
app/src/main/res/drawable/locked_fill.xml
Normal file
@ -0,0 +1,46 @@
|
||||
<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="M21.375,31.175c-0.35,-8.771 6.449,-18.539 14.387,-18.779c8.005,-0.242 16.541,10.97 14.333,19.052h-4.039c0,0 1.562,-7.922 -2.216,-11.253c-1.849,-1.631 -5.256,-4.771 -8.64,-4.292c-2.283,0.323 -6.868,3.452 -7.927,5.421c-2.064,3.837 -1.725,9.817 -1.725,9.817L21.375,31.175z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#D0CFCE"
|
||||
android:strokeColor="#D0CFCE"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M53,32.297l1.875,0l0,26.875l-38,0l0,-26.875l1.875,0z"
|
||||
android:fillColor="#FCEA2B"
|
||||
android:strokeColor="#00000000"/>
|
||||
<path
|
||||
android:pathData="M54.43,32.493l-18.77,26.501l19.307,0z"
|
||||
android:fillColor="#F1B31C"
|
||||
android:strokeColor="#00000000"/>
|
||||
<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>
|
21
app/src/main/res/drawable/logo.xml
Normal file
21
app/src/main/res/drawable/logo.xml
Normal file
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:width="128dp"
|
||||
android:height="128dp"
|
||||
android:gravity="center"
|
||||
android:drawable="@drawable/card" />
|
||||
<item
|
||||
android:width="64dp"
|
||||
android:height="64dp"
|
||||
android:drawable="@drawable/barcode"
|
||||
android:gravity="center"
|
||||
android:right="32dp" />
|
||||
<item
|
||||
android:width="52dp"
|
||||
android:height="52dp"
|
||||
android:drawable="@drawable/bookmark"
|
||||
android:gravity="center"
|
||||
android:left="72dp"
|
||||
android:bottom="20dp" />
|
||||
</layer-list>
|
15
app/src/main/res/drawable/minus.xml
Normal file
15
app/src/main/res/drawable/minus.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<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="M13,31h46v10h-46z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
</group>
|
||||
</vector>
|
36
app/src/main/res/drawable/save.xml
Normal file
36
app/src/main/res/drawable/save.xml
Normal file
@ -0,0 +1,36 @@
|
||||
<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="M11.136,11h50v50h-50z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M56.136,60l0,-24.838l-40,0l0,24.838"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M16.136,12l0,17.607l30.913,0l0,-17.607"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M19.956,38.96h32.031v5.771h-32.031z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
</group>
|
||||
</vector>
|
28
app/src/main/res/drawable/search.xml
Normal file
28
app/src/main/res/drawable/search.xml
Normal file
@ -0,0 +1,28 @@
|
||||
<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="M36.097,37.814a14.637,14.637 77.541,1 0,15.747 -24.678a14.637,14.637 77.541,1 0,-15.747 24.678z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M37.964,34.887a11.166,11.166 122.551,1 0,12.012 -18.825a11.166,11.166 122.551,1 0,-12.012 18.825z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M31.31,39.966l4.406,2.811L25.22,59.227c-0.75,1.175 -2.344,1.499 -3.561,0.722l0,0c-1.217,-0.776 -1.595,-2.358 -0.845,-3.533L31.31,39.966z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/></group>
|
||||
</vector>
|
24
app/src/main/res/drawable/unlocked.xml
Normal file
24
app/src/main/res/drawable/unlocked.xml
Normal file
@ -0,0 +1,24 @@
|
||||
<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="M46.274,28.915c0.477,-6.985 -4.83,-12.683 -10.514,-12.493c-4.673,0.156 -8.616,4.285 -9.828,9.301l-4.05,-0.903c1.661,-6.692 7.219,-12.221 13.812,-12.428c7.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"/>
|
||||
</group>
|
||||
</vector>
|
18
app/src/main/res/layout/act_main.xml
Normal file
18
app/src/main/res/layout/act_main.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/coordinator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
android:orientation="vertical"
|
||||
tools:context=".activity.MainActivity">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:ignore="MergeRootFrame" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
106
app/src/main/res/layout/frag_create_entry.xml
Normal file
106
app/src/main/res/layout/frag_create_entry.xml
Normal file
@ -0,0 +1,106 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
tools:context=".activity.fragment.CreateEntry">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/nameInputLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:hint="@string/title">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/editTextTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/codeInputLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/code"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/checkboxProtected"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/editTextCode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
|
||||
<com.google.android.material.checkbox.MaterialCheckBox
|
||||
android:id="@+id/checkboxProtected"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:button="@drawable/lock_checkbox"
|
||||
android:scaleX="0.40"
|
||||
android:scaleY="0.40"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/codeInputLayout"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/formatInputLayout"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:hint="@string/format"
|
||||
android:labelFor="@id/edit_text_format">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/edit_text_format"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:inputType="none" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageViewPreview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:contentDescription="@string/barcode_preview"
|
||||
android:scaleType="fitCenter" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/btnSave"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_margin="24dp"
|
||||
android:contentDescription="@string/save"
|
||||
app:fabCustomSize="46dp"
|
||||
app:maxImageSize="32dp"
|
||||
app:srcCompat="@drawable/save" />
|
||||
</RelativeLayout>
|
83
app/src/main/res/layout/frag_launcher.xml
Normal file
83
app/src/main/res/layout/frag_launcher.xml
Normal file
@ -0,0 +1,83 @@
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
tools:context=".activity.fragment.Launcher">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/fidelityList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/btnQuery"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="24dp"
|
||||
android:contentDescription="@string/query"
|
||||
app:fabCustomSize="46dp"
|
||||
app:maxImageSize="32dp"
|
||||
app:srcCompat="@drawable/search" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/expandedMenuContainer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_margin="16dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:orientation="vertical"
|
||||
tools:ignore="RelativeOverlap">
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/menuAdd"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/btnScan"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:contentDescription="@string/scan"
|
||||
app:fabCustomSize="46dp"
|
||||
app:maxImageSize="32dp"
|
||||
app:srcCompat="@drawable/camera" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/btnManual"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:contentDescription="@string/manual"
|
||||
app:fabCustomSize="46dp"
|
||||
app:maxImageSize="32dp"
|
||||
app:srcCompat="@drawable/edit" />
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/btnAdd"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="8dp"
|
||||
android:contentDescription="@string/expand"
|
||||
app:fabCustomSize="46dp"
|
||||
app:maxImageSize="32dp" />
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
22
app/src/main/res/layout/frag_scanner.xml
Normal file
22
app/src/main/res/layout/frag_scanner.xml
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
tools:context=".activity.fragment.Scanner">
|
||||
|
||||
<androidx.camera.view.PreviewView
|
||||
android:id="@+id/cameraView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/bottomText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:background="#ffffff"
|
||||
android:textSize="24sp" />
|
||||
|
||||
</RelativeLayout>
|
39
app/src/main/res/layout/frag_view_entry.xml
Normal file
39
app/src/main/res/layout/frag_view_entry.xml
Normal file
@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp"
|
||||
tools:context=".activity.fragment.ViewEntry">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:hint="@string/title"
|
||||
android:textAlignment="center"
|
||||
android:textSize="42sp"
|
||||
app:layout_constraintBottom_toTopOf="@id/imageViewPreview"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.0" />
|
||||
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageViewPreview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:contentDescription="@string/barcode_preview"
|
||||
android:scaleType="fitCenter"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/title" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
10
app/src/main/res/layout/list_item_dropdown.xml
Normal file
10
app/src/main/res/layout/list_item_dropdown.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/textViewFeelings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="15dp"
|
||||
android:text=""
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
30
app/src/main/res/layout/list_item_fidelity.xml
Normal file
30
app/src/main/res/layout/list_item_fidelity.xml
Normal file
@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/card"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="4dp"
|
||||
app:cardMaxElevation="4dp"
|
||||
app:cardPreventCornerOverlap="false"
|
||||
app:cardUseCompatPadding="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
3
app/src/main/res/values/dimens.xml
Normal file
3
app/src/main/res/values/dimens.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
|
||||
</resources>
|
29
app/src/main/res/values/strings.xml
Normal file
29
app/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="kp2aplugin_title" tools:keep="@string/kp2aplugin_title">Fidelity</string>
|
||||
<string name="kp2aplugin_shortdesc">Stores and Displays fidelity and other cards</string>
|
||||
<string name="kp2aplugin_author" tools:keep="@string/kp2aplugin_author">[soraefir](soraefir)</string>
|
||||
|
||||
<string name="app_name">Keepass Fidelity</string>
|
||||
|
||||
<string name="barcode_preview">barcode preview</string>
|
||||
<string name="expand">Expand</string>
|
||||
<string name="manual">Manual</string>
|
||||
<string name="scan">Scan</string>
|
||||
<string name="query">Query</string>
|
||||
<string name="title">Title</string>
|
||||
<string name="code">Code</string>
|
||||
<string name="format">Format</string>
|
||||
<string name="save">Save</string>
|
||||
<string-array name="format_array">
|
||||
<item>CODE_128</item>
|
||||
<item>CODE_39</item>
|
||||
<item>CODE_93</item>
|
||||
<item>EAN_8</item>
|
||||
<item>EAN_13</item>
|
||||
<item>CODE_QR</item>
|
||||
<item>UPC_A</item>
|
||||
<item>UPC_E</item>
|
||||
<item>PDF_417</item>
|
||||
</string-array>
|
||||
</resources>
|
3
app/src/main/res/values/styles.xml
Normal file
3
app/src/main/res/values/styles.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
|
||||
</resources>
|
9
app/src/main/res/values/themes.xml
Normal file
9
app/src/main/res/values/themes.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.Fidelity" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
|
||||
<item name="colorPrimary">?attr/colorAccent</item>
|
||||
|
||||
</style>
|
||||
</resources>
|
7
build.gradle
Normal file
7
build.gradle
Normal file
@ -0,0 +1,7 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
plugins {
|
||||
id 'com.android.application' version '8.3.0' apply false
|
||||
id 'com.android.library' version '8.3.0' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.9.23' apply false
|
||||
}
|
24
gradle.properties
Normal file
24
gradle.properties
Normal file
@ -0,0 +1,24 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
#Sat Mar 09 17:54:03 GMT 2024
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
164
gradlew
vendored
Executable file
164
gradlew
vendored
Executable file
@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn ( ) {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die ( ) {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
esac
|
||||
|
||||
# For Cygwin, ensure paths are in UNIX format before anything is touched.
|
||||
if $cygwin ; then
|
||||
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
|
||||
fi
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >&-
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >&-
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
|
||||
function splitJvmOpts() {
|
||||
JVM_OPTS=("$@")
|
||||
}
|
||||
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
|
||||
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
|
||||
|
||||
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
|
90
gradlew.bat
vendored
Normal file
90
gradlew.bat
vendored
Normal file
@ -0,0 +1,90 @@
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windowz variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
if "%@eval[2+2]" == "4" goto 4NT_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
goto execute
|
||||
|
||||
:4NT_args
|
||||
@rem Get arguments from the 4NT Shell from JP Software
|
||||
set CMD_LINE_ARGS=%$
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
18
settings.gradle
Normal file
18
settings.gradle
Normal file
@ -0,0 +1,18 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
}
|
||||
rootProject.name = "BeenDroid"
|
||||
include ':app'
|
Loading…
x
Reference in New Issue
Block a user