gitignore & init

This commit is contained in:
soraefir 2024-03-17 14:50:15 +01:00
parent 75dfdafebf
commit 7dc43c0682
Signed by: sora
GPG Key ID: A362EA0491E2EEA0
58 changed files with 3060 additions and 0 deletions

13
.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
*.iml
.idea/
node_modules/
temp/
.gradle
local.properties/
.DS_Store
build/
app/build/
captures/
.externalNativeBuild
.cxx
local.properties

54
app/build.gradle Normal file
View File

@ -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'
}

3
app/lint.xml Normal file
View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<lint>
</lint>

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="1"
android:versionName="1.0">
<uses-feature android:name="android.hardware.camera" />
<uses-permission android:name="android.permission.CAMERA" />
<application
android:icon="@drawable/logo"
android:label="@string/app_name"
android:supportsRtl="true">
<activity
android:name=".activity.MainActivity"
android:exported="true"
android:theme="@style/Theme.Fidelity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<receiver
android:name=".pluginSDK.PluginAccessReceiver"
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>
<receiver
android:name=".pluginSDK.PluginActionBroadcastReceiver"
android:exported="true">
<intent-filter>
<action android:name="keepass2android.ACTION_OPEN_ENTRY" />
<action android:name="keepass2android.ACTION_CLOSE_ENTRY_VIEW" />
<action android:name="keepass2android.ACTION_ENTRY_ACTION_SELECTED" />
<action android:name="keepass2android.ACTION_LOCK_DATABASE" />
<action android:name="keepass2android.ACTION_UNLOCK_DATABASE" />
<action android:name="keepass2android.ACTION_CLOSE_DATABASE" />
<action android:name="keepass2android.ACTION_OPEN_DATABASE" />
</intent-filter>
</receiver>
</application>
</manifest>

View File

@ -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()
}
}

View File

@ -0,0 +1,50 @@
package net.helcel.fidelity.activity.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.LinearLayout
import androidx.recyclerview.widget.RecyclerView
import net.helcel.fidelity.databinding.ListItemFidelityBinding
class FidelityListAdapter(
private val triples: ArrayList<Triple<String?, String?, String?>>,
private val onItemClicked: (Triple<String?, String?, String?>) -> Unit
) :
RecyclerView.Adapter<FidelityListAdapter.FidelityViewHolder>() {
private lateinit var binding: ListItemFidelityBinding
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FidelityViewHolder {
binding = ListItemFidelityBinding.inflate(LayoutInflater.from(parent.context))
binding.root.setLayoutParams(
LinearLayout.LayoutParams(
MATCH_PARENT, WRAP_CONTENT
)
)
return FidelityViewHolder(binding.root)
}
override fun onBindViewHolder(holder: FidelityViewHolder, position: Int) {
val triple = triples[position]
holder.bind(triple)
}
override fun getItemCount(): Int = triples.size
inner class FidelityViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(triple: Triple<String?, String?, String?>) {
val text = "${triple.first}"
binding.textView.text = text
binding.card.setOnClickListener { onItemClicked(triple) }
}
}
}

View File

@ -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()
}
}

View File

@ -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)
}
}

View File

@ -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()))
}
}

View File

@ -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
}
}
}

View File

