12 Commits

Author SHA1 Message Date
517f0240e3 CI release 2024-03-23 15:12:15 +01:00
b02920ca41 CI Autorelease 2024-03-23 15:03:09 +01:00
b289648260 CI Artifact 2024-03-23 14:25:04 +01:00
b6de7ca409 CI fix tag 2024-03-23 14:13:37 +01:00
12fb04974e CI on tag 2024-03-23 14:09:54 +01:00
cdd4d6db1b Fix CI 2024-03-23 13:59:01 +01:00
a403bdea61 CI testing 2024-03-23 13:50:35 +01:00
657489e255 Merge pull request 'Update plugin com.android.library to v8.3.1' (#4) from renovate/com.android.library-8.x into main
Reviewed-on: #4
2024-03-23 13:32:22 +01:00
95f609bc53 Merge pull request 'Update dependency gradle to v8.7' (#5) from renovate/gradle-8.x into main
Reviewed-on: #5
2024-03-23 13:32:06 +01:00
f14929cdf6 Prepare R1.0 2024-03-23 13:21:24 +01:00
ab5c32f2db Update dependency gradle to v8.7 2024-03-23 01:05:44 +00:00
cd8a84531e Update plugin com.android.library to v8.3.1 2024-03-23 01:05:33 +00:00
20 changed files with 185 additions and 470 deletions

54
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,54 @@
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
permissions:
contents: write
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.apk
path: app/build/outputs/apk/release/app-release-unsigned.apk
- name: Release
uses: softprops/action-gh-release@v2
with:
files: |
app/build/outputs/apk/release/app-release-unsigned.apk

2
.gitignore vendored
View File

@ -7,6 +7,8 @@ local.properties/
.DS_Store .DS_Store
build/ build/
app/build/ app/build/
app/debug/
app/release/
captures/ captures/
.externalNativeBuild .externalNativeBuild
.cxx .cxx

View File

@ -11,6 +11,7 @@ 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
@ -18,18 +19,29 @@ android {
} }
buildTypes { buildTypes {
debug {
debuggable true
}
release { release {
minifyEnabled false minifyEnabled true
shrinkResources false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
} }
} }
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 coreLibraryDesugaringEnabled true
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
encoding 'utf-8'
} }
kotlinOptions { kotlinOptions {
jvmTarget = '1.8' jvmTarget = JavaVersion.VERSION_17
} }
buildFeatures { buildFeatures {
viewBinding true viewBinding true
} }
@ -37,12 +49,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'

View File

@ -1,5 +1,6 @@
<?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,27 +22,13 @@
<receiver <receiver
android:name=".pluginSDK.PluginAccessBroadcastReceiver" android:name=".pluginSDK.PluginAccessBroadcastReceiver"
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_ENTRY_OUTPUT_MODIFIED" />
<action android:name="keepass2android.ACTION_CLOSE_ENTRY_VIEW" />
<action android:name="keepass2android.ACTION_ADD_ENTRY_ACTION" />
</intent-filter>
</receiver>
</application> </application>
</manifest> </manifest>

View File

@ -9,8 +9,12 @@ 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") @SuppressLint("SourceLockedOrientationActivity")
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@ -38,7 +42,9 @@ class MainActivity : AppCompatActivity() {
} }
} }
if (savedInstanceState == null) if (intent.extras != null)
loadViewEntry()
else if (savedInstanceState == null)
loadLauncher() loadLauncher()
} }
@ -47,5 +53,14 @@ class MainActivity : AppCompatActivity() {
.replace(R.id.container, Launcher()) .replace(R.id.container, Launcher())
.commit() .commit()
} }
private fun loadViewEntry() {
val viewEntry = ViewEntry()
val data = getEntryFieldsFromIntent(intent)
viewEntry.arguments = bundleCreate(entryExtract(data))
supportFragmentManager.beginTransaction()
.replace(R.id.container, viewEntry)
.commit()
}
} }

View File

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

View File

