Update plugin com.android.application to v8.3.1 #3
| @@ -20,12 +20,13 @@ | ||||
|         </activity> | ||||
|  | ||||
|         <receiver | ||||
|             android:name=".pluginSDK.PluginAccessReceiver" | ||||
|             android:name=".pluginSDK.PluginAccessBroadcastReceiver" | ||||
|             android:exported="true"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="keepass2android.ACTION_TRIGGER_REQUEST_ACCESS" /> | ||||
|                 <action android:name="keepass2android.ACTION_RECEIVE_ACCESS" /> | ||||
|                 <action android:name="keepass2android.ACTION_REVOKE_ACCESS" /> | ||||
|  | ||||
|             </intent-filter> | ||||
|         </receiver> | ||||
|  | ||||
| @@ -34,16 +35,13 @@ | ||||
|             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_ENTRY_ACTION_SELECTED" /> | ||||
|                 <action android:name="keepass2android.ACTION_ADD_ENTRY_ACTION" /> | ||||
|  | ||||
|  | ||||
|                 <action android:name="keepass2android.ACTION_LOCK_DATABASE" /> | ||||
|                 <action android:name="keepass2android.ACTION_UNLOCK_DATABASE" /> | ||||
|                 <action android:name="keepass2android.ACTION_CLOSE_DATABASE" /> | ||||
|                 <action android:name="keepass2android.ACTION_OPEN_DATABASE" /> | ||||
|             </intent-filter> | ||||
|         </receiver> | ||||
|  | ||||
|     </application> | ||||
|  | ||||
| </manifest> | ||||
| @@ -1,7 +1,9 @@ | ||||
| package net.helcel.fidelity.activity | ||||
|  | ||||
| import android.annotation.SuppressLint | ||||
| import android.content.Context | ||||
| import android.content.SharedPreferences | ||||
| import android.content.pm.ActivityInfo | ||||
| import android.os.Bundle | ||||
| import androidx.activity.addCallback | ||||
| import androidx.appcompat.app.AppCompatActivity | ||||
| @@ -10,10 +12,10 @@ import net.helcel.fidelity.activity.fragment.Launcher | ||||
| import net.helcel.fidelity.databinding.ActMainBinding | ||||
| import net.helcel.fidelity.tools.CacheManager | ||||
|  | ||||
| @SuppressLint("SourceLockedOrientationActivity") | ||||
| class MainActivity : AppCompatActivity() { | ||||
|  | ||||
|     private lateinit var binding: ActMainBinding | ||||
|  | ||||
|     private lateinit var sharedPreferences: SharedPreferences | ||||
|  | ||||
|  | ||||
| @@ -23,25 +25,26 @@ class MainActivity : AppCompatActivity() { | ||||
|             this.getSharedPreferences(CacheManager.PREF_NAME, Context.MODE_PRIVATE) | ||||
|         CacheManager.loadFidelity(sharedPreferences) | ||||
|  | ||||
|          | ||||
|         binding = ActMainBinding.inflate(layoutInflater) | ||||
|         setContentView(binding.root) | ||||
|  | ||||
|         onBackPressedDispatcher.addCallback(this) { | ||||
|             if (supportFragmentManager.backStackEntryCount > 0) { | ||||
|                 supportFragmentManager.popBackStackImmediate() | ||||
|                 loadLauncher() | ||||
|                 requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT | ||||
|             } else { | ||||
|                 finish() | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (savedInstanceState == null) | ||||
|             loadLauncher() | ||||
|  | ||||
|     } | ||||
|  | ||||
|     private fun loadLauncher() { | ||||
|         supportFragmentManager.beginTransaction() | ||||
|             .add(R.id.container, Launcher()) | ||||
|             .replace(R.id.container, Launcher()) | ||||
|             .commit() | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -26,7 +26,7 @@ class CreateEntry : Fragment() { | ||||
|     private val handler = Handler(Looper.getMainLooper()) | ||||
|     private lateinit var binding: FragCreateEntryBinding | ||||
|  | ||||
|     private val resultLauncherAdd = KeepassWrapper.resultLauncherAdd(this) { | ||||
|     private val resultLauncherAdd = KeepassWrapper.resultLauncher(this) { | ||||
|         val r = KeepassWrapper.entryExtract(it) | ||||
|         if (!KeepassWrapper.isProtected(it)) { | ||||
|             CacheManager.addFidelity(r) | ||||
| @@ -67,7 +67,7 @@ class CreateEntry : Fragment() { | ||||
|                 ErrorToaster.formIncomplete(requireActivity()) | ||||
|  | ||||
|             } else { | ||||
|                 val kpentry = KeepassWrapper.entryCreate( | ||||
|                 val kpEntry = KeepassWrapper.entryCreate( | ||||
|                     this, | ||||
|                     binding.editTextTitle.text.toString(), | ||||
|                     binding.editTextCode.text.toString(), | ||||
| @@ -77,8 +77,8 @@ class CreateEntry : Fragment() { | ||||
|                 try { | ||||
|                     resultLauncherAdd.launch( | ||||
|                         Kp2aControl.getAddEntryIntent( | ||||
|                             kpentry.first, | ||||
|                             kpentry.second | ||||
|                             kpEntry.first, | ||||
|                             kpEntry.second | ||||
|                         ) | ||||
|                     ) | ||||
|                 } catch (e: ActivityNotFoundException) { | ||||
| @@ -108,23 +108,21 @@ class CreateEntry : Fragment() { | ||||
|             binding.editTextCode.error = e.message | ||||
|         } catch (e: Exception) { | ||||
|             binding.imageViewPreview.setImageBitmap(null) | ||||
|             println(e.javaClass) | ||||
|             println(e.message) | ||||
|             e.printStackTrace() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun isValid(): Boolean { | ||||
|         var valid = true | ||||
|         if (binding.editTextTitle.text!!.isEmpty()) { | ||||
|         if (binding.editTextTitle.text.isNullOrEmpty()) { | ||||
|             valid = false | ||||
|             binding.editTextTitle.error = "Title cannot be empty" | ||||
|         } | ||||
|         if (binding.editTextCode.text!!.isEmpty()) { | ||||
|         if (binding.editTextCode.text.isNullOrEmpty()) { | ||||
|             valid = false | ||||
|             binding.editTextCode.error = "Code cannot be empty" | ||||
|         } | ||||
|         if (binding.editTextFormat.text!!.isEmpty()) { | ||||
|         if (binding.editTextFormat.text.isNullOrEmpty()) { | ||||
|             valid = false | ||||
|             binding.editTextFormat.error = "Format cannot be empty" | ||||
|         } | ||||
|   | ||||
| @@ -23,7 +23,7 @@ class Launcher : Fragment() { | ||||
|     private lateinit var binding: FragLauncherBinding | ||||
|     private lateinit var fidelityListAdapter: FidelityListAdapter | ||||
|  | ||||
|     private val resultLauncherQuery = KeepassWrapper.resultLauncherQuery(this) { | ||||
|     private val resultLauncherQuery = KeepassWrapper.resultLauncher(this) { | ||||
|         val r = KeepassWrapper.entryExtract(it) | ||||
|         if (!KeepassWrapper.isProtected(it)) { | ||||
|             CacheManager.addFidelity(r) | ||||
| @@ -80,7 +80,7 @@ class Launcher : Fragment() { | ||||
|  | ||||
|     private fun startGetFromKeepass() { | ||||
|         try { | ||||
|             this.resultLauncherQuery.launch(Kp2aControl.queryEntryIntentForOwnPackage) | ||||
|             this.resultLauncherQuery.launch(Kp2aControl.getQueryEntryForOwnPackageIntent()) | ||||
|         } catch (e: ActivityNotFoundException) { | ||||
|             ErrorToaster.noKP2AFound(requireActivity()) | ||||
|         } | ||||
|   | ||||
| @@ -27,7 +27,6 @@ class Scanner : Fragment() { | ||||
|  | ||||
|     private var code: String = "" | ||||
|     private var fmt: String = "" | ||||
|     private var valid: Boolean = false | ||||
|  | ||||
|     override fun onCreateView( | ||||
|         inflater: LayoutInflater, | ||||
| @@ -35,13 +34,14 @@ class Scanner : Fragment() { | ||||
|         savedInstanceState: Bundle? | ||||
|     ): View { | ||||
|         binding = FragScannerBinding.inflate(layoutInflater) | ||||
|         binding.bottomText.setOnClickListener { | ||||
|         binding.btnScanDone.setOnClickListener { | ||||
|             startCreateEntry() | ||||
|         } | ||||
|         when (hasCameraPermission()) { | ||||
|             true -> bindCameraUseCases() | ||||
|             else -> requestPermission() | ||||
|         } | ||||
|         binding.btnScanDone.isEnabled = false | ||||
|         return binding.root | ||||
|     } | ||||
|  | ||||
| @@ -92,9 +92,10 @@ class Scanner : Fragment() { | ||||
|                 if (code != null && format != null) { | ||||
|                     this.code = code | ||||
|                     this.fmt = format | ||||
|                     this.valid = true | ||||
|                     binding.btnScanDone.isEnabled = true | ||||
|  | ||||
|                 } else { | ||||
|                     this.valid = false | ||||
|                     binding.btnScanDone.isEnabled = false | ||||
|                 } | ||||
|             } | ||||
|             try { | ||||
| @@ -111,6 +112,4 @@ class Scanner : Fragment() { | ||||
|             } | ||||
|         }, ContextCompat.getMainExecutor(requireContext())) | ||||
|     } | ||||
|  | ||||
|  | ||||
| } | ||||
| @@ -1,10 +1,14 @@ | ||||
| package net.helcel.fidelity.activity.fragment | ||||
|  | ||||
| import android.annotation.SuppressLint | ||||
| import android.content.pm.ActivityInfo | ||||
| import android.content.res.Configuration | ||||
| import android.os.Bundle | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| 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 com.google.zxing.FormatException | ||||
| import net.helcel.fidelity.databinding.FragViewEntryBinding | ||||
| @@ -12,16 +16,14 @@ import net.helcel.fidelity.tools.BarcodeGenerator.generateBarcode | ||||
| import net.helcel.fidelity.tools.ErrorToaster | ||||
| import net.helcel.fidelity.tools.KeepassWrapper | ||||
|  | ||||
|  | ||||
| @SuppressLint("SourceLockedOrientationActivity") | ||||
| class ViewEntry : Fragment() { | ||||
|  | ||||
|     private lateinit var binding: FragViewEntryBinding | ||||
|  | ||||
|     private var title: String? = null | ||||
|     private var code: String? = null | ||||
|     private var fmt: String? = null | ||||
|  | ||||
|  | ||||
|     override fun onCreateView( | ||||
|         inflater: LayoutInflater, | ||||
|         container: ViewGroup?, | ||||
| @@ -33,8 +35,15 @@ class ViewEntry : Fragment() { | ||||
|         code = res.second | ||||
|         fmt = res.third | ||||
|  | ||||
|         adjustLayout() | ||||
|         updatePreview() | ||||
|         updateLayout() | ||||
|  | ||||
|         binding.imageViewPreview.setOnClickListener { | ||||
|             requireActivity().requestedOrientation = | ||||
|                 if (isLandscape()) ActivityInfo.SCREEN_ORIENTATION_PORTRAIT | ||||
|                 else ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE | ||||
|         } | ||||
|  | ||||
|         return binding.root | ||||
|     } | ||||
|  | ||||
| @@ -42,7 +51,7 @@ class ViewEntry : Fragment() { | ||||
|         binding.title.text = title | ||||
|         try { | ||||
|             val barcodeBitmap = generateBarcode( | ||||
|                 code!!, fmt!!, 1024 | ||||
|                 code, fmt, 1024 | ||||
|             ) | ||||
|             binding.imageViewPreview.setImageBitmap(barcodeBitmap) | ||||
|         } catch (e: FormatException) { | ||||
| @@ -53,23 +62,25 @@ class ViewEntry : Fragment() { | ||||
|             ErrorToaster.invalidFormat(requireActivity()) | ||||
|         } catch (e: Exception) { | ||||
|             binding.imageViewPreview.setImageBitmap(null) | ||||
|             println(e.javaClass) | ||||
|             println(e.message) | ||||
|             e.printStackTrace() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     override fun onConfigurationChanged(newConfig: Configuration) { | ||||
|         super.onConfigurationChanged(newConfig) | ||||
|         adjustLayout() | ||||
|     } | ||||
|  | ||||
|     private fun adjustLayout() { | ||||
|         if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { | ||||
|     private fun updateLayout() { | ||||
|         if (isLandscape()) { | ||||
|             binding.title.visibility = View.GONE | ||||
|             setScreenBrightness(BRIGHTNESS_OVERRIDE_FULL) | ||||
|         } else { | ||||
|             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 | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,45 @@ | ||||
| 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,141 +2,87 @@ package net.helcel.fidelity.pluginSDK | ||||
|  | ||||
| import android.content.Context | ||||
| import android.content.SharedPreferences | ||||
| import android.content.pm.PackageManager | ||||
| import android.text.TextUtils | ||||
| import android.util.Log | ||||
| import org.json.JSONArray | ||||
| import org.json.JSONException | ||||
|  | ||||
|  | ||||
| class PluginAccessException(msg: String) : Exception(msg) | ||||
|  | ||||
|  | ||||
| object AccessManager { | ||||
|     private const val _tag = "Kp2aPluginSDK" | ||||
|     private const val PREF_KEY_SCOPE = "scope" | ||||
|     private const val PREF_KEY_TOKEN = "token" | ||||
|  | ||||
|     private fun stringArrayToString(values: ArrayList<String?>): String? { | ||||
|         if (values.isEmpty()) return null | ||||
|         val a = JSONArray() | ||||
|         for (i in values.indices) { | ||||
|             a.put(values[i]) | ||||
|         } | ||||
|         return if (values.isNotEmpty()) { | ||||
|             a.toString() | ||||
|         } else { | ||||
|             null | ||||
|         } | ||||
|         values.forEach { a.put(it) } | ||||
|         return a.toString() | ||||
|     } | ||||
|  | ||||
|     private fun stringToStringArray(s: String?): ArrayList<String> { | ||||
|         val strings = ArrayList<String>() | ||||
|         if (!TextUtils.isEmpty(s)) { | ||||
|         if (s.isNullOrEmpty()) return strings | ||||
|  | ||||
|         try { | ||||
|             val a = JSONArray(s) | ||||
|                 for (i in 0 until a.length()) { | ||||
|                     val url = a.optString(i) | ||||
|                     strings.add(url) | ||||
|                 } | ||||
|             for (i in 0 until a.length()) | ||||
|                 strings.add(a.optString(i)) | ||||
|         } catch (e: JSONException) { | ||||
|             e.printStackTrace() | ||||
|         } | ||||
|         } | ||||
|         return strings | ||||
|     } | ||||
|  | ||||
|     fun storeAccessToken( | ||||
|         ctx: Context, | ||||
|         hostPackage: String, | ||||
|         accessToken: String, | ||||
|         hostPackage: String?, | ||||
|         accessToken: String?, | ||||
|         scopes: ArrayList<String?> | ||||
|     ) { | ||||
|         val prefs = getPrefsForHost(ctx, hostPackage) | ||||
|  | ||||
|         val edit = prefs.edit() | ||||
|         edit.putString(PREF_KEY_TOKEN, accessToken) | ||||
|         val scopesString = stringArrayToString(scopes) | ||||
|         edit.putString(PREF_KEY_SCOPE, scopesString) | ||||
|         edit.apply() | ||||
|         Log.d( | ||||
|             _tag, | ||||
|             "stored access token " + accessToken.substring( | ||||
|                 0, | ||||
|                 4 | ||||
|             ) + "... for " + scopes.size + " scopes (" + scopesString + ")." | ||||
|         ) | ||||
|  | ||||
|         val hostPrefs = ctx.getSharedPreferences("KP2A.PluginAccess.hosts", Context.MODE_PRIVATE) | ||||
|         if (!hostPrefs.contains(hostPackage)) { | ||||
|         if (!hostPrefs.contains(hostPackage)) | ||||
|             hostPrefs.edit().putString(hostPackage, "").apply() | ||||
|     } | ||||
|     } | ||||
|  | ||||
|     fun preparePopup(popupMenu: Any) { | ||||
|         try { | ||||
|             val fields = popupMenu.javaClass.declaredFields | ||||
|             for (field in fields) { | ||||
|                 if ("mPopup" == field.name) { | ||||
|                     field.isAccessible = true | ||||
|                     val menuPopupHelper = field[popupMenu] | ||||
|                     val classPopupHelper = Class.forName( | ||||
|                         menuPopupHelper | ||||
|                             .javaClass.name | ||||
|                     ) | ||||
|                     val setForceIcons = classPopupHelper.getMethod( | ||||
|                         "setForceShowIcon", Boolean::class.javaPrimitiveType | ||||
|                     ) | ||||
|                     setForceIcons.invoke(menuPopupHelper, true) | ||||
|                     break | ||||
|                 } | ||||
|             } | ||||
|         } catch (e: Exception) { | ||||
|             e.printStackTrace() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun getPrefsForHost( | ||||
|         ctx: Context, | ||||
|         hostPackage: String | ||||
|         hostPackage: String? | ||||
|     ): SharedPreferences { | ||||
|         val prefs = ctx.getSharedPreferences("KP2A.PluginAccess.$hostPackage", Context.MODE_PRIVATE) | ||||
|         return prefs | ||||
|         return ctx.getSharedPreferences("KP2A.PluginAccess.$hostPackage", Context.MODE_PRIVATE) | ||||
|     } | ||||
|  | ||||
|     fun tryGetAccessToken(ctx: Context, hostPackage: String, scopes: ArrayList<String?>): String? { | ||||
|         if (TextUtils.isEmpty(hostPackage)) { | ||||
|             Log.d(_tag, "hostPackage is empty!") | ||||
|             return null | ||||
|         } | ||||
|         Log.d(_tag, "trying to find prefs for $hostPackage") | ||||
|     fun tryGetAccessToken(ctx: Context, hostPackage: String?, scopes: ArrayList<String?>): String? { | ||||
|         if (hostPackage.isNullOrEmpty()) return null | ||||
|  | ||||
|         val prefs = getPrefsForHost(ctx, hostPackage) | ||||
|         val scopesString = prefs.getString(PREF_KEY_SCOPE, "") | ||||
|         Log.d(_tag, "available scopes: $scopesString") | ||||
|         val currentScope = stringToStringArray(scopesString) | ||||
|         if (isSubset(scopes, currentScope)) { | ||||
|             return prefs.getString(PREF_KEY_TOKEN, null) | ||||
|         } else { | ||||
|             Log.d(_tag, "looks like scope changed. Access token invalid.") | ||||
|         if (!isSubset(scopes, currentScope)) | ||||
|             return null | ||||
|         } | ||||
|         return prefs.getString(PREF_KEY_TOKEN, null) | ||||
|  | ||||
|     } | ||||
|  | ||||
|     private fun isSubset( | ||||
|         requiredScopes: ArrayList<String?>, | ||||
|         availableScopes: ArrayList<String> | ||||
|     ): Boolean { | ||||
|         for (r in requiredScopes) { | ||||
|             if (availableScopes.indexOf(r) < 0) { | ||||
|                 Log.d(_tag, "Scope " + r + " not available. " + availableScopes.size) | ||||
|                 return false | ||||
|             } | ||||
|         } | ||||
|         return true | ||||
|         return availableScopes.containsAll(requiredScopes) | ||||
|     } | ||||
|  | ||||
|     fun removeAccessToken( | ||||
|         ctx: Context, hostPackage: String, | ||||
|         accessToken: String | ||||
|         ctx: Context, hostPackage: String?, | ||||
|         accessToken: String? | ||||
|     ) { | ||||
|         val prefs = getPrefsForHost(ctx, hostPackage) | ||||
|  | ||||
|         Log.d(_tag, "removing AccessToken.") | ||||
|         if (prefs.getString(PREF_KEY_TOKEN, "") == accessToken) { | ||||
|             val edit = prefs.edit() | ||||
|             edit.clear() | ||||
| @@ -149,31 +95,11 @@ object AccessManager { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     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, | ||||
|         context: Context, hostPackage: String?, | ||||
|         scopes: ArrayList<String?> | ||||
|     ): String { | ||||
|         val accessToken = tryGetAccessToken(context, hostPackage, scopes) | ||||
|             ?: throw PluginAccessException(hostPackage, scopes) | ||||
|         return accessToken | ||||
|         return tryGetAccessToken(context, hostPackage, scopes) | ||||
|             ?: throw PluginAccessException(hostPackage + scopes) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,9 @@ | ||||
| package net.helcel.fidelity.pluginSDK | ||||
|  | ||||
| object KeepassDef { | ||||
|     var TitleField: String = "Title" | ||||
|     var UserNameField: String = "UserName" | ||||
|     var PasswordField: String = "Password" | ||||
|     var UrlField: String = "URL" | ||||
|     var NotesField: String = "Notes" | ||||
| } | ||||
| @@ -1,45 +0,0 @@ | ||||
| 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,96 +1,38 @@ | ||||
| package net.helcel.fidelity.pluginSDK | ||||
|  | ||||
| import android.content.Intent | ||||
| import android.text.TextUtils | ||||
| import org.json.JSONException | ||||
| import org.json.JSONObject | ||||
|  | ||||
| object Kp2aControl { | ||||
|     /** | ||||
|      * Creates and returns an intent to launch Keepass2Android for adding an entry with the given fields. | ||||
|      * @param fields Key/Value pairs of the field values. See KeepassDefs for standard keys. | ||||
|      * @param protectedFields List of keys of the protected fields. | ||||
|      * @return Intent to start Keepass2Android. | ||||
|      * @throws JSONException | ||||
|      */ | ||||
|     fun getAddEntryIntent( | ||||
|         fields: HashMap<String?, String?>?, | ||||
|         protectedFields: ArrayList<String?>? | ||||
|     ): Intent { | ||||
|         return getAddEntryIntent(JSONObject((fields as Map<*, *>?)!!).toString(), protectedFields) | ||||
|     } | ||||
|  | ||||
|     private fun getAddEntryIntent( | ||||
|         outputData: String?, | ||||
|     fun getAddEntryIntent( | ||||
|         fields: HashMap<String?, String?>, | ||||
|         protectedFields: ArrayList<String?>? | ||||
|     ): Intent { | ||||
|         val outputData = JSONObject((fields as Map<*, *>)).toString() | ||||
|         val startKp2aIntent = Intent(Strings.ACTION_START_WITH_TASK) | ||||
|         startKp2aIntent.addCategory(Intent.CATEGORY_DEFAULT) | ||||
|         startKp2aIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) | ||||
|         startKp2aIntent.putExtra("KP2A_APPTASK", "CreateEntryThenCloseTask") | ||||
|         startKp2aIntent.putExtra("ShowUserNotifications", "false") //KP2A expects a StringExtra | ||||
|         startKp2aIntent.putExtra("ShowUserNotifications", "false") | ||||
|         startKp2aIntent.putExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA, outputData) | ||||
|         if (protectedFields != null) startKp2aIntent.putStringArrayListExtra( | ||||
|         if (protectedFields != null) | ||||
|             startKp2aIntent.putStringArrayListExtra( | ||||
|                 Strings.EXTRA_PROTECTED_FIELDS_LIST, | ||||
|                 protectedFields | ||||
|             ) | ||||
|  | ||||
|  | ||||
|         return startKp2aIntent | ||||
|     } | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Creates an intent to open a Password Entry matching searchText | ||||
|      * @param searchText queryString | ||||
|      * @param showUserNotifications if true, the notifications (copy to clipboard, keyboard) are displayed | ||||
|      * @param closeAfterOpen if true, the entry is opened and KP2A is immediately closed | ||||
|      * @return Intent to start KP2A with | ||||
|      */ | ||||
|     fun getOpenEntryIntent( | ||||
|         searchText: String?, | ||||
|         showUserNotifications: Boolean, | ||||
|         closeAfterOpen: Boolean | ||||
|     ): Intent { | ||||
|         val startKp2aIntent = Intent(Strings.ACTION_START_WITH_TASK) | ||||
|         startKp2aIntent.addCategory(Intent.CATEGORY_DEFAULT) | ||||
|         startKp2aIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) | ||||
|         startKp2aIntent.putExtra("KP2A_APPTASK", "SearchUrlTask") | ||||
|         startKp2aIntent.putExtra("ShowUserNotifications", showUserNotifications.toString()) | ||||
|         startKp2aIntent.putExtra("CloseAfterCreate", closeAfterOpen.toString()) | ||||
|         startKp2aIntent.putExtra("UrlToSearch", searchText) | ||||
|         return startKp2aIntent | ||||
|     fun getQueryEntryForOwnPackageIntent(): Intent { | ||||
|         return Intent(Strings.ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 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> { | ||||
|     fun getEntryFieldsFromIntent(intent: Intent?): HashMap<String, String> { | ||||
|         val res = HashMap<String, String>() | ||||
|         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() | ||||
|             while (iter.hasNext()) { | ||||
|                 val key = iter.next() | ||||
|   | ||||
| @@ -3,55 +3,29 @@ package net.helcel.fidelity.pluginSDK | ||||
| import android.content.BroadcastReceiver | ||||
| import android.content.Context | ||||
| import android.content.Intent | ||||
| import android.util.Log | ||||
|  | ||||
| /** | ||||
|  * Broadcast flow between Host and Plugin | ||||
|  * ====================================== | ||||
|  * | ||||
|  * The host is responsible for deciding when to initiate the session. It | ||||
|  * should initiate the session as soon as plugins are required or when a plugin | ||||
|  * has been updated through the OS. | ||||
|  * It will then send a broadcast to request the currently required scope. | ||||
|  * The plugin then sends a broadcast to the app which scope is required. If an | ||||
|  * access token is already available, it's sent along with the requset. | ||||
|  * | ||||
|  * If a previous permission has been revoked (or the app settings cleared or the | ||||
|  * permissions have been extended or the token is invalid for any other reason) | ||||
|  * the host will answer with a Revoked-Permission broadcast (i.e. the plugin is | ||||
|  * unconnected.) | ||||
|  * | ||||
|  * Unconnected plugins must be permitted by the user (requiring user action). | ||||
|  * When the user grants access, the plugin will receive an access token for | ||||
|  * the host. This access token is valid for the requested scope. If the scope | ||||
|  * changes (e.g after an update of the plugin), the access token becomes invalid. | ||||
|  * | ||||
|  */ | ||||
| abstract class PluginAccessBroadcastReceiver : BroadcastReceiver() { | ||||
| class PluginAccessBroadcastReceiver : BroadcastReceiver() { | ||||
|     override fun onReceive(ctx: Context, intent: Intent) { | ||||
|         val action = intent.action | ||||
|         Log.d(_tag, "received broadcast with action=$action") | ||||
|         if (action == null) return | ||||
|         val action = intent.action ?: return | ||||
|         when (action) { | ||||
|             Strings.ACTION_TRIGGER_REQUEST_ACCESS -> requestAccess(ctx, intent) | ||||
|             Strings.ACTION_RECEIVE_ACCESS -> receiveAccess(ctx, intent) | ||||
|             Strings.ACTION_REVOKE_ACCESS -> revokeAccess(ctx, intent) | ||||
|             else -> {} | ||||
|             else -> println(action) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     private fun revokeAccess(ctx: Context, intent: Intent) { | ||||
|         val senderPackage = intent.getStringExtra(Strings.EXTRA_SENDER) | ||||
|         val accessToken = intent.getStringExtra(Strings.EXTRA_ACCESS_TOKEN) | ||||
|         AccessManager.removeAccessToken(ctx, senderPackage!!, accessToken!!) | ||||
|         AccessManager.removeAccessToken(ctx, senderPackage, accessToken) | ||||
|     } | ||||
|  | ||||
|  | ||||
|     private fun receiveAccess(ctx: Context, intent: Intent) { | ||||
|         val senderPackage = intent.getStringExtra(Strings.EXTRA_SENDER) | ||||
|         val accessToken = intent.getStringExtra(Strings.EXTRA_ACCESS_TOKEN) | ||||
|         AccessManager.storeAccessToken(ctx, senderPackage!!, accessToken!!, scopes) | ||||
|         AccessManager.storeAccessToken(ctx, senderPackage, accessToken, scopes) | ||||
|     } | ||||
|  | ||||
|     private fun requestAccess(ctx: Context, intent: Intent) { | ||||
| @@ -62,21 +36,18 @@ abstract class PluginAccessBroadcastReceiver : BroadcastReceiver() { | ||||
|         rpi.putExtra(Strings.EXTRA_SENDER, ctx.packageName) | ||||
|         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.putStringArrayListExtra(Strings.EXTRA_SCOPES, scopes) | ||||
|         Log.d(_tag, "requesting access for " + scopes.size + " tokens.") | ||||
|         ctx.sendBroadcast(rpi) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * | ||||
|      * @return the list of required scopes for this plugin. | ||||
|      */ | ||||
|     abstract val scopes: ArrayList<String?> | ||||
|  | ||||
|     companion object { | ||||
|         private const val _tag = "Kp2aPluginSDK" | ||||
|     } | ||||
|     private val scopes: ArrayList<String?> = ArrayList( | ||||
|         listOf( | ||||
|             Strings.SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE, | ||||
|             Strings.SCOPE_DATABASE_ACTIONS, | ||||
|             Strings.SCOPE_CURRENT_ENTRY, | ||||
|         ) | ||||
|     ) | ||||
| } | ||||
|   | ||||
| @@ -1,14 +0,0 @@ | ||||
| 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 | ||||
|     } | ||||
| } | ||||
| @@ -1,16 +0,0 @@ | ||||
| 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) | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @@ -22,7 +22,8 @@ class PluginActionBroadcastReceiver : BroadcastReceiver() { | ||||
|             get() { | ||||
|                 val res = HashMap<String, String>() | ||||
|                 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() | ||||
|                     while (iter.hasNext()) { | ||||
|                         val key = iter.next() | ||||
| @@ -54,14 +55,13 @@ class PluginActionBroadcastReceiver : BroadcastReceiver() { | ||||
|             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 | ||||
|                     context, hostPackage, scope | ||||
|                 ) | ||||
|             ) | ||||
|             i.setPackage(hostPackage) | ||||
| @@ -132,7 +132,6 @@ class PluginActionBroadcastReceiver : BroadcastReceiver() { | ||||
|              */ | ||||
|             get() = protectedFieldsListFromIntent | ||||
|  | ||||
|         @Throws(PluginAccessException::class) | ||||
|         fun addEntryAction( | ||||
|             actionDisplayText: String?, | ||||
|             actionIconResourceId: Int, | ||||
| @@ -141,7 +140,6 @@ class PluginActionBroadcastReceiver : BroadcastReceiver() { | ||||
|             addEntryFieldAction(null, null, actionDisplayText, actionIconResourceId, actionData) | ||||
|         } | ||||
|  | ||||
|         @Throws(PluginAccessException::class) | ||||
|         fun addEntryFieldAction( | ||||
|             actionId: String?, | ||||
|             fieldId: String?, | ||||
| @@ -191,24 +189,28 @@ class PluginActionBroadcastReceiver : BroadcastReceiver() { | ||||
|     } | ||||
|  | ||||
|     override fun onReceive(ctx: Context, intent: Intent) { | ||||
|         val action = intent.action | ||||
|         val action = intent.action ?: return | ||||
|         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) { | ||||
|         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)) | ||||
|         } else if (action == Strings.ACTION_ENTRY_OUTPUT_MODIFIED) { | ||||
|  | ||||
|             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 | ||||
|  | ||||
|             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) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -17,11 +17,6 @@ object Strings { | ||||
|     const val SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE = | ||||
|         "keepass2android.SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE" | ||||
|  | ||||
|     /** | ||||
|      * Plugin may query credentials for a deliberate package | ||||
|      */ | ||||
|     const val SCOPE_QUERY_CREDENTIALS = "keepass2android.SCOPE_QUERY_CREDENTIALS" | ||||
|  | ||||
|     /** | ||||
|      * Extra key to transfer a (json serialized) list of scopes | ||||
|      */ | ||||
| @@ -97,11 +92,6 @@ object Strings { | ||||
|      */ | ||||
|     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) | ||||
| @@ -157,13 +147,6 @@ object Strings { | ||||
|     const val ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE = | ||||
|         "keepass2android.ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE" | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Action when plugin wants to query credentials for a deliberate package | ||||
|      * The query string is passed as intent data | ||||
|      */ | ||||
|     const val ACTION_QUERY_CREDENTIALS = "keepass2android.ACTION_QUERY_CREDENTIALS" | ||||
|  | ||||
|     /** | ||||
|      * Action for an intent from the plugin to KP2A to set (i.e. add or update) a field in the entry. | ||||
|      * May be used to update existing or add new fields at any time while the entry is opened. | ||||
| @@ -189,7 +172,5 @@ object Strings { | ||||
|     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_" | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -20,7 +20,6 @@ object BarcodeFormatConverter { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     fun formatToString(f: Int): String { | ||||
|         return when (f) { | ||||
|             Barcode.FORMAT_CODE_128 -> "CODE_128" | ||||
|   | ||||
| @@ -19,25 +19,25 @@ object BarcodeGenerator { | ||||
|             android.graphics.Color.WHITE | ||||
|     } | ||||
|  | ||||
|     fun generateBarcode(content: String, f: String, width: Int): Bitmap? { | ||||
|         if (content.isEmpty() || f.isEmpty()) { | ||||
|     fun generateBarcode(content: String?, f: String?, w: Int): Bitmap? { | ||||
|         if (content.isNullOrEmpty() || f.isNullOrEmpty()) { | ||||
|             return null | ||||
|         } | ||||
|         try { | ||||
|             val format = stringToFormat(f) | ||||
|             val writer = MultiFormatWriter() | ||||
|             val height = (formatToRatio(format) * width).toInt() | ||||
|             val height = (w * formatToRatio(format)).toInt() | ||||
|             val width = (w * 1.0f).toInt() | ||||
|  | ||||
|  | ||||
|             val bitMatrix: BitMatrix = writer.encode(content, format, width, height) | ||||
|             val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) | ||||
|  | ||||
|             for (x in 0 until width) { | ||||
|                 for (y in 0 until height) { | ||||
|                     bitmap.setPixel( | ||||
|                         x, | ||||
|                         y, | ||||
|                         getPixelColor(bitMatrix, x, y) | ||||
|                         x, y, getPixelColor(bitMatrix, x, y) | ||||
|                     ) | ||||
|  | ||||
|                 } | ||||
|             } | ||||
|             return bitmap | ||||
|   | ||||
| @@ -15,9 +15,9 @@ import net.helcel.fidelity.tools.BarcodeFormatConverter.formatToString | ||||
| import java.util.concurrent.Executors | ||||
| 
 | ||||
| 
 | ||||
| @OptIn(ExperimentalGetImage::class) | ||||
| object BarcodeScanner { | ||||
| 
 | ||||
|     @OptIn(ExperimentalGetImage::class) | ||||
|     private fun processImageProxy( | ||||
|         barcodeScanner: BarcodeScanner, | ||||
|         imageProxy: ImageProxy, | ||||
| @@ -33,8 +33,6 @@ object BarcodeScanner { | ||||
| 
 | ||||
|             barcodeScanner.process(inputImage) | ||||
|                 .addOnSuccessListener { barcodeList -> | ||||
|                     println(barcodeList.map { e -> e.displayValue }) | ||||
|                     println(barcodeList.map { e -> e.format }) | ||||
|                     val barcode = | ||||
|                         barcodeList.getOrNull(0) | ||||
|                     if (barcode != null) | ||||
| @@ -6,7 +6,7 @@ import android.os.Bundle | ||||
| import androidx.activity.result.ActivityResultLauncher | ||||
| import androidx.activity.result.contract.ActivityResultContracts | ||||
| import androidx.fragment.app.Fragment | ||||
| import net.helcel.fidelity.pluginSDK.KeepassDefs | ||||
| import net.helcel.fidelity.pluginSDK.KeepassDef | ||||
| import net.helcel.fidelity.pluginSDK.Kp2aControl | ||||
|  | ||||
| object KeepassWrapper { | ||||
| @@ -25,8 +25,8 @@ object KeepassWrapper { | ||||
|  | ||||
|         val fields = HashMap<String?, String?>() | ||||
|         val protected = ArrayList<String?>() | ||||
|         fields[KeepassDefs.TitleField] = title | ||||
|         fields[KeepassDefs.UrlField] = | ||||
|         fields[KeepassDef.TitleField] = title | ||||
|         fields[KeepassDef.UrlField] = | ||||
|             "androidapp://" + fragment.requireActivity().packageName | ||||
|         fields[CODE_FIELD] = code | ||||
|         fields[FORMAT_FIELD] = format | ||||
| @@ -37,33 +37,17 @@ object KeepassWrapper { | ||||
|     } | ||||
|  | ||||
|  | ||||
|     fun resultLauncherAdd( | ||||
|     fun resultLauncher( | ||||
|         fragment: Fragment, | ||||
|         callback: (HashMap<String, String>) -> Unit | ||||
|     ): ActivityResultLauncher<Intent> { | ||||
|         return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> | ||||
|             if (result.resultCode == Activity.RESULT_OK) { | ||||
|                 val data: Intent? = result.data | ||||
|                 val credentials = Kp2aControl.getEntryFieldsFromIntent( | ||||
|                     data!! | ||||
|                 ) | ||||
|                 println(credentials) | ||||
|                 callback(credentials) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun resultLauncherQuery( | ||||
|         fragment: Fragment, | ||||
|         callback: (HashMap<String, String>) -> Unit | ||||
|     ): ActivityResultLauncher<Intent> { | ||||
|         return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> | ||||
|             println(result.resultCode) | ||||
|             println(result.data.toString()) | ||||
|             if (result.resultCode == Activity.RESULT_OK) { | ||||
|                 val data: Intent? = result.data | ||||
|                 val credentials = Kp2aControl.getEntryFieldsFromIntent( | ||||
|                     data!! | ||||
|                 ) | ||||
|                 println(credentials) | ||||
|                 val credentials = Kp2aControl.getEntryFieldsFromIntent(result.data) | ||||
|                 println(credentials.toList().toString()) | ||||
|                 callback(credentials) | ||||
|             } | ||||
|         } | ||||
| @@ -71,7 +55,7 @@ object KeepassWrapper { | ||||
|  | ||||
|     fun entryExtract(map: HashMap<String, String>): Triple<String?, String?, String?> { | ||||
|         return Triple( | ||||
|             map[KeepassDefs.TitleField], | ||||
|             map[KeepassDef.TitleField], | ||||
|             map[CODE_FIELD], | ||||
|             map[FORMAT_FIELD] | ||||
|         ) | ||||
|   | ||||
| @@ -1,21 +0,0 @@ | ||||
| <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> | ||||
| @@ -1,31 +0,0 @@ | ||||
| <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> | ||||
| @@ -1,31 +0,0 @@ | ||||
| <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,8 +1,6 @@ | ||||
| <?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" | ||||
|     android:id="@+id/coordinator" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     android:fitsSystemWindows="true" | ||||
|   | ||||
| @@ -14,7 +14,6 @@ | ||||
|         android:padding="16dp"> | ||||
|  | ||||
|         <com.google.android.material.textfield.TextInputLayout | ||||
|             android:id="@+id/nameInputLayout" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_marginTop="16dp" | ||||
| @@ -64,7 +63,6 @@ | ||||
|         </androidx.constraintlayout.widget.ConstraintLayout> | ||||
|  | ||||
|         <com.google.android.material.textfield.TextInputLayout | ||||
|             android:id="@+id/formatInputLayout" | ||||
|             style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content" | ||||
|   | ||||
| @@ -9,7 +9,8 @@ | ||||
|     <androidx.recyclerview.widget.RecyclerView | ||||
|         android:id="@+id/fidelityList" | ||||
|         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 | ||||
| @@ -25,7 +26,6 @@ | ||||
|         app:srcCompat="@drawable/search" /> | ||||
|  | ||||
|     <LinearLayout | ||||
|         android:id="@+id/expandedMenuContainer" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_alignParentEnd="true" | ||||
|   | ||||
| @@ -11,12 +11,26 @@ | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="match_parent" /> | ||||
|  | ||||
|     <com.google.android.material.textview.MaterialTextView | ||||
|         android:id="@+id/bottomText" | ||||
|     <net.helcel.fidelity.activity.view.ScannerView | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="64dp" | ||||
|         android:layout_height="match_parent" /> | ||||
|  | ||||
|     <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:background="#ffffff" | ||||
|         android:textSize="24sp" /> | ||||
|         android:layout_centerHorizontal="true" | ||||
|         android:layout_margin="24dp" | ||||
|         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> | ||||
| @@ -1,7 +1,5 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <TextView | ||||
|     xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:id="@+id/textViewFeelings" | ||||
| <TextView xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="wrap_content" | ||||
|     android:padding="15dp" | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <resources xmlns:tools="http://schemas.android.com/tools"> | ||||
|     <string name="kp2aplugin_title" tools:keep="@string/kp2aplugin_title">Fidelity</string> | ||||
|     <string name="kp2aplugin_shortdesc">Stores and Displays fidelity and other cards</string> | ||||
|     <string name="kp2aplugin_author" tools:keep="@string/kp2aplugin_author">[soraefir](soraefir)</string> | ||||
|     <string name="kp2aplugin_shortdesc" tools:keep="@string/kp2aplugin_shortdesc">Fidelity adds an interface to manage fidelity cards and other barcodes to Keepass2Android</string> | ||||
|     <string name="kp2aplugin_author" tools:keep="@string/kp2aplugin_author">Soraefir</string> | ||||
|  | ||||
|     <string name="app_name">Keepass Fidelity</string> | ||||
|  | ||||
|   | ||||
| @@ -1,3 +0,0 @@ | ||||
| <resources> | ||||
|  | ||||
| </resources> | ||||
| @@ -1,9 +1,12 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <resources> | ||||
|  | ||||
|     <style name="Theme.Fidelity" parent="Theme.Material3.DayNight.NoActionBar"> | ||||
|  | ||||
|         <item name="colorPrimary">?attr/colorAccent</item> | ||||
|     <style name="Theme.Fidelity" parent="Theme.MaterialComponents.DayNight.NoActionBar"> | ||||
|  | ||||
|         <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> | ||||
| </resources> | ||||
| @@ -1,7 +1,7 @@ | ||||
| // Top-level build file where you can add configuration options common to all sub-projects/modules. | ||||
|  | ||||
| plugins { | ||||
|     id 'com.android.application' version '8.3.0' apply false | ||||
|     id 'com.android.application' 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 | ||||
| } | ||||
| @@ -15,7 +15,7 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 | ||||
| # Android operating system, and which are packaged with your app's APK | ||||
| # https://developer.android.com/topic/libraries/support-library/androidx-rn | ||||
| android.useAndroidX=true | ||||
| android.enableJetifier=true | ||||
| android.enableJetifier=false | ||||
| # Kotlin code style for this project: "official" or "obsolete": | ||||
| kotlin.code.style=official | ||||
| # Enables namespacing of each library's R class so that its R class includes only the | ||||
|   | ||||
		Reference in New Issue
	
	Block a user