@ -0,0 +1,179 @@
package net.helcel.fidelity.pluginSDK
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.text.TextUtils
import android.util.Log
import org.json.JSONArray
import org.json.JSONException
object AccessManager {
private const val _tag = "Kp2aPluginSDK"
private const val PREF_KEY_SCOPE = "scope"
private const val PREF_KEY_TOKEN = "token"
private fun stringArrayToString(values: ArrayList<String?>): String? {
val a = JSONArray()
for (i in values.indices) {
a.put(values[i])
}
return if (values.isNotEmpty()) {
a.toString()
} else {
null
}
}
private fun stringToStringArray(s: String?): ArrayList<String> {
val strings = ArrayList<String>()
if (!TextUtils.isEmpty(s)) {
try {
val a = JSONArray(s)
for (i in 0 until a.length()) {
val url = a.optString(i)
strings.add(url)
}
} catch (e: JSONException) {
e.printStackTrace()
}
}
return strings
}
fun storeAccessToken(
ctx: Context,
hostPackage: String,
accessToken: String,
scopes: ArrayList<String?>
) {
val prefs = getPrefsForHost(ctx, hostPackage)
val edit = prefs.edit()
edit.putString(PREF_KEY_TOKEN, accessToken)
val scopesString = stringArrayToString(scopes)
edit.putString(PREF_KEY_SCOPE, scopesString)
edit.apply()
Log.d(
_tag,
"stored access token " + accessToken.substring(
0,
4
) + "... for " + scopes.size + " scopes (" + scopesString + ")."
)
val hostPrefs = ctx.getSharedPreferences("KP2A.PluginAccess.hosts", Context.MODE_PRIVATE)
if (!hostPrefs.contains(hostPackage)) {
hostPrefs.edit().putString(hostPackage, "").apply()
}
}
fun preparePopup(popupMenu: Any) {
try {
val fields = popupMenu.javaClass.declaredFields
for (field in fields) {
if ("mPopup" == field.name) {
field.isAccessible = true
val menuPopupHelper = field[popupMenu]
val classPopupHelper = Class.forName(
menuPopupHelper
.javaClass.name
)
val setForceIcons = classPopupHelper.getMethod(
"setForceShowIcon", Boolean::class.javaPrimitiveType
)
setForceIcons.invoke(menuPopupHelper, true)
break
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun getPrefsForHost(
ctx: Context,
hostPackage: String
): SharedPreferences {
val prefs = ctx.getSharedPreferences("KP2A.PluginAccess.$hostPackage", Context.MODE_PRIVATE)
return prefs
}
fun tryGetAccessToken(ctx: Context, hostPackage: String, scopes: ArrayList<String?>): String? {
if (TextUtils.isEmpty(hostPackage)) {
Log.d(_tag, "hostPackage is empty!")
return null
}
Log.d(_tag, "trying to find prefs for $hostPackage")
val prefs = getPrefsForHost(ctx, hostPackage)
val scopesString = prefs.getString(PREF_KEY_SCOPE, "")
Log.d(_tag, "available scopes: $scopesString")
val currentScope = stringToStringArray(scopesString)
if (isSubset(scopes, currentScope)) {
return prefs.getString(PREF_KEY_TOKEN, null)
} else {
Log.d(_tag, "looks like scope changed. Access token invalid.")
return null
}
}
private fun isSubset(
requiredScopes: ArrayList<String?>,
availableScopes: ArrayList<String>
): Boolean {
for (r in requiredScopes) {
if (availableScopes.indexOf(r) < 0) {
Log.d(_tag, "Scope " + r + " not available. " + availableScopes.size)
return false
}
}
return true
}
fun removeAccessToken(
ctx: Context, hostPackage: String,
accessToken: String
) {
val prefs = getPrefsForHost(ctx, hostPackage)
Log.d(_tag, "removing AccessToken.")
if (prefs.getString(PREF_KEY_TOKEN, "") == accessToken) {
val edit = prefs.edit()
edit.clear()
edit.apply()
}
val hostPrefs = ctx.getSharedPreferences("KP2A.PluginAccess.hosts", Context.MODE_PRIVATE)
if (hostPrefs.contains(hostPackage)) {
hostPrefs.edit().remove(hostPackage).apply()
}
}
fun getAllHostPackages(ctx: Context): Set<String> {
val prefs = ctx.getSharedPreferences("KP2A.PluginAccess.hosts", Context.MODE_PRIVATE)
val result: MutableSet<String> = HashSet()
for (host in prefs.all.keys) {
try {
val info = ctx.packageManager.getPackageInfo(host, PackageManager.GET_META_DATA)
//if we get here, the package is still there
result.add(host)
} catch (e: PackageManager.NameNotFoundException) {
//host gone. ignore.
}
}
return result
}
/**
* Returns a valid access token or throws PluginAccessException
*/
fun getAccessToken(
context: Context, hostPackage: String,
scopes: ArrayList<String?>
): String {
val accessToken = tryGetAccessToken(context, hostPackage, scopes)
?: throw PluginAccessException(hostPackage, scopes)
return accessToken
}
}

View File

@ -0,0 +1,45 @@
package net.helcel.fidelity.pluginSDK
object KeepassDefs {
/// <summary>
/// Default identifier string for the title field. Should not contain
/// spaces, tabs or other whitespace.
/// </summary>
var TitleField: String = "Title"
/// <summary>
/// Default identifier string for the user name field. Should not contain
/// spaces, tabs or other whitespace.
/// </summary>
private var UserNameField: String = "UserName"
/// <summary>
/// Default identifier string for the password field. Should not contain
/// spaces, tabs or other whitespace.
/// </summary>
private var PasswordField: String = "Password"
/// <summary>
/// Default identifier string for the URL field. Should not contain
/// spaces, tabs or other whitespace.
/// </summary>
var UrlField: String = "URL"
/// <summary>
/// Default identifier string for the notes field. Should not contain
/// spaces, tabs or other whitespace.
/// </summary>
private var NotesField: String = "Notes"
fun IsStandardField(strFieldName: String?): Boolean {
if (strFieldName == null) return false
if (strFieldName == TitleField) return true
if (strFieldName == UserNameField) return true
if (strFieldName == PasswordField) return true
if (strFieldName == UrlField) return true
if (strFieldName == NotesField) return true
return false
}
}

View File

@ -0,0 +1,107 @@
package net.helcel.fidelity.pluginSDK
import android.content.Intent
import android.text.TextUtils
import org.json.JSONException
import org.json.JSONObject
object Kp2aControl {
/**
* Creates and returns an intent to launch Keepass2Android for adding an entry with the given fields.
* @param fields Key/Value pairs of the field values. See KeepassDefs for standard keys.
* @param protectedFields List of keys of the protected fields.
* @return Intent to start Keepass2Android.
* @throws JSONException
*/
fun getAddEntryIntent(
fields: HashMap<String?, String?>?,
protectedFields: ArrayList<String?>?
): Intent {
return getAddEntryIntent(JSONObject((fields as Map<*, *>?)!!).toString(), protectedFields)
}
private fun getAddEntryIntent(
outputData: String?,
protectedFields: ArrayList<String?>?
): Intent {
val startKp2aIntent = Intent(Strings.ACTION_START_WITH_TASK)
startKp2aIntent.addCategory(Intent.CATEGORY_DEFAULT)
startKp2aIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
startKp2aIntent.putExtra("KP2A_APPTASK", "CreateEntryThenCloseTask")
startKp2aIntent.putExtra("ShowUserNotifications", "false") //KP2A expects a StringExtra
startKp2aIntent.putExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA, outputData)
if (protectedFields != null) startKp2aIntent.putStringArrayListExtra(
Strings.EXTRA_PROTECTED_FIELDS_LIST,
protectedFields
)
return startKp2aIntent
}
/**
* Creates an intent to open a Password Entry matching searchText
* @param searchText queryString
* @param showUserNotifications if true, the notifications (copy to clipboard, keyboard) are displayed
* @param closeAfterOpen if true, the entry is opened and KP2A is immediately closed
* @return Intent to start KP2A with
*/
fun getOpenEntryIntent(
searchText: String?,
showUserNotifications: Boolean,
closeAfterOpen: Boolean
): Intent {
val startKp2aIntent = Intent(Strings.ACTION_START_WITH_TASK)
startKp2aIntent.addCategory(Intent.CATEGORY_DEFAULT)
startKp2aIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
startKp2aIntent.putExtra("KP2A_APPTASK", "SearchUrlTask")
startKp2aIntent.putExtra("ShowUserNotifications", showUserNotifications.toString())
startKp2aIntent.putExtra("CloseAfterCreate", closeAfterOpen.toString())
startKp2aIntent.putExtra("UrlToSearch", searchText)
return startKp2aIntent
}
/**
* Creates an intent to query a password entry from KP2A. The credentials are returned as Activity result.
* @param searchText Text to search for. Should be a URL or "androidapp://com.my.package."
* @return an Intent to start KP2A with
*/
fun getQueryEntryIntent(searchText: String?): Intent {
val i = Intent(Strings.ACTION_QUERY_CREDENTIALS)
if (!TextUtils.isEmpty(searchText)) i.putExtra(Strings.EXTRA_QUERY_STRING, searchText)
return i
}
val queryEntryIntentForOwnPackage: Intent
/**
* Creates an intent to query a password entry from KP2A, matching to the current app's package .
* The credentials are returned as Activity result.
* This requires SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE.
* @return an Intent to start KP2A with
*/
get() = Intent(Strings.ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE)
/**
* Converts the entry fields returned in an intent from a query to a hashmap.
* @param intent data received in onActivityResult after getQueryEntryIntent(ForOwnPackage)
* @return HashMap with keys = field names (see KeepassDefs for standard keys) and values = values
*/
fun getEntryFieldsFromIntent(intent: Intent): HashMap<String, String> {
val res = HashMap<String, String>()
try {
val json = JSONObject(intent.getStringExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA)!!)
val iter = json.keys()
while (iter.hasNext()) {
val key = iter.next()
val value = json[key].toString()
res[key] = value
}
} catch (e: JSONException) {
e.printStackTrace()
} catch (e: NullPointerException) {
e.printStackTrace()
}
return res
}
}

View File

@ -0,0 +1,82 @@
package net.helcel.fidelity.pluginSDK
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
/**
* Broadcast flow between Host and Plugin
* ======================================
*
* The host is responsible for deciding when to initiate the session. It
* should initiate the session as soon as plugins are required or when a plugin
* has been updated through the OS.
* It will then send a broadcast to request the currently required scope.
* The plugin then sends a broadcast to the app which scope is required. If an
* access token is already available, it's sent along with the requset.
*
* If a previous permission has been revoked (or the app settings cleared or the
* permissions have been extended or the token is invalid for any other reason)
* the host will answer with a Revoked-Permission broadcast (i.e. the plugin is
* unconnected.)
*
* Unconnected plugins must be permitted by the user (requiring user action).
* When the user grants access, the plugin will receive an access token for
* the host. This access token is valid for the requested scope. If the scope
* changes (e.g after an update of the plugin), the access token becomes invalid.
*
*/
abstract class PluginAccessBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(ctx: Context, intent: Intent) {
val action = intent.action
Log.d(_tag, "received broadcast with action=$action")
if (action == null) return
when (action) {
Strings.ACTION_TRIGGER_REQUEST_ACCESS -> requestAccess(ctx, intent)
Strings.ACTION_RECEIVE_ACCESS -> receiveAccess(ctx, intent)
Strings.ACTION_REVOKE_ACCESS -> revokeAccess(ctx, intent)
else -> {}
}
}
private fun revokeAccess(ctx: Context, intent: Intent) {
val senderPackage = intent.getStringExtra(Strings.EXTRA_SENDER)
val accessToken = intent.getStringExtra(Strings.EXTRA_ACCESS_TOKEN)
AccessManager.removeAccessToken(ctx, senderPackage!!, accessToken!!)
}
private fun receiveAccess(ctx: Context, intent: Intent) {
val senderPackage = intent.getStringExtra(Strings.EXTRA_SENDER)
val accessToken = intent.getStringExtra(Strings.EXTRA_ACCESS_TOKEN)
AccessManager.storeAccessToken(ctx, senderPackage!!, accessToken!!, scopes)
}
private fun requestAccess(ctx: Context, intent: Intent) {
val senderPackage = intent.getStringExtra(Strings.EXTRA_SENDER)
val requestToken = intent.getStringExtra(Strings.EXTRA_REQUEST_TOKEN)
val rpi = Intent(Strings.ACTION_REQUEST_ACCESS)
rpi.setPackage(senderPackage)
rpi.putExtra(Strings.EXTRA_SENDER, ctx.packageName)
rpi.putExtra(Strings.EXTRA_REQUEST_TOKEN, requestToken)
val token: String? = AccessManager.tryGetAccessToken(ctx, senderPackage!!, scopes)
rpi.putExtra(Strings.EXTRA_ACCESS_TOKEN, token)
rpi.putStringArrayListExtra(Strings.EXTRA_SCOPES, scopes)
Log.d(_tag, "requesting access for " + scopes.size + " tokens.")
ctx.sendBroadcast(rpi)
}
/**
*
* @return the list of required scopes for this plugin.
*/
abstract val scopes: ArrayList<String?>
companion object {
private const val _tag = "Kp2aPluginSDK"
}
}

View File

@ -0,0 +1,14 @@
package net.helcel.fidelity.pluginSDK
class PluginAccessException : Exception {
constructor(what: String?) : super(what)
constructor(hostPackage: String?, scopes: ArrayList<String?>)
companion object {
/**
*
*/
private const val serialVersionUID = 1L
}
}

View File

@ -0,0 +1,16 @@
package net.helcel.fidelity.pluginSDK
import kotlin.collections.ArrayList
class PluginAccessReceiver : PluginAccessBroadcastReceiver() {
override val scopes: ArrayList<String?> = ArrayList()
init {
this.scopes.add(Strings.SCOPE_DATABASE_ACTIONS)
this.scopes.add(Strings.SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE)
}
}

View File

@ -0,0 +1,224 @@
package net.helcel.fidelity.pluginSDK
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
class PluginActionBroadcastReceiver : BroadcastReceiver() {
open class PluginActionBase
(var context: Context, protected var _intent: Intent) {
val hostPackage: String?
get() = _intent.getStringExtra(Strings.EXTRA_SENDER)
}
open class PluginEntryActionBase(context: Context, intent: Intent) :
PluginActionBase(context, intent) {
protected val entryFieldsFromIntent: HashMap<String, String>
get() {
val res = HashMap<String, String>()
try {
val json = JSONObject(_intent.getStringExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA)!!)
val iter = json.keys()
while (iter.hasNext()) {
val key = iter.next()
val value = json[key].toString()
res[key] = value
}
} catch (e: JSONException) {
e.printStackTrace()
}
return res
}
protected val protectedFieldsListFromIntent: Array<String?>?
get() {
try {
val json =
JSONArray(_intent.getStringExtra(Strings.EXTRA_PROTECTED_FIELDS_LIST))
val res = arrayOfNulls<String>(json.length())
for (i in 0 until json.length()) res[i] = json.getString(i)
return res
} catch (e: JSONException) {
e.printStackTrace()
return null
}
}
open val entryId: String?
get() = _intent.getStringExtra(Strings.EXTRA_ENTRY_ID)
@Throws(PluginAccessException::class)
fun setEntryField(fieldId: String?, fieldValue: String?, isProtected: Boolean) {
val i = Intent(Strings.ACTION_SET_ENTRY_FIELD)
val scope = ArrayList<String?>()
scope.add(Strings.SCOPE_CURRENT_ENTRY)
i.putExtra(
Strings.EXTRA_ACCESS_TOKEN, AccessManager.getAccessToken(
context, hostPackage!!, scope
)
)
i.setPackage(hostPackage)
i.putExtra(Strings.EXTRA_SENDER, context.packageName)
i.putExtra(Strings.EXTRA_FIELD_VALUE, fieldValue)
i.putExtra(Strings.EXTRA_ENTRY_ID, entryId)
i.putExtra(Strings.EXTRA_FIELD_ID, fieldId)
i.putExtra(Strings.EXTRA_FIELD_PROTECTED, isProtected)
context.sendBroadcast(i)
}
}
private inner class ActionSelectedAction(ctx: Context, intent: Intent) :
PluginEntryActionBase(ctx, intent) {
val actionData: Bundle?
/**
*
* @return the Bundle associated with the action. This bundle can be set in OpenEntry.add(Entry)FieldAction
*/
get() = _intent.getBundleExtra(Strings.EXTRA_ACTION_DATA)
private val fieldId: String?
/**
*
* @return the field id which was selected. null if an entry action (in the options menu) was selected.
*/
get() = _intent.getStringExtra(Strings.EXTRA_FIELD_ID)
val isEntryAction: Boolean
/**
*
* @return true if an entry action, i.e. an option from the options menu, was selected. False if an option
* in a popup menu for a certain field was selected.
*/
get() = fieldId == null
val entryFields: HashMap<String, String>
/**
*
* @return a hashmap containing the entry fields in key/value form
*/
get() = entryFieldsFromIntent
val protectedFieldsList: Array<String?>?
/**
*
* @return an array with the keys of all protected fields in the entry
*/
get() = protectedFieldsListFromIntent
}
private inner class CloseEntryViewAction(context: Context, intent: Intent) :
PluginEntryActionBase(context, intent) {
override val entryId: String?
get() = _intent.getStringExtra(Strings.EXTRA_ENTRY_ID)
}
private open inner class OpenEntryAction(context: Context, intent: Intent) :
PluginEntryActionBase(context, intent) {
val entryFields: HashMap<String, String>
get() = entryFieldsFromIntent
val protectedFieldsList: Array<String?>?
/**
*
* @return an array with the keys of all protected fields in the entry
*/
get() = protectedFieldsListFromIntent
@Throws(PluginAccessException::class)
fun addEntryAction(
actionDisplayText: String?,
actionIconResourceId: Int,
actionData: Bundle?
) {
addEntryFieldAction(null, null, actionDisplayText, actionIconResourceId, actionData)
}
@Throws(PluginAccessException::class)
fun addEntryFieldAction(
actionId: String?,
fieldId: String?,
actionDisplayText: String?,
actionIconResourceId: Int,
actionData: Bundle?
) {
val i = Intent(Strings.ACTION_ADD_ENTRY_ACTION)
val scope = ArrayList<String?>()
scope.add(Strings.SCOPE_CURRENT_ENTRY)
i.putExtra(
Strings.EXTRA_ACCESS_TOKEN, AccessManager.getAccessToken(
context, hostPackage!!, scope
)
)
i.setPackage(hostPackage)
i.putExtra(Strings.EXTRA_SENDER, context.packageName)
i.putExtra(Strings.EXTRA_ACTION_DATA, actionData)
i.putExtra(Strings.EXTRA_ACTION_DISPLAY_TEXT, actionDisplayText)
i.putExtra(Strings.EXTRA_ACTION_ICON_RES_ID, actionIconResourceId)
i.putExtra(Strings.EXTRA_ENTRY_ID, entryId)
i.putExtra(Strings.EXTRA_FIELD_ID, fieldId)
i.putExtra(Strings.EXTRA_ACTION_ID, actionId)
context.sendBroadcast(i)
}
}
private inner class DatabaseAction(context: Context, intent: Intent) :
PluginActionBase(context, intent) {
val fileDisplayName: String?
get() = _intent.getStringExtra(Strings.EXTRA_DATABASE_FILE_DISPLAYNAME)
val filePath: String?
get() = _intent.getStringExtra(Strings.EXTRA_DATABASE_FILEPATH)
val action: String?
get() = _intent.action
}
//EntryOutputModified is very similar to OpenEntry because it receives the same
//data (+ the field id which was modified)
private inner class EntryOutputModifiedAction(context: Context, intent: Intent) :
OpenEntryAction(context, intent) {
val modifiedFieldId: String?
get() = _intent.getStringExtra(Strings.EXTRA_FIELD_ID)
}
override fun onReceive(ctx: Context, intent: Intent) {
val action = intent.action
Log.d(
"KP2A.pluginsdk",
"received broadcast in PluginActionBroadcastReceiver with action=$action"
)
if (action == null) return
if (action == Strings.ACTION_OPEN_ENTRY) {
openEntry(OpenEntryAction(ctx, intent))
} else if (action == Strings.ACTION_CLOSE_ENTRY_VIEW) {
closeEntryView(CloseEntryViewAction(ctx, intent))
} else if (action == Strings.ACTION_ENTRY_ACTION_SELECTED) {
actionSelected(ActionSelectedAction(ctx, intent))
} else if (action == Strings.ACTION_ENTRY_OUTPUT_MODIFIED) {
entryOutputModified(EntryOutputModifiedAction(ctx, intent))
} else if (action == Strings.ACTION_LOCK_DATABASE || action == Strings.ACTION_UNLOCK_DATABASE || action == Strings.ACTION_OPEN_DATABASE || action == Strings.ACTION_CLOSE_DATABASE) {
dbAction(DatabaseAction(ctx, intent))
} else {
//TODO handle unexpected action
}
}
private fun closeEntryView(closeEntryView: CloseEntryViewAction?) {}
private fun actionSelected(actionSelected: ActionSelectedAction?) {}
private fun openEntry(oe: OpenEntryAction?) {}
private fun entryOutputModified(eom: EntryOutputModifiedAction?) {}
private fun dbAction(db: DatabaseAction?) {}
}