@ -7,9 +7,11 @@ 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
@ -34,7 +36,7 @@ class CreateEntry : Fragment() {
startViewEntry(r.first, r.second, r.third) startViewEntry(r.first, r.second, r.third)
} }
private var isValid: Boolean = false private var isValidBarcode: Boolean = false
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -51,21 +53,89 @@ 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 = { binding.editTextCode.addTextChangedListener { changeListener() }
isValid = false binding.editTextFormat.addTextChangedListener { changeListener() }
binding.editTextFormat.addTextChangedListener { binding.editTextFormat.error = null }
binding.btnSave.setOnClickListener { submit() }
binding.editTextTitle.onDone { submit() }
binding.editTextCode.onDone { submit() }
updatePreview()
return binding.root
}
private fun updatePreview() {
try {
val barcodeBitmap = generateBarcode(
binding.editTextCode.text.toString(),
binding.editTextFormat.text.toString(),
600
)
binding.imageViewPreview.setImageBitmap(barcodeBitmap)
isValidBarcode = true
} catch (e: FormatException) {
binding.imageViewPreview.setImageBitmap(null)
binding.editTextCode.error = "Invalid format"
} catch (e: IllegalArgumentException) {
binding.imageViewPreview.setImageBitmap(null)
binding.editTextCode.error = e.message
} catch (e: Exception) {
binding.imageViewPreview.setImageBitmap(null)
e.printStackTrace()
}
}
private fun isValidForm(): Boolean {
var valid = true
if (binding.editTextFormat.text.isNullOrEmpty()) {
valid = false
binding.editTextFormat.error = "Format cannot be empty"
}
if (binding.editTextCode.text.isNullOrEmpty()) {
valid = false
binding.editTextCode.error = "Code cannot be empty"
}
if (binding.editTextTitle.text.isNullOrEmpty()) {
valid = false
binding.editTextTitle.error = "Title cannot be empty"
}
return valid
}
private fun startViewEntry(title: String?, code: String?, fmt: String?) {
val viewEntryFragment = ViewEntry()
viewEntryFragment.arguments = KeepassWrapper.bundleCreate(title, code, fmt)
requireActivity().supportFragmentManager.beginTransaction()
.replace(R.id.container, viewEntryFragment).commit()
}
private fun changeListener() {
isValidBarcode = false
handler.removeCallbacksAndMessages(null) handler.removeCallbacksAndMessages(null)
handler.postDelayed({ handler.postDelayed({
updatePreview() updatePreview()
}, DEBOUNCE_DELAY) }, DEBOUNCE_DELAY)
} }
binding.editTextCode.addTextChangedListener { changeListener() }
binding.editTextFormat.addTextChangedListener { changeListener() }
binding.editTextFormat.addTextChangedListener { binding.editTextFormat.error = null }
binding.btnSave.setOnClickListener {
if (!isValid() || !isValid) {
ErrorToaster.formIncomplete(requireActivity())
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 { } else {
val kpEntry = KeepassWrapper.entryCreate( val kpEntry = KeepassWrapper.entryCreate(
this, this,
@ -82,60 +152,11 @@ class CreateEntry : Fragment() {
) )
) )
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
ErrorToaster.noKP2AFound(requireActivity()) ErrorToaster.noKP2AFound(context)
}
}
}
updatePreview()
return binding.root
}
private fun updatePreview() {
try {
val barcodeBitmap = generateBarcode(
binding.editTextCode.text.toString(),
binding.editTextFormat.text.toString(),
600
)
binding.imageViewPreview.setImageBitmap(barcodeBitmap)
isValid = true
} catch (e: FormatException) {
binding.imageViewPreview.setImageBitmap(null)
binding.editTextCode.error = "Invalid format"
} catch (e: IllegalArgumentException) {
binding.imageViewPreview.setImageBitmap(null)
binding.editTextCode.error = e.message
} catch (e: Exception) { } catch (e: Exception) {
binding.imageViewPreview.setImageBitmap(null)
e.printStackTrace() e.printStackTrace()
} }
} }
private fun isValid(): Boolean {
var valid = true
if (binding.editTextTitle.text.isNullOrEmpty()) {
valid = false
binding.editTextTitle.error = "Title cannot be empty"
}
if (binding.editTextCode.text.isNullOrEmpty()) {
valid = false
binding.editTextCode.error = "Code cannot be empty"
}
if (binding.editTextFormat.text.isNullOrEmpty()) {
valid = false
binding.editTextFormat.error = "Format cannot be empty"
}
return valid
}
private fun startViewEntry(title: String?, code: String?, fmt: String?) {
val viewEntryFragment = ViewEntry()
viewEntryFragment.arguments = KeepassWrapper.bundleCreate(title, code, fmt)
requireActivity().supportFragmentManager.beginTransaction()
.replace(R.id.container, viewEntryFragment).commit()
} }
} }

View File

@ -6,9 +6,6 @@ import org.json.JSONArray
import org.json.JSONException import org.json.JSONException
class PluginAccessException(msg: String) : Exception(msg)
object AccessManager { object AccessManager {
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"
@ -94,12 +91,4 @@ object AccessManager {
hostPrefs.edit().remove(hostPackage).apply() hostPrefs.edit().remove(hostPackage).apply()
} }
} }
fun getAccessToken(
context: Context, hostPackage: String?,
scopes: ArrayList<String?>
): String {
return tryGetAccessToken(context, hostPackage, scopes)
?: throw PluginAccessException(hostPackage + scopes)
}
} }

