gitignore & init
This commit is contained in:
@ -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()
|
||||
}
|
||||
|
||||
|
||||
}
|
Reference in New Issue
Block a user