View File

@ -0,0 +1,195 @@
package net.helcel.fidelity.pluginSDK
object Strings {
/**
* Plugin is notified about actions like open/close/update a database.
*/
const val SCOPE_DATABASE_ACTIONS = "keepass2android.SCOPE_DATABASE_ACTIONS"
/**
* Plugin is notified when an entry is opened.
*/
const val SCOPE_CURRENT_ENTRY = "keepass2android.SCOPE_CURRENT_ENTRY"
/**
* Plugin may query credentials for its own package
*/
const val SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE =
"keepass2android.SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE"
/**
* Plugin may query credentials for a deliberate package
*/
const val SCOPE_QUERY_CREDENTIALS = "keepass2android.SCOPE_QUERY_CREDENTIALS"
/**
* Extra key to transfer a (json serialized) list of scopes
*/
const val EXTRA_SCOPES = "keepass2android.EXTRA_SCOPES"
const val EXTRA_PLUGIN_PACKAGE = "keepass2android.EXTRA_PLUGIN_PACKAGE"
/**
* Extra key for sending the package name of the sender of a broadcast.
* Should be set in every broadcast.
*/
const val EXTRA_SENDER = "keepass2android.EXTRA_SENDER"
/**
* Extra key for sending a request token. The request token is passed from
* KP2A to the plugin. It's used in the authorization process.
*/
const val EXTRA_REQUEST_TOKEN = "keepass2android.EXTRA_REQUEST_TOKEN"
/**
* Action to start KP2A with an AppTask
*/
const val ACTION_START_WITH_TASK = "keepass2android.ACTION_START_WITH_TASK"
/**
* Action sent from KP2A to the plugin to indicate that the plugin should request
* access (sending it's scopes)
*/
const val ACTION_TRIGGER_REQUEST_ACCESS = "keepass2android.ACTION_TRIGGER_REQUEST_ACCESS"
/**
* Action sent from the plugin to KP2A including the scopes.
*/
const val ACTION_REQUEST_ACCESS = "keepass2android.ACTION_REQUEST_ACCESS"
/**
* Action sent from the KP2A to the plugin when the user grants access.
* Will contain an access token.
*/
const val ACTION_RECEIVE_ACCESS = "keepass2android.ACTION_RECEIVE_ACCESS"
/**
* Action sent from KP2A to the plugin to indicate that access is not or no longer valid.
*/
const val ACTION_REVOKE_ACCESS = "keepass2android.ACTION_REVOKE_ACCESS"
/**
* Action for startActivity(). Opens an activity in the Plugin Host to edit the plugin settings (i.e. enable it)
*/
const val ACTION_EDIT_PLUGIN_SETTINGS = "keepass2android.ACTION_EDIT_PLUGIN_SETTINGS"
/**
* Action sent from KP2A to the plugin to indicate that an entry was opened.
* The Intent contains the full entry data.
*/
const val ACTION_OPEN_ENTRY = "keepass2android.ACTION_OPEN_ENTRY"
/**
* Action sent from KP2A to the plugin to indicate that an entry output field was modified/added.
* The Intent contains the full new entry data.
*/
const val ACTION_ENTRY_OUTPUT_MODIFIED = "keepass2android.ACTION_ENTRY_OUTPUT_MODIFIED"
/**
* Action sent from KP2A to the plugin to indicate that an entry activity was closed.
*/
const val ACTION_CLOSE_ENTRY_VIEW = "keepass2android.ACTION_CLOSE_ENTRY_VIEW"
/**
* Extra key for a string containing the GUID of the entry.
*/
const val EXTRA_ENTRY_ID = "keepass2android.EXTRA_ENTRY_DATA"
/**
* Json serialized data of the PwEntry (C# class) representing the opened entry.
* currently not implemented.
*/
//const val EXTRA_ENTRY_DATA = "keepass2android.EXTRA_ENTRY_DATA";
/**
* Json serialized list of fields, transformed using the database context (i.e. placeholders are replaced already)
*/
const val EXTRA_ENTRY_OUTPUT_DATA = "keepass2android.EXTRA_ENTRY_OUTPUT_DATA"
/**
* Json serialized lisf of field keys, specifying which field of the EXTRA_ENTRY_OUTPUT_DATA is protected.
*/
const val EXTRA_PROTECTED_FIELDS_LIST = "keepass2android.EXTRA_PROTECTED_FIELDS_LIST"
/**
* Extra key for passing the access token (both ways)
*/
const val EXTRA_ACCESS_TOKEN = "keepass2android.EXTRA_ACCESS_TOKEN"
/**
* Action for an intent from the plugin to KP2A to add menu options regarding the currently open entry.
* Requires SCOPE_CURRENT_ENTRY.
*/
const val ACTION_ADD_ENTRY_ACTION = "keepass2android.ACTION_ADD_ENTRY_ACTION"
const val EXTRA_ACTION_DISPLAY_TEXT = "keepass2android.EXTRA_ACTION_DISPLAY_TEXT"
const val EXTRA_ACTION_ICON_RES_ID = "keepass2android.EXTRA_ACTION_ICON_RES_ID"
const val EXTRA_FIELD_ID = "keepass2android.EXTRA_FIELD_ID"
/**
* Used to pass an id for the action. Each actionId may occur only once per field, otherwise the previous
* action with same id is replaced by the new action.
*/
const val EXTRA_ACTION_ID = "keepass2android.EXTRA_ACTION_ID"
/** Extra for ACTION_ADD_ENTRY_ACTION and ACTION_ENTRY_ACTION_SELECTED to pass data specifying the action parameters.*/
const val EXTRA_ACTION_DATA = "keepass2android.EXTRA_ACTION_DATA"
/**
* Action for an intent from KP2A to the plugin when an action added with ACTION_ADD_ENTRY_ACTION was selected by the user.
*
*/
const val ACTION_ENTRY_ACTION_SELECTED = "keepass2android.ACTION_ENTRY_ACTION_SELECTED"
/**
* Extra key for the string which is used to query the credentials. This should be either a URL for
* a web login (google.com or a full URI) or something in the form "androidapp://com.my.package"
*/
const val EXTRA_QUERY_STRING = "keepass2android.EXTRA_QUERY_STRING"
/**
* Action when plugin wants to query credentials for its own package
*/
const val ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE =
"keepass2android.ACTION_QUERY_CREDENTIALS_FOR_OWN_PACKAGE"
/**
* Action when plugin wants to query credentials for a deliberate package
* The query string is passed as intent data
*/
const val ACTION_QUERY_CREDENTIALS = "keepass2android.ACTION_QUERY_CREDENTIALS"
/**
* Action for an intent from the plugin to KP2A to set (i.e. add or update) a field in the entry.
* May be used to update existing or add new fields at any time while the entry is opened.
*/
const val ACTION_SET_ENTRY_FIELD = "keepass2android.ACTION_SET_ENTRY_FIELD"
/** Actions for an intent from KP2A to the plugin to inform that a database was opened, closed, quicklocked or quickunlocked.*/
const val ACTION_OPEN_DATABASE = "keepass2android.ACTION_OPEN_DATABASE"
const val ACTION_CLOSE_DATABASE = "keepass2android.ACTION_CLOSE_DATABASE"
const val ACTION_LOCK_DATABASE = "keepass2android.ACTION_LOCK_DATABASE"
const val ACTION_UNLOCK_DATABASE = "keepass2android.ACTION_UNLOCK_DATABASE"
/** Extra for ACTION_OPEN_DATABASE and ACTION_CLOSE_DATABASE containing a filepath which is used
* by KP2A internally to identify the file. Use only where necessary, might contain credentials
* for accessing the file (on remote storage).*/
const val EXTRA_DATABASE_FILEPATH = "keepass2android.EXTRA_DATABASE_FILEPATH"
/** Extra for ACTION_OPEN_DATABASE and ACTION_CLOSE_DATABASE containing a filepath which can be
* displayed to the user.*/
const val EXTRA_DATABASE_FILE_DISPLAYNAME = "keepass2android.EXTRA_DATABASE_FILE_DISPLAYNAME"
const val EXTRA_FIELD_VALUE = "keepass2android.EXTRA_FIELD_VALUE"
const val EXTRA_FIELD_PROTECTED = "keepass2android.EXTRA_FIELD_PROTECTED"
const val PREFIX_STRING = "STRING_"
const val PREFIX_BINARY = "BINARY_"
}

