Compare commits
1 Commits
main
...
89219b4836
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89219b4836 |
BIN
.github/images/apk.png
vendored
|
Before Width: | Height: | Size: 6.4 KiB |
BIN
.github/images/izzy.png
vendored
|
Before Width: | Height: | Size: 20 KiB |
65
.github/workflows/build.yml
vendored
@@ -1,65 +0,0 @@
|
|||||||
#file: noinspection SpellCheckingInspection
|
|
||||||
|
|
||||||
name: CI-Android APK
|
|
||||||
|
|
||||||
env:
|
|
||||||
main_project_module: app
|
|
||||||
playstore_name: KeepassFidelity
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ main ]
|
|
||||||
tags:
|
|
||||||
- '**'
|
|
||||||
pull_request:
|
|
||||||
branches: [ main ]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v5
|
|
||||||
with:
|
|
||||||
submodules: true
|
|
||||||
- name: set up secrets
|
|
||||||
run: |
|
|
||||||
echo "${{ secrets.RELEASE_KEYSTORE }}" > keystore.asc
|
|
||||||
echo "${{ secrets.RELEASE_KEY}}" > key.asc
|
|
||||||
gpg -d --passphrase "${{ secrets.RELEASE_KEYSTORE_PASSWORD }}" --batch keystore.asc > app/keystore.properties
|
|
||||||
gpg -d --passphrase "${{ secrets.RELEASE_KEYSTORE_PASSWORD }}" --batch key.asc > app/key.jks
|
|
||||||
|
|
||||||
- uses: gradle/wrapper-validation-action@v3
|
|
||||||
|
|
||||||
- name: create and checkout branch
|
|
||||||
if: github.event_name == 'pull_request'
|
|
||||||
env:
|
|
||||||
BRANCH: ${{ github.head_ref }}
|
|
||||||
run: git checkout -B "$BRANCH"
|
|
||||||
|
|
||||||
- name: set up JDK
|
|
||||||
uses: actions/setup-java@v5
|
|
||||||
with:
|
|
||||||
java-version: 17
|
|
||||||
distribution: "temurin"
|
|
||||||
cache: 'gradle'
|
|
||||||
|
|
||||||
- name: Build APK
|
|
||||||
run: ./gradlew assemble
|
|
||||||
|
|
||||||
# - name: Upload APK
|
|
||||||
# uses: actions/upload-artifact@v4
|
|
||||||
# with:
|
|
||||||
# name: app.apk
|
|
||||||
# path: app/build/outputs/apk/release/app-release.apk
|
|
||||||
|
|
||||||
- name: Release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
with:
|
|
||||||
files: |
|
|
||||||
app/build/outputs/apk/release/app-release.apk
|
|
||||||
4
.gitignore
vendored
@@ -7,11 +7,7 @@ local.properties/
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
build/
|
build/
|
||||||
app/build/
|
app/build/
|
||||||
app/debug/
|
|
||||||
app/release/
|
|
||||||
captures/
|
captures/
|
||||||
.externalNativeBuild
|
.externalNativeBuild
|
||||||
.cxx
|
.cxx
|
||||||
local.properties
|
local.properties
|
||||||
keystore.properties
|
|
||||||
key.jks
|
|
||||||
|
|||||||
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
|||||||
[submodule "external/KeePassDX"]
|
|
||||||
path = external/KeePassDX
|
|
||||||
url = https://github.com/Kunzisoft/KeePassDX.git
|
|
||||||
24
LICENSE
@@ -1,24 +0,0 @@
|
|||||||
This is free and unencumbered software released into the public domain.
|
|
||||||
|
|
||||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
||||||
distribute this software, either in source code form or as a compiled
|
|
||||||
binary, for any purpose, commercial or non-commercial, and by any
|
|
||||||
means.
|
|
||||||
|
|
||||||
In jurisdictions that recognize copyright laws, the author or authors
|
|
||||||
of this software dedicate any and all copyright interest in the
|
|
||||||
software to the public domain. We make this dedication for the benefit
|
|
||||||
of the public at large and to the detriment of our heirs and
|
|
||||||
successors. We intend this dedication to be an overt act of
|
|
||||||
relinquishment in perpetuity of all present and future rights to this
|
|
||||||
software under copyright law.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
||||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
||||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
||||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
||||||
OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
|
|
||||||
For more information, please refer to <https://unlicense.org>
|
|
||||||
73
README.md
@@ -1,73 +0,0 @@
|
|||||||
<!--suppress ALL -->
|
|
||||||
<div align="center">
|
|
||||||
<h1>Keepass Fidelity</h1>
|
|
||||||
<img width="100px" src="./metadata/en-US/images/icon.png" alt="Logo">
|
|
||||||
|
|
||||||
<p>A minimalist fidelity/loyalty card plugin</p>
|
|
||||||
|
|
||||||
<img src="https://forthebadge.com/images/badges/built-for-android.svg" alt="Built for Android">
|
|
||||||
<img src="https://forthebadge.com/images/badges/built-with-love.svg" alt="Built with love">
|
|
||||||
<br>
|
|
||||||
<a href="https://github.com/choelzl/keepass-fidelity/actions/workflows/build.yml">
|
|
||||||
<img src="https://github.com/choelzl/keepass-fidelity/actions/workflows/build.yml/badge.svg?branch=main" alt="Build Status">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## 🌄 Screenshots
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td style="width: 33%; height: 100px;"><img src="./metadata/en-US/images/phoneScreenshots/launcher.jpg" alt="Launcher" style="width: 100%; height: 100%;"></td>
|
|
||||||
<td style="width: 33%; height: 100px;"><img src="./metadata/en-US/images/phoneScreenshots/view.jpg" alt="View" style="width: 100%; height: 100%;"></td>
|
|
||||||
<td style="width: 33%; height: 100px;"><img src="./metadata/en-US/images/phoneScreenshots/edit.jpg" alt="Edit" style="width: 100%; height: 100%;"></td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## ⭐ Features
|
|
||||||
|
|
||||||
- Search entries in [Keepass2Android](https://github.com/PhilippC/keepass2android/)
|
|
||||||
- Scan & Create entries
|
|
||||||
- Recently used history for fast access
|
|
||||||
- Protect entries from caching
|
|
||||||
- Minimalist design and features
|
|
||||||
- Supported Formats: CODE_39, CODE_93, CODE_128, EAN_8, EAN_13, UPC_A, UPC_E, CODE_QR, PDF_417, AZTEC, CODABAR, DATA_MATRIX, ITF
|
|
||||||
|
|
||||||
## 📳 Installation
|
|
||||||
|
|
||||||
<div style="display: flex; justify-content: center; align-items: center; flex-direction: row;">
|
|
||||||
<a href="https://apt.izzysoft.de/fdroid/index/apk/net.helcel.fidelity">
|
|
||||||
<img width="200" height="80" alt="Izzy Download" src=".github/images/izzy.png">
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/choelzl/keepass-fidelity/releases/latest">
|
|
||||||
<img width="200" height="84" alt="APK Download" src=".github/images/apk.png">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## ⚙️ Permissions
|
|
||||||
|
|
||||||
- `CAMERA`: necessary for importing barcodes from camera
|
|
||||||
- `READ_MEDIA_VISUAL_USER_SELECTED`: necessary for the importing barcode from images
|
|
||||||
|
|
||||||
## 📝 Contribute
|
|
||||||
|
|
||||||
Keepass-Fidelity is a user-driven project. We welcome any contribution, big or small.
|
|
||||||
|
|
||||||
- **🖥️ Development:** Fix bugs, implement features, or research issues. Open a PR for review.
|
|
||||||
- **🍥 Design:** Improve interfaces, including accessibility and usability.
|
|
||||||
- **📂 Issue Reporting:** Report bugs and edge cases with relevant info.
|
|
||||||
- **🌍 Localization:** Translate if it doesn't support your language.
|
|
||||||
|
|
||||||
## ✏️ Acknowledgements
|
|
||||||
|
|
||||||
Thanks to all contributors, the developers of our dependencies, and our users.
|
|
||||||
|
|
||||||
## 📝 License
|
|
||||||
|
|
||||||
```
|
|
||||||
Copyright (C) 2024 Helcel
|
|
||||||
|
|
||||||
Licensed under the Unlicense
|
|
||||||
For more information, please refer to <https://unlicense.org>
|
|
||||||
```
|
|
||||||
120
app/build.gradle
@@ -1,128 +1,54 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
id 'org.jetbrains.kotlin.android'
|
id 'org.jetbrains.kotlin.android'
|
||||||
id 'org.jetbrains.kotlin.plugin.serialization' version '2.2.21'
|
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.23'
|
||||||
id 'org.jetbrains.kotlin.plugin.compose' version '2.2.21'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'net.helcel.fidelity'
|
namespace 'net.helcel.fidelity'
|
||||||
compileSdk 36
|
compileSdk 34
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId 'net.helcel.fidelity'
|
applicationId 'net.helcel.fidelity'
|
||||||
versionName "1.0d"
|
|
||||||
buildConfigField("String", "APP_NAME", "\"Keepass Fidelity\"")
|
|
||||||
manifestPlaceholders["APP_NAME"] = "Keepass Fidelity"
|
|
||||||
minSdk 28
|
minSdk 28
|
||||||
targetSdk 36
|
targetSdk 34
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
signingConfigs {
|
|
||||||
create("release") {
|
|
||||||
try {
|
|
||||||
def keystorePropertiesFile = rootProject.file("app/keystore.properties")
|
|
||||||
def keystoreProperties = new Properties()
|
|
||||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
|
||||||
|
|
||||||
keyAlias keystoreProperties['keyAlias']
|
|
||||||
keyPassword keystoreProperties['keyPassword']
|
|
||||||
storeFile file(keystoreProperties['storeFile'])
|
|
||||||
storePassword keystoreProperties['storePassword']
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
println("File not found: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
|
||||||
debuggable true
|
|
||||||
}
|
|
||||||
release {
|
release {
|
||||||
minifyEnabled true
|
minifyEnabled false
|
||||||
shrinkResources false
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
signedRelease {
|
|
||||||
initWith(buildTypes.release)
|
|
||||||
matchingFallbacks = ['release']
|
|
||||||
signingConfig = signingConfigs.getByName("release")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
coreLibraryDesugaringEnabled true
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
sourceCompatibility JavaVersion.VERSION_21
|
}
|
||||||
targetCompatibility JavaVersion.VERSION_21
|
kotlinOptions {
|
||||||
encoding 'utf-8'
|
jvmTarget = '1.8'
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding true
|
viewBinding true
|
||||||
compose true
|
|
||||||
buildConfig true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependenciesInfo {
|
|
||||||
// Disables dependency metadata when building APKs.
|
|
||||||
includeInApk = false
|
|
||||||
// Disables dependency metadata when building Android App Bundles.
|
|
||||||
includeInBundle = false
|
|
||||||
}
|
|
||||||
composeOptions {
|
|
||||||
kotlinCompilerExtensionVersion = "2.2.20"
|
|
||||||
}
|
|
||||||
kotlin {
|
|
||||||
jvmToolchain(21)
|
|
||||||
}
|
|
||||||
|
|
||||||
lint {
|
|
||||||
disable 'UsingMaterialAndMaterial3Libraries'
|
|
||||||
disable 'PreviewAnnotationInFunctionWithParameters'
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'androidx.compose.ui:ui'
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
implementation 'androidx.compose.material3:material3:1.4.0'
|
implementation 'androidx.core:core-ktx:1.12.0'
|
||||||
implementation 'androidx.compose.material:material:1.9.4'
|
|
||||||
implementation 'androidx.compose.material:material-icons-extended:1.7.8'
|
|
||||||
implementation 'androidx.navigation:navigation-compose:2.9.5'
|
|
||||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||||
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation "androidx.biometric:biometric:1.2.0-alpha05"
|
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
|
||||||
implementation "androidx.security:security-crypto:1.1.0"
|
implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
|
||||||
implementation "androidx.datastore:datastore-preferences:1.1.7"
|
implementation 'androidx.camera:camera-camera2:1.3.2'
|
||||||
implementation "androidx.security:security-crypto:1.1.0"
|
implementation 'androidx.camera:camera-lifecycle:1.3.2'
|
||||||
|
implementation 'androidx.camera:camera-view:1.3.2'
|
||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.1.5'
|
implementation 'com.google.code.gson:gson:2.10.1'
|
||||||
|
implementation 'com.google.android.material:material:1.11.0'
|
||||||
implementation 'androidx.camera:camera-lifecycle:1.5.1'
|
|
||||||
implementation 'androidx.camera:camera-view:1.5.1'
|
|
||||||
runtimeOnly 'androidx.camera:camera-camera2:1.5.1'
|
|
||||||
|
|
||||||
implementation 'com.google.android.material:material:1.13.0'
|
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0'
|
|
||||||
implementation 'com.google.zxing:core:3.5.3'
|
implementation 'com.google.zxing:core:3.5.3'
|
||||||
|
implementation 'com.google.mlkit:barcode-scanning:17.2.0'
|
||||||
|
|
||||||
implementation project(":database")
|
}
|
||||||
implementation project(":crypto")
|
|
||||||
|
|
||||||
implementation platform('androidx.compose:compose-bom:2025.10.01')
|
|
||||||
implementation 'androidx.compose.ui:ui-tooling:1.9.4'
|
|
||||||
implementation 'androidx.compose.ui:ui-tooling-preview'
|
|
||||||
|
|
||||||
//Submodule
|
|
||||||
//noinspection NewerVersionAvailable
|
|
||||||
implementation 'joda-time:joda-time:2.14.0'
|
|
||||||
implementation 'org.joda:joda-convert:3.0.1'
|
|
||||||
|
|
||||||
}
|
|
||||||
7
app/proguard-rules.pro
vendored
@@ -1,7 +0,0 @@
|
|||||||
# Gson uses generic type information stored in a class file when working with
|
|
||||||
# fields. Proguard removes such information by default, keep it.
|
|
||||||
-keepattributes Signature
|
|
||||||
|
|
||||||
-keep class org.joda.convert.** { *; }
|
|
||||||
# Optional. For using GSON @Expose annotation
|
|
||||||
-keepattributes AnnotationDefault,RuntimeVisibleAnnotations
|
|
||||||
@@ -1,20 +1,49 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:versionCode="1"
|
||||||
|
android:versionName="1.0">
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.camera" />
|
<uses-feature android:name="android.hardware.camera" />
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:icon="@mipmap/ic_launcher_round"
|
android:icon="@drawable/logo"
|
||||||
android:label="${APP_NAME}"
|
android:label="@string/app_name"
|
||||||
android:supportsRtl="true">
|
android:supportsRtl="true">
|
||||||
<activity
|
<activity
|
||||||
android:name=".activity.MainActivity"
|
android:name=".activity.MainActivity"
|
||||||
android:exported="true">
|
android:exported="true"
|
||||||
|
android:theme="@style/Theme.Fidelity">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</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>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
Before Width: | Height: | Size: 11 KiB |
@@ -1,65 +0,0 @@
|
|||||||
package net.helcel.fidelity.activity
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.material.Colors
|
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
|
|
||||||
import androidx.compose.material3.darkColorScheme
|
|
||||||
import androidx.compose.material3.dynamicDarkColorScheme
|
|
||||||
import androidx.compose.material3.dynamicLightColorScheme
|
|
||||||
import androidx.compose.material3.lightColorScheme
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import net.helcel.fidelity.R
|
|
||||||
|
|
||||||
|
|
||||||
object ToastHelper{
|
|
||||||
fun show(context: Context, message: String, duration: Int = Toast.LENGTH_SHORT) {
|
|
||||||
Toast.makeText(context, message, duration).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SysTheme(
|
|
||||||
content: @Composable () -> Unit
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
val themeKey = prefs.getString(stringResource(R.string.key_theme), stringResource(R.string.system))
|
|
||||||
val darkTheme = when (themeKey) {
|
|
||||||
stringResource(R.string.system) -> isSystemInDarkTheme()
|
|
||||||
stringResource(R.string.light) -> false
|
|
||||||
stringResource(R.string.dark) -> true
|
|
||||||
else -> isSystemInDarkTheme()
|
|
||||||
}
|
|
||||||
val colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
if(darkTheme) dynamicDarkColorScheme(LocalContext.current ) else dynamicLightColorScheme(LocalContext.current )
|
|
||||||
} else {
|
|
||||||
if(darkTheme) darkColorScheme() else lightColorScheme()
|
|
||||||
}
|
|
||||||
val m2colors = Colors(
|
|
||||||
primary = colorScheme.primary,
|
|
||||||
primaryVariant = colorScheme.primaryContainer,
|
|
||||||
secondary = colorScheme.secondary,
|
|
||||||
background = colorScheme.background,
|
|
||||||
surface = colorScheme.surface,
|
|
||||||
onPrimary = colorScheme.onPrimary,
|
|
||||||
onSecondary = colorScheme.onSecondary,
|
|
||||||
onBackground = colorScheme.onBackground,
|
|
||||||
onSurface = colorScheme.onSurface,
|
|
||||||
secondaryVariant = colorScheme.secondary,
|
|
||||||
error = colorScheme.error,
|
|
||||||
onError = colorScheme.onError,
|
|
||||||
isLight = !darkTheme,
|
|
||||||
)
|
|
||||||
|
|
||||||
MaterialTheme(
|
|
||||||
colors = m2colors,
|
|
||||||
content = content
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,62 +1,48 @@
|
|||||||
package net.helcel.fidelity.activity
|
package net.helcel.fidelity.activity
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.content.Context
|
||||||
import android.content.pm.ActivityInfo
|
import android.content.SharedPreferences
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.addCallback
|
||||||
import androidx.activity.compose.setContent
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import net.helcel.fidelity.R
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import net.helcel.fidelity.activity.fragment.Launcher
|
||||||
import androidx.fragment.app.FragmentActivity
|
import net.helcel.fidelity.databinding.ActMainBinding
|
||||||
import androidx.navigation.compose.NavHost
|
import net.helcel.fidelity.tools.CacheManager
|
||||||
import androidx.navigation.compose.composable
|
|
||||||
import androidx.navigation.compose.rememberNavController
|
class MainActivity : AppCompatActivity() {
|
||||||
import net.helcel.fidelity.activity.fragment.CreateEntryScreen
|
|
||||||
import net.helcel.fidelity.activity.fragment.FileScanner
|
private lateinit var binding: ActMainBinding
|
||||||
import net.helcel.fidelity.activity.fragment.InitialScreen
|
|
||||||
import net.helcel.fidelity.activity.fragment.LauncherScreen
|
private lateinit var sharedPreferences: SharedPreferences
|
||||||
import net.helcel.fidelity.activity.fragment.ScannerScreen
|
|
||||||
import net.helcel.fidelity.activity.fragment.ViewEntryScreen
|
|
||||||
import net.helcel.fidelity.tools.FidelityRepository.entries
|
|
||||||
import net.helcel.fidelity.tools.FidelityRepository.loadEntries
|
|
||||||
import net.helcel.fidelity.tools.KeePassStore.hasCredentials
|
|
||||||
|
|
||||||
class MainActivity : FragmentActivity() {
|
|
||||||
|
|
||||||
@SuppressLint("SourceLockedOrientationActivity")
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
actionBar?.hide()
|
sharedPreferences =
|
||||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
this.getSharedPreferences(CacheManager.PREF_NAME, Context.MODE_PRIVATE)
|
||||||
loadEntries(this.baseContext)
|
CacheManager.loadFidelity(sharedPreferences)
|
||||||
|
|
||||||
setContent {
|
|
||||||
SysTheme {
|
binding = ActMainBinding.inflate(layoutInflater)
|
||||||
val navController = rememberNavController()
|
setContentView(binding.root)
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
BackHandler {
|
onBackPressedDispatcher.addCallback(this) {
|
||||||
if (!navController.popBackStack()) finish()
|
if (supportFragmentManager.backStackEntryCount > 0) {
|
||||||
}
|
supportFragmentManager.popBackStackImmediate()
|
||||||
LaunchedEffect(Unit) {
|
} else {
|
||||||
if(!hasCredentials(context)) navController.navigate("init")
|
finish()
|
||||||
}
|
|
||||||
NavHost(navController = navController, startDestination = "launcher") {
|
|
||||||
composable("exit") { finish() }
|
|
||||||
composable("launcher") { LauncherScreen(navController) }
|
|
||||||
composable("init"){ InitialScreen (navController)}
|
|
||||||
composable("scanCam") { ScannerScreen(navController) }
|
|
||||||
composable("scanFile") { FileScanner(navController) }
|
|
||||||
composable("edit"){ CreateEntryScreen(navController) }
|
|
||||||
composable("view/{entryId}") { e ->
|
|
||||||
val entry = entries.find {
|
|
||||||
it.uid == (e.arguments?.getString("entryId") ?: "")
|
|
||||||
}
|
|
||||||
if (entry == null) return@composable navController.navigate("launcher")
|
|
||||||
ViewEntryScreen(navController,entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (savedInstanceState == null)
|
||||||
|
loadLauncher()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadLauncher() {
|
||||||
|
supportFragmentManager.beginTransaction()
|
||||||
|
.add(R.id.container, Launcher())
|
||||||
|
.commit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,370 +1,143 @@
|
|||||||
package net.helcel.fidelity.activity.fragment
|
package net.helcel.fidelity.activity.fragment
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.content.ActivityNotFoundException
|
||||||
import androidx.compose.foundation.Image
|
import android.os.Bundle
|
||||||
import androidx.compose.foundation.background
|
import android.os.Handler
|
||||||
import androidx.compose.foundation.clickable
|
import android.os.Looper
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import android.view.LayoutInflater
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import android.view.View
|
||||||
import androidx.compose.foundation.layout.Box
|
import android.view.ViewGroup
|
||||||
import androidx.compose.foundation.layout.Column
|
import android.widget.ArrayAdapter
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.core.widget.addTextChangedListener
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.material.Button
|
|
||||||
import androidx.compose.material.Checkbox
|
|
||||||
import androidx.compose.material.CheckboxDefaults
|
|
||||||
import androidx.compose.material.CircularProgressIndicator
|
|
||||||
import androidx.compose.material.DropdownMenuItem
|
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
|
||||||
import androidx.compose.material.ExposedDropdownMenuBox
|
|
||||||
import androidx.compose.material.Icon
|
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.material.OutlinedTextField
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.material.TextFieldDefaults
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
|
||||||
import androidx.compose.material.icons.filled.Camera
|
|
||||||
import androidx.compose.material.icons.filled.FileOpen
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringArrayResource
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.navigation.NavHostController
|
|
||||||
import com.google.zxing.FormatException
|
import com.google.zxing.FormatException
|
||||||
import com.kunzisoft.keepass.database.element.Entry
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import net.helcel.fidelity.R
|
import net.helcel.fidelity.R
|
||||||
import net.helcel.fidelity.activity.ToastHelper
|
import net.helcel.fidelity.databinding.FragCreateEntryBinding
|
||||||
import net.helcel.fidelity.activity.fragment.CreateEntryEventHandler.onCameraScan
|
import net.helcel.fidelity.pluginSDK.Kp2aControl
|
||||||
import net.helcel.fidelity.activity.fragment.CreateEntryEventHandler.onFileScan
|
|
||||||
import net.helcel.fidelity.activity.fragment.CreateEntryEventHandler.onSubmit
|
|
||||||
import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onRefresh
|
|
||||||
import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onSave
|
|
||||||
import net.helcel.fidelity.tools.BarcodeGenerator.generateBarcode
|
import net.helcel.fidelity.tools.BarcodeGenerator.generateBarcode
|
||||||
import net.helcel.fidelity.tools.FidelityEntry
|
import net.helcel.fidelity.tools.CacheManager
|
||||||
import net.helcel.fidelity.tools.FidelityRepository
|
import net.helcel.fidelity.tools.ErrorToaster
|
||||||
import net.helcel.fidelity.tools.FidelityRepository.activeEntry
|
import net.helcel.fidelity.tools.KeepassWrapper
|
||||||
import net.helcel.fidelity.tools.FidelityRepository.addEntry
|
|
||||||
|
|
||||||
|
private const val DEBOUNCE_DELAY = 500L
|
||||||
|
|
||||||
@Preview
|
class CreateEntry : Fragment() {
|
||||||
@Composable
|
|
||||||
fun CreateEntryScreen(navController: NavHostController?) {
|
|
||||||
var entry by remember { activeEntry }
|
|
||||||
var errorTitle by remember { mutableStateOf("") }
|
|
||||||
var errorCode by remember { mutableStateOf("") }
|
|
||||||
var errorFormat by remember { mutableStateOf("") }
|
|
||||||
|
|
||||||
var barcodeBitmap by remember { mutableStateOf<Bitmap?>(null) }
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
var isValidBarcode by remember { mutableStateOf(false) }
|
private lateinit var binding: FragCreateEntryBinding
|
||||||
var showDialog by remember { mutableStateOf(false) }
|
|
||||||
var isLoading by remember { mutableStateOf(false) }
|
|
||||||
val ctx = LocalContext.current
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
LaunchedEffect(entry) {
|
private val resultLauncherAdd = KeepassWrapper.resultLauncherAdd(this) {
|
||||||
isValidBarcode = false
|
val r = KeepassWrapper.entryExtract(it)
|
||||||
delay(500)
|
if (!KeepassWrapper.isProtected(it)) {
|
||||||
if (entry.code.isEmpty()) return@LaunchedEffect
|
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 {
|
try {
|
||||||
val bmp = generateBarcode(entry.code, entry.format, 600)
|
val barcodeBitmap = generateBarcode(
|
||||||
barcodeBitmap = bmp
|
binding.editTextCode.text.toString(),
|
||||||
isValidBarcode = true
|
binding.editTextFormat.text.toString(),
|
||||||
errorCode = ""
|
600
|
||||||
} catch (_: FormatException) {
|
)
|
||||||
barcodeBitmap = null
|
binding.imageViewPreview.setImageBitmap(barcodeBitmap)
|
||||||
errorCode = "Invalid Format"
|
isValid = true
|
||||||
|
} catch (e: FormatException) {
|
||||||
|
binding.imageViewPreview.setImageBitmap(null)
|
||||||
|
binding.editTextCode.error = "Invalid format"
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
barcodeBitmap = null
|
binding.imageViewPreview.setImageBitmap(null)
|
||||||
errorCode = if (e.message == "com.google.zxing.FormatException") "Invalid Format"
|
binding.editTextCode.error = e.message
|
||||||
else e.message ?: "Invalid Argument"
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
barcodeBitmap = null
|
binding.imageViewPreview.setImageBitmap(null)
|
||||||
ToastHelper.show(ctx, e.message ?: e.toString())
|
println(e.javaClass)
|
||||||
|
println(e.message)
|
||||||
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showDialog) {
|
private fun isValid(): Boolean {
|
||||||
TreeSelectorDialog(
|
var valid = true
|
||||||
onDismiss = {
|
if (binding.editTextTitle.text!!.isEmpty()) {
|
||||||
showDialog = false
|
valid = false
|
||||||
if(it!=null){
|
binding.editTextTitle.error = "Title cannot be empty"
|
||||||
entry = entry.copy(uid = it.nodeId?.id.toString())
|
|
||||||
if(it is Entry){
|
|
||||||
entry = entry.copy(title = it.title)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val formats = stringArrayResource(R.array.format_array)
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(MaterialTheme.colors.background)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(16.dp, 32.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
|
||||||
)
|
|
||||||
{
|
|
||||||
OutlinedTextField(
|
|
||||||
value = entry.title,
|
|
||||||
enabled = entry.uid!=null,
|
|
||||||
onValueChange = {
|
|
||||||
entry = entry.copy(title = it)
|
|
||||||
errorTitle = ""
|
|
||||||
},
|
|
||||||
label = { Text(text = "Title") },
|
|
||||||
isError = errorTitle.isNotEmpty(),
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
singleLine = true,
|
|
||||||
colors = TextFieldDefaults.textFieldColors(
|
|
||||||
textColor = if(entry.uid!=null)MaterialTheme.colors.onBackground
|
|
||||||
else MaterialTheme.colors.secondary
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if (errorTitle.isNotEmpty()) {
|
|
||||||
Text(errorTitle, color = MaterialTheme.colors.error)
|
|
||||||
}
|
|
||||||
|
|
||||||
OutlinedTextField(
|
|
||||||
value = entry.code,
|
|
||||||
onValueChange = {
|
|
||||||
entry = entry.copy(code = it)
|
|
||||||
errorCode = ""
|
|
||||||
},
|
|
||||||
colors = TextFieldDefaults.textFieldColors(
|
|
||||||
textColor = MaterialTheme.colors.onBackground
|
|
||||||
),
|
|
||||||
label = { Text("Code") },
|
|
||||||
isError = errorCode.isNotEmpty(),
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
singleLine = true
|
|
||||||
)
|
|
||||||
if (errorCode.isNotEmpty()) {
|
|
||||||
Text(errorCode, color = MaterialTheme.colors.error)
|
|
||||||
}
|
|
||||||
|
|
||||||
FormatDropdown(
|
|
||||||
formats,
|
|
||||||
entry.format,
|
|
||||||
errorFormat.ifEmpty { null },
|
|
||||||
) {
|
|
||||||
entry = entry.copy(format = it)
|
|
||||||
errorFormat = ""
|
|
||||||
}
|
|
||||||
if (errorFormat.isNotEmpty()) {
|
|
||||||
Text(errorFormat, color = MaterialTheme.colors.error)
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Checkbox(
|
|
||||||
checked = entry.protected,
|
|
||||||
onCheckedChange = {
|
|
||||||
entry = entry.copy(protected = it)
|
|
||||||
},
|
|
||||||
colors = CheckboxDefaults.colors()
|
|
||||||
)
|
|
||||||
Text("Protected", color = MaterialTheme.colors.onBackground)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
Button(onClick = { onCameraScan(navController!!) }) {
|
|
||||||
Icon(Icons.Default.Camera, contentDescription = null)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Button(onClick = { onFileScan(navController!!) }) {
|
|
||||||
Icon(Icons.Default.FileOpen, contentDescription = null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (barcodeBitmap != null) {
|
|
||||||
Image(
|
|
||||||
bitmap = barcodeBitmap!!.asImageBitmap(),
|
|
||||||
contentDescription = "Barcode preview",
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(150.dp)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
if (binding.editTextCode.text!!.isEmpty()) {
|
||||||
Row(
|
valid = false
|
||||||
modifier = Modifier
|
binding.editTextCode.error = "Code cannot be empty"
|
||||||
.align(Alignment.BottomCenter)
|
|
||||||
.padding(48.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
onSubmitIfValid(
|
|
||||||
entry,
|
|
||||||
setErrors = { t, c, f ->
|
|
||||||
errorTitle = t
|
|
||||||
errorCode = c
|
|
||||||
errorFormat = f
|
|
||||||
},
|
|
||||||
isValidBarcode
|
|
||||||
) {
|
|
||||||
if (FidelityRepository.getRoot() == null) {
|
|
||||||
isLoading = true
|
|
||||||
scope.launch {
|
|
||||||
onRefresh(ctx, navController!!)
|
|
||||||
isLoading = false
|
|
||||||
if(entry.uid!=null){
|
|
||||||
addEntry(ctx,entry)
|
|
||||||
isLoading = true
|
|
||||||
onSave(ctx,navController)
|
|
||||||
isLoading = false
|
|
||||||
onSubmit(navController)
|
|
||||||
}else {
|
|
||||||
showDialog = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if(entry.uid!=null){
|
|
||||||
addEntry(ctx,entry)
|
|
||||||
isLoading = true
|
|
||||||
scope.launch {
|
|
||||||
onSave(ctx, navController!!)
|
|
||||||
isLoading = false
|
|
||||||
onSubmit(navController)
|
|
||||||
}
|
|
||||||
}else {
|
|
||||||
showDialog = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
enabled = isValidBarcode.and(entry.uid==null || entry.title.isNotEmpty()),
|
|
||||||
) {
|
|
||||||
Text(if(entry.uid==null)"Select Entry" else "Save", style = MaterialTheme.typography.h6)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (binding.editTextFormat.text!!.isEmpty()) {
|
||||||
if (isLoading) {
|
valid = false
|
||||||
Box(
|
binding.editTextFormat.error = "Format cannot be empty"
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(MaterialTheme.colors.background.copy(alpha = 0.75f))
|
|
||||||
.clickable(
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
indication = null,
|
|
||||||
onClick = { }
|
|
||||||
),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
return valid
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
|
||||||
@Composable
|
|
||||||
fun FormatDropdown(
|
|
||||||
formats: Array<String>,
|
|
||||||
format: String,
|
|
||||||
errorFormat: String?,
|
|
||||||
onFormatChange: (String) -> Unit,
|
|
||||||
) {
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
ExposedDropdownMenuBox(
|
|
||||||
expanded = expanded,
|
|
||||||
onExpandedChange = { expanded = !expanded }
|
|
||||||
) {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = format,
|
|
||||||
onValueChange = {},
|
|
||||||
readOnly = true, // important for dropdown
|
|
||||||
label = { Text("Format", color=MaterialTheme.colors.onBackground) },
|
|
||||||
trailingIcon = {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.ArrowDropDown,
|
|
||||||
contentDescription = "Expand",
|
|
||||||
)
|
|
||||||
},
|
|
||||||
colors = TextFieldDefaults.textFieldColors(
|
|
||||||
textColor = MaterialTheme.colors.onBackground
|
|
||||||
),
|
|
||||||
isError = errorFormat != null,
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
|
|
||||||
ExposedDropdownMenu(
|
|
||||||
expanded = expanded,
|
|
||||||
onDismissRequest = { expanded = false }
|
|
||||||
) {
|
|
||||||
formats.forEach { option ->
|
|
||||||
DropdownMenuItem(
|
|
||||||
onClick = {
|
|
||||||
onFormatChange(option)
|
|
||||||
expanded = false
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(option)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private fun onSubmitIfValid(
|
|
||||||
entry: FidelityEntry,
|
|
||||||
setErrors: (String, String, String) -> Unit,
|
|
||||||
isValidBarcode: Boolean,
|
|
||||||
onValid: (FidelityEntry) -> Unit
|
|
||||||
) {
|
|
||||||
var tErr = ""
|
|
||||||
var cErr = ""
|
|
||||||
var fErr = ""
|
|
||||||
if (entry.uid!=null && entry.title.isBlank()) tErr = "Title cannot be empty"
|
|
||||||
if (entry.code.isBlank()) cErr = "Code cannot be empty"
|
|
||||||
if (entry.format.isBlank()) fErr = "Format cannot be empty"
|
|
||||||
|
|
||||||
setErrors(tErr, cErr, fErr)
|
|
||||||
|
|
||||||
if (tErr.isEmpty() && cErr.isEmpty() && fErr.isEmpty() && isValidBarcode) {
|
|
||||||
onValid(entry.copy())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object CreateEntryEventHandler {
|
|
||||||
fun onSubmit(navController: NavHostController){
|
|
||||||
navController.popBackStack()
|
|
||||||
activeEntry.value = activeEntry.value.copy(null,"","","",false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onFileScan(navController: NavHostController){
|
|
||||||
navController.navigate("scanFile")
|
private fun startViewEntry(title: String?, code: String?, fmt: String?) {
|
||||||
}
|
val viewEntryFragment = ViewEntry()
|
||||||
fun onCameraScan(navController: NavHostController){
|
viewEntryFragment.arguments = KeepassWrapper.bundleCreate(title, code, fmt)
|
||||||
navController.navigate("scanCam")
|
|
||||||
|
requireActivity().supportFragmentManager.beginTransaction()
|
||||||
|
.replace(R.id.container, viewEntryFragment).commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,345 +1,127 @@
|
|||||||
package net.helcel.fidelity.activity.fragment
|
package net.helcel.fidelity.activity.fragment
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.ActivityNotFoundException
|
||||||
import androidx.compose.foundation.background
|
import android.os.Bundle
|
||||||
import androidx.compose.foundation.clickable
|
import android.view.LayoutInflater
|
||||||
import androidx.compose.foundation.combinedClickable
|
import android.view.View
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import android.view.ViewGroup
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import net.helcel.fidelity.R
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import net.helcel.fidelity.activity.adapter.FidelityListAdapter
|
||||||
import androidx.compose.foundation.layout.padding
|
import net.helcel.fidelity.databinding.FragLauncherBinding
|
||||||
import androidx.compose.foundation.layout.size
|
import net.helcel.fidelity.pluginSDK.Kp2aControl
|
||||||
import androidx.compose.foundation.layout.width
|
import net.helcel.fidelity.tools.CacheManager
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
import net.helcel.fidelity.tools.ErrorToaster
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
import net.helcel.fidelity.tools.KeepassWrapper
|
||||||
import androidx.compose.foundation.lazy.grid.items
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.DropdownMenu
|
|
||||||
import androidx.compose.material.DropdownMenuItem
|
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
|
||||||
import androidx.compose.material.FloatingActionButton
|
|
||||||
import androidx.compose.material.Icon
|
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Add
|
|
||||||
import androidx.compose.material.icons.filled.Edit
|
|
||||||
import androidx.compose.material.icons.filled.HideSource
|
|
||||||
import androidx.compose.material.icons.filled.PushPin
|
|
||||||
import androidx.compose.material.icons.filled.Search
|
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.CardDefaults
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.navigation.NavHostController
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onAdd
|
|
||||||
import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onEdit
|
|
||||||
import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onHide
|
|
||||||
import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onPin
|
|
||||||
import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onQuery
|
|
||||||
import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onRefresh
|
|
||||||
import net.helcel.fidelity.activity.fragment.LauncherEventHandlers.onView
|
|
||||||
import net.helcel.fidelity.tools.CredentialResult
|
|
||||||
import net.helcel.fidelity.tools.FidelityEntry
|
|
||||||
import net.helcel.fidelity.tools.FidelityRepository.activeEntry
|
|
||||||
import net.helcel.fidelity.tools.FidelityRepository.end
|
|
||||||
import net.helcel.fidelity.tools.FidelityRepository.entries
|
|
||||||
import net.helcel.fidelity.tools.FidelityRepository.genCredentials
|
|
||||||
import net.helcel.fidelity.tools.FidelityRepository.importDB
|
|
||||||
import net.helcel.fidelity.tools.FidelityRepository.start
|
|
||||||
import net.helcel.fidelity.tools.KeePassStore.loadCredentials
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
class Launcher : Fragment() {
|
||||||
@Composable
|
|
||||||
fun LauncherScreen(
|
private lateinit var binding: FragLauncherBinding
|
||||||
navController: NavHostController?,
|
private lateinit var fidelityListAdapter: FidelityListAdapter
|
||||||
) {
|
|
||||||
if(navController==null) return
|
private val resultLauncherQuery = KeepassWrapper.resultLauncherQuery(this) {
|
||||||
var isRefreshingState by remember { mutableStateOf(false) }
|
val r = KeepassWrapper.entryExtract(it)
|
||||||
var showHidden by remember { mutableStateOf(false) }
|
if (!KeepassWrapper.isProtected(it)) {
|
||||||
val context = LocalContext.current
|
CacheManager.addFidelity(r)
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val sortedEntries = remember(entries) {
|
|
||||||
derivedStateOf {
|
|
||||||
entries.filter{showHidden || !it.hidden}.sortedWith(
|
|
||||||
compareByDescending<FidelityEntry> { it.pinned }
|
|
||||||
.thenBy { it.hidden }
|
|
||||||
.thenByDescending { it.lastUse }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
startViewEntry(r.first, r.second, r.third)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
Box(modifier = Modifier
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
.fillMaxSize()
|
savedInstanceState: Bundle?
|
||||||
.background(MaterialTheme.colors.background)) {
|
): View {
|
||||||
|
binding = FragLauncherBinding.inflate(layoutInflater)
|
||||||
PullToRefreshBox(
|
binding.btnQuery.setOnClickListener { startGetFromKeepass() }
|
||||||
onRefresh = {
|
binding.btnAdd.setOnClickListener {
|
||||||
isRefreshingState = true
|
if (binding.menuAdd.visibility == View.GONE)
|
||||||
scope.launch {
|
showMenuAdd()
|
||||||
onRefresh(context, navController)
|
else
|
||||||
isRefreshingState = false
|
hideMenuAdd()
|
||||||
}
|
|
||||||
},
|
|
||||||
isRefreshing = isRefreshingState,
|
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
) {
|
|
||||||
LazyVerticalGrid(
|
|
||||||
columns = GridCells.Fixed(2),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(16.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
items(sortedEntries.value) { entry ->
|
|
||||||
FidelityRow(navController, entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
FloatingActionButton(
|
|
||||||
onClick = { onQuery() },
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.BottomCenter)
|
|
||||||
.padding(bottom = 16.dp),
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Search,
|
|
||||||
contentDescription = "Query",
|
|
||||||
modifier = Modifier.size(32.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
FloatingActionButton(
|
|
||||||
onClick = { onAdd(navController) }, modifier = Modifier
|
|
||||||
.align(Alignment.BottomEnd)
|
|
||||||
.padding(16.dp)
|
|
||||||
) {
|
|
||||||
Icon(Icons.Default.Add, contentDescription = "Add")
|
|
||||||
}
|
|
||||||
FloatingActionButton(
|
|
||||||
onClick = {
|
|
||||||
showHidden=!showHidden
|
|
||||||
}, modifier = Modifier
|
|
||||||
.align(Alignment.BottomStart)
|
|
||||||
.padding(16.dp).size(24.dp),
|
|
||||||
backgroundColor = if(showHidden) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary,
|
|
||||||
) {
|
|
||||||
Icon(Icons.Default.HideSource,
|
|
||||||
tint= if(showHidden) MaterialTheme.colors.background else MaterialTheme.colors.onSecondary,
|
|
||||||
contentDescription = "Show Hidden")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRefreshingState)
|
hideMenuAdd()
|
||||||
Box(
|
binding.btnScan.setOnClickListener {
|
||||||
modifier = Modifier
|
startScanner()
|
||||||
.fillMaxSize()
|
hideMenuAdd()
|
||||||
.background(MaterialTheme.colors.background.copy(alpha = 0.75f))
|
|
||||||
.clickable(
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
indication = null,
|
|
||||||
onClick = { }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
|
||||||
@Composable
|
|
||||||
fun FidelityRow(
|
|
||||||
navController: NavHostController,
|
|
||||||
e: FidelityEntry
|
|
||||||
) {
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxWidth()) {
|
|
||||||
Card(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(2.dp)
|
|
||||||
.combinedClickable(
|
|
||||||
onClick = { onView(navController, e) },
|
|
||||||
onLongClick = { expanded = true },
|
|
||||||
),
|
|
||||||
shape = RoundedCornerShape(8.dp),
|
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = MaterialTheme.colors.primary,
|
|
||||||
contentColor = MaterialTheme.colors.background
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
Box(modifier = Modifier.fillMaxSize().padding(2.dp)) {
|
|
||||||
Row(modifier = Modifier.padding(14.dp)) {
|
|
||||||
Text(
|
|
||||||
text = e.title,
|
|
||||||
style = MaterialTheme.typography.h6,
|
|
||||||
color = MaterialTheme.colors.onPrimary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Row(modifier = Modifier.align(Alignment.TopEnd)) {
|
|
||||||
if (e.hidden)
|
|
||||||
Icon(
|
|
||||||
Icons.Default.HideSource, contentDescription = null,
|
|
||||||
modifier = Modifier.size(16.dp),
|
|
||||||
tint = MaterialTheme.colors.onPrimary
|
|
||||||
)
|
|
||||||
if (e.hidden && e.pinned)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
if (e.pinned)
|
|
||||||
Icon(
|
|
||||||
Icons.Default.PushPin, contentDescription = null,
|
|
||||||
modifier = Modifier.size(16.dp),
|
|
||||||
tint = MaterialTheme.colors.onPrimary
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
DropdownMenu(
|
|
||||||
modifier = Modifier,
|
binding.btnManual.setOnClickListener {
|
||||||
expanded = expanded,
|
startCreateEntry()
|
||||||
onDismissRequest = { expanded = false }
|
hideMenuAdd()
|
||||||
) {
|
|
||||||
DropdownMenuItem(onClick = {
|
|
||||||
expanded = false
|
|
||||||
onEdit(navController, e)
|
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Edit,
|
|
||||||
contentDescription = "edit",
|
|
||||||
)
|
|
||||||
Spacer(modifier= Modifier.width(8.dp))
|
|
||||||
Text("Edit")
|
|
||||||
}
|
|
||||||
DropdownMenuItem(onClick = {
|
|
||||||
expanded = false
|
|
||||||
onPin(e)
|
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.PushPin,
|
|
||||||
contentDescription = "pin",
|
|
||||||
)
|
|
||||||
Spacer(modifier= Modifier.width(8.dp))
|
|
||||||
if(e.pinned) Text("Unpin")
|
|
||||||
else Text("Pin")
|
|
||||||
}
|
|
||||||
DropdownMenuItem(onClick = {
|
|
||||||
expanded = false
|
|
||||||
onHide(e)
|
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.HideSource,
|
|
||||||
contentDescription = "hide",
|
|
||||||
)
|
|
||||||
Spacer(modifier= Modifier.width(8.dp))
|
|
||||||
if(e.hidden) Text("Unhide")
|
|
||||||
else Text("Hide")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
binding.fidelityList.layoutManager =
|
||||||
|
LinearLayoutManager(requireContext())
|
||||||
|
fidelityListAdapter = FidelityListAdapter(CacheManager.getFidelity()) {
|
||||||
|
startViewEntry(it.first, it.second, it.third)
|
||||||
|
}
|
||||||
|
binding.fidelityList.adapter = fidelityListAdapter
|
||||||
|
|
||||||
object LauncherEventHandlers {
|
recyclerSlideHelper().attachToRecyclerView(binding.fidelityList)
|
||||||
fun onAdd(navController: NavHostController) {
|
return binding.root
|
||||||
navController.navigate("edit")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onQuery() {
|
private fun hideMenuAdd() {
|
||||||
//TODO
|
binding.btnAdd.setImageResource(R.drawable.cross)
|
||||||
}
|
binding.menuAdd.visibility = View.GONE
|
||||||
var CRED: CredentialResult.Success? = null
|
|
||||||
|
|
||||||
suspend fun onSave(context: Context, navController: NavHostController){
|
}
|
||||||
|
|
||||||
|
private fun showMenuAdd() {
|
||||||
|
binding.btnAdd.setImageResource(R.drawable.minus)
|
||||||
|
binding.menuAdd.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun startGetFromKeepass() {
|
||||||
try {
|
try {
|
||||||
if (CRED == null) {
|
this.resultLauncherQuery.launch(Kp2aControl.queryEntryIntentForOwnPackage)
|
||||||
val res = loadCredentials(context)
|
} catch (e: ActivityNotFoundException) {
|
||||||
when (res) {
|
ErrorToaster.noKP2AFound(requireActivity())
|
||||||
CredentialResult.AuthFailed, CredentialResult.NoData -> null
|
|
||||||
is CredentialResult.Success -> CRED = res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CRED!!
|
|
||||||
val cred = withContext(Dispatchers.IO) {
|
|
||||||
genCredentials(context, CRED!!)
|
|
||||||
}
|
|
||||||
if (withContext(Dispatchers.IO) {
|
|
||||||
end(context, CRED!!.db, cred)
|
|
||||||
})
|
|
||||||
throw Exception("Error in saving")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println(e.toString())
|
|
||||||
navController.navigate("init")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onRefresh(context: Context, navController: NavHostController) {
|
private fun startFragment(fragment: Fragment) {
|
||||||
try {
|
requireActivity().supportFragmentManager.beginTransaction()
|
||||||
if (CRED == null) {
|
.addToBackStack("Launcher")
|
||||||
val res = loadCredentials(context)
|
.replace(R.id.container, fragment).commit()
|
||||||
when (res) {
|
}
|
||||||
CredentialResult.AuthFailed, CredentialResult.NoData -> null
|
|
||||||
is CredentialResult.Success -> CRED = res
|
|
||||||
|
|
||||||
}
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun recyclerSlideHelper(): ItemTouchHelper {
|
||||||
|
return ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(
|
||||||
|
0, ItemTouchHelper.LEFT
|
||||||
|
) {
|
||||||
|
override fun onMove(
|
||||||
|
recyclerView: RecyclerView,
|
||||||
|
viewHolder: RecyclerView.ViewHolder,
|
||||||
|
target: RecyclerView.ViewHolder
|
||||||
|
): Boolean = false
|
||||||
|
|
||||||
|
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||||
|
val pos = viewHolder.adapterPosition
|
||||||
|
CacheManager.rmFidelity(pos)
|
||||||
|
fidelityListAdapter.notifyItemRemoved(pos)
|
||||||
}
|
}
|
||||||
CRED!!
|
})
|
||||||
val cred = withContext(Dispatchers.IO) {
|
|
||||||
genCredentials(context, CRED!!)
|
|
||||||
}
|
|
||||||
if (withContext(Dispatchers.IO) {
|
|
||||||
start(context, CRED!!.db, cred)
|
|
||||||
})
|
|
||||||
importDB(context)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println(e.toString())
|
|
||||||
navController.navigate("init")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onView(navController: NavHostController, entry: FidelityEntry) {
|
|
||||||
navController.navigate("view/${entry.uid}")
|
|
||||||
val index = entries.indexOfFirst { it.uid == entry.uid }
|
|
||||||
if (index != -1)
|
|
||||||
entries[index] = entry.copy(lastUse = System.currentTimeMillis().toInt())
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onPin(entry: FidelityEntry){
|
|
||||||
val index = entries.indexOfFirst { it.uid == entry.uid }
|
|
||||||
if (index != -1)
|
|
||||||
entries[index] = entry.copy(pinned = !entry.pinned)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onHide(entry: FidelityEntry){
|
|
||||||
val index = entries.indexOfFirst { it.uid == entry.uid }
|
|
||||||
if (index != -1)
|
|
||||||
entries[index] = entry.copy(hidden = !entry.hidden)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onEdit(navController: NavHostController, entry: FidelityEntry){
|
|
||||||
activeEntry.value = entry
|
|
||||||
navController.navigate("edit")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,224 +1,116 @@
|
|||||||
@file:Suppress("PreviewAnnotationInFunctionWithParameters",
|
|
||||||
"PreviewAnnotationInFunctionWithParameters"
|
|
||||||
)
|
|
||||||
|
|
||||||
package net.helcel.fidelity.activity.fragment
|
package net.helcel.fidelity.activity.fragment
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.graphics.BitmapFactory
|
import android.content.ContentValues
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.view.LayoutInflater
|
||||||
import androidx.activity.compose.BackHandler
|
import android.view.View
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import android.view.ViewGroup
|
||||||
import androidx.activity.result.PickVisualMediaRequest
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.camera.core.Camera
|
|
||||||
import androidx.camera.core.CameraSelector
|
import androidx.camera.core.CameraSelector
|
||||||
import androidx.camera.core.Preview
|
import androidx.camera.core.Preview
|
||||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||||
import androidx.camera.view.PreviewView
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import net.helcel.fidelity.R
|
||||||
import androidx.compose.foundation.layout.padding
|
import net.helcel.fidelity.databinding.FragScannerBinding
|
||||||
import androidx.compose.material.Button
|
import net.helcel.fidelity.tools.BarcodeScanner.getAnalysisUseCase
|
||||||
import androidx.compose.material.CircularProgressIndicator
|
import net.helcel.fidelity.tools.KeepassWrapper
|
||||||
import androidx.compose.material.Icon
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.FlashOn
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.geometry.Size
|
|
||||||
import androidx.compose.ui.graphics.BlendMode
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
|
||||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
|
||||||
import androidx.navigation.NavController
|
|
||||||
import androidx.navigation.NavHostController
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import net.helcel.fidelity.activity.fragment.ScannerEventHandler.onResult
|
|
||||||
import net.helcel.fidelity.tools.BarcodeScanner
|
|
||||||
import net.helcel.fidelity.tools.BarcodeScanner.analysisUseCase
|
|
||||||
import net.helcel.fidelity.tools.FidelityRepository.activeEntry
|
|
||||||
|
|
||||||
@androidx.compose.ui.tooling.preview.Preview
|
private const val CAMERA_PERMISSION_REQUEST_CODE = 1
|
||||||
@Composable
|
|
||||||
fun ScannerScreen(
|
|
||||||
navController: NavController
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
val cameraProviderFuture = remember {
|
class Scanner : Fragment() {
|
||||||
ProcessCameraProvider.getInstance(context)
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
var camera: Camera? by remember { mutableStateOf(null) }
|
|
||||||
var torchOn by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val done = remember { mutableStateOf(false) }
|
private fun startCreateEntry() {
|
||||||
val previewView = remember { PreviewView(context) }
|
val createEntryFragment = CreateEntry()
|
||||||
|
createEntryFragment.arguments =
|
||||||
|
KeepassWrapper.bundleCreate(null, this.code, this.fmt)
|
||||||
|
requireActivity().supportFragmentManager.beginTransaction()
|
||||||
|
.replace(R.id.container, createEntryFragment)
|
||||||
|
.commit()
|
||||||
|
}
|
||||||
|
|
||||||
val permissionLauncher = rememberLauncherForActivityResult(
|
private fun hasCameraPermission() =
|
||||||
contract = ActivityResultContracts.RequestPermission(),
|
ActivityCompat.checkSelfPermission(
|
||||||
onResult = { granted ->
|
requireContext(),
|
||||||
if (granted) {
|
Manifest.permission.CAMERA
|
||||||
val cameraProvider = cameraProviderFuture.get()
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
val previewUseCase = Preview.Builder().build().also {
|
|
||||||
it.surfaceProvider = previewView.surfaceProvider
|
private fun requestPermission() {
|
||||||
}
|
ActivityCompat.requestPermissions(
|
||||||
val analysisUseCase = analysisUseCase { detectedCode, detectedFormat ->
|
requireActivity(),
|
||||||
if (detectedCode.isNullOrEmpty() || detectedFormat.isNullOrEmpty()) return@analysisUseCase
|
arrayOf(Manifest.permission.CAMERA),
|
||||||
if(done.value) return@analysisUseCase
|
CAMERA_PERMISSION_REQUEST_CODE
|
||||||
scope.launch(Dispatchers.Main) {
|
)
|
||||||
activeEntry.value =
|
ActivityCompat.OnRequestPermissionsResultCallback { c, p, i ->
|
||||||
activeEntry.value.copy(code = detectedCode, format = detectedFormat)
|
require(c == CAMERA_PERMISSION_REQUEST_CODE)
|
||||||
done.value = true
|
require(p.contains(Manifest.permission.CAMERA))
|
||||||
onResult(navController)
|
val el = i[p.indexOf(Manifest.permission.CAMERA)]
|
||||||
}
|
if (el != PackageManager.PERMISSION_GRANTED) {
|
||||||
return@analysisUseCase
|
startCreateEntry()
|
||||||
}
|
|
||||||
try {
|
|
||||||
cameraProvider.unbindAll()
|
|
||||||
camera = cameraProvider.bindToLifecycle(
|
|
||||||
lifecycleOwner,
|
|
||||||
CameraSelector.DEFAULT_BACK_CAMERA,
|
|
||||||
previewUseCase,
|
|
||||||
analysisUseCase
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("ScannerScreen", "Camera bind failed: ${e.message}")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Toast.makeText(context, "Camera permission denied", Toast.LENGTH_SHORT).show()
|
|
||||||
scope.launch(Dispatchers.Main){
|
|
||||||
onResult(navController)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
}
|
||||||
permissionLauncher.launch(Manifest.permission.CAMERA)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
private fun bindCameraUseCases() {
|
||||||
AndroidView(
|
val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
|
||||||
factory = { previewView },
|
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
)
|
|
||||||
ScannerOverlay(
|
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
)
|
|
||||||
Button(onClick = {
|
|
||||||
torchOn = !torchOn
|
|
||||||
camera?.cameraControl?.enableTorch(torchOn)
|
|
||||||
}, modifier = Modifier
|
|
||||||
.align(Alignment.BottomEnd)
|
|
||||||
.padding(16.dp),
|
|
||||||
) {
|
|
||||||
Icon(Icons.Default.FlashOn, contentDescription = null)
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!done.value)
|
cameraProviderFuture.addListener({
|
||||||
CircularProgressIndicator(
|
val cameraProvider = cameraProviderFuture.get()
|
||||||
modifier = Modifier
|
val previewUseCase = Preview.Builder()
|
||||||
.align(Alignment.BottomCenter) // same spot as buttons
|
.build()
|
||||||
.padding(bottom =80.dp),
|
.also {
|
||||||
)
|
it.setSurfaceProvider(binding.cameraView.surfaceProvider)
|
||||||
}
|
}
|
||||||
}
|
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
||||||
|
val analysisUseCase = getAnalysisUseCase { code, format ->
|
||||||
|
if (code != null && format != null) {
|
||||||
@Composable
|
this.code = code
|
||||||
fun ScannerOverlay(
|
this.fmt = format
|
||||||
modifier: Modifier = Modifier
|
this.valid = true
|
||||||
) {
|
|
||||||
Canvas(modifier = modifier.fillMaxSize()) {
|
|
||||||
val widthF = size.width
|
|
||||||
val heightF = size.height
|
|
||||||
|
|
||||||
drawRect(
|
|
||||||
color = Color(0x80000000), // semi-transparent black
|
|
||||||
size = size
|
|
||||||
)
|
|
||||||
|
|
||||||
val squareSize = 0.75f * minOf(widthF, heightF)
|
|
||||||
val left = (widthF - squareSize) / 2
|
|
||||||
val top = (heightF - squareSize) / 2
|
|
||||||
|
|
||||||
drawRect(
|
|
||||||
color = Color.Transparent,
|
|
||||||
topLeft = Offset(left, top),
|
|
||||||
size = Size(squareSize, squareSize),
|
|
||||||
blendMode = BlendMode.Clear
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun FileScanner(navController: NavHostController) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
rememberCoroutineScope()
|
|
||||||
val pickImageLauncher = rememberLauncherForActivityResult(
|
|
||||||
contract = ActivityResultContracts.PickVisualMedia()
|
|
||||||
) { uri ->
|
|
||||||
if (uri == null) {
|
|
||||||
Toast.makeText(context, "No file selected", Toast.LENGTH_SHORT).show()
|
|
||||||
onResult(navController)
|
|
||||||
return@rememberLauncherForActivityResult
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
val inputStream = context.contentResolver.openInputStream(uri)
|
|
||||||
val bitmap = BitmapFactory.decodeStream(inputStream)
|
|
||||||
BarcodeScanner.bitmapUseCase(bitmap) { code, format ->
|
|
||||||
if (!code.isNullOrEmpty() && !format.isNullOrEmpty()) {
|
|
||||||
activeEntry.value = activeEntry.value.copy(code=code, format=format)
|
|
||||||
onResult(navController)
|
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(context, "No barcode found", Toast.LENGTH_SHORT).show()
|
this.valid = false
|
||||||
onResult(navController)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
try {
|
||||||
e.printStackTrace()
|
cameraProvider.bindToLifecycle(
|
||||||
Toast.makeText(context, "Failed to load image", Toast.LENGTH_SHORT).show()
|
this,
|
||||||
onResult(navController)
|
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()))
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
pickImageLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
|
|
||||||
}
|
|
||||||
|
|
||||||
BackHandler {
|
}
|
||||||
onResult(navController)
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object ScannerEventHandler {
|
|
||||||
fun onResult(navController: NavController) {
|
|
||||||
navController.popBackStack()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
package net.helcel.fidelity.activity.fragment
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.Button
|
|
||||||
import androidx.compose.material.Icon
|
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.filled.Undo
|
|
||||||
import androidx.compose.material.icons.filled.ExpandMore
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.window.Dialog
|
|
||||||
import com.kunzisoft.keepass.database.element.Group
|
|
||||||
import com.kunzisoft.keepass.database.element.node.Node
|
|
||||||
import net.helcel.fidelity.tools.FidelityRepository
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun TreeSelectorDialog(onDismiss: (Node?) -> Unit = {}) {
|
|
||||||
Dialog(
|
|
||||||
onDismissRequest = {onDismiss(null)},
|
|
||||||
content = {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxWidth().background(
|
|
||||||
MaterialTheme.colors.background,
|
|
||||||
RoundedCornerShape(8.dp)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
var currentRoot by remember { mutableStateOf(FidelityRepository.getRoot()) }
|
|
||||||
var selection by remember { mutableStateOf<Node?>(FidelityRepository.getRoot()) }
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxWidth().padding(8.dp)
|
|
||||||
) {
|
|
||||||
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
selection = currentRoot
|
|
||||||
currentRoot = currentRoot?.parent
|
|
||||||
},
|
|
||||||
enabled = currentRoot?.parent != null
|
|
||||||
) {
|
|
||||||
Icon(Icons.AutoMirrored.Filled.Undo, contentDescription = "up")
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text(
|
|
||||||
currentRoot?.title ?: "?",
|
|
||||||
color = MaterialTheme.colors.onBackground,
|
|
||||||
style = MaterialTheme.typography.h6
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
LazyColumn(modifier = Modifier.fillMaxHeight(0.75f)) {
|
|
||||||
items(currentRoot?.getChildGroups() ?: emptyList()) { entry ->
|
|
||||||
val isSel = (entry.nodeId == selection?.nodeId)
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(color = if (isSel) MaterialTheme.colors.primary else MaterialTheme.colors.background)
|
|
||||||
.clickable {
|
|
||||||
if (entry.getChildEntries().isNotEmpty()) {
|
|
||||||
currentRoot = entry
|
|
||||||
selection = entry
|
|
||||||
} else if (entry.getChildGroups().isNotEmpty()) {
|
|
||||||
currentRoot = entry
|
|
||||||
selection = entry
|
|
||||||
} else {
|
|
||||||
selection = entry
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(8.dp)
|
|
||||||
) {
|
|
||||||
if (entry.getChildEntries().isNotEmpty() || entry.getChildGroups()
|
|
||||||
.isNotEmpty()
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.ExpandMore,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = if (isSel) MaterialTheme.colors.onPrimary else MaterialTheme.colors.onBackground
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Text(
|
|
||||||
entry.title,
|
|
||||||
modifier = Modifier.padding(start = 8.dp),
|
|
||||||
color = if (isSel) MaterialTheme.colors.onPrimary else MaterialTheme.colors.onBackground
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
items(currentRoot?.getChildEntries() ?: emptyList()) { entry ->
|
|
||||||
val isSel = (entry.nodeId == selection?.nodeId)
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(color = if (isSel) MaterialTheme.colors.primary else MaterialTheme.colors.background)
|
|
||||||
.clickable {
|
|
||||||
selection = entry
|
|
||||||
}
|
|
||||||
.padding(8.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
entry.title,
|
|
||||||
modifier = Modifier.padding(start = 8.dp),
|
|
||||||
color = if (isSel) MaterialTheme.colors.onPrimary else MaterialTheme.colors.onBackground
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Button(
|
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
|
||||||
enabled = selection != null,
|
|
||||||
onClick = {
|
|
||||||
onDismiss(selection)
|
|
||||||
}) {
|
|
||||||
Text("Select " + if (selection is Group) "Group" else "Entry")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,274 +0,0 @@
|
|||||||
package net.helcel.fidelity.activity.fragment
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.activity.compose.BackHandler
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.OpenDocument
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
|
||||||
import androidx.compose.material.Button
|
|
||||||
import androidx.compose.material.Checkbox
|
|
||||||
import androidx.compose.material.CheckboxDefaults
|
|
||||||
import androidx.compose.material.CircularProgressIndicator
|
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.material.OutlinedTextField
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.material.TextFieldDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.navigation.NavHostController
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import net.helcel.fidelity.activity.ToastHelper
|
|
||||||
import net.helcel.fidelity.activity.fragment.SetupEventHandlers.onOpen
|
|
||||||
import net.helcel.fidelity.tools.CredentialResult
|
|
||||||
import net.helcel.fidelity.tools.FidelityRepository.genCredentials
|
|
||||||
import net.helcel.fidelity.tools.FidelityRepository.start
|
|
||||||
import net.helcel.fidelity.tools.KeePassStore.loadCredentials
|
|
||||||
import net.helcel.fidelity.tools.KeePassStore.packCredentials
|
|
||||||
import net.helcel.fidelity.tools.KeePassStore.saveCredentials
|
|
||||||
|
|
||||||
|
|
||||||
class GetPersistentContent : OpenDocument() {
|
|
||||||
@SuppressLint("InlinedApi")
|
|
||||||
override fun createIntent(context: Context, input: Array<String>): Intent {
|
|
||||||
return super.createIntent(context, input).apply {
|
|
||||||
addCategory(Intent.CATEGORY_DEFAULT)
|
|
||||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun InitialScreen(
|
|
||||||
navController: NavHostController?
|
|
||||||
) {
|
|
||||||
var loading by remember { mutableStateOf(false) }
|
|
||||||
var dbFile by remember { mutableStateOf<Uri?>(null) }
|
|
||||||
var password by remember { mutableStateOf("") }
|
|
||||||
var keyFile by remember { mutableStateOf<Uri?>(null) }
|
|
||||||
val context = LocalContext.current
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
val dbFilePickerLauncher = rememberLauncherForActivityResult(
|
|
||||||
contract = GetPersistentContent(),
|
|
||||||
) {
|
|
||||||
if(it!=null) {
|
|
||||||
dbFile = it
|
|
||||||
scope.launch(Dispatchers.IO) {
|
|
||||||
context.contentResolver.takePersistableUriPermission(
|
|
||||||
it,
|
|
||||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val keyFilePickerLauncher = rememberLauncherForActivityResult(
|
|
||||||
contract = GetPersistentContent()
|
|
||||||
) {
|
|
||||||
if(it!=null) {
|
|
||||||
keyFile = it
|
|
||||||
scope.launch(Dispatchers.IO) {
|
|
||||||
context.contentResolver.takePersistableUriPermission(
|
|
||||||
it,
|
|
||||||
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BackHandler {
|
|
||||||
navController!!.navigate("exit")
|
|
||||||
}
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
scope.launch(Dispatchers.Main) {
|
|
||||||
when(val res = loadCredentials(context)) {
|
|
||||||
CredentialResult.AuthFailed -> null
|
|
||||||
CredentialResult.NoData -> null
|
|
||||||
is CredentialResult.Success -> {
|
|
||||||
if (res.db != null) dbFile = res.db
|
|
||||||
if (res.key != null) keyFile = res.key
|
|
||||||
if (res.password != "" && password == "") password = res.password
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Box(modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(MaterialTheme.colors.background)) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(16.dp)
|
|
||||||
.background(MaterialTheme.colors.background),
|
|
||||||
verticalArrangement = Arrangement.Center
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
"Keypass Database Setup",
|
|
||||||
style = MaterialTheme.typography.h5,
|
|
||||||
color = MaterialTheme.colors.onBackground
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text("KDBX Database:", color = MaterialTheme.colors.onBackground)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Checkbox(
|
|
||||||
enabled = !loading,
|
|
||||||
modifier = Modifier
|
|
||||||
.background(
|
|
||||||
MaterialTheme.colors.primary,
|
|
||||||
RoundedCornerShape(8.dp)
|
|
||||||
)
|
|
||||||
.size(32.dp),
|
|
||||||
checked = dbFile != null,
|
|
||||||
onCheckedChange = { dbFilePickerLauncher.launch(arrayOf("*/*")) },
|
|
||||||
colors = CheckboxDefaults.colors(
|
|
||||||
uncheckedColor = MaterialTheme.colors.primary,
|
|
||||||
checkedColor = MaterialTheme.colors.primary,
|
|
||||||
checkmarkColor = MaterialTheme.colors.onPrimary
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
OutlinedTextField(
|
|
||||||
enabled = !loading,
|
|
||||||
value = password,
|
|
||||||
onValueChange = { password = it },
|
|
||||||
label = { Text("Password") },
|
|
||||||
singleLine = true,
|
|
||||||
keyboardOptions = KeyboardOptions(
|
|
||||||
capitalization = KeyboardCapitalization.Unspecified,
|
|
||||||
autoCorrectEnabled = false,
|
|
||||||
keyboardType = KeyboardType.Password,
|
|
||||||
imeAction = ImeAction.Done
|
|
||||||
),
|
|
||||||
colors = TextFieldDefaults.textFieldColors(
|
|
||||||
textColor = MaterialTheme.colors.onBackground
|
|
||||||
),
|
|
||||||
visualTransformation = PasswordVisualTransformation(),
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text("KDBX Key File:", color = MaterialTheme.colors.onBackground)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Checkbox(
|
|
||||||
enabled = !loading,
|
|
||||||
modifier = Modifier
|
|
||||||
.background(
|
|
||||||
MaterialTheme.colors.primary,
|
|
||||||
RoundedCornerShape(8.dp)
|
|
||||||
)
|
|
||||||
.size(32.dp),
|
|
||||||
checked = keyFile != null,
|
|
||||||
onCheckedChange = { keyFilePickerLauncher.launch(arrayOf("*/*")) },
|
|
||||||
colors = CheckboxDefaults.colors(
|
|
||||||
uncheckedColor = MaterialTheme.colors.primary,
|
|
||||||
checkedColor = MaterialTheme.colors.primary,
|
|
||||||
checkmarkColor = MaterialTheme.colors.onPrimary
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
Button(
|
|
||||||
enabled = !loading && password.isNotBlank() && dbFile != null ,
|
|
||||||
onClick = {
|
|
||||||
loading = true
|
|
||||||
scope.launch {
|
|
||||||
if(onOpen(context, dbFile!!, password, keyFile)){
|
|
||||||
navController!!.popBackStack()
|
|
||||||
navController.navigate("init")
|
|
||||||
}else{
|
|
||||||
ToastHelper.show(context, "Auth failed...")
|
|
||||||
navController!!.popBackStack()
|
|
||||||
navController.navigate("exit")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Text("Continue")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Box(contentAlignment = Alignment.BottomCenter, modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(32.dp)){
|
|
||||||
|
|
||||||
if(loading )
|
|
||||||
CircularProgressIndicator(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.BottomCenter) // same spot as buttons
|
|
||||||
.padding(bottom = 80.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object SetupEventHandlers {
|
|
||||||
suspend fun onOpen(context: Context, db: Uri, p: String, key: Uri?): Boolean {
|
|
||||||
try {
|
|
||||||
val packCred = packCredentials(db, p, key)
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
start(context, db, genCredentials(context, packCred)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val res = withContext(Dispatchers.Main) {
|
|
||||||
saveCredentials(context, packCred)
|
|
||||||
}
|
|
||||||
return when (res) {
|
|
||||||
CredentialResult.AuthFailed, CredentialResult.NoData -> false
|
|
||||||
is CredentialResult.Success -> true
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
ToastHelper.show(context, e.message.toString())
|
|
||||||
println("Err${e.toString()}")
|
|
||||||
println(e.message)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,125 +1,75 @@
|
|||||||
package net.helcel.fidelity.activity.fragment
|
package net.helcel.fidelity.activity.fragment
|
||||||
|
|
||||||
import android.app.Activity
|
import android.content.res.Configuration
|
||||||
import android.graphics.Bitmap
|
import android.os.Bundle
|
||||||
import android.view.WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
|
import android.view.LayoutInflater
|
||||||
import android.widget.Toast
|
import android.view.View
|
||||||
import androidx.activity.compose.BackHandler
|
import android.view.ViewGroup
|
||||||
import androidx.compose.foundation.Image
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.compose.foundation.background
|
import com.google.zxing.FormatException
|
||||||
import androidx.compose.foundation.clickable
|
import net.helcel.fidelity.databinding.FragViewEntryBinding
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.material.CircularProgressIndicator
|
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.SideEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.rotate
|
|
||||||
import androidx.compose.ui.draw.scale
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.navigation.NavHostController
|
|
||||||
import net.helcel.fidelity.tools.BarcodeGenerator.generateBarcode
|
import net.helcel.fidelity.tools.BarcodeGenerator.generateBarcode
|
||||||
import net.helcel.fidelity.tools.FidelityEntry
|
import net.helcel.fidelity.tools.ErrorToaster
|
||||||
import kotlin.let
|
import net.helcel.fidelity.tools.KeepassWrapper
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
|
|
||||||
@Preview
|
class ViewEntry : Fragment() {
|
||||||
@Composable
|
|
||||||
fun PreviewEntryScreen(){
|
|
||||||
ViewEntryScreen(null, FidelityEntry("Title","AAA","QR"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
private lateinit var binding: FragViewEntryBinding
|
||||||
fun ViewEntryScreen(
|
|
||||||
navController: NavHostController?,
|
|
||||||
entry: FidelityEntry
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val activity = context as? Activity
|
|
||||||
var isFull by remember { mutableStateOf(false) }
|
|
||||||
var bitmap by remember { mutableStateOf<Bitmap?>(null) }
|
|
||||||
|
|
||||||
SideEffect {
|
private var title: String? = null
|
||||||
activity?.window?.attributes = activity.window?.attributes?.apply {
|
private var code: String? = null
|
||||||
screenBrightness = if (isFull) 1f else BRIGHTNESS_OVERRIDE_NONE
|
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 {
|
try {
|
||||||
bitmap = generateBarcode(entry.code, entry.format, 1024)
|
val barcodeBitmap = generateBarcode(
|
||||||
} catch (_: Exception) {
|
code!!, fmt!!, 1024
|
||||||
bitmap = null
|
|
||||||
Toast.makeText(context, "Invalid barcode format", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BackHandler {
|
|
||||||
isFull=false
|
|
||||||
navController!!.popBackStack()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(Color.Black)
|
|
||||||
.clickable(
|
|
||||||
onClick = { isFull = !isFull },
|
|
||||||
indication = null, // remove ripple effect
|
|
||||||
interactionSource = remember { MutableInteractionSource() }
|
|
||||||
),
|
|
||||||
contentAlignment = Alignment.TopCenter
|
|
||||||
) {
|
|
||||||
if (!isFull) {
|
|
||||||
Text(
|
|
||||||
text = entry.title,
|
|
||||||
color = Color.White,
|
|
||||||
style = MaterialTheme.typography.h4,
|
|
||||||
modifier = Modifier.padding(32.dp)
|
|
||||||
)
|
)
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
BoxWithConstraints(
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
modifier = Modifier
|
super.onConfigurationChanged(newConfig)
|
||||||
.fillMaxSize().padding(8.dp),
|
adjustLayout()
|
||||||
contentAlignment = Alignment.Center
|
}
|
||||||
) {
|
|
||||||
bitmap?.let {
|
|
||||||
|
|
||||||
|
private fun adjustLayout() {
|
||||||
val modifier = Modifier
|
if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||||
.fillMaxSize()
|
binding.title.visibility = View.GONE
|
||||||
.width(maxWidth)
|
} else {
|
||||||
.height(maxHeight)
|
binding.title.visibility = View.VISIBLE
|
||||||
.padding(16.dp)
|
|
||||||
.aspectRatio(it.width.toFloat()/it.height.toFloat())
|
|
||||||
.rotate(if (isFull) 90f else 0f)
|
|
||||||
.scale(if(isFull) min(it.width.dp/maxHeight,it.height.dp/maxWidth) else 1f)
|
|
||||||
|
|
||||||
Image(
|
|
||||||
bitmap = it.asImageBitmap(),
|
|
||||||
contentDescription = "Barcode",
|
|
||||||
modifier = modifier,
|
|
||||||
contentScale = ContentScale.Fit,
|
|
||||||
)
|
|
||||||
} ?: CircularProgressIndicator(color = Color.White)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
179
app/src/main/java/net/helcel/fidelity/pluginSDK/AccessManager.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
107
app/src/main/java/net/helcel/fidelity/pluginSDK/Kp2aControl.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package net.helcel.fidelity.pluginSDK
|
||||||
|
|
||||||
|
class PluginAccessException : Exception {
|
||||||
|
constructor(what: String?) : super(what)
|
||||||
|
|
||||||
|
constructor(hostPackage: String?, scopes: ArrayList<String?>)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private const val serialVersionUID = 1L
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package net.helcel.fidelity.pluginSDK
|
||||||
|
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
|
|
||||||
|
class PluginAccessReceiver : PluginAccessBroadcastReceiver() {
|
||||||
|
|
||||||
|
override val scopes: ArrayList<String?> = ArrayList()
|
||||||
|
|
||||||
|
init {
|
||||||
|
this.scopes.add(Strings.SCOPE_DATABASE_ACTIONS)
|
||||||
|
this.scopes.add(Strings.SCOPE_QUERY_CREDENTIALS_FOR_OWN_PACKAGE)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
package net.helcel.fidelity.pluginSDK
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class PluginActionBroadcastReceiver : BroadcastReceiver() {
|
||||||
|
open class PluginActionBase
|
||||||
|
(var context: Context, protected var _intent: Intent) {
|
||||||
|
val hostPackage: String?
|
||||||
|
get() = _intent.getStringExtra(Strings.EXTRA_SENDER)
|
||||||
|
}
|
||||||
|
|
||||||
|
open class PluginEntryActionBase(context: Context, intent: Intent) :
|
||||||
|
PluginActionBase(context, intent) {
|
||||||
|
protected val entryFieldsFromIntent: HashMap<String, String>
|
||||||
|
get() {
|
||||||
|
val res = HashMap<String, String>()
|
||||||
|
try {
|
||||||
|
val json = JSONObject(_intent.getStringExtra(Strings.EXTRA_ENTRY_OUTPUT_DATA)!!)
|
||||||
|
val iter = json.keys()
|
||||||
|
while (iter.hasNext()) {
|
||||||
|
val key = iter.next()
|
||||||
|
val value = json[key].toString()
|
||||||
|
res[key] = value
|
||||||
|
}
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
protected val protectedFieldsListFromIntent: Array<String?>?
|
||||||
|
get() {
|
||||||
|
try {
|
||||||
|
val json =
|
||||||
|
JSONArray(_intent.getStringExtra(Strings.EXTRA_PROTECTED_FIELDS_LIST))
|
||||||
|
val res = arrayOfNulls<String>(json.length())
|
||||||
|
for (i in 0 until json.length()) res[i] = json.getString(i)
|
||||||
|
return res
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
open val entryId: String?
|
||||||
|
get() = _intent.getStringExtra(Strings.EXTRA_ENTRY_ID)
|
||||||
|
|
||||||
|
|
||||||
|
@Throws(PluginAccessException::class)
|
||||||
|
fun setEntryField(fieldId: String?, fieldValue: String?, isProtected: Boolean) {
|
||||||
|
val i = Intent(Strings.ACTION_SET_ENTRY_FIELD)
|
||||||
|
val scope = ArrayList<String?>()
|
||||||
|
scope.add(Strings.SCOPE_CURRENT_ENTRY)
|
||||||
|
i.putExtra(
|
||||||
|
Strings.EXTRA_ACCESS_TOKEN, AccessManager.getAccessToken(
|
||||||
|
context, hostPackage!!, scope
|
||||||
|
)
|
||||||
|
)
|
||||||
|
i.setPackage(hostPackage)
|
||||||
|
i.putExtra(Strings.EXTRA_SENDER, context.packageName)
|
||||||
|
i.putExtra(Strings.EXTRA_FIELD_VALUE, fieldValue)
|
||||||
|
i.putExtra(Strings.EXTRA_ENTRY_ID, entryId)
|
||||||
|
i.putExtra(Strings.EXTRA_FIELD_ID, fieldId)
|
||||||
|
i.putExtra(Strings.EXTRA_FIELD_PROTECTED, isProtected)
|
||||||
|
|
||||||
|
context.sendBroadcast(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class ActionSelectedAction(ctx: Context, intent: Intent) :
|
||||||
|
PluginEntryActionBase(ctx, intent) {
|
||||||
|
val actionData: Bundle?
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @return the Bundle associated with the action. This bundle can be set in OpenEntry.add(Entry)FieldAction
|
||||||
|
*/
|
||||||
|
get() = _intent.getBundleExtra(Strings.EXTRA_ACTION_DATA)
|
||||||
|
|
||||||
|
private val fieldId: String?
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @return the field id which was selected. null if an entry action (in the options menu) was selected.
|
||||||
|
*/
|
||||||
|
get() = _intent.getStringExtra(Strings.EXTRA_FIELD_ID)
|
||||||
|
|
||||||
|
val isEntryAction: Boolean
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @return true if an entry action, i.e. an option from the options menu, was selected. False if an option
|
||||||
|
* in a popup menu for a certain field was selected.
|
||||||
|
*/
|
||||||
|
get() = fieldId == null
|
||||||
|
|
||||||
|
val entryFields: HashMap<String, String>
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @return a hashmap containing the entry fields in key/value form
|
||||||
|
*/
|
||||||
|
get() = entryFieldsFromIntent
|
||||||
|
|
||||||
|
val protectedFieldsList: Array<String?>?
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @return an array with the keys of all protected fields in the entry
|
||||||
|
*/
|
||||||
|
get() = protectedFieldsListFromIntent
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class CloseEntryViewAction(context: Context, intent: Intent) :
|
||||||
|
PluginEntryActionBase(context, intent) {
|
||||||
|
override val entryId: String?
|
||||||
|
get() = _intent.getStringExtra(Strings.EXTRA_ENTRY_ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
private open inner class OpenEntryAction(context: Context, intent: Intent) :
|
||||||
|
PluginEntryActionBase(context, intent) {
|
||||||
|
val entryFields: HashMap<String, String>
|
||||||
|
get() = entryFieldsFromIntent
|
||||||
|
|
||||||
|
val protectedFieldsList: Array<String?>?
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @return an array with the keys of all protected fields in the entry
|
||||||
|
*/
|
||||||
|
get() = protectedFieldsListFromIntent
|
||||||
|
|
||||||
|
@Throws(PluginAccessException::class)
|
||||||
|
fun addEntryAction(
|
||||||
|
actionDisplayText: String?,
|
||||||
|
actionIconResourceId: Int,
|
||||||
|
actionData: Bundle?
|
||||||
|
) {
|
||||||
|
addEntryFieldAction(null, null, actionDisplayText, actionIconResourceId, actionData)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(PluginAccessException::class)
|
||||||
|
fun addEntryFieldAction(
|
||||||
|
actionId: String?,
|
||||||
|
fieldId: String?,
|
||||||
|
actionDisplayText: String?,
|
||||||
|
actionIconResourceId: Int,
|
||||||
|
actionData: Bundle?
|
||||||
|
) {
|
||||||
|
val i = Intent(Strings.ACTION_ADD_ENTRY_ACTION)
|
||||||
|
val scope = ArrayList<String?>()
|
||||||
|
scope.add(Strings.SCOPE_CURRENT_ENTRY)
|
||||||
|
i.putExtra(
|
||||||
|
Strings.EXTRA_ACCESS_TOKEN, AccessManager.getAccessToken(
|
||||||
|
context, hostPackage!!, scope
|
||||||
|
)
|
||||||
|
)
|
||||||
|
i.setPackage(hostPackage)
|
||||||
|
i.putExtra(Strings.EXTRA_SENDER, context.packageName)
|
||||||
|
i.putExtra(Strings.EXTRA_ACTION_DATA, actionData)
|
||||||
|
i.putExtra(Strings.EXTRA_ACTION_DISPLAY_TEXT, actionDisplayText)
|
||||||
|
i.putExtra(Strings.EXTRA_ACTION_ICON_RES_ID, actionIconResourceId)
|
||||||
|
i.putExtra(Strings.EXTRA_ENTRY_ID, entryId)
|
||||||
|
i.putExtra(Strings.EXTRA_FIELD_ID, fieldId)
|
||||||
|
i.putExtra(Strings.EXTRA_ACTION_ID, actionId)
|
||||||
|
|
||||||
|
context.sendBroadcast(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class DatabaseAction(context: Context, intent: Intent) :
|
||||||
|
PluginActionBase(context, intent) {
|
||||||
|
val fileDisplayName: String?
|
||||||
|
get() = _intent.getStringExtra(Strings.EXTRA_DATABASE_FILE_DISPLAYNAME)
|
||||||
|
|
||||||
|
val filePath: String?
|
||||||
|
get() = _intent.getStringExtra(Strings.EXTRA_DATABASE_FILEPATH)
|
||||||
|
|
||||||
|
val action: String?
|
||||||
|
get() = _intent.action
|
||||||
|
}
|
||||||
|
|
||||||
|
//EntryOutputModified is very similar to OpenEntry because it receives the same
|
||||||
|
//data (+ the field id which was modified)
|
||||||
|
private inner class EntryOutputModifiedAction(context: Context, intent: Intent) :
|
||||||
|
OpenEntryAction(context, intent) {
|
||||||
|
val modifiedFieldId: String?
|
||||||
|
get() = _intent.getStringExtra(Strings.EXTRA_FIELD_ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReceive(ctx: Context, intent: Intent) {
|
||||||
|
val action = intent.action
|
||||||
|
Log.d(
|
||||||
|
"KP2A.pluginsdk",
|
||||||
|
"received broadcast in PluginActionBroadcastReceiver with action=$action"
|
||||||
|
)
|
||||||
|
if (action == null) return
|
||||||
|
if (action == Strings.ACTION_OPEN_ENTRY) {
|
||||||
|
openEntry(OpenEntryAction(ctx, intent))
|
||||||
|
} else if (action == Strings.ACTION_CLOSE_ENTRY_VIEW) {
|
||||||
|
closeEntryView(CloseEntryViewAction(ctx, intent))
|
||||||
|
} else if (action == Strings.ACTION_ENTRY_ACTION_SELECTED) {
|
||||||
|
actionSelected(ActionSelectedAction(ctx, intent))
|
||||||
|
} else if (action == Strings.ACTION_ENTRY_OUTPUT_MODIFIED) {
|
||||||
|
entryOutputModified(EntryOutputModifiedAction(ctx, intent))
|
||||||
|
} else if (action == Strings.ACTION_LOCK_DATABASE || action == Strings.ACTION_UNLOCK_DATABASE || action == Strings.ACTION_OPEN_DATABASE || action == Strings.ACTION_CLOSE_DATABASE) {
|
||||||
|
dbAction(DatabaseAction(ctx, intent))
|
||||||
|
} else {
|
||||||
|
//TODO handle unexpected action
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun closeEntryView(closeEntryView: CloseEntryViewAction?) {}
|
||||||
|
|
||||||
|
private fun actionSelected(actionSelected: ActionSelectedAction?) {}
|
||||||
|
|
||||||
|
private fun openEntry(oe: OpenEntryAction?) {}
|
||||||
|
|
||||||
|
private fun entryOutputModified(eom: EntryOutputModifiedAction?) {}
|
||||||
|
|
||||||
|
private fun dbAction(db: DatabaseAction?) {}
|
||||||
|
}
|
||||||
195
app/src/main/java/net/helcel/fidelity/pluginSDK/Strings.kt
Normal 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_"
|
||||||
|
|
||||||
|
}
|
||||||
77
app/src/main/java/net/helcel/fidelity/tools/BacodeScanner.kt
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package net.helcel.fidelity.tools
|
package net.helcel.fidelity.tools
|
||||||
|
|
||||||
|
import com.google.mlkit.vision.barcode.common.Barcode
|
||||||
import com.google.zxing.BarcodeFormat
|
import com.google.zxing.BarcodeFormat
|
||||||
|
|
||||||
object BarcodeFormatConverter {
|
object BarcodeFormatConverter {
|
||||||
@@ -15,40 +16,23 @@ object BarcodeFormatConverter {
|
|||||||
"UPC_A" -> BarcodeFormat.UPC_A
|
"UPC_A" -> BarcodeFormat.UPC_A
|
||||||
"UPC_E" -> BarcodeFormat.UPC_E
|
"UPC_E" -> BarcodeFormat.UPC_E
|
||||||
"PDF_417" -> BarcodeFormat.PDF_417
|
"PDF_417" -> BarcodeFormat.PDF_417
|
||||||
"AZTEC" -> BarcodeFormat.AZTEC
|
|
||||||
"CODABAR" -> BarcodeFormat.CODABAR
|
|
||||||
"MAXICODE" -> BarcodeFormat.MAXICODE
|
|
||||||
"DATA_MATRIX" -> BarcodeFormat.DATA_MATRIX
|
|
||||||
"ITF" -> BarcodeFormat.ITF
|
|
||||||
"RSS_14" -> BarcodeFormat.RSS_14
|
|
||||||
"RSS_EXPANDED" -> BarcodeFormat.RSS_EXPANDED
|
|
||||||
"UPC_EAN" -> BarcodeFormat.UPC_EAN_EXTENSION
|
|
||||||
else -> throw Exception("Unsupported Format: $f")
|
else -> throw Exception("Unsupported Format: $f")
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun formatToString(f: BarcodeFormat): String {
|
|
||||||
|
fun formatToString(f: Int): String {
|
||||||
return when (f) {
|
return when (f) {
|
||||||
BarcodeFormat.CODE_39 -> "CODE_39"
|
Barcode.FORMAT_CODE_128 -> "CODE_128"
|
||||||
BarcodeFormat.CODE_93 -> "CODE_93"
|
Barcode.FORMAT_CODE_39 -> "CODE_39"
|
||||||
BarcodeFormat.CODE_128 -> "CODE_128"
|
Barcode.FORMAT_CODE_93 -> "CODE_93"
|
||||||
BarcodeFormat.EAN_8 -> "EAN_8"
|
Barcode.FORMAT_EAN_8 -> "EAN_8"
|
||||||
BarcodeFormat.EAN_13 -> "EAN_13"
|
Barcode.FORMAT_EAN_13 -> "EAN_13"
|
||||||
BarcodeFormat.QR_CODE -> "CODE_QR"
|
Barcode.FORMAT_QR_CODE -> "CODE_QR"
|
||||||
BarcodeFormat.UPC_A -> "UPC_A"
|
Barcode.FORMAT_UPC_A -> "UPC_A"
|
||||||
BarcodeFormat.UPC_E -> "UPC_E"
|
Barcode.FORMAT_UPC_E -> "UPC_E"
|
||||||
BarcodeFormat.PDF_417 -> "PDF_417"
|
Barcode.FORMAT_PDF417 -> "PDF_417"
|
||||||
BarcodeFormat.AZTEC -> "AZTEC"
|
else -> throw Exception("Unsupported Format: $f")
|
||||||
BarcodeFormat.CODABAR -> "CODABAR"
|
|
||||||
BarcodeFormat.MAXICODE -> "MAXICODE"
|
|
||||||
BarcodeFormat.DATA_MATRIX -> "DATA_MATRIX"
|
|
||||||
BarcodeFormat.ITF -> "ITF"
|
|
||||||
BarcodeFormat.RSS_14 -> "RSS_14"
|
|
||||||
BarcodeFormat.RSS_EXPANDED -> "RSS_EXPANDED"
|
|
||||||
BarcodeFormat.UPC_EAN_EXTENSION -> "UPC_EAN"
|
|
||||||
//else -> throw Exception("Unsupported Format: $f")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,8 +6,6 @@ import com.google.zxing.MultiFormatWriter
|
|||||||
import com.google.zxing.WriterException
|
import com.google.zxing.WriterException
|
||||||
import com.google.zxing.common.BitMatrix
|
import com.google.zxing.common.BitMatrix
|
||||||
import net.helcel.fidelity.tools.BarcodeFormatConverter.stringToFormat
|
import net.helcel.fidelity.tools.BarcodeFormatConverter.stringToFormat
|
||||||
import androidx.core.graphics.set
|
|
||||||
import androidx.core.graphics.createBitmap
|
|
||||||
|
|
||||||
object BarcodeGenerator {
|
object BarcodeGenerator {
|
||||||
|
|
||||||
@@ -21,23 +19,25 @@ object BarcodeGenerator {
|
|||||||
android.graphics.Color.WHITE
|
android.graphics.Color.WHITE
|
||||||
}
|
}
|
||||||
|
|
||||||
fun generateBarcode(content: String?, f: String?, w: Int): Bitmap? {
|
fun generateBarcode(content: String, f: String, width: Int): Bitmap? {
|
||||||
if (content.isNullOrEmpty() || f.isNullOrEmpty()) {
|
if (content.isEmpty() || f.isEmpty()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
val format = stringToFormat(f)
|
val format = stringToFormat(f)
|
||||||
val writer = MultiFormatWriter()
|
val writer = MultiFormatWriter()
|
||||||
val height = (w * formatToRatio(format)).toInt()
|
val height = (formatToRatio(format) * width).toInt()
|
||||||
val width = (w * 1.0f).toInt()
|
|
||||||
|
|
||||||
|
|
||||||
val bitMatrix: BitMatrix = writer.encode(content, format, width, height)
|
val bitMatrix: BitMatrix = writer.encode(content, format, width, height)
|
||||||
val bitmap = createBitmap(width, height)
|
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||||
|
|
||||||
for (x in 0 until width) {
|
for (x in 0 until width) {
|
||||||
for (y in 0 until height) {
|
for (y in 0 until height) {
|
||||||
bitmap[x, y] = getPixelColor(bitMatrix, x, y)
|
bitmap.setPixel(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
getPixelColor(bitMatrix, x, y)
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return bitmap
|
return bitmap
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
package net.helcel.fidelity.tools
|
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import androidx.annotation.OptIn
|
|
||||||
import androidx.camera.core.ExperimentalGetImage
|
|
||||||
import androidx.camera.core.ImageAnalysis
|
|
||||||
import com.google.zxing.BinaryBitmap
|
|
||||||
import com.google.zxing.MultiFormatReader
|
|
||||||
import com.google.zxing.NotFoundException
|
|
||||||
import com.google.zxing.RGBLuminanceSource
|
|
||||||
import com.google.zxing.ReaderException
|
|
||||||
import com.google.zxing.common.HybridBinarizer
|
|
||||||
import net.helcel.fidelity.tools.BarcodeFormatConverter.formatToString
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalGetImage::class)
|
|
||||||
object BarcodeScanner {
|
|
||||||
|
|
||||||
private fun processImage(
|
|
||||||
bitmap: Bitmap,
|
|
||||||
cb: (String?, String?) -> Unit
|
|
||||||
) {
|
|
||||||
val binaryBitmap = createBinaryBitmap(bitmap)
|
|
||||||
val reader = MultiFormatReader()
|
|
||||||
try {
|
|
||||||
val result = reader.decode(binaryBitmap)
|
|
||||||
cb(result.text, formatToString(result.barcodeFormat))
|
|
||||||
} catch (_: NotFoundException) {
|
|
||||||
cb(null, null)
|
|
||||||
} catch (_: ReaderException) {
|
|
||||||
cb(null, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createBinaryBitmap(bitmap: Bitmap): BinaryBitmap {
|
|
||||||
val pixels = IntArray(bitmap.width * bitmap.height)
|
|
||||||
bitmap.getPixels(pixels, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height)
|
|
||||||
val source =
|
|
||||||
RGBLuminanceSource(bitmap.width, bitmap.height, pixels)
|
|
||||||
return BinaryBitmap(HybridBinarizer(source))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun analysisUseCase(cb: (String?, String?) -> Unit): ImageAnalysis {
|
|
||||||
val analysisUseCase = ImageAnalysis.Builder().build()
|
|
||||||
analysisUseCase.setAnalyzer(
|
|
||||||
Executors.newSingleThreadExecutor()
|
|
||||||
) { imageProxy ->
|
|
||||||
val bitmap = imageProxy.toBitmap()
|
|
||||||
imageProxy.close()
|
|
||||||
bitmapUseCase(bitmap, cb)
|
|
||||||
}
|
|
||||||
return analysisUseCase
|
|
||||||
}
|
|
||||||
|
|
||||||
fun bitmapUseCase(bitmap: Bitmap, cb: (String?, String?) -> Unit) {
|
|
||||||
processImage(bitmap, cb)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
package net.helcel.fidelity.tools
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import android.security.keystore.KeyGenParameterSpec
|
|
||||||
import android.security.keystore.KeyProperties
|
|
||||||
import android.util.Base64
|
|
||||||
import androidx.biometric.BiometricPrompt
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import javax.crypto.Cipher
|
|
||||||
import androidx.datastore.preferences.core.*
|
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
|
||||||
import com.kunzisoft.keepass.utils.parseUri
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
import java.security.KeyStore
|
|
||||||
import javax.crypto.KeyGenerator
|
|
||||||
import javax.crypto.SecretKey
|
|
||||||
|
|
||||||
val Context.securePrefs by preferencesDataStore("keepass_prefs")
|
|
||||||
object KeePassKeys {
|
|
||||||
val DB_FILE_PATH = stringPreferencesKey("db_file_path")
|
|
||||||
val PASSWORD = stringPreferencesKey("password_enc")
|
|
||||||
val KEY_FILE_PATH = stringPreferencesKey("key_file_path")
|
|
||||||
val IV = stringPreferencesKey("iv")
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class CredentialResult {
|
|
||||||
data class Success(val db: Uri?, val password: String, val key: Uri?) : CredentialResult()
|
|
||||||
object NoData : CredentialResult()
|
|
||||||
object AuthFailed : CredentialResult()
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val KEY_ALIAS = "keepass_bio_key"
|
|
||||||
|
|
||||||
fun getOrCreateBiometricKey(): SecretKey {
|
|
||||||
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
|
|
||||||
keyStore.getKey(KEY_ALIAS, null)?.let { return it as SecretKey }
|
|
||||||
val keyGenerator =
|
|
||||||
KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
|
|
||||||
val spec = KeyGenParameterSpec.Builder(
|
|
||||||
KEY_ALIAS,
|
|
||||||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
|
|
||||||
).apply {
|
|
||||||
setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
|
||||||
setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
|
||||||
setUserAuthenticationRequired(true)
|
|
||||||
setInvalidatedByBiometricEnrollment(true)
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
keyGenerator.init(spec)
|
|
||||||
return keyGenerator.generateKey()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCipherForDecryption(key: SecretKey, iv: ByteArray?): Cipher {
|
|
||||||
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
|
||||||
if(iv==null) cipher.init(Cipher.ENCRYPT_MODE, key)
|
|
||||||
else cipher.init(Cipher.DECRYPT_MODE, key, javax.crypto.spec.GCMParameterSpec(128, iv))
|
|
||||||
return cipher
|
|
||||||
}
|
|
||||||
object KeePassStore {
|
|
||||||
suspend fun saveCredentials(
|
|
||||||
context: Context, cred: CredentialResult.Success
|
|
||||||
): CredentialResult {
|
|
||||||
val cipher = showBiometricPrompt(context as FragmentActivity, true)
|
|
||||||
?: return CredentialResult.AuthFailed
|
|
||||||
val encPasswordB = cipher.doFinal(cred.password.toByteArray(Charsets.UTF_8))
|
|
||||||
context.securePrefs.edit { prefs ->
|
|
||||||
prefs[KeePassKeys.DB_FILE_PATH] = cred.db.toString()
|
|
||||||
prefs[KeePassKeys.PASSWORD] = Base64.encodeToString(encPasswordB, Base64.DEFAULT)
|
|
||||||
prefs[KeePassKeys.IV] = Base64.encodeToString(cipher.iv, Base64.DEFAULT)
|
|
||||||
cred.key?.let { prefs[KeePassKeys.KEY_FILE_PATH] = it.toString() }
|
|
||||||
}
|
|
||||||
return cred
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun hasCredentials(context: Context): Boolean {
|
|
||||||
val prefs = context.securePrefs.data.first()
|
|
||||||
return prefs[KeePassKeys.DB_FILE_PATH] != null &&
|
|
||||||
prefs[KeePassKeys.PASSWORD] != null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun packCredentials(dbFilePath:Uri?, password: String, keyFilePath: Uri?): CredentialResult.Success {
|
|
||||||
return CredentialResult.Success(dbFilePath, password, keyFilePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun loadCredentials(context: Context): CredentialResult {
|
|
||||||
val prefs = context.securePrefs.data.first { true }
|
|
||||||
val dbFilePath = prefs[KeePassKeys.DB_FILE_PATH] ?: return CredentialResult.NoData
|
|
||||||
val encryptedBase64 = prefs[KeePassKeys.PASSWORD] ?: return CredentialResult.NoData
|
|
||||||
val keyFilePath = prefs[KeePassKeys.KEY_FILE_PATH]
|
|
||||||
val cipher = showBiometricPrompt(context as FragmentActivity, false)
|
|
||||||
?: return CredentialResult.AuthFailed
|
|
||||||
val decrypted = cipher.doFinal(Base64.decode(encryptedBase64, Base64.DEFAULT))
|
|
||||||
return packCredentials(
|
|
||||||
dbFilePath.parseUri(),
|
|
||||||
String(decrypted, Charsets.UTF_8),
|
|
||||||
keyFilePath?.parseUri()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
suspend fun showBiometricPrompt(activity: FragmentActivity, enc: Boolean): Cipher? {
|
|
||||||
val prefs = activity.securePrefs.data.first()
|
|
||||||
return suspendCancellableCoroutine { cont ->
|
|
||||||
val executor = ContextCompat.getMainExecutor(activity)
|
|
||||||
val biometricPrompt = BiometricPrompt(
|
|
||||||
activity,
|
|
||||||
executor,
|
|
||||||
object : BiometricPrompt.AuthenticationCallback() {
|
|
||||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { cont.resume(result.cryptoObject?.cipher) {} }
|
|
||||||
override fun onAuthenticationError(code: Int, msg: CharSequence) { cont.resume(null) {} }
|
|
||||||
override fun onAuthenticationFailed() { cont.resume(null) {} }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
val iv = if(enc) null else prefs[KeePassKeys.IV]?.let { Base64.decode(it, Base64.DEFAULT) }
|
|
||||||
if (!enc && iv == null) { cont.resume(null) {} }
|
|
||||||
val cipher = getCipherForDecryption(getOrCreateBiometricKey(), iv)
|
|
||||||
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
|
||||||
.setTitle("Unlock KeePass")
|
|
||||||
.setSubtitle("Authenticate to access your KeePass database")
|
|
||||||
.setNegativeButtonText("Cancel")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun retrieveResponseFromChallenge(
|
|
||||||
hardwareKey: HardwareKey,
|
|
||||||
seed: ByteArray?,
|
|
||||||
): ByteArray {
|
|
||||||
val response: ByteArray = "".toByteArray()
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
50
app/src/main/java/net/helcel/fidelity/tools/CacheManager.kt
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun rmFidelity(idx: Int) {
|
||||||
|
data.removeAt(idx)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
22
app/src/main/java/net/helcel/fidelity/tools/ErrorToaster.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
package net.helcel.fidelity.tools
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import androidx.core.content.edit
|
|
||||||
import com.kunzisoft.keepass.database.element.Database
|
|
||||||
import com.kunzisoft.keepass.database.element.Field
|
|
||||||
import com.kunzisoft.keepass.database.element.Group
|
|
||||||
import com.kunzisoft.keepass.database.element.MasterCredential
|
|
||||||
import com.kunzisoft.keepass.database.element.binary.BinaryData
|
|
||||||
import com.kunzisoft.keepass.database.element.node.NodeIdUUID
|
|
||||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
|
||||||
import com.kunzisoft.keepass.hardware.HardwareKey
|
|
||||||
import com.kunzisoft.keepass.utils.getBinaryDir
|
|
||||||
import kotlinx.serialization.builtins.ListSerializer
|
|
||||||
import java.io.File
|
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
object FidelityKeepassFields {
|
|
||||||
const val FIDELITYFORMAT = "FidelityFormat"
|
|
||||||
const val FIDELITYCODE = "FidelityCode"
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class FidelityEntry(
|
|
||||||
val uid: String? = null,
|
|
||||||
val title: String = "",
|
|
||||||
val code: String = "",
|
|
||||||
val format: String = "",
|
|
||||||
val protected: Boolean = false,
|
|
||||||
|
|
||||||
val hidden: Boolean = false,
|
|
||||||
val pinned: Boolean = false,
|
|
||||||
val lastUse: Int = 0,
|
|
||||||
)
|
|
||||||
|
|
||||||
object FidelityRepository {
|
|
||||||
private var db: Database = Database()
|
|
||||||
private var binaryDir: File? = null
|
|
||||||
val entries = mutableStateListOf<FidelityEntry>()
|
|
||||||
val activeEntry = mutableStateOf(FidelityEntry())
|
|
||||||
|
|
||||||
|
|
||||||
fun getRoot(): Group? {
|
|
||||||
return db.rootGroup
|
|
||||||
}
|
|
||||||
|
|
||||||
fun start(ctx: Context, uri: Uri?, c: MasterCredential): Boolean {
|
|
||||||
if (binaryDir == null) binaryDir = ctx.getBinaryDir()
|
|
||||||
if (uri == null) return false
|
|
||||||
try {
|
|
||||||
val bitStream =
|
|
||||||
ByteArrayInputStream(ctx.contentResolver.openInputStream(uri)?.readBytes())
|
|
||||||
db.loadData(
|
|
||||||
bitStream, c,
|
|
||||||
{ hardwareKey, seed -> retrieveResponseFromChallenge(hardwareKey, seed) },
|
|
||||||
false, binaryDir!!,
|
|
||||||
{ BinaryData.canMemoryBeAllocatedInRAM(ctx, it) },
|
|
||||||
false, null
|
|
||||||
)
|
|
||||||
return true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println(e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun end(ctx: Context, uri: Uri?, c: MasterCredential): Boolean {
|
|
||||||
if (uri == null) return false
|
|
||||||
db.saveData(
|
|
||||||
File(binaryDir, db.binaryCache.hashCode().toString()),{ ctx.contentResolver.openOutputStream(uri) },
|
|
||||||
false, c, { hardwareKey, seed -> retrieveResponseFromChallenge(hardwareKey, seed) })
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun genCredentials(
|
|
||||||
ctx: Context,
|
|
||||||
cred: CredentialResult.Success,
|
|
||||||
hardwareKey: HardwareKey? = null
|
|
||||||
): MasterCredential {
|
|
||||||
return MasterCredential(
|
|
||||||
cred.password,
|
|
||||||
cred.key?.let { ctx.contentResolver.openInputStream(cred.key)?.readBytes() },
|
|
||||||
hardwareKey
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun importDB(context: Context) {
|
|
||||||
val seenID= arrayListOf<String>()
|
|
||||||
fun importDBRec(group: Group) {
|
|
||||||
group.getChildEntries().forEach {
|
|
||||||
val fields = it.getExtraFields()
|
|
||||||
val code = fields.firstOrNull { e -> e.name == FidelityKeepassFields.FIDELITYCODE }
|
|
||||||
val format =
|
|
||||||
fields.firstOrNull { e -> e.name == FidelityKeepassFields.FIDELITYFORMAT }
|
|
||||||
if (code == null || format == null) return@forEach
|
|
||||||
|
|
||||||
val newEntry = FidelityEntry(
|
|
||||||
uid=it.nodeId.id.toString(),
|
|
||||||
title=it.title,
|
|
||||||
code=code.protectedValue.stringValue,
|
|
||||||
format=format.protectedValue.stringValue,
|
|
||||||
protected=code.protectedValue.isProtected,
|
|
||||||
)
|
|
||||||
val idx = entries.indexOfFirst { e -> e.uid == newEntry.uid }
|
|
||||||
seenID.add(newEntry.uid!!)
|
|
||||||
if (idx >= 0) {
|
|
||||||
val oldEntry = entries[idx]
|
|
||||||
entries[idx] = newEntry.copy(
|
|
||||||
pinned = oldEntry.pinned,
|
|
||||||
hidden = oldEntry.hidden,
|
|
||||||
lastUse = oldEntry.lastUse
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
entries.add(newEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
group.getChildGroups().forEach { importDBRec(it) }
|
|
||||||
}
|
|
||||||
if (db.rootGroup != null)
|
|
||||||
importDBRec(db.rootGroup!!)
|
|
||||||
entries.removeAll { !seenID.contains(it.uid)}
|
|
||||||
val distinct = entries.distinctBy { it.uid }
|
|
||||||
entries.clear()
|
|
||||||
entries.addAll(distinct)
|
|
||||||
saveEntries(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveEntries(context: Context) {
|
|
||||||
val prefs = context.getSharedPreferences("fidelity_prefs", Context.MODE_PRIVATE)
|
|
||||||
prefs.edit { putString("entries", Json.encodeToString(
|
|
||||||
ListSerializer(FidelityEntry.serializer()),
|
|
||||||
entries
|
|
||||||
)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun loadEntries(context: Context) {
|
|
||||||
val prefs = context.getSharedPreferences("fidelity_prefs", Context.MODE_PRIVATE)
|
|
||||||
try {
|
|
||||||
val json = prefs.getString("entries", null) ?: return
|
|
||||||
val list = Json.decodeFromString(
|
|
||||||
ListSerializer(FidelityEntry.serializer()),
|
|
||||||
json
|
|
||||||
)
|
|
||||||
|
|
||||||
entries.clear()
|
|
||||||
entries.addAll(list)
|
|
||||||
}catch(_: Exception){
|
|
||||||
prefs.edit{ putString("entries",Json.encodeToString(
|
|
||||||
ListSerializer(FidelityEntry.serializer()),emptyList()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addEntry(ctx: Context, entry: FidelityEntry) {
|
|
||||||
val dbEntry = db.getEntryById(NodeIdUUID(UUID.fromString(entry.uid))) ?: db.createEntry()
|
|
||||||
val dbParent = db.getGroupById(NodeIdUUID(UUID.fromString(entry.uid)))
|
|
||||||
dbEntry?.apply {
|
|
||||||
putExtraField(
|
|
||||||
Field(
|
|
||||||
FidelityKeepassFields.FIDELITYCODE,
|
|
||||||
ProtectedString(entry.protected, entry.code)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
putExtraField(
|
|
||||||
Field(
|
|
||||||
FidelityKeepassFields.FIDELITYFORMAT,
|
|
||||||
ProtectedString(string= entry.format)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if(dbParent!=null) title = entry.title
|
|
||||||
dbParent?.addChildEntry(dbEntry)
|
|
||||||
}
|
|
||||||
entries.removeIf {it.uid == entry.uid}
|
|
||||||
entries.add(entry.copy(uid=dbEntry?.nodeId?.id.toString()))
|
|
||||||
saveEntries(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
101
app/src/main/java/net/helcel/fidelity/tools/KeepassWrapper.kt
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
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()
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
21
app/src/main/res/drawable/heart.xml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="72dp"
|
||||||
|
android:height="72dp"
|
||||||
|
android:viewportWidth="52"
|
||||||
|
android:viewportHeight="52">
|
||||||
|
|
||||||
|
<group
|
||||||
|
android:translateX="-10"
|
||||||
|
android:translateY="-10">
|
||||||
|
<path
|
||||||
|
android:fillColor="#EA5A47"
|
||||||
|
android:pathData="M60.7,26.2c0,-7.2 -5.9,-13.1 -13.1,-13.1c-5,0 -9.3,2.8 -11.5,6.9c-2.2,-4.1 -6.6,-6.9 -11.5,-6.9c-7.2,0 -13.1,5.9 -13.1,13.1c0,3.1 1.1,6 2.9,8.2l0,0l21.8,27l21.8,-27l0,0C59.6,32.2 60.7,29.4 60.7,26.2z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M60.7,26.2c0,-7.2 -5.9,-13.1 -13.1,-13.1c-5,0 -9.3,2.8 -11.5,6.9c-2.2,-4.1 -6.6,-6.9 -11.5,-6.9c-7.2,0 -13.1,5.9 -13.1,13.1c0,3.1 1.1,6 2.9,8.2l0,0l21.8,27l21.8,-27l0,0C59.6,32.2 60.7,29.4 60.7,26.2z"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="128"
|
|
||||||
android:viewportHeight="128">
|
|
||||||
<group android:scaleX="1.2833333"
|
|
||||||
android:scaleY="1.2833333"
|
|
||||||
android:translateX="-16.612345"
|
|
||||||
android:translateY="-16.612345">
|
|
||||||
<group
|
|
||||||
android:translateX="34"
|
|
||||||
android:translateY="26">
|
|
||||||
<group
|
|
||||||
android:scaleX="0.8"
|
|
||||||
android:scaleY="1.0"
|
|
||||||
android:translateX="0"
|
|
||||||
android:translateY="0">
|
|
||||||
<path
|
|
||||||
android:fillColor="@color/blue"
|
|
||||||
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>
|
|
||||||
<group
|
|
||||||
android:scaleX="0.4"
|
|
||||||
android:scaleY="0.5"
|
|
||||||
android:translateX="27"
|
|
||||||
android:translateY="15.75">
|
|
||||||
<path
|
|
||||||
android:fillColor="@color/red"
|
|
||||||
android:pathData="M46.5,56l-10,-11l-10,11l0,-45l20,0z"
|
|
||||||
android:strokeLineCap="round"
|
|
||||||
android:strokeLineJoin="round" />
|
|
||||||
<path
|
|
||||||
android:fillColor="@color/red2"
|
|
||||||
android:fillAlpha="1.0"
|
|
||||||
android:pathData="M41.5,11l0,39l5,6l0,-45z"
|
|
||||||
android:strokeColor="#00000000" />
|
|
||||||
|
|
||||||
</group>
|
|
||||||
<group
|
|
||||||
android:scaleX="0.75"
|
|
||||||
android:scaleY="0.75"
|
|
||||||
android:translateX="6"
|
|
||||||
android:translateY="10">
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M9,21V52 M12,21V52 M20,21V50 M28,21V50 M15,50V21H17V50H15 M23,50V21H25V50H23 M31,50V21H32V50H31 M35,21V52 M38,21V52"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:strokeColor="#000"
|
|
||||||
android:strokeLineCap="round"
|
|
||||||
android:strokeLineJoin="round" />
|
|
||||||
</group>
|
|
||||||
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
</vector>
|
|
||||||
31
app/src/main/res/drawable/key.xml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="72dp"
|
||||||
|
android:height="72dp"
|
||||||
|
android:viewportWidth="52"
|
||||||
|
android:viewportHeight="52">
|
||||||
|
<group
|
||||||
|
android:translateX="-10"
|
||||||
|
android:translateY="-10">
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M30.735,34.656l-16.432,16.026l0,7.24l7.565,0l0,-4.637l5.125,0l0,-5.857l5.098,0l2.404,-2.404l0,-4.358l2.015,0"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M48.52,23.998m-3.952,0a3.952,3.952 0,1 1,7.904 0a3.952,3.952 0,1 1,-7.904 0"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M34.226,31.178c-1.43,-4.238 -0.347,-9.221 3.18,-12.695c4.845,-4.772 12.465,-4.889 17.022,-0.263s4.322,12.244 -0.522,17.016c-3.917,3.858 -9.648,4.674 -14.108,2.4"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
31
app/src/main/res/drawable/locked.xml
Normal file
@@ -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>
|
||||||
21
app/src/main/res/drawable/logo.xml
Normal 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>
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="200dp"
|
|
||||||
android:height="200dp"
|
|
||||||
android:viewportWidth="128"
|
|
||||||
android:viewportHeight="128">
|
|
||||||
<group
|
|
||||||
android:translateX="34"
|
|
||||||
android:translateY="26">
|
|
||||||
<group
|
|
||||||
android:scaleX="0.8"
|
|
||||||
android:scaleY="1.0"
|
|
||||||
android:translateX="0"
|
|
||||||
android:translateY="0">
|
|
||||||
<path
|
|
||||||
android:fillColor="@color/blue"
|
|
||||||
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>
|
|
||||||
<group
|
|
||||||
android:scaleX="0.4"
|
|
||||||
android:scaleY="0.5"
|
|
||||||
android:translateX="27"
|
|
||||||
android:translateY="15.75">
|
|
||||||
<path
|
|
||||||
android:fillColor="@color/red"
|
|
||||||
android:pathData="M46.5,56l-10,-11l-10,11l0,-45l20,0z"
|
|
||||||
android:strokeLineCap="round"
|
|
||||||
android:strokeLineJoin="round" />
|
|
||||||
<path
|
|
||||||
android:fillColor="@color/red2"
|
|
||||||
android:fillAlpha="1.0"
|
|
||||||
android:pathData="M41.5,11l0,39l5,6l0,-45z"
|
|
||||||
android:strokeColor="#00000000" />
|
|
||||||
|
|
||||||
</group>
|
|
||||||
<group
|
|
||||||
android:scaleX="0.75"
|
|
||||||
android:scaleY="0.75"
|
|
||||||
android:translateX="6"
|
|
||||||
android:translateY="10">
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M9,21V52 M12,21V52 M20,21V50 M28,21V50 M15,50V21H17V50H15 M23,50V21H25V50H23 M31,50V21H32V50H31 M35,21V52 M38,21V52"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:strokeColor="#000"
|
|
||||||
android:strokeLineCap="round"
|
|
||||||
android:strokeLineJoin="round" />
|
|
||||||
</group>
|
|
||||||
|
|
||||||
</group>
|
|
||||||
</vector>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="72dp"
|
|
||||||
android:height="72dp"
|
|
||||||
android:viewportWidth="58"
|
|
||||||
android:viewportHeight="58">
|
|
||||||
<group android:translateX="-10" android:translateY="-8">
|
|
||||||
<path
|
|
||||||
android:pathData="m57.008,20.304v-3.356l-27.338,-0.002c-0.198,0 -0.359,-0.165 -0.359,-0.368l-0.069,-1.517c-0.116,-1.788 -1.34,-3.003 -2.997,-3.003h-11.287c-1.657,0 -3,1.343 -3,3v40.943"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="m17.027,55.568c-0.59,1.954 -2.972,4.139 -4.646,4.394l44.665,0.011c1.657,0 2.323,-0.439 3,-3s7,-31.657 7,-31.657c0,-0.552 -0.448,-1 -1,-1H24.965c-0.552,0 -1,0.448 -1,1 0,0 -6.348,28.299 -6.938,30.253Z"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
</group>
|
|
||||||
</vector>
|
|
||||||
@@ -1,302 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="72dp"
|
|
||||||
android:height="72dp"
|
|
||||||
android:viewportWidth="58"
|
|
||||||
android:viewportHeight="58">
|
|
||||||
<group android:translateX="-8" android:translateY="-8">
|
|
||||||
<path
|
|
||||||
android:pathData="M20,20h4v4h-4z"
|
|
||||||
android:fillColor="#000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M20,48h4v4h-4z"
|
|
||||||
android:fillColor="#000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M48,20h4v4h-4z"
|
|
||||||
android:fillColor="#000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M18,40m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
|
|
||||||
android:fillColor="#000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M16,38m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
|
|
||||||
android:fillColor="#000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M20,38m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
|
|
||||||
android:fillColor="#000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M34,46m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
|
|
||||||
android:fillColor="#000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M40,38m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
|
|
||||||
android:fillColor="#000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M40,28m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
|
|
||||||
android:fillColor="#000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M32,16m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
|
|
||||||
android:fillColor="#000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M46,32m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
|
|
||||||
android:fillColor="#000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M52,32m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
|
|
||||||
android:fillColor="#000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M52,44m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
|
|
||||||
android:fillColor="#000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M54,48m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
|
|
||||||
android:fillColor="#000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M56,56m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
|
|
||||||
android:fillColor="#000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M32,56m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
|
|
||||||
android:fillColor="#000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M44,56m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
|
|
||||||
android:fillColor="#000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M46,54m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
|
|
||||||
android:fillColor="#000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M44,52m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
|
|
||||||
android:fillColor="#000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M16,32m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
|
|
||||||
android:fillColor="#000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M40,54m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
|
|
||||||
android:fillColor="#000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M12,12h48v48h-48z"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M16,16h12v12h-12z"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M20,20h4v4h-4z"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M16,44h12v12h-12z"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M20,48h4v4h-4z"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M44,16h12v12h-12z"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M48,20h4v4h-4z"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M18,36V34H26"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M20,34V32"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M24,34V40"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M24,38H26"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M38,32V30"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M56,34H54"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M42,42H44V40"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M28,32H30"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M34,32H40"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M38,16V20H36V28"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M36,26H32V28"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M36,20H32"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M36,22H34V18"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M28,36H36"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M30,36V40H28"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M34,36V38"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M32,44V42H38V48H42V46H50V56"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M36,40V44H42M46,40H42V48H44"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M48,34V38H50V42H48V46"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M50,38V36H52"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M52,50H48V52"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M32,52H34V54H36V50"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M56,32V38H54"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M44,36V34"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M56,42V44"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M54,52H56"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M40,22V24"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
</group>
|
|
||||||
</vector>
|
|
||||||
18
app/src/main/res/layout/act_main.xml
Normal 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>
|
||||||
106
app/src/main/res/layout/frag_create_entry.xml
Normal 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>
|
||||||
83
app/src/main/res/layout/frag_launcher.xml
Normal 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>
|
||||||
22
app/src/main/res/layout/frag_scanner.xml
Normal 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>
|
||||||
39
app/src/main/res/layout/frag_view_entry.xml
Normal 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>
|
||||||
10
app/src/main/res/layout/list_item_dropdown.xml
Normal 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" />
|
||||||
30
app/src/main/res/layout/list_item_fidelity.xml
Normal 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>
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
|
||||||
</adaptive-icon>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
|
||||||
</adaptive-icon>
|
|
||||||
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 10 KiB |
@@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="black">#FF000000</color>
|
|
||||||
<color name="darkgray">#FF0C1D2E</color>
|
|
||||||
<color name="gray">#425F7C</color>
|
|
||||||
<color name="lightgray">#FF93A9BE</color>
|
|
||||||
<color name="white">#FFF0F3F7</color>
|
|
||||||
|
|
||||||
<color name="blue">#7DB9F5</color>
|
|
||||||
<color name="blue2">#3193F5</color>
|
|
||||||
<color name="red">#F57D7D</color>
|
|
||||||
<color name="red2">#F53131</color>
|
|
||||||
|
|
||||||
</resources>
|
|
||||||
3
app/src/main/res/values/dimens.xml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<resources>
|
||||||
|
|
||||||
|
</resources>
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="ic_launcher_background">#0C1D2E</color>
|
|
||||||
</resources>
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<string name="key_theme">App theme</string>
|
<string name="kp2aplugin_title" tools:keep="@string/kp2aplugin_title">Fidelity</string>
|
||||||
<string name="system">System</string>
|
<string name="kp2aplugin_shortdesc">Stores and Displays fidelity and other cards</string>
|
||||||
<string name="light">Light</string>
|
<string name="kp2aplugin_author" tools:keep="@string/kp2aplugin_author">[soraefir](soraefir)</string>
|
||||||
<string name="dark">Dark</string>
|
|
||||||
<string name="key_stats">Statistics</string>
|
<string name="app_name">Keepass Fidelity</string>
|
||||||
|
|
||||||
<string name="barcode_preview">barcode preview</string>
|
<string name="barcode_preview">barcode preview</string>
|
||||||
<string name="expand">Expand</string>
|
<string name="expand">Expand</string>
|
||||||
@@ -15,20 +15,15 @@
|
|||||||
<string name="code">Code</string>
|
<string name="code">Code</string>
|
||||||
<string name="format">Format</string>
|
<string name="format">Format</string>
|
||||||
<string name="save">Save</string>
|
<string name="save">Save</string>
|
||||||
<string name="open">Open</string>
|
|
||||||
<string-array name="format_array">
|
<string-array name="format_array">
|
||||||
|
<item>CODE_128</item>
|
||||||
<item>CODE_39</item>
|
<item>CODE_39</item>
|
||||||
<item>CODE_93</item>
|
<item>CODE_93</item>
|
||||||
<item>CODE_128</item>
|
|
||||||
<item>EAN_8</item>
|
<item>EAN_8</item>
|
||||||
<item>EAN_13</item>
|
<item>EAN_13</item>
|
||||||
<item>CODE_QR</item>
|
<item>CODE_QR</item>
|
||||||
<item>UPC_A</item>
|
<item>UPC_A</item>
|
||||||
<item>UPC_E</item>
|
<item>UPC_E</item>
|
||||||
<item>PDF_417</item>
|
<item>PDF_417</item>
|
||||||
<item>AZTEC</item>
|
|
||||||
<item>CODABAR</item>
|
|
||||||
<item>DATA_MATRIX</item>
|
|
||||||
<item>ITF</item>
|
|
||||||
</string-array>
|
</string-array>
|
||||||
</resources>
|
</resources>
|
||||||
3
app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<resources>
|
||||||
|
|
||||||
|
</resources>
|
||||||
9
app/src/main/res/values/themes.xml
Normal 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>
|
||||||
15
build.gradle
@@ -1,16 +1,7 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
buildscript {
|
|
||||||
// ext.kotlin_version = '1.8.20'
|
|
||||||
// ext.android_core_version = '1.10.1'
|
|
||||||
// ext.android_appcompat_version = '1.6.1'
|
|
||||||
// ext.android_material_version = '1.9.0'
|
|
||||||
ext.android_test_version = '1.5.2'
|
|
||||||
}
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application' version '8.13.0' apply false
|
id 'com.android.application' version '8.3.0' apply false
|
||||||
id 'com.android.library' version '8.13.0' apply false
|
id 'com.android.library' version '8.3.1' apply false
|
||||||
id 'org.jetbrains.kotlin.android' version '2.2.21' apply false
|
id 'org.jetbrains.kotlin.android' version '1.9.23' apply false
|
||||||
id 'com.autonomousapps.dependency-analysis' version '3.2.0' apply true
|
|
||||||
}
|
}
|
||||||
1
external/KeePassDX
vendored
@@ -15,7 +15,7 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
|||||||
# Android operating system, and which are packaged with your app's APK
|
# Android operating system, and which are packaged with your app's APK
|
||||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=false
|
android.enableJetifier=true
|
||||||
# Kotlin code style for this project: "official" or "obsolete":
|
# Kotlin code style for this project: "official" or "obsolete":
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
# Enables namespacing of each library's R class so that its R class includes only the
|
# Enables namespacing of each library's R class so that its R class includes only the
|
||||||
|
|||||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
15
gradlew
vendored
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
#
|
#
|
||||||
# Copyright © 2015 the original authors.
|
# Copyright © 2015-2021 the original authors.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -15,8 +15,6 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
#
|
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
#
|
#
|
||||||
@@ -57,7 +55,7 @@
|
|||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (3) This script is generated from the Groovy template
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
# within the Gradle project.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
@@ -86,7 +84,7 @@ done
|
|||||||
# shellcheck disable=SC2034
|
# shellcheck disable=SC2034
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
@@ -114,6 +112,7 @@ case "$( uname )" in #(
|
|||||||
NONSTOP* ) nonstop=true ;;
|
NONSTOP* ) nonstop=true ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
# Determine the Java command to use to start the JVM.
|
||||||
@@ -171,6 +170,7 @@ fi
|
|||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
if "$cygwin" || "$msys" ; then
|
if "$cygwin" || "$msys" ; then
|
||||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
@@ -203,14 +203,15 @@ fi
|
|||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Collect all arguments for the java command:
|
# Collect all arguments for the java command:
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
# and any embedded shellness will be escaped.
|
# and any embedded shellness will be escaped.
|
||||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
# treated as '${Hostname}' itself on the command line.
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
set -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
"$@"
|
"$@"
|
||||||
|
|
||||||
# Stop when "xargs" is not available.
|
# Stop when "xargs" is not available.
|
||||||
|
|||||||
5
gradlew.bat
vendored
@@ -13,8 +13,6 @@
|
|||||||
@rem See the License for the specific language governing permissions and
|
@rem See the License for the specific language governing permissions and
|
||||||
@rem limitations under the License.
|
@rem limitations under the License.
|
||||||
@rem
|
@rem
|
||||||
@rem SPDX-License-Identifier: Apache-2.0
|
|
||||||
@rem
|
|
||||||
|
|
||||||
@if "%DEBUG%"=="" @echo off
|
@if "%DEBUG%"=="" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@@ -70,10 +68,11 @@ goto fail
|
|||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute Gradle
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
:end
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<p><i>Keepass-Fidelity</i> adds an interface to view/save barcodes (QR included) to Keepass through the plugin interface of the Keepass2Android app.</p><p><br></p><ul><li><b>Launcher:</b> view and launch recent entries (a per entry flag can disable this behaviour)</li><li><b>View:</b> view entries from the history or queried from Keepass2Android</li><li><b>Create:</b> add entries from the camera, an image of by filling out a form. The entry is then created in the Keepass2Android app</li><li><b>Data:</b> the app uses the following data Title (entry name), barcode type (QR, UPC, ...), barcode content (number/text content) and a "secure" flag (enable/disable caching the entry).</li></ul>
|
|
||||||
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 25 KiB |
@@ -1 +0,0 @@
|
|||||||
<ul><li><b>CAMERA:</b> necessary for importing barcodes from camera</li><li><b>READ_MEDIA_VISUAL_USER_SELECTED:</b> necessary for the importing barcode from images</li></ul>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
Fidelity (Membership/Loyalty) Card plugin for Keepass2Android
|
|
||||||
@@ -14,11 +14,5 @@ dependencyResolutionManagement {
|
|||||||
maven { url 'https://jitpack.io' }
|
maven { url 'https://jitpack.io' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
include(":database")
|
rootProject.name = "BeenDroid"
|
||||||
project(":database").projectDir = file("external/KeePassDX/database")
|
|
||||||
|
|
||||||
include(":crypto")
|
|
||||||
project(":crypto").projectDir = file("external/KeePassDX/crypto")
|
|
||||||
|
|
||||||
rootProject.name = "Fidelity"
|
|
||||||
include ':app'
|
include ':app'
|
||||||
|
|||||||