View File

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

View File

@ -32,10 +32,10 @@ object Kp2aControl {
fun getEntryFieldsFromIntent(intent: Intent?): HashMap<String, String> { 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 iter = json.keys() val itr = json.keys()
while (iter.hasNext()) { while (itr.hasNext()) {
val key = iter.next() val key = itr.next()
val value = json[key].toString() val value = json[key].toString()
res[key] = value res[key] = value
} }

View File

@ -11,7 +11,7 @@ class PluginAccessBroadcastReceiver : BroadcastReceiver() {
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)
Strings.ACTION_REVOKE_ACCESS -> revokeAccess(ctx, intent) Strings.ACTION_REVOKE_ACCESS -> revokeAccess(ctx, intent)
else -> println(action) else -> {}
} }
} }
@ -46,8 +46,6 @@ class PluginAccessBroadcastReceiver : BroadcastReceiver() {
private val scopes: ArrayList<String?> = ArrayList( private val scopes: ArrayList<String?> = ArrayList(
listOf( listOf(
Strings.SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE, Strings.SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE,
Strings.SCOPE_DATABASE_ACTIONS,
Strings.SCOPE_CURRENT_ENTRY,
) )
) )
} }

View File

@ -1,226 +0,0 @@
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)
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
fun addEntryAction(
actionDisplayText: String?,
actionIconResourceId: Int,
actionData: Bundle?
) {
addEntryFieldAction(null, null, actionDisplayText, actionIconResourceId, actionData)
}
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 ?: return
Log.d(
"KP2A.pluginsdk",
"received broadcast in PluginActionBroadcastReceiver with action=$action"
)
println(action)
when (action) {
Strings.ACTION_OPEN_ENTRY -> openEntry(OpenEntryAction(ctx, intent))
Strings.ACTION_CLOSE_ENTRY_VIEW -> closeEntryView(CloseEntryViewAction(ctx, intent))
Strings.ACTION_ENTRY_ACTION_SELECTED ->
actionSelected(ActionSelectedAction(ctx, intent))
Strings.ACTION_ENTRY_OUTPUT_MODIFIED ->
entryOutputModified(EntryOutputModifiedAction(ctx, intent))
Strings.ACTION_LOCK_DATABASE -> dbAction(DatabaseAction(ctx, intent))
Strings.ACTION_UNLOCK_DATABASE -> dbAction(DatabaseAction(ctx, intent))
Strings.ACTION_OPEN_DATABASE -> dbAction(DatabaseAction(ctx, intent))
Strings.ACTION_CLOSE_DATABASE -> dbAction(DatabaseAction(ctx, intent))
else -> println(action)
}
}
private fun closeEntryView(closeEntryView: CloseEntryViewAction?) {}
private fun actionSelected(actionSelected: ActionSelectedAction?) {}
private fun openEntry(oe: OpenEntryAction?) {}
private fun entryOutputModified(eom: EntryOutputModifiedAction?) {}
private fun dbAction(db: DatabaseAction?) {}
}

View File

@ -1,176 +1,30 @@
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"
/**
* 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 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 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"
} }

View File

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

View File

@ -42,12 +42,8 @@ object KeepassWrapper {
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 ->
println(result.resultCode)
println(result.data.toString())
if (result.resultCode == Activity.RESULT_OK) { if (result.resultCode == Activity.RESULT_OK) {
val credentials = Kp2aControl.getEntryFieldsFromIntent(result.data) val credentials = Kp2aControl.getEntryFieldsFromIntent(result.data)
println(credentials.toList().toString())
callback(credentials) callback(credentials)
} }
} }
@ -69,6 +65,10 @@ object KeepassWrapper {
return data return data
} }
fun bundleCreate(triple: Triple<String?, String?, String?>): Bundle {
return bundleCreate(triple.first, triple.second, triple.third)
}
fun bundleExtract(data: Bundle?): Triple<String?, String?, String?> { fun bundleExtract(data: Bundle?): Triple<String?, String?, String?> {
return Triple( return Triple(
data?.getString("title"), data?.getString("title"),

View File

@ -22,7 +22,11 @@
<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>
@ -45,7 +49,11 @@
<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>
@ -75,6 +83,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_weight="1" android:layout_weight="1"
android:focusable="false"
android:inputType="none" /> android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>

View File

@ -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.0' apply false id 'com.android.library' version '8.3.1' apply false
id 'org.jetbrains.kotlin.android' version '1.9.23' apply false id 'org.jetbrains.kotlin.android' version '1.9.23' apply false
} }

Binary file not shown.

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View File

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