diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..efae956 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +*.iml +.idea/ +node_modules/ +temp/ +.gradle +local.properties/ +.DS_Store +build/ +app/build/ +captures/ +.externalNativeBuild +.cxx +local.properties diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..c553917 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,54 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' + id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.23' +} + + +android { + namespace 'net.helcel.fidelity' + compileSdk 34 + + defaultConfig { + applicationId 'net.helcel.fidelity' + minSdk 28 + targetSdk 34 + versionCode 1 + versionName "1.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + buildFeatures { + viewBinding true + } +} + + +dependencies { + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.core:core-ktx:1.12.0' + implementation 'androidx.preference:preference-ktx:1.2.1' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7' + implementation 'androidx.navigation:navigation-ui-ktx:2.7.7' + implementation 'androidx.camera:camera-camera2:1.3.2' + implementation 'androidx.camera:camera-lifecycle:1.3.2' + implementation 'androidx.camera:camera-view:1.3.2' + implementation 'com.google.code.gson:gson:2.10.1' + implementation 'com.google.android.material:material:1.11.0' + implementation 'com.google.zxing:core:3.5.3' + implementation 'com.google.mlkit:barcode-scanning:17.2.0' + +} \ No newline at end of file diff --git a/app/lint.xml b/app/lint.xml new file mode 100644 index 0000000..8423c0e --- /dev/null +++ b/app/lint.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..648a679 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/net/helcel/fidelity/activity/MainActivity.kt b/app/src/main/java/net/helcel/fidelity/activity/MainActivity.kt new file mode 100644 index 0000000..e06eb18 --- /dev/null +++ b/app/src/main/java/net/helcel/fidelity/activity/MainActivity.kt @@ -0,0 +1,48 @@ +package net.helcel.fidelity.activity + +import android.content.Context +import android.content.SharedPreferences +import android.os.Bundle +import androidx.activity.addCallback +import androidx.appcompat.app.AppCompatActivity +import net.helcel.fidelity.R +import net.helcel.fidelity.activity.fragment.Launcher +import net.helcel.fidelity.databinding.ActMainBinding +import net.helcel.fidelity.tools.CacheManager + +class MainActivity : AppCompatActivity() { + + private lateinit var binding: ActMainBinding + + private lateinit var sharedPreferences: SharedPreferences + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + sharedPreferences = + this.getSharedPreferences(CacheManager.PREF_NAME, Context.MODE_PRIVATE) + CacheManager.loadFidelity(sharedPreferences) + + + binding = ActMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + onBackPressedDispatcher.addCallback(this) { + if (supportFragmentManager.backStackEntryCount > 0) { + supportFragmentManager.popBackStackImmediate() + } else { + finish() + } + } + if (savedInstanceState == null) + loadLauncher() + + } + + private fun loadLauncher() { + supportFragmentManager.beginTransaction() + .add(R.id.container, Launcher()) + .commit() + } +} + diff --git a/app/src/main/java/net/helcel/fidelity/activity/adapter/FidelityListAdapter.kt b/app/src/main/java/net/helcel/fidelity/activity/adapter/FidelityListAdapter.kt new file mode 100644 index 0000000..b13c884 --- /dev/null +++ b/app/src/main/java/net/helcel/fidelity/activity/adapter/FidelityListAdapter.kt @@ -0,0 +1,50 @@ +package net.helcel.fidelity.activity.adapter + + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import android.widget.LinearLayout +import androidx.recyclerview.widget.RecyclerView +import net.helcel.fidelity.databinding.ListItemFidelityBinding + +class FidelityListAdapter( + private val triples: ArrayList>, + private val onItemClicked: (Triple) -> Unit +) : + RecyclerView.Adapter() { + + private lateinit var binding: ListItemFidelityBinding + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FidelityViewHolder { + binding = ListItemFidelityBinding.inflate(LayoutInflater.from(parent.context)) + binding.root.setLayoutParams( + LinearLayout.LayoutParams( + MATCH_PARENT, WRAP_CONTENT + ) + ) + return FidelityViewHolder(binding.root) + } + + override fun onBindViewHolder(holder: FidelityViewHolder, position: Int) { + val triple = triples[position] + holder.bind(triple) + } + + override fun getItemCount(): Int = triples.size + + inner class FidelityViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + + + fun bind(triple: Triple) { + val text = "${triple.first}" + binding.textView.text = text + binding.card.setOnClickListener { onItemClicked(triple) } + } + + + } + +} diff --git a/app/src/main/java/net/helcel/fidelity/activity/fragment/CreateEntry.kt b/app/src/main/java/net/helcel/fidelity/activity/fragment/CreateEntry.kt new file mode 100644 index 0000000..068156c --- /dev/null +++ b/app/src/main/java/net/helcel/fidelity/activity/fragment/CreateEntry.kt @@ -0,0 +1,143 @@ +package net.helcel.fidelity.activity.fragment + +import android.content.ActivityNotFoundException +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import androidx.core.widget.addTextChangedListener +import androidx.fragment.app.Fragment +import com.google.zxing.FormatException +import net.helcel.fidelity.R +import net.helcel.fidelity.databinding.FragCreateEntryBinding +import net.helcel.fidelity.pluginSDK.Kp2aControl +import net.helcel.fidelity.tools.BarcodeGenerator.generateBarcode +import net.helcel.fidelity.tools.CacheManager +import net.helcel.fidelity.tools.ErrorToaster +import net.helcel.fidelity.tools.KeepassWrapper + +private const val DEBOUNCE_DELAY = 500L + +class CreateEntry : Fragment() { + + private val handler = Handler(Looper.getMainLooper()) + private lateinit var binding: FragCreateEntryBinding + + private val resultLauncherAdd = KeepassWrapper.resultLauncherAdd(this) { + val r = KeepassWrapper.entryExtract(it) + if (!KeepassWrapper.isProtected(it)) { + CacheManager.addFidelity(r) + } + startViewEntry(r.first, r.second, r.third) + } + + private var isValid: Boolean = false + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragCreateEntryBinding.inflate(layoutInflater) + + val formats = resources.getStringArray(R.array.format_array) + val arrayAdapter = ArrayAdapter(requireContext(), R.layout.list_item_dropdown, formats) + binding.editTextFormat.setAdapter(arrayAdapter) + + val res = KeepassWrapper.bundleExtract(arguments) + binding.editTextCode.setText(res.second) + binding.editTextFormat.setText(res.third, false) + + val changeListener = { + isValid = false + handler.removeCallbacksAndMessages(null) + handler.postDelayed({ + updatePreview() + }, DEBOUNCE_DELAY) + } + + binding.editTextCode.addTextChangedListener { changeListener() } + binding.editTextFormat.addTextChangedListener { changeListener() } + binding.editTextFormat.addTextChangedListener { binding.editTextFormat.error = null } + binding.btnSave.setOnClickListener { + if (!isValid() || !isValid) { + ErrorToaster.formIncomplete(requireActivity()) + + } else { + val kpentry = KeepassWrapper.entryCreate( + this, + binding.editTextTitle.text.toString(), + binding.editTextCode.text.toString(), + binding.editTextFormat.text.toString(), + binding.checkboxProtected.isChecked, + ) + try { + resultLauncherAdd.launch( + Kp2aControl.getAddEntryIntent( + kpentry.first, + kpentry.second + ) + ) + } catch (e: ActivityNotFoundException) { + ErrorToaster.noKP2AFound(requireActivity()) + } + } + } + + updatePreview() + return binding.root + } + + private fun updatePreview() { + try { + val barcodeBitmap = generateBarcode( + binding.editTextCode.text.toString(), + binding.editTextFormat.text.toString(), + 600 + ) + binding.imageViewPreview.setImageBitmap(barcodeBitmap) + isValid = true + } catch (e: FormatException) { + binding.imageViewPreview.setImageBitmap(null) + binding.editTextCode.error = "Invalid format" + } catch (e: IllegalArgumentException) { + binding.imageViewPreview.setImageBitmap(null) + binding.editTextCode.error = e.message + } catch (e: Exception) { + binding.imageViewPreview.setImageBitmap(null) + println(e.javaClass) + println(e.message) + e.printStackTrace() + } + } + + private fun isValid(): Boolean { + var valid = true + if (binding.editTextTitle.text!!.isEmpty()) { + valid = false + binding.editTextTitle.error = "Title cannot be empty" + } + if (binding.editTextCode.text!!.isEmpty()) { + valid = false + binding.editTextCode.error = "Code cannot be empty" + } + if (binding.editTextFormat.text!!.isEmpty()) { + valid = false + binding.editTextFormat.error = "Format cannot be empty" + } + return valid + } + + + private fun startViewEntry(title: String?, code: String?, fmt: String?) { + val viewEntryFragment = ViewEntry() + viewEntryFragment.arguments = KeepassWrapper.bundleCreate(title, code, fmt) + + requireActivity().supportFragmentManager.beginTransaction() + .replace(R.id.container, viewEntryFragment).commit() + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/helcel/fidelity/activity/fragment/Launcher.kt b/app/src/main/java/net/helcel/fidelity/activity/fragment/Launcher.kt new file mode 100644 index 0000000..e02be35 --- /dev/null +++ b/app/src/main/java/net/helcel/fidelity/activity/fragment/Launcher.kt @@ -0,0 +1,105 @@ +package net.helcel.fidelity.activity.fragment + +import android.content.ActivityNotFoundException +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import net.helcel.fidelity.R +import net.helcel.fidelity.activity.adapter.FidelityListAdapter +import net.helcel.fidelity.databinding.FragLauncherBinding +import net.helcel.fidelity.pluginSDK.Kp2aControl +import net.helcel.fidelity.tools.CacheManager +import net.helcel.fidelity.tools.ErrorToaster +import net.helcel.fidelity.tools.KeepassWrapper + + +class Launcher : Fragment() { + + private lateinit var binding: FragLauncherBinding + private lateinit var fidelityListAdapter: FidelityListAdapter + + private val resultLauncherQuery = KeepassWrapper.resultLauncherQuery(this) { + val r = KeepassWrapper.entryExtract(it) + if (!KeepassWrapper.isProtected(it)) { + CacheManager.addFidelity(r) + } + startViewEntry(r.first, r.second, r.third) + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragLauncherBinding.inflate(layoutInflater) + binding.btnQuery.setOnClickListener { startGetFromKeepass() } + binding.btnAdd.setOnClickListener { + if (binding.menuAdd.visibility == View.GONE) + showMenuAdd() + else + hideMenuAdd() + } + + hideMenuAdd() + binding.btnScan.setOnClickListener { + startScanner() + hideMenuAdd() + } + + binding.btnManual.setOnClickListener { + startCreateEntry() + hideMenuAdd() + } + + binding.fidelityList.layoutManager = + LinearLayoutManager(requireContext()) + fidelityListAdapter = FidelityListAdapter(CacheManager.getFidelity()) { + startViewEntry(it.first, it.second, it.third) + } + binding.fidelityList.adapter = fidelityListAdapter + return binding.root + } + + private fun hideMenuAdd() { + binding.btnAdd.setImageResource(R.drawable.cross) + binding.menuAdd.visibility = View.GONE + + } + + private fun showMenuAdd() { + binding.btnAdd.setImageResource(R.drawable.minus) + binding.menuAdd.visibility = View.VISIBLE + } + + + private fun startGetFromKeepass() { + try { + this.resultLauncherQuery.launch(Kp2aControl.queryEntryIntentForOwnPackage) + } catch (e: ActivityNotFoundException) { + ErrorToaster.noKP2AFound(requireActivity()) + } + } + + private fun startFragment(fragment: Fragment) { + requireActivity().supportFragmentManager.beginTransaction() + .addToBackStack("Launcher") + .replace(R.id.container, fragment).commit() + } + + private fun startScanner() { + startFragment(Scanner()) + } + + private fun startCreateEntry() { + startFragment(CreateEntry()) + } + + + private fun startViewEntry(title: String?, code: String?, fmt: String?) { + val viewEntryFragment = ViewEntry() + viewEntryFragment.arguments = KeepassWrapper.bundleCreate(title, code, fmt) + startFragment(viewEntryFragment) + } +} \ No newline at end of file diff --git a/app/src/main/java/net/helcel/fidelity/activity/fragment/Scanner.kt b/app/src/main/java/net/helcel/fidelity/activity/fragment/Scanner.kt new file mode 100644 index 0000000..925ae3a --- /dev/null +++ b/app/src/main/java/net/helcel/fidelity/activity/fragment/Scanner.kt @@ -0,0 +1,116 @@ +package net.helcel.fidelity.activity.fragment + +import android.Manifest +import android.content.ContentValues +import android.content.pm.PackageManager +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.camera.core.CameraSelector +import androidx.camera.core.Preview +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import net.helcel.fidelity.R +import net.helcel.fidelity.databinding.FragScannerBinding +import net.helcel.fidelity.tools.BarcodeScanner.getAnalysisUseCase +import net.helcel.fidelity.tools.KeepassWrapper + +private const val CAMERA_PERMISSION_REQUEST_CODE = 1 + +class Scanner : Fragment() { + + private lateinit var binding: FragScannerBinding + + private var code: String = "" + private var fmt: String = "" + private var valid: Boolean = false + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragScannerBinding.inflate(layoutInflater) + binding.bottomText.setOnClickListener { + startCreateEntry() + } + when (hasCameraPermission()) { + true -> bindCameraUseCases() + else -> requestPermission() + } + return binding.root + } + + private fun startCreateEntry() { + val createEntryFragment = CreateEntry() + createEntryFragment.arguments = + KeepassWrapper.bundleCreate(null, this.code, this.fmt) + requireActivity().supportFragmentManager.beginTransaction() + .replace(R.id.container, createEntryFragment) + .commit() + } + + private fun hasCameraPermission() = + ActivityCompat.checkSelfPermission( + requireContext(), + Manifest.permission.CAMERA + ) == PackageManager.PERMISSION_GRANTED + + private fun requestPermission() { + ActivityCompat.requestPermissions( + requireActivity(), + arrayOf(Manifest.permission.CAMERA), + CAMERA_PERMISSION_REQUEST_CODE + ) + ActivityCompat.OnRequestPermissionsResultCallback { c, p, i -> + require(c == CAMERA_PERMISSION_REQUEST_CODE) + require(p.contains(Manifest.permission.CAMERA)) + val el = i[p.indexOf(Manifest.permission.CAMERA)] + if (el != PackageManager.PERMISSION_GRANTED) { + startCreateEntry() + } + + } + } + + private fun bindCameraUseCases() { + val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext()) + + cameraProviderFuture.addListener({ + val cameraProvider = cameraProviderFuture.get() + val previewUseCase = Preview.Builder() + .build() + .also { + it.setSurfaceProvider(binding.cameraView.surfaceProvider) + } + val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA + val analysisUseCase = getAnalysisUseCase { code, format -> + if (code != null && format != null) { + this.code = code + this.fmt = format + this.valid = true + } else { + this.valid = false + } + } + try { + cameraProvider.bindToLifecycle( + this, + cameraSelector, + previewUseCase, + analysisUseCase + ) + } catch (illegalStateException: IllegalStateException) { + Log.e(ContentValues.TAG, illegalStateException.message.orEmpty()) + } catch (illegalArgumentException: IllegalArgumentException) { + Log.e(ContentValues.TAG, illegalArgumentException.message.orEmpty()) + } + }, ContextCompat.getMainExecutor(requireContext())) + } + + +} \ No newline at end of file diff --git a/app/src/main/java/net/helcel/fidelity/activity/fragment/ViewEntry.kt b/app/src/main/java/net/helcel/fidelity/activity/fragment/ViewEntry.kt new file mode 100644 index 0000000..9659f4a --- /dev/null +++ b/app/src/main/java/net/helcel/fidelity/activity/fragment/ViewEntry.kt @@ -0,0 +1,75 @@ +package net.helcel.fidelity.activity.fragment + +import android.content.res.Configuration +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import com.google.zxing.FormatException +import net.helcel.fidelity.databinding.FragViewEntryBinding +import net.helcel.fidelity.tools.BarcodeGenerator.generateBarcode +import net.helcel.fidelity.tools.ErrorToaster +import net.helcel.fidelity.tools.KeepassWrapper + + +class ViewEntry : Fragment() { + + private lateinit var binding: FragViewEntryBinding + + private var title: String? = null + private var code: String? = null + private var fmt: String? = null + + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragViewEntryBinding.inflate(layoutInflater) + val res = KeepassWrapper.bundleExtract(arguments) + title = res.first + code = res.second + fmt = res.third + + adjustLayout() + updatePreview() + return binding.root + } + + private fun updatePreview() { + binding.title.text = title + try { + val barcodeBitmap = generateBarcode( + code!!, fmt!!, 1024 + ) + binding.imageViewPreview.setImageBitmap(barcodeBitmap) + } catch (e: FormatException) { + ErrorToaster.invalidFormat(requireActivity()) + binding.imageViewPreview.setImageBitmap(null) + } catch (e: IllegalArgumentException) { + binding.imageViewPreview.setImageBitmap(null) + ErrorToaster.invalidFormat(requireActivity()) + } catch (e: Exception) { + binding.imageViewPreview.setImageBitmap(null) + println(e.javaClass) + println(e.message) + e.printStackTrace() + } + } + + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + adjustLayout() + } + + private fun adjustLayout() { + if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { + binding.title.visibility = View.GONE + } else { + binding.title.visibility = View.VISIBLE + } + } +} \ No newline at end of file diff --git a/app/src/main/java/net/helcel/fidelity/pluginSDK/AccessManager.kt b/app/src/main/java/net/helcel/fidelity/pluginSDK/AccessManager.kt new file mode 100644 index 0000000..2ab93f0 --- /dev/null +++ b/app/src/main/java/net/helcel/fidelity/pluginSDK/AccessManager.kt @@ -0,0 +1,179 @@ +package net.helcel.fidelity.pluginSDK + +import android.content.Context +import android.content.SharedPreferences +import android.content.pm.PackageManager +import android.text.TextUtils +import android.util.Log +import org.json.JSONArray +import org.json.JSONException + +object AccessManager { + private const val _tag = "Kp2aPluginSDK" + private const val PREF_KEY_SCOPE = "scope" + private const val PREF_KEY_TOKEN = "token" + + private fun stringArrayToString(values: ArrayList): String? { + val a = JSONArray() + for (i in values.indices) { + a.put(values[i]) + } + return if (values.isNotEmpty()) { + a.toString() + } else { + null + } + } + + private fun stringToStringArray(s: String?): ArrayList { + val strings = ArrayList() + if (!TextUtils.isEmpty(s)) { + try { + val a = JSONArray(s) + for (i in 0 until a.length()) { + val url = a.optString(i) + strings.add(url) + } + } catch (e: JSONException) { + e.printStackTrace() + } + } + return strings + } + + fun storeAccessToken( + ctx: Context, + hostPackage: String, + accessToken: String, + scopes: ArrayList + ) { + val prefs = getPrefsForHost(ctx, hostPackage) + + val edit = prefs.edit() + edit.putString(PREF_KEY_TOKEN, accessToken) + val scopesString = stringArrayToString(scopes) + edit.putString(PREF_KEY_SCOPE, scopesString) + edit.apply() + Log.d( + _tag, + "stored access token " + accessToken.substring( + 0, + 4 + ) + "... for " + scopes.size + " scopes (" + scopesString + ")." + ) + + val hostPrefs = ctx.getSharedPreferences("KP2A.PluginAccess.hosts", Context.MODE_PRIVATE) + if (!hostPrefs.contains(hostPackage)) { + hostPrefs.edit().putString(hostPackage, "").apply() + } + } + + fun preparePopup(popupMenu: Any) { + try { + val fields = popupMenu.javaClass.declaredFields + for (field in fields) { + if ("mPopup" == field.name) { + field.isAccessible = true + val menuPopupHelper = field[popupMenu] + val classPopupHelper = Class.forName( + menuPopupHelper + .javaClass.name + ) + val setForceIcons = classPopupHelper.getMethod( + "setForceShowIcon", Boolean::class.javaPrimitiveType + ) + setForceIcons.invoke(menuPopupHelper, true) + break + } + } + } catch (e: Exception) { + e.printStackTrace() + } + } + + private fun getPrefsForHost( + ctx: Context, + hostPackage: String + ): SharedPreferences { + val prefs = ctx.getSharedPreferences("KP2A.PluginAccess.$hostPackage", Context.MODE_PRIVATE) + return prefs + } + + fun tryGetAccessToken(ctx: Context, hostPackage: String, scopes: ArrayList): String? { + if (TextUtils.isEmpty(hostPackage)) { + Log.d(_tag, "hostPackage is empty!") + return null + } + Log.d(_tag, "trying to find prefs for $hostPackage") + val prefs = getPrefsForHost(ctx, hostPackage) + val scopesString = prefs.getString(PREF_KEY_SCOPE, "") + Log.d(_tag, "available scopes: $scopesString") + val currentScope = stringToStringArray(scopesString) + if (isSubset(scopes, currentScope)) { + return prefs.getString(PREF_KEY_TOKEN, null) + } else { + Log.d(_tag, "looks like scope changed. Access token invalid.") + return null + } + } + + private fun isSubset( + requiredScopes: ArrayList, + availableScopes: ArrayList + ): Boolean { + for (r in requiredScopes) { + if (availableScopes.indexOf(r) < 0) { + Log.d(_tag, "Scope " + r + " not available. " + availableScopes.size) + return false + } + } + return true + } + + fun removeAccessToken( + ctx: Context, hostPackage: String, + accessToken: String + ) { + val prefs = getPrefsForHost(ctx, hostPackage) + + Log.d(_tag, "removing AccessToken.") + if (prefs.getString(PREF_KEY_TOKEN, "") == accessToken) { + val edit = prefs.edit() + edit.clear() + edit.apply() + } + + val hostPrefs = ctx.getSharedPreferences("KP2A.PluginAccess.hosts", Context.MODE_PRIVATE) + if (hostPrefs.contains(hostPackage)) { + hostPrefs.edit().remove(hostPackage).apply() + } + } + + fun getAllHostPackages(ctx: Context): Set { + val prefs = ctx.getSharedPreferences("KP2A.PluginAccess.hosts", Context.MODE_PRIVATE) + val result: MutableSet = HashSet() + for (host in prefs.all.keys) { + try { + val info = ctx.packageManager.getPackageInfo(host, PackageManager.GET_META_DATA) + //if we get here, the package is still there + result.add(host) + } catch (e: PackageManager.NameNotFoundException) { + //host gone. ignore. + } + } + return result + } + + + /** + * Returns a valid access token or throws PluginAccessException + */ + fun getAccessToken( + context: Context, hostPackage: String, + scopes: ArrayList + ): String { + val accessToken = tryGetAccessToken(context, hostPackage, scopes) + ?: throw PluginAccessException(hostPackage, scopes) + return accessToken + } +} diff --git a/app/src/main/java/net/helcel/fidelity/pluginSDK/KeepassDefs.kt b/app/src/main/java/net/helcel/fidelity/pluginSDK/KeepassDefs.kt new file mode 100644 index 0000000..7a0ea22 --- /dev/null +++ b/app/src/main/java/net/helcel/fidelity/pluginSDK/KeepassDefs.kt @@ -0,0 +1,45 @@ +package net.helcel.fidelity.pluginSDK + +object KeepassDefs { + /// + /// Default identifier string for the title field. Should not contain + /// spaces, tabs or other whitespace. + /// + var TitleField: String = "Title" + + /// + /// Default identifier string for the user name field. Should not contain + /// spaces, tabs or other whitespace. + /// + private var UserNameField: String = "UserName" + + /// + /// Default identifier string for the password field. Should not contain + /// spaces, tabs or other whitespace. + /// + private var PasswordField: String = "Password" + + /// + /// Default identifier string for the URL field. Should not contain + /// spaces, tabs or other whitespace. + /// + var UrlField: String = "URL" + + /// + /// Default identifier string for the notes field. Should not contain + /// spaces, tabs or other whitespace. + /// + 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 + } +} diff --git a/app/src/main/java/net/helcel/fidelity/pluginSDK/Kp2aControl.kt b/app/src/main/java/net/helcel/fidelity/pluginSDK/Kp2aControl.kt new file mode 100644 index 0000000..54d2bfd --- /dev/null +++ b/app/src/main/java/net/helcel/fidelity/pluginSDK/Kp2aControl.kt @@ -0,0 +1,107 @@ +package net.helcel.fidelity.pluginSDK + +import android.content.Intent +import android.text.TextUtils +import org.json.JSONException +import org.json.JSONObject + +object Kp2aControl { + /** + * Creates and returns an intent to launch Keepass2Android for adding an entry with the given fields. + * @param fields Key/Value pairs of the field values. See KeepassDefs for standard keys. + * @param protectedFields List of keys of the protected fields. + * @return Intent to start Keepass2Android. + * @throws JSONException + */ + fun getAddEntryIntent( + fields: HashMap?, + protectedFields: ArrayList? + ): Intent { + return getAddEntryIntent(JSONObject((fields as Map<*, *>?)!!).toString(), protectedFields) + } + + private fun getAddEntryIntent( + outputData: String?, + protectedFields: ArrayList? + ): Intent { + val startKp2aIntent = Intent(Strings.ACTION_START_WITH_TASK) + startKp2aIntent.addCategory(Intent.CATEGORY_DEFAULT) + startKp2aIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + startKp2aIntent.putExtra("KP2A_APPTASK", "CreateEntryThenCloseTask") + startKp2aIntent.putExtra("ShowUserNotifications", "false") //KP2A expects a StringExtra + startKp2aIntent.putExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA, outputData) + if (protectedFields != null) startKp2aIntent.putStringArrayListExtra( + Strings.EXTRA_PROTECTED_FIELDS_LIST, + protectedFields + ) + + + return startKp2aIntent + } + + + /** + * Creates an intent to open a Password Entry matching searchText + * @param searchText queryString + * @param showUserNotifications if true, the notifications (copy to clipboard, keyboard) are displayed + * @param closeAfterOpen if true, the entry is opened and KP2A is immediately closed + * @return Intent to start KP2A with + */ + fun getOpenEntryIntent( + searchText: String?, + showUserNotifications: Boolean, + closeAfterOpen: Boolean + ): Intent { + val startKp2aIntent = Intent(Strings.ACTION_START_WITH_TASK) + startKp2aIntent.addCategory(Intent.CATEGORY_DEFAULT) + startKp2aIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + startKp2aIntent.putExtra("KP2A_APPTASK", "SearchUrlTask") + startKp2aIntent.putExtra("ShowUserNotifications", showUserNotifications.toString()) + startKp2aIntent.putExtra("CloseAfterCreate", closeAfterOpen.toString()) + startKp2aIntent.putExtra("UrlToSearch", searchText) + return startKp2aIntent + } + + /** + * Creates an intent to query a password entry from KP2A. The credentials are returned as Activity result. + * @param searchText Text to search for. Should be a URL or "androidapp://com.my.package." + * @return an Intent to start KP2A with + */ + fun getQueryEntryIntent(searchText: String?): Intent { + val i = Intent(Strings.ACTION_QUERY_CREDENTIALS) + if (!TextUtils.isEmpty(searchText)) i.putExtra(Strings.EXTRA_QUERY_STRING, searchText) + return i + } + + val queryEntryIntentForOwnPackage: Intent + /** + * Creates an intent to query a password entry from KP2A, matching to the current app's package . + * The credentials are returned as Activity result. + * This requires SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE. + * @return an Intent to start KP2A with + */ + get() = Intent(Strings.ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE) + + /** + * Converts the entry fields returned in an intent from a query to a hashmap. + * @param intent data received in onActivityResult after getQueryEntryIntent(ForOwnPackage) + * @return HashMap with keys = field names (see KeepassDefs for standard keys) and values = values + */ + fun getEntryFieldsFromIntent(intent: Intent): HashMap { + val res = HashMap() + try { + val json = JSONObject(intent.getStringExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA)!!) + val iter = json.keys() + while (iter.hasNext()) { + val key = iter.next() + val value = json[key].toString() + res[key] = value + } + } catch (e: JSONException) { + e.printStackTrace() + } catch (e: NullPointerException) { + e.printStackTrace() + } + return res + } +} diff --git a/app/src/main/java/net/helcel/fidelity/pluginSDK/PluginAccessBroadcastReceiver.kt b/app/src/main/java/net/helcel/fidelity/pluginSDK/PluginAccessBroadcastReceiver.kt new file mode 100644 index 0000000..97a1d2b --- /dev/null +++ b/app/src/main/java/net/helcel/fidelity/pluginSDK/PluginAccessBroadcastReceiver.kt @@ -0,0 +1,82 @@ +package net.helcel.fidelity.pluginSDK + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log + +/** + * Broadcast flow between Host and Plugin + * ====================================== + * + * The host is responsible for deciding when to initiate the session. It + * should initiate the session as soon as plugins are required or when a plugin + * has been updated through the OS. + * It will then send a broadcast to request the currently required scope. + * The plugin then sends a broadcast to the app which scope is required. If an + * access token is already available, it's sent along with the requset. + * + * If a previous permission has been revoked (or the app settings cleared or the + * permissions have been extended or the token is invalid for any other reason) + * the host will answer with a Revoked-Permission broadcast (i.e. the plugin is + * unconnected.) + * + * Unconnected plugins must be permitted by the user (requiring user action). + * When the user grants access, the plugin will receive an access token for + * the host. This access token is valid for the requested scope. If the scope + * changes (e.g after an update of the plugin), the access token becomes invalid. + * + */ +abstract class PluginAccessBroadcastReceiver : BroadcastReceiver() { + override fun onReceive(ctx: Context, intent: Intent) { + val action = intent.action + Log.d(_tag, "received broadcast with action=$action") + if (action == null) return + when (action) { + Strings.ACTION_TRIGGER_REQUEST_ACCESS -> requestAccess(ctx, intent) + Strings.ACTION_RECEIVE_ACCESS -> receiveAccess(ctx, intent) + Strings.ACTION_REVOKE_ACCESS -> revokeAccess(ctx, intent) + else -> {} + } + } + + + private fun revokeAccess(ctx: Context, intent: Intent) { + val senderPackage = intent.getStringExtra(Strings.EXTRA_SENDER) + val accessToken = intent.getStringExtra(Strings.EXTRA_ACCESS_TOKEN) + AccessManager.removeAccessToken(ctx, senderPackage!!, accessToken!!) + } + + + private fun receiveAccess(ctx: Context, intent: Intent) { + val senderPackage = intent.getStringExtra(Strings.EXTRA_SENDER) + val accessToken = intent.getStringExtra(Strings.EXTRA_ACCESS_TOKEN) + AccessManager.storeAccessToken(ctx, senderPackage!!, accessToken!!, scopes) + } + + private fun requestAccess(ctx: Context, intent: Intent) { + val senderPackage = intent.getStringExtra(Strings.EXTRA_SENDER) + val requestToken = intent.getStringExtra(Strings.EXTRA_REQUEST_TOKEN) + val rpi = Intent(Strings.ACTION_REQUEST_ACCESS) + rpi.setPackage(senderPackage) + rpi.putExtra(Strings.EXTRA_SENDER, ctx.packageName) + rpi.putExtra(Strings.EXTRA_REQUEST_TOKEN, requestToken) + + val token: String? = AccessManager.tryGetAccessToken(ctx, senderPackage!!, scopes) + rpi.putExtra(Strings.EXTRA_ACCESS_TOKEN, token) + + rpi.putStringArrayListExtra(Strings.EXTRA_SCOPES, scopes) + Log.d(_tag, "requesting access for " + scopes.size + " tokens.") + ctx.sendBroadcast(rpi) + } + + /** + * + * @return the list of required scopes for this plugin. + */ + abstract val scopes: ArrayList + + companion object { + private const val _tag = "Kp2aPluginSDK" + } +} diff --git a/app/src/main/java/net/helcel/fidelity/pluginSDK/PluginAccessException.kt b/app/src/main/java/net/helcel/fidelity/pluginSDK/PluginAccessException.kt new file mode 100644 index 0000000..571b879 --- /dev/null +++ b/app/src/main/java/net/helcel/fidelity/pluginSDK/PluginAccessException.kt @@ -0,0 +1,14 @@ +package net.helcel.fidelity.pluginSDK + +class PluginAccessException : Exception { + constructor(what: String?) : super(what) + + constructor(hostPackage: String?, scopes: ArrayList) + + companion object { + /** + * + */ + private const val serialVersionUID = 1L + } +} diff --git a/app/src/main/java/net/helcel/fidelity/pluginSDK/PluginAccessReceiver.kt b/app/src/main/java/net/helcel/fidelity/pluginSDK/PluginAccessReceiver.kt new file mode 100644 index 0000000..63895d6 --- /dev/null +++ b/app/src/main/java/net/helcel/fidelity/pluginSDK/PluginAccessReceiver.kt @@ -0,0 +1,16 @@ +package net.helcel.fidelity.pluginSDK + +import kotlin.collections.ArrayList + + +class PluginAccessReceiver : PluginAccessBroadcastReceiver() { + + override val scopes: ArrayList = ArrayList() + + init { + this.scopes.add(Strings.SCOPE_DATABASE_ACTIONS) + this.scopes.add(Strings.SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE) + } + +} + diff --git a/app/src/main/java/net/helcel/fidelity/pluginSDK/PluginActionBroadcastReceiver.kt b/app/src/main/java/net/helcel/fidelity/pluginSDK/PluginActionBroadcastReceiver.kt new file mode 100644 index 0000000..9b2af63 --- /dev/null +++ b/app/src/main/java/net/helcel/fidelity/pluginSDK/PluginActionBroadcastReceiver.kt @@ -0,0 +1,224 @@ +package net.helcel.fidelity.pluginSDK + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.util.Log +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject + +class PluginActionBroadcastReceiver : BroadcastReceiver() { + open class PluginActionBase + (var context: Context, protected var _intent: Intent) { + val hostPackage: String? + get() = _intent.getStringExtra(Strings.EXTRA_SENDER) + } + + open class PluginEntryActionBase(context: Context, intent: Intent) : + PluginActionBase(context, intent) { + protected val entryFieldsFromIntent: HashMap + get() { + val res = HashMap() + 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? + get() { + try { + val json = + JSONArray(_intent.getStringExtra(Strings.EXTRA_PROTECTED_FIELDS_LIST)) + val res = arrayOfNulls(json.length()) + for (i in 0 until json.length()) res[i] = json.getString(i) + return res + } catch (e: JSONException) { + e.printStackTrace() + return null + } + } + + + open val entryId: String? + get() = _intent.getStringExtra(Strings.EXTRA_ENTRY_ID) + + + @Throws(PluginAccessException::class) + fun setEntryField(fieldId: String?, fieldValue: String?, isProtected: Boolean) { + val i = Intent(Strings.ACTION_SET_ENTRY_FIELD) + val scope = ArrayList() + 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 + /** + * + * @return a hashmap containing the entry fields in key/value form + */ + get() = entryFieldsFromIntent + + val protectedFieldsList: Array? + /** + * + * @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 + get() = entryFieldsFromIntent + + val protectedFieldsList: Array? + /** + * + * @return an array with the keys of all protected fields in the entry + */ + get() = protectedFieldsListFromIntent + + @Throws(PluginAccessException::class) + fun addEntryAction( + actionDisplayText: String?, + actionIconResourceId: Int, + actionData: Bundle? + ) { + addEntryFieldAction(null, null, actionDisplayText, actionIconResourceId, actionData) + } + + @Throws(PluginAccessException::class) + fun addEntryFieldAction( + actionId: String?, + fieldId: String?, + actionDisplayText: String?, + actionIconResourceId: Int, + actionData: Bundle? + ) { + val i = Intent(Strings.ACTION_ADD_ENTRY_ACTION) + val scope = ArrayList() + scope.add(Strings.SCOPE_CURRENT_ENTRY) + i.putExtra( + Strings.EXTRA_ACCESS_TOKEN, AccessManager.getAccessToken( + context, hostPackage!!, scope + ) + ) + i.setPackage(hostPackage) + i.putExtra(Strings.EXTRA_SENDER, context.packageName) + i.putExtra(Strings.EXTRA_ACTION_DATA, actionData) + i.putExtra(Strings.EXTRA_ACTION_DISPLAY_TEXT, actionDisplayText) + i.putExtra(Strings.EXTRA_ACTION_ICON_RES_ID, actionIconResourceId) + i.putExtra(Strings.EXTRA_ENTRY_ID, entryId) + i.putExtra(Strings.EXTRA_FIELD_ID, fieldId) + i.putExtra(Strings.EXTRA_ACTION_ID, actionId) + + context.sendBroadcast(i) + } + } + + private inner class DatabaseAction(context: Context, intent: Intent) : + PluginActionBase(context, intent) { + val fileDisplayName: String? + get() = _intent.getStringExtra(Strings.EXTRA_DATABASE_FILE_DISPLAYNAME) + + val filePath: String? + get() = _intent.getStringExtra(Strings.EXTRA_DATABASE_FILEPATH) + + val action: String? + get() = _intent.action + } + + //EntryOutputModified is very similar to OpenEntry because it receives the same + //data (+ the field id which was modified) + private inner class EntryOutputModifiedAction(context: Context, intent: Intent) : + OpenEntryAction(context, intent) { + val modifiedFieldId: String? + get() = _intent.getStringExtra(Strings.EXTRA_FIELD_ID) + } + + override fun onReceive(ctx: Context, intent: Intent) { + val action = intent.action + Log.d( + "KP2A.pluginsdk", + "received broadcast in PluginActionBroadcastReceiver with action=$action" + ) + if (action == null) return + if (action == Strings.ACTION_OPEN_ENTRY) { + openEntry(OpenEntryAction(ctx, intent)) + } else if (action == Strings.ACTION_CLOSE_ENTRY_VIEW) { + closeEntryView(CloseEntryViewAction(ctx, intent)) + } else if (action == Strings.ACTION_ENTRY_ACTION_SELECTED) { + actionSelected(ActionSelectedAction(ctx, intent)) + } else if (action == Strings.ACTION_ENTRY_OUTPUT_MODIFIED) { + entryOutputModified(EntryOutputModifiedAction(ctx, intent)) + } else if (action == Strings.ACTION_LOCK_DATABASE || action == Strings.ACTION_UNLOCK_DATABASE || action == Strings.ACTION_OPEN_DATABASE || action == Strings.ACTION_CLOSE_DATABASE) { + dbAction(DatabaseAction(ctx, intent)) + } else { + //TODO handle unexpected action + } + } + + private fun closeEntryView(closeEntryView: CloseEntryViewAction?) {} + + private fun actionSelected(actionSelected: ActionSelectedAction?) {} + + private fun openEntry(oe: OpenEntryAction?) {} + + private fun entryOutputModified(eom: EntryOutputModifiedAction?) {} + + private fun dbAction(db: DatabaseAction?) {} +} diff --git a/app/src/main/java/net/helcel/fidelity/pluginSDK/Strings.kt b/app/src/main/java/net/helcel/fidelity/pluginSDK/Strings.kt new file mode 100644 index 0000000..7b0a46e --- /dev/null +++ b/app/src/main/java/net/helcel/fidelity/pluginSDK/Strings.kt @@ -0,0 +1,195 @@ +package net.helcel.fidelity.pluginSDK + +object Strings { + /** + * Plugin is notified about actions like open/close/update a database. + */ + const val SCOPE_DATABASE_ACTIONS = "keepass2android.SCOPE_DATABASE_ACTIONS" + + /** + * Plugin is notified when an entry is opened. + */ + const val SCOPE_CURRENT_ENTRY = "keepass2android.SCOPE_CURRENT_ENTRY" + + /** + * Plugin may query credentials for its own package + */ + const val SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE = + "keepass2android.SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE" + + /** + * Plugin may query credentials for a deliberate package + */ + const val SCOPE_QUERY_CREDENTIALS = "keepass2android.SCOPE_QUERY_CREDENTIALS" + + /** + * Extra key to transfer a (json serialized) list of scopes + */ + const val EXTRA_SCOPES = "keepass2android.EXTRA_SCOPES" + + + const val EXTRA_PLUGIN_PACKAGE = "keepass2android.EXTRA_PLUGIN_PACKAGE" + + /** + * Extra key for sending the package name of the sender of a broadcast. + * Should be set in every broadcast. + */ + const val EXTRA_SENDER = "keepass2android.EXTRA_SENDER" + + /** + * Extra key for sending a request token. The request token is passed from + * KP2A to the plugin. It's used in the authorization process. + */ + const val EXTRA_REQUEST_TOKEN = "keepass2android.EXTRA_REQUEST_TOKEN" + + /** + * Action to start KP2A with an AppTask + */ + const val ACTION_START_WITH_TASK = "keepass2android.ACTION_START_WITH_TASK" + + /** + * Action sent from KP2A to the plugin to indicate that the plugin should request + * access (sending it's scopes) + */ + const val ACTION_TRIGGER_REQUEST_ACCESS = "keepass2android.ACTION_TRIGGER_REQUEST_ACCESS" + + /** + * Action sent from the plugin to KP2A including the scopes. + */ + const val ACTION_REQUEST_ACCESS = "keepass2android.ACTION_REQUEST_ACCESS" + + /** + * Action sent from the KP2A to the plugin when the user grants access. + * Will contain an access token. + */ + const val ACTION_RECEIVE_ACCESS = "keepass2android.ACTION_RECEIVE_ACCESS" + + /** + * Action sent from KP2A to the plugin to indicate that access is not or no longer valid. + */ + const val ACTION_REVOKE_ACCESS = "keepass2android.ACTION_REVOKE_ACCESS" + + + /** + * Action for startActivity(). Opens an activity in the Plugin Host to edit the plugin settings (i.e. enable it) + */ + const val ACTION_EDIT_PLUGIN_SETTINGS = "keepass2android.ACTION_EDIT_PLUGIN_SETTINGS" + + /** + * Action sent from KP2A to the plugin to indicate that an entry was opened. + * The Intent contains the full entry data. + */ + const val ACTION_OPEN_ENTRY = "keepass2android.ACTION_OPEN_ENTRY" + + /** + * Action sent from KP2A to the plugin to indicate that an entry output field was modified/added. + * The Intent contains the full new entry data. + */ + const val ACTION_ENTRY_OUTPUT_MODIFIED = "keepass2android.ACTION_ENTRY_OUTPUT_MODIFIED" + + /** + * Action sent from KP2A to the plugin to indicate that an entry activity was closed. + */ + const val ACTION_CLOSE_ENTRY_VIEW = "keepass2android.ACTION_CLOSE_ENTRY_VIEW" + + /** + * Extra key for a string containing the GUID of the entry. + */ + const val EXTRA_ENTRY_ID = "keepass2android.EXTRA_ENTRY_DATA" + + /** + * Json serialized data of the PwEntry (C# class) representing the opened entry. + * currently not implemented. + */ + //const val EXTRA_ENTRY_DATA = "keepass2android.EXTRA_ENTRY_DATA"; + + /** + * Json serialized list of fields, transformed using the database context (i.e. placeholders are replaced already) + */ + const val EXTRA_ENTRY_OUTPUT_DATA = "keepass2android.EXTRA_ENTRY_OUTPUT_DATA" + + /** + * Json serialized lisf of field keys, specifying which field of the EXTRA_ENTRY_OUTPUT_DATA is protected. + */ + const val EXTRA_PROTECTED_FIELDS_LIST = "keepass2android.EXTRA_PROTECTED_FIELDS_LIST" + + + /** + * Extra key for passing the access token (both ways) + */ + const val EXTRA_ACCESS_TOKEN = "keepass2android.EXTRA_ACCESS_TOKEN" + + /** + * Action for an intent from the plugin to KP2A to add menu options regarding the currently open entry. + * Requires SCOPE_CURRENT_ENTRY. + */ + const val ACTION_ADD_ENTRY_ACTION = "keepass2android.ACTION_ADD_ENTRY_ACTION" + + const val EXTRA_ACTION_DISPLAY_TEXT = "keepass2android.EXTRA_ACTION_DISPLAY_TEXT" + const val EXTRA_ACTION_ICON_RES_ID = "keepass2android.EXTRA_ACTION_ICON_RES_ID" + + const val EXTRA_FIELD_ID = "keepass2android.EXTRA_FIELD_ID" + + /** + * Used to pass an id for the action. Each actionId may occur only once per field, otherwise the previous + * action with same id is replaced by the new action. + */ + const val EXTRA_ACTION_ID = "keepass2android.EXTRA_ACTION_ID" + + /** Extra for ACTION_ADD_ENTRY_ACTION and ACTION_ENTRY_ACTION_SELECTED to pass data specifying the action parameters.*/ + const val EXTRA_ACTION_DATA = "keepass2android.EXTRA_ACTION_DATA" + + /** + * Action for an intent from KP2A to the plugin when an action added with ACTION_ADD_ENTRY_ACTION was selected by the user. + * + */ + const val ACTION_ENTRY_ACTION_SELECTED = "keepass2android.ACTION_ENTRY_ACTION_SELECTED" + + /** + * Extra key for the string which is used to query the credentials. This should be either a URL for + * a web login (google.com or a full URI) or something in the form "androidapp://com.my.package" + */ + const val EXTRA_QUERY_STRING = "keepass2android.EXTRA_QUERY_STRING" + + /** + * Action when plugin wants to query credentials for its own package + */ + const val ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE = + "keepass2android.ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE" + + + /** + * Action when plugin wants to query credentials for a deliberate package + * The query string is passed as intent data + */ + const val ACTION_QUERY_CREDENTIALS = "keepass2android.ACTION_QUERY_CREDENTIALS" + + /** + * Action for an intent from the plugin to KP2A to set (i.e. add or update) a field in the entry. + * May be used to update existing or add new fields at any time while the entry is opened. + */ + const val ACTION_SET_ENTRY_FIELD = "keepass2android.ACTION_SET_ENTRY_FIELD" + + /** Actions for an intent from KP2A to the plugin to inform that a database was opened, closed, quicklocked or quickunlocked.*/ + const val ACTION_OPEN_DATABASE = "keepass2android.ACTION_OPEN_DATABASE" + const val ACTION_CLOSE_DATABASE = "keepass2android.ACTION_CLOSE_DATABASE" + const val ACTION_LOCK_DATABASE = "keepass2android.ACTION_LOCK_DATABASE" + const val ACTION_UNLOCK_DATABASE = "keepass2android.ACTION_UNLOCK_DATABASE" + + /** Extra for ACTION_OPEN_DATABASE and ACTION_CLOSE_DATABASE containing a filepath which is used + * by KP2A internally to identify the file. Use only where necessary, might contain credentials + * for accessing the file (on remote storage).*/ + const val EXTRA_DATABASE_FILEPATH = "keepass2android.EXTRA_DATABASE_FILEPATH" + + /** Extra for ACTION_OPEN_DATABASE and ACTION_CLOSE_DATABASE containing a filepath which can be + * displayed to the user.*/ + const val EXTRA_DATABASE_FILE_DISPLAYNAME = "keepass2android.EXTRA_DATABASE_FILE_DISPLAYNAME" + + + const val EXTRA_FIELD_VALUE = "keepass2android.EXTRA_FIELD_VALUE" + const val EXTRA_FIELD_PROTECTED = "keepass2android.EXTRA_FIELD_PROTECTED" + + const val PREFIX_STRING = "STRING_" + const val PREFIX_BINARY = "BINARY_" + +} diff --git a/app/src/main/java/net/helcel/fidelity/tools/BacodeScanner.kt b/app/src/main/java/net/helcel/fidelity/tools/BacodeScanner.kt new file mode 100644 index 0000000..28803b3 --- /dev/null +++ b/app/src/main/java/net/helcel/fidelity/tools/BacodeScanner.kt @@ -0,0 +1,77 @@ +package net.helcel.fidelity.tools + +import android.content.ContentValues +import android.util.Log +import androidx.annotation.OptIn +import androidx.camera.core.ExperimentalGetImage +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.ImageProxy +import com.google.mlkit.vision.barcode.BarcodeScanner +import com.google.mlkit.vision.barcode.BarcodeScannerOptions +import com.google.mlkit.vision.barcode.BarcodeScanning +import com.google.mlkit.vision.barcode.common.Barcode +import com.google.mlkit.vision.common.InputImage +import net.helcel.fidelity.tools.BarcodeFormatConverter.formatToString +import java.util.concurrent.Executors + + +@OptIn(ExperimentalGetImage::class) +object BarcodeScanner { + + private fun processImageProxy( + barcodeScanner: BarcodeScanner, + imageProxy: ImageProxy, + cb: (String?, String?) -> Unit + ) { + + imageProxy.image?.let { image -> + val inputImage = + InputImage.fromMediaImage( + image, + imageProxy.imageInfo.rotationDegrees + ) + + barcodeScanner.process(inputImage) + .addOnSuccessListener { barcodeList -> + println(barcodeList.map { e -> e.displayValue }) + println(barcodeList.map { e -> e.format }) + val barcode = + barcodeList.getOrNull(0) + if (barcode != null) + cb(barcode.displayValue, formatToString(barcode.format)) + } + .addOnFailureListener { + Log.e(ContentValues.TAG, it.message.orEmpty()) + }.addOnCompleteListener { + imageProxy.image?.close() + imageProxy.close() + } + } + } + + fun getAnalysisUseCase(cb: (String?, String?) -> Unit): ImageAnalysis { + val options = BarcodeScannerOptions.Builder().setBarcodeFormats( + Barcode.FORMAT_CODE_128, + Barcode.FORMAT_CODE_39, + Barcode.FORMAT_CODE_93, + Barcode.FORMAT_EAN_8, + Barcode.FORMAT_EAN_13, + Barcode.FORMAT_QR_CODE, + Barcode.FORMAT_UPC_A, + Barcode.FORMAT_UPC_E, + Barcode.FORMAT_PDF417 + ).build() + val scanner = BarcodeScanning.getClient(options) + val analysisUseCase = ImageAnalysis.Builder() + .build() + + analysisUseCase.setAnalyzer( + Executors.newSingleThreadExecutor() + ) { imageProxy -> + processImageProxy(scanner, imageProxy, cb) + } + return analysisUseCase + } + + +} \ No newline at end of file diff --git a/app/src/main/java/net/helcel/fidelity/tools/BarcodeFormatConverter.kt b/app/src/main/java/net/helcel/fidelity/tools/BarcodeFormatConverter.kt new file mode 100644 index 0000000..03df33a --- /dev/null +++ b/app/src/main/java/net/helcel/fidelity/tools/BarcodeFormatConverter.kt @@ -0,0 +1,38 @@ +package net.helcel.fidelity.tools + +import com.google.mlkit.vision.barcode.common.Barcode +import com.google.zxing.BarcodeFormat + +object BarcodeFormatConverter { + + fun stringToFormat(f: String): BarcodeFormat { + return when (f) { + "CODE_128" -> BarcodeFormat.CODE_128 + "CODE_39" -> BarcodeFormat.CODE_39 + "CODE_93" -> BarcodeFormat.CODE_93 + "EAN_8" -> BarcodeFormat.EAN_8 + "EAN_13" -> BarcodeFormat.EAN_13 + "CODE_QR" -> BarcodeFormat.QR_CODE + "UPC_A" -> BarcodeFormat.UPC_A + "UPC_E" -> BarcodeFormat.UPC_E + "PDF_417" -> BarcodeFormat.PDF_417 + else -> throw Exception("Unsupported Format: $f") + } + } + + + fun formatToString(f: Int): String { + return when (f) { + Barcode.FORMAT_CODE_128 -> "CODE_128" + Barcode.FORMAT_CODE_39 -> "CODE_39" + Barcode.FORMAT_CODE_93 -> "CODE_93" + Barcode.FORMAT_EAN_8 -> "EAN_8" + Barcode.FORMAT_EAN_13 -> "EAN_13" + Barcode.FORMAT_QR_CODE -> "CODE_QR" + Barcode.FORMAT_UPC_A -> "UPC_A" + Barcode.FORMAT_UPC_E -> "UPC_E" + Barcode.FORMAT_PDF417 -> "PDF_417" + else -> throw Exception("Unsupported Format: $f") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/net/helcel/fidelity/tools/BarcodeGenerator.kt b/app/src/main/java/net/helcel/fidelity/tools/BarcodeGenerator.kt new file mode 100644 index 0000000..d74aa80 --- /dev/null +++ b/app/src/main/java/net/helcel/fidelity/tools/BarcodeGenerator.kt @@ -0,0 +1,57 @@ +package net.helcel.fidelity.tools + +import android.graphics.Bitmap +import com.google.zxing.BarcodeFormat +import com.google.zxing.MultiFormatWriter +import com.google.zxing.WriterException +import com.google.zxing.common.BitMatrix +import net.helcel.fidelity.tools.BarcodeFormatConverter.stringToFormat + +object BarcodeGenerator { + + private fun getPixelColor(bitMatrix: BitMatrix, x: Int, y: Int): Int { + if (x >= bitMatrix.width || y >= bitMatrix.height) + return android.graphics.Color.WHITE + + return if (bitMatrix[x, y]) + android.graphics.Color.BLACK + else + android.graphics.Color.WHITE + } + + fun generateBarcode(content: String, f: String, width: Int): Bitmap? { + if (content.isEmpty() || f.isEmpty()) { + return null + } + try { + val format = stringToFormat(f) + val writer = MultiFormatWriter() + val height = (formatToRatio(format) * width).toInt() + val bitMatrix: BitMatrix = writer.encode(content, format, width, height) + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + + for (x in 0 until width) { + for (y in 0 until height) { + bitmap.setPixel( + x, + y, + getPixelColor(bitMatrix, x, y) + ) + + } + } + return bitmap + } catch (e: WriterException) { + e.printStackTrace() + } + return null + } + + private fun formatToRatio(format: BarcodeFormat): Double { + return when (format) { + BarcodeFormat.QR_CODE -> 1.0 + BarcodeFormat.PDF_417 -> 0.4 + else -> 0.5 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/net/helcel/fidelity/tools/CacheManager.kt b/app/src/main/java/net/helcel/fidelity/tools/CacheManager.kt new file mode 100644 index 0000000..ccbe2c6 --- /dev/null +++ b/app/src/main/java/net/helcel/fidelity/tools/CacheManager.kt @@ -0,0 +1,45 @@ +package net.helcel.fidelity.tools + +import android.content.SharedPreferences +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken + + +object CacheManager { + + const val PREF_NAME = "FIDELITY" + private const val ENTRY_KEY = "FIDELITY" + private var data: ArrayList> = ArrayList() + private var pref: SharedPreferences? = null + + fun addFidelity(item: Triple) { + val exists = data.find { it.first == item.first } + if (exists != null) + data.remove(exists) + + data.add(0, item) + saveFidelity() + } + + private fun saveFidelity() { + val editor = pref?.edit() + val gson = Gson() + val json = gson.toJson(data) + editor?.putString(ENTRY_KEY, json) + editor?.apply() + } + + fun loadFidelity(pref: SharedPreferences) { + this.pref = pref + val gson = Gson() + val json = pref.getString(ENTRY_KEY, null) + val type = object : TypeToken>>() {}.type + data = gson.fromJson(json, type) ?: ArrayList() + + } + + fun getFidelity(): ArrayList> { + return data + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/helcel/fidelity/tools/ErrorToaster.kt b/app/src/main/java/net/helcel/fidelity/tools/ErrorToaster.kt new file mode 100644 index 0000000..8f44ed1 --- /dev/null +++ b/app/src/main/java/net/helcel/fidelity/tools/ErrorToaster.kt @@ -0,0 +1,22 @@ +package net.helcel.fidelity.tools + +import android.app.Activity +import android.widget.Toast + +object ErrorToaster { + private fun helper(activity: Activity, message: String, length: Int) { + Toast.makeText(activity, message, length).show() + } + + fun noKP2AFound(activity: Activity) { + helper(activity, "KeePass2Android Not Installed", Toast.LENGTH_LONG) + } + + fun formIncomplete(activity: Activity) { + helper(activity, "Form Incomplete", Toast.LENGTH_SHORT) + } + + fun invalidFormat(activity: Activity) { + helper(activity, "Invalid Format", Toast.LENGTH_SHORT) + } +} diff --git a/app/src/main/java/net/helcel/fidelity/tools/KeepassWrapper.kt b/app/src/main/java/net/helcel/fidelity/tools/KeepassWrapper.kt new file mode 100644 index 0000000..8d93062 --- /dev/null +++ b/app/src/main/java/net/helcel/fidelity/tools/KeepassWrapper.kt @@ -0,0 +1,103 @@ +package net.helcel.fidelity.tools + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.fragment.app.Fragment +import net.helcel.fidelity.pluginSDK.KeepassDefs +import net.helcel.fidelity.pluginSDK.Kp2aControl + +object KeepassWrapper { + + private const val CODE_FIELD: String = "FidelityCode" + private const val FORMAT_FIELD: String = "FidelityFormat" + private const val PROTECT_CODE_FIELD: String = "FidelityProtectedCode" + + fun entryCreate( + fragment: Fragment, + title: String, + code: String, + format: String, + protectCode: Boolean, + ): Pair, ArrayList> { + + val fields = HashMap() + val protected = ArrayList() + fields[KeepassDefs.TitleField] = title + fields[KeepassDefs.UrlField] = + "androidapp://" + fragment.requireActivity().packageName + fields[CODE_FIELD] = code + fields[FORMAT_FIELD] = format + fields[PROTECT_CODE_FIELD] = protectCode.toString() + + if (protectCode) { + protected.add(CODE_FIELD) + } + return Pair(fields, protected) + } + + + fun resultLauncherAdd( + fragment: Fragment, + callback: (HashMap) -> Unit + ): ActivityResultLauncher { + 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) -> Unit + ): ActivityResultLauncher { + return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + val data: Intent? = result.data + val credentials = Kp2aControl.getEntryFieldsFromIntent( + data!! + ) + println(credentials) + callback(credentials) + } + } + } + + fun entryExtract(map: HashMap): Triple { + return Triple( + map[KeepassDefs.TitleField], + map[CODE_FIELD], + map[FORMAT_FIELD] + ) + } + + fun bundleCreate(title: String?, code: String?, fmt: String?): Bundle { + val data = Bundle() + data.putString("title", title) + data.putString("code", code) + data.putString("fmt", fmt) + return data + } + + fun bundleExtract(data: Bundle?): Triple { + return Triple( + data?.getString("title"), + data?.getString("code"), + data?.getString("fmt") + ) + } + + fun isProtected(map: HashMap): Boolean { + return map[PROTECT_CODE_FIELD].toBoolean() + } + + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/barcode.xml b/app/src/main/res/drawable/barcode.xml new file mode 100644 index 0000000..2ba4120 --- /dev/null +++ b/app/src/main/res/drawable/barcode.xml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/bookmark.xml b/app/src/main/res/drawable/bookmark.xml new file mode 100644 index 0000000..4299d66 --- /dev/null +++ b/app/src/main/res/drawable/bookmark.xml @@ -0,0 +1,35 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/camera.xml b/app/src/main/res/drawable/camera.xml new file mode 100644 index 0000000..463d6e2 --- /dev/null +++ b/app/src/main/res/drawable/camera.xml @@ -0,0 +1,50 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/card.xml b/app/src/main/res/drawable/card.xml new file mode 100644 index 0000000..39b1967 --- /dev/null +++ b/app/src/main/res/drawable/card.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/cross.xml b/app/src/main/res/drawable/cross.xml new file mode 100644 index 0000000..1cce650 --- /dev/null +++ b/app/src/main/res/drawable/cross.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/edit.xml b/app/src/main/res/drawable/edit.xml new file mode 100644 index 0000000..2993565 --- /dev/null +++ b/app/src/main/res/drawable/edit.xml @@ -0,0 +1,44 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/heart.xml b/app/src/main/res/drawable/heart.xml new file mode 100644 index 0000000..9a4cca7 --- /dev/null +++ b/app/src/main/res/drawable/heart.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/drawable/key.xml b/app/src/main/res/drawable/key.xml new file mode 100644 index 0000000..0b51384 --- /dev/null +++ b/app/src/main/res/drawable/key.xml @@ -0,0 +1,31 @@ + + + + + + + diff --git a/app/src/main/res/drawable/lock_checkbox.xml b/app/src/main/res/drawable/lock_checkbox.xml new file mode 100644 index 0000000..57ef708 --- /dev/null +++ b/app/src/main/res/drawable/lock_checkbox.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/locked.xml b/app/src/main/res/drawable/locked.xml new file mode 100644 index 0000000..21f1db9 --- /dev/null +++ b/app/src/main/res/drawable/locked.xml @@ -0,0 +1,31 @@ + + + + + + + diff --git a/app/src/main/res/drawable/locked_fill.xml b/app/src/main/res/drawable/locked_fill.xml new file mode 100644 index 0000000..7e9298e --- /dev/null +++ b/app/src/main/res/drawable/locked_fill.xml @@ -0,0 +1,46 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/logo.xml b/app/src/main/res/drawable/logo.xml new file mode 100644 index 0000000..b0eaf36 --- /dev/null +++ b/app/src/main/res/drawable/logo.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/minus.xml b/app/src/main/res/drawable/minus.xml new file mode 100644 index 0000000..30bf750 --- /dev/null +++ b/app/src/main/res/drawable/minus.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/save.xml b/app/src/main/res/drawable/save.xml new file mode 100644 index 0000000..9f33171 --- /dev/null +++ b/app/src/main/res/drawable/save.xml @@ -0,0 +1,36 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/search.xml b/app/src/main/res/drawable/search.xml new file mode 100644 index 0000000..40e0395 --- /dev/null +++ b/app/src/main/res/drawable/search.xml @@ -0,0 +1,28 @@ + + + + + + diff --git a/app/src/main/res/drawable/unlocked.xml b/app/src/main/res/drawable/unlocked.xml new file mode 100644 index 0000000..3e7bf6f --- /dev/null +++ b/app/src/main/res/drawable/unlocked.xml @@ -0,0 +1,24 @@ + + + + + + diff --git a/app/src/main/res/layout/act_main.xml b/app/src/main/res/layout/act_main.xml new file mode 100644 index 0000000..7b75a3e --- /dev/null +++ b/app/src/main/res/layout/act_main.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/frag_create_entry.xml b/app/src/main/res/layout/frag_create_entry.xml new file mode 100644 index 0000000..886d3b4 --- /dev/null +++ b/app/src/main/res/layout/frag_create_entry.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/frag_launcher.xml b/app/src/main/res/layout/frag_launcher.xml new file mode 100644 index 0000000..3b6bbae --- /dev/null +++ b/app/src/main/res/layout/frag_launcher.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/frag_scanner.xml b/app/src/main/res/layout/frag_scanner.xml new file mode 100644 index 0000000..fba9927 --- /dev/null +++ b/app/src/main/res/layout/frag_scanner.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/frag_view_entry.xml b/app/src/main/res/layout/frag_view_entry.xml new file mode 100644 index 0000000..b12a9dd --- /dev/null +++ b/app/src/main/res/layout/frag_view_entry.xml @@ -0,0 +1,39 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/list_item_dropdown.xml b/app/src/main/res/layout/list_item_dropdown.xml new file mode 100644 index 0000000..606dcfa --- /dev/null +++ b/app/src/main/res/layout/list_item_dropdown.xml @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_item_fidelity.xml b/app/src/main/res/layout/list_item_fidelity.xml new file mode 100644 index 0000000..c744e4b --- /dev/null +++ b/app/src/main/res/layout/list_item_fidelity.xml @@ -0,0 +1,30 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..94fd7e0 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,3 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..35b495d --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,29 @@ + + + Fidelity + Stores and Displays fidelity and other cards + [soraefir](soraefir) + + Keepass Fidelity + + barcode preview + Expand + Manual + Scan + Query + Title + Code + Format + Save + + CODE_128 + CODE_39 + CODE_93 + EAN_8 + EAN_13 + CODE_QR + UPC_A + UPC_E + PDF_417 + + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..f11f745 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,3 @@ + + + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..952b68d --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..5cccdb8 --- /dev/null +++ b/build.gradle @@ -0,0 +1,7 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +plugins { + id 'com.android.application' version '8.3.0' apply false + id 'com.android.library' version '8.3.0' apply false + id 'org.jetbrains.kotlin.android' version '1.9.23' apply false +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..1a982b5 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,24 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +android.enableJetifier=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..8c0fb64 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..52cdcda --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sat Mar 09 17:54:03 GMT 2024 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..91a7e26 --- /dev/null +++ b/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..9377a07 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + maven { url 'https://jitpack.io' } + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + maven { url 'https://jitpack.io' } + } +} +rootProject.name = "BeenDroid" +include ':app'