View File

@ -0,0 +1,77 @@
package net.helcel.fidelity.tools
import android.content.ContentValues
import android.util.Log
import androidx.annotation.OptIn
import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import com.google.mlkit.vision.barcode.BarcodeScanner
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.common.InputImage
import net.helcel.fidelity.tools.BarcodeFormatConverter.formatToString
import java.util.concurrent.Executors
@OptIn(ExperimentalGetImage::class)
object BarcodeScanner {
private fun processImageProxy(
barcodeScanner: BarcodeScanner,
imageProxy: ImageProxy,
cb: (String?, String?) -> Unit
) {
imageProxy.image?.let { image ->
val inputImage =
InputImage.fromMediaImage(
image,
imageProxy.imageInfo.rotationDegrees
)
barcodeScanner.process(inputImage)
.addOnSuccessListener { barcodeList ->
println(barcodeList.map { e -> e.displayValue })
println(barcodeList.map { e -> e.format })
val barcode =
barcodeList.getOrNull(0)
if (barcode != null)
cb(barcode.displayValue, formatToString(barcode.format))
}
.addOnFailureListener {
Log.e(ContentValues.TAG, it.message.orEmpty())
}.addOnCompleteListener {
imageProxy.image?.close()
imageProxy.close()
}
}
}
fun getAnalysisUseCase(cb: (String?, String?) -> Unit): ImageAnalysis {
val options = BarcodeScannerOptions.Builder().setBarcodeFormats(
Barcode.FORMAT_CODE_128,
Barcode.FORMAT_CODE_39,
Barcode.FORMAT_CODE_93,
Barcode.FORMAT_EAN_8,
Barcode.FORMAT_EAN_13,
Barcode.FORMAT_QR_CODE,
Barcode.FORMAT_UPC_A,
Barcode.FORMAT_UPC_E,
Barcode.FORMAT_PDF417
).build()
val scanner = BarcodeScanning.getClient(options)
val analysisUseCase = ImageAnalysis.Builder()
.build()
analysisUseCase.setAnalyzer(
Executors.newSingleThreadExecutor()
) { imageProxy ->
processImageProxy(scanner, imageProxy, cb)
}
return analysisUseCase
}
}

