Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
386f882944 | ||
|
|
65979cbddd | ||
|
|
26747d709f | ||
|
|
22b6a9f66e | ||
|
|
ded532c523 | ||
|
|
17c49a4a27 | ||
|
|
7ff8db0880 | ||
|
|
d7837f5795 | ||
|
|
1f5c77e802 | ||
|
|
a4da488a41 | ||
|
|
d177c08420 | ||
|
|
a6123ed233 | ||
|
|
2ebb376a8b | ||
|
|
6cac7e8ead | ||
|
|
59f6c9f654 | ||
|
|
f0c1d2a936 | ||
|
|
e7aff150f6 | ||
|
|
10755555ff | ||
|
|
a78a85df2c | ||
|
|
de9c490429 | ||
|
|
77e686af75 | ||
|
|
0ae13f22b6 | ||
|
|
bedfcdec14 | ||
|
|
a6ba968ea8 | ||
|
|
5301da5900 | ||
|
|
cb7d64991b | ||
|
|
6194dc29d5 | ||
|
|
3fa39b9fe0 | ||
|
|
f37b846241 | ||
|
|
8ce33c0071 | ||
|
|
e6823386b0 | ||
|
|
3e57a59e16 | ||
|
|
758220200e | ||
|
|
b3584392b3 | ||
|
|
3bb26f06d1 | ||
|
|
fe4a021cd0 | ||
|
|
0de5565b78 | ||
|
|
d9a9acd8c1 | ||
|
|
1be31f71ed | ||
|
|
8859c61119 | ||
|
|
c52778f59d | ||
|
|
5bde3947e3 | ||
|
|
08be31814f | ||
|
|
c5db04b84e | ||
|
|
9dccc6a7f5 | ||
|
|
a5deab656b | ||
|
|
b773dfa392 | ||
|
|
7b5106f09d | ||
|
|
16ce45093e | ||
|
|
e6345f09b3 | ||
|
|
de00ed779a | ||
|
|
d740aa121b | ||
|
|
38c40d36bb | ||
|
|
3e51f70c7c | ||
|
|
502702c7a5 | ||
|
|
96ff9b6239 | ||
|
|
afd6c74c80 | ||
|
|
5ffea2edd8 | ||
|
|
78d62395e2 | ||
|
|
a5d1465528 | ||
|
|
14517bad17 | ||
|
|
7a5e07fb3c | ||
|
|
c6b3ca08f4 | ||
|
|
73047cdee6 | ||
|
|
bec4aa3120 | ||
|
|
eaaaaf14c2 | ||
|
|
971742aff4 | ||
|
|
28915eacdb | ||
|
|
a2c1d11b5b | ||
|
|
e9dd396f34 | ||
|
|
c7edba746c | ||
|
|
34c8dfd169 | ||
|
|
459980899f | ||
|
|
2e0693cb45 | ||
|
|
187825932e | ||
|
|
a80802b4bc | ||
|
|
aa589a5d53 | ||
|
|
6a42e54f0b | ||
|
|
e134b18bba | ||
|
|
a112484dcb | ||
|
|
4f36030e38 | ||
|
|
90c2c03d12 | ||
|
|
3f853be51a | ||
|
|
835a348fad | ||
|
|
198a35fc15 | ||
|
|
6be1b25c38 | ||
|
|
ac632e3aac | ||
|
|
ddc5a14558 | ||
|
|
8b1ffe3e5a | ||
|
|
e9ba66848f | ||
|
|
7bdef27a50 | ||
|
|
ccd16a0f95 | ||
|
|
a385678de1 | ||
|
|
43c597f6fb | ||
|
|
ecccf34418 | ||
|
|
4027ce2cc6 | ||
|
|
209e6c5c30 | ||
|
|
5e29f3fbe9 | ||
|
|
ba3d63277a | ||
|
|
fe34b7209d | ||
|
|
0efdca7b4a | ||
|
|
827fbedc1f | ||
|
|
107b5bafd4 | ||
|
|
19a71480fa | ||
|
|
909e97a591 | ||
|
|
bc2be96871 | ||
|
|
d834621d99 | ||
|
|
ad83be1a60 | ||
|
|
d65ea02b49 | ||
|
|
c8d1357ceb | ||
|
|
4c05d046e6 | ||
|
|
4423ee62c8 | ||
|
|
bc69f99f8b | ||
|
|
6b088031fd | ||
|
|
b27d03e9c0 | ||
|
|
9a13838f28 | ||
|
|
cce2a460f6 | ||
|
|
36352ecde2 | ||
|
|
9a0bc324cb | ||
|
|
6dabafd6a3 | ||
|
|
dde4b1e0bd | ||
|
|
8a8c7bd1ff | ||
|
|
a2aed42e25 | ||
|
|
a38b7b647f | ||
|
|
76e82ed842 | ||
|
|
fd54bc604e | ||
|
|
59b62ecbc4 | ||
|
|
de6a2863f3 | ||
|
|
4b2eb80612 | ||
|
|
2de40d2277 | ||
|
|
7673b7cb0d | ||
|
|
097c5e4453 |
@@ -1,5 +1,83 @@
|
||||
# Change Log
|
||||
|
||||
## 1.73.1
|
||||
* Update ZXingCpp and support Telepen format
|
||||
* Update vdstools
|
||||
* Fix stale history row highlighting
|
||||
|
||||
## 1.73.0
|
||||
* Support Material You and Dynamic Colors
|
||||
* Strip common tracking parameters before opening scanned URLs
|
||||
* Parse and display otpauth:// code fields
|
||||
* Update Italian and Latvian language
|
||||
* Update BouncyCastle and vdstools library
|
||||
* Fix deeplink argument parsing
|
||||
|
||||
## 1.72.2
|
||||
* Import/export preferences using QR codes
|
||||
* Add predefined expert and simple profiles
|
||||
* Update ZXingCpp library
|
||||
* Fix beeping on scans
|
||||
|
||||
## 1.72.1
|
||||
* Fix migrating DATA_BAR entries
|
||||
* Italian language update
|
||||
|
||||
## 1.72.0
|
||||
* Improve result screen
|
||||
* Update vdstools
|
||||
* Fix camera initialization
|
||||
|
||||
## 1.71.0
|
||||
* Parse VDS and IDB barcodes
|
||||
* Restrict scanning to horizontal crosshair if set
|
||||
* Include barcode format in history search
|
||||
* Add a dedicated barcode icon for PDF417
|
||||
* Add barcode format descriptions
|
||||
* Fix handling deep links
|
||||
* Fix detecting web URLs
|
||||
|
||||
## 1.70.0
|
||||
* Add barcode format icons in history
|
||||
* Add beep audio stream preference
|
||||
* Parse EPC-QR codes and show the data
|
||||
* Improve layout of decode screen
|
||||
* Fix parsing first field of subfile in IDLParser
|
||||
* Use quiet zone from ZXingCpp
|
||||
* Italian language update
|
||||
|
||||
## 1.69.1
|
||||
* Update translations
|
||||
|
||||
## 1.69.0
|
||||
* Add an option to show crosshairs
|
||||
* Add preference for codes that should not be saved
|
||||
* Add option to restrict searching to a specific format
|
||||
* Pin history items
|
||||
* Improve preference icons
|
||||
* Optimize vector drawables
|
||||
* Fix beeping when phone is set to Vibrate mode
|
||||
|
||||
## 1.68.7
|
||||
* Add icons to preferences screen
|
||||
* Update history listing on resume
|
||||
* Fix quiet zone for recreated barcodes
|
||||
* Fix automatic screen brightening
|
||||
* Migrate to CameraX
|
||||
|
||||
## 1.68.6
|
||||
* Update ZXingCpp
|
||||
* Ask for confirmation before deleting a scan from the result screen
|
||||
|
||||
## 1.68.5
|
||||
* Downgrade ZXingCpp for compatibilty
|
||||
|
||||
## 1.68.4
|
||||
* Fix generating 1D barcodes
|
||||
|
||||
## 1.68.3
|
||||
* Stability improvement
|
||||
|
||||
## 1.68.2
|
||||
* Fix barcode regeneration
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ ZXing can read the following barcode formats:
|
||||
* [ITF][itf]
|
||||
* [MAXICODE][maxicode] (partial)
|
||||
* [PDF417][pdf417]
|
||||
* [Telepen][telepen]
|
||||
* [QR CODE][qr_code]
|
||||
* [Micro QR Code][micro_qr_code]
|
||||
* [rMQR Code][rmqr_code]
|
||||
@@ -207,6 +208,7 @@ In order to set up Bluetooth forwarding to a computer:
|
||||
[itf]: https://en.wikipedia.org/wiki/Interleaved_2_of_5
|
||||
[maxicode]: https://en.wikipedia.org/wiki/MaxiCode
|
||||
[pdf417]: https://en.wikipedia.org/wiki/PDF417
|
||||
[telepen]: https://en.wikipedia.org/wiki/Telepen
|
||||
[qr_code]: https://en.wikipedia.org/wiki/QR_code
|
||||
[micro_qr_code]: https://en.wikipedia.org/wiki/QR_code#Micro_QR_code
|
||||
[rmqr_code]: https://www.qrcode.com/en/codes/rmqr.html
|
||||
|
||||
+21
-11
@@ -5,11 +5,11 @@ android {
|
||||
compileSdk sdk_version
|
||||
|
||||
defaultConfig {
|
||||
minSdk 9
|
||||
minSdk 21
|
||||
targetSdk sdk_version
|
||||
|
||||
versionCode 156
|
||||
versionName '1.68.2'
|
||||
versionCode 170
|
||||
versionName '1.73.1'
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@@ -67,20 +67,30 @@ android {
|
||||
dependencies {
|
||||
// Kotlin
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.8.0'
|
||||
implementation "androidx.core:core-ktx:1.17.0" // Keep for minSDK 21.
|
||||
|
||||
// Support
|
||||
implementation "com.android.support:appcompat-v7:$support_version"
|
||||
implementation "com.android.support:design:$support_version"
|
||||
implementation "com.android.support:preference-v7:$support_version"
|
||||
implementation "com.android.support:preference-v14:$support_version"
|
||||
implementation "androidx.appcompat:appcompat:1.7.1"
|
||||
implementation "com.google.android.material:material:1.13.0" // Keep for minSDK 21.
|
||||
implementation "androidx.preference:preference:1.2.1"
|
||||
|
||||
// External
|
||||
implementation 'com.github.markusfisch:CameraView:1.10.0'
|
||||
implementation 'com.github.markusfisch:ScalingImageView:1.4.1'
|
||||
implementation 'com.github.markusfisch:zxing-cpp:v3.0.2.0'
|
||||
var camerax_version = '1.4.2' // Keep for minSDK 21.
|
||||
implementation "androidx.camera:camera-core:$camerax_version"
|
||||
implementation "androidx.camera:camera-camera2:$camerax_version"
|
||||
implementation "androidx.camera:camera-lifecycle:$camerax_version"
|
||||
implementation "androidx.camera:camera-view:$camerax_version"
|
||||
implementation 'com.github.markusfisch:ScalingImageView:1.4.3'
|
||||
implementation 'com.github.markusfisch:zxing-cpp:v3.0.2.6'
|
||||
implementation 'de.tsenger:vdstools:0.17.0'
|
||||
implementation 'org.bouncycastle:bcpkix-jdk15to18:1.84'
|
||||
|
||||
// Testing
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.robolectric:robolectric:4.16.1'
|
||||
|
||||
// Debugging
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14'
|
||||
}
|
||||
|
||||
Binary file not shown.
Vendored
+2
@@ -3,3 +3,5 @@
|
||||
-dontwarn android.support.v8.**
|
||||
|
||||
-keep class android.support.v7.widget.SearchView { *; }
|
||||
|
||||
-keep class de.tsenger.vdstools.** { *; }
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<uses-feature android:name="android.hardware.camera"/>
|
||||
<uses-feature android:name="android.hardware.camera.autofocus"
|
||||
android:required="false" />
|
||||
<uses-sdk tools:overrideLibrary="android.support.v14.preference"/>
|
||||
<uses-sdk tools:overrideLibrary="androidx.preference"/>
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
@@ -112,7 +112,13 @@
|
||||
android:name=".activity.WelcomeActivity"
|
||||
android:exported="true"/>
|
||||
<activity
|
||||
android:name=".activity.MainActivity"
|
||||
android:name=".activity.PreferencesActivity"
|
||||
android:exported="true"/>
|
||||
<activity
|
||||
android:name=".activity.HistoryActivity"
|
||||
android:exported="true"/>
|
||||
<activity
|
||||
android:name=".activity.EncodeActivity"
|
||||
android:exported="true">
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
@@ -130,6 +136,24 @@
|
||||
<data android:scheme="binaryeye" android:host="encode"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activity.DecodeActivity"
|
||||
android:exported="false"/>
|
||||
<activity
|
||||
android:name=".activity.BarcodeActivity"
|
||||
android:exported="false"/>
|
||||
<activity
|
||||
android:name=".activity.ProfilesActivity"
|
||||
android:exported="false"/>
|
||||
<activity
|
||||
android:name=".activity.AutomatedActionsActivity"
|
||||
android:exported="false"/>
|
||||
<activity
|
||||
android:name=".activity.IgnoreCodesActivity"
|
||||
android:exported="false"/>
|
||||
<activity
|
||||
android:name=".activity.NetworkSuggestionsActivity"
|
||||
android:exported="false"/>
|
||||
<service
|
||||
android:name=".service.ScanTileService"
|
||||
android:icon="@drawable/ic_action_scan"
|
||||
@@ -142,7 +166,7 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
|
||||
Binary file not shown.
@@ -2,15 +2,15 @@ package de.markusfisch.android.binaryeye.actions
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import de.markusfisch.android.binaryeye.content.execShareIntent
|
||||
import de.markusfisch.android.binaryeye.content.openUrl
|
||||
import de.markusfisch.android.binaryeye.content.startIntentOrToast
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
|
||||
abstract class Action {
|
||||
abstract val iconResId: Int
|
||||
abstract val titleResId: Int
|
||||
|
||||
var fired: Boolean = false
|
||||
var executed: Boolean = false
|
||||
|
||||
abstract fun canExecuteOn(data: ByteArray): Boolean
|
||||
abstract suspend fun execute(context: Context, data: ByteArray)
|
||||
@@ -24,7 +24,7 @@ abstract class IntentAction : Action() {
|
||||
if (intent == null) {
|
||||
context.toast(errorMsg)
|
||||
} else {
|
||||
fired = context.execShareIntent(intent)
|
||||
executed = context.startIntentOrToast(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,6 @@ abstract class SchemeAction : Action() {
|
||||
}
|
||||
|
||||
final override suspend fun execute(context: Context, data: ByteArray) {
|
||||
fired = context.openUrl(String(data))
|
||||
executed = context.openUrl(String(data))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package de.markusfisch.android.binaryeye.actions.mail
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||
|
||||
@@ -24,7 +24,7 @@ object MailAction : IntentAction() {
|
||||
)?.groupValues?.get(1) ?: return null
|
||||
return Intent(
|
||||
Intent.ACTION_SENDTO,
|
||||
Uri.parse("mailto:$mailWithMessage")
|
||||
"mailto:$mailWithMessage".toUri()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package de.markusfisch.android.binaryeye.actions.mail
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||
|
||||
@@ -26,7 +26,7 @@ object MatMsgAction : IntentAction() {
|
||||
}
|
||||
return Intent(
|
||||
Intent.ACTION_SENDTO,
|
||||
Uri.parse("mailto:")
|
||||
"mailto:".toUri()
|
||||
).apply {
|
||||
mm.to?.let {
|
||||
putExtra(Intent.EXTRA_EMAIL, arrayOf(mm.to))
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ object OpenOrSearchAction : Action() {
|
||||
override fun canExecuteOn(data: ByteArray): Boolean = false
|
||||
|
||||
override suspend fun execute(context: Context, data: ByteArray) {
|
||||
fired = openUrlOrSearch(context, String(data))
|
||||
executed = openUrlOrSearch(context, String(data))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ package de.markusfisch.android.binaryeye.actions.sms
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||
|
||||
@@ -27,7 +27,7 @@ object SmsAction : IntentAction() {
|
||||
} ?: return null
|
||||
return Intent(
|
||||
Intent.ACTION_SENDTO,
|
||||
Uri.parse("smsto:$number")
|
||||
"smsto:$number".toUri()
|
||||
).apply {
|
||||
if (message.isNotEmpty()) {
|
||||
putExtra("sms_body", message)
|
||||
|
||||
+27
-30
@@ -3,7 +3,6 @@ package de.markusfisch.android.binaryeye.actions.vtype.vcard
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.provider.ContactsContract
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||
@@ -98,38 +97,36 @@ object VCardAction : IntentAction() {
|
||||
}
|
||||
putExtra(ContactsContract.Intents.Insert.POSTAL, addr.value.locationFormat)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
val urls = info["URL"]?.mapNotNull {
|
||||
it.value.takeIf { url -> url.isNotBlank() }?.let { url ->
|
||||
ContentValues(2).apply {
|
||||
put(
|
||||
ContactsContract.Data.MIMETYPE,
|
||||
ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE
|
||||
)
|
||||
put(ContactsContract.CommonDataKinds.Website.URL, url)
|
||||
}
|
||||
val urls = info["URL"]?.mapNotNull {
|
||||
it.value.takeIf { url -> url.isNotBlank() }?.let { url ->
|
||||
ContentValues(2).apply {
|
||||
put(
|
||||
ContactsContract.Data.MIMETYPE,
|
||||
ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE
|
||||
)
|
||||
put(ContactsContract.CommonDataKinds.Website.URL, url)
|
||||
}
|
||||
}
|
||||
val bDays = info["BDAY"]?.mapNotNull {
|
||||
it.value.takeIf { day -> day.isNotBlank() }?.let { day ->
|
||||
ContentValues(3).apply {
|
||||
put(
|
||||
ContactsContract.Data.MIMETYPE,
|
||||
ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE
|
||||
)
|
||||
put(
|
||||
ContactsContract.CommonDataKinds.Event.TYPE,
|
||||
ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY
|
||||
)
|
||||
put(ContactsContract.CommonDataKinds.Event.START_DATE, day)
|
||||
}
|
||||
}
|
||||
}
|
||||
putParcelableArrayListExtra(
|
||||
ContactsContract.Intents.Insert.DATA,
|
||||
ArrayList(listOfNotNull(urls, bDays).flatten())
|
||||
)
|
||||
}
|
||||
val bDays = info["BDAY"]?.mapNotNull {
|
||||
it.value.takeIf { day -> day.isNotBlank() }?.let { day ->
|
||||
ContentValues(3).apply {
|
||||
put(
|
||||
ContactsContract.Data.MIMETYPE,
|
||||
ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE
|
||||
)
|
||||
put(
|
||||
ContactsContract.CommonDataKinds.Event.TYPE,
|
||||
ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY
|
||||
)
|
||||
put(ContactsContract.CommonDataKinds.Event.START_DATE, day)
|
||||
}
|
||||
}
|
||||
}
|
||||
putParcelableArrayListExtra(
|
||||
ContactsContract.Intents.Insert.DATA,
|
||||
ArrayList(listOfNotNull(urls, bDays).flatten())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-6
@@ -2,9 +2,7 @@ package de.markusfisch.android.binaryeye.actions.vtype.vevent
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.provider.CalendarContract
|
||||
import android.support.annotation.RequiresApi
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IntentAction
|
||||
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
|
||||
@@ -21,14 +19,10 @@ object VEventAction : IntentAction() {
|
||||
get() = R.string.vevent_failed
|
||||
|
||||
override fun canExecuteOn(data: ByteArray): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
|
||||
return false
|
||||
}
|
||||
val type = VTypeParser.parseVType(String(data))
|
||||
return type == "VEVENT" || type == "VCALENDAR"
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
|
||||
override suspend fun createIntent(context: Context, data: ByteArray): Intent? {
|
||||
val info = VTypeParser.parseMap(String(data))
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import de.markusfisch.android.binaryeye.content.openUrl
|
||||
|
||||
object WebAction : Action() {
|
||||
private val colloquialRegex =
|
||||
"^(URL:[ ]*)*(http[s]*://)*[A-Za-z0-9-]{3,}\\.[A-Za-z]{2,}[^ \t\r\n]*$".toRegex()
|
||||
"^(URL:[ ]*)*(http[s]*://)*[A-Za-z0-9.-]{3,}\\.[A-Za-z]{2,}[^ \t\r\n]*$".toRegex()
|
||||
|
||||
override val iconResId: Int = R.drawable.ic_action_open
|
||||
override val titleResId: Int = R.string.open_url
|
||||
@@ -24,6 +24,6 @@ object WebAction : Action() {
|
||||
if (!url.startsWith("http") && !url.startsWith("ftp")) {
|
||||
url = "http://${url}"
|
||||
}
|
||||
fired = context.openUrl(url)
|
||||
executed = context.openUrl(url)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,12 +23,12 @@ object WifiAction : Action() {
|
||||
|
||||
override suspend fun execute(context: Context, data: ByteArray) {
|
||||
withContext(Dispatchers.IO) {
|
||||
fired = WifiConnector.parse(String(data))?.let {
|
||||
executed = WifiConnector.parse(String(data))?.let {
|
||||
WifiConnector.addNetwork(context, it)
|
||||
} ?: false
|
||||
withContext(Dispatchers.Main) {
|
||||
context.toast(
|
||||
if (fired) {
|
||||
if (executed) {
|
||||
R.string.wifi_added
|
||||
} else {
|
||||
R.string.wifi_config_failed
|
||||
|
||||
+17
-21
@@ -8,7 +8,7 @@ import android.net.wifi.WifiManager
|
||||
import android.net.wifi.WifiNetworkSuggestion
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import android.support.annotation.RequiresApi
|
||||
import androidx.annotation.RequiresApi
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
@@ -113,34 +113,30 @@ object WifiConnector {
|
||||
get() = inputMap["I"] ?: ""
|
||||
internal val eapMethod: Int?
|
||||
get() = if (inputMap["E"].isNullOrEmpty()) {
|
||||
requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
WifiEnterpriseConfig.Eap.NONE
|
||||
}
|
||||
WifiEnterpriseConfig.Eap.NONE
|
||||
} else when (inputMap["E"]?.uppercase()) {
|
||||
"AKA" -> requireSdk(Build.VERSION_CODES.LOLLIPOP) { WifiEnterpriseConfig.Eap.AKA }
|
||||
"AKA" -> WifiEnterpriseConfig.Eap.AKA
|
||||
"AKA_PRIME" -> requireSdk(Build.VERSION_CODES.M) { WifiEnterpriseConfig.Eap.AKA_PRIME }
|
||||
"NONE" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.NONE }
|
||||
"PEAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.PEAP }
|
||||
"PWD" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.PWD }
|
||||
"SIM" -> requireSdk(Build.VERSION_CODES.LOLLIPOP) { WifiEnterpriseConfig.Eap.SIM }
|
||||
"TLS" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.TLS }
|
||||
"TTLS" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Eap.TTLS }
|
||||
"NONE" -> WifiEnterpriseConfig.Eap.NONE
|
||||
"PEAP" -> WifiEnterpriseConfig.Eap.PEAP
|
||||
"PWD" -> WifiEnterpriseConfig.Eap.PWD
|
||||
"SIM" -> WifiEnterpriseConfig.Eap.SIM
|
||||
"TLS" -> WifiEnterpriseConfig.Eap.TLS
|
||||
"TTLS" -> WifiEnterpriseConfig.Eap.TTLS
|
||||
"UNAUTH_TLS" -> requireSdk(Build.VERSION_CODES.N) { WifiEnterpriseConfig.Eap.UNAUTH_TLS }
|
||||
else -> null
|
||||
}
|
||||
internal val phase2Method: Int?
|
||||
get() = if (inputMap["PH2"].isNullOrEmpty()) {
|
||||
requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
WifiEnterpriseConfig.Phase2.NONE
|
||||
}
|
||||
WifiEnterpriseConfig.Phase2.NONE
|
||||
} else when (inputMap["PH2"]?.uppercase()) {
|
||||
"AKA" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.AKA }
|
||||
"AKA_PRIME" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.AKA_PRIME }
|
||||
"GTC" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.GTC }
|
||||
"MSCHAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.MSCHAP }
|
||||
"MSCHAPV2" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.MSCHAPV2 }
|
||||
"NONE" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.NONE }
|
||||
"PAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) { WifiEnterpriseConfig.Phase2.PAP }
|
||||
"GTC" -> WifiEnterpriseConfig.Phase2.GTC
|
||||
"MSCHAP" -> WifiEnterpriseConfig.Phase2.MSCHAP
|
||||
"MSCHAPV2" -> WifiEnterpriseConfig.Phase2.MSCHAPV2
|
||||
"NONE" -> WifiEnterpriseConfig.Phase2.NONE
|
||||
"PAP" -> WifiEnterpriseConfig.Phase2.PAP
|
||||
"SIM" -> requireSdk(Build.VERSION_CODES.O) { WifiEnterpriseConfig.Phase2.SIM }
|
||||
else -> null
|
||||
}
|
||||
@@ -265,7 +261,7 @@ object WifiConnector {
|
||||
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP)
|
||||
}
|
||||
|
||||
"WPA2-EAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
"WPA2-EAP" -> {
|
||||
passwordWithQuotes?.let {
|
||||
preSharedKey = it
|
||||
} ?: return null
|
||||
@@ -285,7 +281,7 @@ object WifiConnector {
|
||||
data.eapMethod ?: return null // non valid eapMethod
|
||||
enterpriseConfig.phase2Method =
|
||||
data.phase2Method ?: return null // non valid phase2Method
|
||||
} ?: return null // api isn't high enough
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.isNotEmpty
|
||||
import de.markusfisch.android.binaryeye.app.PERMISSION_LOCATION
|
||||
import de.markusfisch.android.binaryeye.app.PERMISSION_WRITE
|
||||
import de.markusfisch.android.binaryeye.app.applyLocale
|
||||
import de.markusfisch.android.binaryeye.app.permissionGrantedCallback
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.view.initBars
|
||||
|
||||
abstract class AbstractBaseActivity : AppCompatActivity() {
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
base?.applyLocale(prefs.customLocale)
|
||||
super.attachBaseContext(base)
|
||||
}
|
||||
|
||||
protected fun setScreenContentView(@LayoutRes layoutId: Int) {
|
||||
setContentView(layoutId)
|
||||
initBars()
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
finish()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
super.onRequestPermissionsResult(
|
||||
requestCode,
|
||||
permissions,
|
||||
grantResults
|
||||
)
|
||||
when (requestCode) {
|
||||
PERMISSION_LOCATION, PERMISSION_WRITE -> {
|
||||
if (grantResults.isNotEmpty() &&
|
||||
grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
permissionGrantedCallback?.invoke()
|
||||
permissionGrantedCallback = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
onCreateOptionsMenu(menu, menuInflater)
|
||||
return menu.isNotEmpty() || super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
protected open fun onCreateOptionsMenu(
|
||||
menu: Menu,
|
||||
inflater: MenuInflater
|
||||
) = Unit
|
||||
}
|
||||
+55
-30
@@ -1,49 +1,41 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.EditText
|
||||
import android.widget.ListView
|
||||
import android.widget.RadioGroup
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.automation.AutomatedAction
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.view.systemBarListViewScrollListener
|
||||
|
||||
class AutomatedActionsFragment : Fragment() {
|
||||
class AutomatedActionsActivity : AbstractBaseActivity() {
|
||||
private val actions = ArrayList<AutomatedAction>()
|
||||
|
||||
private lateinit var adapter: ActionsAdapter
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
state: Bundle?
|
||||
): View? {
|
||||
val ac = activity ?: return null
|
||||
ac.setTitle(R.string.automated_actions)
|
||||
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_automated_actions,
|
||||
container,
|
||||
false
|
||||
)
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setScreenContentView(R.layout.activity_automated_actions)
|
||||
setTitle(R.string.automated_actions)
|
||||
|
||||
actions.clear()
|
||||
actions.addAll(prefs.automatedActions)
|
||||
|
||||
val listView = view.findViewById<ListView>(R.id.actions)
|
||||
listView.emptyView = view.findViewById(R.id.no_actions)
|
||||
adapter = ActionsAdapter(ac, actions)
|
||||
val listView = findViewById<ListView>(R.id.actions)
|
||||
listView.emptyView = findViewById(R.id.no_actions)
|
||||
adapter = ActionsAdapter(this, actions)
|
||||
listView.adapter = adapter
|
||||
listView.setOnScrollListener(systemBarListViewScrollListener)
|
||||
listView.setOnItemClickListener { _, _, position, _ ->
|
||||
@@ -54,24 +46,22 @@ class AutomatedActionsFragment : Fragment() {
|
||||
true
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.add).setOnClickListener {
|
||||
findViewById<View>(R.id.add).setOnClickListener {
|
||||
editAction(null)
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
listView.setPaddingFromWindowInsets()
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
private fun editAction(position: Int?) {
|
||||
val ac = activity ?: return
|
||||
val action = position?.let { actions[it] }
|
||||
val view = ac.layoutInflater.inflate(
|
||||
val view = layoutInflater.inflate(
|
||||
R.layout.dialog_automated_action,
|
||||
null
|
||||
)
|
||||
val regexView = view.findViewById<EditText>(R.id.regex)
|
||||
val formatView = view.findViewById<Spinner>(R.id.format)
|
||||
val urlView = view.findViewById<EditText>(R.id.url_template)
|
||||
val typeGroup = view.findViewById<RadioGroup>(R.id.action_type)
|
||||
val urlSection = view.findViewById<View>(R.id.url_section)
|
||||
@@ -79,9 +69,31 @@ class AutomatedActionsFragment : Fragment() {
|
||||
val intentUriView = view.findViewById<EditText>(
|
||||
R.id.intent_uri_template
|
||||
)
|
||||
val names = resources.getStringArray(
|
||||
R.array.barcode_formats_names
|
||||
).toMutableList().apply {
|
||||
add(0, getString(R.string.all_formats))
|
||||
}
|
||||
val values = resources.getStringArray(
|
||||
R.array.barcode_formats_values
|
||||
).toMutableList().apply {
|
||||
add(0, "")
|
||||
}
|
||||
formatView.adapter = ArrayAdapter(
|
||||
this,
|
||||
android.R.layout.simple_spinner_item,
|
||||
names
|
||||
).apply {
|
||||
setDropDownViewResource(
|
||||
android.R.layout.simple_spinner_dropdown_item
|
||||
)
|
||||
}
|
||||
|
||||
if (action != null) {
|
||||
regexView.setText(action.pattern)
|
||||
formatView.setSelection(
|
||||
values.indexOf(action.format ?: "").coerceAtLeast(0)
|
||||
)
|
||||
when (action.type) {
|
||||
AutomatedAction.Type.Intent -> {
|
||||
urlView.setText(action.urlTemplate ?: "")
|
||||
@@ -91,6 +103,8 @@ class AutomatedActionsFragment : Fragment() {
|
||||
intentUriView.setText(action.intentUriTemplate ?: "")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
formatView.setSelection(0)
|
||||
}
|
||||
|
||||
fun updateTypeSections(checkedId: Int) {
|
||||
@@ -112,7 +126,7 @@ class AutomatedActionsFragment : Fragment() {
|
||||
typeGroup.check(initialTypeId)
|
||||
updateTypeSections(initialTypeId)
|
||||
|
||||
val dialog = AlertDialog.Builder(ac)
|
||||
val dialog = MaterialAlertDialogBuilder(this)
|
||||
.setTitle(
|
||||
if (action == null) {
|
||||
R.string.automated_action_add
|
||||
@@ -130,6 +144,8 @@ class AutomatedActionsFragment : Fragment() {
|
||||
addOrUpdateAction(
|
||||
position,
|
||||
regexView,
|
||||
formatView,
|
||||
values,
|
||||
urlView,
|
||||
typeGroup,
|
||||
intentUriView,
|
||||
@@ -144,6 +160,8 @@ class AutomatedActionsFragment : Fragment() {
|
||||
private fun addOrUpdateAction(
|
||||
position: Int?,
|
||||
regexView: EditText,
|
||||
formatView: Spinner,
|
||||
values: List<String>,
|
||||
urlView: EditText,
|
||||
typeGroup: RadioGroup,
|
||||
intentUriView: EditText,
|
||||
@@ -161,6 +179,9 @@ class AutomatedActionsFragment : Fragment() {
|
||||
return
|
||||
}
|
||||
val isIntent = typeGroup.checkedRadioButtonId == R.id.action_type_intent
|
||||
val format = formatView.selectedItemPosition.let {
|
||||
values[it].ifEmpty { null }
|
||||
}
|
||||
val newAction = if (isIntent) {
|
||||
val urlTemplate = urlView.text.toString().trim()
|
||||
if (urlTemplate.isEmpty()) {
|
||||
@@ -170,6 +191,7 @@ class AutomatedActionsFragment : Fragment() {
|
||||
AutomatedAction(
|
||||
pattern = pattern,
|
||||
type = AutomatedAction.Type.Intent,
|
||||
format = format,
|
||||
urlTemplate = urlTemplate
|
||||
)
|
||||
} else {
|
||||
@@ -193,6 +215,7 @@ class AutomatedActionsFragment : Fragment() {
|
||||
AutomatedAction(
|
||||
pattern = pattern,
|
||||
type = AutomatedAction.Type.CustomIntent,
|
||||
format = format,
|
||||
intentUriTemplate = intentUriTemplate
|
||||
)
|
||||
}
|
||||
@@ -207,8 +230,7 @@ class AutomatedActionsFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun confirmRemoveAction(position: Int) {
|
||||
val ac = activity ?: return
|
||||
AlertDialog.Builder(ac)
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.automated_action_remove_confirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
actions.removeAt(position)
|
||||
@@ -259,6 +281,9 @@ class AutomatedActionsFragment : Fragment() {
|
||||
)
|
||||
}
|
||||
}
|
||||
action.format?.let { format ->
|
||||
subtitle.text = "${subtitle.text} (${format.prettifyFormatName()})"
|
||||
}
|
||||
}
|
||||
return view
|
||||
}
|
||||
+67
-89
@@ -1,23 +1,21 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Matrix
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.SeekBar
|
||||
import android.widget.TextView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.db
|
||||
import de.markusfisch.android.binaryeye.app.hasWritePermission
|
||||
@@ -55,7 +53,7 @@ import java.util.Locale
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class BarcodeFragment : Fragment() {
|
||||
class BarcodeActivity : AbstractBaseActivity() {
|
||||
private enum class FileType {
|
||||
PNG, JPG, SVG, TXT
|
||||
}
|
||||
@@ -65,36 +63,24 @@ class BarcodeFragment : Fragment() {
|
||||
|
||||
private lateinit var imageView: ConfinedScalingImageView
|
||||
private lateinit var barcode: Barcode<*>
|
||||
private lateinit var addToHistoryItem: MenuItem
|
||||
private lateinit var brightenScreenItem: MenuItem
|
||||
|
||||
private var contentView: View? = null
|
||||
private var addToHistoryItem: MenuItem? = null
|
||||
private var brightenScreenItem: MenuItem? = null
|
||||
private var currentBrightness = -1f
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
state: Bundle?
|
||||
): View? {
|
||||
val ac = activity ?: return null
|
||||
ac.setTitle(R.string.view_barcode)
|
||||
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_barcode,
|
||||
container,
|
||||
false
|
||||
)
|
||||
setScreenContentView(R.layout.activity_barcode)
|
||||
setTitle(R.string.view_barcode)
|
||||
contentView = findViewById(android.R.id.content)
|
||||
|
||||
// Make `imageView` available for `onPause()`.
|
||||
imageView = view.findViewById(R.id.barcode)
|
||||
imageView = findViewById(R.id.barcode)
|
||||
|
||||
val bitmap: Bitmap
|
||||
try {
|
||||
barcode = arguments?.toBarcode() ?: throw IllegalArgumentException(
|
||||
barcode = intent?.extras?.toBarcode() ?: throw IllegalArgumentException(
|
||||
"Illegal arguments"
|
||||
)
|
||||
// Catch exceptions from encoding.
|
||||
@@ -104,11 +90,9 @@ class BarcodeFragment : Fragment() {
|
||||
if (message.isNullOrEmpty()) {
|
||||
message = getString(R.string.error_encoding_barcode)
|
||||
}
|
||||
message?.let {
|
||||
ac.toast(message)
|
||||
}
|
||||
fragmentManager.popBackStack()
|
||||
return null
|
||||
toast(message)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
imageView.setImageBitmap(bitmap)
|
||||
@@ -128,7 +112,7 @@ class BarcodeFragment : Fragment() {
|
||||
)
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.share).setOnClickListener {
|
||||
findViewById<View>(R.id.share).setOnClickListener {
|
||||
it.context?.apply {
|
||||
pickFileType(R.string.share_as) { fileType ->
|
||||
shareAs(fileType)
|
||||
@@ -136,12 +120,10 @@ class BarcodeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
imageView.doOnApplyWindowInsets { _, insets ->
|
||||
imageView.insets.set(insets)
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
private fun Bundle.toBarcode(): Barcode<*> {
|
||||
@@ -172,7 +154,7 @@ class BarcodeFragment : Fragment() {
|
||||
content,
|
||||
barcodeFormat,
|
||||
getInt(EC_LEVEL),
|
||||
getInt(MARGIN),
|
||||
getBoolean(ADD_QUIET_ZONE),
|
||||
colors
|
||||
)
|
||||
}
|
||||
@@ -182,9 +164,9 @@ class BarcodeFragment : Fragment() {
|
||||
super.onResume()
|
||||
if (prefs.brightenScreen) {
|
||||
// Post to make sure brightenScreenItem is initialized.
|
||||
view?.post {
|
||||
contentView?.postDelayed({
|
||||
brightenScreen()
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,8 +180,8 @@ class BarcodeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
parentJob.cancel()
|
||||
}
|
||||
|
||||
@@ -216,8 +198,8 @@ class BarcodeFragment : Fragment() {
|
||||
barcode.bitmap(),
|
||||
barcode.format
|
||||
)
|
||||
addToHistoryItem.isVisible = false
|
||||
context.toast(R.string.added_to_history)
|
||||
addToHistoryItem?.isVisible = false
|
||||
toast(R.string.added_to_history)
|
||||
true
|
||||
}
|
||||
|
||||
@@ -232,7 +214,7 @@ class BarcodeFragment : Fragment() {
|
||||
}
|
||||
|
||||
R.id.export_to_file -> {
|
||||
context.pickFileType(R.string.export_as) { fileType ->
|
||||
pickFileType(R.string.export_as) { fileType ->
|
||||
when (fileType) {
|
||||
FileType.PNG,
|
||||
FileType.JPG -> askForSize { size ->
|
||||
@@ -261,10 +243,8 @@ class BarcodeFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun copyToClipboard(text: String, isSensitive: Boolean = false) {
|
||||
activity?.apply {
|
||||
copyToClipboard(text, isSensitive)
|
||||
toast(R.string.copied_to_clipboard)
|
||||
}
|
||||
(this as android.content.Context).copyToClipboard(text, isSensitive)
|
||||
toast(R.string.copied_to_clipboard)
|
||||
}
|
||||
|
||||
private fun deeplinkToCopy() = createEncodeDeeplink(
|
||||
@@ -277,7 +257,7 @@ class BarcodeFragment : Fragment() {
|
||||
action: (FileType) -> Unit
|
||||
) {
|
||||
val fileTypes = FileType.entries.toTypedArray()
|
||||
AlertDialog.Builder(this)
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(title)
|
||||
.setItems(fileTypes.map { it.name }.toTypedArray()) { _, which ->
|
||||
action(fileTypes[which])
|
||||
@@ -288,21 +268,20 @@ class BarcodeFragment : Fragment() {
|
||||
// Dialogs do not have a parent view.
|
||||
@SuppressLint("InflateParams")
|
||||
private fun askForFileNameAndSave(fileType: FileType, bitmapSize: Int) {
|
||||
val ac = activity ?: return
|
||||
// Write permission is only required before Android Q.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||
!ac.hasWritePermission {
|
||||
!hasWritePermission {
|
||||
askForFileNameAndSave(fileType, bitmapSize)
|
||||
}
|
||||
) {
|
||||
return
|
||||
}
|
||||
val view = ac.layoutInflater.inflate(R.layout.dialog_save_file, null)
|
||||
val view = layoutInflater.inflate(R.layout.dialog_save_file, null)
|
||||
val editText = view.findViewById<EditText>(R.id.file_name)
|
||||
editText.setText(
|
||||
encodeFileName("${barcode.format}_${barcode.content}")
|
||||
)
|
||||
AlertDialog.Builder(ac)
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val fileName = editText.text.toString()
|
||||
@@ -344,12 +323,11 @@ class BarcodeFragment : Fragment() {
|
||||
// Dialogs do not have a parent view.
|
||||
@SuppressLint("InflateParams")
|
||||
private fun askForSize(write: (size: Int) -> Unit) {
|
||||
val ac = activity ?: return
|
||||
val view = ac.layoutInflater.inflate(R.layout.dialog_size, null)
|
||||
val view = layoutInflater.inflate(R.layout.dialog_size, null)
|
||||
val sizeView = view.findViewById<TextView>(R.id.size_display)
|
||||
val sizeBarView = view.findViewById<SeekBar>(R.id.size_bar)
|
||||
sizeBarView.initSizeBar(sizeView)
|
||||
AlertDialog.Builder(ac)
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
write(getSize(sizeBarView.progress))
|
||||
@@ -392,13 +370,12 @@ class BarcodeFragment : Fragment() {
|
||||
mimeType: String,
|
||||
write: (outputStream: OutputStream) -> Unit
|
||||
) {
|
||||
val ac = activity ?: return
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val message = ac.writeExternalFile(
|
||||
val message = writeExternalFile(
|
||||
fileName, mimeType, write
|
||||
).toSaveResult()
|
||||
withContext(Dispatchers.Main) {
|
||||
ac.toast(message)
|
||||
toast(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -449,21 +426,19 @@ class BarcodeFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun brightenScreen() {
|
||||
activity?.let {
|
||||
if (!isAdded) {
|
||||
return
|
||||
}
|
||||
currentBrightness = it.getScreenBrightness()
|
||||
it.setScreenBrightness(1f)
|
||||
brightenScreenItem.isChecked = true
|
||||
if (isFinishing || isDestroyed) {
|
||||
return
|
||||
}
|
||||
currentBrightness = getScreenBrightness()
|
||||
setScreenBrightness(1f)
|
||||
brightenScreenItem?.isChecked = true
|
||||
}
|
||||
|
||||
private fun restoreScreenBrightness() {
|
||||
if (currentBrightness > -1f && isAdded) {
|
||||
activity?.setScreenBrightness(currentBrightness)
|
||||
if (currentBrightness > -1f) {
|
||||
setScreenBrightness(currentBrightness)
|
||||
currentBrightness = -1f
|
||||
brightenScreenItem.isChecked = false
|
||||
brightenScreenItem?.isChecked = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,22 +450,23 @@ class BarcodeFragment : Fragment() {
|
||||
private const val CONTENT_RAW = "content_raw"
|
||||
private const val FORMAT = "format"
|
||||
private const val EC_LEVEL = "ec_level"
|
||||
private const val MARGIN = "margin"
|
||||
private const val ADD_QUIET_ZONE = "add_quiet_zone"
|
||||
private const val COLORS = "colors"
|
||||
private const val MIME_PNG = "image/png"
|
||||
private const val MIME_JPG = "image/jpeg"
|
||||
private const val MIME_SVG = "image/svg+xmg"
|
||||
private const val MIME_TXT = "text/plain"
|
||||
|
||||
fun newInstance(
|
||||
fun newIntent(
|
||||
context: Context,
|
||||
barcode: Barcode<*>,
|
||||
colors: Int = 0
|
||||
): Fragment {
|
||||
): Intent {
|
||||
val args = Bundle()
|
||||
when (barcode) {
|
||||
is ContentBarcode -> args.putEcLevelMargin(
|
||||
is ContentBarcode -> args.putEcLevelQuietZone(
|
||||
barcode.ecLevel,
|
||||
barcode.margin
|
||||
barcode.addQuietZone
|
||||
)
|
||||
|
||||
is BitMatrixBarcode -> {
|
||||
@@ -498,32 +474,31 @@ class BarcodeFragment : Fragment() {
|
||||
args.putInt(BIT_MATRIX_HEIGHT, barcode.bitMatrix.height)
|
||||
args.putByteArray(BIT_MATRIX_DATA, barcode.bitMatrix.data)
|
||||
}
|
||||
|
||||
else -> throw IllegalArgumentException("unknown barcode")
|
||||
}
|
||||
args.putContentFormatColors(
|
||||
barcode.content,
|
||||
barcode.format,
|
||||
colors
|
||||
)
|
||||
val fragment = BarcodeFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
return Intent(context, BarcodeActivity::class.java).apply {
|
||||
putExtras(args)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> newInstance(
|
||||
fun <T> newIntent(
|
||||
context: Context,
|
||||
content: T,
|
||||
format: BarcodeFormat,
|
||||
ecLevel: Int = -1,
|
||||
margin: Int = -1,
|
||||
addQuietZone: Boolean = false,
|
||||
colors: Int = 0
|
||||
): Fragment {
|
||||
): Intent {
|
||||
val args = Bundle()
|
||||
args.putContentFormatColors(content, format, colors)
|
||||
args.putEcLevelMargin(ecLevel, margin)
|
||||
val fragment = BarcodeFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
args.putEcLevelQuietZone(ecLevel, addQuietZone)
|
||||
return Intent(context, BarcodeActivity::class.java).apply {
|
||||
putExtras(args)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> Bundle.putContentFormatColors(
|
||||
@@ -550,12 +525,12 @@ class BarcodeFragment : Fragment() {
|
||||
putInt(COLORS, colors)
|
||||
}
|
||||
|
||||
private fun Bundle.putEcLevelMargin(
|
||||
private fun Bundle.putEcLevelQuietZone(
|
||||
ecLevel: Int,
|
||||
margin: Int
|
||||
addQuietZone: Boolean
|
||||
) {
|
||||
putInt(EC_LEVEL, ecLevel)
|
||||
putInt(MARGIN, margin)
|
||||
putBoolean(ADD_QUIET_ZONE, addQuietZone)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -610,7 +585,10 @@ private fun readAndAddToHistory(
|
||||
formats = setOf(format)
|
||||
)
|
||||
)?.first {
|
||||
db.insertScan(it.toScan())
|
||||
val scan = it.toScan()
|
||||
if (!prefs.shouldIgnoreHistoryContent(scan.text, scan.format.name)) {
|
||||
db.insertScan(scan)
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+376
-187
@@ -1,14 +1,14 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.provider.DocumentsContract
|
||||
import android.support.design.widget.FloatingActionButton
|
||||
import android.support.v4.app.Fragment
|
||||
import android.text.Editable
|
||||
import android.text.Html
|
||||
import android.text.Spannable
|
||||
@@ -16,37 +16,43 @@ import android.text.SpannableString
|
||||
import android.text.TextWatcher
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.text.style.TypefaceSpan
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TableLayout
|
||||
import android.widget.TableRow
|
||||
import android.widget.TextView
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.widget.TextViewCompat
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.ActionRegistry
|
||||
import de.markusfisch.android.binaryeye.actions.mail.MatMsg
|
||||
import de.markusfisch.android.binaryeye.actions.mail.MatMsgAction
|
||||
import de.markusfisch.android.binaryeye.actions.otpauth.OtpauthAction
|
||||
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
|
||||
import de.markusfisch.android.binaryeye.actions.vtype.vcard.VCardAction
|
||||
import de.markusfisch.android.binaryeye.actions.vtype.vevent.VEventAction
|
||||
import de.markusfisch.android.binaryeye.actions.web.WebAction
|
||||
import de.markusfisch.android.binaryeye.actions.wifi.WifiAction
|
||||
import de.markusfisch.android.binaryeye.actions.wifi.WifiConnector
|
||||
import de.markusfisch.android.binaryeye.activity.MainActivity
|
||||
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
|
||||
import de.markusfisch.android.binaryeye.app.addFragment
|
||||
import de.markusfisch.android.binaryeye.adapter.toFormatDescriptionResId
|
||||
import de.markusfisch.android.binaryeye.app.db
|
||||
import de.markusfisch.android.binaryeye.app.hasLocationPermission
|
||||
import de.markusfisch.android.binaryeye.app.hasWritePermission
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.content.ContentBarcode
|
||||
import de.markusfisch.android.binaryeye.content.EpcQrParser
|
||||
import de.markusfisch.android.binaryeye.content.IdlParser
|
||||
import de.markusfisch.android.binaryeye.content.SealParser
|
||||
import de.markusfisch.android.binaryeye.content.copyToClipboard
|
||||
import de.markusfisch.android.binaryeye.content.epcQrToRes
|
||||
import de.markusfisch.android.binaryeye.content.idlToRes
|
||||
import de.markusfisch.android.binaryeye.content.shareAsFile
|
||||
import de.markusfisch.android.binaryeye.content.shareText
|
||||
@@ -54,6 +60,8 @@ import de.markusfisch.android.binaryeye.content.toBarcode
|
||||
import de.markusfisch.android.binaryeye.content.toErrorCorrectionInt
|
||||
import de.markusfisch.android.binaryeye.content.wipeLastShareFile
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import de.markusfisch.android.binaryeye.database.toBundle
|
||||
import de.markusfisch.android.binaryeye.database.toScan
|
||||
import de.markusfisch.android.binaryeye.io.askForFileName
|
||||
import de.markusfisch.android.binaryeye.io.toSaveResult
|
||||
import de.markusfisch.android.binaryeye.io.writeExternalFile
|
||||
@@ -67,16 +75,24 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class DecodeFragment : Fragment() {
|
||||
class DecodeActivity : AbstractBaseActivity() {
|
||||
private lateinit var contentView: EditText
|
||||
private lateinit var formatView: TextView
|
||||
private lateinit var dataView: TableLayout
|
||||
private lateinit var contentHeadlineView: TextView
|
||||
private lateinit var characterCountView: TextView
|
||||
private lateinit var dataHeadlineView: TextView
|
||||
private lateinit var formatHeadlineView: TextView
|
||||
private lateinit var dataView: LinearLayout
|
||||
private lateinit var hexHeadlineView: TextView
|
||||
private lateinit var metaView: TableLayout
|
||||
private lateinit var hexView: TextView
|
||||
private lateinit var formatDescriptionView: TextView
|
||||
private lateinit var labelHeadlineView: TextView
|
||||
private lateinit var stampView: TextView
|
||||
private lateinit var recreationView: ImageView
|
||||
private lateinit var labelView: EditText
|
||||
@@ -97,6 +113,9 @@ class DecodeFragment : Fragment() {
|
||||
private var action = ActionRegistry.DEFAULT_ACTION
|
||||
private var isBinary = false
|
||||
private var originalBytes: ByteArray = ByteArray(0)
|
||||
private var lastParsedDataItems: List<Field> = emptyList()
|
||||
private var lastParsedDataTitleResId = 0
|
||||
private var showingDimmed = false
|
||||
private var label: String? = null
|
||||
private var recreationSize = 0
|
||||
|
||||
@@ -105,11 +124,12 @@ class DecodeFragment : Fragment() {
|
||||
resultCode: Int,
|
||||
resultData: Intent?
|
||||
) {
|
||||
super.onActivityResult(requestCode, resultCode, resultData)
|
||||
when (requestCode) {
|
||||
OPEN_DOCUMENT -> {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
val uri = resultData?.data ?: return
|
||||
activity?.openPickedFile(uri)
|
||||
openPickedFile(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,71 +137,79 @@ class DecodeFragment : Fragment() {
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
setScreenContentView(R.layout.activity_decode)
|
||||
setTitle(R.string.content)
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
state: Bundle?
|
||||
): View {
|
||||
activity?.setTitle(R.string.content)
|
||||
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_decode,
|
||||
container,
|
||||
false
|
||||
)
|
||||
|
||||
val justScanned = activity?.intent?.hasExtra(
|
||||
MainActivity.DECODED
|
||||
) == true
|
||||
val justScanned = intent.hasExtra(DECODED) || intent.hasExtra(DECODED_SCAN)
|
||||
closeAutomatically = prefs.closeAutomatically && justScanned
|
||||
|
||||
scan = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
arguments?.getParcelable(SCAN, Scan::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
arguments?.getParcelable(SCAN)
|
||||
} ?: throw IllegalArgumentException("DecodeFragment needs a Scan")
|
||||
scan = intent?.extras?.getBundle(SCAN_BUNDLE)?.toScan()
|
||||
?: throw IllegalArgumentException("DecodeActivity needs a Scan")
|
||||
|
||||
isBinary = scan.text.isEmpty()
|
||||
originalBytes = scan.raw ?: scan.text.toByteArray()
|
||||
format = scan.format.name
|
||||
recreationSize = (200f * dp).roundToInt()
|
||||
|
||||
contentView = view.findViewById(R.id.content)
|
||||
formatView = view.findViewById(R.id.format)
|
||||
dataView = view.findViewById(R.id.data)
|
||||
metaView = view.findViewById(R.id.meta)
|
||||
hexView = view.findViewById(R.id.hex)
|
||||
stampView = view.findViewById(R.id.stamp)
|
||||
recreationView = view.findViewById(R.id.recreation)
|
||||
labelView = view.findViewById(R.id.label)
|
||||
fab = view.findViewById(R.id.open)
|
||||
contentView = findViewById(R.id.content)
|
||||
contentHeadlineView = findViewById(R.id.content_headline)
|
||||
characterCountView = findViewById(R.id.character_count)
|
||||
dataHeadlineView = findViewById(R.id.data_headline)
|
||||
formatHeadlineView = findViewById(R.id.format_headline)
|
||||
dataView = findViewById(R.id.data)
|
||||
hexHeadlineView = findViewById(R.id.hex_headline)
|
||||
metaView = findViewById(R.id.meta)
|
||||
hexView = findViewById(R.id.hex)
|
||||
formatDescriptionView = findViewById(R.id.format_description)
|
||||
labelHeadlineView = findViewById(R.id.label_headline)
|
||||
stampView = findViewById(R.id.stamp)
|
||||
recreationView = findViewById(R.id.recreation)
|
||||
labelView = findViewById(R.id.label)
|
||||
fab = findViewById(R.id.open)
|
||||
|
||||
contentHeadlineView.setText(
|
||||
if (isBinary) {
|
||||
R.string.section_raw_barcode_content
|
||||
} else {
|
||||
R.string.section_raw_barcode_content_editable
|
||||
}
|
||||
)
|
||||
|
||||
if (prefs.showMetaData) {
|
||||
val descResId = format.toFormatDescriptionResId()
|
||||
formatHeadlineView.text = getString(
|
||||
R.string.section_format_details,
|
||||
format.prettifyFormatName()
|
||||
)
|
||||
formatHeadlineView.visibility = View.VISIBLE
|
||||
if (descResId != 0) {
|
||||
formatDescriptionView.setText(descResId)
|
||||
} else {
|
||||
formatDescriptionView.visibility = View.GONE
|
||||
}
|
||||
metaView.fillMetaView(scan)
|
||||
} else {
|
||||
formatHeadlineView.visibility = View.GONE
|
||||
formatDescriptionView.visibility = View.GONE
|
||||
metaView.visibility = View.GONE
|
||||
}
|
||||
|
||||
if (scan.id > 0) {
|
||||
labelHeadlineView.visibility = View.VISIBLE
|
||||
scan.label?.let {
|
||||
labelView.setText(it)
|
||||
label = it
|
||||
}
|
||||
} else {
|
||||
labelHeadlineView.visibility = View.GONE
|
||||
labelView.visibility = View.GONE
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
view.findViewById<View>(R.id.scroll_view).setPaddingFromWindowInsets()
|
||||
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
findViewById<View>(R.id.scroll_view).setPaddingFromWindowInsets()
|
||||
|
||||
initContentAndFab(justScanned)
|
||||
updateViewsAndFab(scan.text, originalBytes)
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
@@ -194,7 +222,7 @@ class DecodeFragment : Fragment() {
|
||||
label = newLabel
|
||||
}
|
||||
}
|
||||
if (action.fired) {
|
||||
if (action.executed) {
|
||||
maybeBackOrFinish()
|
||||
}
|
||||
}
|
||||
@@ -272,15 +300,13 @@ class DecodeFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun updateViews(text: String, bytes: ByteArray) {
|
||||
formatView.text = resources.getQuantityString(
|
||||
R.plurals.barcode_info,
|
||||
dataView.fillDataView(text, bytes, text != scan.text)
|
||||
stampView.setTrackingLink(bytes, format)
|
||||
characterCountView.text = resources.getQuantityString(
|
||||
R.plurals.character_count,
|
||||
bytes.size,
|
||||
format.prettifyFormatName(),
|
||||
bytes.size
|
||||
)
|
||||
hexView.showIf(prefs.showHexDump) { v ->
|
||||
v.text = hexDump(bytes)
|
||||
}
|
||||
recreationView.showIf(prefs.showRecreation) { v ->
|
||||
val unmodified: Boolean
|
||||
val content = if (isBinary) {
|
||||
@@ -304,7 +330,8 @@ class DecodeFragment : Fragment() {
|
||||
ContentBarcode(
|
||||
content,
|
||||
scan.format,
|
||||
scan.errorCorrectionLevel.toErrorCorrectionInt()
|
||||
scan.errorCorrectionLevel.toErrorCorrectionInt(),
|
||||
addQuietZone = true
|
||||
)
|
||||
}
|
||||
try {
|
||||
@@ -312,8 +339,8 @@ class DecodeFragment : Fragment() {
|
||||
v.setOnClickListener {
|
||||
v.context.hideSoftKeyboard(contentView)
|
||||
v.post {
|
||||
fragmentManager?.addFragment(
|
||||
BarcodeFragment.newInstance(barcode)
|
||||
startActivity(
|
||||
BarcodeActivity.newIntent(this, barcode)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -321,8 +348,13 @@ class DecodeFragment : Fragment() {
|
||||
clearRecreation()
|
||||
}
|
||||
}
|
||||
dataView.fillDataView(text, bytes)
|
||||
stampView.setTrackingLink(bytes, format)
|
||||
hexView.showIf(prefs.showHexDump) { v ->
|
||||
hexHeadlineView.visibility = View.VISIBLE
|
||||
v.text = hexDump(bytes)
|
||||
}
|
||||
if (!prefs.showHexDump) {
|
||||
hexHeadlineView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearRecreation() {
|
||||
@@ -332,93 +364,170 @@ class DecodeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun TableLayout.fillDataView(text: String, bytes: ByteArray) {
|
||||
val items = LinkedHashMap<Any, CharSequence?>()
|
||||
private fun LinearLayout.fillDataView(
|
||||
text: String,
|
||||
bytes: ByteArray,
|
||||
isEditing: Boolean
|
||||
) {
|
||||
val items = mutableListOf<Field>()
|
||||
when (prefs.showChecksum) {
|
||||
"CRC4" -> items[R.string.crc4] = String.format("%X", crc4(bytes))
|
||||
"MD5" -> items[R.string.md5] = bytes.md5().toHexString().fold()
|
||||
"SHA1" -> items[R.string.sha1] = bytes.sha1().toHexString().fold()
|
||||
"SHA256" -> items[R.string.sha256] =
|
||||
bytes.sha256().toHexString().fold()
|
||||
"CRC4" -> items.add(Field(R.string.crc4, String.format("%X", crc4(bytes))))
|
||||
"MD5" -> items.add(Field(R.string.md5, bytes.md5().toHexString().fold()))
|
||||
"SHA1" -> items.add(Field(R.string.sha1, bytes.sha1().toHexString().fold()))
|
||||
"SHA256" -> items.add(Field(R.string.sha256, bytes.sha256().toHexString().fold()))
|
||||
}
|
||||
val count = items.count()
|
||||
val parsedDataTitleResId = parseData(items, text, bytes)
|
||||
if (items.count() > count) {
|
||||
lastParsedDataItems = items
|
||||
lastParsedDataTitleResId = parsedDataTitleResId
|
||||
.takeIf { it != 0 } ?: R.string.section_parsed_data
|
||||
fillDataItems(items, false, lastParsedDataTitleResId)
|
||||
return
|
||||
}
|
||||
if (isEditing && lastParsedDataItems.isNotEmpty()) {
|
||||
if (!showingDimmed) {
|
||||
fillDataItems(lastParsedDataItems, true, lastParsedDataTitleResId)
|
||||
}
|
||||
return
|
||||
}
|
||||
val titleResId = if (items.isNotEmpty()) {
|
||||
R.string.section_parsed_data
|
||||
} else {
|
||||
0
|
||||
}
|
||||
fillDataItems(items, false, titleResId)
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
private fun parseData(
|
||||
items: MutableList<Field>,
|
||||
text: String,
|
||||
bytes: ByteArray
|
||||
): Int {
|
||||
val ctx = this
|
||||
|
||||
try {
|
||||
items.add(Field(R.string.formatted_json, JSONObject(text).toString(2)))
|
||||
return R.string.parsed_type_json
|
||||
} catch (_: JSONException) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
IdlParser.parse(String(bytes))?.let {
|
||||
items.putAll(mapOf("IIN" to it.iin))
|
||||
items.putAll(
|
||||
it.elements.entries.associate { (id, value) ->
|
||||
context.idlToRes(id) to value
|
||||
}
|
||||
)
|
||||
items.add(Field("IIN", it.iin))
|
||||
it.elements.forEach { (id, value) ->
|
||||
items.add(Field(ctx.idlToRes(id), value))
|
||||
}
|
||||
return R.string.parsed_type_international_driver_license
|
||||
}
|
||||
|
||||
SealParser.parse(ctx, bytes)?.let { sealFields ->
|
||||
sealFields.forEach { vf ->
|
||||
items.add(Field(vf.name, vf.value))
|
||||
}
|
||||
return R.string.parsed_type_digital_seal
|
||||
}
|
||||
|
||||
EpcQrParser.parse(text)?.let {
|
||||
items.addAll(it.map { (id, value) ->
|
||||
Field(ctx.epcQrToRes(id), value)
|
||||
})
|
||||
return R.string.parsed_type_sepa_epc_qr
|
||||
}
|
||||
|
||||
when (action) {
|
||||
is MatMsgAction -> items.putAll(
|
||||
MatMsg(text).run {
|
||||
mapOf(
|
||||
R.string.email_to to to,
|
||||
R.string.email_subject to sub,
|
||||
R.string.email_body to body
|
||||
)
|
||||
}
|
||||
)
|
||||
is MatMsgAction -> MatMsg(text).run {
|
||||
items.add(Field(R.string.email_to, to))
|
||||
items.add(Field(R.string.email_subject, sub))
|
||||
items.add(Field(R.string.email_body, body))
|
||||
return R.string.parsed_type_email
|
||||
}
|
||||
|
||||
is VCardAction,
|
||||
is VEventAction -> VTypeParser.parseMap(text).let { vData ->
|
||||
items.putAll(
|
||||
vData.map { item ->
|
||||
item.key to item.value.joinToString("\n") {
|
||||
it.value
|
||||
}
|
||||
}.toMap()
|
||||
)
|
||||
is VEventAction -> VTypeParser.parseMap(text).let { parsedMap ->
|
||||
parsedMap.forEach { item ->
|
||||
items.add(Field(item.key, item.value.joinToString("\n") { it.value }))
|
||||
}
|
||||
return if (action is VCardAction) {
|
||||
R.string.parsed_type_contact_card
|
||||
} else {
|
||||
R.string.parsed_type_calendar_event
|
||||
}
|
||||
}
|
||||
|
||||
is WebAction -> try {
|
||||
items.putAll(
|
||||
Uri.parse(text).run {
|
||||
mapOf(
|
||||
R.string.scheme to scheme,
|
||||
R.string.host to host,
|
||||
R.string.query to query
|
||||
)
|
||||
text.toUri().run {
|
||||
items.add(Field(R.string.scheme, scheme))
|
||||
items.add(Field(R.string.host, host))
|
||||
items.add(Field(R.string.query, query))
|
||||
}
|
||||
return R.string.parsed_type_url
|
||||
} catch (_: Exception) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
is OtpauthAction -> try {
|
||||
text.toUri().run {
|
||||
val label = pathSegments.firstOrNull()?.let {
|
||||
Uri.decode(it)
|
||||
} ?: ""
|
||||
val colonIdx = label.indexOf(':')
|
||||
val issuerFromLabel = if (colonIdx >= 0) {
|
||||
label.substring(0, colonIdx)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
val account = if (colonIdx >= 0) {
|
||||
label.substring(colonIdx + 1).trim()
|
||||
} else {
|
||||
label
|
||||
}
|
||||
val issuer = getQueryParameter("issuer") ?: issuerFromLabel
|
||||
items.add(Field(R.string.entry_type, host?.uppercase()))
|
||||
items.add(Field(R.string.otp_account, account))
|
||||
items.add(Field(R.string.otp_issuer, issuer))
|
||||
items.add(Field(R.string.otp_algorithm, getQueryParameter("algorithm")))
|
||||
items.add(Field(R.string.otp_digits, getQueryParameter("digits")))
|
||||
if (host == "totp") {
|
||||
items.add(Field(R.string.otp_period, getQueryParameter("period")))
|
||||
} else {
|
||||
items.add(Field(R.string.otp_counter, getQueryParameter("counter")))
|
||||
}
|
||||
}
|
||||
return R.string.parsed_type_otp
|
||||
} catch (_: Exception) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
is WifiAction -> WifiConnector.parseMap(text)?.let { wifiData ->
|
||||
items.putAll(
|
||||
linkedMapOf(
|
||||
R.string.entry_type to getString(R.string.wifi_network),
|
||||
R.string.wifi_ssid to wifiData["S"],
|
||||
R.string.wifi_password to wifiData["P"],
|
||||
R.string.wifi_type to wifiData["T"],
|
||||
R.string.wifi_hidden to wifiData["H"],
|
||||
R.string.wifi_eap to wifiData["E"],
|
||||
R.string.wifi_identity to wifiData["I"],
|
||||
R.string.wifi_anonymous_identity to wifiData["A"],
|
||||
R.string.wifi_phase2 to wifiData["PH2"]
|
||||
)
|
||||
)
|
||||
items.add(Field(R.string.entry_type, getString(R.string.wifi_network)))
|
||||
items.add(Field(R.string.wifi_ssid, wifiData["S"]))
|
||||
items.add(Field(R.string.wifi_password, wifiData["P"]))
|
||||
items.add(Field(R.string.wifi_type, wifiData["T"]))
|
||||
items.add(Field(R.string.wifi_hidden, wifiData["H"]))
|
||||
items.add(Field(R.string.wifi_eap, wifiData["E"]))
|
||||
items.add(Field(R.string.wifi_identity, wifiData["I"]))
|
||||
items.add(Field(R.string.wifi_anonymous_identity, wifiData["A"]))
|
||||
items.add(Field(R.string.wifi_phase2, wifiData["PH2"]))
|
||||
return R.string.parsed_type_wifi_network
|
||||
}
|
||||
}
|
||||
fill(items)
|
||||
return 0
|
||||
}
|
||||
|
||||
private fun TableLayout.fillMetaView(scan: Scan) {
|
||||
val items = linkedMapOf<Any, CharSequence?>(
|
||||
R.string.error_correction_level to scan.errorCorrectionLevel,
|
||||
R.string.sequence_size to scan.sequenceSize.positiveToString(),
|
||||
R.string.sequence_index to scan.sequenceIndex.positiveToString(),
|
||||
R.string.sequence_id to scan.sequenceId,
|
||||
R.string.gtin_country to scan.country,
|
||||
R.string.gtin_add_on to scan.addOn,
|
||||
R.string.gtin_price to scan.price,
|
||||
R.string.gtin_issue_number to scan.issueNumber,
|
||||
val items = mutableListOf(
|
||||
Field(R.string.error_correction_level, scan.errorCorrectionLevel),
|
||||
Field(R.string.sequence_size, scan.sequenceSize.positiveToString()),
|
||||
Field(R.string.sequence_index, scan.sequenceIndex.positiveToString()),
|
||||
Field(R.string.sequence_id, scan.sequenceId),
|
||||
Field(R.string.gtin_country, scan.country),
|
||||
Field(R.string.gtin_add_on, scan.addOn),
|
||||
Field(R.string.gtin_price, scan.price),
|
||||
Field(R.string.gtin_issue_number, scan.issueNumber),
|
||||
)
|
||||
if (!scan.version.isNullOrBlank()) {
|
||||
val versionString = if (scan.format.name == "QR_CODE") {
|
||||
val versionString = if (scan.format == ZxingCpp.BarcodeFormat.QRCode) {
|
||||
getString(
|
||||
R.string.qr_version_and_modules,
|
||||
scan.version,
|
||||
@@ -427,27 +536,17 @@ class DecodeFragment : Fragment() {
|
||||
} else {
|
||||
scan.version
|
||||
}
|
||||
items.putAll(
|
||||
linkedMapOf(
|
||||
R.string.barcode_version_number to versionString
|
||||
)
|
||||
)
|
||||
items.add(Field(R.string.barcode_version_number, versionString))
|
||||
}
|
||||
if (scan.format == ZxingCpp.BarcodeFormat.QR_CODE &&
|
||||
if (scan.format == ZxingCpp.BarcodeFormat.QRCode &&
|
||||
scan.dataMask > -1
|
||||
) {
|
||||
items.putAll(
|
||||
linkedMapOf(
|
||||
R.string.qr_data_mask to scan.dataMask.toString()
|
||||
)
|
||||
)
|
||||
items.add(Field(R.string.qr_data_mask, scan.dataMask.toString()))
|
||||
}
|
||||
fill(items)
|
||||
fillTable(items)
|
||||
}
|
||||
|
||||
private fun TableLayout.fill(
|
||||
items: LinkedHashMap<Any, CharSequence?>
|
||||
) {
|
||||
private fun TableLayout.fillTable(items: List<Field>) {
|
||||
removeAllViews()
|
||||
visibility = if (items.isEmpty()) View.GONE else {
|
||||
val ctx = context
|
||||
@@ -456,11 +555,12 @@ class DecodeFragment : Fragment() {
|
||||
val text = item.value
|
||||
if (!text.isNullOrBlank()) {
|
||||
val tr = TableRow(ctx)
|
||||
val keyView = TextView(ctx)
|
||||
when (val key = item.key) {
|
||||
is Int -> keyView.setText(key)
|
||||
is String -> keyView.text = key
|
||||
else -> keyView.text = key.toString()
|
||||
val keyView = TextView(ctx).apply {
|
||||
when (val key = item.key) {
|
||||
is Int -> setText(key)
|
||||
is String -> this.text = key
|
||||
else -> this.text = key.toString()
|
||||
}
|
||||
}
|
||||
val valueView = TextView(ctx).apply {
|
||||
setPadding(spaceBetween, 0, 0, 0)
|
||||
@@ -478,9 +578,93 @@ class DecodeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun LinearLayout.fillDataItems(
|
||||
items: List<Field>,
|
||||
dimmed: Boolean,
|
||||
titleResId: Int
|
||||
) {
|
||||
showingDimmed = dimmed
|
||||
removeAllViews()
|
||||
if (items.isEmpty()) {
|
||||
dataHeadlineView.visibility = View.GONE
|
||||
visibility = View.GONE
|
||||
return
|
||||
}
|
||||
if (titleResId != 0) {
|
||||
dataHeadlineView.setText(titleResId)
|
||||
dataHeadlineView.visibility = View.VISIBLE
|
||||
} else {
|
||||
dataHeadlineView.visibility = View.GONE
|
||||
}
|
||||
val ctx = context
|
||||
val spaceBetween = (16f * dp).roundToInt()
|
||||
for (item in items) {
|
||||
val text = item.value
|
||||
if (text.isNullOrBlank()) {
|
||||
continue
|
||||
}
|
||||
|
||||
val rowView = LinearLayout(ctx).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
bottomMargin = spaceBetween
|
||||
}
|
||||
setBackgroundResource(R.drawable.list_selector)
|
||||
setOnClickListener {
|
||||
copyToClipboard(text.toString())
|
||||
}
|
||||
}
|
||||
|
||||
val keyView = TextView(ctx).apply {
|
||||
TextViewCompat.setTextAppearance(
|
||||
this,
|
||||
R.style.SecondaryText
|
||||
)
|
||||
when (val key = item.key) {
|
||||
is Int -> setText(key)
|
||||
is String -> this.text = key
|
||||
else -> this.text = key.toString()
|
||||
}
|
||||
}
|
||||
|
||||
val valueView = TextView(ctx).apply {
|
||||
TextViewCompat.setTextAppearance(
|
||||
this,
|
||||
android.R.style.TextAppearance_Medium
|
||||
)
|
||||
setTextColor(
|
||||
ColorStateList.valueOf(
|
||||
ctx.obtainStyledAttributes(
|
||||
intArrayOf(android.R.attr.textColorPrimary)
|
||||
).run {
|
||||
try {
|
||||
getColor(0, currentTextColor)
|
||||
} finally {
|
||||
recycle()
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
this.text = text
|
||||
}
|
||||
|
||||
val alpha = if (dimmed) 0.5f else 1f
|
||||
keyView.alpha = alpha
|
||||
valueView.alpha = alpha
|
||||
|
||||
rowView.addView(keyView)
|
||||
rowView.addView(valueView)
|
||||
addView(rowView)
|
||||
}
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.fragment_decode, menu)
|
||||
if (id > 0L) {
|
||||
if (scan.id > 0L) {
|
||||
menu.findItem(R.id.remove).isVisible = true
|
||||
}
|
||||
if (action is WifiAction) {
|
||||
@@ -509,29 +693,26 @@ class DecodeFragment : Fragment() {
|
||||
}
|
||||
|
||||
R.id.share -> {
|
||||
context?.apply {
|
||||
shareText(textOrHex())
|
||||
maybeBackOrFinish()
|
||||
}
|
||||
shareText(textOrHex())
|
||||
maybeBackOrFinish()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.share_file -> {
|
||||
context?.apply {
|
||||
val (data, name) = if (isBinary) {
|
||||
Pair(originalBytes, "barcode_content.bin")
|
||||
} else {
|
||||
Pair(content, "barcode_content.txt")
|
||||
}
|
||||
shareAsFile(data, name)
|
||||
maybeBackOrFinish()
|
||||
val (data, name) = if (isBinary) {
|
||||
Pair(originalBytes, "barcode_content.bin")
|
||||
} else {
|
||||
Pair(content, "barcode_content.txt")
|
||||
}
|
||||
shareAsFile(data, name)
|
||||
maybeBackOrFinish()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.create -> {
|
||||
fragmentManager?.addFragment(
|
||||
EncodeFragment.newInstance(
|
||||
startActivity(
|
||||
EncodeActivity.newIntent(
|
||||
this,
|
||||
if (isBinary) originalBytes else content,
|
||||
format
|
||||
)
|
||||
@@ -540,8 +721,7 @@ class DecodeFragment : Fragment() {
|
||||
}
|
||||
|
||||
R.id.remove -> {
|
||||
db.removeScan(scan.id)
|
||||
backOrFinish()
|
||||
askToRemoveScan(scan.id)
|
||||
true
|
||||
}
|
||||
|
||||
@@ -549,6 +729,18 @@ class DecodeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun askToRemoveScan(id: Long) {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.really_remove_scan)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
db.removeScan(id)
|
||||
backOrFinish()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun textOrHex() = if (isBinary) {
|
||||
originalBytes.toHexString()
|
||||
} else {
|
||||
@@ -570,12 +762,10 @@ class DecodeFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun copyToClipboard(text: String, isSensitive: Boolean = false) {
|
||||
activity?.apply {
|
||||
copyToClipboard(text, isSensitive)
|
||||
// There's a clipboard popup from Android 13 on.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
toast(R.string.copied_to_clipboard)
|
||||
}
|
||||
(this as android.content.Context).copyToClipboard(text, isSensitive)
|
||||
// There's a clipboard popup from Android 13 on.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
toast(R.string.copied_to_clipboard)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -586,27 +776,21 @@ class DecodeFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun backOrFinish() {
|
||||
val fm = fragmentManager
|
||||
if (fm != null && fm.backStackEntryCount > 0) {
|
||||
fm.popBackStack()
|
||||
} else {
|
||||
activity?.finish()
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun executeAction(str: String) {
|
||||
val ac = activity ?: return
|
||||
if (str.isEmpty() || openLocalDocument(str)) {
|
||||
return
|
||||
}
|
||||
if (action is WifiAction &&
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||
!ac.hasLocationPermission { executeAction(str) }
|
||||
!hasLocationPermission { executeAction(str) }
|
||||
) {
|
||||
return
|
||||
}
|
||||
scope.launch {
|
||||
action.execute(ac, str.toByteArray())
|
||||
action.execute(this@DecodeActivity, str.toByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -642,44 +826,48 @@ class DecodeFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun askForFileNameAndSave(raw: ByteArray) {
|
||||
val ac = activity ?: return
|
||||
// Write permission is only required before Android Q.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||
!ac.hasWritePermission { askForFileNameAndSave(raw) }
|
||||
!hasWritePermission { askForFileNameAndSave(raw) }
|
||||
) {
|
||||
return
|
||||
}
|
||||
scope.launch(Dispatchers.Main) {
|
||||
val name = ac.askForFileName() ?: return@launch
|
||||
val name = askForFileName() ?: return@launch
|
||||
withContext(Dispatchers.IO) {
|
||||
val message = ac.writeExternalFile(
|
||||
val message = writeExternalFile(
|
||||
name,
|
||||
"application/octet-stream"
|
||||
) {
|
||||
it.write(raw)
|
||||
}.toSaveResult()
|
||||
withContext(Dispatchers.Main) {
|
||||
ac.toast(message)
|
||||
toast(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SCAN = "scan"
|
||||
const val DECODED_SCAN = "decoded_scan"
|
||||
const val DECODED = "decoded"
|
||||
private const val SCAN_BUNDLE = "scan_bundle"
|
||||
private const val OPEN_DOCUMENT = 1
|
||||
private const val SCHEME_FILE = "file://"
|
||||
|
||||
fun newInstance(scan: Scan): Fragment {
|
||||
fun newIntent(context: Context, scan: Scan): Intent {
|
||||
val args = Bundle()
|
||||
args.putParcelable(SCAN, scan)
|
||||
val fragment = DecodeFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
args.putBundle(SCAN_BUNDLE, scan.toBundle())
|
||||
return Intent(context, DecodeActivity::class.java).apply {
|
||||
putExtra(DECODED_SCAN, true)
|
||||
putExtras(args)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class Field(val key: Any, val value: CharSequence?)
|
||||
|
||||
private inline fun <T : View> T.showIf(
|
||||
visible: Boolean,
|
||||
block: (T) -> Unit
|
||||
@@ -741,6 +929,7 @@ private fun Int.positiveToString() = if (this > -1) this.toString() else ""
|
||||
private fun TextView.setTrackingLink(bytes: ByteArray, format: String) {
|
||||
val trackingLink = generateDpTrackingLink(bytes, format)
|
||||
if (trackingLink != null) {
|
||||
visibility = View.VISIBLE
|
||||
text = trackingLink.fromHtml()
|
||||
isClickable = true
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
@@ -753,7 +942,7 @@ private fun generateDpTrackingLink(bytes: ByteArray, format: String): String? {
|
||||
// Check for Deutsche Post Matrixcode stamp.
|
||||
var isStamp = false
|
||||
var rawData = bytes
|
||||
if (format == "DATA_MATRIX" &&
|
||||
if (format == ZxingCpp.BarcodeFormat.DataMatrix.name &&
|
||||
bytes.toString(Charsets.ISO_8859_1).startsWith("DEA5")
|
||||
) {
|
||||
if (bytes.size == 47) {
|
||||
+176
-125
@@ -1,28 +1,26 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v7.widget.SwitchCompat
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.CheckBox
|
||||
import android.widget.EditText
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.widget.SwitchCompat
|
||||
import androidx.core.net.toUri
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
|
||||
import de.markusfisch.android.binaryeye.app.addFragment
|
||||
import de.markusfisch.android.binaryeye.adapter.toFormatDescriptionResId
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.app.setFragment
|
||||
import de.markusfisch.android.binaryeye.net.isEncodeDeeplink
|
||||
import de.markusfisch.android.binaryeye.text.unescape
|
||||
import de.markusfisch.android.binaryeye.view.hideSoftKeyboard
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
@@ -31,8 +29,9 @@ import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||
import java.io.InputStream
|
||||
import kotlin.math.min
|
||||
|
||||
class EncodeFragment : Fragment() {
|
||||
class EncodeActivity : AbstractBaseActivity() {
|
||||
private lateinit var formatView: Spinner
|
||||
private lateinit var formatDescriptionView: TextView
|
||||
private lateinit var ecLabel: TextView
|
||||
private lateinit var ecSpinner: Spinner
|
||||
private lateinit var colorsLabel: TextView
|
||||
@@ -41,18 +40,33 @@ class EncodeFragment : Fragment() {
|
||||
private lateinit var contentView: EditText
|
||||
private lateinit var unescapeCheckBox: CheckBox
|
||||
|
||||
private val formats = arrayListOf(
|
||||
BarcodeFormat.AZTEC,
|
||||
BarcodeFormat.CODABAR,
|
||||
BarcodeFormat.CODE_39,
|
||||
BarcodeFormat.CODE_128,
|
||||
BarcodeFormat.DATA_MATRIX,
|
||||
BarcodeFormat.EAN_8,
|
||||
BarcodeFormat.EAN_13,
|
||||
private val writeableFormats = arrayListOf(
|
||||
BarcodeFormat.Aztec,
|
||||
BarcodeFormat.Codabar,
|
||||
BarcodeFormat.Code39Std,
|
||||
BarcodeFormat.Code39Ext,
|
||||
BarcodeFormat.Code32,
|
||||
BarcodeFormat.PZN,
|
||||
BarcodeFormat.Code93,
|
||||
BarcodeFormat.Code128,
|
||||
BarcodeFormat.DataBar,
|
||||
BarcodeFormat.DataBarOmni,
|
||||
BarcodeFormat.DataBarStk,
|
||||
BarcodeFormat.DataBarLtd,
|
||||
BarcodeFormat.DataBarExp,
|
||||
BarcodeFormat.DataBarExpStk,
|
||||
BarcodeFormat.DataMatrix,
|
||||
BarcodeFormat.DXFilmEdge,
|
||||
BarcodeFormat.EAN8,
|
||||
BarcodeFormat.EAN13,
|
||||
BarcodeFormat.ITF,
|
||||
BarcodeFormat.PDF_417,
|
||||
BarcodeFormat.QR_CODE,
|
||||
BarcodeFormat.UPC_A
|
||||
BarcodeFormat.MaxiCode,
|
||||
BarcodeFormat.PDF417,
|
||||
BarcodeFormat.QRCode,
|
||||
BarcodeFormat.MicroQRCode,
|
||||
BarcodeFormat.RMQRCode,
|
||||
BarcodeFormat.UPCA,
|
||||
BarcodeFormat.UPCE,
|
||||
)
|
||||
|
||||
private var bytes: ByteArray? = null
|
||||
@@ -62,16 +76,16 @@ class EncodeFragment : Fragment() {
|
||||
resultCode: Int,
|
||||
resultData: Intent?
|
||||
) {
|
||||
super.onActivityResult(requestCode, resultCode, resultData)
|
||||
when (requestCode) {
|
||||
PICK_FILE_RESULT_CODE -> {
|
||||
if (resultCode == Activity.RESULT_OK &&
|
||||
resultData != null &&
|
||||
resultData.data != null
|
||||
) {
|
||||
val ac = activity ?: return
|
||||
ac.hideSoftKeyboard(contentView)
|
||||
hideSoftKeyboard(contentView)
|
||||
val uri = resultData.data ?: return
|
||||
bytes = ac.contentResolver?.openInputStream(uri)?.use {
|
||||
bytes = contentResolver?.openInputStream(uri)?.use {
|
||||
it.readBytesMax(4096)
|
||||
} ?: return
|
||||
setEncodeByteArray()
|
||||
@@ -82,28 +96,15 @@ class EncodeFragment : Fragment() {
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
setScreenContentView(R.layout.activity_encode)
|
||||
setTitle(R.string.compose_barcode)
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
state: Bundle?
|
||||
): View? {
|
||||
val ac = activity ?: return null
|
||||
ac.setTitle(R.string.compose_barcode)
|
||||
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_encode,
|
||||
container,
|
||||
false
|
||||
)
|
||||
|
||||
formatView = view.findViewById(R.id.format)
|
||||
formatView = findViewById(R.id.format)
|
||||
formatDescriptionView = findViewById(R.id.format_description)
|
||||
val formatAdapter = ArrayAdapter(
|
||||
ac,
|
||||
this,
|
||||
android.R.layout.simple_spinner_item,
|
||||
formats.map { it.name.prettifyFormatName() }
|
||||
writeableFormats.map { it.name.prettifyFormatName() }
|
||||
)
|
||||
formatAdapter.setDropDownViewResource(
|
||||
android.R.layout.simple_spinner_dropdown_item
|
||||
@@ -116,11 +117,20 @@ class EncodeFragment : Fragment() {
|
||||
position: Int,
|
||||
id: Long
|
||||
) {
|
||||
val format = formats[position]
|
||||
val format = writeableFormats[position]
|
||||
val descResId = format.name.toFormatDescriptionResId()
|
||||
if (descResId != 0) {
|
||||
formatDescriptionView.setText(descResId)
|
||||
formatDescriptionView.visibility = View.VISIBLE
|
||||
} else {
|
||||
formatDescriptionView.visibility = View.GONE
|
||||
}
|
||||
val arrayId = when (format) {
|
||||
BarcodeFormat.AZTEC -> R.array.aztec_error_correction_levels
|
||||
BarcodeFormat.QR_CODE -> R.array.qr_error_correction_levels
|
||||
BarcodeFormat.PDF_417 -> R.array.pdf417_error_correction_levels
|
||||
BarcodeFormat.Aztec -> R.array.aztec_error_correction_levels
|
||||
BarcodeFormat.QRCode -> R.array.qr_error_correction_levels
|
||||
BarcodeFormat.MicroQRCode -> R.array.mqr_error_correction_levels
|
||||
BarcodeFormat.RMQRCode -> R.array.rmqr_error_correction_levels
|
||||
BarcodeFormat.PDF417 -> R.array.pdf417_error_correction_levels
|
||||
else -> 0
|
||||
}
|
||||
if (arrayId > 0) {
|
||||
@@ -138,21 +148,13 @@ class EncodeFragment : Fragment() {
|
||||
format.canBeInverted().setVisibilityFor(
|
||||
colorsLabel, colorsSpinner
|
||||
)
|
||||
when (format) {
|
||||
BarcodeFormat.AZTEC,
|
||||
BarcodeFormat.DATA_MATRIX,
|
||||
BarcodeFormat.QR_CODE,
|
||||
BarcodeFormat.PDF_417 -> true
|
||||
|
||||
else -> false
|
||||
}.setVisibilityFor(addQuietZoneSwitch)
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parentView: AdapterView<*>?) {}
|
||||
}
|
||||
|
||||
ecLabel = view.findViewById(R.id.error_correction_label)
|
||||
ecSpinner = view.findViewById(R.id.error_correction_level)
|
||||
ecLabel = findViewById(R.id.error_correction_label)
|
||||
ecSpinner = findViewById(R.id.error_correction_level)
|
||||
ecSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(
|
||||
parentView: AdapterView<*>?,
|
||||
@@ -160,7 +162,7 @@ class EncodeFragment : Fragment() {
|
||||
position: Int,
|
||||
id: Long
|
||||
) {
|
||||
val format = formats[formatView.selectedItemPosition]
|
||||
val format = writeableFormats[formatView.selectedItemPosition]
|
||||
prefs.indexOfLastSelectedEcLevel = format.packEcLevel(
|
||||
prefs.indexOfLastSelectedEcLevel,
|
||||
position
|
||||
@@ -170,55 +172,29 @@ class EncodeFragment : Fragment() {
|
||||
override fun onNothingSelected(parentView: AdapterView<*>?) {}
|
||||
}
|
||||
|
||||
colorsLabel = view.findViewById(R.id.colors_label)
|
||||
colorsSpinner = view.findViewById(R.id.colors)
|
||||
addQuietZoneSwitch = view.findViewById(R.id.add_quiet_zone)
|
||||
colorsLabel = findViewById(R.id.colors_label)
|
||||
colorsSpinner = findViewById(R.id.colors)
|
||||
addQuietZoneSwitch = findViewById(R.id.add_quiet_zone)
|
||||
addQuietZoneSwitch.isChecked = prefs.addQuietZone
|
||||
|
||||
contentView = view.findViewById(R.id.content)
|
||||
unescapeCheckBox = view.findViewById(R.id.unescape)
|
||||
contentView = findViewById(R.id.content)
|
||||
unescapeCheckBox = findViewById(R.id.unescape)
|
||||
unescapeCheckBox.isChecked = prefs.expandEscapeSequences
|
||||
|
||||
var complete = false
|
||||
val args = arguments
|
||||
args?.getString(CONTENT_TEXT)?.let {
|
||||
contentView.setText(it)
|
||||
complete = it.isNotEmpty()
|
||||
}
|
||||
args?.getByteArray(CONTENT_RAW)?.let {
|
||||
bytes = it
|
||||
complete = it.isNotEmpty()
|
||||
setEncodeByteArray()
|
||||
}
|
||||
applyIntent(state == null)
|
||||
|
||||
val barcodeFormat = args?.getString(FORMAT)
|
||||
if (barcodeFormat != null) {
|
||||
formatView.setSelection(
|
||||
formats.indexOf(barcodeFormat.toFormat())
|
||||
)
|
||||
} else if (state == null) {
|
||||
formatView.post {
|
||||
formatView.setSelection(prefs.indexOfLastSelectedFormat)
|
||||
}
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.encode).setOnClickListener {
|
||||
findViewById<View>(R.id.encode).setOnClickListener {
|
||||
it.context.encode()
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
view.findViewById<View>(R.id.scroll_view).setPaddingFromWindowInsets()
|
||||
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
findViewById<View>(R.id.scroll_view).setPaddingFromWindowInsets()
|
||||
}
|
||||
|
||||
if (complete &&
|
||||
args?.getBoolean(EXECUTE, false) == true
|
||||
) {
|
||||
view.post {
|
||||
val content = view.context.getContent() ?: return@post
|
||||
fragmentManager?.setFragment(newEncodeFragment(content))
|
||||
}
|
||||
}
|
||||
|
||||
return view
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
applyIntent(true)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
@@ -251,30 +227,75 @@ class EncodeFragment : Fragment() {
|
||||
prefs.addQuietZone = addQuietZoneSwitch.isChecked
|
||||
}
|
||||
|
||||
private fun applyIntent(applyDefaultFormat: Boolean) {
|
||||
val args = Bundle().apply {
|
||||
intent?.extras?.let { putAll(it) }
|
||||
getExtrasFromDeeplink()?.let { putAll(it) }
|
||||
}.takeIf { !it.isEmpty }
|
||||
val hasTextContent = args?.containsKey(CONTENT_TEXT) == true
|
||||
val hasRawContent = args?.containsKey(CONTENT_RAW) == true
|
||||
var complete = bytes?.isNotEmpty() == true ||
|
||||
contentView.text?.isNotEmpty() == true
|
||||
|
||||
if (hasTextContent || hasRawContent) {
|
||||
bytes = null
|
||||
contentView.isEnabled = true
|
||||
contentView.hint = null
|
||||
unescapeCheckBox.isEnabled = true
|
||||
}
|
||||
|
||||
if (hasTextContent) {
|
||||
args.getString(CONTENT_TEXT)?.let {
|
||||
contentView.setText(it)
|
||||
complete = it.isNotEmpty()
|
||||
}
|
||||
}
|
||||
if (hasRawContent) {
|
||||
args.getByteArray(CONTENT_RAW)?.let {
|
||||
bytes = it
|
||||
complete = it.isNotEmpty()
|
||||
setEncodeByteArray()
|
||||
}
|
||||
}
|
||||
|
||||
val barcodeFormat = args?.getString(FORMAT)
|
||||
if (barcodeFormat != null) {
|
||||
formatView.setSelection(
|
||||
writeableFormats.indexOf(barcodeFormat.toFormat())
|
||||
)
|
||||
} else if (applyDefaultFormat) {
|
||||
formatView.post {
|
||||
formatView.setSelection(prefs.indexOfLastSelectedFormat)
|
||||
}
|
||||
}
|
||||
|
||||
if (complete &&
|
||||
args?.getBoolean(EXECUTE, false) == true
|
||||
) {
|
||||
contentView.post {
|
||||
val content = contentView.context.getContent() ?: return@post
|
||||
startActivity(newBarcodeIntent(content))
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Context.encode() {
|
||||
hideSoftKeyboard(contentView)
|
||||
val content = getContent() ?: return
|
||||
contentView.post {
|
||||
fragmentManager?.addFragment(newEncodeFragment(content))
|
||||
startActivity(newBarcodeIntent(content))
|
||||
}
|
||||
}
|
||||
|
||||
private fun newEncodeFragment(content: Any): Fragment {
|
||||
val format = formats[formatView.selectedItemPosition]
|
||||
return BarcodeFragment.newInstance(
|
||||
private fun newBarcodeIntent(content: Any): Intent {
|
||||
val format = writeableFormats[formatView.selectedItemPosition]
|
||||
return BarcodeActivity.newIntent(
|
||||
this,
|
||||
content,
|
||||
format,
|
||||
format.getErrorCorrectionLevel(ecSpinner.selectedItemPosition),
|
||||
when (format) {
|
||||
BarcodeFormat.AZTEC,
|
||||
BarcodeFormat.DATA_MATRIX,
|
||||
BarcodeFormat.QR_CODE,
|
||||
BarcodeFormat.PDF_417 -> if (
|
||||
addQuietZoneSwitch.isChecked
|
||||
) 1 else 0
|
||||
|
||||
else -> -1
|
||||
},
|
||||
addQuietZoneSwitch.isChecked,
|
||||
if (format.canBeInverted()) {
|
||||
colorsSpinner.selectedItemPosition
|
||||
} else 0
|
||||
@@ -316,6 +337,28 @@ class EncodeFragment : Fragment() {
|
||||
unescapeCheckBox.isEnabled = false
|
||||
}
|
||||
|
||||
private fun getExtrasFromDeeplink(): Bundle? {
|
||||
val data = intent?.dataString ?: return null
|
||||
if (!isEncodeDeeplink(data)) {
|
||||
return null
|
||||
}
|
||||
val uri = data.toUri()
|
||||
return Bundle().apply {
|
||||
uri.getQueryParameter("content")?.let {
|
||||
putString(CONTENT_TEXT, it)
|
||||
}
|
||||
uri.getQueryParameter("format")?.let {
|
||||
putString(FORMAT, it)
|
||||
}
|
||||
putBoolean(
|
||||
EXECUTE,
|
||||
uri.getQueryParameter("execute").let {
|
||||
it == "" || it?.toBoolean() == true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CONTENT_TEXT = "content_text"
|
||||
private const val CONTENT_RAW = "content_raw"
|
||||
@@ -323,11 +366,12 @@ class EncodeFragment : Fragment() {
|
||||
private const val EXECUTE = "execute"
|
||||
private const val PICK_FILE_RESULT_CODE = 1
|
||||
|
||||
fun <T> newInstance(
|
||||
fun <T> newIntent(
|
||||
context: Context,
|
||||
content: T? = null,
|
||||
format: String? = null,
|
||||
execute: Boolean = false
|
||||
): Fragment {
|
||||
): Intent {
|
||||
val args = Bundle()
|
||||
content?.let {
|
||||
when (content) {
|
||||
@@ -340,9 +384,9 @@ class EncodeFragment : Fragment() {
|
||||
args.putBoolean(EXECUTE, execute)
|
||||
}
|
||||
format?.let { args.putString(FORMAT, it) }
|
||||
val fragment = EncodeFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
return Intent(context, EncodeActivity::class.java).apply {
|
||||
putExtras(args)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -363,22 +407,26 @@ private fun BarcodeFormat.unpackEcLevel(packed: Int) =
|
||||
(packed shr ecLevelShift()) and 15
|
||||
|
||||
private fun BarcodeFormat.ecLevelShift() = when (this) {
|
||||
BarcodeFormat.AZTEC -> 0
|
||||
BarcodeFormat.QR_CODE -> 4
|
||||
BarcodeFormat.PDF_417 -> 8
|
||||
BarcodeFormat.Aztec -> 0
|
||||
BarcodeFormat.QRCode -> 4
|
||||
BarcodeFormat.MicroQRCode -> 3
|
||||
BarcodeFormat.RMQRCode -> 2
|
||||
BarcodeFormat.PDF417 -> 8
|
||||
else -> throw IllegalArgumentException("$this does not have error levels")
|
||||
}
|
||||
|
||||
private fun BarcodeFormat.canBeInverted() = when (this) {
|
||||
BarcodeFormat.AZTEC,
|
||||
BarcodeFormat.DATA_MATRIX,
|
||||
BarcodeFormat.QR_CODE -> true
|
||||
BarcodeFormat.Aztec,
|
||||
BarcodeFormat.DataMatrix,
|
||||
BarcodeFormat.QRCode,
|
||||
BarcodeFormat.MicroQRCode,
|
||||
BarcodeFormat.RMQRCode -> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
private fun String.toFormat(
|
||||
default: BarcodeFormat = BarcodeFormat.QR_CODE
|
||||
default: BarcodeFormat = BarcodeFormat.QRCode
|
||||
): BarcodeFormat = try {
|
||||
BarcodeFormat.valueOf(this)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
@@ -386,9 +434,12 @@ private fun String.toFormat(
|
||||
}
|
||||
|
||||
private fun BarcodeFormat.getErrorCorrectionLevel(position: Int) = when (this) {
|
||||
BarcodeFormat.AZTEC -> position
|
||||
BarcodeFormat.QR_CODE -> (position + 1) * 2
|
||||
BarcodeFormat.PDF_417 -> position
|
||||
BarcodeFormat.Aztec -> position
|
||||
BarcodeFormat.QRCode,
|
||||
BarcodeFormat.MicroQRCode -> (position + 1) * 2
|
||||
|
||||
BarcodeFormat.RMQRCode -> (position + 1) * 4
|
||||
BarcodeFormat.PDF417 -> position
|
||||
else -> 0
|
||||
}.coerceIn(0, 8)
|
||||
|
||||
+69
-83
@@ -1,31 +1,27 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.app.SearchManager
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.support.v4.app.ActivityCompat
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.view.ActionMode
|
||||
import android.support.v7.widget.SearchView
|
||||
import android.support.v7.widget.SwitchCompat
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import android.widget.ListView
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.appcompat.widget.SwitchCompat
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.adapter.ScansAdapter
|
||||
import de.markusfisch.android.binaryeye.app.addFragment
|
||||
import de.markusfisch.android.binaryeye.app.alertDialog
|
||||
import de.markusfisch.android.binaryeye.app.db
|
||||
import de.markusfisch.android.binaryeye.app.hasWritePermission
|
||||
@@ -38,7 +34,6 @@ import de.markusfisch.android.binaryeye.database.Database
|
||||
import de.markusfisch.android.binaryeye.database.exportCsv
|
||||
import de.markusfisch.android.binaryeye.database.exportDatabase
|
||||
import de.markusfisch.android.binaryeye.database.exportJson
|
||||
import de.markusfisch.android.binaryeye.database.use
|
||||
import de.markusfisch.android.binaryeye.io.askForFileName
|
||||
import de.markusfisch.android.binaryeye.io.toSaveResult
|
||||
import de.markusfisch.android.binaryeye.view.lockStatusBarColor
|
||||
@@ -53,7 +48,7 @@ import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class HistoryFragment : Fragment() {
|
||||
class HistoryActivity : AbstractBaseActivity() {
|
||||
private lateinit var useHistorySwitch: SwitchCompat
|
||||
private lateinit var listView: ListView
|
||||
private lateinit var fab: View
|
||||
@@ -70,16 +65,15 @@ class HistoryFragment : Fragment() {
|
||||
R.menu.fragment_history_edit,
|
||||
menu
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
|
||||
// SDK35+ doesn't draw a status bar background anymore.
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM
|
||||
) {
|
||||
// SDK35+ doesn't draw a status bar background anymore.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||
lockStatusBarColor()
|
||||
val ac = activity ?: return false
|
||||
val ac = this@HistoryActivity
|
||||
@Suppress("DEPRECATION")
|
||||
ac.window.statusBarColor = ContextCompat.getColor(
|
||||
ac.window.statusBarColor = MaterialColors.getColor(
|
||||
ac,
|
||||
R.color.accent_dark
|
||||
com.google.android.material.R.attr.colorSecondaryContainer,
|
||||
Color.TRANSPARENT
|
||||
)
|
||||
}
|
||||
return true
|
||||
@@ -96,7 +90,7 @@ class HistoryFragment : Fragment() {
|
||||
mode: ActionMode,
|
||||
item: MenuItem
|
||||
): Boolean {
|
||||
val ac = activity ?: return false
|
||||
val ac = this@HistoryActivity
|
||||
return when (item.itemId) {
|
||||
R.id.copy_scan -> {
|
||||
scansAdapter?.getSelectedContent("\n")?.let {
|
||||
@@ -148,29 +142,15 @@ class HistoryFragment : Fragment() {
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
setScreenContentView(R.layout.activity_history)
|
||||
setTitle(R.string.history)
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
state: Bundle?
|
||||
): View? {
|
||||
val ac = activity ?: return null
|
||||
ac.setTitle(R.string.history)
|
||||
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_history,
|
||||
container,
|
||||
false
|
||||
)
|
||||
|
||||
useHistorySwitch = view.findViewById(
|
||||
useHistorySwitch = findViewById(
|
||||
R.id.use_history
|
||||
)
|
||||
initHistorySwitch(useHistorySwitch)
|
||||
|
||||
listView = view.findViewById(R.id.scans)
|
||||
listView = findViewById(R.id.scans)
|
||||
listView.setOnItemClickListener { _, v, position, id ->
|
||||
if (actionMode != null) {
|
||||
scansAdapter?.select(v, id, position)
|
||||
@@ -180,8 +160,8 @@ class HistoryFragment : Fragment() {
|
||||
}
|
||||
listView.setOnItemLongClickListener { _, v, position, id ->
|
||||
scansAdapter?.select(v, id, position)
|
||||
if (actionMode == null && ac is AppCompatActivity) {
|
||||
actionMode = ac.delegate.startSupportActionMode(
|
||||
if (actionMode == null) {
|
||||
actionMode = delegate.startSupportActionMode(
|
||||
actionModeCallback
|
||||
)
|
||||
}
|
||||
@@ -189,19 +169,15 @@ class HistoryFragment : Fragment() {
|
||||
}
|
||||
listView.setOnScrollListener(systemBarListViewScrollListener)
|
||||
|
||||
fab = view.findViewById(R.id.share)
|
||||
fab = findViewById(R.id.share)
|
||||
fab.setOnClickListener { v ->
|
||||
v.context.pickListSeparatorAndShare(false)
|
||||
}
|
||||
|
||||
progressView = view.findViewById(R.id.progress_view)
|
||||
progressView = findViewById(R.id.progress_view)
|
||||
|
||||
view.findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
listView.setPaddingFromWindowInsets()
|
||||
|
||||
update()
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@@ -211,6 +187,11 @@ class HistoryFragment : Fragment() {
|
||||
wipeLastShareFile()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
update()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
listViewState = listView.onSaveInstanceState()
|
||||
@@ -235,35 +216,31 @@ class HistoryFragment : Fragment() {
|
||||
|
||||
private fun initSearchView(item: MenuItem?) {
|
||||
item ?: return
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
|
||||
item.isVisible = false
|
||||
} else {
|
||||
val ac = activity ?: return
|
||||
val searchView = item.actionView as SearchView
|
||||
val searchManager = ac.getSystemService(
|
||||
Context.SEARCH_SERVICE
|
||||
) as SearchManager
|
||||
searchView.setSearchableInfo(
|
||||
searchManager.getSearchableInfo(ac.componentName)
|
||||
)
|
||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String): Boolean {
|
||||
update(query)
|
||||
return false
|
||||
}
|
||||
val ac = this
|
||||
val searchView = item.actionView as SearchView
|
||||
val searchManager = ac.getSystemService(
|
||||
Context.SEARCH_SERVICE
|
||||
) as SearchManager
|
||||
searchView.setSearchableInfo(
|
||||
searchManager.getSearchableInfo(ac.componentName)
|
||||
)
|
||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String): Boolean {
|
||||
update(query)
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(query: String): Boolean {
|
||||
update(query)
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
override fun onQueryTextChange(query: String): Boolean {
|
||||
update(query)
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.clear -> {
|
||||
context.askToRemoveScans()
|
||||
this.askToRemoveScans()
|
||||
true
|
||||
}
|
||||
|
||||
@@ -273,7 +250,7 @@ class HistoryFragment : Fragment() {
|
||||
}
|
||||
|
||||
R.id.share_as_file -> {
|
||||
context.pickListSeparatorAndShare(true)
|
||||
this.pickListSeparatorAndShare(true)
|
||||
true
|
||||
}
|
||||
|
||||
@@ -300,13 +277,13 @@ class HistoryFragment : Fragment() {
|
||||
scope.launch {
|
||||
val cursor = db.getScans(filter)
|
||||
withContext(Dispatchers.Main) {
|
||||
val ac = activity ?: return@withContext
|
||||
val ac = this@HistoryActivity
|
||||
val hasScans = cursor.count > 0
|
||||
if (filter == null) {
|
||||
if (!hasScans) {
|
||||
listView.emptyView = useHistorySwitch
|
||||
}
|
||||
ActivityCompat.invalidateOptionsMenu(ac)
|
||||
ac.invalidateOptionsMenu()
|
||||
}
|
||||
enableMenuItems(hasScans)
|
||||
fab.visibility = if (hasScans) {
|
||||
@@ -317,7 +294,16 @@ class HistoryFragment : Fragment() {
|
||||
cursor.let { cursor ->
|
||||
// Close previous cursor.
|
||||
scansAdapter?.also { it.changeCursor(null) }
|
||||
scansAdapter = ScansAdapter(ac, cursor)
|
||||
scansAdapter = ScansAdapter(ac, cursor) { id, pinned ->
|
||||
if (actionMode == null) {
|
||||
scope.launch {
|
||||
db.setPinned(id, pinned)
|
||||
withContext(Dispatchers.Main) {
|
||||
update()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
listView.adapter = scansAdapter
|
||||
listViewState?.also {
|
||||
listView.onRestoreInstanceState(it)
|
||||
@@ -344,7 +330,7 @@ class HistoryFragment : Fragment() {
|
||||
private fun showScan(id: Long) = db.getScan(id)?.also { scan ->
|
||||
closeActionMode()
|
||||
try {
|
||||
fragmentManager?.addFragment(DecodeFragment.newInstance(scan))
|
||||
startActivity(DecodeActivity.newIntent(this, scan))
|
||||
} catch (_: IllegalArgumentException) {
|
||||
// Ignore, can never happen.
|
||||
}
|
||||
@@ -358,7 +344,7 @@ class HistoryFragment : Fragment() {
|
||||
)
|
||||
val nameView = view.findViewById<EditText>(R.id.name)
|
||||
nameView.setText(text)
|
||||
AlertDialog.Builder(this)
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(
|
||||
if (content.isNullOrEmpty()) {
|
||||
getString(R.string.binary_data)
|
||||
@@ -377,7 +363,7 @@ class HistoryFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun Context.askToRemoveScans(ids: List<Long>) {
|
||||
AlertDialog.Builder(this)
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(
|
||||
if (ids.size > 1) {
|
||||
R.string.really_remove_selected_scans
|
||||
@@ -399,7 +385,7 @@ class HistoryFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun Context.askToRemoveScans() {
|
||||
AlertDialog.Builder(this)
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(
|
||||
if (filter == null) {
|
||||
R.string.really_remove_all_scans
|
||||
@@ -418,7 +404,7 @@ class HistoryFragment : Fragment() {
|
||||
|
||||
private fun askToExportToFile() {
|
||||
scope.launch {
|
||||
val ac = activity ?: return@launch
|
||||
val ac = this@HistoryActivity
|
||||
progressView.useVisibility {
|
||||
// Write permission is only required before Android Q.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||
@@ -464,7 +450,7 @@ class HistoryFragment : Fragment() {
|
||||
val separators = resources.getStringArray(
|
||||
R.array.list_separators_values
|
||||
)
|
||||
AlertDialog.Builder(this)
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.pick_list_separator)
|
||||
.setItems(R.array.list_separators_names) { _, which ->
|
||||
shareScans(separators[which], asFile)
|
||||
@@ -490,12 +476,12 @@ class HistoryFragment : Fragment() {
|
||||
text?.let {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (asFile) {
|
||||
context.shareAsFile(
|
||||
this@HistoryActivity.shareAsFile(
|
||||
it,
|
||||
String.format("scans.%s", ext)
|
||||
)
|
||||
} else {
|
||||
context.shareText(it)
|
||||
this@HistoryActivity.shareText(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.EditText
|
||||
import android.widget.ListView
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
|
||||
import de.markusfisch.android.binaryeye.adapter.setupFormatSpinner
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.preference.IgnoreCode
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.view.systemBarListViewScrollListener
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
|
||||
class IgnoreCodesActivity : AbstractBaseActivity() {
|
||||
private val patterns = ArrayList<IgnoreCode>()
|
||||
|
||||
private lateinit var adapter: PatternsAdapter
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setScreenContentView(R.layout.activity_ignore_codes)
|
||||
setTitle(R.string.ignore_codes)
|
||||
|
||||
patterns.addAll(prefs.ignoreCodes)
|
||||
|
||||
val listView = findViewById<ListView>(R.id.actions)
|
||||
listView.emptyView = findViewById(R.id.no_actions)
|
||||
findViewById<TextView>(R.id.no_actions).setText(
|
||||
R.string.ignore_codes_empty
|
||||
)
|
||||
adapter = PatternsAdapter(patterns)
|
||||
listView.adapter = adapter
|
||||
listView.setOnScrollListener(systemBarListViewScrollListener)
|
||||
listView.setOnItemClickListener { _, _, position, _ ->
|
||||
editPattern(position)
|
||||
}
|
||||
listView.setOnItemLongClickListener { _, _, position, _ ->
|
||||
confirmRemovePattern(position)
|
||||
true
|
||||
}
|
||||
|
||||
findViewById<View>(R.id.add).apply {
|
||||
contentDescription = getString(R.string.ignore_code_add)
|
||||
setOnClickListener {
|
||||
editPattern(null)
|
||||
}
|
||||
}
|
||||
|
||||
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
listView.setPaddingFromWindowInsets()
|
||||
}
|
||||
|
||||
private fun editPattern(position: Int?) {
|
||||
val pattern = position?.let { patterns[it] }
|
||||
val view = layoutInflater.inflate(R.layout.dialog_ignore_code, null)
|
||||
val regexView = view.findViewById<EditText>(R.id.regex)
|
||||
val formatView = view.findViewById<Spinner>(R.id.format)
|
||||
val values = setupFormatSpinner(formatView)
|
||||
if (pattern != null) {
|
||||
regexView.setText(pattern.pattern)
|
||||
regexView.setSelection(pattern.pattern.length)
|
||||
formatView.setSelection(
|
||||
values.indexOf(pattern.format ?: "").coerceAtLeast(0)
|
||||
)
|
||||
} else {
|
||||
formatView.setSelection(0)
|
||||
}
|
||||
|
||||
val dialog = MaterialAlertDialogBuilder(this)
|
||||
.setTitle(
|
||||
if (pattern == null) {
|
||||
R.string.ignore_code_add
|
||||
} else {
|
||||
R.string.ignore_code_edit
|
||||
}
|
||||
)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.create()
|
||||
|
||||
dialog.setOnShowListener {
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
|
||||
addOrUpdatePattern(position, regexView, formatView, values, dialog)
|
||||
}
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
private fun addOrUpdatePattern(
|
||||
position: Int?,
|
||||
regexView: EditText,
|
||||
formatView: Spinner,
|
||||
values: List<String>,
|
||||
dialog: Dialog,
|
||||
) {
|
||||
val pattern = regexView.text.toString().trim()
|
||||
if (pattern.isEmpty()) {
|
||||
regexView.error = getString(R.string.ignore_code_regex_required)
|
||||
return
|
||||
}
|
||||
try {
|
||||
Regex(pattern)
|
||||
} catch (e: Exception) {
|
||||
val message = e.message ?: getString(
|
||||
R.string.ignore_code_regex_invalid
|
||||
)
|
||||
regexView.error = message
|
||||
toast(message)
|
||||
return
|
||||
}
|
||||
val item = IgnoreCode(
|
||||
pattern = pattern,
|
||||
format = values[formatView.selectedItemPosition].ifEmpty { null }
|
||||
)
|
||||
if (position == null) {
|
||||
patterns.add(item)
|
||||
} else {
|
||||
patterns[position] = item
|
||||
}
|
||||
prefs.setIgnoreCodes(patterns)
|
||||
adapter.notifyDataSetChanged()
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
private fun confirmRemovePattern(position: Int) {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.ignore_code_remove_confirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
patterns.removeAt(position)
|
||||
prefs.setIgnoreCodes(patterns)
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
|
||||
private inner class PatternsAdapter(
|
||||
items: List<IgnoreCode>
|
||||
) : ArrayAdapter<IgnoreCode>(
|
||||
this,
|
||||
android.R.layout.simple_list_item_2,
|
||||
android.R.id.text1,
|
||||
items
|
||||
) {
|
||||
override fun getView(
|
||||
position: Int,
|
||||
convertView: View?,
|
||||
parent: ViewGroup
|
||||
): View {
|
||||
val view = super.getView(position, convertView, parent)
|
||||
val item = patterns[position]
|
||||
val title = view.findViewById<TextView>(android.R.id.text1)
|
||||
val subtitle = view.findViewById<TextView>(android.R.id.text2)
|
||||
title.text = item.pattern
|
||||
subtitle.text = item.format?.prettifyFormatName() ?: getString(
|
||||
R.string.all_formats
|
||||
)
|
||||
return view
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.PERMISSION_LOCATION
|
||||
import de.markusfisch.android.binaryeye.app.PERMISSION_WRITE
|
||||
import de.markusfisch.android.binaryeye.app.applyLocale
|
||||
import de.markusfisch.android.binaryeye.app.permissionGrantedCallback
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.app.setFragment
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import de.markusfisch.android.binaryeye.fragment.DecodeFragment
|
||||
import de.markusfisch.android.binaryeye.fragment.EncodeFragment
|
||||
import de.markusfisch.android.binaryeye.fragment.HistoryFragment
|
||||
import de.markusfisch.android.binaryeye.fragment.PreferencesFragment
|
||||
import de.markusfisch.android.binaryeye.net.isEncodeDeeplink
|
||||
import de.markusfisch.android.binaryeye.view.colorSystemAndToolBars
|
||||
import de.markusfisch.android.binaryeye.view.initBars
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
when (requestCode) {
|
||||
PERMISSION_LOCATION, PERMISSION_WRITE -> if (
|
||||
grantResults.isNotEmpty() &&
|
||||
grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
permissionGrantedCallback?.invoke()
|
||||
permissionGrantedCallback = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
val fm = supportFragmentManager
|
||||
if (fm != null && fm.backStackEntryCount > 0) {
|
||||
fm.popBackStack()
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
base?.applyLocale(prefs.customLocale)
|
||||
super.attachBaseContext(base)
|
||||
}
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
initBars()
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
supportFragmentManager.addOnBackStackChangedListener {
|
||||
colorSystemAndToolBars(this@MainActivity)
|
||||
}
|
||||
|
||||
if (state == null) {
|
||||
supportFragmentManager?.setFragment(getFragmentForIntent(intent))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREFERENCES = "preferences"
|
||||
private const val HISTORY = "history"
|
||||
private const val ENCODE = "encode"
|
||||
const val DECODED = "decoded"
|
||||
|
||||
private fun getFragmentForIntent(intent: Intent?): Fragment {
|
||||
intent ?: return PreferencesFragment()
|
||||
return when {
|
||||
intent.hasExtra(PREFERENCES) -> PreferencesFragment()
|
||||
intent.hasExtra(HISTORY) -> HistoryFragment()
|
||||
|
||||
intent.dataString?.let(::isEncodeDeeplink) == true -> {
|
||||
val uri = Uri.parse(intent.dataString)
|
||||
EncodeFragment.newInstance(
|
||||
content = uri.getQueryParameter("content"),
|
||||
format = uri.getQueryParameter("format")?.uppercase(),
|
||||
execute = uri.getQueryParameter("execute")
|
||||
.let { it == "" || it.toBoolean() }
|
||||
)
|
||||
}
|
||||
|
||||
intent.hasExtra(ENCODE) -> EncodeFragment.newInstance(
|
||||
intent.getStringExtra(ENCODE)
|
||||
)
|
||||
|
||||
intent.hasExtra(DECODED) -> intent.getScanExtra(DECODED)?.let {
|
||||
DecodeFragment.newInstance(it)
|
||||
} ?: DecodeFragment()
|
||||
|
||||
else -> PreferencesFragment()
|
||||
}
|
||||
}
|
||||
|
||||
fun getPreferencesIntent(context: Context): Intent {
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
intent.putExtra(PREFERENCES, true)
|
||||
return intent
|
||||
}
|
||||
|
||||
fun getHistoryIntent(context: Context): Intent {
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
intent.putExtra(HISTORY, true)
|
||||
return intent
|
||||
}
|
||||
|
||||
fun getEncodeIntent(
|
||||
context: Context,
|
||||
text: String? = "",
|
||||
isExternal: Boolean = false
|
||||
): Intent {
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
intent.putExtra(ENCODE, text)
|
||||
if (isExternal) {
|
||||
val flagActivityClearTask = if (
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB
|
||||
) {
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
} else 0
|
||||
intent.addFlags(
|
||||
Intent.FLAG_ACTIVITY_NO_HISTORY or
|
||||
flagActivityClearTask or
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
)
|
||||
}
|
||||
return intent
|
||||
}
|
||||
|
||||
fun getDecodeIntent(context: Context, scan: Scan): Intent {
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
intent.putExtra(DECODED, scan)
|
||||
return intent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Intent.getScanExtra(name: String): Scan? = try {
|
||||
if (
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
|
||||
) {
|
||||
@Suppress("DEPRECATION")
|
||||
getParcelableExtra(name)
|
||||
} else {
|
||||
getParcelableExtra(name, Scan::class.java)
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
+16
-29
@@ -1,43 +1,32 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.content.Context
|
||||
import android.net.wifi.WifiManager
|
||||
import android.net.wifi.WifiNetworkSuggestion
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.annotation.RequiresApi
|
||||
import android.support.v4.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ListView
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.util.size
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.view.systemBarListViewScrollListener
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
|
||||
class NetworkSuggestionsFragment : Fragment() {
|
||||
class NetworkSuggestionsActivity : AbstractBaseActivity() {
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
state: Bundle?
|
||||
): View? {
|
||||
val ac = activity ?: return null
|
||||
ac.setTitle(R.string.network_suggestions)
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setScreenContentView(R.layout.activity_network_suggestions)
|
||||
setTitle(R.string.network_suggestions)
|
||||
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_network_suggestions,
|
||||
container,
|
||||
false
|
||||
)
|
||||
|
||||
val wm = ac.applicationContext.getSystemService(
|
||||
val wm = applicationContext.getSystemService(
|
||||
Context.WIFI_SERVICE
|
||||
) as WifiManager
|
||||
val suggestionArrayAdapter = ArrayAdapter(
|
||||
ac,
|
||||
this,
|
||||
android.R.layout.simple_list_item_checked,
|
||||
wm.networkSuggestions.map {
|
||||
Suggestion(
|
||||
@@ -47,18 +36,18 @@ class NetworkSuggestionsFragment : Fragment() {
|
||||
}
|
||||
)
|
||||
|
||||
val listView = view.findViewById<ListView>(R.id.suggestions)
|
||||
listView.emptyView = view.findViewById(R.id.no_suggestions)
|
||||
val listView = findViewById<ListView>(R.id.suggestions)
|
||||
listView.emptyView = findViewById(R.id.no_suggestions)
|
||||
listView.adapter = suggestionArrayAdapter
|
||||
listView.setOnScrollListener(systemBarListViewScrollListener)
|
||||
view.findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
listView.setPaddingFromWindowInsets()
|
||||
|
||||
view.findViewById<View>(R.id.remove).setOnClickListener {
|
||||
findViewById<View>(R.id.remove).setOnClickListener {
|
||||
val removeList = ArrayList<WifiNetworkSuggestion>()
|
||||
val removeFromAdapter = ArrayList<Suggestion>()
|
||||
val checked = listView.checkedItemPositions
|
||||
val size = checked.size()
|
||||
val size = checked.size
|
||||
for (i in 0 until size) {
|
||||
if (checked.valueAt(i)) {
|
||||
val pos = checked.keyAt(i)
|
||||
@@ -76,13 +65,11 @@ class NetworkSuggestionsFragment : Fragment() {
|
||||
}
|
||||
wm.removeNetworkSuggestions(removeList)
|
||||
} else {
|
||||
ac.toast(
|
||||
toast(
|
||||
R.string.clear_network_suggestions_nothing_to_remove
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,10 @@ import android.graphics.RectF
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.applyLocale
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
@@ -109,7 +110,7 @@ class PickActivity : AppCompatActivity() {
|
||||
cropHandleName = "picker_crop_handle"
|
||||
}
|
||||
|
||||
findViewById(R.id.scan).setOnClickListener {
|
||||
findViewById<View>(R.id.scan).setOnClickListener {
|
||||
showResult()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.applyLocale
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.fragment.PreferencesFragment
|
||||
import de.markusfisch.android.binaryeye.view.initBars
|
||||
|
||||
class PreferencesActivity : AppCompatActivity() {
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
base?.applyLocale(prefs.customLocale)
|
||||
super.attachBaseContext(base)
|
||||
}
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setContentView(R.layout.activity_main)
|
||||
initBars()
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
if (state == null) {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.content_frame, PreferencesFragment())
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
finish()
|
||||
return true
|
||||
}
|
||||
}
|
||||
+46
-45
@@ -1,46 +1,36 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.EditText
|
||||
import android.widget.ListView
|
||||
import android.widget.TextView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.view.systemBarListViewScrollListener
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||
|
||||
class ProfilesFragment : Fragment() {
|
||||
class ProfilesActivity : AbstractBaseActivity() {
|
||||
private val profiles = ArrayList<ProfileItem>()
|
||||
|
||||
private lateinit var adapter: ProfilesAdapter
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
state: Bundle?
|
||||
): View? {
|
||||
val ac = activity ?: return null
|
||||
ac.setTitle(R.string.profile)
|
||||
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_profiles,
|
||||
container,
|
||||
false
|
||||
)
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setScreenContentView(R.layout.activity_profiles)
|
||||
setTitle(R.string.profile)
|
||||
|
||||
refreshProfiles()
|
||||
|
||||
val listView = view.findViewById<ListView>(R.id.profiles)
|
||||
listView.emptyView = view.findViewById(R.id.no_profiles)
|
||||
adapter = ProfilesAdapter(ac, profiles)
|
||||
val listView = findViewById<ListView>(R.id.profiles)
|
||||
listView.emptyView = findViewById(R.id.no_profiles)
|
||||
adapter = ProfilesAdapter(this, profiles)
|
||||
listView.adapter = adapter
|
||||
listView.setOnScrollListener(systemBarListViewScrollListener)
|
||||
listView.setOnItemClickListener { _, _, position, _ ->
|
||||
@@ -51,14 +41,12 @@ class ProfilesFragment : Fragment() {
|
||||
true
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.add).setOnClickListener {
|
||||
findViewById<View>(R.id.add).setOnClickListener {
|
||||
addProfile()
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
listView.setPaddingFromWindowInsets()
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
private fun refreshProfiles() {
|
||||
@@ -66,11 +54,11 @@ class ProfilesFragment : Fragment() {
|
||||
profiles.add(
|
||||
ProfileItem(
|
||||
null,
|
||||
getString(R.string.profile_default)
|
||||
prefs.profileLabel(this, null)
|
||||
)
|
||||
)
|
||||
prefs.profiles.forEach { name ->
|
||||
profiles.add(ProfileItem(name, name))
|
||||
profiles.add(ProfileItem(name, prefs.profileLabel(this, name)))
|
||||
}
|
||||
if (this::adapter.isInitialized) {
|
||||
adapter.notifyDataSetChanged()
|
||||
@@ -78,48 +66,45 @@ class ProfilesFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun selectProfile(position: Int) {
|
||||
val ac = activity ?: return
|
||||
val profile = profiles[position].name
|
||||
if (prefs.profile == profile) {
|
||||
return
|
||||
}
|
||||
prefs.load(ac, profile)
|
||||
prefs.load(this, profile)
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
// Dialogs do not have a parent view.
|
||||
@Suppress("InflateParams")
|
||||
private fun addProfile() {
|
||||
val ac = activity ?: return
|
||||
val view = ac.layoutInflater.inflate(
|
||||
val view = layoutInflater.inflate(
|
||||
R.layout.dialog_profile_name, null
|
||||
)
|
||||
val editText = view.findViewById<EditText>(R.id.name)
|
||||
AlertDialog.Builder(ac)
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val name = editText.text.toString().trim()
|
||||
if (name.isNotEmpty() && prefs.addProfile(name)) {
|
||||
prefs.load(ac, name)
|
||||
prefs.load(this, name)
|
||||
refreshProfiles()
|
||||
} else {
|
||||
ac.toast(R.string.profile_invalid_name)
|
||||
toast(R.string.profile_invalid_name)
|
||||
}
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun confirmRemoveProfile(position: Int) {
|
||||
val ac = activity ?: return
|
||||
val profile = profiles[position]
|
||||
val profileName = profile.name ?: return
|
||||
AlertDialog.Builder(ac)
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.profile_remove_confirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val isCurrent = prefs.profile == profileName
|
||||
prefs.removeProfile(profileName)
|
||||
if (isCurrent) {
|
||||
prefs.load(ac, null)
|
||||
prefs.load(this, null)
|
||||
}
|
||||
refreshProfiles()
|
||||
}
|
||||
@@ -132,8 +117,8 @@ class ProfilesFragment : Fragment() {
|
||||
private val items: List<ProfileItem>
|
||||
) : ArrayAdapter<ProfileItem>(
|
||||
context,
|
||||
android.R.layout.simple_list_item_2,
|
||||
android.R.id.text1,
|
||||
R.layout.row_profile,
|
||||
R.id.title,
|
||||
items
|
||||
) {
|
||||
override fun getView(
|
||||
@@ -145,6 +130,7 @@ class ProfilesFragment : Fragment() {
|
||||
val item = items[position]
|
||||
val isCurrent = item.name == prefs.profile
|
||||
getViewHolder(view).apply {
|
||||
current = item
|
||||
title.text = item.label
|
||||
if (isCurrent) {
|
||||
subtitle.text = context.getString(R.string.profile_current)
|
||||
@@ -160,16 +146,31 @@ class ProfilesFragment : Fragment() {
|
||||
private fun getViewHolder(
|
||||
view: View
|
||||
): ViewHolder = view.tag as ViewHolder? ?: ViewHolder(
|
||||
view.findViewById(android.R.id.text1),
|
||||
view.findViewById(android.R.id.text2),
|
||||
).also {
|
||||
view.tag = it
|
||||
view.findViewById(R.id.title),
|
||||
view.findViewById(R.id.subtitle),
|
||||
view.findViewById(R.id.share_qr),
|
||||
).also { holder ->
|
||||
holder.shareQr.setOnClickListener {
|
||||
val item = holder.current ?: return@setOnClickListener
|
||||
context.startActivity(
|
||||
BarcodeActivity.newIntent(
|
||||
context,
|
||||
prefs.toJson(context, item.name),
|
||||
BarcodeFormat.QRCode,
|
||||
addQuietZone = true
|
||||
)
|
||||
)
|
||||
}
|
||||
view.tag = holder
|
||||
}
|
||||
|
||||
private data class ViewHolder(
|
||||
private class ViewHolder(
|
||||
val title: TextView,
|
||||
val subtitle: TextView,
|
||||
)
|
||||
val shareQr: View,
|
||||
) {
|
||||
var current: ProfileItem? = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
||||
class SplashActivity : AppCompatActivity() {
|
||||
override fun onCreate(state: Bundle?) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
|
||||
@@ -9,23 +10,12 @@ class WelcomeActivity : AppCompatActivity() {
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
setContentView(R.layout.activity_welcome)
|
||||
findViewById(R.id.expert).setOnClickListener {
|
||||
findViewById<View>(R.id.expert).setOnClickListener {
|
||||
prefs.loadExpertProfile(this)
|
||||
finish()
|
||||
}
|
||||
findViewById(R.id.simple).setOnClickListener {
|
||||
prefs.apply {
|
||||
showCropHandle = false
|
||||
zoomBySwiping = false
|
||||
beep = true
|
||||
copyImmediately = true
|
||||
openImmediately = true
|
||||
showMetaData = false
|
||||
showHexDump = false
|
||||
defaultSearchUrl = "https://www.google.com/search?q="
|
||||
openWithUrl = defaultSearchUrl
|
||||
expandEscapeSequences = false
|
||||
brightenScreen = true
|
||||
}
|
||||
findViewById<View>(R.id.simple).setOnClickListener {
|
||||
prefs.loadSimpleProfile(this)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,14 @@ package de.markusfisch.android.binaryeye.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.CursorAdapter
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.database.Database
|
||||
@@ -15,18 +18,19 @@ import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class ScansAdapter(context: Context, cursor: Cursor) :
|
||||
class ScansAdapter(
|
||||
context: Context,
|
||||
cursor: Cursor,
|
||||
private val onPinClick: (Long, Boolean) -> Unit
|
||||
) :
|
||||
CursorAdapter(context, cursor, false) {
|
||||
private val idIndex = cursor.getColumnIndex(Database.SCANS_ID)
|
||||
private val pinnedIndex = cursor.getColumnIndex(Database.SCANS_PINNED)
|
||||
private val timeIndex = cursor.getColumnIndex(Database.SCANS_DATETIME)
|
||||
private val nameIndex = cursor.getColumnIndex(Database.SCANS_NAME)
|
||||
private val textIndex = cursor.getColumnIndex(Database.SCANS_TEXT)
|
||||
private val formatIndex = cursor.getColumnIndex(Database.SCANS_FORMAT)
|
||||
private val selections = mutableMapOf<Long, Int>()
|
||||
private val selectedColor = ContextCompat.getColor(
|
||||
context, R.color.selected_row
|
||||
)
|
||||
|
||||
fun select(view: View, id: Long, position: Int) {
|
||||
view.select(
|
||||
if (selections[id] == null) {
|
||||
@@ -89,8 +93,16 @@ class ScansAdapter(context: Context, cursor: Cursor) :
|
||||
context: Context,
|
||||
cursor: Cursor
|
||||
) {
|
||||
val id = cursor.getLong(idIndex)
|
||||
val pinned = cursor.getInt(pinnedIndex) != 0
|
||||
val format = cursor.getString(formatIndex)
|
||||
getViewHolder(view).apply {
|
||||
timeView.text = formatDateTime(cursor.getString(timeIndex))
|
||||
metaView.text = context.getString(
|
||||
R.string.scan_meta_data,
|
||||
formatDateTime(cursor.getString(timeIndex)),
|
||||
format.prettifyFormatName()
|
||||
)
|
||||
formatIconView.setImageResource(format.toBarcodeIcon())
|
||||
val name = cursor.getString(nameIndex)
|
||||
val text = cursor.getString(textIndex)
|
||||
var icon = 0
|
||||
@@ -109,9 +121,27 @@ class ScansAdapter(context: Context, cursor: Cursor) :
|
||||
contentView.setCompoundDrawablesWithIntrinsicBounds(
|
||||
icon, 0, 0, 0
|
||||
)
|
||||
formatView.text = cursor.getString(formatIndex).prettifyFormatName()
|
||||
pinView.apply {
|
||||
setImageResource(
|
||||
if (pinned) {
|
||||
R.drawable.ic_action_pin
|
||||
} else {
|
||||
R.drawable.ic_action_pin_outline
|
||||
}
|
||||
)
|
||||
contentDescription = context.getString(
|
||||
if (pinned) {
|
||||
R.string.unpin_item
|
||||
} else {
|
||||
R.string.pin_item
|
||||
}
|
||||
)
|
||||
setOnClickListener {
|
||||
onPinClick(id, !pinned)
|
||||
}
|
||||
}
|
||||
}
|
||||
val selected = selections[cursor.getLong(idIndex)] != null
|
||||
val selected = selections[id] != null
|
||||
view.post {
|
||||
// Needs to be put on the queue to work.
|
||||
view.select(selected)
|
||||
@@ -119,27 +149,107 @@ class ScansAdapter(context: Context, cursor: Cursor) :
|
||||
}
|
||||
|
||||
private fun View.select(selected: Boolean) {
|
||||
setBackgroundColor(if (selected) selectedColor else 0)
|
||||
isActivated = selected
|
||||
refreshDrawableState()
|
||||
}
|
||||
|
||||
private fun getViewHolder(
|
||||
view: View
|
||||
): ViewHolder = view.tag as ViewHolder? ?: ViewHolder(
|
||||
view.findViewById(R.id.time),
|
||||
view.findViewById(R.id.format_icon),
|
||||
view.findViewById(R.id.meta),
|
||||
view.findViewById(R.id.content),
|
||||
view.findViewById(R.id.format)
|
||||
view.findViewById(R.id.pin)
|
||||
).also {
|
||||
view.tag = it
|
||||
}
|
||||
|
||||
private data class ViewHolder(
|
||||
val timeView: TextView,
|
||||
val formatIconView: ImageView,
|
||||
val metaView: TextView,
|
||||
val contentView: TextView,
|
||||
val formatView: TextView
|
||||
val pinView: ImageButton
|
||||
)
|
||||
}
|
||||
|
||||
private val formatWordBoundary =
|
||||
"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|(?<=[A-Za-z])(?=\\d)|(?<=\\d)(?=[A-Za-z])".toRegex()
|
||||
|
||||
fun Context.setupFormatSpinner(spinner: Spinner): List<String> {
|
||||
val names = resources.getStringArray(
|
||||
R.array.barcode_formats_names
|
||||
).toMutableList().apply {
|
||||
add(0, getString(R.string.all_formats))
|
||||
}
|
||||
val values = resources.getStringArray(
|
||||
R.array.barcode_formats_values
|
||||
).toMutableList().apply {
|
||||
add(0, "")
|
||||
}
|
||||
spinner.adapter = ArrayAdapter(
|
||||
this,
|
||||
android.R.layout.simple_spinner_item,
|
||||
names
|
||||
).apply {
|
||||
setDropDownViewResource(
|
||||
android.R.layout.simple_spinner_dropdown_item
|
||||
)
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
fun String.prettifyFormatName() = replace("_", " ")
|
||||
.replace(formatWordBoundary, " ")
|
||||
|
||||
fun String.toFormatDescriptionResId(): Int = when (this) {
|
||||
"Aztec" -> R.string.format_description_aztec
|
||||
"Codabar" -> R.string.format_description_codabar
|
||||
"Code39",
|
||||
"Code39Std" -> R.string.format_description_code39
|
||||
|
||||
"Code39Ext" -> R.string.format_description_code39ext
|
||||
"Code32" -> R.string.format_description_code32
|
||||
"PZN" -> R.string.format_description_pzn
|
||||
"Code93" -> R.string.format_description_code93
|
||||
"Code128" -> R.string.format_description_code128
|
||||
"DataBar" -> R.string.format_description_databar
|
||||
"DataBarOmni" -> R.string.format_description_databar_omni
|
||||
"DataBarStk" -> R.string.format_description_databar_stk
|
||||
"DataBarLtd" -> R.string.format_description_databar_ltd
|
||||
"DataBarExp" -> R.string.format_description_databar_exp
|
||||
"DataBarExpStk" -> R.string.format_description_databar_exp_stk
|
||||
"DataMatrix" -> R.string.format_description_data_matrix
|
||||
"DXFilmEdge" -> R.string.format_description_dx_film_edge
|
||||
"EAN8" -> R.string.format_description_ean8
|
||||
"EAN13" -> R.string.format_description_ean13
|
||||
"ITF" -> R.string.format_description_itf
|
||||
"MaxiCode" -> R.string.format_description_maxicode
|
||||
"PDF417" -> R.string.format_description_pdf417
|
||||
"Telepen" -> R.string.format_description_telepen
|
||||
"TelepenAlpha" -> R.string.format_description_telepen_alpha
|
||||
"TelepenNumeric" -> R.string.format_description_telepen_numeric
|
||||
"QRCode" -> R.string.format_description_qr_code
|
||||
"MicroQRCode" -> R.string.format_description_micro_qr_code
|
||||
"RMQRCode" -> R.string.format_description_rmqr_code
|
||||
"UPCA" -> R.string.format_description_upca
|
||||
"UPCE" -> R.string.format_description_upce
|
||||
else -> 0
|
||||
}
|
||||
|
||||
private fun String?.toBarcodeIcon(): Int = when (this) {
|
||||
"QRCode",
|
||||
"RMQRCode" -> R.drawable.ic_barcode_qr_code
|
||||
|
||||
"MicroQRCode" -> R.drawable.ic_barcode_micro_qr
|
||||
|
||||
"Aztec" -> R.drawable.ic_barcode_aztec
|
||||
"DataMatrix",
|
||||
"MaxiCode" -> R.drawable.ic_barcode_data_matrix
|
||||
|
||||
"PDF417" -> R.drawable.ic_barcode_pdf417
|
||||
|
||||
else -> R.drawable.ic_barcode_linear
|
||||
}
|
||||
|
||||
private fun formatDateTime(rfc: String): String {
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.resume
|
||||
@@ -9,10 +9,10 @@ import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
suspend inline fun <T : Any> alertDialog(
|
||||
context: Context,
|
||||
crossinline build: AlertDialog.Builder.(resume: (T?) -> Unit) -> Unit
|
||||
crossinline build: MaterialAlertDialogBuilder.(resume: (T?) -> Unit) -> Unit
|
||||
): T? = withContext(Dispatchers.Main) {
|
||||
suspendCoroutine { continuation ->
|
||||
AlertDialog.Builder(context).apply {
|
||||
MaterialAlertDialogBuilder(context).apply {
|
||||
setOnCancelListener {
|
||||
continuation.resume(null)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.app.Application
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import de.markusfisch.android.binaryeye.database.Database
|
||||
import de.markusfisch.android.binaryeye.preference.Preferences
|
||||
|
||||
@@ -10,7 +11,11 @@ val prefs = Preferences()
|
||||
class BinaryEyeApp : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
db.open(this)
|
||||
prefs.init(this)
|
||||
DynamicColors.applyToActivitiesIfAvailable(
|
||||
this,
|
||||
DynamicColors.Precondition { _, _ -> prefs.dynamicColors }
|
||||
)
|
||||
db.open(this)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.app.FragmentManager
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
fun FragmentManager.setFragment(fragment: Fragment) {
|
||||
getTransaction(fragment).commit()
|
||||
}
|
||||
|
||||
fun FragmentManager.addFragment(fragment: Fragment) {
|
||||
getTransaction(fragment).addToBackStack(null).commit()
|
||||
}
|
||||
|
||||
@SuppressLint("CommitTransaction")
|
||||
private fun FragmentManager.getTransaction(
|
||||
fragment: Fragment
|
||||
) = beginTransaction().replace(R.id.content_frame, fragment)
|
||||
@@ -4,8 +4,8 @@ import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.support.v4.app.ActivityCompat
|
||||
import android.support.v4.content.ContextCompat
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
var permissionGrantedCallback: (() -> Any)? = null
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import java.util.Locale
|
||||
data class AutomatedAction(
|
||||
val pattern: String,
|
||||
val type: Type,
|
||||
val format: String? = null,
|
||||
val urlTemplate: String? = null,
|
||||
val intentUriTemplate: String? = null
|
||||
) {
|
||||
@@ -16,8 +17,9 @@ data class AutomatedAction(
|
||||
Intent, CustomIntent
|
||||
}
|
||||
|
||||
fun matches(content: String): Boolean = try {
|
||||
Regex(pattern).matches(content)
|
||||
fun matches(content: String, format: String): Boolean = try {
|
||||
(this.format == null || this.format == format) &&
|
||||
Regex(pattern).matches(content)
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
@@ -25,6 +27,7 @@ data class AutomatedAction(
|
||||
fun toJson(): JSONObject = JSONObject().apply {
|
||||
put(KEY_PATTERN, pattern)
|
||||
put(KEY_TYPE, type.name.lowercase(Locale.ROOT))
|
||||
put(KEY_FORMAT, format ?: "")
|
||||
put(KEY_URL_TEMPLATE, urlTemplate ?: "")
|
||||
put(KEY_INTENT_URI_TEMPLATE, intentUriTemplate ?: "")
|
||||
}
|
||||
@@ -32,6 +35,7 @@ data class AutomatedAction(
|
||||
companion object {
|
||||
private const val KEY_PATTERN = "pattern"
|
||||
private const val KEY_TYPE = "type"
|
||||
private const val KEY_FORMAT = "format"
|
||||
private const val KEY_URL_TEMPLATE = "url_template"
|
||||
private const val KEY_INTENT_URI_TEMPLATE = "intent_uri_template"
|
||||
|
||||
@@ -46,6 +50,7 @@ data class AutomatedAction(
|
||||
"html" -> return null
|
||||
else -> Type.Intent
|
||||
}
|
||||
val format = obj.optString(KEY_FORMAT, "").trim()
|
||||
val urlTemplate = obj.optString(KEY_URL_TEMPLATE, "").trim()
|
||||
val intentUriTemplate = obj.optString(
|
||||
KEY_INTENT_URI_TEMPLATE,
|
||||
@@ -54,6 +59,7 @@ data class AutomatedAction(
|
||||
return AutomatedAction(
|
||||
pattern = pattern,
|
||||
type = type,
|
||||
format = format.ifEmpty { null },
|
||||
urlTemplate = urlTemplate.ifEmpty { null },
|
||||
intentUriTemplate = intentUriTemplate.ifEmpty { null }
|
||||
)
|
||||
|
||||
+3
-3
@@ -4,8 +4,8 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.content.execShareIntent
|
||||
import de.markusfisch.android.binaryeye.content.openUrl
|
||||
import de.markusfisch.android.binaryeye.content.startIntentOrToast
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
|
||||
@@ -19,7 +19,7 @@ fun Context.runAutomatedActions(scan: Scan): Boolean {
|
||||
return false
|
||||
}
|
||||
for (action in actions) {
|
||||
if (action.matches(content)) {
|
||||
if (action.matches(content, scan.format.name)) {
|
||||
return when (action.type) {
|
||||
AutomatedAction.Type.Intent -> {
|
||||
val url = action.urlTemplate?.buildUrl(scan)
|
||||
@@ -55,5 +55,5 @@ private fun Context.execCustomIntent(
|
||||
toast(R.string.automated_action_intent_uri_invalid)
|
||||
return false
|
||||
}
|
||||
return execShareIntent(intent)
|
||||
return startIntentOrToast(intent)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package de.markusfisch.android.binaryeye.bluetooth
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.bluetooth.BluetoothSocket
|
||||
import android.support.v7.preference.ListPreference
|
||||
import androidx.preference.ListPreference
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package de.markusfisch.android.binaryeye.content
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.util.Size
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import de.markusfisch.android.binaryeye.graphics.COLOR_BLACK
|
||||
import de.markusfisch.android.binaryeye.graphics.COLOR_WHITE
|
||||
@@ -48,7 +49,8 @@ fun Scan.toBarcode(): Barcode<*> {
|
||||
ContentBarcode(
|
||||
content,
|
||||
format,
|
||||
errorCorrectionLevel.toErrorCorrectionInt()
|
||||
errorCorrectionLevel.toErrorCorrectionInt(),
|
||||
addQuietZone = true
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -108,21 +110,24 @@ class ContentBarcode<T>(
|
||||
content: T,
|
||||
format: BarcodeFormat,
|
||||
val ecLevel: Int = -1,
|
||||
val margin: Int = 1,
|
||||
val addQuietZone: Boolean = false,
|
||||
colors: BarcodeColors = BarcodeColors.BLACK_ON_WHITE
|
||||
) : Barcode<T>(content, format, colors) {
|
||||
override fun toBitmap(size: Int) = ZxingCpp.encodeAsBitmap(
|
||||
content, format, size, format.height(size), margin, ecLevel,
|
||||
setColor = colors.foregroundColor(),
|
||||
unsetColor = colors.backgroundColor()
|
||||
)
|
||||
override fun toBitmap(size: Int): Bitmap {
|
||||
val s = format.resolveSize(size)
|
||||
return ZxingCpp.encodeAsBitmap(
|
||||
content, format, s.width, s.height, addQuietZone, ecLevel,
|
||||
setColor = colors.foregroundColor(),
|
||||
unsetColor = colors.backgroundColor()
|
||||
)
|
||||
}
|
||||
|
||||
override fun toSvg() = ZxingCpp.encodeAsSvg(
|
||||
content, format, margin, ecLevel
|
||||
content, format, addQuietZone, ecLevel
|
||||
)
|
||||
|
||||
override fun toText() = ZxingCpp.encodeAsText(
|
||||
content, format, margin, ecLevel,
|
||||
content, format, addQuietZone, ecLevel,
|
||||
inverted = colors == BarcodeColors.BLACK_ON_WHITE
|
||||
)
|
||||
}
|
||||
@@ -184,20 +189,33 @@ private fun BitMatrix.inflate(
|
||||
return scaled
|
||||
}
|
||||
|
||||
private fun BarcodeFormat.height(size: Int) = when (this) {
|
||||
private fun BarcodeFormat.resolveSize(size: Int): Size = when (this) {
|
||||
// 1D barcodes don't need as much vertical space.
|
||||
BarcodeFormat.CODABAR,
|
||||
BarcodeFormat.CODE_39,
|
||||
BarcodeFormat.CODE_93,
|
||||
BarcodeFormat.CODE_128,
|
||||
BarcodeFormat.DATA_BAR,
|
||||
BarcodeFormat.DATA_BAR_EXPANDED,
|
||||
BarcodeFormat.DX_FILM_EDGE,
|
||||
BarcodeFormat.EAN_8,
|
||||
BarcodeFormat.EAN_13,
|
||||
BarcodeFormat.Codabar,
|
||||
BarcodeFormat.Code39,
|
||||
BarcodeFormat.Code39Std,
|
||||
BarcodeFormat.Code39Ext,
|
||||
BarcodeFormat.Code32,
|
||||
BarcodeFormat.PZN,
|
||||
BarcodeFormat.Code93,
|
||||
BarcodeFormat.Code128,
|
||||
BarcodeFormat.DataBarOmni,
|
||||
BarcodeFormat.DataBarStk,
|
||||
BarcodeFormat.DataBarLtd,
|
||||
BarcodeFormat.DataBarExp,
|
||||
BarcodeFormat.DataBarExpStk,
|
||||
BarcodeFormat.DXFilmEdge,
|
||||
BarcodeFormat.EAN8,
|
||||
BarcodeFormat.EAN13,
|
||||
BarcodeFormat.ITF,
|
||||
BarcodeFormat.UPC_A,
|
||||
BarcodeFormat.UPC_E -> size / 3
|
||||
BarcodeFormat.Telepen,
|
||||
BarcodeFormat.TelepenAlpha,
|
||||
BarcodeFormat.TelepenNumeric,
|
||||
BarcodeFormat.UPCA,
|
||||
BarcodeFormat.UPCE -> Size(size, size / 3)
|
||||
|
||||
else -> size
|
||||
}
|
||||
// PDF417 requires more horizontal space.
|
||||
BarcodeFormat.PDF417 -> Size(size * 3, size / 2)
|
||||
|
||||
else -> Size(size, size)
|
||||
}
|
||||
|
||||
@@ -8,16 +8,11 @@ import android.os.PersistableBundle
|
||||
|
||||
fun Context.copyToClipboard(text: String, isSensitive: Boolean = false) {
|
||||
val service = getSystemService(Context.CLIPBOARD_SERVICE)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
|
||||
@Suppress("DEPRECATION")
|
||||
(service as android.text.ClipboardManager).text = text
|
||||
} else {
|
||||
(service as android.content.ClipboardManager).setPrimaryClip(
|
||||
ClipData.newPlainText("plain text", text).apply {
|
||||
setSensitive(isSensitive)
|
||||
}
|
||||
)
|
||||
}
|
||||
(service as android.content.ClipboardManager).setPrimaryClip(
|
||||
ClipData.newPlainText("plain text", text).apply {
|
||||
setSensitive(isSensitive)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun ClipData.setSensitive(isSensitive: Boolean) {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package de.markusfisch.android.binaryeye.content
|
||||
|
||||
import android.content.Context
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
private val elementToLabel = mapOf(
|
||||
EpcQrElement.SERVICE_TAG to R.string.epc_service_tag,
|
||||
EpcQrElement.VERSION to R.string.epc_version,
|
||||
EpcQrElement.CHARSET to R.string.epc_charset,
|
||||
EpcQrElement.IDENTIFICATION to R.string.epc_identification,
|
||||
EpcQrElement.BIC to R.string.epc_bic,
|
||||
EpcQrElement.NAME to R.string.epc_name,
|
||||
EpcQrElement.IBAN to R.string.epc_iban,
|
||||
EpcQrElement.AMOUNT to R.string.epc_amount,
|
||||
EpcQrElement.PURPOSE to R.string.epc_purpose,
|
||||
EpcQrElement.REFERENCE to R.string.epc_reference,
|
||||
EpcQrElement.TEXT to R.string.epc_text,
|
||||
EpcQrElement.INFORMATION to R.string.epc_information
|
||||
)
|
||||
|
||||
fun Context.epcQrToRes(element: EpcQrElement): String {
|
||||
return elementToLabel[element]?.let { getString(it) } ?: element.name
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package de.markusfisch.android.binaryeye.content
|
||||
|
||||
enum class EpcQrElement {
|
||||
SERVICE_TAG, VERSION, CHARSET, IDENTIFICATION,
|
||||
BIC, NAME, IBAN, AMOUNT, PURPOSE, REFERENCE, TEXT, INFORMATION
|
||||
}
|
||||
|
||||
object EpcQrParser {
|
||||
private val BIC_REGEX = Regex("[A-Z0-9]{8}([A-Z0-9]{3})?")
|
||||
private val IBAN_REGEX = Regex("[A-Z]{2}[0-9A-Z]{13,32}")
|
||||
private val AMOUNT_REGEX = Regex("EUR\\d{1,10}(\\.\\d{2})?")
|
||||
private val PURPOSE_REGEX = Regex("[A-Z]{4}")
|
||||
|
||||
private val charsetMap = mapOf(
|
||||
"1" to "UTF-8",
|
||||
"2" to "ISO 8859-1",
|
||||
"3" to "ISO 8859-2",
|
||||
"4" to "ISO 8859-4",
|
||||
"5" to "ISO 8859-5",
|
||||
"6" to "ISO 8859-7",
|
||||
"7" to "ISO 8859-10",
|
||||
"8" to "ISO 8859-15"
|
||||
)
|
||||
|
||||
private fun splitLines(s: String): List<String>? {
|
||||
val normalized = s
|
||||
.replace("\r\n", "\n")
|
||||
.replace('\r', '\n')
|
||||
.removeSuffix("\n")
|
||||
if (normalized.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
return normalized.split('\n')
|
||||
}
|
||||
|
||||
private fun isValidVersion(value: String): Boolean {
|
||||
return value == "001" || value == "002"
|
||||
}
|
||||
|
||||
private fun isValidCharset(value: String): Boolean {
|
||||
return charsetMap.containsKey(value)
|
||||
}
|
||||
|
||||
private fun isValidIdentification(value: String): Boolean {
|
||||
return value == "SCT"
|
||||
}
|
||||
|
||||
private fun isValidBic(value: String): Boolean {
|
||||
return value.matches(BIC_REGEX)
|
||||
}
|
||||
|
||||
private fun isValidIban(value: String): Boolean {
|
||||
return value.matches(IBAN_REGEX)
|
||||
}
|
||||
|
||||
private fun isValidAmount(value: String): Boolean {
|
||||
return value.matches(AMOUNT_REGEX)
|
||||
}
|
||||
|
||||
fun parse(s: String?): Map<EpcQrElement, String>? {
|
||||
if (s.isNullOrEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val lines = splitLines(s) ?: return null
|
||||
if (lines.size !in 7..12) {
|
||||
return null
|
||||
}
|
||||
if (lines[0] != "BCD") {
|
||||
return null
|
||||
}
|
||||
if (!isValidVersion(lines[1])) {
|
||||
return null
|
||||
}
|
||||
if (!isValidCharset(lines[2])) {
|
||||
return null
|
||||
}
|
||||
if (!isValidIdentification(lines[3])) {
|
||||
return null
|
||||
}
|
||||
|
||||
val bic = lines.getOrElse(4) { "" }
|
||||
if ((lines[1] == "001" || bic.isNotEmpty()) && !isValidBic(bic)) {
|
||||
return null
|
||||
}
|
||||
|
||||
val recipient = lines.getOrElse(5) { "" }
|
||||
if (recipient.isBlank() || recipient.length > 70) {
|
||||
return null
|
||||
}
|
||||
|
||||
val iban = lines.getOrElse(6) { "" }
|
||||
if (!isValidIban(iban)) {
|
||||
return null
|
||||
}
|
||||
|
||||
val amount = lines.getOrElse(7) { "" }
|
||||
if (amount.isNotEmpty() && !isValidAmount(amount)) {
|
||||
return null
|
||||
}
|
||||
|
||||
val purpose = lines.getOrElse(8) { "" }
|
||||
if (purpose.isNotEmpty() && !purpose.matches(PURPOSE_REGEX)) {
|
||||
return null
|
||||
}
|
||||
|
||||
val remittanceReference = lines.getOrElse(9) { "" }
|
||||
if (remittanceReference.length > 25) {
|
||||
return null
|
||||
}
|
||||
|
||||
val remittanceText = lines.getOrElse(10) { "" }
|
||||
if (remittanceText.length > 140) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (remittanceReference.isNotEmpty() && remittanceText.isNotEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val information = lines.getOrElse(11) { "" }
|
||||
if (information.length > 70) {
|
||||
return null
|
||||
}
|
||||
|
||||
return linkedMapOf(
|
||||
EpcQrElement.SERVICE_TAG to lines[0],
|
||||
EpcQrElement.VERSION to lines[1],
|
||||
EpcQrElement.CHARSET to (charsetMap[lines[2]] ?: lines[2]),
|
||||
EpcQrElement.IDENTIFICATION to lines[3],
|
||||
EpcQrElement.BIC to bic,
|
||||
EpcQrElement.NAME to recipient,
|
||||
EpcQrElement.IBAN to iban,
|
||||
EpcQrElement.AMOUNT to amount.removePrefix("EUR"),
|
||||
EpcQrElement.PURPOSE to purpose,
|
||||
EpcQrElement.REFERENCE to remittanceReference,
|
||||
EpcQrElement.TEXT to remittanceText,
|
||||
EpcQrElement.INFORMATION to information
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
package de.markusfisch.android.binaryeye.content
|
||||
|
||||
import android.content.Context
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.tsenger.vdstools.DataEncoder
|
||||
import de.tsenger.vdstools.Verifier
|
||||
import de.tsenger.vdstools.idb.IdbSeal
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier
|
||||
import org.bouncycastle.asn1.icao.CscaMasterList
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
|
||||
import org.bouncycastle.asn1.x9.ECNamedCurveTable
|
||||
import org.bouncycastle.asn1.x9.X962Parameters
|
||||
import org.bouncycastle.cert.X509CertificateHolder
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
||||
import org.bouncycastle.cms.CMSSignedData
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import java.security.Provider
|
||||
import java.security.Security
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.Date
|
||||
|
||||
object IdbVerifier {
|
||||
private const val MASTER_LIST_ASSET = "DE_ML_2026-01-08-12-20-54.ml"
|
||||
|
||||
private val provider: Provider by lazy {
|
||||
BouncyCastleProvider().also { bcProvider ->
|
||||
if (Security.getProvider(bcProvider.name) == null) {
|
||||
Security.addProvider(bcProvider)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Volatile
|
||||
private var cachedMasterListCertificates: List<X509Certificate>? = null
|
||||
|
||||
fun verify(context: Context, seal: IdbSeal): List<SealField> {
|
||||
val signatureInfo = seal.signatureInfo
|
||||
?: return listOf(
|
||||
SealField(
|
||||
R.string.idb_verification,
|
||||
context.getString(R.string.idb_verification_unsigned)
|
||||
)
|
||||
)
|
||||
val headerCertificateReference = seal.signerCertReference
|
||||
?.hexToByteArrayOrNull()
|
||||
val signerCertificate = context.findCertificateByReference(
|
||||
headerCertificateReference
|
||||
)
|
||||
val curveName = signerCertificate?.let {
|
||||
curveNameFrom(it)
|
||||
}
|
||||
val signedBytes = seal.signedBytes
|
||||
val signatureResult = if (
|
||||
signerCertificate == null ||
|
||||
curveName.isNullOrBlank() ||
|
||||
signedBytes == null
|
||||
) {
|
||||
Verifier.Result.VerifyError
|
||||
} else {
|
||||
Verifier(
|
||||
signedBytes,
|
||||
signatureInfo.plainSignatureBytes,
|
||||
signerCertificate.publicKey.encoded,
|
||||
curveName
|
||||
).verify()
|
||||
}
|
||||
val referenceMatches = signerCertificate != null &&
|
||||
headerCertificateReference != null &&
|
||||
DataEncoder.buildCertificateReference(signerCertificate.encoded)
|
||||
.contentEquals(headerCertificateReference)
|
||||
val certificateValidNow = signerCertificate != null && runCatching {
|
||||
signerCertificate.checkValidity(Date())
|
||||
true
|
||||
}.getOrElse {
|
||||
false
|
||||
}
|
||||
val verification = context.getString(
|
||||
when {
|
||||
signerCertificate == null ->
|
||||
R.string.idb_verification_signer_certificate_issuer_not_found_in_master_list
|
||||
|
||||
!referenceMatches ->
|
||||
R.string.idb_verification_signature_invalid
|
||||
|
||||
!certificateValidNow ->
|
||||
R.string.idb_verification_signature_invalid
|
||||
|
||||
signatureResult == Verifier.Result.SignatureValid ->
|
||||
R.string.idb_verification_verified
|
||||
|
||||
signatureResult == Verifier.Result.SignatureInvalid ->
|
||||
R.string.idb_verification_signature_invalid
|
||||
|
||||
else -> R.string.idb_verification_error
|
||||
}
|
||||
)
|
||||
|
||||
val fields = mutableListOf(
|
||||
SealField(R.string.idb_verification, verification),
|
||||
SealField(
|
||||
R.string.idb_certificate_reference,
|
||||
signatureInfo.signerCertificateReference
|
||||
),
|
||||
SealField(
|
||||
R.string.vds_signing_date,
|
||||
signatureInfo.signingDate.toString()
|
||||
),
|
||||
SealField(
|
||||
R.string.idb_signature_verification,
|
||||
signatureResult.name
|
||||
),
|
||||
)
|
||||
if (signerCertificate != null) {
|
||||
fields.add(
|
||||
SealField(
|
||||
R.string.idb_signer_certificate_subject,
|
||||
signerCertificate.subjectX500Principal.name
|
||||
)
|
||||
)
|
||||
fields.add(
|
||||
SealField(
|
||||
R.string.idb_signer_certificate_issuer,
|
||||
signerCertificate.issuerX500Principal.name
|
||||
)
|
||||
)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
private fun String.hexToByteArrayOrNull(): ByteArray? {
|
||||
if (length % 2 != 0) {
|
||||
return null
|
||||
}
|
||||
return runCatching {
|
||||
ByteArray(length / 2) { index ->
|
||||
substring(index * 2, index * 2 + 2).toInt(16).toByte()
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private fun Context.loadMasterListCertificates(): List<X509Certificate> {
|
||||
cachedMasterListCertificates?.let {
|
||||
return it
|
||||
}
|
||||
synchronized(this) {
|
||||
cachedMasterListCertificates?.let {
|
||||
return it
|
||||
}
|
||||
val cmsSignedData = assets.open(MASTER_LIST_ASSET).use { input ->
|
||||
CMSSignedData(input.readBytes())
|
||||
}
|
||||
val signedContent = cmsSignedData.signedContent?.content as? ByteArray
|
||||
?: throw IllegalArgumentException("Master list does not contain signed content")
|
||||
val masterList = CscaMasterList.getInstance(signedContent)
|
||||
val certificates = masterList.certStructs.map { cert ->
|
||||
JcaX509CertificateConverter()
|
||||
.setProvider(provider)
|
||||
.getCertificate(X509CertificateHolder(cert))
|
||||
}
|
||||
cachedMasterListCertificates = certificates
|
||||
return certificates
|
||||
}
|
||||
}
|
||||
|
||||
private fun Context.findCertificateByReference(
|
||||
certificateReference: ByteArray?
|
||||
): X509Certificate? {
|
||||
if (certificateReference == null || certificateReference.size != 5) {
|
||||
return null
|
||||
}
|
||||
return runCatching {
|
||||
loadMasterListCertificates().firstOrNull { certificate ->
|
||||
DataEncoder.buildCertificateReference(certificate.encoded)
|
||||
.contentEquals(certificateReference)
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private fun curveNameFrom(certificate: X509Certificate): String? {
|
||||
return runCatching {
|
||||
val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(
|
||||
certificate.publicKey.encoded
|
||||
)
|
||||
val params = X962Parameters.getInstance(subjectPublicKeyInfo.algorithm.parameters)
|
||||
val oid = params.parameters as? ASN1ObjectIdentifier
|
||||
oid?.let {
|
||||
ECNamedCurveTable.getName(it)
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
@@ -96,15 +96,30 @@ object IdlParser {
|
||||
val data = findSubType(s, idlInfo)
|
||||
val len = data.length
|
||||
var start = 0
|
||||
var skipSubtype = false
|
||||
|
||||
for (p in 0..len) {
|
||||
if (p < len && data[p] > '\u001f') {
|
||||
continue
|
||||
}
|
||||
|
||||
if (p - start > 3) {
|
||||
val vp = start + 3
|
||||
val key = data.substring(start, vp)
|
||||
// Per spec, CR (0x0d) is the subfile boundary marker. The segment
|
||||
// immediately following a CR may begin with a 2-char subfile type
|
||||
// identifier (e.g. "ZV", "ZI") prepended to the first field.
|
||||
// Detect this by checking that the marker matches the key prefix
|
||||
// (jurisdiction fields always share their 2-char subfile prefix).
|
||||
val seg = if (skipSubtype && p - start >= 4 &&
|
||||
data[start] == data[start + 2] &&
|
||||
data[start + 1] == data[start + 3]
|
||||
) {
|
||||
start + 2
|
||||
} else {
|
||||
start
|
||||
}
|
||||
|
||||
if (p - seg > 3) {
|
||||
val vp = seg + 3
|
||||
val key = data.substring(seg, vp)
|
||||
|
||||
if (key.matches(Regex("[DZ][A-Z]{2}"))) {
|
||||
var value = data.substring(vp, p)
|
||||
@@ -114,6 +129,7 @@ object IdlParser {
|
||||
idlInfo.elements[key] = value.trim()
|
||||
}
|
||||
}
|
||||
skipSubtype = p < len && data[p] == '\r'
|
||||
start = p + 1
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
package de.markusfisch.android.binaryeye.content
|
||||
|
||||
import android.content.Context
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.tsenger.vdstools.asn1.DerTlv
|
||||
import de.tsenger.vdstools.generic.Message
|
||||
import de.tsenger.vdstools.generic.MessageCoding
|
||||
import de.tsenger.vdstools.generic.MessageValue
|
||||
import de.tsenger.vdstools.generic.Seal
|
||||
import de.tsenger.vdstools.idb.IdbSeal
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import de.tsenger.vdstools.generic.SealParser as VdsSealParser
|
||||
|
||||
data class SealField(val name: Any, val value: String)
|
||||
private data class IdbChildMessageDefinition(
|
||||
val name: String,
|
||||
val coding: MessageCoding
|
||||
)
|
||||
|
||||
object SealParser {
|
||||
private val idbChildMessagesByName: Map<String, Map<Int, IdbChildMessageDefinition>> by lazy {
|
||||
loadIdbChildMessagesByName()
|
||||
}
|
||||
|
||||
fun parse(
|
||||
context: Context,
|
||||
bytes: ByteArray
|
||||
): List<SealField>? = try {
|
||||
context.buildFields(
|
||||
VdsSealParser().parse(bytes.toString(Charsets.ISO_8859_1))
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
private fun Context.buildFields(seal: Seal): List<SealField> = buildList {
|
||||
add(SealField(R.string.vds_document_type, seal.documentType))
|
||||
add(SealField(R.string.vds_issuing_country, seal.issuingCountry))
|
||||
if (seal is IdbSeal) {
|
||||
addAll(IdbVerifier.verify(this@buildFields, seal))
|
||||
}
|
||||
for (message in seal.messageList) {
|
||||
addAll(message.toFields())
|
||||
}
|
||||
}
|
||||
|
||||
private fun Message.toFields(): List<SealField> = buildList {
|
||||
add(
|
||||
if (value is MessageValue.BytesValue) {
|
||||
SealField(
|
||||
"$name (${value.rawBytes.size} bytes)",
|
||||
value.rawBytes.toHexString().uppercase()
|
||||
)
|
||||
} else {
|
||||
SealField(name, value.toString())
|
||||
}
|
||||
)
|
||||
for (child in childMessages()) {
|
||||
addAll(child.toFields())
|
||||
}
|
||||
}
|
||||
|
||||
private fun Message.childMessages(): List<Message> {
|
||||
val childDefinitionsByTag = idbChildMessagesByName[name] ?: return emptyList()
|
||||
return runCatching {
|
||||
DerTlv.parseAll(value.rawBytes).mapNotNull { derTlv ->
|
||||
val childTag = derTlv.tag.toInt() and 0xff
|
||||
val childDefinition = childDefinitionsByTag[childTag]
|
||||
?: return@mapNotNull null
|
||||
Message(
|
||||
childTag,
|
||||
childDefinition.name,
|
||||
childDefinition.coding,
|
||||
MessageValue.fromBytes(
|
||||
derTlv.value,
|
||||
childDefinition.coding
|
||||
)
|
||||
)
|
||||
}
|
||||
}.getOrElse {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadIdbChildMessagesByName(): Map<String, Map<Int, IdbChildMessageDefinition>> {
|
||||
val resourceConstantsClass = runCatching {
|
||||
Class.forName("de.tsenger.vdstools.generated.ResourceConstants")
|
||||
}.getOrElse {
|
||||
return emptyMap()
|
||||
}
|
||||
val json = runCatching {
|
||||
resourceConstantsClass.getField(
|
||||
"IDB_MESSAGE_TYPES_JSON"
|
||||
).get(null) as String
|
||||
}.getOrElse {
|
||||
return emptyMap()
|
||||
}
|
||||
val childMessagesByName =
|
||||
mutableMapOf<String, Map<Int, IdbChildMessageDefinition>>()
|
||||
val messageTypes = JSONArray(json)
|
||||
for (i in 0 until messageTypes.length()) {
|
||||
val messageType = messageTypes.getJSONObject(i)
|
||||
val messages = messageType.optJSONArray("messages") ?: continue
|
||||
collectChildMessages(
|
||||
messageType.getString("name"),
|
||||
messages,
|
||||
childMessagesByName
|
||||
)
|
||||
}
|
||||
return childMessagesByName
|
||||
}
|
||||
|
||||
private fun collectChildMessages(
|
||||
parentName: String,
|
||||
messages: JSONArray,
|
||||
childMessagesByName: MutableMap<String, Map<Int, IdbChildMessageDefinition>>
|
||||
) {
|
||||
val childMessages = mutableMapOf<Int, IdbChildMessageDefinition>()
|
||||
for (i in 0 until messages.length()) {
|
||||
val message = messages.getJSONObject(i)
|
||||
val childDefinition = message.toChildMessageDefinition()
|
||||
childMessages[message.getInt("tag")] = childDefinition
|
||||
val nestedMessages = message.optJSONArray("messages") ?: continue
|
||||
collectChildMessages(
|
||||
childDefinition.name,
|
||||
nestedMessages,
|
||||
childMessagesByName
|
||||
)
|
||||
}
|
||||
if (childMessages.isNotEmpty()) {
|
||||
childMessagesByName[parentName] = childMessages
|
||||
}
|
||||
}
|
||||
|
||||
private fun JSONObject.toChildMessageDefinition(): IdbChildMessageDefinition {
|
||||
return IdbChildMessageDefinition(
|
||||
getString("name"),
|
||||
MessageCoding.valueOf(getString("coding"))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -5,15 +5,17 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.support.v4.content.FileProvider
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.net.toUri
|
||||
import de.markusfisch.android.binaryeye.BuildConfig
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
|
||||
fun Context.execShareIntent(intent: Intent): Boolean = if (
|
||||
fun Context.startIntentOrToast(intent: Intent): Boolean = if (
|
||||
!startIntent(intent)
|
||||
) {
|
||||
toast(R.string.cannot_resolve_action)
|
||||
@@ -42,28 +44,27 @@ fun Context.openUrl(url: String, silent: Boolean = false): Boolean {
|
||||
}
|
||||
|
||||
fun Context.openUri(uri: Uri, silent: Boolean = false): Boolean {
|
||||
val intent = Intent(Intent.ACTION_VIEW, uri).apply {
|
||||
if (uri.scheme == "content") {
|
||||
val cleaned = if (prefs.stripTrackingParams) {
|
||||
uri.stripTrackingParams()
|
||||
} else {
|
||||
uri
|
||||
}
|
||||
val intent = Intent(Intent.ACTION_VIEW, cleaned).apply {
|
||||
if (cleaned.scheme == "content") {
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
}
|
||||
return if (silent) {
|
||||
startIntent(intent)
|
||||
} else {
|
||||
execShareIntent(intent)
|
||||
startIntentOrToast(intent)
|
||||
}
|
||||
}
|
||||
|
||||
fun String.parseAndNormalizeUri(): Uri = Uri.parse(this).let {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
it.normalizeScheme()
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
fun String.parseAndNormalizeUri(): Uri = toUri().normalizeScheme()
|
||||
|
||||
fun Context.shareText(text: String, mimeType: String = "text/plain") {
|
||||
execShareIntent(Intent(Intent.ACTION_SEND).apply {
|
||||
startIntentOrToast(Intent(Intent.ACTION_SEND).apply {
|
||||
putExtra(Intent.EXTRA_TEXT, text)
|
||||
type = mimeType
|
||||
})
|
||||
@@ -126,7 +127,7 @@ private fun Context.getUriForFile(file: File): Uri? = if (
|
||||
}
|
||||
|
||||
private fun Context.shareUri(uri: Uri, mimeType: String) {
|
||||
execShareIntent(Intent(Intent.ACTION_SEND).apply {
|
||||
startIntentOrToast(Intent(Intent.ACTION_SEND).apply {
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
type = mimeType
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package de.markusfisch.android.binaryeye.content
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
private val TRACKING_PARAMS = setOf(
|
||||
"utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content",
|
||||
"utm_id", "utm_name", "utm_referrer", "utm_brand", "utm_social",
|
||||
"fbclid", "gclid", "dclid", "gclsrc", "yclid", "msclkid",
|
||||
"mc_eid", "mc_cid",
|
||||
"_ga", "_gl", "ga_source", "ga_medium",
|
||||
"igshid", "igsh",
|
||||
"hsa_acc", "hsa_cam", "hsa_grp", "hsa_ad", "hsa_src",
|
||||
"hsa_tgt", "hsa_kw", "hsa_mt", "hsa_net", "hsa_ver",
|
||||
"spm", "scm", "vero_id", "vero_conv"
|
||||
)
|
||||
|
||||
fun Uri.stripTrackingParams(): Uri {
|
||||
val scheme = scheme?.lowercase()
|
||||
if (scheme != "http" && scheme != "https") {
|
||||
return this
|
||||
}
|
||||
|
||||
val queryNames = runCatching {
|
||||
queryParameterNames
|
||||
}.getOrDefault(emptySet())
|
||||
if (queryNames.isEmpty()) {
|
||||
return this
|
||||
}
|
||||
|
||||
val keep = queryNames.filter { paramName ->
|
||||
paramName.lowercase() !in TRACKING_PARAMS
|
||||
}
|
||||
if (keep.size == queryNames.size) {
|
||||
return this
|
||||
}
|
||||
|
||||
val builder = buildUpon().clearQuery()
|
||||
for (paramName in keep) {
|
||||
getQueryParameters(paramName).forEach { paramValue ->
|
||||
builder.appendQueryParameter(paramName, paramValue)
|
||||
}
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
@@ -29,6 +29,7 @@ private fun exportCsv(
|
||||
return
|
||||
}
|
||||
val columns = arrayOf(
|
||||
Database.SCANS_PINNED,
|
||||
Database.SCANS_DATETIME,
|
||||
Database.SCANS_FORMAT,
|
||||
Database.SCANS_NAME,
|
||||
|
||||
@@ -3,15 +3,6 @@ package de.markusfisch.android.binaryeye.database
|
||||
import android.database.Cursor
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp
|
||||
|
||||
// Overwrite Kotlin's ".use" function for Cursor because Cursor cannot
|
||||
// be cast to Closeable below API level 16. This should be removed when
|
||||
// the minSDK is increased to at least JELLY_BEAN (16).
|
||||
inline fun <R> Cursor.use(block: (Cursor) -> R): R = try {
|
||||
block.invoke(this)
|
||||
} finally {
|
||||
close()
|
||||
}
|
||||
|
||||
fun Cursor.getInt(name: String): Int {
|
||||
val idx = getColumnIndex(name)
|
||||
return if (idx < 0) 0 else getInt(idx)
|
||||
|
||||
@@ -7,6 +7,8 @@ import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.preference.Preferences
|
||||
import de.markusfisch.android.binaryeye.zxingcpp.migrateBarcodeFormatName
|
||||
import de.markusfisch.android.binaryeye.zxingcpp.oldToNewFormatNames
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||
|
||||
class Database {
|
||||
@@ -19,19 +21,21 @@ class Database {
|
||||
fun getScans(query: String? = null): Cursor = db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID,
|
||||
$SCANS_PINNED,
|
||||
$SCANS_DATETIME,
|
||||
$SCANS_NAME,
|
||||
$SCANS_TEXT,
|
||||
$SCANS_FORMAT
|
||||
FROM $SCANS
|
||||
${getWhereClause(query)}
|
||||
ORDER BY $SCANS_DATETIME DESC
|
||||
ORDER BY $SCANS_PINNED DESC, $SCANS_DATETIME DESC
|
||||
""".trimMargin(), getWhereArguments(query)
|
||||
)
|
||||
|
||||
fun getScansDetailed(query: String? = null): Cursor = db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID,
|
||||
$SCANS_PINNED,
|
||||
$SCANS_DATETIME,
|
||||
$SCANS_NAME,
|
||||
$SCANS_TEXT,
|
||||
@@ -48,7 +52,7 @@ class Database {
|
||||
$SCANS_GTIN_ISSUE_NUMBER
|
||||
FROM $SCANS
|
||||
${getWhereClause(query)}
|
||||
ORDER BY $SCANS_DATETIME DESC
|
||||
ORDER BY $SCANS_PINNED DESC, $SCANS_DATETIME DESC
|
||||
""".trimMargin(), getWhereArguments(query)
|
||||
)
|
||||
|
||||
@@ -57,7 +61,8 @@ class Database {
|
||||
prefix: String = "WHERE"
|
||||
) = if (query?.isNotEmpty() == true) {
|
||||
"""$prefix $SCANS_TEXT LIKE ?
|
||||
OR $SCANS_NAME LIKE ?"""
|
||||
OR $SCANS_NAME LIKE ?
|
||||
OR $SCANS_FORMAT LIKE ?"""
|
||||
} else {
|
||||
""
|
||||
}
|
||||
@@ -66,7 +71,7 @@ class Database {
|
||||
query: String?
|
||||
) = if (query?.isNotEmpty() == true) {
|
||||
val instr = "%$query%"
|
||||
arrayOf(instr, instr)
|
||||
arrayOf(instr, instr, instr)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -74,6 +79,7 @@ class Database {
|
||||
fun getScansDetailed(ids: LongArray): Cursor = db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID,
|
||||
$SCANS_PINNED,
|
||||
$SCANS_DATETIME,
|
||||
$SCANS_NAME,
|
||||
$SCANS_TEXT,
|
||||
@@ -90,13 +96,14 @@ class Database {
|
||||
$SCANS_GTIN_ISSUE_NUMBER
|
||||
FROM $SCANS
|
||||
WHERE $SCANS_ID IN (${ids.joinToString(",")})
|
||||
ORDER BY $SCANS_DATETIME DESC
|
||||
ORDER BY $SCANS_PINNED DESC, $SCANS_DATETIME DESC
|
||||
""".trimMargin(), null
|
||||
)
|
||||
|
||||
fun getScan(id: Long): Scan? = db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID,
|
||||
$SCANS_PINNED,
|
||||
$SCANS_DATETIME,
|
||||
$SCANS_NAME,
|
||||
$SCANS_TEXT,
|
||||
@@ -123,7 +130,9 @@ class Database {
|
||||
Scan(
|
||||
it.getString(SCANS_TEXT),
|
||||
it.getBlob(SCANS_RAW),
|
||||
BarcodeFormat.valueOf(it.getString(SCANS_FORMAT)),
|
||||
BarcodeFormat.valueOf(
|
||||
it.getString(SCANS_FORMAT).migrateBarcodeFormatName()
|
||||
),
|
||||
it.getString(SCANS_ERROR_CORRECTION_LEVEL),
|
||||
it.getString(SCANS_VERSION),
|
||||
it.getInt(SCANS_DATA_MASK),
|
||||
@@ -142,6 +151,7 @@ class Database {
|
||||
it.getString(SCANS_DATETIME),
|
||||
it.getLong(SCANS_ID),
|
||||
it.getString(SCANS_NAME),
|
||||
it.getInt(SCANS_PINNED) != 0
|
||||
)
|
||||
} else {
|
||||
null
|
||||
@@ -197,6 +207,7 @@ class Database {
|
||||
scan.addOn?.let { put(SCANS_GTIN_ADD_ON, it) }
|
||||
scan.price?.let { put(SCANS_GTIN_PRICE, it) }
|
||||
scan.issueNumber?.let { put(SCANS_GTIN_ISSUE_NUMBER, it) }
|
||||
put(SCANS_PINNED, if (scan.pinned) 1 else 0)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -262,8 +273,14 @@ class Database {
|
||||
db.update(SCANS, cv, "$SCANS_ID = ?", arrayOf("$id"))
|
||||
}
|
||||
|
||||
fun setPinned(id: Long, pinned: Boolean) {
|
||||
val cv = ContentValues()
|
||||
cv.put(SCANS_PINNED, if (pinned) 1 else 0)
|
||||
db.update(SCANS, cv, "$SCANS_ID = ?", arrayOf("$id"))
|
||||
}
|
||||
|
||||
private class OpenHelper(context: Context) :
|
||||
SQLiteOpenHelper(context, FILE_NAME, null, 8) {
|
||||
SQLiteOpenHelper(context, FILE_NAME, null, 10) {
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
db.createScans()
|
||||
}
|
||||
@@ -294,6 +311,12 @@ class Database {
|
||||
if (oldVersion < 8) {
|
||||
db.addSymbolColumns()
|
||||
}
|
||||
if (oldVersion < 9) {
|
||||
db.migrateToNativeFormatNames()
|
||||
}
|
||||
if (oldVersion < 10) {
|
||||
db.addPinnedColumn()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,6 +350,7 @@ class Database {
|
||||
const val SCANS_GTIN_ADD_ON = "gtin_add_on"
|
||||
const val SCANS_GTIN_PRICE = "gtin_price"
|
||||
const val SCANS_GTIN_ISSUE_NUMBER = "gtin_issue_number"
|
||||
const val SCANS_PINNED = "pinned"
|
||||
|
||||
private fun SQLiteDatabase.createScans() {
|
||||
execSQL("DROP TABLE IF EXISTS $SCANS".trimMargin())
|
||||
@@ -350,7 +374,8 @@ class Database {
|
||||
$SCANS_GTIN_COUNTRY TEXT,
|
||||
$SCANS_GTIN_ADD_ON TEXT,
|
||||
$SCANS_GTIN_PRICE TEXT,
|
||||
$SCANS_GTIN_ISSUE_NUMBER TEXT
|
||||
$SCANS_GTIN_ISSUE_NUMBER TEXT,
|
||||
$SCANS_PINNED INTEGER NOT NULL DEFAULT 0
|
||||
)""".trimMargin()
|
||||
)
|
||||
}
|
||||
@@ -391,6 +416,15 @@ class Database {
|
||||
execSQL("UPDATE $SCANS SET $SCANS_GTIN_ISSUE_NUMBER = $SCANS_ISSUE_NUMBER")
|
||||
}
|
||||
|
||||
private fun SQLiteDatabase.migrateToNativeFormatNames() {
|
||||
for ((oldName, newName) in oldToNewFormatNames) {
|
||||
execSQL(
|
||||
"UPDATE $SCANS SET $SCANS_FORMAT = ? WHERE $SCANS_FORMAT = ?",
|
||||
arrayOf(newName, oldName)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun SQLiteDatabase.migrateToVersionString() {
|
||||
execSQL("ALTER TABLE $SCANS ADD $SCANS_VERSION TEXT")
|
||||
execSQL("UPDATE $SCANS SET $SCANS_VERSION = $SCANS_VERSION_NUMBER")
|
||||
@@ -406,5 +440,11 @@ class Database {
|
||||
execSQL("ALTER TABLE $SCANS ADD $SCANS_SYMBOL_HEIGHT INTEGER")
|
||||
execSQL("ALTER TABLE $SCANS ADD $SCANS_SYMBOL_DATA BLOB")
|
||||
}
|
||||
|
||||
private fun SQLiteDatabase.addPinnedColumn() {
|
||||
execSQL(
|
||||
"ALTER TABLE $SCANS ADD COLUMN $SCANS_PINNED INTEGER NOT NULL DEFAULT 0"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ fun Cursor.exportJson(): String? {
|
||||
return null
|
||||
}
|
||||
val columns = arrayOf(
|
||||
Database.SCANS_PINNED,
|
||||
Database.SCANS_DATETIME,
|
||||
Database.SCANS_FORMAT,
|
||||
Database.SCANS_NAME,
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package de.markusfisch.android.binaryeye.database
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateFormat
|
||||
import de.markusfisch.android.binaryeye.zxingcpp.migrateBarcodeFormatName
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BitMatrix
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.ContentType
|
||||
@@ -27,8 +26,9 @@ data class Scan(
|
||||
val issueNumber: String? = null,
|
||||
val dateTime: String = getDateTime(),
|
||||
var id: Long = 0L,
|
||||
val label: String? = null
|
||||
) : Parcelable {
|
||||
val label: String? = null,
|
||||
val pinned: Boolean = false
|
||||
) {
|
||||
// Needs to be overwritten manually, as ByteArray is an array and
|
||||
// this isn't handled well by Kotlin.
|
||||
override fun equals(other: Any?): Boolean {
|
||||
@@ -57,7 +57,9 @@ data class Scan(
|
||||
country == other.country &&
|
||||
addOn == other.addOn &&
|
||||
price == other.price &&
|
||||
issueNumber == other.issueNumber
|
||||
issueNumber == other.issueNumber &&
|
||||
label == other.label &&
|
||||
pinned == other.pinned
|
||||
}
|
||||
|
||||
// Needs to be overwritten manually, as ByteArray is an array and
|
||||
@@ -78,60 +80,11 @@ data class Scan(
|
||||
result = 31 * result + (addOn?.hashCode() ?: 0)
|
||||
result = 31 * result + (price?.hashCode() ?: 0)
|
||||
result = 31 * result + (issueNumber?.hashCode() ?: 0)
|
||||
result = 31 * result + (label?.hashCode() ?: 0)
|
||||
result = 31 * result + pinned.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
private constructor(parcel: Parcel) : this(
|
||||
text = parcel.readString() ?: "",
|
||||
raw = parcel.readSizedByteArray(),
|
||||
format = BarcodeFormat.valueOf(parcel.readString() ?: ""),
|
||||
errorCorrectionLevel = parcel.readString(),
|
||||
version = parcel.readString(),
|
||||
dataMask = parcel.readInt(),
|
||||
symbol = parcel.readBitMatrix(),
|
||||
sequenceSize = parcel.readInt(),
|
||||
sequenceIndex = parcel.readInt(),
|
||||
sequenceId = parcel.readString() ?: "",
|
||||
country = parcel.readString(),
|
||||
addOn = parcel.readString(),
|
||||
price = parcel.readString(),
|
||||
issueNumber = parcel.readString(),
|
||||
dateTime = parcel.readString() ?: "",
|
||||
id = parcel.readLong(),
|
||||
label = parcel.readString()
|
||||
)
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.apply {
|
||||
writeString(text)
|
||||
writeSizedByteArray(raw)
|
||||
writeString(format.name)
|
||||
writeString(errorCorrectionLevel)
|
||||
writeString(version)
|
||||
writeInt(dataMask)
|
||||
writeBitMatrix(symbol)
|
||||
writeInt(sequenceSize)
|
||||
writeInt(sequenceIndex)
|
||||
writeString(sequenceId)
|
||||
writeString(country)
|
||||
writeString(addOn)
|
||||
writeString(price)
|
||||
writeString(issueNumber)
|
||||
writeString(dateTime)
|
||||
writeLong(id)
|
||||
writeString(label)
|
||||
}
|
||||
}
|
||||
|
||||
override fun describeContents() = 0
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val CREATOR = object : Parcelable.Creator<Scan> {
|
||||
override fun createFromParcel(parcel: Parcel) = Scan(parcel)
|
||||
override fun newArray(size: Int) = arrayOfNulls<Scan>(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Result.toScan(): Scan {
|
||||
@@ -159,16 +112,64 @@ fun Result.toScan(): Scan {
|
||||
)
|
||||
}
|
||||
|
||||
fun Scan.toBundle() = Bundle().apply {
|
||||
putString(SCAN_TEXT, text)
|
||||
putByteArray(SCAN_RAW, raw)
|
||||
putString(SCAN_FORMAT, format.name)
|
||||
putString(SCAN_ERROR_CORRECTION_LEVEL, errorCorrectionLevel)
|
||||
putString(SCAN_VERSION, version)
|
||||
putInt(SCAN_DATA_MASK, dataMask)
|
||||
putInt(SCAN_SYMBOL_WIDTH, symbol?.width ?: 0)
|
||||
putInt(SCAN_SYMBOL_HEIGHT, symbol?.height ?: 0)
|
||||
putByteArray(SCAN_SYMBOL_DATA, symbol?.data)
|
||||
putInt(SCAN_SEQUENCE_SIZE, sequenceSize)
|
||||
putInt(SCAN_SEQUENCE_INDEX, sequenceIndex)
|
||||
putString(SCAN_SEQUENCE_ID, sequenceId)
|
||||
putString(SCAN_COUNTRY, country)
|
||||
putString(SCAN_ADD_ON, addOn)
|
||||
putString(SCAN_PRICE, price)
|
||||
putString(SCAN_ISSUE_NUMBER, issueNumber)
|
||||
putString(SCAN_DATE_TIME, dateTime)
|
||||
putLong(SCAN_ID, id)
|
||||
putString(SCAN_LABEL, label)
|
||||
putBoolean(SCAN_PINNED, pinned)
|
||||
}
|
||||
|
||||
fun Bundle.toScan(): Scan? {
|
||||
val formatName = getString(SCAN_FORMAT)?.migrateBarcodeFormatName()
|
||||
if (formatName.isNullOrEmpty()) {
|
||||
return null
|
||||
}
|
||||
return try {
|
||||
Scan(
|
||||
text = getString(SCAN_TEXT) ?: "",
|
||||
raw = getByteArray(SCAN_RAW),
|
||||
format = BarcodeFormat.valueOf(formatName),
|
||||
errorCorrectionLevel = getString(SCAN_ERROR_CORRECTION_LEVEL),
|
||||
version = getString(SCAN_VERSION),
|
||||
dataMask = getInt(SCAN_DATA_MASK, -1),
|
||||
symbol = getBitMatrix(),
|
||||
sequenceSize = getInt(SCAN_SEQUENCE_SIZE, -1),
|
||||
sequenceIndex = getInt(SCAN_SEQUENCE_INDEX, -1),
|
||||
sequenceId = getString(SCAN_SEQUENCE_ID) ?: "",
|
||||
country = getString(SCAN_COUNTRY),
|
||||
addOn = getString(SCAN_ADD_ON),
|
||||
price = getString(SCAN_PRICE),
|
||||
issueNumber = getString(SCAN_ISSUE_NUMBER),
|
||||
dateTime = getString(SCAN_DATE_TIME) ?: getDateTime(),
|
||||
id = getLong(SCAN_ID, 0L),
|
||||
label = getString(SCAN_LABEL),
|
||||
pinned = getBoolean(SCAN_PINNED, false)
|
||||
)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDateTime(
|
||||
time: Long = System.currentTimeMillis()
|
||||
) = DateFormat.format(
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
// Up to API level 17, 'k' was interpreted as 'H', which wasn't
|
||||
// implemented until API level 18.
|
||||
"yyyy-MM-dd kk:mm:ss"
|
||||
} else {
|
||||
"yyyy-MM-dd HH:mm:ss"
|
||||
},
|
||||
"yyyy-MM-dd HH:mm:ss",
|
||||
time
|
||||
).toString() + String.format(
|
||||
Locale.getDefault(),
|
||||
@@ -176,39 +177,33 @@ private fun getDateTime(
|
||||
time % 1000
|
||||
)
|
||||
|
||||
private fun Parcel.writeSizedByteArray(array: ByteArray?) {
|
||||
val size = array?.size ?: 0
|
||||
writeInt(size)
|
||||
if (size > 0) {
|
||||
writeByteArray(array)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Parcel.readSizedByteArray(): ByteArray? {
|
||||
val size = readInt()
|
||||
return if (size > 0) {
|
||||
val array = ByteArray(size)
|
||||
readByteArray(array)
|
||||
array
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun Parcel.writeBitMatrix(bm: BitMatrix?) {
|
||||
writeInt(bm?.width ?: 0)
|
||||
bm?.let {
|
||||
writeInt(bm.height)
|
||||
writeSizedByteArray(bm.data)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Parcel.readBitMatrix(): BitMatrix? {
|
||||
val width = readInt()
|
||||
if (width < 1) {
|
||||
private fun Bundle.getBitMatrix(): BitMatrix? {
|
||||
val width = getInt(SCAN_SYMBOL_WIDTH, 0)
|
||||
val height = getInt(SCAN_SYMBOL_HEIGHT, 0)
|
||||
val data = getByteArray(SCAN_SYMBOL_DATA)
|
||||
if (width < 1 || height < 1 || data == null) {
|
||||
return null
|
||||
}
|
||||
val height = readInt()
|
||||
val data = readSizedByteArray() ?: return null
|
||||
return BitMatrix(width, height, data)
|
||||
}
|
||||
|
||||
private const val SCAN_TEXT = "text"
|
||||
private const val SCAN_RAW = "raw"
|
||||
private const val SCAN_FORMAT = "format"
|
||||
private const val SCAN_ERROR_CORRECTION_LEVEL = "error_correction_level"
|
||||
private const val SCAN_VERSION = "version"
|
||||
private const val SCAN_DATA_MASK = "data_mask"
|
||||
private const val SCAN_SYMBOL_WIDTH = "symbol_width"
|
||||
private const val SCAN_SYMBOL_HEIGHT = "symbol_height"
|
||||
private const val SCAN_SYMBOL_DATA = "symbol_data"
|
||||
private const val SCAN_SEQUENCE_SIZE = "sequence_size"
|
||||
private const val SCAN_SEQUENCE_INDEX = "sequence_index"
|
||||
private const val SCAN_SEQUENCE_ID = "sequence_id"
|
||||
private const val SCAN_COUNTRY = "country"
|
||||
private const val SCAN_ADD_ON = "add_on"
|
||||
private const val SCAN_PRICE = "price"
|
||||
private const val SCAN_ISSUE_NUMBER = "issue_number"
|
||||
private const val SCAN_DATE_TIME = "date_time"
|
||||
private const val SCAN_ID = "id"
|
||||
private const val SCAN_LABEL = "label"
|
||||
private const val SCAN_PINNED = "pinned"
|
||||
|
||||
+144
-57
@@ -1,7 +1,6 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
@@ -9,15 +8,20 @@ import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.annotation.RequiresApi
|
||||
import android.support.v14.preference.MultiSelectListPreference
|
||||
import android.support.v7.preference.ListPreference
|
||||
import android.support.v7.preference.Preference
|
||||
import android.support.v7.preference.PreferenceFragmentCompat
|
||||
import android.support.v7.preference.PreferenceGroup
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.MultiSelectListPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceGroup
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.activity.AutomatedActionsActivity
|
||||
import de.markusfisch.android.binaryeye.activity.IgnoreCodesActivity
|
||||
import de.markusfisch.android.binaryeye.activity.NetworkSuggestionsActivity
|
||||
import de.markusfisch.android.binaryeye.activity.ProfilesActivity
|
||||
import de.markusfisch.android.binaryeye.activity.SplashActivity
|
||||
import de.markusfisch.android.binaryeye.app.addFragment
|
||||
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
|
||||
import de.markusfisch.android.binaryeye.app.hasBluetoothPermission
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.bluetooth.setBluetoothHosts
|
||||
@@ -36,17 +40,19 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
key: String?
|
||||
) {
|
||||
key ?: return
|
||||
val preference = findPreference(key) ?: return
|
||||
val preference = findPreference<Preference>(key) ?: return
|
||||
if (preference.key != PROFILE) {
|
||||
prefs.update()
|
||||
}
|
||||
when (preference.key) {
|
||||
"custom_locale" -> {
|
||||
"custom_locale",
|
||||
"dynamic_colors" -> {
|
||||
activity?.restartApp()
|
||||
return
|
||||
}
|
||||
|
||||
"beep_tone_name" -> beepConfirm()
|
||||
"beep_tone_name",
|
||||
"beep_stream_name" -> beepConfirm()
|
||||
|
||||
"send_scan_bluetooth" -> if (
|
||||
prefs.sendScanBluetooth &&
|
||||
@@ -76,33 +82,48 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
preferenceScreen = null
|
||||
setPreferencesFromResource(R.xml.preferences, rootKey)
|
||||
preferenceScreen.sharedPreferences
|
||||
.registerOnSharedPreferenceChangeListener(changeListener)
|
||||
?.registerOnSharedPreferenceChangeListener(changeListener)
|
||||
setSummaries(preferenceScreen)
|
||||
setIcons()
|
||||
|
||||
wireProfiles()
|
||||
wireAutomatedActions()
|
||||
wireIgnoreCodes()
|
||||
setBluetoothResources()
|
||||
wireClearNetworkPreferences()
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||
findPreference<PreferenceGroup>("appearance_category")?.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun wireProfiles() {
|
||||
findPreference(PROFILE).apply {
|
||||
findPreference<Preference>(PROFILE)?.apply {
|
||||
updateProfileSummary(this)
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
activity?.supportFragmentManager?.addFragment(
|
||||
ProfilesFragment()
|
||||
)
|
||||
startActivity(Intent(activity, ProfilesActivity::class.java))
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun wireAutomatedActions() {
|
||||
findPreference(AUTOMATED_ACTIONS).apply {
|
||||
findPreference<Preference>(AUTOMATED_ACTIONS)?.apply {
|
||||
updateAutomatedActionsSummary(this)
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
activity?.supportFragmentManager?.addFragment(
|
||||
AutomatedActionsFragment()
|
||||
startActivity(
|
||||
Intent(activity, AutomatedActionsActivity::class.java)
|
||||
)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun wireIgnoreCodes() {
|
||||
findPreference<Preference>(IGNORE_CODES)?.apply {
|
||||
updateIgnoreCodesSummary(this)
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
startActivity(
|
||||
Intent(activity, IgnoreCodesActivity::class.java)
|
||||
)
|
||||
true
|
||||
}
|
||||
@@ -113,20 +134,20 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
if (prefs.sendScanBluetooth &&
|
||||
activity?.hasBluetoothPermission() == true
|
||||
) {
|
||||
setBluetoothHosts(
|
||||
findPreference("send_scan_bluetooth_host") as ListPreference
|
||||
)
|
||||
findPreference<ListPreference>("send_scan_bluetooth_host")?.let {
|
||||
setBluetoothHosts(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun wireClearNetworkPreferences() {
|
||||
findPreference("clear_network_suggestions").apply {
|
||||
findPreference<Preference>("clear_network_suggestions")?.apply {
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
|
||||
// From R+ we can query past network suggestions and
|
||||
// make them editable.
|
||||
setOnPreferenceClickListener {
|
||||
activity?.supportFragmentManager?.addFragment(
|
||||
NetworkSuggestionsFragment()
|
||||
startActivity(
|
||||
Intent(activity, NetworkSuggestionsActivity::class.java)
|
||||
)
|
||||
true
|
||||
}
|
||||
@@ -136,7 +157,7 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
// adding network suggestions on Q as well, so we
|
||||
// need to keep this option.
|
||||
setOnPreferenceClickListener {
|
||||
context.askToClearNetworkSuggestions()
|
||||
context?.askToClearNetworkSuggestions()
|
||||
true
|
||||
}
|
||||
} else {
|
||||
@@ -148,7 +169,7 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
private fun Context.askToClearNetworkSuggestions() {
|
||||
AlertDialog.Builder(this)
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.really_remove_all_networks)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
clearNetworkSuggestions()
|
||||
@@ -176,10 +197,13 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
loadPreferences()
|
||||
}
|
||||
activity?.setTitle(R.string.preferences)
|
||||
findPreference(AUTOMATED_ACTIONS)?.let {
|
||||
findPreference<Preference>(AUTOMATED_ACTIONS)?.let {
|
||||
updateAutomatedActionsSummary(it)
|
||||
}
|
||||
findPreference(PROFILE)?.let {
|
||||
findPreference<Preference>(IGNORE_CODES)?.let {
|
||||
updateIgnoreCodesSummary(it)
|
||||
}
|
||||
findPreference<Preference>(PROFILE)?.let {
|
||||
updateProfileSummary(it)
|
||||
}
|
||||
listView.setPaddingFromWindowInsets()
|
||||
@@ -190,12 +214,12 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
preferenceScreen.sharedPreferences
|
||||
.unregisterOnSharedPreferenceChangeListener(changeListener)
|
||||
?.unregisterOnSharedPreferenceChangeListener(changeListener)
|
||||
}
|
||||
|
||||
override fun onDisplayPreferenceDialog(preference: Preference) {
|
||||
if (preference is UrlPreference) {
|
||||
val fm = fragmentManager
|
||||
val fm = parentFragmentManager
|
||||
UrlDialogFragment.newInstance(preference.key).apply {
|
||||
setTargetFragment(this@PreferencesFragment, 0)
|
||||
show(fm, null)
|
||||
@@ -219,31 +243,82 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
|
||||
private fun setSummary(preference: Preference) {
|
||||
if (preference.key == AUTOMATED_ACTIONS) {
|
||||
updateAutomatedActionsSummary(preference)
|
||||
return
|
||||
when (preference.key) {
|
||||
AUTOMATED_ACTIONS -> return updateAutomatedActionsSummary(preference)
|
||||
IGNORE_CODES -> return updateIgnoreCodesSummary(preference)
|
||||
}
|
||||
when (preference) {
|
||||
is UrlPreference -> {
|
||||
preference.summary = preference.getUrl()
|
||||
}
|
||||
is UrlPreference -> preference.summary = preference.getUrl()
|
||||
is ListPreference -> preference.setSummary(preference.entry)
|
||||
is MultiSelectListPreference -> preference.summary =
|
||||
getSortedSummary(preference)
|
||||
|
||||
is ListPreference -> {
|
||||
preference.setSummary(preference.entry)
|
||||
}
|
||||
|
||||
is MultiSelectListPreference -> {
|
||||
preference.summary = preference.values.joinToString(", ") {
|
||||
it.replace(Regex("_"), " ")
|
||||
}
|
||||
}
|
||||
|
||||
is PreferenceGroup -> {
|
||||
setSummaries(preference)
|
||||
}
|
||||
is PreferenceGroup -> setSummaries(preference)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSortedSummary(preference: MultiSelectListPreference): String {
|
||||
val labelsByValue = mutableMapOf<String, String>()
|
||||
val entries = preference.entries
|
||||
val entryValues = preference.entryValues
|
||||
val count = minOf(entries.size, entryValues.size)
|
||||
|
||||
for (i in 0 until count) {
|
||||
labelsByValue[entryValues[i].toString()] = entries[i].toString()
|
||||
}
|
||||
|
||||
return preference.values
|
||||
.map { value ->
|
||||
labelsByValue[value] ?: value.prettifyFormatName()
|
||||
}
|
||||
.sortedBy { it.lowercase() }
|
||||
.joinToString(", ")
|
||||
}
|
||||
|
||||
private fun setIcons() {
|
||||
setIcon(PROFILE, R.drawable.ic_action_profile)
|
||||
setIcon("formats", R.drawable.ic_action_scan)
|
||||
setIcon("show_crop_handle", R.drawable.ic_action_crop)
|
||||
setIcon("show_crosshairs", R.drawable.ic_action_search)
|
||||
setIcon("zoom_by_swiping", R.drawable.ic_action_search)
|
||||
setIcon("auto_rotate", R.drawable.ic_action_down)
|
||||
setIcon("try_harder", R.drawable.ic_action_search)
|
||||
setIcon("bulk_mode", R.drawable.ic_action_bulk_mode)
|
||||
setIcon("bulk_mode_delay", R.drawable.ic_action_delay)
|
||||
setIcon("show_toast_in_bulk_mode", R.drawable.ic_action_toast)
|
||||
setIcon("vibrate", R.drawable.ic_action_vibrate)
|
||||
setIcon("beep", R.drawable.ic_action_beep)
|
||||
setIcon("beep_tone_name", R.drawable.ic_action_notification_sound)
|
||||
setIcon("beep_stream_name", R.drawable.ic_action_notification_sound)
|
||||
setIcon("use_history", R.drawable.ic_action_history)
|
||||
setIcon("ignore_duplicates_name", R.drawable.ic_action_remove)
|
||||
setIcon(IGNORE_CODES, R.drawable.ic_action_remove)
|
||||
setIcon("copy_immediately", R.drawable.ic_action_copy)
|
||||
setIcon("send_scan_active", R.drawable.ic_action_forward)
|
||||
setIcon("send_scan_url", R.drawable.ic_action_link)
|
||||
setIcon("send_scan_type", R.drawable.ic_action_link)
|
||||
setIcon("send_scan_bluetooth", R.drawable.ic_action_bluetooth)
|
||||
setIcon("send_scan_bluetooth_host", R.drawable.ic_action_bluetooth)
|
||||
setIcon("open_immediately", R.drawable.ic_action_open)
|
||||
setIcon("strip_tracking_params", R.drawable.ic_action_remove)
|
||||
setIcon("show_meta_data", R.drawable.ic_action_info)
|
||||
setIcon("show_hex_dump", R.drawable.ic_action_info)
|
||||
setIcon("show_checksum", R.drawable.ic_action_info)
|
||||
setIcon("show_recreation", R.drawable.ic_action_scan)
|
||||
setIcon("close_automatically", R.drawable.ic_action_back)
|
||||
setIcon("default_search_url", R.drawable.ic_action_search)
|
||||
setIcon("open_with_url", R.drawable.ic_action_link)
|
||||
setIcon(AUTOMATED_ACTIONS, R.drawable.ic_action_create)
|
||||
setIcon("clear_network_suggestions", R.drawable.ic_action_wifi)
|
||||
setIcon("brighten_screen", R.drawable.ic_action_bright)
|
||||
setIcon("custom_locale", R.drawable.ic_action_preferences)
|
||||
setIcon("dynamic_colors", R.drawable.ic_action_preferences)
|
||||
}
|
||||
|
||||
private fun setIcon(key: String, iconResId: Int) {
|
||||
findPreference<Preference>(key)?.setIcon(iconResId)
|
||||
}
|
||||
|
||||
private fun updateAutomatedActionsSummary(preference: Preference) {
|
||||
val count = prefs.automatedActions.size
|
||||
preference.summary = if (count == 0) {
|
||||
@@ -257,25 +332,37 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateIgnoreCodesSummary(preference: Preference) {
|
||||
val count = prefs.ignoreCodes.size
|
||||
preference.summary = if (count == 0) {
|
||||
getString(R.string.ignore_codes_none)
|
||||
} else {
|
||||
resources.getQuantityString(
|
||||
R.plurals.ignore_codes_count,
|
||||
count,
|
||||
count
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateProfileSummary(preference: Preference) {
|
||||
preference.summary = prefs.profile ?: getString(R.string.profile_default)
|
||||
preference.summary = prefs.profileLabel(requireContext(), prefs.profile)
|
||||
lastProfile = prefs.profile
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PROFILE = "profile"
|
||||
private const val AUTOMATED_ACTIONS = "automated_actions"
|
||||
private const val IGNORE_CODES = "ignore_codes"
|
||||
}
|
||||
}
|
||||
|
||||
private fun Activity.restartApp() {
|
||||
val intent = Intent(this, SplashActivity::class.java)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
intent.addFlags(
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
)
|
||||
}
|
||||
intent.addFlags(
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
// Restart to begin with an unmodified Locale to follow system settings.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v7.preference.PreferenceDialogFragmentCompat
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.preference.PreferenceDialogFragmentCompat
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
@@ -15,10 +15,10 @@ class UrlDialogFragment : PreferenceDialogFragmentCompat() {
|
||||
private var urlView: TextView? = null
|
||||
private var testButton: TextView? = null
|
||||
|
||||
override fun onBindDialogView(view: View?) {
|
||||
override fun onBindDialogView(view: View) {
|
||||
super.onBindDialogView(view)
|
||||
urlView = view?.findViewById(R.id.url)
|
||||
testButton = view?.findViewById(R.id.test_url)
|
||||
urlView = view.findViewById(R.id.url)
|
||||
testButton = view.findViewById(R.id.test_url)
|
||||
testButton?.setOnClickListener {
|
||||
testUrl(testButton)
|
||||
}
|
||||
@@ -51,7 +51,7 @@ class UrlDialogFragment : PreferenceDialogFragmentCompat() {
|
||||
Scan(
|
||||
"test",
|
||||
null,
|
||||
BarcodeFormat.NONE
|
||||
BarcodeFormat.None
|
||||
).sendAsync(url, prefs.sendScanType) { code, body ->
|
||||
textView.text = when {
|
||||
code != null -> "$code"
|
||||
|
||||
@@ -3,12 +3,12 @@ package de.markusfisch.android.binaryeye.graphics
|
||||
import android.content.Context
|
||||
import android.graphics.DashPathEffect
|
||||
import android.graphics.Paint
|
||||
import android.support.v4.content.ContextCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
fun Context.getDashedBorderPaint() = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
val dp = resources.displayMetrics.density
|
||||
color = ContextCompat.getColor(applicationContext, R.color.crop_bound)
|
||||
color = ContextCompat.getColor(this@getDashedBorderPaint, R.color.crop_bound)
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = dp * 2f
|
||||
pathEffect = DashPathEffect(floatArrayOf(10f * dp, 10f * dp), 0f)
|
||||
|
||||
@@ -1,30 +1,32 @@
|
||||
package de.markusfisch.android.binaryeye.graphics
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.content.res.Resources.Theme
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.graphics.createBitmap
|
||||
|
||||
fun Resources.getBitmapFromDrawable(
|
||||
resId: Int
|
||||
): Bitmap = getBitmapFromDrawable(getDrawableCompat(resId))
|
||||
): Bitmap = getBitmapFromDrawable(resId, null)
|
||||
|
||||
fun Resources.getDrawableCompat(
|
||||
resId: Int
|
||||
): Drawable = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
getDrawable(resId, null)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
getDrawable(resId)
|
||||
}
|
||||
fun Resources.getBitmapFromDrawable(
|
||||
resId: Int,
|
||||
theme: Theme?
|
||||
): Bitmap = getBitmapFromDrawable(
|
||||
ResourcesCompat.getDrawable(this, resId, theme) ?: throw Resources.NotFoundException(
|
||||
"Drawable resource ID #0x${resId.toString(16)}"
|
||||
)
|
||||
)
|
||||
|
||||
private fun getBitmapFromDrawable(drawable: Drawable): Bitmap {
|
||||
if (drawable is BitmapDrawable) {
|
||||
return drawable.bitmap
|
||||
}
|
||||
val bitmap = Bitmap.createBitmap(
|
||||
val bitmap = createBitmap(
|
||||
drawable.intrinsicWidth,
|
||||
drawable.intrinsicHeight,
|
||||
Bitmap.Config.ARGB_8888
|
||||
|
||||
@@ -40,6 +40,15 @@ fun Rect.setFrameRoi(
|
||||
frameRoiF.bottom.roundToInt()
|
||||
)
|
||||
}
|
||||
val clampedLeft = left.coerceIn(0, frameMetrics.width)
|
||||
val clampedTop = top.coerceIn(0, frameMetrics.height)
|
||||
val clampedRight = right.coerceIn(0, frameMetrics.width)
|
||||
val clampedBottom = bottom.coerceIn(0, frameMetrics.height)
|
||||
if (clampedLeft >= clampedRight || clampedTop >= clampedBottom) {
|
||||
setEmpty()
|
||||
} else {
|
||||
set(clampedLeft, clampedTop, clampedRight, clampedBottom)
|
||||
}
|
||||
}
|
||||
|
||||
fun Matrix.setFrameToView(
|
||||
@@ -88,3 +97,57 @@ fun Matrix.mapPosition(
|
||||
mapPoints(coords)
|
||||
return i
|
||||
}
|
||||
|
||||
fun Rect.setCovered(
|
||||
viewWidth: Int,
|
||||
viewHeight: Int,
|
||||
frameMetrics: FrameMetrics
|
||||
) {
|
||||
val frameWidth: Int
|
||||
val frameHeight: Int
|
||||
when (frameMetrics.orientation) {
|
||||
90, 270 -> {
|
||||
frameWidth = frameMetrics.height
|
||||
frameHeight = frameMetrics.width
|
||||
}
|
||||
|
||||
else -> {
|
||||
frameWidth = frameMetrics.width
|
||||
frameHeight = frameMetrics.height
|
||||
}
|
||||
}
|
||||
if (frameWidth < 1 || frameHeight < 1) {
|
||||
set(0, 0, 0, 0)
|
||||
return
|
||||
}
|
||||
var coveredWidth = frameWidth
|
||||
var coveredHeight = frameHeight
|
||||
if (viewWidth.toLong() * coveredWidth <
|
||||
viewHeight.toLong() * coveredHeight
|
||||
) {
|
||||
coveredWidth = coveredWidth * viewHeight / coveredHeight
|
||||
coveredHeight = viewHeight
|
||||
} else {
|
||||
coveredHeight = coveredHeight * viewWidth / coveredWidth
|
||||
coveredWidth = viewWidth
|
||||
}
|
||||
val left = (viewWidth - coveredWidth) / 2
|
||||
val top = (viewHeight - coveredHeight) / 2
|
||||
set(
|
||||
left,
|
||||
top,
|
||||
left + coveredWidth,
|
||||
top + coveredHeight
|
||||
)
|
||||
}
|
||||
|
||||
fun Matrix.mapViewYToFrame(viewY: Float): Float {
|
||||
val values = FloatArray(9)
|
||||
getValues(values)
|
||||
val scaleY = values[Matrix.MSCALE_Y]
|
||||
return if (scaleY != 0f) {
|
||||
(viewY - values[Matrix.MTRANS_Y]) / scaleY
|
||||
} else {
|
||||
viewY
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,13 @@ package de.markusfisch.android.binaryeye.graphics
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import androidx.core.graphics.createBitmap
|
||||
|
||||
fun Bitmap.fixTransparency(): Bitmap {
|
||||
val result = analyzeTransparency()
|
||||
val cfg = config
|
||||
return if (cfg != null && result.hasTransparentPixels) {
|
||||
val copy = Bitmap.createBitmap(
|
||||
val copy = createBitmap(
|
||||
width,
|
||||
height,
|
||||
cfg
|
||||
|
||||
@@ -6,8 +6,8 @@ import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.support.annotation.MainThread
|
||||
import android.widget.EditText
|
||||
import androidx.annotation.MainThread
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.alertDialog
|
||||
import java.io.File
|
||||
|
||||
@@ -5,13 +5,20 @@ import android.media.ToneGenerator
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
|
||||
private var confirmToneGenerator: ToneGenerator? = null
|
||||
private var confirmToneStream = AudioManager.STREAM_MUSIC
|
||||
private var errorToneGenerator: ToneGenerator? = null
|
||||
|
||||
fun beepConfirm() {
|
||||
val tg = confirmToneGenerator ?: ToneGenerator(
|
||||
AudioManager.STREAM_NOTIFICATION,
|
||||
ToneGenerator.MAX_VOLUME
|
||||
)
|
||||
val stream = prefs.beepStream()
|
||||
val tg = if (confirmToneGenerator == null || confirmToneStream != stream) {
|
||||
confirmToneGenerator?.release()
|
||||
ToneGenerator(stream, ToneGenerator.MAX_VOLUME).also {
|
||||
confirmToneGenerator = it
|
||||
confirmToneStream = stream
|
||||
}
|
||||
} else {
|
||||
confirmToneGenerator!!
|
||||
}
|
||||
confirmToneGenerator = tg
|
||||
tg.startTone(prefs.beepTone())
|
||||
}
|
||||
@@ -28,6 +35,7 @@ fun beepError() {
|
||||
fun releaseToneGenerators() {
|
||||
confirmToneGenerator?.release()
|
||||
confirmToneGenerator = null
|
||||
confirmToneStream = AudioManager.STREAM_MUSIC
|
||||
errorToneGenerator?.release()
|
||||
errorToneGenerator = null
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package de.markusfisch.android.binaryeye.preference
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Point
|
||||
import androidx.core.content.edit
|
||||
import org.json.JSONObject
|
||||
|
||||
fun SharedPreferences.storeCropHandle(
|
||||
@@ -10,13 +11,15 @@ fun SharedPreferences.storeCropHandle(
|
||||
viewHeight: Int,
|
||||
cropHandle: Point
|
||||
) {
|
||||
edit().putString(
|
||||
cropHandleKey(name, viewWidth, viewHeight),
|
||||
JSONObject().apply {
|
||||
put("x", cropHandle.x)
|
||||
put("y", cropHandle.y)
|
||||
}.toString()
|
||||
).apply()
|
||||
edit {
|
||||
putString(
|
||||
cropHandleKey(name, viewWidth, viewHeight),
|
||||
JSONObject().apply {
|
||||
put("x", cropHandle.x)
|
||||
put("y", cropHandle.y)
|
||||
}.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun SharedPreferences.restoreCropHandle(
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package de.markusfisch.android.binaryeye.preference
|
||||
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
data class IgnoreCode(
|
||||
val pattern: String,
|
||||
val format: String? = null
|
||||
) {
|
||||
private val compiledRegex: Regex? by lazy {
|
||||
try {
|
||||
Regex(pattern)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun matches(content: String, format: String): Boolean =
|
||||
(this.format == null || this.format == format) &&
|
||||
compiledRegex?.containsMatchIn(content) == true
|
||||
|
||||
fun toJson(): JSONObject = JSONObject().apply {
|
||||
put(KEY_PATTERN, pattern)
|
||||
put(KEY_FORMAT, format ?: "")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_PATTERN = "pattern"
|
||||
private const val KEY_FORMAT = "format"
|
||||
|
||||
fun fromJson(obj: JSONObject): IgnoreCode? {
|
||||
val pattern = obj.optString(KEY_PATTERN, "").trim()
|
||||
if (pattern.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
val format = obj.optString(KEY_FORMAT, "").trim()
|
||||
return IgnoreCode(
|
||||
pattern = pattern,
|
||||
format = format.ifEmpty { null }
|
||||
)
|
||||
}
|
||||
|
||||
fun fromJsonArray(json: String): MutableList<IgnoreCode> {
|
||||
val items = ArrayList<IgnoreCode>()
|
||||
val array = try {
|
||||
JSONArray(json)
|
||||
} catch (_: Exception) {
|
||||
JSONArray()
|
||||
}
|
||||
for (i in 0 until array.length()) {
|
||||
when (val item = array.opt(i)) {
|
||||
is String -> {
|
||||
val pattern = item.trim()
|
||||
if (pattern.isNotEmpty()) {
|
||||
items.add(IgnoreCode(pattern))
|
||||
}
|
||||
}
|
||||
|
||||
is JSONObject -> {
|
||||
fromJson(item)?.let { items.add(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
fun toJsonArray(items: List<IgnoreCode>): String {
|
||||
val array = JSONArray()
|
||||
items.forEach { item ->
|
||||
array.put(item.toJson())
|
||||
}
|
||||
return array.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,21 @@ import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Point
|
||||
import android.media.ToneGenerator
|
||||
import android.os.Build
|
||||
import android.preference.PreferenceManager
|
||||
import android.support.annotation.RequiresApi
|
||||
import androidx.core.content.edit
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.automation.AutomatedAction
|
||||
import de.markusfisch.android.binaryeye.automation.AutomatedAction.Companion.fromJsonArray
|
||||
import de.markusfisch.android.binaryeye.automation.AutomatedAction.Companion.toJsonArray
|
||||
import de.markusfisch.android.binaryeye.zxingcpp.migrateBarcodeFormatName
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import de.markusfisch.android.binaryeye.preference.IgnoreCode.Companion.fromJsonArray as ignoreCodesFromJsonArray
|
||||
import de.markusfisch.android.binaryeye.preference.IgnoreCode.Companion.toJsonArray as ignoreCodesToJsonArray
|
||||
|
||||
class Preferences {
|
||||
lateinit var defaultPreferences: SharedPreferences
|
||||
@@ -22,31 +29,42 @@ class Preferences {
|
||||
|
||||
var profile: String? = null
|
||||
set(value) {
|
||||
defaultPreferences.edit().putString(PROFILE, value).apply()
|
||||
defaultPreferences.edit {
|
||||
putString(PROFILE, value)
|
||||
}
|
||||
field = value
|
||||
}
|
||||
var barcodeFormats = setOf(
|
||||
BarcodeFormat.AZTEC.name,
|
||||
BarcodeFormat.CODABAR.name,
|
||||
BarcodeFormat.CODE_39.name,
|
||||
BarcodeFormat.CODE_93.name,
|
||||
BarcodeFormat.CODE_128.name,
|
||||
BarcodeFormat.DATA_BAR.name,
|
||||
BarcodeFormat.DATA_BAR_EXPANDED.name,
|
||||
BarcodeFormat.DATA_MATRIX.name,
|
||||
BarcodeFormat.DX_FILM_EDGE.name,
|
||||
BarcodeFormat.EAN_8.name,
|
||||
BarcodeFormat.EAN_13.name,
|
||||
BarcodeFormat.Aztec.name,
|
||||
BarcodeFormat.Codabar.name,
|
||||
BarcodeFormat.Code39Std.name,
|
||||
BarcodeFormat.Code39Ext.name,
|
||||
BarcodeFormat.Code32.name,
|
||||
BarcodeFormat.PZN.name,
|
||||
BarcodeFormat.Code93.name,
|
||||
BarcodeFormat.Code128.name,
|
||||
BarcodeFormat.DataBar.name,
|
||||
BarcodeFormat.DataBarOmni.name,
|
||||
BarcodeFormat.DataBarStk.name,
|
||||
BarcodeFormat.DataBarLtd.name,
|
||||
BarcodeFormat.DataBarExp.name,
|
||||
BarcodeFormat.DataBarExpStk.name,
|
||||
BarcodeFormat.DataMatrix.name,
|
||||
BarcodeFormat.DXFilmEdge.name,
|
||||
BarcodeFormat.EAN8.name,
|
||||
BarcodeFormat.EAN13.name,
|
||||
BarcodeFormat.ITF.name,
|
||||
BarcodeFormat.MAXICODE.name,
|
||||
BarcodeFormat.PDF_417.name,
|
||||
BarcodeFormat.QR_CODE.name,
|
||||
BarcodeFormat.MICRO_QR_CODE.name,
|
||||
BarcodeFormat.RMQR_CODE.name,
|
||||
BarcodeFormat.UPC_A.name,
|
||||
BarcodeFormat.UPC_E.name,
|
||||
BarcodeFormat.MaxiCode.name,
|
||||
BarcodeFormat.PDF417.name,
|
||||
BarcodeFormat.Telepen.name,
|
||||
BarcodeFormat.TelepenAlpha.name,
|
||||
BarcodeFormat.TelepenNumeric.name,
|
||||
BarcodeFormat.QRCode.name,
|
||||
BarcodeFormat.MicroQRCode.name,
|
||||
BarcodeFormat.RMQRCode.name,
|
||||
BarcodeFormat.UPCA.name,
|
||||
BarcodeFormat.UPCE.name,
|
||||
)
|
||||
@RequiresApi(Build.VERSION_CODES.HONEYCOMB)
|
||||
set(value) {
|
||||
apply(BARCODE_FORMATS, value)
|
||||
field = value
|
||||
@@ -56,6 +74,11 @@ class Preferences {
|
||||
apply(SHOW_CROP_HANDLE, value)
|
||||
field = value
|
||||
}
|
||||
var showCrosshairs = false
|
||||
set(value) {
|
||||
apply(SHOW_CROSSHAIRS, value)
|
||||
field = value
|
||||
}
|
||||
var zoomBySwiping = true
|
||||
set(value) {
|
||||
apply(ZOOM_BY_SWIPING, value)
|
||||
@@ -101,6 +124,11 @@ class Preferences {
|
||||
apply(BEEP_TONE_NAME, value)
|
||||
field = value
|
||||
}
|
||||
var beepStreamName = "stream_music"
|
||||
set(value) {
|
||||
apply(BEEP_STREAM_NAME, value)
|
||||
field = value
|
||||
}
|
||||
var useHistory = false
|
||||
set(value) {
|
||||
apply(USE_HISTORY, value)
|
||||
@@ -111,6 +139,8 @@ class Preferences {
|
||||
apply(IGNORE_DUPLICATES_NAME, value)
|
||||
field = value
|
||||
}
|
||||
var ignoreCodes = mutableListOf(IgnoreCode(DEFAULT_IGNORE_CODE_PATTERN))
|
||||
private set
|
||||
var copyImmediately = false
|
||||
set(value) {
|
||||
apply(COPY_IMMEDIATELY, value)
|
||||
@@ -121,6 +151,11 @@ class Preferences {
|
||||
apply(OPEN_IMMEDIATELY, value)
|
||||
field = value
|
||||
}
|
||||
var stripTrackingParams = true
|
||||
set(value) {
|
||||
apply(STRIP_TRACKING_PARAMS, value)
|
||||
field = value
|
||||
}
|
||||
var showMetaData = true
|
||||
set(value) {
|
||||
apply(SHOW_META_DATA, value)
|
||||
@@ -223,6 +258,11 @@ class Preferences {
|
||||
apply(PREVIEW_SCALE, value)
|
||||
field = value
|
||||
}
|
||||
var dynamicColors = true
|
||||
set(value) {
|
||||
apply(DYNAMIC_COLORS, value)
|
||||
field = value
|
||||
}
|
||||
var automatedActions = mutableListOf<AutomatedAction>()
|
||||
private set
|
||||
|
||||
@@ -233,6 +273,7 @@ class Preferences {
|
||||
defaultPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
loadProfiles()
|
||||
addPredefinedProfiles(context)
|
||||
load(context, defaultPreferences.getString(PROFILE, null))
|
||||
}
|
||||
|
||||
@@ -270,36 +311,230 @@ class Preferences {
|
||||
}
|
||||
|
||||
fun saveProfiles() {
|
||||
defaultPreferences.edit().putString(
|
||||
PROFILES,
|
||||
JSONArray(profiles).toString()
|
||||
).apply()
|
||||
defaultPreferences.edit {
|
||||
putString(PROFILES, org.json.JSONArray(profiles).toString())
|
||||
}
|
||||
}
|
||||
|
||||
fun loadProfiles() {
|
||||
profiles.clear()
|
||||
JSONArray(
|
||||
org.json.JSONArray(
|
||||
defaultPreferences.getString(PROFILES, "[]")
|
||||
).let {
|
||||
profiles.addAll(Array(it.length()) { i -> it.getString(i) })
|
||||
}
|
||||
}
|
||||
|
||||
fun update() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
preferences.getStringSet(BARCODE_FORMATS, barcodeFormats)?.let {
|
||||
barcodeFormats = addFormatsOnUpdate(
|
||||
it,
|
||||
BarcodeFormat.RMQR_CODE,
|
||||
BarcodeFormat.DX_FILM_EDGE
|
||||
)
|
||||
fun profileLabel(context: Context, name: String?) = when (name) {
|
||||
null -> context.getString(R.string.profile_default)
|
||||
PROFILE_EXPERT -> context.getString(R.string.mode_expert)
|
||||
PROFILE_SIMPLE -> context.getString(R.string.mode_simple)
|
||||
else -> name
|
||||
}
|
||||
|
||||
fun loadExpertProfile(context: Context) {
|
||||
load(context, PROFILE_EXPERT)
|
||||
}
|
||||
|
||||
fun loadSimpleProfile(context: Context) {
|
||||
load(context, PROFILE_SIMPLE)
|
||||
}
|
||||
|
||||
fun toJson(context: Context, name: String?): String {
|
||||
val defaults = Preferences()
|
||||
val p = if (name.isNullOrEmpty()) {
|
||||
defaultPreferences
|
||||
} else {
|
||||
context.getSharedPreferences(name, Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
val json = JSONObject()
|
||||
json.put("type", PROFILE_EXPORT_TYPE)
|
||||
json.put("name", name ?: "")
|
||||
json.put(BARCODE_FORMATS, JSONArray().apply {
|
||||
(p.getStringSet(BARCODE_FORMATS, null) ?: defaults.barcodeFormats).forEach {
|
||||
put(it)
|
||||
}
|
||||
})
|
||||
json.put(SHOW_CROP_HANDLE, p, defaults.showCropHandle)
|
||||
json.put(SHOW_CROSSHAIRS, p, defaults.showCrosshairs)
|
||||
json.put(ZOOM_BY_SWIPING, p, defaults.zoomBySwiping)
|
||||
json.put(AUTO_ROTATE, p, defaults.autoRotate)
|
||||
json.put(TRY_HARDER, p, defaults.tryHarder)
|
||||
json.put(BULK_MODE, p, defaults.bulkMode)
|
||||
json.put(BULK_MODE_DELAY, p, defaults.bulkModeDelay)
|
||||
json.put(SHOW_TOAST_IN_BULK_MODE, p, defaults.showToastInBulkMode)
|
||||
json.put(VIBRATE, p, defaults.vibrate)
|
||||
json.put(BEEP, p, defaults.beep)
|
||||
json.put(BEEP_TONE_NAME, p, defaults.beepToneName)
|
||||
json.put(BEEP_STREAM_NAME, p, defaults.beepStreamName)
|
||||
json.put(USE_HISTORY, p, defaults.useHistory)
|
||||
json.put(IGNORE_DUPLICATES_NAME, p, defaults.ignoreDuplicatesName)
|
||||
json.put(IGNORE_CODES, p, ignoreCodesToJsonArray(defaults.ignoreCodes))
|
||||
json.put(COPY_IMMEDIATELY, p, defaults.copyImmediately)
|
||||
json.put(OPEN_IMMEDIATELY, p, defaults.openImmediately)
|
||||
json.put(STRIP_TRACKING_PARAMS, p, defaults.stripTrackingParams)
|
||||
json.put(SHOW_META_DATA, p, defaults.showMetaData)
|
||||
json.put(SHOW_HEX_DUMP, p, defaults.showHexDump)
|
||||
json.put(SHOW_CHECKSUM, p, defaults.showChecksum)
|
||||
json.put(SHOW_RECREATION, p, defaults.showRecreation)
|
||||
json.put(CLOSE_AUTOMATICALLY, p, defaults.closeAutomatically)
|
||||
json.put(DEFAULT_SEARCH_URL, p, defaults.defaultSearchUrl)
|
||||
json.put(OPEN_WITH_URL, p, defaults.openWithUrl)
|
||||
json.put(SEND_SCAN_ACTIVE, p, defaults.sendScanActive)
|
||||
json.put(SEND_SCAN_URL, p, defaults.sendScanUrl)
|
||||
json.put(SEND_SCAN_TYPE, p, defaults.sendScanType)
|
||||
json.put(SEND_SCAN_BLUETOOTH, p, defaults.sendScanBluetooth)
|
||||
json.put(SEND_SCAN_BLUETOOTH_HOST, p, defaults.sendScanBluetoothHost)
|
||||
json.put(CUSTOM_LOCALE, p, defaults.customLocale)
|
||||
json.put(INDEX_OF_LAST_SELECTED_FORMAT, p, defaults.indexOfLastSelectedFormat)
|
||||
json.put(INDEX_OF_LAST_SELECTED_EC_LEVEL, p, defaults.indexOfLastSelectedEcLevel)
|
||||
json.put(FREE_ROTATION, p, defaults.freeRotation)
|
||||
json.put(EXPAND_ESCAPE_SEQUENCES, p, defaults.expandEscapeSequences)
|
||||
json.put(ADD_QUIET_ZONE, p, defaults.addQuietZone)
|
||||
json.put(BRIGHTEN_SCREEN, p, defaults.brightenScreen)
|
||||
json.put(PREVIEW_SCALE, p, defaults.previewScale)
|
||||
json.put(AUTOMATED_ACTIONS, p.getString(AUTOMATED_ACTIONS, null) ?: "[]")
|
||||
|
||||
return json.toString()
|
||||
}
|
||||
|
||||
fun importProfile(context: Context, text: String): String? {
|
||||
val json = try {
|
||||
JSONObject(text)
|
||||
} catch (_: Exception) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (json.optString("type") != PROFILE_EXPORT_TYPE) {
|
||||
return null
|
||||
}
|
||||
|
||||
var sanitized = json.optString("name").ifEmpty {
|
||||
"imported"
|
||||
}.replace(Regex("[^a-zA-Z0-9_-]+"), "_")
|
||||
.trim('_')
|
||||
if (sanitized.isEmpty()) {
|
||||
sanitized = "imported"
|
||||
}
|
||||
|
||||
val name = if (profiles.contains(sanitized)) {
|
||||
val timestamp = SimpleDateFormat(
|
||||
"yyyyMMdd_HHmmss",
|
||||
Locale.US
|
||||
).format(Date())
|
||||
"${sanitized}_$timestamp"
|
||||
} else {
|
||||
sanitized
|
||||
}
|
||||
if (!addProfile(name)) {
|
||||
return null
|
||||
}
|
||||
|
||||
val target = context.getSharedPreferences(
|
||||
name,
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
try {
|
||||
target.edit {
|
||||
val formatsArray = json.optJSONArray(BARCODE_FORMATS)
|
||||
if (formatsArray != null) {
|
||||
val formats = mutableSetOf<String>()
|
||||
for (i in 0 until formatsArray.length()) {
|
||||
val format = formatsArray.getString(i)
|
||||
.migrateBarcodeFormatName()
|
||||
try {
|
||||
formats.add(BarcodeFormat.valueOf(format).name)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
// Ignore unknown formats.
|
||||
}
|
||||
}
|
||||
if (formats.isNotEmpty()) {
|
||||
putStringSet(BARCODE_FORMATS, formats)
|
||||
}
|
||||
}
|
||||
putBoolean(SHOW_CROP_HANDLE, json)
|
||||
putBoolean(SHOW_CROSSHAIRS, json)
|
||||
putBoolean(ZOOM_BY_SWIPING, json)
|
||||
putBoolean(AUTO_ROTATE, json)
|
||||
putBoolean(TRY_HARDER, json)
|
||||
putBoolean(BULK_MODE, json)
|
||||
putString(BULK_MODE_DELAY, json)
|
||||
putBoolean(SHOW_TOAST_IN_BULK_MODE, json)
|
||||
putBoolean(VIBRATE, json)
|
||||
putBoolean(BEEP, json)
|
||||
putString(BEEP_TONE_NAME, json)
|
||||
putString(BEEP_STREAM_NAME, json)
|
||||
putBoolean(USE_HISTORY, json)
|
||||
putString(IGNORE_DUPLICATES_NAME, json)
|
||||
putString(IGNORE_CODES, json)
|
||||
putBoolean(COPY_IMMEDIATELY, json)
|
||||
putBoolean(OPEN_IMMEDIATELY, json)
|
||||
putBoolean(STRIP_TRACKING_PARAMS, json)
|
||||
putBoolean(SHOW_META_DATA, json)
|
||||
putBoolean(SHOW_HEX_DUMP, json)
|
||||
putString(SHOW_CHECKSUM, json)
|
||||
putBoolean(SHOW_RECREATION, json)
|
||||
putBoolean(CLOSE_AUTOMATICALLY, json)
|
||||
putString(DEFAULT_SEARCH_URL, json)
|
||||
putString(OPEN_WITH_URL, json)
|
||||
putBoolean(SEND_SCAN_ACTIVE, json)
|
||||
putString(SEND_SCAN_URL, json)
|
||||
putString(SEND_SCAN_TYPE, json)
|
||||
putBoolean(SEND_SCAN_BLUETOOTH, json)
|
||||
putString(SEND_SCAN_BLUETOOTH_HOST, json)
|
||||
putString(CUSTOM_LOCALE, json)
|
||||
putInt(INDEX_OF_LAST_SELECTED_FORMAT, json)
|
||||
putInt(INDEX_OF_LAST_SELECTED_EC_LEVEL, json)
|
||||
putBoolean(FREE_ROTATION, json)
|
||||
putBoolean(EXPAND_ESCAPE_SEQUENCES, json)
|
||||
putBoolean(ADD_QUIET_ZONE, json)
|
||||
putBoolean(BRIGHTEN_SCREEN, json)
|
||||
putFloat(PREVIEW_SCALE, json)
|
||||
putString(AUTOMATED_ACTIONS, json)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
removeProfile(name)
|
||||
target.edit {
|
||||
clear()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
fun update() {
|
||||
preferences.getStringSet(BARCODE_FORMATS, barcodeFormats)?.let {
|
||||
barcodeFormats = addFormatsOnUpdate(
|
||||
migrateCode39ToCode39Std(
|
||||
migrateBarcodeFormatNames(it)
|
||||
),
|
||||
BarcodeFormat.Code39Std,
|
||||
BarcodeFormat.Code39Ext,
|
||||
BarcodeFormat.Code32,
|
||||
BarcodeFormat.PZN,
|
||||
BarcodeFormat.DataBarOmni,
|
||||
BarcodeFormat.DataBarStk,
|
||||
BarcodeFormat.DataBarLtd,
|
||||
BarcodeFormat.DataBarExp,
|
||||
BarcodeFormat.DataBarExpStk,
|
||||
BarcodeFormat.RMQRCode,
|
||||
BarcodeFormat.DXFilmEdge,
|
||||
BarcodeFormat.Telepen,
|
||||
BarcodeFormat.TelepenAlpha,
|
||||
BarcodeFormat.TelepenNumeric
|
||||
)
|
||||
}
|
||||
|
||||
showCropHandle = preferences.getBoolean(
|
||||
SHOW_CROP_HANDLE,
|
||||
showCropHandle
|
||||
)
|
||||
showCrosshairs = preferences.getBoolean(
|
||||
SHOW_CROSSHAIRS,
|
||||
showCrosshairs
|
||||
)
|
||||
zoomBySwiping = preferences.getBoolean(ZOOM_BY_SWIPING, zoomBySwiping)
|
||||
autoRotate = preferences.getBoolean(AUTO_ROTATE, autoRotate)
|
||||
tryHarder = preferences.getBoolean(TRY_HARDER, tryHarder)
|
||||
@@ -317,12 +552,17 @@ class Preferences {
|
||||
preferences.getString(BEEP_TONE_NAME, beepToneName)?.also {
|
||||
beepToneName = it
|
||||
}
|
||||
preferences.getString(BEEP_STREAM_NAME, beepStreamName)?.also {
|
||||
beepStreamName = it
|
||||
}
|
||||
useHistory = preferences.getBoolean(USE_HISTORY, useHistory)
|
||||
|
||||
// Map old setting to new one if it wasn't on default value.
|
||||
val ignoreConsecutiveDuplicates = "ignore_consecutive_duplicates"
|
||||
if (!preferences.getBoolean(ignoreConsecutiveDuplicates, true)) {
|
||||
preferences.edit().remove(ignoreConsecutiveDuplicates).apply()
|
||||
preferences.edit {
|
||||
remove(ignoreConsecutiveDuplicates)
|
||||
}
|
||||
ignoreDuplicatesName = "accept_duplicates"
|
||||
}
|
||||
|
||||
@@ -332,6 +572,14 @@ class Preferences {
|
||||
)?.also {
|
||||
ignoreDuplicatesName = it
|
||||
}
|
||||
ignoreCodes = ignoreCodesFromJsonArray(
|
||||
preferences.getString(
|
||||
IGNORE_CODES,
|
||||
ignoreCodesToJsonArray(
|
||||
listOf(IgnoreCode(DEFAULT_IGNORE_CODE_PATTERN))
|
||||
)
|
||||
) ?: "[]"
|
||||
)
|
||||
copyImmediately = preferences.getBoolean(
|
||||
COPY_IMMEDIATELY,
|
||||
copyImmediately
|
||||
@@ -340,6 +588,10 @@ class Preferences {
|
||||
OPEN_IMMEDIATELY,
|
||||
openImmediately
|
||||
)
|
||||
stripTrackingParams = preferences.getBoolean(
|
||||
STRIP_TRACKING_PARAMS,
|
||||
stripTrackingParams
|
||||
)
|
||||
showMetaData = preferences.getBoolean(SHOW_META_DATA, showMetaData)
|
||||
showHexDump = preferences.getBoolean(SHOW_HEX_DUMP, showHexDump)
|
||||
preferences.getString(SHOW_CHECKSUM, showChecksum)?.also {
|
||||
@@ -401,8 +653,9 @@ class Preferences {
|
||||
PREVIEW_SCALE,
|
||||
previewScale
|
||||
)
|
||||
dynamicColors = preferences.getBoolean(DYNAMIC_COLORS, dynamicColors)
|
||||
automatedActions = fromJsonArray(
|
||||
preferences.getString(AUTOMATED_ACTIONS, "[]") ?: "[]"
|
||||
preferences.getString(AUTOMATED_ACTIONS, null) ?: "[]"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -411,19 +664,14 @@ class Preferences {
|
||||
apply(AUTOMATED_ACTIONS, toJsonArray(actions))
|
||||
}
|
||||
|
||||
private fun addFormatsOnUpdate(
|
||||
restored: Set<String>,
|
||||
vararg formats: BarcodeFormat
|
||||
) = restored.toMutableSet().apply {
|
||||
for (format in formats) {
|
||||
val name = "${format.name}_added"
|
||||
if (!preferences.getBoolean(name, false)) {
|
||||
preferences.edit().putBoolean(name, true).apply()
|
||||
add(format.name)
|
||||
}
|
||||
}
|
||||
fun setIgnoreCodes(patterns: List<IgnoreCode>) {
|
||||
ignoreCodes = patterns.toMutableList()
|
||||
apply(IGNORE_CODES, ignoreCodesToJsonArray(patterns))
|
||||
}
|
||||
|
||||
fun shouldIgnoreHistoryContent(content: String, format: String): Boolean =
|
||||
ignoreCodes.any { it.matches(content, format) }
|
||||
|
||||
fun restoreCropHandle(
|
||||
name: String,
|
||||
viewWidth: Int,
|
||||
@@ -446,39 +694,170 @@ class Preferences {
|
||||
else -> ToneGenerator.TONE_PROP_BEEP
|
||||
}
|
||||
|
||||
fun beepStream() = when (beepStreamName) {
|
||||
"stream_notification" -> android.media.AudioManager.STREAM_NOTIFICATION
|
||||
else -> android.media.AudioManager.STREAM_MUSIC
|
||||
}
|
||||
|
||||
fun ignoreDuplicates() = when (ignoreDuplicatesName) {
|
||||
"ignore_consecutive_duplicates" -> IgnoreDuplicates.Consecutive
|
||||
"ignore_any_duplicates" -> IgnoreDuplicates.Any
|
||||
else -> IgnoreDuplicates.Never
|
||||
}
|
||||
|
||||
private fun put(label: String, value: Boolean) =
|
||||
preferences.edit().putBoolean(label, value)
|
||||
private fun migrateCode39ToCode39Std(
|
||||
restored: Set<String>
|
||||
) = restored.toMutableSet().apply {
|
||||
// Migrate Code39 to the more specific Code39Std.
|
||||
if (contains(BarcodeFormat.Code39.name)) {
|
||||
remove(BarcodeFormat.Code39.name)
|
||||
if (!contains(BarcodeFormat.Code39Std.name)) {
|
||||
add(BarcodeFormat.Code39Std.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun migrateBarcodeFormatNames(formats: Set<String>): Set<String> {
|
||||
return formats.mapTo(mutableSetOf()) { format ->
|
||||
format.migrateBarcodeFormatName()
|
||||
}
|
||||
}
|
||||
|
||||
private fun addFormatsOnUpdate(
|
||||
restored: Set<String>,
|
||||
vararg formats: BarcodeFormat
|
||||
) = restored.toMutableSet().apply {
|
||||
for (format in formats) {
|
||||
val name = "${format.name}_added"
|
||||
if (!preferences.getBoolean(name, false)) {
|
||||
preferences.edit {
|
||||
putBoolean(name, true)
|
||||
}
|
||||
add(format.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addPredefinedProfiles(context: Context) {
|
||||
val name = "predefined_profiles_added"
|
||||
if (defaultPreferences.getBoolean(name, false)) {
|
||||
return
|
||||
}
|
||||
defaultPreferences.edit {
|
||||
putBoolean(name, true)
|
||||
}
|
||||
|
||||
profiles.add(PROFILE_EXPERT)
|
||||
context.getSharedPreferences(PROFILE_EXPERT, Context.MODE_PRIVATE)
|
||||
.edit {
|
||||
clear()
|
||||
applyExpertProfileSettings()
|
||||
}
|
||||
|
||||
profiles.add(PROFILE_SIMPLE)
|
||||
context.getSharedPreferences(PROFILE_SIMPLE, Context.MODE_PRIVATE)
|
||||
.edit {
|
||||
clear()
|
||||
applySimpleProfileSettings()
|
||||
}
|
||||
|
||||
saveProfiles()
|
||||
}
|
||||
|
||||
private fun SharedPreferences.Editor.applyExpertProfileSettings() {
|
||||
val defaults = Preferences()
|
||||
putStringSet(BARCODE_FORMATS, defaults.barcodeFormats)
|
||||
putBoolean(SHOW_CROP_HANDLE, defaults.showCropHandle)
|
||||
putBoolean(SHOW_CROSSHAIRS, defaults.showCrosshairs)
|
||||
putBoolean(ZOOM_BY_SWIPING, defaults.zoomBySwiping)
|
||||
putBoolean(AUTO_ROTATE, defaults.autoRotate)
|
||||
putBoolean(TRY_HARDER, defaults.tryHarder)
|
||||
putBoolean(BULK_MODE, defaults.bulkMode)
|
||||
putString(BULK_MODE_DELAY, defaults.bulkModeDelay)
|
||||
putBoolean(SHOW_TOAST_IN_BULK_MODE, defaults.showToastInBulkMode)
|
||||
putBoolean(VIBRATE, defaults.vibrate)
|
||||
putBoolean(BEEP, defaults.beep)
|
||||
putString(BEEP_TONE_NAME, defaults.beepToneName)
|
||||
putString(BEEP_STREAM_NAME, defaults.beepStreamName)
|
||||
putBoolean(USE_HISTORY, defaults.useHistory)
|
||||
putString(IGNORE_DUPLICATES_NAME, defaults.ignoreDuplicatesName)
|
||||
putString(IGNORE_CODES, ignoreCodesToJsonArray(defaults.ignoreCodes))
|
||||
putBoolean(COPY_IMMEDIATELY, defaults.copyImmediately)
|
||||
putBoolean(OPEN_IMMEDIATELY, defaults.openImmediately)
|
||||
putBoolean(STRIP_TRACKING_PARAMS, defaults.stripTrackingParams)
|
||||
putBoolean(SHOW_META_DATA, defaults.showMetaData)
|
||||
putBoolean(SHOW_HEX_DUMP, defaults.showHexDump)
|
||||
putString(SHOW_CHECKSUM, defaults.showChecksum)
|
||||
putBoolean(SHOW_RECREATION, defaults.showRecreation)
|
||||
putBoolean(CLOSE_AUTOMATICALLY, defaults.closeAutomatically)
|
||||
putString(DEFAULT_SEARCH_URL, defaults.defaultSearchUrl)
|
||||
putString(OPEN_WITH_URL, defaults.openWithUrl)
|
||||
putBoolean(SEND_SCAN_ACTIVE, defaults.sendScanActive)
|
||||
putString(SEND_SCAN_URL, defaults.sendScanUrl)
|
||||
putString(SEND_SCAN_TYPE, defaults.sendScanType)
|
||||
putBoolean(SEND_SCAN_BLUETOOTH, defaults.sendScanBluetooth)
|
||||
putString(SEND_SCAN_BLUETOOTH_HOST, defaults.sendScanBluetoothHost)
|
||||
putString(CUSTOM_LOCALE, defaults.customLocale)
|
||||
putInt(INDEX_OF_LAST_SELECTED_FORMAT, defaults.indexOfLastSelectedFormat)
|
||||
putInt(INDEX_OF_LAST_SELECTED_EC_LEVEL, defaults.indexOfLastSelectedEcLevel)
|
||||
putBoolean(FREE_ROTATION, defaults.freeRotation)
|
||||
putBoolean(EXPAND_ESCAPE_SEQUENCES, defaults.expandEscapeSequences)
|
||||
putBoolean(ADD_QUIET_ZONE, defaults.addQuietZone)
|
||||
putBoolean(BRIGHTEN_SCREEN, defaults.brightenScreen)
|
||||
putFloat(PREVIEW_SCALE, defaults.previewScale)
|
||||
putString(AUTOMATED_ACTIONS, toJsonArray(defaults.automatedActions))
|
||||
}
|
||||
|
||||
private fun SharedPreferences.Editor.applySimpleProfileSettings() {
|
||||
applyExpertProfileSettings()
|
||||
putBoolean(SHOW_CROP_HANDLE, false)
|
||||
putBoolean(ZOOM_BY_SWIPING, false)
|
||||
putBoolean(BEEP, true)
|
||||
putBoolean(COPY_IMMEDIATELY, true)
|
||||
putBoolean(OPEN_IMMEDIATELY, true)
|
||||
putBoolean(SHOW_META_DATA, false)
|
||||
putBoolean(SHOW_HEX_DUMP, false)
|
||||
putString(DEFAULT_SEARCH_URL, SIMPLE_SEARCH_URL)
|
||||
putString(OPEN_WITH_URL, SIMPLE_SEARCH_URL)
|
||||
putBoolean(EXPAND_ESCAPE_SEQUENCES, false)
|
||||
putBoolean(BRIGHTEN_SCREEN, true)
|
||||
}
|
||||
|
||||
private fun apply(label: String, value: Boolean) {
|
||||
put(label, value).apply()
|
||||
preferences.edit {
|
||||
putBoolean(label, value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun apply(label: String, value: String) {
|
||||
preferences.edit().putString(label, value).apply()
|
||||
preferences.edit {
|
||||
putString(label, value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun apply(label: String, value: Int) {
|
||||
preferences.edit().putInt(label, value).apply()
|
||||
preferences.edit {
|
||||
putInt(label, value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun apply(label: String, value: Float) {
|
||||
preferences.edit().putFloat(label, value).apply()
|
||||
preferences.edit {
|
||||
putFloat(label, value)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ApplySharedPref")
|
||||
private fun commit(label: String, value: String) {
|
||||
preferences.edit().putString(label, value).commit()
|
||||
preferences.edit(true) {
|
||||
putString(label, value)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.HONEYCOMB)
|
||||
private fun apply(label: String, value: Set<String>) {
|
||||
preferences.edit().putStringSet(label, value).apply()
|
||||
preferences.edit {
|
||||
putStringSet(label, value)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -486,10 +865,15 @@ class Preferences {
|
||||
Consecutive, Any, Never
|
||||
}
|
||||
|
||||
private const val PROFILE_EXPORT_TYPE = "binaryeye_profile"
|
||||
private const val PROFILES = "profiles"
|
||||
private const val PROFILE = "profile"
|
||||
private const val PROFILE_EXPERT = "builtin:expert"
|
||||
private const val PROFILE_SIMPLE = "builtin:simple"
|
||||
private const val SIMPLE_SEARCH_URL = "https://www.google.com/search?q="
|
||||
private const val BARCODE_FORMATS = "formats"
|
||||
private const val SHOW_CROP_HANDLE = "show_crop_handle"
|
||||
private const val SHOW_CROSSHAIRS = "show_crosshairs"
|
||||
private const val ZOOM_BY_SWIPING = "zoom_by_swiping"
|
||||
private const val AUTO_ROTATE = "auto_rotate"
|
||||
private const val TRY_HARDER = "try_harder"
|
||||
@@ -499,9 +883,12 @@ class Preferences {
|
||||
private const val VIBRATE = "vibrate"
|
||||
private const val BEEP = "beep"
|
||||
private const val BEEP_TONE_NAME = "beep_tone_name"
|
||||
private const val BEEP_STREAM_NAME = "beep_stream_name"
|
||||
private const val USE_HISTORY = "use_history"
|
||||
private const val IGNORE_DUPLICATES_NAME = "ignore_duplicates_name"
|
||||
private const val IGNORE_CODES = "ignore_codes"
|
||||
private const val OPEN_IMMEDIATELY = "open_immediately"
|
||||
private const val STRIP_TRACKING_PARAMS = "strip_tracking_params"
|
||||
private const val COPY_IMMEDIATELY = "copy_immediately"
|
||||
private const val SHOW_META_DATA = "show_meta_data"
|
||||
private const val SHOW_HEX_DUMP = "show_hex_dump"
|
||||
@@ -524,5 +911,47 @@ class Preferences {
|
||||
private const val BRIGHTEN_SCREEN = "brighten_screen"
|
||||
private const val PREVIEW_SCALE = "preview_scale"
|
||||
private const val AUTOMATED_ACTIONS = "automated_actions"
|
||||
private const val DYNAMIC_COLORS = "dynamic_colors"
|
||||
private const val DEFAULT_IGNORE_CODE_PATTERN = "^FIDO://.*"
|
||||
}
|
||||
}
|
||||
|
||||
private fun JSONObject.put(key: String, p: SharedPreferences, default: Boolean) {
|
||||
put(key, p.getBoolean(key, default))
|
||||
}
|
||||
|
||||
private fun JSONObject.put(key: String, p: SharedPreferences, default: String) {
|
||||
put(key, p.getString(key, null) ?: default)
|
||||
}
|
||||
|
||||
private fun JSONObject.put(key: String, p: SharedPreferences, default: Int) {
|
||||
put(key, p.getInt(key, default))
|
||||
}
|
||||
|
||||
private fun JSONObject.put(key: String, p: SharedPreferences, default: Float) {
|
||||
put(key, p.getFloat(key, default))
|
||||
}
|
||||
|
||||
private fun SharedPreferences.Editor.putBoolean(key: String, json: JSONObject) {
|
||||
if (json.has(key)) {
|
||||
putBoolean(key, json.getBoolean(key))
|
||||
}
|
||||
}
|
||||
|
||||
private fun SharedPreferences.Editor.putString(key: String, json: JSONObject) {
|
||||
if (json.has(key)) {
|
||||
putString(key, json.getString(key))
|
||||
}
|
||||
}
|
||||
|
||||
private fun SharedPreferences.Editor.putInt(key: String, json: JSONObject) {
|
||||
if (json.has(key)) {
|
||||
putInt(key, json.getInt(key))
|
||||
}
|
||||
}
|
||||
|
||||
private fun SharedPreferences.Editor.putFloat(key: String, json: JSONObject) {
|
||||
if (json.has(key)) {
|
||||
putFloat(key, json.getDouble(key).toFloat())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package de.markusfisch.android.binaryeye.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.support.v7.preference.DialogPreference
|
||||
import android.util.AttributeSet
|
||||
import androidx.preference.DialogPreference
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
class UrlPreference(
|
||||
context: Context?,
|
||||
context: Context,
|
||||
attrs: AttributeSet?
|
||||
) : DialogPreference(context, attrs) {
|
||||
private var url: String? = null
|
||||
|
||||
@@ -5,7 +5,7 @@ import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.service.quicksettings.TileService
|
||||
import android.support.annotation.RequiresApi
|
||||
import androidx.annotation.RequiresApi
|
||||
import de.markusfisch.android.binaryeye.activity.CameraActivity
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
|
||||
@@ -29,10 +29,14 @@ fun Context.errorFeedback() {
|
||||
|
||||
private fun Context.isSilent(): Boolean {
|
||||
val am = getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
return when (am.ringerMode) {
|
||||
AudioManager.RINGER_MODE_SILENT,
|
||||
AudioManager.RINGER_MODE_VIBRATE -> true
|
||||
return if (prefs.beepStream() == AudioManager.STREAM_MUSIC) {
|
||||
am.getStreamVolume(AudioManager.STREAM_MUSIC) == 0
|
||||
} else {
|
||||
when (am.ringerMode) {
|
||||
AudioManager.RINGER_MODE_SILENT,
|
||||
AudioManager.RINGER_MODE_VIBRATE -> true
|
||||
|
||||
else -> false
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,18 @@ package de.markusfisch.android.binaryeye.view
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.AbsListView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
val systemBarListViewScrollListener = object : AbsListView.OnScrollListener {
|
||||
@@ -43,7 +45,7 @@ val systemBarRecyclerViewScrollListener = object : RecyclerView.OnScrollListener
|
||||
layoutManager.findFirstCompletelyVisibleItemPosition() != 0
|
||||
val scrollable = scrolled ||
|
||||
layoutManager.findLastVisibleItemPosition() <
|
||||
recyclerView.adapter.itemCount - 1
|
||||
(recyclerView.adapter?.itemCount ?: 0) - 1
|
||||
colorSystemAndToolBars(recyclerView.context, scrolled, scrollable)
|
||||
}
|
||||
|
||||
@@ -62,24 +64,22 @@ private fun lastChildOutOfView(listView: AbsListView): Boolean {
|
||||
}
|
||||
|
||||
fun AppCompatActivity.initBars() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
// Keeps the soft keyboard from repositioning the layout.
|
||||
window.setSoftInputMode(
|
||||
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
|
||||
)
|
||||
window.decorView.systemUiVisibility =
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
}
|
||||
// Keeps the soft keyboard from repositioning the layout.
|
||||
window.setSoftInputMode(
|
||||
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
|
||||
)
|
||||
window.decorView.systemUiVisibility =
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
colorSystemAndToolBars(this)
|
||||
setPaddingFromWindowInsets(
|
||||
findViewById(R.id.main),
|
||||
(findViewById(R.id.toolbar) as Toolbar).apply {
|
||||
setSupportActionBar(this)
|
||||
},
|
||||
findViewById(R.id.navbar)?.apply {
|
||||
setBackgroundColor(translucentPrimaryColor)
|
||||
findViewById<View>(R.id.navbar)?.apply {
|
||||
setBackgroundColor(getTranslucentPrimaryColor(this@initBars))
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -93,55 +93,49 @@ fun unlockStatusBarColor() {
|
||||
statusBarColorLocked = false
|
||||
}
|
||||
|
||||
private var translucentPrimaryColor = 0
|
||||
private val actionBarBackground = ColorDrawable()
|
||||
private fun getTranslucentPrimaryColor(context: Context): Int {
|
||||
return ColorUtils.setAlphaComponent(
|
||||
MaterialColors.getColor(
|
||||
context,
|
||||
com.google.android.material.R.attr.colorSurfaceContainerHigh,
|
||||
Color.BLACK
|
||||
),
|
||||
0xd8
|
||||
)
|
||||
}
|
||||
|
||||
fun colorSystemAndToolBars(
|
||||
context: Context,
|
||||
scrolled: Boolean = false,
|
||||
scrollable: Boolean = false
|
||||
) {
|
||||
if (translucentPrimaryColor == 0) {
|
||||
translucentPrimaryColor = ContextCompat.getColor(
|
||||
context,
|
||||
R.color.primary_translucent
|
||||
)
|
||||
}
|
||||
val translucentPrimaryColor = getTranslucentPrimaryColor(context)
|
||||
val topColor = if (scrolled) translucentPrimaryColor else 0
|
||||
val activity = getAppCompatActivity(context) ?: return
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
// System bars no longer have a background from SDK35+.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||
val window = activity.window
|
||||
if (!statusBarColorLocked) {
|
||||
@Suppress("DEPRECATION")
|
||||
window.statusBarColor = topColor
|
||||
}
|
||||
// System bars no longer have a background from SDK35+.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||
val window = activity.window
|
||||
if (!statusBarColorLocked) {
|
||||
@Suppress("DEPRECATION")
|
||||
window.navigationBarColor = if (scrolled || scrollable) {
|
||||
translucentPrimaryColor
|
||||
} else {
|
||||
0
|
||||
}
|
||||
window.statusBarColor = topColor
|
||||
}
|
||||
@Suppress("DEPRECATION")
|
||||
window.navigationBarColor = if (scrolled || scrollable) {
|
||||
translucentPrimaryColor
|
||||
} else {
|
||||
activity.findViewById(R.id.navbar)?.apply {
|
||||
visibility = if (scrolled || scrollable) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
0
|
||||
}
|
||||
} else {
|
||||
activity.findViewById<View>(R.id.navbar)?.apply {
|
||||
visibility = if (scrolled || scrollable) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
setBackgroundColor(translucentPrimaryColor)
|
||||
}
|
||||
}
|
||||
activity.supportActionBar?.setBackgroundDrawable(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
// Avoid allocation on Honeycomb and better.
|
||||
actionBarBackground.color = topColor
|
||||
actionBarBackground
|
||||
} else {
|
||||
// ColorDrawable.setColor() doesn't exist pre Honeycomb.
|
||||
ColorDrawable(topColor)
|
||||
}
|
||||
)
|
||||
activity.supportActionBar?.setBackgroundDrawable(topColor.toDrawable())
|
||||
}
|
||||
|
||||
private fun getAppCompatActivity(context: Context): AppCompatActivity? {
|
||||
|
||||
@@ -2,12 +2,11 @@ package de.markusfisch.android.binaryeye.view
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.support.annotation.RequiresApi
|
||||
import android.support.v4.view.ViewCompat
|
||||
import android.support.v4.view.WindowInsetsCompat
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.view.View
|
||||
import android.view.WindowInsets
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
|
||||
private var toolbarHeight = 0
|
||||
fun setPaddingFromWindowInsets(
|
||||
@@ -53,19 +52,15 @@ fun View.setPaddingFromWindowInsets() {
|
||||
// For the original post see here:
|
||||
// https://medium.com/androiddevelopers/windowinsets-listeners-to-layouts-8f9ccc8fa4d1
|
||||
fun View.doOnApplyWindowInsets(f: (View, Rect) -> Unit) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
f(this, insetsWithToolbar())
|
||||
} else {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets ->
|
||||
f(v, insetsWithToolbar(insets))
|
||||
insets
|
||||
}
|
||||
// It's important to explicitly request the insets (again) in
|
||||
// case the view was created in Fragment.onCreateView() because
|
||||
// setOnApplyWindowInsetsListener() won't fire when the view
|
||||
// isn't attached.
|
||||
requestApplyInsetsWhenAttached()
|
||||
ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets ->
|
||||
f(v, insetsWithToolbar(insets))
|
||||
insets
|
||||
}
|
||||
// It's important to explicitly request the insets (again) in
|
||||
// case the view was created in Fragment.onCreateView() because
|
||||
// setOnApplyWindowInsetsListener() won't fire when the view
|
||||
// isn't attached.
|
||||
requestApplyInsetsWhenAttached()
|
||||
}
|
||||
|
||||
private fun insetsWithToolbar(insets: WindowInsetsCompat? = null) = Rect(
|
||||
@@ -75,7 +70,6 @@ private fun insetsWithToolbar(insets: WindowInsetsCompat? = null) = Rect(
|
||||
insets?.systemWindowInsetBottom ?: 0
|
||||
)
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.KITKAT)
|
||||
private fun View.requestApplyInsetsWhenAttached() {
|
||||
if (isAttachedToWindow) {
|
||||
ViewCompat.requestApplyInsets(this)
|
||||
|
||||
@@ -3,20 +3,22 @@ package de.markusfisch.android.binaryeye.widget
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.graphics.Point
|
||||
import android.graphics.Rect
|
||||
import android.graphics.Region
|
||||
import android.os.Build
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.graphics.withSave
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.graphics.getBitmapFromDrawable
|
||||
import de.markusfisch.android.binaryeye.graphics.getDashedBorderPaint
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
@@ -34,14 +36,27 @@ class DetectorView : View {
|
||||
coordinatesLast = 0
|
||||
invalidate()
|
||||
}
|
||||
private val roiPaint = context.getDashedBorderPaint()
|
||||
private val overlayColor = ColorUtils.setAlphaComponent(
|
||||
MaterialColors.getColor(
|
||||
context,
|
||||
com.google.android.material.R.attr.colorSecondaryContainer,
|
||||
Color.WHITE
|
||||
),
|
||||
0x99
|
||||
)
|
||||
private val dotPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = ContextCompat.getColor(context, R.color.dot)
|
||||
color = overlayColor
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
private val crosshairPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = overlayColor
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = context.resources.displayMetrics.density
|
||||
}
|
||||
private val dotRadius: Float
|
||||
private val handleBitmap = resources.getBitmapFromDrawable(
|
||||
R.drawable.button_crop
|
||||
R.drawable.button_crop,
|
||||
context.theme
|
||||
)
|
||||
private val handleXRadius = handleBitmap.width / 2
|
||||
private val handleYRadius = handleBitmap.height / 2
|
||||
@@ -248,12 +263,21 @@ class DetectorView : View {
|
||||
if (handleActive) {
|
||||
canvas.drawClip()
|
||||
}
|
||||
if (prefs.showCrosshairs) {
|
||||
canvas.drawCrosshairs()
|
||||
}
|
||||
canvas.drawDots()
|
||||
if (prefs.showCropHandle) {
|
||||
canvas.drawHandle()
|
||||
}
|
||||
}
|
||||
|
||||
fun getHorizontalCrosshairY() = if (handleActive) {
|
||||
(roi.top + roi.bottom) * .5f
|
||||
} else {
|
||||
viewHeight * .5f
|
||||
}
|
||||
|
||||
private fun Canvas.drawDots() {
|
||||
var i = 0
|
||||
while (i < coordinatesLast) {
|
||||
@@ -270,10 +294,8 @@ class DetectorView : View {
|
||||
if (minDist < 1) {
|
||||
return
|
||||
}
|
||||
// canvas.clipRect() doesn't work reliably below KITKAT.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
val radius = min(minDist / 2, cornerRadius).toFloat()
|
||||
save()
|
||||
val radius = min(minDist / 2, cornerRadius).toFloat()
|
||||
withSave {
|
||||
clipOutPathCompat(
|
||||
calculateRoundedRectPath(
|
||||
roi.left.toFloat(),
|
||||
@@ -285,9 +307,6 @@ class DetectorView : View {
|
||||
)
|
||||
)
|
||||
drawColor(shadeColor)
|
||||
restore()
|
||||
} else {
|
||||
drawRect(roi, roiPaint)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,6 +319,28 @@ class DetectorView : View {
|
||||
)
|
||||
}
|
||||
|
||||
private fun Canvas.drawCrosshairs() {
|
||||
val left: Float
|
||||
val top: Float
|
||||
val right: Float
|
||||
val bottom: Float
|
||||
if (handleActive) {
|
||||
left = roi.left.toFloat()
|
||||
top = roi.top.toFloat()
|
||||
right = roi.right.toFloat()
|
||||
bottom = roi.bottom.toFloat()
|
||||
} else {
|
||||
left = 0f
|
||||
top = 0f
|
||||
right = viewWidth.toFloat()
|
||||
bottom = viewHeight.toFloat()
|
||||
}
|
||||
val cx = (left + right) * .5f
|
||||
val cy = (top + bottom) * .5f
|
||||
drawLine(cx, top, cx, bottom, crosshairPaint)
|
||||
drawLine(left, cy, right, cy, crosshairPaint)
|
||||
}
|
||||
|
||||
private fun updateClipRect() {
|
||||
clampHandlePos()
|
||||
val dx = abs(handlePos.x - center.x)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package de.markusfisch.android.binaryeye.zxingcpp
|
||||
|
||||
val oldToNewFormatNames = mapOf(
|
||||
"NONE" to "None",
|
||||
"AZTEC" to "Aztec",
|
||||
"CODABAR" to "Codabar",
|
||||
"CODE_39" to "Code39",
|
||||
"CODE_39_EXT" to "Code39Ext",
|
||||
"CODE_32" to "Code32",
|
||||
"PZN" to "PZN",
|
||||
"CODE_93" to "Code93",
|
||||
"CODE_128" to "Code128",
|
||||
"DATA_BAR" to "DataBar",
|
||||
"DATA_BAR_OMNI" to "DataBarOmni",
|
||||
"DATA_BAR_STK" to "DataBarStk",
|
||||
"DATA_BAR_LTD" to "DataBarLtd",
|
||||
"DATA_BAR_EXPANDED" to "DataBarExp",
|
||||
"DATA_BAR_EXP_STK" to "DataBarExpStk",
|
||||
"DATA_MATRIX" to "DataMatrix",
|
||||
"DX_FILM_EDGE" to "DXFilmEdge",
|
||||
"EAN_8" to "EAN8",
|
||||
"EAN_13" to "EAN13",
|
||||
"ITF" to "ITF",
|
||||
"MAXICODE" to "MaxiCode",
|
||||
"PDF_417" to "PDF417",
|
||||
"TELEPEN" to "Telepen",
|
||||
"TELEPEN_ALPHA" to "TelepenAlpha",
|
||||
"TELEPEN_NUMERIC" to "TelepenNumeric",
|
||||
"QR_CODE" to "QRCode",
|
||||
"MICRO_QR_CODE" to "MicroQRCode",
|
||||
"RMQR_CODE" to "RMQRCode",
|
||||
"UPC_A" to "UPCA",
|
||||
"UPC_E" to "UPCE"
|
||||
)
|
||||
|
||||
/**
|
||||
* Translate legacy barcode format names from ZXingC++ before 3.x
|
||||
* to current BarcodeFormat enum names.
|
||||
*/
|
||||
fun String.migrateBarcodeFormatName() = oldToNewFormatNames[this] ?: this
|
||||
@@ -0,0 +1,3 @@
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="?attr/colorSecondaryContainer"/>
|
||||
</selector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="?attr/colorSecondaryContainer"/>
|
||||
</selector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="?attr/colorOnSecondaryContainer"/>
|
||||
</selector>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
@@ -1,5 +1,5 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@color/primary"/>
|
||||
<item android:drawable="?attr/colorSurface"/>
|
||||
<item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@color/background_color"/>
|
||||
<item>
|
||||
<bitmap
|
||||
android:gravity="top|right"
|
||||
android:src="@drawable/wallpaper"/>
|
||||
</item>
|
||||
<item android:drawable="?attr/colorSurface"/>
|
||||
<item
|
||||
android:gravity="top|right"
|
||||
android:drawable="@drawable/wallpaper"/>
|
||||
</layer-list>
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt" android:width="56dp" android:height="56dp" android:viewportWidth="56" android:viewportHeight="56">
|
||||
<path android:pathData="M4 32a24 24 0 1 1 48 0 24 24 0 1 1-48 0">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient android:gradientRadius="24" android:centerX="28" android:centerY="32" android:type="radial">
|
||||
<item android:offset="0" android:color="#FF000000"/>
|
||||
<item android:offset="1" android:color="#00000000"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path android:pathData="M4 28a24 24 0 1 1 48 0 24 24 0 1 1-48 0" android:fillColor="#B6D46F"/>
|
||||
<path android:pathData="M33 31h2v-8c0-1.1-0.9-2-2-2h-8v2h8v8zm-10 2V17h-2v4h-4v2h4v10c0 1.1 0.9 2 2 2h10v4h2v-4h4v-2H23z" android:fillColor="#FFFFFF"/>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="56dp"
|
||||
android:height="56dp"
|
||||
android:viewportWidth="56"
|
||||
android:viewportHeight="56">
|
||||
<path android:pathData="M4 32a24 24 0 1 1 48 0 24 24 0 1 1-48 0">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:gradientRadius="24"
|
||||
android:centerX="28"
|
||||
android:centerY="32"
|
||||
android:type="radial">
|
||||
<item android:offset="0" android:color="#FF000000"/>
|
||||
<item android:offset="1" android:color="#00000000"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M4 28a24 24 0 1 1 48 0 24 24 0 1 1-48 0"
|
||||
android:fillColor="@color/crop_handle_fill"/>
|
||||
<path
|
||||
android:pathData="M33 31h2v-8c0-1.1-0.9-2-2-2h-8v2h8v8zm-10 2V17h-2v4h-4v2h4v10c0 1.1 0.9 2 2 2h10v4h2v-4h4v-2H23z"
|
||||
android:fillColor="@color/crop_handle_icon"/>
|
||||
</vector>
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
<item android:state_pressed="false">
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="2dp"/>
|
||||
<solid android:color="@color/accent"/>
|
||||
<solid android:color="?attr/colorSecondary"/>
|
||||
</shape>
|
||||
</item>
|
||||
<item android:state_pressed="true">
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="2dp"/>
|
||||
<solid android:color="@color/accent_dark"/>
|
||||
<solid android:color="?attr/colorSecondaryContainer"/>
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
<selector
|
||||
<ripple
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:constantSize="true">
|
||||
<item android:state_pressed="false">
|
||||
android:color="?attr/colorControlHighlight">
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="2dp"/>
|
||||
<solid android:color="@color/selected_row"/>
|
||||
<solid android:color="?attr/colorSecondaryContainer"/>
|
||||
</shape>
|
||||
</item>
|
||||
<item android:state_pressed="true">
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="2dp"/>
|
||||
<solid android:color="@color/pressed_row"/>
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
</ripple>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M313 520l224 224-57 56-320-320 320-320 57 56-224 224h487v80H313z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M480 340q13 0 21.5-9t8.5-21q0-13-8.5-21.5T480 280q-12 0-21 8.5t-9 21.5q0 12 9 21t21 9zM172 595q-15-26-23.5-54.5T140 480q0-32 8.5-60.5T172 365l64 50q-8 15-12 31t-4 34q0 18 4 34t12 31l-64 50zM62 681q-29-44-45.5-94.5T0 480q0-56 16.5-106.5T62 279l63 50q-21 33-33 71t-12 80q0 42 12 80.5t33 71.5l-63 49zm298 119q-33 0-56.5-23.5T280 720V240q0-33 23.5-56.5T360 160h240q33 0 56.5 23.5T680 240v480q0 33-23.5 56.5T600 800H360zm0-80h240V240H360v480zm427-125l-64-50q8-15 12.5-31t4.5-34q0-18-4.5-34T723 415l64-50q15 26 24 54.5t9 60.5q0 32-9 60.5T787 595zm111 85l-63-49q21-33 33-71t12-80q0-42-12-80.5T835 328l63-49q29 43 45.5 93.5T960 479q0 56-16.5 106.5T898 680zm-538 40V240v480z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M440 880V576L256 760l-56-56 224-224-224-224 56-56 184 184V80h40l228 228-172 172 172 172-228 228h-40zm80-496l76-76-76-74v150zm0 342l76-74-76-76v150z"/>
|
||||
</vector>
|
||||
@@ -1,3 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
<path android:fillColor="#fff" android:pathData="M20 8.69V4h-4.69L12 0.69 8.69 4H4v4.69L0.69 12 4 15.31V20h4.69L12 23.31 15.31 20H20v-4.69L23.31 12 20 8.69zM12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6 6 2.69 6 6-2.69 6-6 6zm0-10c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4z"/>
|
||||
</vector>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<vector android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<vector android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M14 10H2v2h12v-2zm0-4H2v2h12V6zm4 8v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zM2 16h8v-2H2v2z"/>
|
||||
</vector>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0">
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:tint="?attr/colorControlNormal" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0">
|
||||
<path android:fillColor="#FFFFFFFF" android:pathData="M16 1H4C2.9 1 2 1.9 2 3v14h2V3h12V1zm3 4H8C6.9 5 6 5.9 6 7v14c0 1.1 0.9 2 2 2h11c1.1 0 2-0.9 2-2V7c0-1.1-0.9-2-2-2zm0 16H8V7h11v14z"/>
|
||||
</vector>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<vector android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M12.65 10C11.83 7.67 9.61 6 7 6c-3.31 0-6 2.69-6 6s2.69 6 6 6c2.61 0 4.83-1.67 5.65-4H17v4h4v-4h2v-4H12.65zM7 14c-1.1 0-2-0.9-2-2s0.9-2 2-2 2 0.9 2 2-0.9 2-2 2z"/>
|
||||
</vector>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0">
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:tint="?attr/colorControlNormal" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0">
|
||||
<path android:fillColor="#FFFFFFFF" android:pathData="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||
</vector>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M680 920V760H280q-33 0-56.5-23.5T200 680V280H40v-80h160V40h80v640h640v80H760v160h-80zm0-320V280H360v-80h320q33 0 56.5 23.5T760 280v320h-80z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M340.5 811.5Q275 783 226 734t-77.5-114.5Q120 554 120 480q0-44 10-85.5t29-78q19-36.5 45.5-68T264 192l272 272-56 56-216-216q-30 36-47 80.5T200 480q0 116 82 198t198 82q116 0 198-82t82-198q0-107-68.5-184.5T520 204v76h-80V120h40q74 0 139.5 28.5T734 226q49 49 77.5 114.5T840 480q0 74-28.5 139.5T734 734q-49 49-114.5 77.5T480 840q-74 0-139.5-28.5zm-89-303Q240 497 240 480t11.5-28.5Q263 440 280 440t28.5 11.5Q320 463 320 480t-11.5 28.5Q297 520 280 520t-28.5-11.5zm200 200Q440 697 440 680t11.5-28.5Q463 640 480 640t28.5 11.5Q520 663 520 680t-11.5 28.5Q497 720 480 720t-28.5-11.5zm200-200Q640 497 640 480t11.5-28.5Q663 440 680 440t28.5 11.5Q720 463 720 480t-11.5 28.5Q697 520 680 520t-28.5-11.5z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M440 160v487L216 423l-56 57 320 320 320-320-56-57-224 224V160h-80z"/>
|
||||
</vector>
|
||||
@@ -1,3 +1,3 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<vector android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c0.39-0.39 0.39-1.02 0-1.41l-2.34-2.34c-0.39-0.39-1.02-0.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/>
|
||||
</vector>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<vector android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M9 3L5 6.99h3V14h2V6.99h3L9 3zm7 14.01V10h-2v7.01h-3L15 21l4-3.99h-3z"/>
|
||||
</vector>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0">
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:tint="?attr/colorControlNormal" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0">
|
||||
<path android:fillColor="#FFFFFFFF" android:pathData="M7 2v11h3v9l7-12h-4l4-8z"/>
|
||||
</vector>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user