Compare commits
1 Commits
1.0-rc3
...
6c9be64030
Author | SHA1 | Date | |
---|---|---|---|
6c9be64030 |
46
.github/workflows/build.yml
vendored
46
.github/workflows/build.yml
vendored
@ -1,46 +0,0 @@
|
|||||||
|
|
||||||
name: CI-Android APK
|
|
||||||
|
|
||||||
env:
|
|
||||||
main_project_module: app
|
|
||||||
playstore_name: KeepassFidelity
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ release ]
|
|
||||||
tags:
|
|
||||||
- '**'
|
|
||||||
pull_request:
|
|
||||||
branches: [ release ]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: gradle/wrapper-validation-action@v2
|
|
||||||
|
|
||||||
- name: create and checkout branch
|
|
||||||
if: github.event_name == 'pull_request'
|
|
||||||
env:
|
|
||||||
BRANCH: ${{ github.head_ref }}
|
|
||||||
run: git checkout -B "$BRANCH"
|
|
||||||
|
|
||||||
- name: set up JDK
|
|
||||||
uses: actions/setup-java@v4
|
|
||||||
with:
|
|
||||||
java-version: 17
|
|
||||||
distribution: "temurin"
|
|
||||||
cache: 'gradle'
|
|
||||||
|
|
||||||
- name: Build APK
|
|
||||||
run: ./gradlew assemble
|
|
||||||
|
|
||||||
- name: Upload APK
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: app
|
|
||||||
path: app/build/outputs/apk/release/*.apk
|
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -7,8 +7,6 @@ local.properties/
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
build/
|
build/
|
||||||
app/build/
|
app/build/
|
||||||
app/debug/
|
|
||||||
app/release/
|
|
||||||
captures/
|
captures/
|
||||||
.externalNativeBuild
|
.externalNativeBuild
|
||||||
.cxx
|
.cxx
|
||||||
|
@ -11,7 +11,6 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId 'net.helcel.fidelity'
|
applicationId 'net.helcel.fidelity'
|
||||||
resValue "string", "app_name", "Keepass Fideity"
|
|
||||||
minSdk 28
|
minSdk 28
|
||||||
targetSdk 34
|
targetSdk 34
|
||||||
versionCode 1
|
versionCode 1
|
||||||
@ -19,29 +18,18 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
|
||||||
debuggable true
|
|
||||||
}
|
|
||||||
release {
|
release {
|
||||||
minifyEnabled true
|
minifyEnabled false
|
||||||
shrinkResources false
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
coreLibraryDesugaringEnabled true
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
sourceCompatibility JavaVersion.VERSION_17
|
|
||||||
targetCompatibility JavaVersion.VERSION_17
|
|
||||||
encoding 'utf-8'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = JavaVersion.VERSION_17
|
jvmTarget = '1.8'
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding true
|
viewBinding true
|
||||||
}
|
}
|
||||||
@ -49,12 +37,12 @@ android {
|
|||||||
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.4'
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
implementation 'androidx.core:core-ktx:1.12.0'
|
implementation 'androidx.core:core-ktx:1.12.0'
|
||||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
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-camera2:1.3.2'
|
||||||
implementation 'androidx.camera:camera-lifecycle:1.3.2'
|
implementation 'androidx.camera:camera-lifecycle:1.3.2'
|
||||||
implementation 'androidx.camera:camera-view:1.3.2'
|
implementation 'androidx.camera:camera-view:1.3.2'
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:versionCode="1"
|
android:versionCode="1"
|
||||||
android:versionName="1.0">
|
android:versionName="1.0">
|
||||||
|
|
||||||
@ -21,14 +20,30 @@
|
|||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".pluginSDK.PluginAccessBroadcastReceiver"
|
android:name=".pluginSDK.PluginAccessReceiver"
|
||||||
android:exported="true"
|
android:exported="true">
|
||||||
tools:ignore="ExportedReceiver">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="keepass2android.ACTION_TRIGGER_REQUEST_ACCESS" />
|
<action android:name="keepass2android.ACTION_TRIGGER_REQUEST_ACCESS" />
|
||||||
<action android:name="keepass2android.ACTION_RECEIVE_ACCESS" />
|
<action android:name="keepass2android.ACTION_RECEIVE_ACCESS" />
|
||||||
<action android:name="keepass2android.ACTION_REVOKE_ACCESS" />
|
<action android:name="keepass2android.ACTION_REVOKE_ACCESS" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".pluginSDK.PluginActionBroadcastReceiver"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="keepass2android.ACTION_OPEN_ENTRY" />
|
||||||
|
<action android:name="keepass2android.ACTION_CLOSE_ENTRY_VIEW" />
|
||||||
|
<action android:name="keepass2android.ACTION_ENTRY_ACTION_SELECTED" />
|
||||||
|
|
||||||
|
<action android:name="keepass2android.ACTION_LOCK_DATABASE" />
|
||||||
|
<action android:name="keepass2android.ACTION_UNLOCK_DATABASE" />
|
||||||
|
<action android:name="keepass2android.ACTION_CLOSE_DATABASE" />
|
||||||
|
<action android:name="keepass2android.ACTION_OPEN_DATABASE" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
@ -1,25 +1,19 @@
|
|||||||
package net.helcel.fidelity.activity
|
package net.helcel.fidelity.activity
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.content.pm.ActivityInfo
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.addCallback
|
import androidx.activity.addCallback
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import net.helcel.fidelity.R
|
import net.helcel.fidelity.R
|
||||||
import net.helcel.fidelity.activity.fragment.Launcher
|
import net.helcel.fidelity.activity.fragment.Launcher
|
||||||
import net.helcel.fidelity.activity.fragment.ViewEntry
|
|
||||||
import net.helcel.fidelity.databinding.ActMainBinding
|
import net.helcel.fidelity.databinding.ActMainBinding
|
||||||
import net.helcel.fidelity.pluginSDK.Kp2aControl.getEntryFieldsFromIntent
|
|
||||||
import net.helcel.fidelity.tools.CacheManager
|
import net.helcel.fidelity.tools.CacheManager
|
||||||
import net.helcel.fidelity.tools.KeepassWrapper.bundleCreate
|
|
||||||
import net.helcel.fidelity.tools.KeepassWrapper.entryExtract
|
|
||||||
|
|
||||||
@SuppressLint("SourceLockedOrientationActivity")
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private lateinit var binding: ActMainBinding
|
private lateinit var binding: ActMainBinding
|
||||||
|
|
||||||
private lateinit var sharedPreferences: SharedPreferences
|
private lateinit var sharedPreferences: SharedPreferences
|
||||||
|
|
||||||
|
|
||||||
@ -29,37 +23,25 @@ class MainActivity : AppCompatActivity() {
|
|||||||
this.getSharedPreferences(CacheManager.PREF_NAME, Context.MODE_PRIVATE)
|
this.getSharedPreferences(CacheManager.PREF_NAME, Context.MODE_PRIVATE)
|
||||||
CacheManager.loadFidelity(sharedPreferences)
|
CacheManager.loadFidelity(sharedPreferences)
|
||||||
|
|
||||||
|
|
||||||
binding = ActMainBinding.inflate(layoutInflater)
|
binding = ActMainBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
onBackPressedDispatcher.addCallback(this) {
|
onBackPressedDispatcher.addCallback(this) {
|
||||||
if (supportFragmentManager.backStackEntryCount > 0) {
|
if (supportFragmentManager.backStackEntryCount > 0) {
|
||||||
supportFragmentManager.popBackStackImmediate()
|
supportFragmentManager.popBackStackImmediate()
|
||||||
loadLauncher()
|
|
||||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
|
||||||
} else {
|
} else {
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (savedInstanceState == null)
|
||||||
if (intent.extras != null)
|
|
||||||
loadViewEntry()
|
|
||||||
else if (savedInstanceState == null)
|
|
||||||
loadLauncher()
|
loadLauncher()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadLauncher() {
|
private fun loadLauncher() {
|
||||||
supportFragmentManager.beginTransaction()
|
supportFragmentManager.beginTransaction()
|
||||||
.replace(R.id.container, Launcher())
|
.add(R.id.container, Launcher())
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadViewEntry() {
|
|
||||||
val viewEntry = ViewEntry()
|
|
||||||
val data = getEntryFieldsFromIntent(intent)
|
|
||||||
viewEntry.arguments = bundleCreate(entryExtract(data))
|
|
||||||
supportFragmentManager.beginTransaction()
|
|
||||||
.replace(R.id.container, viewEntry)
|
|
||||||
.commit()
|
.commit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,7 @@ class FidelityListAdapter(
|
|||||||
|
|
||||||
inner class FidelityViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
inner class FidelityViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
|
||||||
|
|
||||||
fun bind(triple: Triple<String?, String?, String?>) {
|
fun bind(triple: Triple<String?, String?, String?>) {
|
||||||
val text = "${triple.first}"
|
val text = "${triple.first}"
|
||||||
binding.textView.text = text
|
binding.textView.text = text
|
||||||
|
@ -7,11 +7,9 @@ import android.os.Looper
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.inputmethod.EditorInfo
|
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import androidx.core.widget.addTextChangedListener
|
import androidx.core.widget.addTextChangedListener
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
|
||||||
import com.google.zxing.FormatException
|
import com.google.zxing.FormatException
|
||||||
import net.helcel.fidelity.R
|
import net.helcel.fidelity.R
|
||||||
import net.helcel.fidelity.databinding.FragCreateEntryBinding
|
import net.helcel.fidelity.databinding.FragCreateEntryBinding
|
||||||
@ -28,7 +26,7 @@ class CreateEntry : Fragment() {
|
|||||||
private val handler = Handler(Looper.getMainLooper())
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
private lateinit var binding: FragCreateEntryBinding
|
private lateinit var binding: FragCreateEntryBinding
|
||||||
|
|
||||||
private val resultLauncherAdd = KeepassWrapper.resultLauncher(this) {
|
private val resultLauncherAdd = KeepassWrapper.resultLauncherAdd(this) {
|
||||||
val r = KeepassWrapper.entryExtract(it)
|
val r = KeepassWrapper.entryExtract(it)
|
||||||
if (!KeepassWrapper.isProtected(it)) {
|
if (!KeepassWrapper.isProtected(it)) {
|
||||||
CacheManager.addFidelity(r)
|
CacheManager.addFidelity(r)
|
||||||
@ -36,7 +34,7 @@ class CreateEntry : Fragment() {
|
|||||||
startViewEntry(r.first, r.second, r.third)
|
startViewEntry(r.first, r.second, r.third)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isValidBarcode: Boolean = false
|
private var isValid: Boolean = false
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
@ -53,14 +51,41 @@ class CreateEntry : Fragment() {
|
|||||||
binding.editTextCode.setText(res.second)
|
binding.editTextCode.setText(res.second)
|
||||||
binding.editTextFormat.setText(res.third, false)
|
binding.editTextFormat.setText(res.third, false)
|
||||||
|
|
||||||
|
val changeListener = {
|
||||||
|
isValid = false
|
||||||
|
handler.removeCallbacksAndMessages(null)
|
||||||
|
handler.postDelayed({
|
||||||
|
updatePreview()
|
||||||
|
}, DEBOUNCE_DELAY)
|
||||||
|
}
|
||||||
|
|
||||||
binding.editTextCode.addTextChangedListener { changeListener() }
|
binding.editTextCode.addTextChangedListener { changeListener() }
|
||||||
binding.editTextFormat.addTextChangedListener { changeListener() }
|
binding.editTextFormat.addTextChangedListener { changeListener() }
|
||||||
binding.editTextFormat.addTextChangedListener { binding.editTextFormat.error = null }
|
binding.editTextFormat.addTextChangedListener { binding.editTextFormat.error = null }
|
||||||
binding.btnSave.setOnClickListener { submit() }
|
binding.btnSave.setOnClickListener {
|
||||||
|
if (!isValid() || !isValid) {
|
||||||
binding.editTextTitle.onDone { submit() }
|
ErrorToaster.formIncomplete(requireActivity())
|
||||||
binding.editTextCode.onDone { submit() }
|
|
||||||
|
|
||||||
|
} else {
|
||||||
|
val kpentry = KeepassWrapper.entryCreate(
|
||||||
|
this,
|
||||||
|
binding.editTextTitle.text.toString(),
|
||||||
|
binding.editTextCode.text.toString(),
|
||||||
|
binding.editTextFormat.text.toString(),
|
||||||
|
binding.checkboxProtected.isChecked,
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
resultLauncherAdd.launch(
|
||||||
|
Kp2aControl.getAddEntryIntent(
|
||||||
|
kpentry.first,
|
||||||
|
kpentry.second
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
ErrorToaster.noKP2AFound(requireActivity())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updatePreview()
|
updatePreview()
|
||||||
return binding.root
|
return binding.root
|
||||||
@ -74,7 +99,7 @@ class CreateEntry : Fragment() {
|
|||||||
600
|
600
|
||||||
)
|
)
|
||||||
binding.imageViewPreview.setImageBitmap(barcodeBitmap)
|
binding.imageViewPreview.setImageBitmap(barcodeBitmap)
|
||||||
isValidBarcode = true
|
isValid = true
|
||||||
} catch (e: FormatException) {
|
} catch (e: FormatException) {
|
||||||
binding.imageViewPreview.setImageBitmap(null)
|
binding.imageViewPreview.setImageBitmap(null)
|
||||||
binding.editTextCode.error = "Invalid format"
|
binding.editTextCode.error = "Invalid format"
|
||||||
@ -83,23 +108,25 @@ class CreateEntry : Fragment() {
|
|||||||
binding.editTextCode.error = e.message
|
binding.editTextCode.error = e.message
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
binding.imageViewPreview.setImageBitmap(null)
|
binding.imageViewPreview.setImageBitmap(null)
|
||||||
|
println(e.javaClass)
|
||||||
|
println(e.message)
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isValidForm(): Boolean {
|
private fun isValid(): Boolean {
|
||||||
var valid = true
|
var valid = true
|
||||||
if (binding.editTextFormat.text.isNullOrEmpty()) {
|
if (binding.editTextTitle.text!!.isEmpty()) {
|
||||||
valid = false
|
valid = false
|
||||||
binding.editTextFormat.error = "Format cannot be empty"
|
binding.editTextTitle.error = "Title cannot be empty"
|
||||||
}
|
}
|
||||||
if (binding.editTextCode.text.isNullOrEmpty()) {
|
if (binding.editTextCode.text!!.isEmpty()) {
|
||||||
valid = false
|
valid = false
|
||||||
binding.editTextCode.error = "Code cannot be empty"
|
binding.editTextCode.error = "Code cannot be empty"
|
||||||
}
|
}
|
||||||
if (binding.editTextTitle.text.isNullOrEmpty()) {
|
if (binding.editTextFormat.text!!.isEmpty()) {
|
||||||
valid = false
|
valid = false
|
||||||
binding.editTextTitle.error = "Title cannot be empty"
|
binding.editTextFormat.error = "Format cannot be empty"
|
||||||
}
|
}
|
||||||
return valid
|
return valid
|
||||||
}
|
}
|
||||||
@ -113,50 +140,4 @@ class CreateEntry : Fragment() {
|
|||||||
.replace(R.id.container, viewEntryFragment).commit()
|
.replace(R.id.container, viewEntryFragment).commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun changeListener() {
|
|
||||||
isValidBarcode = false
|
|
||||||
handler.removeCallbacksAndMessages(null)
|
|
||||||
handler.postDelayed({
|
|
||||||
updatePreview()
|
|
||||||
}, DEBOUNCE_DELAY)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun TextInputEditText.onDone(callback: () -> Unit) {
|
|
||||||
setOnEditorActionListener { _, actionId, _ ->
|
|
||||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
|
||||||
callback.invoke()
|
|
||||||
return@setOnEditorActionListener true
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun submit() {
|
|
||||||
if (!isValidForm() || !isValidBarcode) {
|
|
||||||
ErrorToaster.formIncomplete(context)
|
|
||||||
} else {
|
|
||||||
val kpEntry = KeepassWrapper.entryCreate(
|
|
||||||
this,
|
|
||||||
binding.editTextTitle.text.toString(),
|
|
||||||
binding.editTextCode.text.toString(),
|
|
||||||
binding.editTextFormat.text.toString(),
|
|
||||||
binding.checkboxProtected.isChecked,
|
|
||||||
)
|
|
||||||
try {
|
|
||||||
resultLauncherAdd.launch(
|
|
||||||
Kp2aControl.getAddEntryIntent(
|
|
||||||
kpEntry.first,
|
|
||||||
kpEntry.second
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} catch (e: ActivityNotFoundException) {
|
|
||||||
ErrorToaster.noKP2AFound(context)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
@ -23,7 +23,7 @@ class Launcher : Fragment() {
|
|||||||
private lateinit var binding: FragLauncherBinding
|
private lateinit var binding: FragLauncherBinding
|
||||||
private lateinit var fidelityListAdapter: FidelityListAdapter
|
private lateinit var fidelityListAdapter: FidelityListAdapter
|
||||||
|
|
||||||
private val resultLauncherQuery = KeepassWrapper.resultLauncher(this) {
|
private val resultLauncherQuery = KeepassWrapper.resultLauncherQuery(this) {
|
||||||
val r = KeepassWrapper.entryExtract(it)
|
val r = KeepassWrapper.entryExtract(it)
|
||||||
if (!KeepassWrapper.isProtected(it)) {
|
if (!KeepassWrapper.isProtected(it)) {
|
||||||
CacheManager.addFidelity(r)
|
CacheManager.addFidelity(r)
|
||||||
@ -80,7 +80,7 @@ class Launcher : Fragment() {
|
|||||||
|
|
||||||
private fun startGetFromKeepass() {
|
private fun startGetFromKeepass() {
|
||||||
try {
|
try {
|
||||||
this.resultLauncherQuery.launch(Kp2aControl.getQueryEntryForOwnPackageIntent())
|
this.resultLauncherQuery.launch(Kp2aControl.queryEntryIntentForOwnPackage)
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
ErrorToaster.noKP2AFound(requireActivity())
|
ErrorToaster.noKP2AFound(requireActivity())
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,7 @@ class Scanner : Fragment() {
|
|||||||
|
|
||||||
private var code: String = ""
|
private var code: String = ""
|
||||||
private var fmt: String = ""
|
private var fmt: String = ""
|
||||||
|
private var valid: Boolean = false
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
@ -34,14 +35,13 @@ class Scanner : Fragment() {
|
|||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
binding = FragScannerBinding.inflate(layoutInflater)
|
binding = FragScannerBinding.inflate(layoutInflater)
|
||||||
binding.btnScanDone.setOnClickListener {
|
binding.bottomText.setOnClickListener {
|
||||||
startCreateEntry()
|
startCreateEntry()
|
||||||
}
|
}
|
||||||
when (hasCameraPermission()) {
|
when (hasCameraPermission()) {
|
||||||
true -> bindCameraUseCases()
|
true -> bindCameraUseCases()
|
||||||
else -> requestPermission()
|
else -> requestPermission()
|
||||||
}
|
}
|
||||||
binding.btnScanDone.isEnabled = false
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,10 +92,9 @@ class Scanner : Fragment() {
|
|||||||
if (code != null && format != null) {
|
if (code != null && format != null) {
|
||||||
this.code = code
|
this.code = code
|
||||||
this.fmt = format
|
this.fmt = format
|
||||||
binding.btnScanDone.isEnabled = true
|
this.valid = true
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
binding.btnScanDone.isEnabled = false
|
this.valid = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@ -112,4 +111,6 @@ class Scanner : Fragment() {
|
|||||||
}
|
}
|
||||||
}, ContextCompat.getMainExecutor(requireContext()))
|
}, ContextCompat.getMainExecutor(requireContext()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -1,14 +1,10 @@
|
|||||||
package net.helcel.fidelity.activity.fragment
|
package net.helcel.fidelity.activity.fragment
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.pm.ActivityInfo
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL
|
|
||||||
import android.view.WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.google.zxing.FormatException
|
import com.google.zxing.FormatException
|
||||||
import net.helcel.fidelity.databinding.FragViewEntryBinding
|
import net.helcel.fidelity.databinding.FragViewEntryBinding
|
||||||
@ -16,14 +12,16 @@ import net.helcel.fidelity.tools.BarcodeGenerator.generateBarcode
|
|||||||
import net.helcel.fidelity.tools.ErrorToaster
|
import net.helcel.fidelity.tools.ErrorToaster
|
||||||
import net.helcel.fidelity.tools.KeepassWrapper
|
import net.helcel.fidelity.tools.KeepassWrapper
|
||||||
|
|
||||||
@SuppressLint("SourceLockedOrientationActivity")
|
|
||||||
class ViewEntry : Fragment() {
|
class ViewEntry : Fragment() {
|
||||||
|
|
||||||
private lateinit var binding: FragViewEntryBinding
|
private lateinit var binding: FragViewEntryBinding
|
||||||
|
|
||||||
private var title: String? = null
|
private var title: String? = null
|
||||||
private var code: String? = null
|
private var code: String? = null
|
||||||
private var fmt: String? = null
|
private var fmt: String? = null
|
||||||
|
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
@ -35,15 +33,8 @@ class ViewEntry : Fragment() {
|
|||||||
code = res.second
|
code = res.second
|
||||||
fmt = res.third
|
fmt = res.third
|
||||||
|
|
||||||
|
adjustLayout()
|
||||||
updatePreview()
|
updatePreview()
|
||||||
updateLayout()
|
|
||||||
|
|
||||||
binding.imageViewPreview.setOnClickListener {
|
|
||||||
requireActivity().requestedOrientation =
|
|
||||||
if (isLandscape()) ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
|
||||||
else ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
|
||||||
}
|
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,7 +42,7 @@ class ViewEntry : Fragment() {
|
|||||||
binding.title.text = title
|
binding.title.text = title
|
||||||
try {
|
try {
|
||||||
val barcodeBitmap = generateBarcode(
|
val barcodeBitmap = generateBarcode(
|
||||||
code, fmt, 1024
|
code!!, fmt!!, 1024
|
||||||
)
|
)
|
||||||
binding.imageViewPreview.setImageBitmap(barcodeBitmap)
|
binding.imageViewPreview.setImageBitmap(barcodeBitmap)
|
||||||
} catch (e: FormatException) {
|
} catch (e: FormatException) {
|
||||||
@ -62,25 +53,23 @@ class ViewEntry : Fragment() {
|
|||||||
ErrorToaster.invalidFormat(requireActivity())
|
ErrorToaster.invalidFormat(requireActivity())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
binding.imageViewPreview.setImageBitmap(null)
|
binding.imageViewPreview.setImageBitmap(null)
|
||||||
|
println(e.javaClass)
|
||||||
|
println(e.message)
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateLayout() {
|
|
||||||
if (isLandscape()) {
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
adjustLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun adjustLayout() {
|
||||||
|
if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||||
binding.title.visibility = View.GONE
|
binding.title.visibility = View.GONE
|
||||||
setScreenBrightness(BRIGHTNESS_OVERRIDE_FULL)
|
|
||||||
} else {
|
} else {
|
||||||
binding.title.visibility = View.VISIBLE
|
binding.title.visibility = View.VISIBLE
|
||||||
setScreenBrightness(BRIGHTNESS_OVERRIDE_NONE)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isLandscape(): Boolean {
|
|
||||||
return (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setScreenBrightness(brightness: Float?) {
|
|
||||||
requireActivity().window?.attributes?.screenBrightness = brightness
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,45 +0,0 @@
|
|||||||
package net.helcel.fidelity.activity.view
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.graphics.Paint
|
|
||||||
import android.graphics.PorterDuff
|
|
||||||
import android.graphics.PorterDuffXfermode
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.View
|
|
||||||
|
|
||||||
class ScannerView : View {
|
|
||||||
|
|
||||||
private val overlayPaint = Paint().apply {
|
|
||||||
color = Color.parseColor("#80000000") // Semi-transparent black
|
|
||||||
style = Paint.Style.FILL
|
|
||||||
}
|
|
||||||
|
|
||||||
private val clearPaint = Paint().apply {
|
|
||||||
xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(context: Context) : super(context)
|
|
||||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
|
||||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
|
|
||||||
context,
|
|
||||||
attrs,
|
|
||||||
defStyleAttr
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun onDraw(canvas: Canvas) {
|
|
||||||
super.onDraw(canvas)
|
|
||||||
|
|
||||||
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), overlayPaint)
|
|
||||||
|
|
||||||
val centerX = width / 2f
|
|
||||||
val centerY = height / 2f
|
|
||||||
val squareSize = 0.75f * width.coerceAtMost(height)
|
|
||||||
canvas.drawRect(
|
|
||||||
centerX - squareSize / 2, centerY - squareSize / 2,
|
|
||||||
centerX + squareSize / 2, centerY + squareSize / 2, clearPaint
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,84 +2,141 @@ package net.helcel.fidelity.pluginSDK
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.text.TextUtils
|
||||||
|
import android.util.Log
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONException
|
import org.json.JSONException
|
||||||
|
|
||||||
|
|
||||||
object AccessManager {
|
object AccessManager {
|
||||||
|
private const val _tag = "Kp2aPluginSDK"
|
||||||
private const val PREF_KEY_SCOPE = "scope"
|
private const val PREF_KEY_SCOPE = "scope"
|
||||||
private const val PREF_KEY_TOKEN = "token"
|
private const val PREF_KEY_TOKEN = "token"
|
||||||
|
|
||||||
private fun stringArrayToString(values: ArrayList<String?>): String? {
|
private fun stringArrayToString(values: ArrayList<String?>): String? {
|
||||||
if (values.isEmpty()) return null
|
|
||||||
val a = JSONArray()
|
val a = JSONArray()
|
||||||
values.forEach { a.put(it) }
|
for (i in values.indices) {
|
||||||
return a.toString()
|
a.put(values[i])
|
||||||
|
}
|
||||||
|
return if (values.isNotEmpty()) {
|
||||||
|
a.toString()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stringToStringArray(s: String?): ArrayList<String> {
|
private fun stringToStringArray(s: String?): ArrayList<String> {
|
||||||
val strings = ArrayList<String>()
|
val strings = ArrayList<String>()
|
||||||
if (s.isNullOrEmpty()) return strings
|
if (!TextUtils.isEmpty(s)) {
|
||||||
|
try {
|
||||||
try {
|
val a = JSONArray(s)
|
||||||
val a = JSONArray(s)
|
for (i in 0 until a.length()) {
|
||||||
for (i in 0 until a.length())
|
val url = a.optString(i)
|
||||||
strings.add(a.optString(i))
|
strings.add(url)
|
||||||
} catch (e: JSONException) {
|
}
|
||||||
e.printStackTrace()
|
} catch (e: JSONException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return strings
|
return strings
|
||||||
}
|
}
|
||||||
|
|
||||||
fun storeAccessToken(
|
fun storeAccessToken(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
hostPackage: String?,
|
hostPackage: String,
|
||||||
accessToken: String?,
|
accessToken: String,
|
||||||
scopes: ArrayList<String?>
|
scopes: ArrayList<String?>
|
||||||
) {
|
) {
|
||||||
val prefs = getPrefsForHost(ctx, hostPackage)
|
val prefs = getPrefsForHost(ctx, hostPackage)
|
||||||
|
|
||||||
val edit = prefs.edit()
|
val edit = prefs.edit()
|
||||||
edit.putString(PREF_KEY_TOKEN, accessToken)
|
edit.putString(PREF_KEY_TOKEN, accessToken)
|
||||||
val scopesString = stringArrayToString(scopes)
|
val scopesString = stringArrayToString(scopes)
|
||||||
edit.putString(PREF_KEY_SCOPE, scopesString)
|
edit.putString(PREF_KEY_SCOPE, scopesString)
|
||||||
edit.apply()
|
edit.apply()
|
||||||
|
Log.d(
|
||||||
|
_tag,
|
||||||
|
"stored access token " + accessToken.substring(
|
||||||
|
0,
|
||||||
|
4
|
||||||
|
) + "... for " + scopes.size + " scopes (" + scopesString + ")."
|
||||||
|
)
|
||||||
|
|
||||||
val hostPrefs = ctx.getSharedPreferences("KP2A.PluginAccess.hosts", Context.MODE_PRIVATE)
|
val hostPrefs = ctx.getSharedPreferences("KP2A.PluginAccess.hosts", Context.MODE_PRIVATE)
|
||||||
if (!hostPrefs.contains(hostPackage))
|
if (!hostPrefs.contains(hostPackage)) {
|
||||||
hostPrefs.edit().putString(hostPackage, "").apply()
|
hostPrefs.edit().putString(hostPackage, "").apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun preparePopup(popupMenu: Any) {
|
||||||
|
try {
|
||||||
|
val fields = popupMenu.javaClass.declaredFields
|
||||||
|
for (field in fields) {
|
||||||
|
if ("mPopup" == field.name) {
|
||||||
|
field.isAccessible = true
|
||||||
|
val menuPopupHelper = field[popupMenu]
|
||||||
|
val classPopupHelper = Class.forName(
|
||||||
|
menuPopupHelper
|
||||||
|
.javaClass.name
|
||||||
|
)
|
||||||
|
val setForceIcons = classPopupHelper.getMethod(
|
||||||
|
"setForceShowIcon", Boolean::class.javaPrimitiveType
|
||||||
|
)
|
||||||
|
setForceIcons.invoke(menuPopupHelper, true)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPrefsForHost(
|
private fun getPrefsForHost(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
hostPackage: String?
|
hostPackage: String
|
||||||
): SharedPreferences {
|
): SharedPreferences {
|
||||||
return ctx.getSharedPreferences("KP2A.PluginAccess.$hostPackage", Context.MODE_PRIVATE)
|
val prefs = ctx.getSharedPreferences("KP2A.PluginAccess.$hostPackage", Context.MODE_PRIVATE)
|
||||||
|
return prefs
|
||||||
}
|
}
|
||||||
|
|
||||||
fun tryGetAccessToken(ctx: Context, hostPackage: String?, scopes: ArrayList<String?>): String? {
|
fun tryGetAccessToken(ctx: Context, hostPackage: String, scopes: ArrayList<String?>): String? {
|
||||||
if (hostPackage.isNullOrEmpty()) return null
|
if (TextUtils.isEmpty(hostPackage)) {
|
||||||
|
Log.d(_tag, "hostPackage is empty!")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
Log.d(_tag, "trying to find prefs for $hostPackage")
|
||||||
val prefs = getPrefsForHost(ctx, hostPackage)
|
val prefs = getPrefsForHost(ctx, hostPackage)
|
||||||
val scopesString = prefs.getString(PREF_KEY_SCOPE, "")
|
val scopesString = prefs.getString(PREF_KEY_SCOPE, "")
|
||||||
|
Log.d(_tag, "available scopes: $scopesString")
|
||||||
val currentScope = stringToStringArray(scopesString)
|
val currentScope = stringToStringArray(scopesString)
|
||||||
if (!isSubset(scopes, currentScope))
|
if (isSubset(scopes, currentScope)) {
|
||||||
|
return prefs.getString(PREF_KEY_TOKEN, null)
|
||||||
|
} else {
|
||||||
|
Log.d(_tag, "looks like scope changed. Access token invalid.")
|
||||||
return null
|
return null
|
||||||
return prefs.getString(PREF_KEY_TOKEN, null)
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isSubset(
|
private fun isSubset(
|
||||||
requiredScopes: ArrayList<String?>,
|
requiredScopes: ArrayList<String?>,
|
||||||
availableScopes: ArrayList<String>
|
availableScopes: ArrayList<String>
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return availableScopes.containsAll(requiredScopes)
|
for (r in requiredScopes) {
|
||||||
|
if (availableScopes.indexOf(r) < 0) {
|
||||||
|
Log.d(_tag, "Scope " + r + " not available. " + availableScopes.size)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeAccessToken(
|
fun removeAccessToken(
|
||||||
ctx: Context, hostPackage: String?,
|
ctx: Context, hostPackage: String,
|
||||||
accessToken: String?
|
accessToken: String
|
||||||
) {
|
) {
|
||||||
val prefs = getPrefsForHost(ctx, hostPackage)
|
val prefs = getPrefsForHost(ctx, hostPackage)
|
||||||
|
|
||||||
|
Log.d(_tag, "removing AccessToken.")
|
||||||
if (prefs.getString(PREF_KEY_TOKEN, "") == accessToken) {
|
if (prefs.getString(PREF_KEY_TOKEN, "") == accessToken) {
|
||||||
val edit = prefs.edit()
|
val edit = prefs.edit()
|
||||||
edit.clear()
|
edit.clear()
|
||||||
@ -91,4 +148,32 @@ object AccessManager {
|
|||||||
hostPrefs.edit().remove(hostPackage).apply()
|
hostPrefs.edit().remove(hostPackage).apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getAllHostPackages(ctx: Context): Set<String> {
|
||||||
|
val prefs = ctx.getSharedPreferences("KP2A.PluginAccess.hosts", Context.MODE_PRIVATE)
|
||||||
|
val result: MutableSet<String> = HashSet()
|
||||||
|
for (host in prefs.all.keys) {
|
||||||
|
try {
|
||||||
|
val info = ctx.packageManager.getPackageInfo(host, PackageManager.GET_META_DATA)
|
||||||
|
//if we get here, the package is still there
|
||||||
|
result.add(host)
|
||||||
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
|
//host gone. ignore.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a valid access token or throws PluginAccessException
|
||||||
|
*/
|
||||||
|
fun getAccessToken(
|
||||||
|
context: Context, hostPackage: String,
|
||||||
|
scopes: ArrayList<String?>
|
||||||
|
): String {
|
||||||
|
val accessToken = tryGetAccessToken(context, hostPackage, scopes)
|
||||||
|
?: throw PluginAccessException(hostPackage, scopes)
|
||||||
|
return accessToken
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
package net.helcel.fidelity.pluginSDK
|
|
||||||
|
|
||||||
@Suppress("unused")
|
|
||||||
object KeepassDef {
|
|
||||||
var TitleField: String = "Title"
|
|
||||||
var UserNameField: String = "UserName"
|
|
||||||
var PasswordField: String = "Password"
|
|
||||||
var UrlField: String = "URL"
|
|
||||||
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -1,41 +1,99 @@
|
|||||||
package net.helcel.fidelity.pluginSDK
|
package net.helcel.fidelity.pluginSDK
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.text.TextUtils
|
||||||
import org.json.JSONException
|
import org.json.JSONException
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
||||||
object Kp2aControl {
|
object Kp2aControl {
|
||||||
|
/**
|
||||||
|
* Creates and returns an intent to launch Keepass2Android for adding an entry with the given fields.
|
||||||
|
* @param fields Key/Value pairs of the field values. See KeepassDefs for standard keys.
|
||||||
|
* @param protectedFields List of keys of the protected fields.
|
||||||
|
* @return Intent to start Keepass2Android.
|
||||||
|
* @throws JSONException
|
||||||
|
*/
|
||||||
fun getAddEntryIntent(
|
fun getAddEntryIntent(
|
||||||
fields: HashMap<String?, String?>,
|
fields: HashMap<String?, String?>?,
|
||||||
|
protectedFields: ArrayList<String?>?
|
||||||
|
): Intent {
|
||||||
|
return getAddEntryIntent(JSONObject((fields as Map<*, *>?)!!).toString(), protectedFields)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAddEntryIntent(
|
||||||
|
outputData: String?,
|
||||||
protectedFields: ArrayList<String?>?
|
protectedFields: ArrayList<String?>?
|
||||||
): Intent {
|
): Intent {
|
||||||
val outputData = JSONObject((fields as Map<*, *>)).toString()
|
|
||||||
val startKp2aIntent = Intent(Strings.ACTION_START_WITH_TASK)
|
val startKp2aIntent = Intent(Strings.ACTION_START_WITH_TASK)
|
||||||
startKp2aIntent.addCategory(Intent.CATEGORY_DEFAULT)
|
startKp2aIntent.addCategory(Intent.CATEGORY_DEFAULT)
|
||||||
startKp2aIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
startKp2aIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||||
startKp2aIntent.putExtra("KP2A_APPTASK", "CreateEntryThenCloseTask")
|
startKp2aIntent.putExtra("KP2A_APPTASK", "CreateEntryThenCloseTask")
|
||||||
startKp2aIntent.putExtra("ShowUserNotifications", "false")
|
startKp2aIntent.putExtra("ShowUserNotifications", "false") //KP2A expects a StringExtra
|
||||||
startKp2aIntent.putExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA, outputData)
|
startKp2aIntent.putExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA, outputData)
|
||||||
if (protectedFields != null)
|
if (protectedFields != null) startKp2aIntent.putStringArrayListExtra(
|
||||||
startKp2aIntent.putStringArrayListExtra(
|
Strings.EXTRA_PROTECTED_FIELDS_LIST,
|
||||||
Strings.EXTRA_PROTECTED_FIELDS_LIST,
|
protectedFields
|
||||||
protectedFields
|
)
|
||||||
)
|
|
||||||
|
|
||||||
return startKp2aIntent
|
return startKp2aIntent
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getQueryEntryForOwnPackageIntent(): Intent {
|
|
||||||
return Intent(Strings.ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE)
|
/**
|
||||||
|
* Creates an intent to open a Password Entry matching searchText
|
||||||
|
* @param searchText queryString
|
||||||
|
* @param showUserNotifications if true, the notifications (copy to clipboard, keyboard) are displayed
|
||||||
|
* @param closeAfterOpen if true, the entry is opened and KP2A is immediately closed
|
||||||
|
* @return Intent to start KP2A with
|
||||||
|
*/
|
||||||
|
fun getOpenEntryIntent(
|
||||||
|
searchText: String?,
|
||||||
|
showUserNotifications: Boolean,
|
||||||
|
closeAfterOpen: Boolean
|
||||||
|
): Intent {
|
||||||
|
val startKp2aIntent = Intent(Strings.ACTION_START_WITH_TASK)
|
||||||
|
startKp2aIntent.addCategory(Intent.CATEGORY_DEFAULT)
|
||||||
|
startKp2aIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||||
|
startKp2aIntent.putExtra("KP2A_APPTASK", "SearchUrlTask")
|
||||||
|
startKp2aIntent.putExtra("ShowUserNotifications", showUserNotifications.toString())
|
||||||
|
startKp2aIntent.putExtra("CloseAfterCreate", closeAfterOpen.toString())
|
||||||
|
startKp2aIntent.putExtra("UrlToSearch", searchText)
|
||||||
|
return startKp2aIntent
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getEntryFieldsFromIntent(intent: Intent?): HashMap<String, String> {
|
/**
|
||||||
|
* Creates an intent to query a password entry from KP2A. The credentials are returned as Activity result.
|
||||||
|
* @param searchText Text to search for. Should be a URL or "androidapp://com.my.package."
|
||||||
|
* @return an Intent to start KP2A with
|
||||||
|
*/
|
||||||
|
fun getQueryEntryIntent(searchText: String?): Intent {
|
||||||
|
val i = Intent(Strings.ACTION_QUERY_CREDENTIALS)
|
||||||
|
if (!TextUtils.isEmpty(searchText)) i.putExtra(Strings.EXTRA_QUERY_STRING, searchText)
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
val queryEntryIntentForOwnPackage: Intent
|
||||||
|
/**
|
||||||
|
* Creates an intent to query a password entry from KP2A, matching to the current app's package .
|
||||||
|
* The credentials are returned as Activity result.
|
||||||
|
* This requires SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE.
|
||||||
|
* @return an Intent to start KP2A with
|
||||||
|
*/
|
||||||
|
get() = Intent(Strings.ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the entry fields returned in an intent from a query to a hashmap.
|
||||||
|
* @param intent data received in onActivityResult after getQueryEntryIntent(ForOwnPackage)
|
||||||
|
* @return HashMap with keys = field names (see KeepassDefs for standard keys) and values = values
|
||||||
|
*/
|
||||||
|
fun getEntryFieldsFromIntent(intent: Intent): HashMap<String, String> {
|
||||||
val res = HashMap<String, String>()
|
val res = HashMap<String, String>()
|
||||||
try {
|
try {
|
||||||
val json = JSONObject(intent?.getStringExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA) ?: "")
|
val json = JSONObject(intent.getStringExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA)!!)
|
||||||
val itr = json.keys()
|
val iter = json.keys()
|
||||||
while (itr.hasNext()) {
|
while (iter.hasNext()) {
|
||||||
val key = itr.next()
|
val key = iter.next()
|
||||||
val value = json[key].toString()
|
val value = json[key].toString()
|
||||||
res[key] = value
|
res[key] = value
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,35 @@ package net.helcel.fidelity.pluginSDK
|
|||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.util.Log
|
||||||
|
|
||||||
class PluginAccessBroadcastReceiver : BroadcastReceiver() {
|
/**
|
||||||
|
* Broadcast flow between Host and Plugin
|
||||||
|
* ======================================
|
||||||
|
*
|
||||||
|
* The host is responsible for deciding when to initiate the session. It
|
||||||
|
* should initiate the session as soon as plugins are required or when a plugin
|
||||||
|
* has been updated through the OS.
|
||||||
|
* It will then send a broadcast to request the currently required scope.
|
||||||
|
* The plugin then sends a broadcast to the app which scope is required. If an
|
||||||
|
* access token is already available, it's sent along with the requset.
|
||||||
|
*
|
||||||
|
* If a previous permission has been revoked (or the app settings cleared or the
|
||||||
|
* permissions have been extended or the token is invalid for any other reason)
|
||||||
|
* the host will answer with a Revoked-Permission broadcast (i.e. the plugin is
|
||||||
|
* unconnected.)
|
||||||
|
*
|
||||||
|
* Unconnected plugins must be permitted by the user (requiring user action).
|
||||||
|
* When the user grants access, the plugin will receive an access token for
|
||||||
|
* the host. This access token is valid for the requested scope. If the scope
|
||||||
|
* changes (e.g after an update of the plugin), the access token becomes invalid.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
abstract class PluginAccessBroadcastReceiver : BroadcastReceiver() {
|
||||||
override fun onReceive(ctx: Context, intent: Intent) {
|
override fun onReceive(ctx: Context, intent: Intent) {
|
||||||
val action = intent.action ?: return
|
val action = intent.action
|
||||||
|
Log.d(_tag, "received broadcast with action=$action")
|
||||||
|
if (action == null) return
|
||||||
when (action) {
|
when (action) {
|
||||||
Strings.ACTION_TRIGGER_REQUEST_ACCESS -> requestAccess(ctx, intent)
|
Strings.ACTION_TRIGGER_REQUEST_ACCESS -> requestAccess(ctx, intent)
|
||||||
Strings.ACTION_RECEIVE_ACCESS -> receiveAccess(ctx, intent)
|
Strings.ACTION_RECEIVE_ACCESS -> receiveAccess(ctx, intent)
|
||||||
@ -15,17 +40,18 @@ class PluginAccessBroadcastReceiver : BroadcastReceiver() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun revokeAccess(ctx: Context, intent: Intent) {
|
private fun revokeAccess(ctx: Context, intent: Intent) {
|
||||||
val senderPackage = intent.getStringExtra(Strings.EXTRA_SENDER)
|
val senderPackage = intent.getStringExtra(Strings.EXTRA_SENDER)
|
||||||
val accessToken = intent.getStringExtra(Strings.EXTRA_ACCESS_TOKEN)
|
val accessToken = intent.getStringExtra(Strings.EXTRA_ACCESS_TOKEN)
|
||||||
AccessManager.removeAccessToken(ctx, senderPackage, accessToken)
|
AccessManager.removeAccessToken(ctx, senderPackage!!, accessToken!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun receiveAccess(ctx: Context, intent: Intent) {
|
private fun receiveAccess(ctx: Context, intent: Intent) {
|
||||||
val senderPackage = intent.getStringExtra(Strings.EXTRA_SENDER)
|
val senderPackage = intent.getStringExtra(Strings.EXTRA_SENDER)
|
||||||
val accessToken = intent.getStringExtra(Strings.EXTRA_ACCESS_TOKEN)
|
val accessToken = intent.getStringExtra(Strings.EXTRA_ACCESS_TOKEN)
|
||||||
AccessManager.storeAccessToken(ctx, senderPackage, accessToken, scopes)
|
AccessManager.storeAccessToken(ctx, senderPackage!!, accessToken!!, scopes)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requestAccess(ctx: Context, intent: Intent) {
|
private fun requestAccess(ctx: Context, intent: Intent) {
|
||||||
@ -36,16 +62,21 @@ class PluginAccessBroadcastReceiver : BroadcastReceiver() {
|
|||||||
rpi.putExtra(Strings.EXTRA_SENDER, ctx.packageName)
|
rpi.putExtra(Strings.EXTRA_SENDER, ctx.packageName)
|
||||||
rpi.putExtra(Strings.EXTRA_REQUEST_TOKEN, requestToken)
|
rpi.putExtra(Strings.EXTRA_REQUEST_TOKEN, requestToken)
|
||||||
|
|
||||||
val token: String? = AccessManager.tryGetAccessToken(ctx, senderPackage, scopes)
|
val token: String? = AccessManager.tryGetAccessToken(ctx, senderPackage!!, scopes)
|
||||||
rpi.putExtra(Strings.EXTRA_ACCESS_TOKEN, token)
|
rpi.putExtra(Strings.EXTRA_ACCESS_TOKEN, token)
|
||||||
|
|
||||||
rpi.putStringArrayListExtra(Strings.EXTRA_SCOPES, scopes)
|
rpi.putStringArrayListExtra(Strings.EXTRA_SCOPES, scopes)
|
||||||
|
Log.d(_tag, "requesting access for " + scopes.size + " tokens.")
|
||||||
ctx.sendBroadcast(rpi)
|
ctx.sendBroadcast(rpi)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val scopes: ArrayList<String?> = ArrayList(
|
/**
|
||||||
listOf(
|
*
|
||||||
Strings.SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE,
|
* @return the list of required scopes for this plugin.
|
||||||
)
|
*/
|
||||||
)
|
abstract val scopes: ArrayList<String?>
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val _tag = "Kp2aPluginSDK"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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?) {}
|
||||||
|
}
|
@ -1,30 +1,195 @@
|
|||||||
package net.helcel.fidelity.pluginSDK
|
package net.helcel.fidelity.pluginSDK
|
||||||
|
|
||||||
@Suppress("unused")
|
|
||||||
object Strings {
|
object Strings {
|
||||||
|
/**
|
||||||
|
* Plugin is notified about actions like open/close/update a database.
|
||||||
|
*/
|
||||||
const val SCOPE_DATABASE_ACTIONS = "keepass2android.SCOPE_DATABASE_ACTIONS"
|
const val SCOPE_DATABASE_ACTIONS = "keepass2android.SCOPE_DATABASE_ACTIONS"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin is notified when an entry is opened.
|
||||||
|
*/
|
||||||
const val SCOPE_CURRENT_ENTRY = "keepass2android.SCOPE_CURRENT_ENTRY"
|
const val SCOPE_CURRENT_ENTRY = "keepass2android.SCOPE_CURRENT_ENTRY"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin may query credentials for its own package
|
||||||
|
*/
|
||||||
const val SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE =
|
const val SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE =
|
||||||
"keepass2android.SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE"
|
"keepass2android.SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin may query credentials for a deliberate package
|
||||||
|
*/
|
||||||
|
const val SCOPE_QUERY_CREDENTIALS = "keepass2android.SCOPE_QUERY_CREDENTIALS"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extra key to transfer a (json serialized) list of scopes
|
||||||
|
*/
|
||||||
const val EXTRA_SCOPES = "keepass2android.EXTRA_SCOPES"
|
const val EXTRA_SCOPES = "keepass2android.EXTRA_SCOPES"
|
||||||
|
|
||||||
|
|
||||||
const val EXTRA_PLUGIN_PACKAGE = "keepass2android.EXTRA_PLUGIN_PACKAGE"
|
const val EXTRA_PLUGIN_PACKAGE = "keepass2android.EXTRA_PLUGIN_PACKAGE"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extra key for sending the package name of the sender of a broadcast.
|
||||||
|
* Should be set in every broadcast.
|
||||||
|
*/
|
||||||
const val EXTRA_SENDER = "keepass2android.EXTRA_SENDER"
|
const val EXTRA_SENDER = "keepass2android.EXTRA_SENDER"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extra key for sending a request token. The request token is passed from
|
||||||
|
* KP2A to the plugin. It's used in the authorization process.
|
||||||
|
*/
|
||||||
const val EXTRA_REQUEST_TOKEN = "keepass2android.EXTRA_REQUEST_TOKEN"
|
const val EXTRA_REQUEST_TOKEN = "keepass2android.EXTRA_REQUEST_TOKEN"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action to start KP2A with an AppTask
|
||||||
|
*/
|
||||||
const val ACTION_START_WITH_TASK = "keepass2android.ACTION_START_WITH_TASK"
|
const val ACTION_START_WITH_TASK = "keepass2android.ACTION_START_WITH_TASK"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action sent from KP2A to the plugin to indicate that the plugin should request
|
||||||
|
* access (sending it's scopes)
|
||||||
|
*/
|
||||||
const val ACTION_TRIGGER_REQUEST_ACCESS = "keepass2android.ACTION_TRIGGER_REQUEST_ACCESS"
|
const val ACTION_TRIGGER_REQUEST_ACCESS = "keepass2android.ACTION_TRIGGER_REQUEST_ACCESS"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action sent from the plugin to KP2A including the scopes.
|
||||||
|
*/
|
||||||
const val ACTION_REQUEST_ACCESS = "keepass2android.ACTION_REQUEST_ACCESS"
|
const val ACTION_REQUEST_ACCESS = "keepass2android.ACTION_REQUEST_ACCESS"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action sent from the KP2A to the plugin when the user grants access.
|
||||||
|
* Will contain an access token.
|
||||||
|
*/
|
||||||
const val ACTION_RECEIVE_ACCESS = "keepass2android.ACTION_RECEIVE_ACCESS"
|
const val ACTION_RECEIVE_ACCESS = "keepass2android.ACTION_RECEIVE_ACCESS"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action sent from KP2A to the plugin to indicate that access is not or no longer valid.
|
||||||
|
*/
|
||||||
const val ACTION_REVOKE_ACCESS = "keepass2android.ACTION_REVOKE_ACCESS"
|
const val ACTION_REVOKE_ACCESS = "keepass2android.ACTION_REVOKE_ACCESS"
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action for startActivity(). Opens an activity in the Plugin Host to edit the plugin settings (i.e. enable it)
|
||||||
|
*/
|
||||||
|
const val ACTION_EDIT_PLUGIN_SETTINGS = "keepass2android.ACTION_EDIT_PLUGIN_SETTINGS"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action sent from KP2A to the plugin to indicate that an entry was opened.
|
||||||
|
* The Intent contains the full entry data.
|
||||||
|
*/
|
||||||
|
const val ACTION_OPEN_ENTRY = "keepass2android.ACTION_OPEN_ENTRY"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action sent from KP2A to the plugin to indicate that an entry output field was modified/added.
|
||||||
|
* The Intent contains the full new entry data.
|
||||||
|
*/
|
||||||
|
const val ACTION_ENTRY_OUTPUT_MODIFIED = "keepass2android.ACTION_ENTRY_OUTPUT_MODIFIED"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action sent from KP2A to the plugin to indicate that an entry activity was closed.
|
||||||
|
*/
|
||||||
|
const val ACTION_CLOSE_ENTRY_VIEW = "keepass2android.ACTION_CLOSE_ENTRY_VIEW"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extra key for a string containing the GUID of the entry.
|
||||||
|
*/
|
||||||
|
const val EXTRA_ENTRY_ID = "keepass2android.EXTRA_ENTRY_DATA"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Json serialized data of the PwEntry (C# class) representing the opened entry.
|
||||||
|
* currently not implemented.
|
||||||
|
*/
|
||||||
|
//const val EXTRA_ENTRY_DATA = "keepass2android.EXTRA_ENTRY_DATA";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Json serialized list of fields, transformed using the database context (i.e. placeholders are replaced already)
|
||||||
|
*/
|
||||||
const val EXTRA_ENTRY_OUTPUT_DATA = "keepass2android.EXTRA_ENTRY_OUTPUT_DATA"
|
const val EXTRA_ENTRY_OUTPUT_DATA = "keepass2android.EXTRA_ENTRY_OUTPUT_DATA"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Json serialized lisf of field keys, specifying which field of the EXTRA_ENTRY_OUTPUT_DATA is protected.
|
||||||
|
*/
|
||||||
const val EXTRA_PROTECTED_FIELDS_LIST = "keepass2android.EXTRA_PROTECTED_FIELDS_LIST"
|
const val EXTRA_PROTECTED_FIELDS_LIST = "keepass2android.EXTRA_PROTECTED_FIELDS_LIST"
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extra key for passing the access token (both ways)
|
||||||
|
*/
|
||||||
const val EXTRA_ACCESS_TOKEN = "keepass2android.EXTRA_ACCESS_TOKEN"
|
const val EXTRA_ACCESS_TOKEN = "keepass2android.EXTRA_ACCESS_TOKEN"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action for an intent from the plugin to KP2A to add menu options regarding the currently open entry.
|
||||||
|
* Requires SCOPE_CURRENT_ENTRY.
|
||||||
|
*/
|
||||||
|
const val ACTION_ADD_ENTRY_ACTION = "keepass2android.ACTION_ADD_ENTRY_ACTION"
|
||||||
|
|
||||||
|
const val EXTRA_ACTION_DISPLAY_TEXT = "keepass2android.EXTRA_ACTION_DISPLAY_TEXT"
|
||||||
|
const val EXTRA_ACTION_ICON_RES_ID = "keepass2android.EXTRA_ACTION_ICON_RES_ID"
|
||||||
|
|
||||||
|
const val EXTRA_FIELD_ID = "keepass2android.EXTRA_FIELD_ID"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to pass an id for the action. Each actionId may occur only once per field, otherwise the previous
|
||||||
|
* action with same id is replaced by the new action.
|
||||||
|
*/
|
||||||
|
const val EXTRA_ACTION_ID = "keepass2android.EXTRA_ACTION_ID"
|
||||||
|
|
||||||
|
/** Extra for ACTION_ADD_ENTRY_ACTION and ACTION_ENTRY_ACTION_SELECTED to pass data specifying the action parameters.*/
|
||||||
|
const val EXTRA_ACTION_DATA = "keepass2android.EXTRA_ACTION_DATA"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action for an intent from KP2A to the plugin when an action added with ACTION_ADD_ENTRY_ACTION was selected by the user.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const val ACTION_ENTRY_ACTION_SELECTED = "keepass2android.ACTION_ENTRY_ACTION_SELECTED"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extra key for the string which is used to query the credentials. This should be either a URL for
|
||||||
|
* a web login (google.com or a full URI) or something in the form "androidapp://com.my.package"
|
||||||
|
*/
|
||||||
|
const val EXTRA_QUERY_STRING = "keepass2android.EXTRA_QUERY_STRING"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action when plugin wants to query credentials for its own package
|
||||||
|
*/
|
||||||
const val ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE =
|
const val ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE =
|
||||||
"keepass2android.ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE"
|
"keepass2android.ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE"
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action when plugin wants to query credentials for a deliberate package
|
||||||
|
* The query string is passed as intent data
|
||||||
|
*/
|
||||||
|
const val ACTION_QUERY_CREDENTIALS = "keepass2android.ACTION_QUERY_CREDENTIALS"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action for an intent from the plugin to KP2A to set (i.e. add or update) a field in the entry.
|
||||||
|
* May be used to update existing or add new fields at any time while the entry is opened.
|
||||||
|
*/
|
||||||
|
const val ACTION_SET_ENTRY_FIELD = "keepass2android.ACTION_SET_ENTRY_FIELD"
|
||||||
|
|
||||||
|
/** Actions for an intent from KP2A to the plugin to inform that a database was opened, closed, quicklocked or quickunlocked.*/
|
||||||
|
const val ACTION_OPEN_DATABASE = "keepass2android.ACTION_OPEN_DATABASE"
|
||||||
|
const val ACTION_CLOSE_DATABASE = "keepass2android.ACTION_CLOSE_DATABASE"
|
||||||
|
const val ACTION_LOCK_DATABASE = "keepass2android.ACTION_LOCK_DATABASE"
|
||||||
|
const val ACTION_UNLOCK_DATABASE = "keepass2android.ACTION_UNLOCK_DATABASE"
|
||||||
|
|
||||||
|
/** Extra for ACTION_OPEN_DATABASE and ACTION_CLOSE_DATABASE containing a filepath which is used
|
||||||
|
* by KP2A internally to identify the file. Use only where necessary, might contain credentials
|
||||||
|
* for accessing the file (on remote storage).*/
|
||||||
|
const val EXTRA_DATABASE_FILEPATH = "keepass2android.EXTRA_DATABASE_FILEPATH"
|
||||||
|
|
||||||
|
/** Extra for ACTION_OPEN_DATABASE and ACTION_CLOSE_DATABASE containing a filepath which can be
|
||||||
|
* displayed to the user.*/
|
||||||
|
const val EXTRA_DATABASE_FILE_DISPLAYNAME = "keepass2android.EXTRA_DATABASE_FILE_DISPLAYNAME"
|
||||||
|
|
||||||
|
|
||||||
|
const val EXTRA_FIELD_VALUE = "keepass2android.EXTRA_FIELD_VALUE"
|
||||||
|
const val EXTRA_FIELD_PROTECTED = "keepass2android.EXTRA_FIELD_PROTECTED"
|
||||||
|
|
||||||
|
const val PREFIX_STRING = "STRING_"
|
||||||
|
const val PREFIX_BINARY = "BINARY_"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -15,9 +15,9 @@ import net.helcel.fidelity.tools.BarcodeFormatConverter.formatToString
|
|||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalGetImage::class)
|
||||||
object BarcodeScanner {
|
object BarcodeScanner {
|
||||||
|
|
||||||
@OptIn(ExperimentalGetImage::class)
|
|
||||||
private fun processImageProxy(
|
private fun processImageProxy(
|
||||||
barcodeScanner: BarcodeScanner,
|
barcodeScanner: BarcodeScanner,
|
||||||
imageProxy: ImageProxy,
|
imageProxy: ImageProxy,
|
||||||
@ -33,6 +33,8 @@ object BarcodeScanner {
|
|||||||
|
|
||||||
barcodeScanner.process(inputImage)
|
barcodeScanner.process(inputImage)
|
||||||
.addOnSuccessListener { barcodeList ->
|
.addOnSuccessListener { barcodeList ->
|
||||||
|
println(barcodeList.map { e -> e.displayValue })
|
||||||
|
println(barcodeList.map { e -> e.format })
|
||||||
val barcode =
|
val barcode =
|
||||||
barcodeList.getOrNull(0)
|
barcodeList.getOrNull(0)
|
||||||
if (barcode != null)
|
if (barcode != null)
|
@ -20,6 +20,7 @@ object BarcodeFormatConverter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun formatToString(f: Int): String {
|
fun formatToString(f: Int): String {
|
||||||
return when (f) {
|
return when (f) {
|
||||||
Barcode.FORMAT_CODE_128 -> "CODE_128"
|
Barcode.FORMAT_CODE_128 -> "CODE_128"
|
||||||
|
@ -19,25 +19,25 @@ object BarcodeGenerator {
|
|||||||
android.graphics.Color.WHITE
|
android.graphics.Color.WHITE
|
||||||
}
|
}
|
||||||
|
|
||||||
fun generateBarcode(content: String?, f: String?, w: Int): Bitmap? {
|
fun generateBarcode(content: String, f: String, width: Int): Bitmap? {
|
||||||
if (content.isNullOrEmpty() || f.isNullOrEmpty()) {
|
if (content.isEmpty() || f.isEmpty()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
val format = stringToFormat(f)
|
val format = stringToFormat(f)
|
||||||
val writer = MultiFormatWriter()
|
val writer = MultiFormatWriter()
|
||||||
val height = (w * formatToRatio(format)).toInt()
|
val height = (formatToRatio(format) * width).toInt()
|
||||||
val width = (w * 1.0f).toInt()
|
|
||||||
|
|
||||||
|
|
||||||
val bitMatrix: BitMatrix = writer.encode(content, format, width, height)
|
val bitMatrix: BitMatrix = writer.encode(content, format, width, height)
|
||||||
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||||
|
|
||||||
for (x in 0 until width) {
|
for (x in 0 until width) {
|
||||||
for (y in 0 until height) {
|
for (y in 0 until height) {
|
||||||
bitmap.setPixel(
|
bitmap.setPixel(
|
||||||
x, y, getPixelColor(bitMatrix, x, y)
|
x,
|
||||||
|
y,
|
||||||
|
getPixelColor(bitMatrix, x, y)
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return bitmap
|
return bitmap
|
||||||
|
@ -1,23 +1,22 @@
|
|||||||
package net.helcel.fidelity.tools
|
package net.helcel.fidelity.tools
|
||||||
|
|
||||||
import android.content.Context
|
import android.app.Activity
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
|
||||||
object ErrorToaster {
|
object ErrorToaster {
|
||||||
private fun helper(activity: Context?, message: String, length: Int) {
|
private fun helper(activity: Activity, message: String, length: Int) {
|
||||||
if (activity != null)
|
Toast.makeText(activity, message, length).show()
|
||||||
Toast.makeText(activity, message, length).show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun noKP2AFound(activity: Context?) {
|
fun noKP2AFound(activity: Activity) {
|
||||||
helper(activity, "KeePass2Android Not Installed", Toast.LENGTH_LONG)
|
helper(activity, "KeePass2Android Not Installed", Toast.LENGTH_LONG)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun formIncomplete(activity: Context?) {
|
fun formIncomplete(activity: Activity) {
|
||||||
helper(activity, "Form Incomplete", Toast.LENGTH_SHORT)
|
helper(activity, "Form Incomplete", Toast.LENGTH_SHORT)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun invalidFormat(activity: Context?) {
|
fun invalidFormat(activity: Activity) {
|
||||||
helper(activity, "Invalid Format", Toast.LENGTH_SHORT)
|
helper(activity, "Invalid Format", Toast.LENGTH_SHORT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import android.os.Bundle
|
|||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import net.helcel.fidelity.pluginSDK.KeepassDef
|
import net.helcel.fidelity.pluginSDK.KeepassDefs
|
||||||
import net.helcel.fidelity.pluginSDK.Kp2aControl
|
import net.helcel.fidelity.pluginSDK.Kp2aControl
|
||||||
|
|
||||||
object KeepassWrapper {
|
object KeepassWrapper {
|
||||||
@ -25,8 +25,8 @@ object KeepassWrapper {
|
|||||||
|
|
||||||
val fields = HashMap<String?, String?>()
|
val fields = HashMap<String?, String?>()
|
||||||
val protected = ArrayList<String?>()
|
val protected = ArrayList<String?>()
|
||||||
fields[KeepassDef.TitleField] = title
|
fields[KeepassDefs.TitleField] = title
|
||||||
fields[KeepassDef.UrlField] =
|
fields[KeepassDefs.UrlField] =
|
||||||
"androidapp://" + fragment.requireActivity().packageName
|
"androidapp://" + fragment.requireActivity().packageName
|
||||||
fields[CODE_FIELD] = code
|
fields[CODE_FIELD] = code
|
||||||
fields[FORMAT_FIELD] = format
|
fields[FORMAT_FIELD] = format
|
||||||
@ -37,13 +37,33 @@ object KeepassWrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun resultLauncher(
|
fun resultLauncherAdd(
|
||||||
fragment: Fragment,
|
fragment: Fragment,
|
||||||
callback: (HashMap<String, String>) -> Unit
|
callback: (HashMap<String, String>) -> Unit
|
||||||
): ActivityResultLauncher<Intent> {
|
): ActivityResultLauncher<Intent> {
|
||||||
return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
if (result.resultCode == Activity.RESULT_OK) {
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
val credentials = Kp2aControl.getEntryFieldsFromIntent(result.data)
|
val data: Intent? = result.data
|
||||||
|
val credentials = Kp2aControl.getEntryFieldsFromIntent(
|
||||||
|
data!!
|
||||||
|
)
|
||||||
|
println(credentials)
|
||||||
|
callback(credentials)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resultLauncherQuery(
|
||||||
|
fragment: Fragment,
|
||||||
|
callback: (HashMap<String, String>) -> Unit
|
||||||
|
): ActivityResultLauncher<Intent> {
|
||||||
|
return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
val data: Intent? = result.data
|
||||||
|
val credentials = Kp2aControl.getEntryFieldsFromIntent(
|
||||||
|
data!!
|
||||||
|
)
|
||||||
|
println(credentials)
|
||||||
callback(credentials)
|
callback(credentials)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -51,7 +71,7 @@ object KeepassWrapper {
|
|||||||
|
|
||||||
fun entryExtract(map: HashMap<String, String>): Triple<String?, String?, String?> {
|
fun entryExtract(map: HashMap<String, String>): Triple<String?, String?, String?> {
|
||||||
return Triple(
|
return Triple(
|
||||||
map[KeepassDef.TitleField],
|
map[KeepassDefs.TitleField],
|
||||||
map[CODE_FIELD],
|
map[CODE_FIELD],
|
||||||
map[FORMAT_FIELD]
|
map[FORMAT_FIELD]
|
||||||
)
|
)
|
||||||
@ -65,10 +85,6 @@ object KeepassWrapper {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bundleCreate(triple: Triple<String?, String?, String?>): Bundle {
|
|
||||||
return bundleCreate(triple.first, triple.second, triple.third)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun bundleExtract(data: Bundle?): Triple<String?, String?, String?> {
|
fun bundleExtract(data: Bundle?): Triple<String?, String?, String?> {
|
||||||
return Triple(
|
return Triple(
|
||||||
data?.getString("title"),
|
data?.getString("title"),
|
||||||
|
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>
|
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>
|
@ -1,6 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/coordinator"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:fitsSystemWindows="true"
|
android:fitsSystemWindows="true"
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
android:padding="16dp">
|
android:padding="16dp">
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/nameInputLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
@ -22,11 +23,7 @@
|
|||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/editTextTitle"
|
android:id="@+id/editTextTitle"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content" />
|
||||||
android:imeOptions="actionNext"
|
|
||||||
android:inputType="text"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:minLines="1" />
|
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
@ -49,11 +46,7 @@
|
|||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/editTextCode"
|
android:id="@+id/editTextCode"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content" />
|
||||||
android:imeOptions="actionDone"
|
|
||||||
android:inputType="text"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:minLines="1" />
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
|
||||||
@ -71,6 +64,7 @@
|
|||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/formatInputLayout"
|
||||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@ -83,7 +77,6 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:focusable="false"
|
|
||||||
android:inputType="none" />
|
android:inputType="none" />
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
@ -9,8 +9,7 @@
|
|||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/fidelityList"
|
android:id="@+id/fidelityList"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent" />
|
||||||
android:layout_margin="24dp" />
|
|
||||||
|
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
@ -26,6 +25,7 @@
|
|||||||
app:srcCompat="@drawable/search" />
|
app:srcCompat="@drawable/search" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/expandedMenuContainer"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignParentEnd="true"
|
android:layout_alignParentEnd="true"
|
||||||
|
@ -11,26 +11,12 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
<net.helcel.fidelity.activity.view.ScannerView
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/bottomText"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="64dp"
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
||||||
android:id="@+id/btnScanDone"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentBottom="true"
|
android:layout_alignParentBottom="true"
|
||||||
android:layout_centerHorizontal="true"
|
android:background="#ffffff"
|
||||||
android:layout_margin="24dp"
|
android:textSize="24sp" />
|
||||||
android:contentDescription="@string/manual" />
|
|
||||||
|
|
||||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentBottom="true"
|
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:layout_margin="28dp"
|
|
||||||
android:indeterminate="true" />
|
|
||||||
|
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
@ -1,5 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
<TextView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/textViewFeelings"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:padding="15dp"
|
android:padding="15dp"
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<string name="kp2aplugin_title" tools:keep="@string/kp2aplugin_title">Fidelity</string>
|
<string name="kp2aplugin_title" tools:keep="@string/kp2aplugin_title">Fidelity</string>
|
||||||
<string name="kp2aplugin_shortdesc" tools:keep="@string/kp2aplugin_shortdesc">Fidelity adds an interface to manage fidelity cards and other barcodes to Keepass2Android</string>
|
<string name="kp2aplugin_shortdesc">Stores and Displays fidelity and other cards</string>
|
||||||
<string name="kp2aplugin_author" tools:keep="@string/kp2aplugin_author">Soraefir</string>
|
<string name="kp2aplugin_author" tools:keep="@string/kp2aplugin_author">[soraefir](soraefir)</string>
|
||||||
|
|
||||||
<string name="app_name">Keepass Fidelity</string>
|
<string name="app_name">Keepass Fidelity</string>
|
||||||
|
|
||||||
|
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>
|
@ -1,12 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<style name="Theme.Fidelity" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
<style name="Theme.Fidelity" parent="Theme.Material3.DayNight.NoActionBar">
|
||||||
|
|
||||||
|
<item name="colorPrimary">?attr/colorAccent</item>
|
||||||
|
|
||||||
<item name="colorPrimary">#7DB9F5</item>
|
|
||||||
<item name="colorPrimaryVariant">#7DB9F5</item>
|
|
||||||
<item name="colorSecondary">#7DB9F5</item>
|
|
||||||
<item name="colorSecondaryVariant">#7DB9F5</item>
|
|
||||||
<item name="colorOnPrimary">#030B12</item>
|
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
@ -2,6 +2,6 @@
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application' version '8.3.1' apply false
|
id 'com.android.application' version '8.3.1' apply false
|
||||||
id 'com.android.library' version '8.3.1' apply false
|
id 'com.android.library' version '8.3.0' apply false
|
||||||
id 'org.jetbrains.kotlin.android' version '1.9.23' apply false
|
id 'org.jetbrains.kotlin.android' version '1.9.23' apply false
|
||||||
}
|
}
|
@ -15,7 +15,7 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
|||||||
# Android operating system, and which are packaged with your app's APK
|
# Android operating system, and which are packaged with your app's APK
|
||||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=false
|
android.enableJetifier=true
|
||||||
# Kotlin code style for this project: "official" or "obsolete":
|
# Kotlin code style for this project: "official" or "obsolete":
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
# Enables namespacing of each library's R class so that its R class includes only the
|
# Enables namespacing of each library's R class so that its R class includes only the
|
||||||
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
@ -14,5 +14,5 @@ dependencyResolutionManagement {
|
|||||||
maven { url 'https://jitpack.io' }
|
maven { url 'https://jitpack.io' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rootProject.name = "Fidelity"
|
rootProject.name = "BeenDroid"
|
||||||
include ':app'
|
include ':app'
|
||||||
|
Reference in New Issue
Block a user