View File

@ -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")
}
}
}

View File

@ -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
}
}
}

View File

@ -0,0 +1,45 @@
package net.helcel.fidelity.tools
import android.content.SharedPreferences
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
object CacheManager {
const val PREF_NAME = "FIDELITY"
private const val ENTRY_KEY = "FIDELITY"
private var data: ArrayList<Triple<String?, String?, String?>> = ArrayList()
private var pref: SharedPreferences? = null
fun addFidelity(item: Triple<String?, String?, String?>) {
val exists = data.find { it.first == item.first }
if (exists != null)
data.remove(exists)
data.add(0, item)
saveFidelity()
}
private fun saveFidelity() {
val editor = pref?.edit()
val gson = Gson()
val json = gson.toJson(data)
editor?.putString(ENTRY_KEY, json)
editor?.apply()
}
fun loadFidelity(pref: SharedPreferences) {
this.pref = pref
val gson = Gson()
val json = pref.getString(ENTRY_KEY, null)
val type = object : TypeToken<List<Triple<String, String, Int>>>() {}.type
data = gson.fromJson(json, type) ?: ArrayList()
}
fun getFidelity(): ArrayList<Triple<String?, String?, String?>> {
return data
}
}

View File

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

View File

@ -0,0 +1,103 @@
package net.helcel.fidelity.tools
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment
import net.helcel.fidelity.pluginSDK.KeepassDefs
import net.helcel.fidelity.pluginSDK.Kp2aControl
object KeepassWrapper {
private const val CODE_FIELD: String = "FidelityCode"
private const val FORMAT_FIELD: String = "FidelityFormat"
private const val PROTECT_CODE_FIELD: String = "FidelityProtectedCode"
fun entryCreate(
fragment: Fragment,
title: String,
code: String,
format: String,
protectCode: Boolean,
): Pair<HashMap<String?, String?>, ArrayList<String?>> {
val fields = HashMap<String?, String?>()
val protected = ArrayList<String?>()
fields[KeepassDefs.TitleField] = title
fields[KeepassDefs.UrlField] =
"androidapp://" + fragment.requireActivity().packageName
fields[CODE_FIELD] = code
fields[FORMAT_FIELD] = format
fields[PROTECT_CODE_FIELD] = protectCode.toString()
if (protectCode) {
protected.add(CODE_FIELD)
}
return Pair(fields, protected)
}
fun resultLauncherAdd(
fragment: Fragment,
callback: (HashMap<String, String>) -> Unit
): ActivityResultLauncher<Intent> {
return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val data: Intent? = result.data
val credentials = Kp2aControl.getEntryFieldsFromIntent(
data!!
)
println(credentials)
callback(credentials)
}
}
}
fun resultLauncherQuery(
fragment: Fragment,
callback: (HashMap<String, String>) -> Unit
): ActivityResultLauncher<Intent> {
return fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val data: Intent? = result.data
val credentials = Kp2aControl.getEntryFieldsFromIntent(
data!!
)
println(credentials)
callback(credentials)
}
}
}
fun entryExtract(map: HashMap<String, String>): Triple<String?, String?, String?> {
return Triple(
map[KeepassDefs.TitleField],
map[CODE_FIELD],
map[FORMAT_FIELD]
)
}
fun bundleCreate(title: String?, code: String?, fmt: String?): Bundle {
val data = Bundle()
data.putString("title", title)
data.putString("code", code)
data.putString("fmt", fmt)
return data
}
fun bundleExtract(data: Bundle?): Triple<String?, String?, String?> {
return Triple(
data?.getString("title"),
data?.getString("code"),
data?.getString("fmt")
)
}
fun isProtected(map: HashMap<String, String>): Boolean {
return map[PROTECT_CODE_FIELD].toBoolean()
}
}

View File

@ -0,0 +1,120 @@
<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="M9,21V52"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M12,21V52"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M20,21V50"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M28,21V50"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M15,50V21H17V50H15Z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M23,50V21H25V50H23Z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M31,50V21H32V50H31Z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M46,21V50"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M49,21V50"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M57,21V50"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M41,50V21H43V50H41Z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M52,50V21H54V50H52Z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M60,21V52"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M63,21V52"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M35,21V52"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M38,21V52"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
</group>
</vector>

View File

@ -0,0 +1,35 @@
<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="M46.5,56l-10,-11.151l-10,11.151l0,-45.042l20,0z"
android:strokeWidth="2"
android:strokeColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#D22F27"
android:pathData="M41.864,12.03l0,37.854l4.523,5.044l0,-42.898z"
android:strokeColor="#00000000" />
<path
android:fillColor="#00000000"
android:pathData="M46.5,56l-10,-11.151l-10,11.151l0,-45.042l20,0z"
android:strokeWidth="2"
android:strokeColor="#000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M46.5,56l-10,-11.151l-10,11.151l0,-45.042l20,0z"
android:strokeWidth="2"
android:strokeColor="#000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</group>
</vector>

View File

@ -0,0 +1,50 @@
<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="m15.27,22.918c-1.737,0 -3.144,1.371 -3.144,3.062v23.221c0,1.691 1.408,3.062 3.144,3.062h41.661c1.737,0 3.144,-1.371 3.144,-3.062v-23.221c0,-1.691 -1.408,-3.062 -3.144,-3.062 0,0 -41.661,0 -41.661,0Z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M47.152,33.197L59.618,33.197"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M12.582,33.197L20.629,33.197"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M33.852,37.591m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M33.852,37.591m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="m27.831,19.12v-1.078c0,-0.564 -0.469,-1.021 -1.048,-1.021h-7.677c-0.579,0 -1.048,0.457 -1.048,1.021v1.078"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
</group>
</vector>

View File

@ -0,0 +1,15 @@
<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="#92D3F5"
android:pathData="M59.959,52.794H12.041c-0.552,0 -1,-0.448 -1,-1v-29.547c0,-0.552 0.448,-1 1,-1h47.918c0.552,0 1,0.448 1,1v29.547C60.959,52.347 60.511,52.794 59.959,52.794z"
android:strokeWidth="2"
android:strokeColor="#000000" />
</group>
</vector>

View File

@ -0,0 +1,15 @@
<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="M31,31l0,-18l10,0l0,18l18,0l0,10l-18,0l0,18l-10,0l0,-18l-18,0l0,-10z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
</group>
</vector>

View File

@ -0,0 +1,44 @@
<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="M18.63,56.82l9.197,-3.526l25.993,-25.993l-9.9,-9.899l-25.993,25.993l-3.538,9.209z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M47.335,13.987l3.503,-3.503l9.9,9.899l-3.474,3.474"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M18.556,42.766L28.456,52.666"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M14.398,52.582l-2.491,6.733l6.748,-2.506"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M36.91,25.007L46.423,34.52"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
</group>
</vector>

View File

@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="72dp"
android:height="72dp"
android:viewportWidth="52"
android:viewportHeight="52">
<group
android:translateX="-10"
android:translateY="-10">
<path
android:fillColor="#EA5A47"
android:pathData="M60.7,26.2c0,-7.2 -5.9,-13.1 -13.1,-13.1c-5,0 -9.3,2.8 -11.5,6.9c-2.2,-4.1 -6.6,-6.9 -11.5,-6.9c-7.2,0 -13.1,5.9 -13.1,13.1c0,3.1 1.1,6 2.9,8.2l0,0l21.8,27l21.8,-27l0,0C59.6,32.2 60.7,29.4 60.7,26.2z" />
<path
android:fillColor="#00000000"
android:pathData="M60.7,26.2c0,-7.2 -5.9,-13.1 -13.1,-13.1c-5,0 -9.3,2.8 -11.5,6.9c-2.2,-4.1 -6.6,-6.9 -11.5,-6.9c-7.2,0 -13.1,5.9 -13.1,13.1c0,3.1 1.1,6 2.9,8.2l0,0l21.8,27l21.8,-27l0,0C59.6,32.2 60.7,29.4 60.7,26.2z"
android:strokeWidth="2"
android:strokeColor="#000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</group>
</vector>

View File

@ -0,0 +1,31 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="72dp"
android:height="72dp"
android:viewportWidth="52"
android:viewportHeight="52">
<group
android:translateX="-10"
android:translateY="-10">
<path
android:fillColor="#00000000"
android:pathData="M30.735,34.656l-16.432,16.026l0,7.24l7.565,0l0,-4.637l5.125,0l0,-5.857l5.098,0l2.404,-2.404l0,-4.358l2.015,0"
android:strokeWidth="2"
android:strokeColor="#000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M48.52,23.998m-3.952,0a3.952,3.952 0,1 1,7.904 0a3.952,3.952 0,1 1,-7.904 0"
android:strokeWidth="2"
android:strokeColor="#000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#00000000"
android:pathData="M34.226,31.178c-1.43,-4.238 -0.347,-9.221 3.18,-12.695c4.845,-4.772 12.465,-4.889 17.022,-0.263s4.322,12.244 -0.522,17.016c-3.917,3.858 -9.648,4.674 -14.108,2.4"
android:strokeWidth="2"
android:strokeColor="#000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</group>
</vector>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/unlocked" android:state_checked="false" />
<item android:drawable="@drawable/locked_fill" android:state_checked="true" />
</selector>

View File

@ -0,0 +1,31 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="72dp"
android:height="72dp"
android:viewportWidth="52"
android:viewportHeight="52">
<group
android:translateX="-10"
android:translateY="-10">
<path
android:pathData="M53,32.25l1.875,0l0,26.875l-38,0l0,-26.875l1.875,0z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M21.375,28.915c0,-8.379 6.415,-16.274 14.318,-16.523c7.97,-0.251 15.41,7.285 14.742,16.523"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M25.548,28.915c0,-6.335 4.576,-12.305 10.212,-12.493c5.684,-0.19 10.991,5.508 10.514,12.493"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
</group>
</vector>

View File

@ -0,0 +1,46 @@
<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="M21.375,31.175c-0.35,-8.771 6.449,-18.539 14.387,-18.779c8.005,-0.242 16.541,10.97 14.333,19.052h-4.039c0,0 1.562,-7.922 -2.216,-11.253c-1.849,-1.631 -5.256,-4.771 -8.64,-4.292c-2.283,0.323 -6.868,3.452 -7.927,5.421c-2.064,3.837 -1.725,9.817 -1.725,9.817L21.375,31.175z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#D0CFCE"
android:strokeColor="#D0CFCE"
android:strokeLineCap="round"/>
<path
android:pathData="M53,32.297l1.875,0l0,26.875l-38,0l0,-26.875l1.875,0z"
android:fillColor="#FCEA2B"
android:strokeColor="#00000000"/>
<path
android:pathData="M54.43,32.493l-18.77,26.501l19.307,0z"
android:fillColor="#F1B31C"
android:strokeColor="#00000000"/>
<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>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:width="128dp"
android:height="128dp"
android:gravity="center"
android:drawable="@drawable/card" />
<item
android:width="64dp"
android:height="64dp"
android:drawable="@drawable/barcode"
android:gravity="center"
android:right="32dp" />
<item
android:width="52dp"
android:height="52dp"
android:drawable="@drawable/bookmark"
android:gravity="center"
android:left="72dp"
android:bottom="20dp" />
</layer-list>

View File

@ -0,0 +1,15 @@
<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="M13,31h46v10h-46z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
</group>
</vector>

View File

@ -0,0 +1,36 @@
<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="M11.136,11h50v50h-50z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M56.136,60l0,-24.838l-40,0l0,24.838"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M16.136,12l0,17.607l30.913,0l0,-17.607"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M19.956,38.96h32.031v5.771h-32.031z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
</group>
</vector>

View File

@ -0,0 +1,28 @@
<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="M36.097,37.814a14.637,14.637 77.541,1 0,15.747 -24.678a14.637,14.637 77.541,1 0,-15.747 24.678z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M37.964,34.887a11.166,11.166 122.551,1 0,12.012 -18.825a11.166,11.166 122.551,1 0,-12.012 18.825z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
<path
android:pathData="M31.31,39.966l4.406,2.811L25.22,59.227c-0.75,1.175 -2.344,1.499 -3.561,0.722l0,0c-1.217,-0.776 -1.595,-2.358 -0.845,-3.533L31.31,39.966z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/></group>
</vector>

View File

@ -0,0 +1,24 @@
<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="M46.274,28.915c0.477,-6.985 -4.83,-12.683 -10.514,-12.493c-4.673,0.156 -8.616,4.285 -9.828,9.301l-4.05,-0.903c1.661,-6.692 7.219,-12.221 13.812,-12.428c7.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"/>
</group>
</vector>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<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"
android:orientation="vertical"
tools:context=".activity.MainActivity">
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="MergeRootFrame" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,106 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".activity.fragment.CreateEntry">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
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"
android:hint="@string/title">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editTextTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/codeInputLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/code"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/checkboxProtected"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editTextCode"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/checkboxProtected"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:button="@drawable/lock_checkbox"
android:scaleX="0.40"
android:scaleY="0.40"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/codeInputLayout"
app:layout_constraintTop_toTopOf="parent" />
</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"
android:layout_marginTop="16dp"
android:hint="@string/format"
android:labelFor="@id/edit_text_format">
<AutoCompleteTextView
android:id="@+id/edit_text_format"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
<ImageView
android:id="@+id/imageViewPreview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:contentDescription="@string/barcode_preview"
android:scaleType="fitCenter" />
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btnSave"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_margin="24dp"
android:contentDescription="@string/save"
app:fabCustomSize="46dp"
app:maxImageSize="32dp"
app:srcCompat="@drawable/save" />
</RelativeLayout>

View File

@ -0,0 +1,83 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".activity.fragment.Launcher">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/fidelityList"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btnQuery"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_gravity="center"
android:layout_margin="24dp"
android:contentDescription="@string/query"
app:fabCustomSize="46dp"
app:maxImageSize="32dp"
app:srcCompat="@drawable/search" />
<LinearLayout
android:id="@+id/expandedMenuContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_margin="16dp"
android:background="@android:color/transparent"
android:orientation="vertical"
tools:ignore="RelativeOverlap">
<LinearLayout
android:id="@+id/menuAdd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btnScan"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:contentDescription="@string/scan"
app:fabCustomSize="46dp"
app:maxImageSize="32dp"
app:srcCompat="@drawable/camera" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btnManual"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:contentDescription="@string/manual"
app:fabCustomSize="46dp"
app:maxImageSize="32dp"
app:srcCompat="@drawable/edit" />
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btnAdd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="8dp"
android:contentDescription="@string/expand"
app:fabCustomSize="46dp"
app:maxImageSize="32dp" />
</LinearLayout>
</RelativeLayout>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".activity.fragment.Scanner">
<androidx.camera.view.PreviewView
android:id="@+id/cameraView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/bottomText"
android:layout_width="match_parent"
android:layout_height="64dp"
android:layout_alignParentBottom="true"
android:background="#ffffff"
android:textSize="24sp" />
</RelativeLayout>

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
tools:context=".activity.fragment.ViewEntry">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:hint="@string/title"
android:textAlignment="center"
android:textSize="42sp"
app:layout_constraintBottom_toTopOf="@id/imageViewPreview"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0" />
<ImageView
android:id="@+id/imageViewPreview"
android:layout_width="match_parent"
android:layout_height="0dp"
android:contentDescription="@string/barcode_preview"
android:scaleType="fitCenter"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/textViewFeelings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="15dp"
android:text=""
android:textSize="18sp"
android:textStyle="bold" />

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp"
app:cardMaxElevation="4dp"
app:cardPreventCornerOverlap="false"
app:cardUseCompatPadding="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textStyle="bold" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -0,0 +1,3 @@
<resources>
</resources>

View File

@ -0,0 +1,29 @@
<?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="app_name">Keepass Fidelity</string>
<string name="barcode_preview">barcode preview</string>
<string name="expand">Expand</string>
<string name="manual">Manual</string>
<string name="scan">Scan</string>
<string name="query">Query</string>
<string name="title">Title</string>
<string name="code">Code</string>
<string name="format">Format</string>
<string name="save">Save</string>
<string-array name="format_array">
<item>CODE_128</item>
<item>CODE_39</item>
<item>CODE_93</item>
<item>EAN_8</item>
<item>EAN_13</item>
<item>CODE_QR</item>
<item>UPC_A</item>
<item>UPC_E</item>
<item>PDF_417</item>
</string-array>
</resources>

View File

@ -0,0 +1,3 @@
<resources>
</resources>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Fidelity" parent="Theme.Material3.DayNight.NoActionBar">
<item name="colorPrimary">?attr/colorAccent</item>
</style>
</resources>

7
build.gradle Normal file
View File

@ -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
}

24
gradle.properties Normal file
View File

@ -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

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -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

164
gradlew vendored Executable file
View File

@ -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 "$@"

90
gradlew.bat vendored Normal file
View File

@ -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

18
settings.gradle Normal file
View File

@ -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'