Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1aaa45d14e | ||
|
|
b14028f9c4 | ||
|
|
124d8f2e6d | ||
|
|
0a5b9215af | ||
|
|
4672839146 | ||
|
|
7610fdd386 | ||
|
|
fba7b0d70f | ||
|
|
cb48544896 | ||
|
|
10ceb4ac4e | ||
|
|
a48f67ebd5 | ||
|
|
328d0d704c | ||
|
|
dcecec4d6f | ||
|
|
7546cf25c0 | ||
|
|
bedb0a81d6 | ||
|
|
658988525a | ||
|
|
8298ce70ef | ||
|
|
ae29a4d207 | ||
|
|
7fe8126703 | ||
|
|
4adce7c958 | ||
|
|
d11dac7f13 | ||
|
|
f90aaac4f9 | ||
|
|
d5ffcabdd2 | ||
|
|
52ca14c9ba | ||
|
|
6c2c7bef90 | ||
|
|
f285e1d200 | ||
|
|
d7e687871c | ||
|
|
5472993e19 | ||
|
|
702dc75ddd | ||
|
|
45054c80f8 | ||
|
|
5a899b5eb3 | ||
|
|
1d002b645d | ||
|
|
456da10f84 | ||
|
|
7fbf89380c | ||
|
|
91a549e96e | ||
|
|
3ef6f874b8 | ||
|
|
45661b75ba | ||
|
|
cd30a86c69 | ||
|
|
13eabaee19 | ||
|
|
2035c935dc | ||
|
|
1ff170a279 | ||
|
|
4fab21c88d | ||
|
|
1ef9f39978 | ||
|
|
5378e50fe9 | ||
|
|
c325713a8c | ||
|
|
a043fba948 | ||
|
|
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 |
@@ -1,5 +1,57 @@
|
||||
# Change Log
|
||||
|
||||
## 1.75.1
|
||||
* Update ZXingCpp
|
||||
* Keep KDE Connect device picker visible
|
||||
* Fix URL scan concatenation
|
||||
|
||||
## 1.75.0
|
||||
* Forward scans with KDE Connect
|
||||
* Add default camera preference
|
||||
|
||||
## 1.74.3
|
||||
* Fix layout for Material You
|
||||
|
||||
## 1.74.2
|
||||
* Fix layout for some Android versions
|
||||
|
||||
## 1.74.1
|
||||
* Allow scanning while device is locked
|
||||
* Fix rendering bug for some GPUs
|
||||
|
||||
## 1.74.0
|
||||
* Add an option to choose a custom beep tone
|
||||
* Add an option to set a custom scanner ID
|
||||
* Improve display of content type
|
||||
* Support fido:/ URIs
|
||||
* Update Italian and French translations
|
||||
* Update ZXingCpp
|
||||
* Fix FAB and crop handle positioning
|
||||
* Fix splash screen for some Android versions
|
||||
|
||||
## 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
|
||||
|
||||
@@ -5,10 +5,10 @@ all: debug install start
|
||||
debug:
|
||||
./gradlew assembleDebug
|
||||
|
||||
release: lint
|
||||
release: lint test
|
||||
./gradlew assembleRelease
|
||||
|
||||
bundle: lint
|
||||
bundle: lint test
|
||||
./gradlew bundleRelease
|
||||
|
||||
lint:
|
||||
|
||||
@@ -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]
|
||||
@@ -186,7 +187,7 @@ In order to set up Bluetooth forwarding to a computer:
|
||||
|
||||
1. Pair your devices (phone and pc) if not paired already
|
||||
2. Download and run a companion app:
|
||||
- Windows: https://github.com/KamaleiZestri/BinaryReceptorWindows
|
||||
- Windows: https://github.com/KamaleiZestri/BinaryReceptor
|
||||
- Linux: https://github.com/sean666888/bin_eye_bt_receiver
|
||||
3. In the settings of Binary Eye, enable "Forward scans with Bluetooth"
|
||||
and select the appropriate target host device that is running the
|
||||
@@ -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
|
||||
|
||||
+14
-13
@@ -8,8 +8,8 @@ android {
|
||||
minSdk 21
|
||||
targetSdk sdk_version
|
||||
|
||||
versionCode 166
|
||||
versionName '1.72.0'
|
||||
versionCode 176
|
||||
versionName '1.75.1'
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@@ -67,24 +67,25 @@ 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-datetime:0.7.1'
|
||||
implementation "androidx.core:core-ktx:1.17.0"
|
||||
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 "androidx.appcompat:appcompat:1.7.1"
|
||||
implementation "com.google.android.material:material:1.13.0"
|
||||
implementation "com.google.android.material:material:1.13.0" // Keep for minSDK 21.
|
||||
implementation "androidx.preference:preference:1.2.1"
|
||||
|
||||
// External
|
||||
implementation 'androidx.camera:camera-core:1.4.2'
|
||||
implementation 'androidx.camera:camera-camera2:1.4.2'
|
||||
implementation 'androidx.camera:camera-lifecycle:1.4.2'
|
||||
implementation 'androidx.camera:camera-view:1.4.2'
|
||||
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.3'
|
||||
implementation 'de.tsenger:vdstools:0.16.0-SNAPSHOT'
|
||||
implementation 'org.bouncycastle:bcpkix-jdk15to18:1.83'
|
||||
implementation 'com.github.markusfisch:zxing-cpp:v3.1.0.0'
|
||||
implementation 'de.tsenger:vdstools:0.17.0'
|
||||
implementation 'org.bouncycastle:bcpkix-jdk15to18:1.84'
|
||||
|
||||
// Testing
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
android:theme="@style/AppTheme">
|
||||
<activity
|
||||
android:name=".activity.SplashActivity"
|
||||
android:theme="@style/SplashTheme"
|
||||
android:theme="@style/AppTheme"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package de.markusfisch.android.binaryeye.actions
|
||||
|
||||
import de.markusfisch.android.binaryeye.actions.fido.FidoAction
|
||||
import de.markusfisch.android.binaryeye.actions.mail.MailAction
|
||||
import de.markusfisch.android.binaryeye.actions.mail.MatMsgAction
|
||||
import de.markusfisch.android.binaryeye.actions.otpauth.OtpauthAction
|
||||
@@ -17,6 +18,7 @@ object ActionRegistry {
|
||||
private val REGISTRY: Set<Action> = setOf(
|
||||
MailAction,
|
||||
MatMsgAction,
|
||||
FidoAction,
|
||||
OtpauthAction,
|
||||
SmsAction,
|
||||
TelAction,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package de.markusfisch.android.binaryeye.actions.fido
|
||||
|
||||
import android.content.Context
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.Action
|
||||
import de.markusfisch.android.binaryeye.content.openUrl
|
||||
|
||||
object FidoAction : Action() {
|
||||
private val fidoUriRegex = "^fido:/[0-9]+$".toRegex(
|
||||
RegexOption.IGNORE_CASE
|
||||
)
|
||||
|
||||
override val iconResId: Int = R.drawable.ic_action_open
|
||||
override val titleResId: Int = R.string.open_url
|
||||
|
||||
override fun canExecuteOn(data: ByteArray): Boolean {
|
||||
return String(data).trim().matches(fidoUriRegex)
|
||||
}
|
||||
|
||||
override suspend fun execute(context: Context, data: ByteArray) {
|
||||
executed = context.openUrl(String(data).trim())
|
||||
}
|
||||
}
|
||||
+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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+5
-4
@@ -1,6 +1,5 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
@@ -12,6 +11,8 @@ 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
|
||||
@@ -49,7 +50,7 @@ class AutomatedActionsActivity : AbstractBaseActivity() {
|
||||
editAction(null)
|
||||
}
|
||||
|
||||
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets(bottom = false)
|
||||
listView.setPaddingFromWindowInsets()
|
||||
}
|
||||
|
||||
@@ -125,7 +126,7 @@ class AutomatedActionsActivity : AbstractBaseActivity() {
|
||||
typeGroup.check(initialTypeId)
|
||||
updateTypeSections(initialTypeId)
|
||||
|
||||
val dialog = AlertDialog.Builder(this)
|
||||
val dialog = MaterialAlertDialogBuilder(this)
|
||||
.setTitle(
|
||||
if (action == null) {
|
||||
R.string.automated_action_add
|
||||
@@ -229,7 +230,7 @@ class AutomatedActionsActivity : AbstractBaseActivity() {
|
||||
}
|
||||
|
||||
private fun confirmRemoveAction(position: Int) {
|
||||
AlertDialog.Builder(this)
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.automated_action_remove_confirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
actions.removeAt(position)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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
|
||||
@@ -16,6 +15,7 @@ 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
|
||||
@@ -120,8 +120,8 @@ class BarcodeActivity : AbstractBaseActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
imageView.doOnApplyWindowInsets { _, insets ->
|
||||
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets(bottom = false)
|
||||
imageView.doOnApplyWindowInsets { _, insets, _ ->
|
||||
imageView.insets.set(insets)
|
||||
}
|
||||
}
|
||||
@@ -257,7 +257,7 @@ class BarcodeActivity : AbstractBaseActivity() {
|
||||
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])
|
||||
@@ -281,7 +281,7 @@ class BarcodeActivity : AbstractBaseActivity() {
|
||||
editText.setText(
|
||||
encodeFileName("${barcode.format}_${barcode.content}")
|
||||
)
|
||||
AlertDialog.Builder(this)
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val fileName = editText.text.toString()
|
||||
@@ -327,7 +327,7 @@ class BarcodeActivity : AbstractBaseActivity() {
|
||||
val sizeView = view.findViewById<TextView>(R.id.size_display)
|
||||
val sizeBarView = view.findViewById<SeekBar>(R.id.size_bar)
|
||||
sizeBarView.initSizeBar(sizeView)
|
||||
AlertDialog.Builder(this)
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
write(getSize(sizeBarView.progress))
|
||||
|
||||
@@ -2,7 +2,6 @@ package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
@@ -21,7 +20,6 @@ import android.widget.SeekBar
|
||||
import android.widget.Spinner
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.camera.core.Camera
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.core.FocusMeteringAction
|
||||
import androidx.camera.core.ImageAnalysis
|
||||
import androidx.camera.core.ImageProxy
|
||||
@@ -35,6 +33,7 @@ import androidx.camera.view.PreviewView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.net.toUri
|
||||
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.adapter.prettifyFormatName
|
||||
@@ -47,19 +46,23 @@ import de.markusfisch.android.binaryeye.app.hasCameraPermission
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.automation.runAutomatedActions
|
||||
import de.markusfisch.android.binaryeye.bluetooth.sendBluetoothAsync
|
||||
import de.markusfisch.android.binaryeye.camera.availableDefaultCameras
|
||||
import de.markusfisch.android.binaryeye.camera.cameraSelector
|
||||
import de.markusfisch.android.binaryeye.content.copyToClipboard
|
||||
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.toScan
|
||||
import de.markusfisch.android.binaryeye.graphics.FrameMetrics
|
||||
import de.markusfisch.android.binaryeye.graphics.mapPosition
|
||||
import de.markusfisch.android.binaryeye.graphics.mapViewYToFrame
|
||||
import de.markusfisch.android.binaryeye.graphics.setCovered
|
||||
import de.markusfisch.android.binaryeye.graphics.setFrameRoi
|
||||
import de.markusfisch.android.binaryeye.graphics.setFrameToView
|
||||
import de.markusfisch.android.binaryeye.kdeconnect.sendToKdeConnect
|
||||
import de.markusfisch.android.binaryeye.media.releaseToneGenerators
|
||||
import de.markusfisch.android.binaryeye.net.isScanDeeplink
|
||||
import de.markusfisch.android.binaryeye.net.sendAsync
|
||||
import de.markusfisch.android.binaryeye.net.urlEncode
|
||||
import de.markusfisch.android.binaryeye.os.showWhenLocked
|
||||
import de.markusfisch.android.binaryeye.preference.Preferences
|
||||
import de.markusfisch.android.binaryeye.view.errorFeedback
|
||||
import de.markusfisch.android.binaryeye.view.initBars
|
||||
@@ -84,10 +87,6 @@ import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class CameraActivity : AppCompatActivity() {
|
||||
private val frameRoi = Rect()
|
||||
private val previewRect = Rect()
|
||||
private val viewBounds = Rect()
|
||||
private val matrix = Matrix()
|
||||
private val analyzerExecutor: ExecutorService =
|
||||
Executors.newSingleThreadExecutor()
|
||||
private val readerOptions = ReaderOptions(
|
||||
@@ -106,15 +105,16 @@ class CameraActivity : AppCompatActivity() {
|
||||
|
||||
private var cameraProvider: ProcessCameraProvider? = null
|
||||
private var camera: Camera? = null
|
||||
private var preview: Preview? = null
|
||||
private var imageAnalysis: ImageAnalysis? = null
|
||||
private var currentProfile = prefs.profile
|
||||
private var shouldStoreSettings = true
|
||||
private var formatsToRead = setOf<BarcodeFormat>()
|
||||
private var frameMetrics = FrameMetrics()
|
||||
private var decoding = true
|
||||
private var returnResult = false
|
||||
private var returnUrlTemplate: String? = null
|
||||
private var finishAfterShowingResult = false
|
||||
private var frontFacing = false
|
||||
private var selectedCamera = prefs.defaultCamera
|
||||
private var bulkMode = prefs.bulkMode
|
||||
private var restrictFormat: String? = null
|
||||
private var searchTerm: Regex? = null
|
||||
@@ -122,6 +122,29 @@ class CameraActivity : AppCompatActivity() {
|
||||
private var requestCameraPermission = true
|
||||
private var useLocalAverage = false
|
||||
|
||||
@Volatile
|
||||
private var cameraViewSnapshot = CameraViewSnapshot()
|
||||
|
||||
private data class CameraViewSnapshot(
|
||||
val viewWidth: Int = 0,
|
||||
val viewHeight: Int = 0,
|
||||
val roiLeft: Int = 0,
|
||||
val roiTop: Int = 0,
|
||||
val roiRight: Int = 0,
|
||||
val roiBottom: Int = 0,
|
||||
val horizontalCrosshairY: Float = 0f
|
||||
) {
|
||||
fun isValid() = viewWidth > 0 && viewHeight > 0
|
||||
|
||||
fun toViewRoi() = Rect(roiLeft, roiTop, roiRight, roiBottom)
|
||||
}
|
||||
|
||||
private data class CameraFrameMapping(
|
||||
val frameRoi: Rect,
|
||||
val matrix: Matrix,
|
||||
val horizontalCrosshairY: Float
|
||||
)
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
@@ -170,6 +193,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
showWhenLocked()
|
||||
setContentView(R.layout.activity_camera)
|
||||
|
||||
// Necessary to get the right translation after setting a
|
||||
@@ -305,6 +329,10 @@ class CameraActivity : AppCompatActivity() {
|
||||
|
||||
private fun unbindCameraUseCases() {
|
||||
decoding = false
|
||||
imageAnalysis?.clearAnalyzer()
|
||||
imageAnalysis = null
|
||||
preview?.surfaceProvider = null
|
||||
preview = null
|
||||
cameraProvider?.unbindAll()
|
||||
camera = null
|
||||
}
|
||||
@@ -317,7 +345,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
override fun onRestoreInstanceState(savedState: Bundle) {
|
||||
super.onRestoreInstanceState(savedState)
|
||||
zoomBar.progress = savedState.getInt(ZOOM_LEVEL)
|
||||
frontFacing = savedState.getBoolean(FRONT_FACING)
|
||||
selectedCamera = savedState.getString(SELECTED_CAMERA, selectedCamera)
|
||||
bulkMode = savedState.getBoolean(BULK_MODE)
|
||||
restrictFormat = savedState.getString(RESTRICT_FORMAT)
|
||||
searchTerm = savedState.getString(SEARCH_TERM)?.toRegex()
|
||||
@@ -325,7 +353,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putInt(ZOOM_LEVEL, zoomBar.progress)
|
||||
outState.putBoolean(FRONT_FACING, frontFacing)
|
||||
outState.putString(SELECTED_CAMERA, selectedCamera)
|
||||
outState.putBoolean(BULK_MODE, bulkMode)
|
||||
outState.putString(RESTRICT_FORMAT, restrictFormat)
|
||||
outState.putString(SEARCH_TERM, searchTerm?.toString())
|
||||
@@ -346,7 +374,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
menu.findItem(R.id.bulk_mode).isChecked = bulkMode
|
||||
menu.findItem(R.id.profile).title = getString(
|
||||
R.string.current_profile,
|
||||
prefs.profile ?: getString(R.string.profile_default)
|
||||
prefs.profileLabel(this, prefs.profile)
|
||||
)
|
||||
return true
|
||||
}
|
||||
@@ -424,7 +452,10 @@ class CameraActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun switchCamera() {
|
||||
frontFacing = frontFacing xor true
|
||||
val cameras = availableDefaultCameras(this)
|
||||
selectedCamera = cameras[
|
||||
(cameras.indexOf(selectedCamera) + 1).mod(cameras.size)
|
||||
]
|
||||
bindCameraUseCases()
|
||||
}
|
||||
|
||||
@@ -439,7 +470,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
names.add(0, getString(R.string.remove_restriction))
|
||||
formats.add(0, null)
|
||||
}
|
||||
AlertDialog.Builder(this).apply {
|
||||
MaterialAlertDialogBuilder(this).apply {
|
||||
setTitle(R.string.restrict_format)
|
||||
setItems(names.toTypedArray()) { _, which ->
|
||||
restrictFormat = formats[which]
|
||||
@@ -462,7 +493,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
formatView.setSelection(
|
||||
values.indexOf(restrictFormat ?: "").coerceAtLeast(0)
|
||||
)
|
||||
AlertDialog.Builder(this)
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val term = editText.text.toString().trim()
|
||||
@@ -488,35 +519,32 @@ class CameraActivity : AppCompatActivity() {
|
||||
Intent.ACTION_VIEW,
|
||||
getString(R.string.project_url).toUri()
|
||||
)
|
||||
execShareIntent(intent)
|
||||
startIntentOrToast(intent)
|
||||
}
|
||||
|
||||
private fun pickProfile() {
|
||||
val profiles = arrayOf(
|
||||
getString(R.string.profile_default),
|
||||
) + prefs.profiles
|
||||
AlertDialog.Builder(this)
|
||||
val profiles = arrayOf<String?>(null) + prefs.profiles
|
||||
val labels = profiles.map {
|
||||
prefs.profileLabel(this, it)
|
||||
}.toTypedArray()
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.profile)
|
||||
.setItems(profiles) { _, which ->
|
||||
val newProfile = when (which) {
|
||||
0 -> {
|
||||
null
|
||||
}
|
||||
|
||||
else -> {
|
||||
profiles[which]
|
||||
}
|
||||
}
|
||||
.setItems(labels) { _, which ->
|
||||
val newProfile = profiles[which]
|
||||
if (currentProfile != newProfile) {
|
||||
storeSettings()
|
||||
prefs.load(this, newProfile)
|
||||
loadPreferences()
|
||||
refreshIfProfileChanged()
|
||||
switchProfile(newProfile)
|
||||
}
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun switchProfile(name: String?) {
|
||||
storeSettings()
|
||||
prefs.load(this, name)
|
||||
loadPreferences()
|
||||
refreshIfProfileChanged()
|
||||
}
|
||||
|
||||
private fun handleSendText(intent: Intent) {
|
||||
val text = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||
|
||||
@@ -562,7 +590,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
|
||||
private fun initPreviewView() {
|
||||
cameraView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
|
||||
updateFrameRoiAndMappingMatrix()
|
||||
updateCameraViewSnapshot()
|
||||
}
|
||||
@Suppress("ClickableViewAccessibility")
|
||||
val touchSlop = ViewConfiguration.get(this).scaledTouchSlop
|
||||
@@ -671,7 +699,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
}
|
||||
detectorView.onRoiChanged = {
|
||||
decoding = true
|
||||
updateFrameRoiAndMappingMatrix()
|
||||
updateCameraViewSnapshot()
|
||||
}
|
||||
detectorView.setPaddingFromWindowInsets()
|
||||
detectorView.cropHandleName = "camera_crop_handle"
|
||||
@@ -715,15 +743,17 @@ class CameraActivity : AppCompatActivity() {
|
||||
analyzeImage(image)
|
||||
}
|
||||
|
||||
val cameraSelector = CameraSelector.Builder()
|
||||
.requireLensFacing(
|
||||
if (frontFacing) {
|
||||
CameraSelector.LENS_FACING_FRONT
|
||||
} else {
|
||||
CameraSelector.LENS_FACING_BACK
|
||||
}
|
||||
)
|
||||
.build()
|
||||
var cameraSelector = cameraSelector(selectedCamera)
|
||||
if (!provider.hasCamera(cameraSelector)) {
|
||||
selectedCamera = prefs.defaultCamera
|
||||
cameraSelector = cameraSelector(selectedCamera)
|
||||
}
|
||||
if (!provider.hasCamera(cameraSelector)) {
|
||||
selectedCamera = availableDefaultCameras(this)
|
||||
.firstOrNull { provider.hasCamera(cameraSelector(it)) }
|
||||
?: selectedCamera
|
||||
cameraSelector = cameraSelector(selectedCamera)
|
||||
}
|
||||
|
||||
try {
|
||||
provider.unbindAll()
|
||||
@@ -734,12 +764,14 @@ class CameraActivity : AppCompatActivity() {
|
||||
imageAnalysis
|
||||
)
|
||||
preview.surfaceProvider = cameraView.surfaceProvider
|
||||
this.preview = preview
|
||||
this.imageAnalysis = imageAnalysis
|
||||
ignoreNext = null
|
||||
decoding = true
|
||||
useLocalAverage = false
|
||||
updateZoomState()
|
||||
updateFlashFab(camera?.cameraInfo?.hasFlashUnit() ?: false)
|
||||
updateFrameRoiAndMappingMatrix()
|
||||
updateCameraViewSnapshot()
|
||||
} catch (_: Exception) {
|
||||
camera = null
|
||||
toast(R.string.camera_error)
|
||||
@@ -752,21 +784,18 @@ class CameraActivity : AppCompatActivity() {
|
||||
if (!decoding) {
|
||||
return
|
||||
}
|
||||
frameMetrics = FrameMetrics(
|
||||
val frameMetrics = FrameMetrics(
|
||||
image.width,
|
||||
image.height,
|
||||
image.imageInfo.rotationDegrees
|
||||
)
|
||||
updateFrameRoiAndMappingMatrix()
|
||||
if (frameRoi.width() < 1 || frameRoi.height() < 1) {
|
||||
return
|
||||
}
|
||||
val frameMapping = getFrameMapping(frameMetrics) ?: return
|
||||
useLocalAverage = useLocalAverage xor true
|
||||
val yPlane = image.planes[0]
|
||||
ZxingCpp.readYBuffer(
|
||||
yPlane.buffer,
|
||||
yPlane.rowStride,
|
||||
frameRoi,
|
||||
frameMapping.frameRoi,
|
||||
frameMetrics.orientation,
|
||||
readerOptions.apply {
|
||||
binarizer = if (useLocalAverage) {
|
||||
@@ -779,14 +808,17 @@ class CameraActivity : AppCompatActivity() {
|
||||
tryRotate = prefs.autoRotate
|
||||
}
|
||||
)?.firstOrNull()?.let { result ->
|
||||
handleDecodeResult(result)
|
||||
handleDecodeResult(result, frameMapping)
|
||||
}
|
||||
} finally {
|
||||
image.close()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDecodeResult(result: Result) {
|
||||
private fun handleDecodeResult(
|
||||
result: Result,
|
||||
frameMapping: CameraFrameMapping
|
||||
) {
|
||||
val text = result.text
|
||||
if (text == ignoreNext) {
|
||||
return
|
||||
@@ -804,16 +836,21 @@ class CameraActivity : AppCompatActivity() {
|
||||
return
|
||||
}
|
||||
if (prefs.showCrosshairs &&
|
||||
!resultTouchesHorizontalCrosshair(result)
|
||||
!resultTouchesHorizontalCrosshair(result, frameMapping)
|
||||
) {
|
||||
return
|
||||
}
|
||||
decoding = false
|
||||
postResult(result)
|
||||
postResult(result, frameMapping)
|
||||
}
|
||||
|
||||
private fun resultTouchesHorizontalCrosshair(result: Result): Boolean {
|
||||
val crosshairY = matrix.mapViewYToFrame(detectorView.getHorizontalCrosshairY())
|
||||
private fun resultTouchesHorizontalCrosshair(
|
||||
result: Result,
|
||||
frameMapping: CameraFrameMapping
|
||||
): Boolean {
|
||||
val crosshairY = frameMapping.matrix.mapViewYToFrame(
|
||||
frameMapping.horizontalCrosshairY
|
||||
)
|
||||
val pos = result.position
|
||||
val minY = minOf(pos.topLeft.y, pos.topRight.y, pos.bottomLeft.y, pos.bottomRight.y)
|
||||
val maxY = maxOf(pos.topLeft.y, pos.topRight.y, pos.bottomLeft.y, pos.bottomRight.y)
|
||||
@@ -833,29 +870,55 @@ class CameraActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFrameRoiAndMappingMatrix() {
|
||||
viewBounds.set(0, 0, cameraView.width, cameraView.height)
|
||||
if (viewBounds.width() < 1 ||
|
||||
viewBounds.height() < 1 ||
|
||||
!frameMetrics.isValid()
|
||||
) {
|
||||
return
|
||||
}
|
||||
previewRect.setCovered(
|
||||
viewBounds.width(),
|
||||
viewBounds.height(),
|
||||
frameMetrics
|
||||
)
|
||||
private fun updateCameraViewSnapshot() {
|
||||
val viewRoi = if (
|
||||
detectorView.roi.width() < 1 ||
|
||||
detectorView.roi.height() < 1
|
||||
) {
|
||||
viewBounds
|
||||
Rect(0, 0, cameraView.width, cameraView.height)
|
||||
} else {
|
||||
detectorView.roi
|
||||
}
|
||||
frameRoi.setFrameRoi(frameMetrics, previewRect, viewRoi)
|
||||
matrix.setFrameToView(frameMetrics, previewRect, viewRoi)
|
||||
cameraViewSnapshot = CameraViewSnapshot(
|
||||
cameraView.width,
|
||||
cameraView.height,
|
||||
viewRoi.left,
|
||||
viewRoi.top,
|
||||
viewRoi.right,
|
||||
viewRoi.bottom,
|
||||
detectorView.getHorizontalCrosshairY()
|
||||
)
|
||||
}
|
||||
|
||||
private fun getFrameMapping(
|
||||
frameMetrics: FrameMetrics
|
||||
): CameraFrameMapping? {
|
||||
val snapshot = cameraViewSnapshot
|
||||
if (!snapshot.isValid() || !frameMetrics.isValid()) {
|
||||
return null
|
||||
}
|
||||
val previewRect = Rect().apply {
|
||||
setCovered(
|
||||
snapshot.viewWidth,
|
||||
snapshot.viewHeight,
|
||||
frameMetrics
|
||||
)
|
||||
}
|
||||
val viewRoi = snapshot.toViewRoi()
|
||||
val frameRoi = Rect().apply {
|
||||
setFrameRoi(frameMetrics, previewRect, viewRoi)
|
||||
}
|
||||
if (frameRoi.width() < 1 || frameRoi.height() < 1) {
|
||||
return null
|
||||
}
|
||||
val matrix = Matrix().apply {
|
||||
setFrameToView(frameMetrics, previewRect, viewRoi)
|
||||
}
|
||||
return CameraFrameMapping(
|
||||
frameRoi,
|
||||
matrix,
|
||||
snapshot.horizontalCrosshairY
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateFlashFab(available: Boolean) {
|
||||
@@ -875,53 +938,13 @@ class CameraActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
private fun Rect.setCovered(
|
||||
viewWidth: Int,
|
||||
viewHeight: Int,
|
||||
frameMetrics: FrameMetrics
|
||||
private fun postResult(
|
||||
result: Result,
|
||||
frameMapping: CameraFrameMapping
|
||||
) {
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
private fun postResult(result: Result) {
|
||||
cameraView.post {
|
||||
detectorView.update(
|
||||
matrix.mapPosition(
|
||||
frameMapping.matrix.mapPosition(
|
||||
result.position,
|
||||
detectorView.coordinates
|
||||
)
|
||||
@@ -944,12 +967,15 @@ class CameraActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
returnUri != null -> {
|
||||
execShareIntent(
|
||||
startIntentOrToast(
|
||||
Intent(Intent.ACTION_VIEW, returnUri)
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
if (importProfile(result.text)) {
|
||||
return@post
|
||||
}
|
||||
showResult(result, bulkMode)
|
||||
// If this app was invoked via a deep link but without
|
||||
// a return URI, we probably don't want to return to
|
||||
@@ -979,11 +1005,26 @@ class CameraActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun importProfile(text: String): Boolean {
|
||||
val name = prefs.importProfile(this, text) ?: return false
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(getString(R.string.profile_import_set_default, name))
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
switchProfile(name)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setOnDismissListener {
|
||||
decoding = true
|
||||
}
|
||||
.show()
|
||||
return true
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PICK_FILE_RESULT_CODE = 1
|
||||
private const val INITIAL_ZOOM = 0
|
||||
private const val ZOOM_LEVEL = "zoom"
|
||||
private const val FRONT_FACING = "front_facing"
|
||||
private const val SELECTED_CAMERA = "selected_camera"
|
||||
private const val BULK_MODE = "bulk_mode"
|
||||
private const val RESTRICT_FORMAT = "restrict_format"
|
||||
private const val SEARCH_TERM = "search_term"
|
||||
@@ -1008,16 +1049,7 @@ fun Activity.showResult(
|
||||
scan.id = db.insertScan(scan)
|
||||
}
|
||||
if (prefs.sendScanActive && prefs.sendScanUrl.isNotEmpty()) {
|
||||
if (prefs.sendScanType == "4") {
|
||||
openUrl(
|
||||
prefs.sendScanUrl + scan.text.urlEncode()
|
||||
)
|
||||
return
|
||||
}
|
||||
scan.sendAsync(
|
||||
prefs.sendScanUrl,
|
||||
prefs.sendScanType
|
||||
) { code, body ->
|
||||
sendAsync(scan) { code, body ->
|
||||
if (code == null || code < 200 || code > 299) {
|
||||
errorFeedback()
|
||||
}
|
||||
@@ -1054,11 +1086,17 @@ fun Activity.showResult(
|
||||
)
|
||||
}
|
||||
}
|
||||
if (prefs.sendScanKdeConnect &&
|
||||
!bulkMode &&
|
||||
sendToKdeConnect(scan)
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (runAutomatedActions(scan)) {
|
||||
return
|
||||
}
|
||||
if (!bulkMode) {
|
||||
startActivity(DecodeActivity.newIntent(this, scan))
|
||||
startActivity(DecodeActivity.newIntent(this, scan, showWhenLocked = true))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
@@ -11,11 +10,9 @@ import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.provider.DocumentsContract
|
||||
import android.text.Editable
|
||||
import android.text.Html
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.TextWatcher
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.text.style.TypefaceSpan
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
@@ -27,19 +24,12 @@ 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.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.adapter.prettifyFormatName
|
||||
import de.markusfisch.android.binaryeye.adapter.toFormatDescriptionResId
|
||||
import de.markusfisch.android.binaryeye.app.db
|
||||
@@ -47,12 +37,10 @@ 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.ParsedContentType
|
||||
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.openUrl
|
||||
import de.markusfisch.android.binaryeye.content.parseData
|
||||
import de.markusfisch.android.binaryeye.content.shareAsFile
|
||||
import de.markusfisch.android.binaryeye.content.shareText
|
||||
import de.markusfisch.android.binaryeye.content.toBarcode
|
||||
@@ -65,6 +53,7 @@ import de.markusfisch.android.binaryeye.io.askForFileName
|
||||
import de.markusfisch.android.binaryeye.io.toSaveResult
|
||||
import de.markusfisch.android.binaryeye.io.writeExternalFile
|
||||
import de.markusfisch.android.binaryeye.net.createEncodeDeeplink
|
||||
import de.markusfisch.android.binaryeye.os.showWhenLocked
|
||||
import de.markusfisch.android.binaryeye.view.hideSoftKeyboard
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
@@ -74,25 +63,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
|
||||
import de.markusfisch.android.binaryeye.content.ParsedField as Field
|
||||
|
||||
class DecodeActivity : AbstractBaseActivity() {
|
||||
private lateinit var labelHeadlineView: TextView
|
||||
private lateinit var contentView: EditText
|
||||
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 metaHeadlineView: TextView
|
||||
private lateinit var metaView: TableLayout
|
||||
private lateinit var hexHeadlineView: TextView
|
||||
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
|
||||
private lateinit var fab: FloatingActionButton
|
||||
@@ -113,7 +101,6 @@ class DecodeActivity : AbstractBaseActivity() {
|
||||
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
|
||||
@@ -136,6 +123,9 @@ class DecodeActivity : AbstractBaseActivity() {
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
if (intent.getBooleanExtra(SHOW_WHEN_LOCKED, false)) {
|
||||
showWhenLocked()
|
||||
}
|
||||
setScreenContentView(R.layout.activity_decode)
|
||||
setTitle(R.string.content)
|
||||
|
||||
@@ -150,18 +140,18 @@ class DecodeActivity : AbstractBaseActivity() {
|
||||
format = scan.format.name
|
||||
recreationSize = (200f * dp).roundToInt()
|
||||
|
||||
labelHeadlineView = findViewById(R.id.label_headline)
|
||||
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)
|
||||
metaHeadlineView = findViewById(R.id.meta_headline)
|
||||
metaView = findViewById(R.id.meta)
|
||||
hexHeadlineView = findViewById(R.id.hex_headline)
|
||||
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)
|
||||
@@ -190,6 +180,7 @@ class DecodeActivity : AbstractBaseActivity() {
|
||||
} else {
|
||||
formatHeadlineView.visibility = View.GONE
|
||||
formatDescriptionView.visibility = View.GONE
|
||||
metaHeadlineView.visibility = View.GONE
|
||||
metaView.visibility = View.GONE
|
||||
}
|
||||
|
||||
@@ -204,7 +195,7 @@ class DecodeActivity : AbstractBaseActivity() {
|
||||
labelView.visibility = View.GONE
|
||||
}
|
||||
|
||||
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets(bottom = false)
|
||||
findViewById<View>(R.id.scroll_view).setPaddingFromWindowInsets()
|
||||
|
||||
initContentAndFab(justScanned)
|
||||
@@ -221,7 +212,7 @@ class DecodeActivity : AbstractBaseActivity() {
|
||||
label = newLabel
|
||||
}
|
||||
}
|
||||
if (action.fired) {
|
||||
if (action.executed) {
|
||||
maybeBackOrFinish()
|
||||
}
|
||||
}
|
||||
@@ -300,7 +291,6 @@ class DecodeActivity : AbstractBaseActivity() {
|
||||
|
||||
private fun updateViews(text: String, bytes: ByteArray) {
|
||||
dataView.fillDataView(text, bytes, text != scan.text)
|
||||
stampView.setTrackingLink(bytes, format)
|
||||
characterCountView.text = resources.getQuantityString(
|
||||
R.plurals.character_count,
|
||||
bytes.size,
|
||||
@@ -350,6 +340,9 @@ class DecodeActivity : AbstractBaseActivity() {
|
||||
hexView.showIf(prefs.showHexDump) { v ->
|
||||
hexHeadlineView.visibility = View.VISIBLE
|
||||
v.text = hexDump(bytes)
|
||||
v.setOnClickListener {
|
||||
copyToClipboard(bytes.toHexString())
|
||||
}
|
||||
}
|
||||
if (!prefs.showHexDump) {
|
||||
hexHeadlineView.visibility = View.GONE
|
||||
@@ -369,111 +362,33 @@ class DecodeActivity : AbstractBaseActivity() {
|
||||
isEditing: Boolean
|
||||
) {
|
||||
val items = mutableListOf<Field>()
|
||||
val parsedData = parseData(this@DecodeActivity, text, bytes, format, action)
|
||||
items.addAll(parsedData.fields)
|
||||
when (prefs.showChecksum) {
|
||||
"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()))
|
||||
"CRC4" -> items.addField(R.string.crc4, String.format("%X", crc4(bytes)))
|
||||
"MD5" -> items.addField(R.string.md5, bytes.md5().toHexString().fold())
|
||||
"SHA1" -> items.addField(R.string.sha1, bytes.sha1().toHexString().fold())
|
||||
"SHA256" -> items.addField(R.string.sha256, bytes.sha256().toHexString().fold())
|
||||
}
|
||||
val count = items.count()
|
||||
val parsedDataTitleResId = parseData(items, text, bytes)
|
||||
if (items.count() > count) {
|
||||
if (parsedData.type != ParsedContentType.UNKNOWN) {
|
||||
items.add(
|
||||
0,
|
||||
Field(
|
||||
R.string.parsed_type,
|
||||
getString(parsedData.type.resId)
|
||||
)
|
||||
)
|
||||
lastParsedDataItems = items
|
||||
lastParsedDataTitleResId = parsedDataTitleResId
|
||||
.takeIf { it != 0 } ?: R.string.section_parsed_data
|
||||
fillDataItems(items, false, lastParsedDataTitleResId)
|
||||
fillDataItems(items, false)
|
||||
return
|
||||
}
|
||||
if (isEditing && lastParsedDataItems.isNotEmpty()) {
|
||||
if (!showingDimmed) {
|
||||
fillDataItems(lastParsedDataItems, true, lastParsedDataTitleResId)
|
||||
fillDataItems(lastParsedDataItems, true)
|
||||
}
|
||||
return
|
||||
}
|
||||
val titleResId = if (items.isNotEmpty()) {
|
||||
R.string.section_parsed_data
|
||||
} else {
|
||||
0
|
||||
}
|
||||
fillDataItems(items, false, titleResId)
|
||||
}
|
||||
|
||||
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.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 -> 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 { 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 {
|
||||
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 WifiAction -> WifiConnector.parseMap(text)?.let { wifiData ->
|
||||
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
|
||||
}
|
||||
}
|
||||
return 0
|
||||
fillDataItems(items, false)
|
||||
}
|
||||
|
||||
private fun TableLayout.fillMetaView(scan: Scan) {
|
||||
@@ -497,14 +412,18 @@ class DecodeActivity : AbstractBaseActivity() {
|
||||
} else {
|
||||
scan.version
|
||||
}
|
||||
items.add(Field(R.string.barcode_version_number, versionString))
|
||||
items.addField(R.string.barcode_version_number, versionString)
|
||||
}
|
||||
if (scan.format == ZxingCpp.BarcodeFormat.QRCode &&
|
||||
scan.dataMask > -1
|
||||
) {
|
||||
items.add(Field(R.string.qr_data_mask, scan.dataMask.toString()))
|
||||
items.addField(R.string.qr_data_mask, scan.dataMask.toString())
|
||||
}
|
||||
val itemsWithValues = items.filter { !it.value.isNullOrBlank() }
|
||||
if (!itemsWithValues.isEmpty()) {
|
||||
metaHeadlineView.visibility = View.VISIBLE
|
||||
fillTable(itemsWithValues)
|
||||
}
|
||||
fillTable(items)
|
||||
}
|
||||
|
||||
private fun TableLayout.fillTable(items: List<Field>) {
|
||||
@@ -514,26 +433,24 @@ class DecodeActivity : AbstractBaseActivity() {
|
||||
val spaceBetween = (16f * dp).roundToInt()
|
||||
items.forEach { item ->
|
||||
val text = item.value
|
||||
if (!text.isNullOrBlank()) {
|
||||
val tr = TableRow(ctx)
|
||||
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 tr = TableRow(ctx)
|
||||
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)
|
||||
this.text = text
|
||||
setOnClickListener {
|
||||
copyToClipboard(text.toString())
|
||||
}
|
||||
}
|
||||
tr.addView(keyView)
|
||||
tr.addView(valueView)
|
||||
addView(tr)
|
||||
}
|
||||
val valueView = TextView(ctx).apply {
|
||||
setPadding(spaceBetween, 0, 0, 0)
|
||||
this.text = text
|
||||
setOnClickListener {
|
||||
copyToClipboard(text.toString())
|
||||
}
|
||||
}
|
||||
tr.addView(keyView)
|
||||
tr.addView(valueView)
|
||||
addView(tr)
|
||||
}
|
||||
View.VISIBLE
|
||||
}
|
||||
@@ -541,8 +458,7 @@ class DecodeActivity : AbstractBaseActivity() {
|
||||
|
||||
private fun LinearLayout.fillDataItems(
|
||||
items: List<Field>,
|
||||
dimmed: Boolean,
|
||||
titleResId: Int
|
||||
dimmed: Boolean
|
||||
) {
|
||||
showingDimmed = dimmed
|
||||
removeAllViews()
|
||||
@@ -551,12 +467,7 @@ class DecodeActivity : AbstractBaseActivity() {
|
||||
visibility = View.GONE
|
||||
return
|
||||
}
|
||||
if (titleResId != 0) {
|
||||
dataHeadlineView.setText(titleResId)
|
||||
dataHeadlineView.visibility = View.VISIBLE
|
||||
} else {
|
||||
dataHeadlineView.visibility = View.GONE
|
||||
}
|
||||
dataHeadlineView.visibility = View.VISIBLE
|
||||
val ctx = context
|
||||
val spaceBetween = (16f * dp).roundToInt()
|
||||
for (item in items) {
|
||||
@@ -575,6 +486,9 @@ class DecodeActivity : AbstractBaseActivity() {
|
||||
}
|
||||
setBackgroundResource(R.drawable.list_selector)
|
||||
setOnClickListener {
|
||||
item.link?.let {
|
||||
openUrl(it)
|
||||
}
|
||||
copyToClipboard(text.toString())
|
||||
}
|
||||
}
|
||||
@@ -610,6 +524,15 @@ class DecodeActivity : AbstractBaseActivity() {
|
||||
)
|
||||
)
|
||||
this.text = text
|
||||
if (item.link != null) {
|
||||
setCompoundDrawablesWithIntrinsicBounds(
|
||||
0,
|
||||
0,
|
||||
R.drawable.ic_action_open,
|
||||
0
|
||||
)
|
||||
compoundDrawablePadding = spaceBetween / 2
|
||||
}
|
||||
}
|
||||
|
||||
val alpha = if (dimmed) 0.5f else 1f
|
||||
@@ -691,7 +614,7 @@ class DecodeActivity : AbstractBaseActivity() {
|
||||
}
|
||||
|
||||
private fun askToRemoveScan(id: Long) {
|
||||
AlertDialog.Builder(this)
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.really_remove_scan)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
db.removeScan(id)
|
||||
@@ -815,19 +738,31 @@ class DecodeActivity : AbstractBaseActivity() {
|
||||
private const val SCAN_BUNDLE = "scan_bundle"
|
||||
private const val OPEN_DOCUMENT = 1
|
||||
private const val SCHEME_FILE = "file://"
|
||||
private const val SHOW_WHEN_LOCKED = "show_when_locked"
|
||||
|
||||
fun newIntent(context: Context, scan: Scan): Intent {
|
||||
fun newIntent(
|
||||
context: Context,
|
||||
scan: Scan,
|
||||
showWhenLocked: Boolean = false
|
||||
): Intent {
|
||||
val args = Bundle()
|
||||
args.putBundle(SCAN_BUNDLE, scan.toBundle())
|
||||
return Intent(context, DecodeActivity::class.java).apply {
|
||||
putExtra(DECODED_SCAN, true)
|
||||
putExtra(SHOW_WHEN_LOCKED, showWhenLocked)
|
||||
putExtras(args)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class Field(val key: Any, val value: CharSequence?)
|
||||
private fun MutableList<Field>.addField(
|
||||
key: Any,
|
||||
value: CharSequence?,
|
||||
link: String? = null
|
||||
) {
|
||||
add(Field(key, value, link))
|
||||
}
|
||||
|
||||
private inline fun <T : View> T.showIf(
|
||||
visible: Boolean,
|
||||
@@ -887,61 +822,6 @@ private fun hexDump(bytes: ByteArray, charsPerLine: Int = 33): String {
|
||||
|
||||
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()
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateDpTrackingLink(bytes: ByteArray, format: String): String? {
|
||||
// Check for Deutsche Post Matrixcode stamp.
|
||||
var isStamp = false
|
||||
var rawData = bytes
|
||||
if (format == ZxingCpp.BarcodeFormat.DataMatrix.name &&
|
||||
bytes.toString(Charsets.ISO_8859_1).startsWith("DEA5")
|
||||
) {
|
||||
if (bytes.size == 47) {
|
||||
isStamp = true
|
||||
} else if (bytes.size > 47) {
|
||||
// Transform back to original data.
|
||||
rawData = bytes.toString(Charsets.UTF_8).toByteArray(
|
||||
Charsets.ISO_8859_1
|
||||
)
|
||||
if (rawData.size == 47) {
|
||||
isStamp = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isStamp) {
|
||||
return null
|
||||
}
|
||||
|
||||
val hex = StringBuilder()
|
||||
hex.append(String.format("%02X", rawData[9]))
|
||||
hex.append(String.format("%02X", rawData[10]))
|
||||
hex.append(String.format("%02X", rawData[11]))
|
||||
hex.append(String.format("%02X", rawData[12]))
|
||||
hex.append(String.format("%02X", rawData[13]))
|
||||
hex.append(String.format("%X", (rawData[4].toInt() and 0x0f).toByte()))
|
||||
hex.append(String.format("%02X", rawData[5]))
|
||||
hex.append(String.format("%02X", rawData[6]))
|
||||
hex.append(String.format("%02X", rawData[7]))
|
||||
hex.append(String.format("%02X", rawData[8]))
|
||||
val hexString = hex.toString()
|
||||
val trackingNumber = hexString + String.format(
|
||||
"%X",
|
||||
crc4(hexString.toByteArray(Charsets.ISO_8859_1))
|
||||
)
|
||||
return "<a href=\"https://www.deutschepost.de/de/s/sendungsverfolgung.html?piececode=$trackingNumber\">Deutsche Post: $trackingNumber</a>"
|
||||
}
|
||||
|
||||
// CRC-4 with polynomial x^4 + x + 1.
|
||||
private fun crc4(input: ByteArray): Int {
|
||||
var crc = 0
|
||||
@@ -966,15 +846,6 @@ private fun crc4(input: ByteArray): Int {
|
||||
return crc
|
||||
}
|
||||
|
||||
private fun String.fromHtml() = if (
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
|
||||
) {
|
||||
Html.fromHtml(this, Html.FROM_HTML_MODE_LEGACY)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
Html.fromHtml(this)
|
||||
}
|
||||
|
||||
private fun ByteArray.md5(): ByteArray = MessageDigest.getInstance(
|
||||
"MD5"
|
||||
).run {
|
||||
|
||||
@@ -187,7 +187,7 @@ class EncodeActivity : AbstractBaseActivity() {
|
||||
it.context.encode()
|
||||
}
|
||||
|
||||
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets(bottom = false)
|
||||
findViewById<View>(R.id.scroll_view).setPaddingFromWindowInsets()
|
||||
}
|
||||
|
||||
@@ -228,7 +228,10 @@ class EncodeActivity : AbstractBaseActivity() {
|
||||
}
|
||||
|
||||
private fun applyIntent(applyDefaultFormat: Boolean) {
|
||||
val args = intent?.extras ?: getExtrasFromDeeplink()
|
||||
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 ||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
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
|
||||
@@ -18,7 +18,8 @@ import android.widget.ListView
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.appcompat.widget.SwitchCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
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.alertDialog
|
||||
@@ -69,9 +70,10 @@ class HistoryActivity : AbstractBaseActivity() {
|
||||
lockStatusBarColor()
|
||||
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
|
||||
@@ -174,7 +176,7 @@ class HistoryActivity : AbstractBaseActivity() {
|
||||
|
||||
progressView = findViewById(R.id.progress_view)
|
||||
|
||||
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets(bottom = false)
|
||||
listView.setPaddingFromWindowInsets()
|
||||
}
|
||||
|
||||
@@ -342,7 +344,7 @@ class HistoryActivity : AbstractBaseActivity() {
|
||||
)
|
||||
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)
|
||||
@@ -361,7 +363,7 @@ class HistoryActivity : AbstractBaseActivity() {
|
||||
}
|
||||
|
||||
private fun Context.askToRemoveScans(ids: List<Long>) {
|
||||
AlertDialog.Builder(this)
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(
|
||||
if (ids.size > 1) {
|
||||
R.string.really_remove_selected_scans
|
||||
@@ -383,7 +385,7 @@ class HistoryActivity : AbstractBaseActivity() {
|
||||
}
|
||||
|
||||
private fun Context.askToRemoveScans() {
|
||||
AlertDialog.Builder(this)
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(
|
||||
if (filter == null) {
|
||||
R.string.really_remove_all_scans
|
||||
@@ -448,7 +450,7 @@ class HistoryActivity : AbstractBaseActivity() {
|
||||
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)
|
||||
|
||||
+5
-4
@@ -1,6 +1,5 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
@@ -10,6 +9,8 @@ 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
|
||||
@@ -54,7 +55,7 @@ class IgnoreCodesActivity : AbstractBaseActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets(bottom = false)
|
||||
listView.setPaddingFromWindowInsets()
|
||||
}
|
||||
|
||||
@@ -74,7 +75,7 @@ class IgnoreCodesActivity : AbstractBaseActivity() {
|
||||
formatView.setSelection(0)
|
||||
}
|
||||
|
||||
val dialog = AlertDialog.Builder(this)
|
||||
val dialog = MaterialAlertDialogBuilder(this)
|
||||
.setTitle(
|
||||
if (pattern == null) {
|
||||
R.string.ignore_code_add
|
||||
@@ -133,7 +134,7 @@ class IgnoreCodesActivity : AbstractBaseActivity() {
|
||||
}
|
||||
|
||||
private fun confirmRemovePattern(position: Int) {
|
||||
AlertDialog.Builder(this)
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.ignore_code_remove_confirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
patterns.removeAt(position)
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ class NetworkSuggestionsActivity : AbstractBaseActivity() {
|
||||
listView.emptyView = findViewById(R.id.no_suggestions)
|
||||
listView.adapter = suggestionArrayAdapter
|
||||
listView.setOnScrollListener(systemBarListViewScrollListener)
|
||||
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets(bottom = false)
|
||||
listView.setPaddingFromWindowInsets()
|
||||
|
||||
findViewById<View>(R.id.remove).setOnClickListener {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
@@ -9,11 +8,13 @@ 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 ProfilesActivity : AbstractBaseActivity() {
|
||||
private val profiles = ArrayList<ProfileItem>()
|
||||
@@ -44,7 +45,7 @@ class ProfilesActivity : AbstractBaseActivity() {
|
||||
addProfile()
|
||||
}
|
||||
|
||||
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets()
|
||||
findViewById<View>(R.id.inset_layout).setPaddingFromWindowInsets(bottom = false)
|
||||
listView.setPaddingFromWindowInsets()
|
||||
}
|
||||
|
||||
@@ -53,11 +54,11 @@ class ProfilesActivity : AbstractBaseActivity() {
|
||||
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()
|
||||
@@ -80,7 +81,7 @@ class ProfilesActivity : AbstractBaseActivity() {
|
||||
R.layout.dialog_profile_name, null
|
||||
)
|
||||
val editText = view.findViewById<EditText>(R.id.name)
|
||||
AlertDialog.Builder(this)
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val name = editText.text.toString().trim()
|
||||
@@ -97,7 +98,7 @@ class ProfilesActivity : AbstractBaseActivity() {
|
||||
private fun confirmRemoveProfile(position: Int) {
|
||||
val profile = profiles[position]
|
||||
val profileName = profile.name ?: return
|
||||
AlertDialog.Builder(this)
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.profile_remove_confirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val isCurrent = prefs.profile == profileName
|
||||
@@ -116,8 +117,8 @@ class ProfilesActivity : AbstractBaseActivity() {
|
||||
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(
|
||||
@@ -129,6 +130,7 @@ class ProfilesActivity : AbstractBaseActivity() {
|
||||
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)
|
||||
@@ -144,16 +146,31 @@ class ProfilesActivity : AbstractBaseActivity() {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package de.markusfisch.android.binaryeye.activity
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
||||
class SplashActivity : AppCompatActivity() {
|
||||
class SplashActivity : Activity() {
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
|
||||
|
||||
@@ -11,22 +11,11 @@ class WelcomeActivity : AppCompatActivity() {
|
||||
super.onCreate(state)
|
||||
setContentView(R.layout.activity_welcome)
|
||||
findViewById<View>(R.id.expert).setOnClickListener {
|
||||
prefs.loadExpertProfile(this)
|
||||
finish()
|
||||
}
|
||||
findViewById<View>(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
|
||||
}
|
||||
prefs.loadSimpleProfile(this)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,10 @@ import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.content.ParsedContentType
|
||||
import de.markusfisch.android.binaryeye.content.getContentPreview
|
||||
import de.markusfisch.android.binaryeye.content.parsedContentTypeFromName
|
||||
import de.markusfisch.android.binaryeye.database.Database
|
||||
import java.text.DateFormat
|
||||
import java.text.ParseException
|
||||
@@ -30,12 +32,10 @@ class ScansAdapter(
|
||||
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 rawIndex = cursor.getColumnIndex(Database.SCANS_RAW)
|
||||
private val contentTypeIndex = cursor.getColumnIndex(Database.SCANS_CONTENT_TYPE)
|
||||
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) {
|
||||
@@ -101,15 +101,19 @@ class ScansAdapter(
|
||||
val id = cursor.getLong(idIndex)
|
||||
val pinned = cursor.getInt(pinnedIndex) != 0
|
||||
val format = cursor.getString(formatIndex)
|
||||
val contentType = parsedContentTypeFromName(
|
||||
cursor.getString(contentTypeIndex)
|
||||
)
|
||||
getViewHolder(view).apply {
|
||||
metaView.text = context.getString(
|
||||
R.string.scan_meta_data,
|
||||
formatDateTime(cursor.getString(timeIndex)),
|
||||
format.prettifyFormatName()
|
||||
metaView.text = formatDateTime(cursor.getString(timeIndex))
|
||||
formatView.text = format.prettifyFormatName().withContentType(
|
||||
context,
|
||||
contentType
|
||||
)
|
||||
formatIconView.setImageResource(format.toBarcodeIcon())
|
||||
val name = cursor.getString(nameIndex)
|
||||
val text = cursor.getString(textIndex)
|
||||
val raw = cursor.getBlob(rawIndex)
|
||||
var icon = 0
|
||||
contentView.text = when {
|
||||
name?.isNotEmpty() == true -> {
|
||||
@@ -117,8 +121,11 @@ class ScansAdapter(
|
||||
name
|
||||
}
|
||||
|
||||
text.isNullOrEmpty() -> context.getString(
|
||||
R.string.binary_data
|
||||
text.isNullOrEmpty() -> getContentPreview(
|
||||
context,
|
||||
text ?: "",
|
||||
raw ?: ByteArray(0),
|
||||
format
|
||||
)
|
||||
|
||||
else -> text
|
||||
@@ -154,7 +161,8 @@ class ScansAdapter(
|
||||
}
|
||||
|
||||
private fun View.select(selected: Boolean) {
|
||||
setBackgroundColor(if (selected) selectedColor else 0)
|
||||
isActivated = selected
|
||||
refreshDrawableState()
|
||||
}
|
||||
|
||||
private fun getViewHolder(
|
||||
@@ -162,6 +170,7 @@ class ScansAdapter(
|
||||
): ViewHolder = view.tag as ViewHolder? ?: ViewHolder(
|
||||
view.findViewById(R.id.format_icon),
|
||||
view.findViewById(R.id.meta),
|
||||
view.findViewById(R.id.format),
|
||||
view.findViewById(R.id.content),
|
||||
view.findViewById(R.id.pin)
|
||||
).also {
|
||||
@@ -171,6 +180,7 @@ class ScansAdapter(
|
||||
private data class ViewHolder(
|
||||
val formatIconView: ImageView,
|
||||
val metaView: TextView,
|
||||
val formatView: TextView,
|
||||
val contentView: TextView,
|
||||
val pinView: ImageButton
|
||||
)
|
||||
@@ -205,6 +215,19 @@ fun Context.setupFormatSpinner(spinner: Spinner): List<String> {
|
||||
fun String.prettifyFormatName() = replace("_", " ")
|
||||
.replace(formatWordBoundary, " ")
|
||||
|
||||
private fun String.withContentType(
|
||||
context: Context,
|
||||
type: ParsedContentType
|
||||
) = if (type == ParsedContentType.UNKNOWN) {
|
||||
this
|
||||
} else {
|
||||
context.getString(
|
||||
R.string.scan_meta_data,
|
||||
this,
|
||||
context.getString(type.resId)
|
||||
)
|
||||
}
|
||||
|
||||
fun String.toFormatDescriptionResId(): Int = when (this) {
|
||||
"Aztec" -> R.string.format_description_aztec
|
||||
"Codabar" -> R.string.format_description_codabar
|
||||
@@ -229,6 +252,9 @@ fun String.toFormatDescriptionResId(): Int = when (this) {
|
||||
"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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
@@ -55,5 +55,5 @@ private fun Context.execCustomIntent(
|
||||
toast(R.string.automated_action_intent_uri_invalid)
|
||||
return false
|
||||
}
|
||||
return execShareIntent(intent)
|
||||
return startIntentOrToast(intent)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package de.markusfisch.android.binaryeye.camera
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.hardware.camera2.CameraCharacteristics
|
||||
import android.hardware.camera2.CameraManager
|
||||
import androidx.camera.core.CameraSelector
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
|
||||
const val DEFAULT_CAMERA_BACK = "back"
|
||||
const val DEFAULT_CAMERA_FRONT = "front"
|
||||
const val DEFAULT_CAMERA_OTHER = "other"
|
||||
|
||||
fun availableDefaultCameras(context: Context): List<String> {
|
||||
val lensFacings = try {
|
||||
(context.getSystemService(Context.CAMERA_SERVICE) as CameraManager)
|
||||
.run {
|
||||
cameraIdList.mapNotNull { id ->
|
||||
getCameraCharacteristics(id)
|
||||
.get(CameraCharacteristics.LENS_FACING)
|
||||
}
|
||||
}.toSet()
|
||||
} catch (_: Exception) {
|
||||
emptySet()
|
||||
}
|
||||
return buildList {
|
||||
if (lensFacings.isEmpty() ||
|
||||
lensFacings.contains(CameraCharacteristics.LENS_FACING_BACK)
|
||||
) {
|
||||
add(DEFAULT_CAMERA_BACK)
|
||||
}
|
||||
if (lensFacings.contains(CameraCharacteristics.LENS_FACING_FRONT)) {
|
||||
add(DEFAULT_CAMERA_FRONT)
|
||||
}
|
||||
if (lensFacings.contains(CameraCharacteristics.LENS_FACING_EXTERNAL)) {
|
||||
add(DEFAULT_CAMERA_OTHER)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun defaultCameraEntries(context: Context, values: List<String>) =
|
||||
values.map {
|
||||
when (it) {
|
||||
DEFAULT_CAMERA_FRONT -> context.getString(R.string.default_camera_front)
|
||||
DEFAULT_CAMERA_OTHER -> context.getString(R.string.default_camera_other)
|
||||
else -> context.getString(R.string.default_camera_back)
|
||||
}
|
||||
}.toTypedArray()
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
fun cameraSelector(camera: String) = CameraSelector.Builder()
|
||||
.requireLensFacing(
|
||||
when (camera) {
|
||||
DEFAULT_CAMERA_FRONT -> CameraSelector.LENS_FACING_FRONT
|
||||
DEFAULT_CAMERA_OTHER -> CameraSelector.LENS_FACING_EXTERNAL
|
||||
else -> CameraSelector.LENS_FACING_BACK
|
||||
}
|
||||
)
|
||||
.build()
|
||||
@@ -208,6 +208,9 @@ private fun BarcodeFormat.resolveSize(size: Int): Size = when (this) {
|
||||
BarcodeFormat.EAN8,
|
||||
BarcodeFormat.EAN13,
|
||||
BarcodeFormat.ITF,
|
||||
BarcodeFormat.Telepen,
|
||||
BarcodeFormat.TelepenAlpha,
|
||||
BarcodeFormat.TelepenNumeric,
|
||||
BarcodeFormat.UPCA,
|
||||
BarcodeFormat.UPCE -> Size(size, size / 3)
|
||||
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
package de.markusfisch.android.binaryeye.content
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.Action
|
||||
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 org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
enum class ParsedContentType(
|
||||
val resId: Int
|
||||
) {
|
||||
UNKNOWN(0),
|
||||
DEUTSCHE_POST(R.string.parsed_type_deutsche_post),
|
||||
JSON(R.string.parsed_type_json),
|
||||
INTERNATIONAL_DRIVER_LICENSE(
|
||||
R.string.parsed_type_international_driver_license
|
||||
),
|
||||
DIGITAL_SEAL(R.string.parsed_type_digital_seal),
|
||||
SEPA_EPC_QR(R.string.parsed_type_sepa_epc_qr),
|
||||
EMAIL(R.string.parsed_type_email),
|
||||
URL(R.string.parsed_type_url),
|
||||
OTP(R.string.parsed_type_otp),
|
||||
CONTACT_CARD(R.string.parsed_type_contact_card),
|
||||
CALENDAR_EVENT(R.string.parsed_type_calendar_event),
|
||||
WIFI_NETWORK(R.string.parsed_type_wifi_network);
|
||||
}
|
||||
|
||||
data class ParsedData(
|
||||
val type: ParsedContentType,
|
||||
val fields: List<ParsedField>
|
||||
)
|
||||
|
||||
data class ParsedField(
|
||||
val key: Any,
|
||||
val value: CharSequence?,
|
||||
val link: String? = null
|
||||
)
|
||||
|
||||
fun parseData(
|
||||
context: Context,
|
||||
text: String,
|
||||
bytes: ByteArray,
|
||||
format: String,
|
||||
action: Action = ActionRegistry.getAction(bytes)
|
||||
): ParsedData {
|
||||
val fields = mutableListOf<ParsedField>()
|
||||
|
||||
DeutschePostParser.parse(bytes, format)?.let {
|
||||
fields.addField(
|
||||
context.getString(R.string.tracking_number),
|
||||
it.label,
|
||||
it.link,
|
||||
)
|
||||
return ParsedData(ParsedContentType.DEUTSCHE_POST, fields)
|
||||
}
|
||||
|
||||
try {
|
||||
fields.addField(R.string.formatted_json, JSONObject(text).toString(2))
|
||||
return ParsedData(ParsedContentType.JSON, fields)
|
||||
} catch (_: JSONException) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
IdlParser.parse(String(bytes))?.let {
|
||||
fields.addField("IIN", it.iin)
|
||||
it.elements.forEach { (id, value) ->
|
||||
fields.addField(context.idlToRes(id), value)
|
||||
}
|
||||
return ParsedData(ParsedContentType.INTERNATIONAL_DRIVER_LICENSE, fields)
|
||||
}
|
||||
|
||||
SealParser.parse(context, bytes)?.let { sealFields ->
|
||||
sealFields.forEach { vf ->
|
||||
fields.addField(vf.name, vf.value)
|
||||
}
|
||||
return ParsedData(ParsedContentType.DIGITAL_SEAL, fields)
|
||||
}
|
||||
|
||||
EpcQrParser.parse(text)?.let {
|
||||
fields.addAll(it.map { (id, value) ->
|
||||
ParsedField(context.epcQrToRes(id), value)
|
||||
})
|
||||
return ParsedData(ParsedContentType.SEPA_EPC_QR, fields)
|
||||
}
|
||||
|
||||
when (action) {
|
||||
is MatMsgAction -> MatMsg(text).run {
|
||||
fields.addField(R.string.email_to, to)
|
||||
fields.addField(R.string.email_subject, sub)
|
||||
fields.addField(R.string.email_body, body)
|
||||
return ParsedData(ParsedContentType.EMAIL, fields)
|
||||
}
|
||||
|
||||
is VCardAction,
|
||||
is VEventAction -> VTypeParser.parseMap(text).let { parsedMap ->
|
||||
parsedMap.forEach { item ->
|
||||
fields.addField(item.key, item.value.joinToString("\n") { it.value })
|
||||
}
|
||||
return ParsedData(
|
||||
if (action is VCardAction) {
|
||||
ParsedContentType.CONTACT_CARD
|
||||
} else {
|
||||
ParsedContentType.CALENDAR_EVENT
|
||||
},
|
||||
fields
|
||||
)
|
||||
}
|
||||
|
||||
is WebAction -> try {
|
||||
text.toUri().run {
|
||||
fields.addField(R.string.scheme, scheme)
|
||||
fields.addField(R.string.host, host)
|
||||
fields.addField(R.string.query, query)
|
||||
}
|
||||
return ParsedData(ParsedContentType.URL, fields)
|
||||
} 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
|
||||
fields.addField(R.string.entry_type, host?.uppercase())
|
||||
fields.addField(R.string.otp_account, account)
|
||||
fields.addField(R.string.otp_issuer, issuer)
|
||||
fields.addField(R.string.otp_algorithm, getQueryParameter("algorithm"))
|
||||
fields.addField(R.string.otp_digits, getQueryParameter("digits"))
|
||||
if (host == "totp") {
|
||||
fields.addField(R.string.otp_period, getQueryParameter("period"))
|
||||
} else {
|
||||
fields.addField(R.string.otp_counter, getQueryParameter("counter"))
|
||||
}
|
||||
}
|
||||
return ParsedData(ParsedContentType.OTP, fields)
|
||||
} catch (_: Exception) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
is WifiAction -> WifiConnector.parseMap(text)?.let { wifiData ->
|
||||
fields.addField(R.string.wifi_ssid, wifiData["S"])
|
||||
fields.addField(R.string.wifi_password, wifiData["P"])
|
||||
fields.addField(R.string.wifi_type, wifiData["T"])
|
||||
fields.addField(R.string.wifi_hidden, wifiData["H"])
|
||||
fields.addField(R.string.wifi_eap, wifiData["E"])
|
||||
fields.addField(R.string.wifi_identity, wifiData["I"])
|
||||
fields.addField(R.string.wifi_anonymous_identity, wifiData["A"])
|
||||
fields.addField(R.string.wifi_phase2, wifiData["PH2"])
|
||||
return ParsedData(ParsedContentType.WIFI_NETWORK, fields)
|
||||
}
|
||||
}
|
||||
return ParsedData(ParsedContentType.UNKNOWN, emptyList())
|
||||
}
|
||||
|
||||
fun parsedContentTypeFromName(
|
||||
name: String?
|
||||
): ParsedContentType = try {
|
||||
ParsedContentType.valueOf(name ?: "")
|
||||
} catch (_: IllegalArgumentException) {
|
||||
ParsedContentType.UNKNOWN
|
||||
}
|
||||
|
||||
fun getContentPreview(
|
||||
context: Context,
|
||||
text: String,
|
||||
bytes: ByteArray,
|
||||
format: String,
|
||||
action: Action = ActionRegistry.getAction(bytes)
|
||||
): CharSequence {
|
||||
val parsedData = parseData(context, text, bytes, format, action)
|
||||
return when (parsedData.type) {
|
||||
ParsedContentType.UNKNOWN -> text.ifEmpty {
|
||||
bytes.toHexString()
|
||||
}
|
||||
|
||||
ParsedContentType.URL -> parsedData.valueFor(R.string.host)
|
||||
?: text
|
||||
|
||||
ParsedContentType.WIFI_NETWORK -> parsedData.valueFor(R.string.wifi_ssid)
|
||||
?: parsedData.firstValue()
|
||||
?: text
|
||||
|
||||
ParsedContentType.EMAIL -> parsedData.valueFor(R.string.email_to)
|
||||
?: parsedData.valueFor(R.string.email_subject)
|
||||
?: parsedData.firstValue()
|
||||
?: text
|
||||
|
||||
ParsedContentType.OTP -> parsedData.valueFor(R.string.otp_account)
|
||||
?: parsedData.firstValue()
|
||||
?: text
|
||||
|
||||
else -> parsedData.firstValue() ?: text
|
||||
}
|
||||
}
|
||||
|
||||
private fun ParsedData.valueFor(key: Int): CharSequence? =
|
||||
fields.firstValue { it.key == key }
|
||||
|
||||
private fun ParsedData.firstValue(): CharSequence? =
|
||||
fields.firstValue()
|
||||
|
||||
private fun List<ParsedField>.firstValue(
|
||||
predicate: (ParsedField) -> Boolean = { true }
|
||||
): CharSequence? = firstOrNull {
|
||||
predicate(it) && !it.value.isNullOrBlank()
|
||||
}?.value
|
||||
|
||||
private fun MutableList<ParsedField>.addField(
|
||||
key: Any,
|
||||
value: CharSequence?,
|
||||
link: String? = null
|
||||
) {
|
||||
add(ParsedField(key, value, link))
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package de.markusfisch.android.binaryeye.content
|
||||
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp
|
||||
|
||||
data class DeutschePostTrackingLink(
|
||||
val label: String,
|
||||
val link: String
|
||||
)
|
||||
|
||||
object DeutschePostParser {
|
||||
fun parse(
|
||||
bytes: ByteArray,
|
||||
format: String
|
||||
): DeutschePostTrackingLink? {
|
||||
var isStamp = false
|
||||
var rawData = bytes
|
||||
if (format == ZxingCpp.BarcodeFormat.DataMatrix.name &&
|
||||
bytes.toString(Charsets.ISO_8859_1).startsWith("DEA5")
|
||||
) {
|
||||
if (bytes.size == 47) {
|
||||
isStamp = true
|
||||
} else if (bytes.size > 47) {
|
||||
// Transform back to original data.
|
||||
rawData = bytes.toString(Charsets.UTF_8).toByteArray(
|
||||
Charsets.ISO_8859_1
|
||||
)
|
||||
if (rawData.size == 47) {
|
||||
isStamp = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isStamp) {
|
||||
return null
|
||||
}
|
||||
|
||||
val hex = StringBuilder()
|
||||
hex.append(String.format("%02X", rawData[9]))
|
||||
hex.append(String.format("%02X", rawData[10]))
|
||||
hex.append(String.format("%02X", rawData[11]))
|
||||
hex.append(String.format("%02X", rawData[12]))
|
||||
hex.append(String.format("%02X", rawData[13]))
|
||||
hex.append(String.format("%X", (rawData[4].toInt() and 0x0f).toByte()))
|
||||
hex.append(String.format("%02X", rawData[5]))
|
||||
hex.append(String.format("%02X", rawData[6]))
|
||||
hex.append(String.format("%02X", rawData[7]))
|
||||
hex.append(String.format("%02X", rawData[8]))
|
||||
val hexString = hex.toString()
|
||||
val trackingNumber = hexString + String.format(
|
||||
"%X",
|
||||
crc4(hexString.toByteArray(Charsets.ISO_8859_1))
|
||||
)
|
||||
return DeutschePostTrackingLink(
|
||||
trackingNumber,
|
||||
"https://www.deutschepost.de/de/s/sendungsverfolgung.html?piececode=$trackingNumber",
|
||||
)
|
||||
}
|
||||
|
||||
// CRC-4 with polynomial x^4 + x + 1.
|
||||
private fun crc4(input: ByteArray): Int {
|
||||
var crc = 0
|
||||
var i = 0
|
||||
while (i < input.size) {
|
||||
val c = input[i].toInt()
|
||||
var j = 0x80
|
||||
while (j != 0) {
|
||||
var bit = crc and 0x8
|
||||
crc = crc shl 1
|
||||
if (c and j != 0) {
|
||||
bit = bit xor 0x8
|
||||
}
|
||||
if (bit != 0) {
|
||||
crc = crc xor 0x3
|
||||
}
|
||||
j = j ushr 1
|
||||
}
|
||||
++i
|
||||
}
|
||||
crc = crc and 0xF
|
||||
return crc
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,13 @@ 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)
|
||||
@@ -36,6 +37,8 @@ fun Context.startIntent(intent: Intent): Boolean = try {
|
||||
true
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
false
|
||||
} catch (_: SecurityException) {
|
||||
false
|
||||
}
|
||||
|
||||
fun Context.openUrl(url: String, silent: Boolean = false): Boolean {
|
||||
@@ -43,24 +46,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 = toUri().let {
|
||||
it.normalizeScheme()
|
||||
}
|
||||
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
|
||||
})
|
||||
@@ -123,7 +129,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()
|
||||
}
|
||||
@@ -6,6 +6,9 @@ import android.database.Cursor
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.content.ParsedContentType
|
||||
import de.markusfisch.android.binaryeye.content.parseData
|
||||
import de.markusfisch.android.binaryeye.content.parsedContentTypeFromName
|
||||
import de.markusfisch.android.binaryeye.preference.Preferences
|
||||
import de.markusfisch.android.binaryeye.zxingcpp.migrateBarcodeFormatName
|
||||
import de.markusfisch.android.binaryeye.zxingcpp.oldToNewFormatNames
|
||||
@@ -13,9 +16,12 @@ import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||
|
||||
class Database {
|
||||
private lateinit var db: SQLiteDatabase
|
||||
private lateinit var context: Context
|
||||
|
||||
fun open(context: Context) {
|
||||
this.context = context.applicationContext
|
||||
db = OpenHelper(context).writableDatabase
|
||||
fillMissingContentTypes()
|
||||
}
|
||||
|
||||
fun getScans(query: String? = null): Cursor = db.rawQuery(
|
||||
@@ -25,6 +31,8 @@ class Database {
|
||||
$SCANS_DATETIME,
|
||||
$SCANS_NAME,
|
||||
$SCANS_TEXT,
|
||||
$SCANS_RAW,
|
||||
$SCANS_CONTENT_TYPE,
|
||||
$SCANS_FORMAT
|
||||
FROM $SCANS
|
||||
${getWhereClause(query)}
|
||||
@@ -49,7 +57,8 @@ class Database {
|
||||
$SCANS_GTIN_COUNTRY,
|
||||
$SCANS_GTIN_ADD_ON,
|
||||
$SCANS_GTIN_PRICE,
|
||||
$SCANS_GTIN_ISSUE_NUMBER
|
||||
$SCANS_GTIN_ISSUE_NUMBER,
|
||||
$SCANS_CONTENT_TYPE
|
||||
FROM $SCANS
|
||||
${getWhereClause(query)}
|
||||
ORDER BY $SCANS_PINNED DESC, $SCANS_DATETIME DESC
|
||||
@@ -93,7 +102,8 @@ class Database {
|
||||
$SCANS_GTIN_COUNTRY,
|
||||
$SCANS_GTIN_ADD_ON,
|
||||
$SCANS_GTIN_PRICE,
|
||||
$SCANS_GTIN_ISSUE_NUMBER
|
||||
$SCANS_GTIN_ISSUE_NUMBER,
|
||||
$SCANS_CONTENT_TYPE
|
||||
FROM $SCANS
|
||||
WHERE $SCANS_ID IN (${ids.joinToString(",")})
|
||||
ORDER BY $SCANS_PINNED DESC, $SCANS_DATETIME DESC
|
||||
@@ -121,7 +131,8 @@ class Database {
|
||||
$SCANS_GTIN_COUNTRY,
|
||||
$SCANS_GTIN_ADD_ON,
|
||||
$SCANS_GTIN_PRICE,
|
||||
$SCANS_GTIN_ISSUE_NUMBER
|
||||
$SCANS_GTIN_ISSUE_NUMBER,
|
||||
$SCANS_CONTENT_TYPE
|
||||
FROM $SCANS
|
||||
WHERE $SCANS_ID = ?
|
||||
""".trimMargin(), arrayOf("$id")
|
||||
@@ -151,7 +162,8 @@ class Database {
|
||||
it.getString(SCANS_DATETIME),
|
||||
it.getLong(SCANS_ID),
|
||||
it.getString(SCANS_NAME),
|
||||
it.getInt(SCANS_PINNED) != 0
|
||||
it.getInt(SCANS_PINNED) != 0,
|
||||
parsedContentTypeFromName(it.getString(SCANS_CONTENT_TYPE))
|
||||
)
|
||||
} else {
|
||||
null
|
||||
@@ -208,6 +220,10 @@ class Database {
|
||||
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)
|
||||
put(
|
||||
SCANS_CONTENT_TYPE,
|
||||
getContentType(scan.text, scan.raw, scan.format.name).name
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -279,8 +295,52 @@ class Database {
|
||||
db.update(SCANS, cv, "$SCANS_ID = ?", arrayOf("$id"))
|
||||
}
|
||||
|
||||
private fun fillMissingContentTypes() {
|
||||
Thread {
|
||||
db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID,
|
||||
$SCANS_TEXT,
|
||||
$SCANS_RAW,
|
||||
$SCANS_FORMAT
|
||||
FROM $SCANS
|
||||
WHERE $SCANS_CONTENT_TYPE IS NULL
|
||||
""".trimMargin(),
|
||||
null
|
||||
).use {
|
||||
while (it.moveToNext()) {
|
||||
val id = it.getLong(SCANS_ID)
|
||||
val type = getContentType(
|
||||
it.getString(SCANS_TEXT),
|
||||
it.getBlob(SCANS_RAW),
|
||||
it.getString(SCANS_FORMAT)
|
||||
)
|
||||
db.update(
|
||||
SCANS,
|
||||
ContentValues().apply {
|
||||
put(SCANS_CONTENT_TYPE, type.name)
|
||||
},
|
||||
"$SCANS_ID = ?",
|
||||
arrayOf("$id")
|
||||
)
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun getContentType(
|
||||
text: String,
|
||||
raw: ByteArray?,
|
||||
format: String
|
||||
) = parseData(
|
||||
context,
|
||||
text,
|
||||
raw ?: text.toByteArray(),
|
||||
format
|
||||
).type
|
||||
|
||||
private class OpenHelper(context: Context) :
|
||||
SQLiteOpenHelper(context, FILE_NAME, null, 10) {
|
||||
SQLiteOpenHelper(context, FILE_NAME, null, 11) {
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
db.createScans()
|
||||
}
|
||||
@@ -317,6 +377,9 @@ class Database {
|
||||
if (oldVersion < 10) {
|
||||
db.addPinnedColumn()
|
||||
}
|
||||
if (oldVersion < 11) {
|
||||
db.addContentTypeColumn()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,6 +414,7 @@ class Database {
|
||||
const val SCANS_GTIN_PRICE = "gtin_price"
|
||||
const val SCANS_GTIN_ISSUE_NUMBER = "gtin_issue_number"
|
||||
const val SCANS_PINNED = "pinned"
|
||||
const val SCANS_CONTENT_TYPE = "content_type"
|
||||
|
||||
private fun SQLiteDatabase.createScans() {
|
||||
execSQL("DROP TABLE IF EXISTS $SCANS".trimMargin())
|
||||
@@ -375,7 +439,8 @@ class Database {
|
||||
$SCANS_GTIN_ADD_ON TEXT,
|
||||
$SCANS_GTIN_PRICE TEXT,
|
||||
$SCANS_GTIN_ISSUE_NUMBER TEXT,
|
||||
$SCANS_PINNED INTEGER NOT NULL DEFAULT 0
|
||||
$SCANS_PINNED INTEGER NOT NULL DEFAULT 0,
|
||||
$SCANS_CONTENT_TYPE TEXT NOT NULL DEFAULT '${ParsedContentType.UNKNOWN.name}'
|
||||
)""".trimMargin()
|
||||
)
|
||||
}
|
||||
@@ -446,5 +511,9 @@ class Database {
|
||||
"ALTER TABLE $SCANS ADD COLUMN $SCANS_PINNED INTEGER NOT NULL DEFAULT 0"
|
||||
)
|
||||
}
|
||||
|
||||
private fun SQLiteDatabase.addContentTypeColumn() {
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_CONTENT_TYPE TEXT")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package de.markusfisch.android.binaryeye.database
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateFormat
|
||||
import de.markusfisch.android.binaryeye.content.ParsedContentType
|
||||
import de.markusfisch.android.binaryeye.content.parsedContentTypeFromName
|
||||
import de.markusfisch.android.binaryeye.zxingcpp.migrateBarcodeFormatName
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BitMatrix
|
||||
@@ -27,7 +29,8 @@ data class Scan(
|
||||
val dateTime: String = getDateTime(),
|
||||
var id: Long = 0L,
|
||||
val label: String? = null,
|
||||
val pinned: Boolean = false
|
||||
val pinned: Boolean = false,
|
||||
val contentType: ParsedContentType = ParsedContentType.UNKNOWN
|
||||
) {
|
||||
// Needs to be overwritten manually, as ByteArray is an array and
|
||||
// this isn't handled well by Kotlin.
|
||||
@@ -59,7 +62,8 @@ data class Scan(
|
||||
price == other.price &&
|
||||
issueNumber == other.issueNumber &&
|
||||
label == other.label &&
|
||||
pinned == other.pinned
|
||||
pinned == other.pinned &&
|
||||
contentType == other.contentType
|
||||
}
|
||||
|
||||
// Needs to be overwritten manually, as ByteArray is an array and
|
||||
@@ -82,6 +86,7 @@ data class Scan(
|
||||
result = 31 * result + (issueNumber?.hashCode() ?: 0)
|
||||
result = 31 * result + (label?.hashCode() ?: 0)
|
||||
result = 31 * result + pinned.hashCode()
|
||||
result = 31 * result + contentType.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -133,6 +138,7 @@ fun Scan.toBundle() = Bundle().apply {
|
||||
putLong(SCAN_ID, id)
|
||||
putString(SCAN_LABEL, label)
|
||||
putBoolean(SCAN_PINNED, pinned)
|
||||
putString(SCAN_CONTENT_TYPE, contentType.name)
|
||||
}
|
||||
|
||||
fun Bundle.toScan(): Scan? {
|
||||
@@ -159,7 +165,8 @@ fun Bundle.toScan(): Scan? {
|
||||
dateTime = getString(SCAN_DATE_TIME) ?: getDateTime(),
|
||||
id = getLong(SCAN_ID, 0L),
|
||||
label = getString(SCAN_LABEL),
|
||||
pinned = getBoolean(SCAN_PINNED, false)
|
||||
pinned = getBoolean(SCAN_PINNED, false),
|
||||
contentType = parsedContentTypeFromName(getString(SCAN_CONTENT_TYPE))
|
||||
)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
null
|
||||
@@ -207,3 +214,4 @@ 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"
|
||||
private const val SCAN_CONTENT_TYPE = "content_type"
|
||||
|
||||
+174
-17
@@ -1,20 +1,26 @@
|
||||
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
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.OpenableColumns
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.net.toUri
|
||||
import androidx.preference.EditTextPreference
|
||||
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
|
||||
@@ -25,7 +31,10 @@ 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
|
||||
import de.markusfisch.android.binaryeye.camera.availableDefaultCameras
|
||||
import de.markusfisch.android.binaryeye.camera.defaultCameraEntries
|
||||
import de.markusfisch.android.binaryeye.media.beepConfirm
|
||||
import de.markusfisch.android.binaryeye.preference.Preferences
|
||||
import de.markusfisch.android.binaryeye.preference.UrlPreference
|
||||
import de.markusfisch.android.binaryeye.view.setPaddingFromWindowInsets
|
||||
import de.markusfisch.android.binaryeye.view.systemBarRecyclerViewScrollListener
|
||||
@@ -33,6 +42,13 @@ import de.markusfisch.android.binaryeye.widget.toast
|
||||
|
||||
class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
private var lastProfile: String? = null
|
||||
private val pickBeepTone = registerForActivityResult(
|
||||
ActivityResultContracts.OpenDocument()
|
||||
) { uri ->
|
||||
if (uri != null) {
|
||||
setCustomBeepTone(uri)
|
||||
}
|
||||
}
|
||||
|
||||
private val changeListener = object : OnSharedPreferenceChangeListener {
|
||||
override fun onSharedPreferenceChanged(
|
||||
@@ -45,15 +61,16 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
prefs.update()
|
||||
}
|
||||
when (preference.key) {
|
||||
"custom_locale" -> {
|
||||
CUSTOM_LOCALE,
|
||||
DYNAMIC_COLORS -> {
|
||||
activity?.restartApp()
|
||||
return
|
||||
}
|
||||
|
||||
"beep_tone_name",
|
||||
"beep_stream_name" -> beepConfirm()
|
||||
BEEP_TONE_NAME,
|
||||
BEEP_STREAM_NAME -> activity?.beepConfirm()
|
||||
|
||||
"send_scan_bluetooth" -> if (
|
||||
SEND_SCAN_BLUETOOTH -> if (
|
||||
prefs.sendScanBluetooth &&
|
||||
activity?.hasBluetoothPermission() == false
|
||||
) {
|
||||
@@ -85,11 +102,16 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
setSummaries(preferenceScreen)
|
||||
setIcons()
|
||||
|
||||
wireBeepTonePicker()
|
||||
setDefaultCameras()
|
||||
wireProfiles()
|
||||
wireAutomatedActions()
|
||||
wireIgnoreCodes()
|
||||
setBluetoothResources()
|
||||
wireClearNetworkPreferences()
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||
findPreference<PreferenceGroup>("appearance_category")?.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun wireProfiles() {
|
||||
@@ -102,6 +124,40 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun wireBeepTonePicker() {
|
||||
findPreference<ListPreference>(BEEP_TONE_NAME)?.apply {
|
||||
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
val toneName = newValue as? String ?: return@OnPreferenceChangeListener false
|
||||
if (toneName == Preferences.CUSTOM_BEEP_TONE_NAME) {
|
||||
pickBeepTone.launch(arrayOf("audio/*"))
|
||||
false
|
||||
} else {
|
||||
if (prefs.usesCustomBeepTone()) {
|
||||
clearCustomBeepTone()
|
||||
}
|
||||
prefs.beepToneName = toneName
|
||||
value = toneName
|
||||
setSummary(this)
|
||||
activity?.beepConfirm()
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setDefaultCameras() {
|
||||
findPreference<ListPreference>(DEFAULT_CAMERA)?.apply {
|
||||
val values = availableDefaultCameras(requireContext())
|
||||
entries = defaultCameraEntries(requireContext(), values)
|
||||
entryValues = values.toTypedArray()
|
||||
if (!values.contains(value)) {
|
||||
value = values.firstOrNull()
|
||||
}
|
||||
isVisible = values.size > 1
|
||||
setSummary(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun wireAutomatedActions() {
|
||||
findPreference<Preference>(AUTOMATED_ACTIONS)?.apply {
|
||||
updateAutomatedActionsSummary(this)
|
||||
@@ -130,14 +186,14 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
if (prefs.sendScanBluetooth &&
|
||||
activity?.hasBluetoothPermission() == true
|
||||
) {
|
||||
findPreference<ListPreference>("send_scan_bluetooth_host")?.let {
|
||||
findPreference<ListPreference>(SEND_SCAN_BLUETOOTH_HOST)?.let {
|
||||
setBluetoothHosts(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun wireClearNetworkPreferences() {
|
||||
findPreference<Preference>("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.
|
||||
@@ -153,7 +209,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 {
|
||||
@@ -165,7 +221,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()
|
||||
@@ -220,7 +276,7 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
setTargetFragment(this@PreferencesFragment, 0)
|
||||
show(fm, null)
|
||||
}
|
||||
} else if (preference.key == "send_scan_bluetooth_host") {
|
||||
} else if (preference.key == SEND_SCAN_BLUETOOTH_HOST) {
|
||||
val ac = activity ?: return
|
||||
if (ac.hasBluetoothPermission()) {
|
||||
setBluetoothHosts(preference as ListPreference)
|
||||
@@ -242,8 +298,13 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
when (preference.key) {
|
||||
AUTOMATED_ACTIONS -> return updateAutomatedActionsSummary(preference)
|
||||
IGNORE_CODES -> return updateIgnoreCodesSummary(preference)
|
||||
BEEP_TONE_NAME -> {
|
||||
setBeepToneSummary(preference)
|
||||
return
|
||||
}
|
||||
}
|
||||
when (preference) {
|
||||
is EditTextPreference -> preference.summary = preference.text
|
||||
is UrlPreference -> preference.summary = preference.getUrl()
|
||||
is ListPreference -> preference.setSummary(preference.entry)
|
||||
is MultiSelectListPreference -> preference.summary =
|
||||
@@ -253,6 +314,21 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun setBeepToneSummary(preference: Preference) {
|
||||
preference.summary = if (prefs.usesCustomBeepTone()) {
|
||||
getBeepToneLabel(prefs.beepToneUri.toUri())
|
||||
} else {
|
||||
findPreference<ListPreference>(BEEP_TONE_NAME)?.let {
|
||||
val index = it.findIndexOfValue(prefs.beepToneName)
|
||||
if (index >= 0) {
|
||||
it.entries[index]
|
||||
} else {
|
||||
""
|
||||
}
|
||||
} ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSortedSummary(preference: MultiSelectListPreference): String {
|
||||
val labelsByValue = mutableMapOf<String, String>()
|
||||
val entries = preference.entries
|
||||
@@ -280,12 +356,13 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
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(DEFAULT_CAMERA, R.drawable.ic_action_switch_camera)
|
||||
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(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)
|
||||
@@ -293,9 +370,12 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
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("send_scan_device_id", R.drawable.ic_label)
|
||||
setIcon(SEND_SCAN_BLUETOOTH, R.drawable.ic_action_bluetooth)
|
||||
setIcon(SEND_SCAN_BLUETOOTH_HOST, R.drawable.ic_action_bluetooth)
|
||||
setIcon(SEND_SCAN_KDE_CONNECT, R.drawable.ic_action_forward)
|
||||
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)
|
||||
@@ -304,9 +384,10 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
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(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(CUSTOM_LOCALE, R.drawable.ic_action_preferences)
|
||||
setIcon(DYNAMIC_COLORS, R.drawable.ic_action_preferences)
|
||||
}
|
||||
|
||||
private fun setIcon(key: String, iconResId: Int) {
|
||||
@@ -340,14 +421,90 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
|
||||
private fun updateProfileSummary(preference: Preference) {
|
||||
preference.summary = prefs.profile ?: getString(R.string.profile_default)
|
||||
preference.summary = prefs.profileLabel(requireContext(), prefs.profile)
|
||||
lastProfile = prefs.profile
|
||||
}
|
||||
|
||||
private fun setCustomBeepTone(uri: Uri) {
|
||||
val context = requireContext()
|
||||
val resolver = context.contentResolver
|
||||
clearCustomBeepTone()
|
||||
try {
|
||||
resolver.takePersistableUriPermission(
|
||||
uri,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
)
|
||||
} catch (e: SecurityException) {
|
||||
val message = e.message
|
||||
if (!message.isNullOrEmpty()) {
|
||||
context.toast(message)
|
||||
}
|
||||
return
|
||||
}
|
||||
prefs.beepToneUri = uri.toString()
|
||||
prefs.beepToneName = Preferences.CUSTOM_BEEP_TONE_NAME
|
||||
findPreference<ListPreference>(BEEP_TONE_NAME)?.apply {
|
||||
value = Preferences.CUSTOM_BEEP_TONE_NAME
|
||||
setSummary(this)
|
||||
}
|
||||
activity?.beepConfirm()
|
||||
}
|
||||
|
||||
private fun clearCustomBeepTone(releaseSelection: Boolean = true) {
|
||||
if (releaseSelection) {
|
||||
releaseBeepTonePermission(prefs.beepToneUri)
|
||||
}
|
||||
prefs.beepToneUri = ""
|
||||
}
|
||||
|
||||
private fun releaseBeepTonePermission(uriString: String) {
|
||||
if (uriString.isEmpty()) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
requireContext().contentResolver.releasePersistableUriPermission(
|
||||
uriString.toUri(),
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
)
|
||||
} catch (_: SecurityException) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBeepToneLabel(uri: Uri): String {
|
||||
var cursor: Cursor? = null
|
||||
return try {
|
||||
cursor = requireContext().contentResolver.query(
|
||||
uri,
|
||||
arrayOf(OpenableColumns.DISPLAY_NAME),
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
if (cursor?.moveToFirst() == true) {
|
||||
cursor.getString(0)
|
||||
} else {
|
||||
uri.lastPathSegment ?: uri.toString()
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
uri.lastPathSegment ?: uri.toString()
|
||||
} finally {
|
||||
cursor?.close()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PROFILE = "profile"
|
||||
private const val AUTOMATED_ACTIONS = "automated_actions"
|
||||
private const val BEEP_TONE_NAME = "beep_tone_name"
|
||||
private const val BEEP_STREAM_NAME = "beep_stream_name"
|
||||
private const val CLEAR_NETWORK_SUGGESTIONS = "clear_network_suggestions"
|
||||
private const val CUSTOM_LOCALE = "custom_locale"
|
||||
private const val DYNAMIC_COLORS = "dynamic_colors"
|
||||
private const val DEFAULT_CAMERA = "default_camera"
|
||||
private const val IGNORE_CODES = "ignore_codes"
|
||||
private const val SEND_SCAN_BLUETOOTH = "send_scan_bluetooth"
|
||||
private const val SEND_SCAN_BLUETOOTH_HOST = "send_scan_bluetooth_host"
|
||||
private const val SEND_SCAN_KDE_CONNECT = "send_scan_kde_connect"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,11 +48,14 @@ class UrlDialogFragment : PreferenceDialogFragmentCompat() {
|
||||
}
|
||||
textView ?: return
|
||||
textView.text = "…"
|
||||
Scan(
|
||||
"test",
|
||||
null,
|
||||
BarcodeFormat.None
|
||||
).sendAsync(url, prefs.sendScanType) { code, body ->
|
||||
textView.context.sendAsync(
|
||||
Scan(
|
||||
"test",
|
||||
null,
|
||||
BarcodeFormat.None
|
||||
),
|
||||
url
|
||||
) { code, body ->
|
||||
textView.text = when {
|
||||
code != null -> "$code"
|
||||
body != null -> body
|
||||
|
||||
@@ -8,7 +8,7 @@ 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,6 +1,7 @@
|
||||
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
|
||||
@@ -10,8 +11,13 @@ import androidx.core.graphics.createBitmap
|
||||
|
||||
fun Resources.getBitmapFromDrawable(
|
||||
resId: Int
|
||||
): Bitmap = getBitmapFromDrawable(resId, null)
|
||||
|
||||
fun Resources.getBitmapFromDrawable(
|
||||
resId: Int,
|
||||
theme: Theme?
|
||||
): Bitmap = getBitmapFromDrawable(
|
||||
ResourcesCompat.getDrawable(this, resId, null) ?: throw Resources.NotFoundException(
|
||||
ResourcesCompat.getDrawable(this, resId, theme) ?: throw Resources.NotFoundException(
|
||||
"Drawable resource ID #0x${resId.toString(16)}"
|
||||
)
|
||||
)
|
||||
|
||||
@@ -98,6 +98,49 @@ fun Matrix.mapPosition(
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package de.markusfisch.android.binaryeye.kdeconnect
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import de.markusfisch.android.binaryeye.content.startIntentOrToast
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
|
||||
private const val KDE_CONNECT_PACKAGE = "org.kde.kdeconnect_tp"
|
||||
private const val KDE_CONNECT_KEYSTROKES_ACTIVITY =
|
||||
"org.kde.kdeconnect.plugins.mousepad.SendKeystrokesToHostActivity"
|
||||
private const val KEYSTROKES_MIME_TYPE = "text/x-keystrokes"
|
||||
|
||||
fun Context.sendToKdeConnect(scan: Scan): Boolean {
|
||||
if (scan.text.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
return startIntentOrToast(createKdeConnectIntent(scan.text))
|
||||
}
|
||||
|
||||
fun createKdeConnectIntent(text: String): Intent {
|
||||
return Intent(Intent.ACTION_SEND).apply {
|
||||
component = ComponentName(
|
||||
KDE_CONNECT_PACKAGE,
|
||||
KDE_CONNECT_KEYSTROKES_ACTIVITY
|
||||
)
|
||||
type = KEYSTROKES_MIME_TYPE
|
||||
putExtra(Intent.EXTRA_TEXT, text)
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,25 @@
|
||||
package de.markusfisch.android.binaryeye.media
|
||||
|
||||
import android.content.Context
|
||||
import android.media.AudioAttributes
|
||||
import android.media.AudioManager
|
||||
import android.media.MediaPlayer
|
||||
import android.media.ToneGenerator
|
||||
import androidx.core.net.toUri
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
|
||||
private var confirmToneGenerator: ToneGenerator? = null
|
||||
private var confirmToneStream = AudioManager.STREAM_MUSIC
|
||||
private var confirmMediaPlayer: MediaPlayer? = null
|
||||
private var errorToneGenerator: ToneGenerator? = null
|
||||
|
||||
fun beepConfirm() {
|
||||
fun Context.beepConfirm() {
|
||||
val stream = prefs.beepStream()
|
||||
if (prefs.usesCustomBeepTone() &&
|
||||
playCustomConfirmTone(prefs.beepToneUri, stream)
|
||||
) {
|
||||
return
|
||||
}
|
||||
val tg = if (confirmToneGenerator == null || confirmToneStream != stream) {
|
||||
confirmToneGenerator?.release()
|
||||
ToneGenerator(stream, ToneGenerator.MAX_VOLUME).also {
|
||||
@@ -23,6 +33,48 @@ fun beepConfirm() {
|
||||
tg.startTone(prefs.beepTone())
|
||||
}
|
||||
|
||||
private fun Context.playCustomConfirmTone(
|
||||
uriString: String,
|
||||
stream: Int
|
||||
): Boolean {
|
||||
return try {
|
||||
confirmToneGenerator?.release()
|
||||
confirmToneGenerator = null
|
||||
confirmToneStream = stream
|
||||
confirmMediaPlayer?.release()
|
||||
confirmMediaPlayer = MediaPlayer().also { mp ->
|
||||
mp.apply {
|
||||
setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
.setLegacyStreamType(stream)
|
||||
.build()
|
||||
)
|
||||
setDataSource(this@playCustomConfirmTone, uriString.toUri())
|
||||
setOnCompletionListener { completed ->
|
||||
if (confirmMediaPlayer === completed) {
|
||||
confirmMediaPlayer = null
|
||||
}
|
||||
completed.release()
|
||||
}
|
||||
setOnErrorListener { failed, _, _ ->
|
||||
if (confirmMediaPlayer === failed) {
|
||||
confirmMediaPlayer = null
|
||||
}
|
||||
failed.release()
|
||||
true
|
||||
}
|
||||
prepare()
|
||||
start()
|
||||
}
|
||||
}
|
||||
true
|
||||
} catch (_: Exception) {
|
||||
confirmMediaPlayer?.release()
|
||||
confirmMediaPlayer = null
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun beepError() {
|
||||
val tg = errorToneGenerator ?: ToneGenerator(
|
||||
AudioManager.STREAM_ALARM,
|
||||
@@ -36,6 +88,8 @@ fun releaseToneGenerators() {
|
||||
confirmToneGenerator?.release()
|
||||
confirmToneGenerator = null
|
||||
confirmToneStream = AudioManager.STREAM_MUSIC
|
||||
confirmMediaPlayer?.release()
|
||||
confirmMediaPlayer = null
|
||||
errorToneGenerator?.release()
|
||||
errorToneGenerator = null
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package de.markusfisch.android.binaryeye.net
|
||||
|
||||
import android.content.Context
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.content.openUrl
|
||||
import de.markusfisch.android.binaryeye.database.Scan
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -14,61 +17,95 @@ import java.net.HttpURLConnection
|
||||
import java.net.ProtocolException
|
||||
import java.net.URL
|
||||
|
||||
fun Scan.sendAsync(
|
||||
url: String,
|
||||
type: String,
|
||||
private const val SEND_TYPE_GET_ADD_CONTENT = "0"
|
||||
private const val SEND_TYPE_GET_QUERY_STRING = "1"
|
||||
private const val SEND_TYPE_POST_FORM = "2"
|
||||
private const val SEND_TYPE_POST_JSON = "3"
|
||||
private const val SEND_TYPE_EXTERNAL_BROWSER = "4"
|
||||
|
||||
fun Context.sendAsync(
|
||||
scan: Scan,
|
||||
url: String = prefs.sendScanUrl,
|
||||
type: String = prefs.sendScanType,
|
||||
callback: (Int?, String?) -> Unit
|
||||
) {
|
||||
val extras = mapOf(
|
||||
"deviceId" to prefs.sendScanDeviceId.trim()
|
||||
)
|
||||
if (type == SEND_TYPE_EXTERNAL_BROWSER) {
|
||||
openUrl(buildUrl(url, scan.asUrlArguments(extras)))
|
||||
return
|
||||
}
|
||||
CoroutineScope(Dispatchers.IO).launch(Dispatchers.IO) {
|
||||
val response = send(url, type)
|
||||
val response = scan.send(url, type, extras) ?: return@launch
|
||||
withContext(Dispatchers.Main) {
|
||||
callback(response.code, response.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Scan.send(url: String, type: String): Response {
|
||||
return when (type) {
|
||||
"1" -> request(
|
||||
url + "?" + asUrlArguments()
|
||||
private fun Scan.send(
|
||||
url: String,
|
||||
type: String,
|
||||
extras: Map<String, String>
|
||||
): Response? = when (type) {
|
||||
SEND_TYPE_GET_ADD_CONTENT -> request(
|
||||
url + text.urlEncode()
|
||||
)
|
||||
|
||||
SEND_TYPE_GET_QUERY_STRING -> request(
|
||||
buildUrl(url, asUrlArguments(extras))
|
||||
)
|
||||
|
||||
SEND_TYPE_POST_FORM -> request(url) { con ->
|
||||
con.requestMethod = "POST"
|
||||
con.setRequestProperty(
|
||||
"Content-Type",
|
||||
"application/x-www-form-urlencoded"
|
||||
)
|
||||
|
||||
"2" -> request(url) { con ->
|
||||
con.requestMethod = "POST"
|
||||
con.setRequestProperty(
|
||||
"Content-Type",
|
||||
"application/x-www-form-urlencoded"
|
||||
)
|
||||
con.outputStream.apply {
|
||||
write(asUrlArguments().toByteArray())
|
||||
close()
|
||||
}
|
||||
con.outputStream.apply {
|
||||
write(asUrlArguments(extras).toByteArray())
|
||||
close()
|
||||
}
|
||||
|
||||
"3" -> request(url) { con ->
|
||||
con.requestMethod = "POST"
|
||||
con.setRequestProperty("Content-Type", "application/json")
|
||||
con.outputStream.apply {
|
||||
write(asJson().toString().toByteArray())
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
else -> request(
|
||||
url + text.urlEncode()
|
||||
)
|
||||
}
|
||||
|
||||
SEND_TYPE_POST_JSON -> request(url) { con ->
|
||||
con.requestMethod = "POST"
|
||||
con.setRequestProperty("Content-Type", "application/json")
|
||||
con.outputStream.apply {
|
||||
write(asJson(extras).toString().toByteArray())
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun Scan.asUrlArguments(): String = getMap().map { (k, v) ->
|
||||
private fun buildUrl(url: String, arguments: String): String {
|
||||
if (arguments.isEmpty()) {
|
||||
return url
|
||||
}
|
||||
val separator = when {
|
||||
url.endsWith("?") || url.endsWith("&") -> ""
|
||||
url.contains("?") -> "&"
|
||||
else -> "?"
|
||||
}
|
||||
return url + separator + arguments
|
||||
}
|
||||
|
||||
private fun Scan.asUrlArguments(
|
||||
extras: Map<String, String>
|
||||
): String = getMap(extras).map { (k, v) ->
|
||||
"${k}=${v.urlEncode()}"
|
||||
}.joinToString("&")
|
||||
|
||||
private fun Scan.asJson() = JSONObject().apply {
|
||||
getMap().forEach { (k, v) -> put(k, v) }
|
||||
private fun Scan.asJson(
|
||||
extras: Map<String, String>
|
||||
) = JSONObject().apply {
|
||||
getMap(extras).forEach { (k, v) -> put(k, v) }
|
||||
}
|
||||
|
||||
private fun Scan.getMap(): Map<String, String> = mapOf(
|
||||
private fun Scan.getMap(extras: Map<String, String>): Map<String, String> = (mapOf(
|
||||
"content" to text,
|
||||
"raw" to raw?.toHexString(),
|
||||
"format" to format.name,
|
||||
@@ -81,8 +118,8 @@ private fun Scan.getMap(): Map<String, String> = mapOf(
|
||||
"addOn" to addOn,
|
||||
"price" to price,
|
||||
"issueNumber" to issueNumber,
|
||||
"timestamp" to dateTime
|
||||
).filterNullValues()
|
||||
"timestamp" to dateTime,
|
||||
) + extras).filterNullValues()
|
||||
|
||||
private fun Map<String, String?>.filterNullValues() =
|
||||
filterValues { it != null }.mapValues { it.value as String }
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package de.markusfisch.android.binaryeye.os
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import android.view.WindowManager
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun Activity.showWhenLocked() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||
setShowWhenLocked(true)
|
||||
} else {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED)
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,17 @@ import android.graphics.Point
|
||||
import android.media.ToneGenerator
|
||||
import android.preference.PreferenceManager
|
||||
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
|
||||
|
||||
@@ -50,6 +56,9 @@ class Preferences {
|
||||
BarcodeFormat.ITF.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,
|
||||
@@ -90,6 +99,11 @@ class Preferences {
|
||||
apply(BULK_MODE, value)
|
||||
field = value
|
||||
}
|
||||
var defaultCamera = "back"
|
||||
set(value) {
|
||||
apply(DEFAULT_CAMERA, value)
|
||||
field = value
|
||||
}
|
||||
var bulkModeDelay = "500"
|
||||
set(value) {
|
||||
apply(BULK_MODE_DELAY, value)
|
||||
@@ -120,6 +134,11 @@ class Preferences {
|
||||
apply(BEEP_STREAM_NAME, value)
|
||||
field = value
|
||||
}
|
||||
var beepToneUri = ""
|
||||
set(value) {
|
||||
apply(BEEP_TONE_URI, value)
|
||||
field = value
|
||||
}
|
||||
var useHistory = false
|
||||
set(value) {
|
||||
apply(USE_HISTORY, value)
|
||||
@@ -142,6 +161,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)
|
||||
@@ -192,6 +216,11 @@ class Preferences {
|
||||
apply(SEND_SCAN_TYPE, value)
|
||||
field = value
|
||||
}
|
||||
var sendScanDeviceId: String = ""
|
||||
set(value) {
|
||||
apply(SEND_SCAN_DEVICE_ID, value)
|
||||
field = value
|
||||
}
|
||||
var sendScanBluetooth = false
|
||||
set(value) {
|
||||
apply(SEND_SCAN_BLUETOOTH, value)
|
||||
@@ -202,6 +231,11 @@ class Preferences {
|
||||
apply(SEND_SCAN_BLUETOOTH_HOST, value)
|
||||
field = value
|
||||
}
|
||||
var sendScanKdeConnect = false
|
||||
set(value) {
|
||||
apply(SEND_SCAN_KDE_CONNECT, value)
|
||||
field = value
|
||||
}
|
||||
var customLocale: String = ""
|
||||
set(value) {
|
||||
// Make sure this setting is written immediately because
|
||||
@@ -244,6 +278,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
|
||||
|
||||
@@ -254,6 +293,7 @@ class Preferences {
|
||||
defaultPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
loadProfiles()
|
||||
addPredefinedProfiles(context)
|
||||
load(context, defaultPreferences.getString(PROFILE, null))
|
||||
}
|
||||
|
||||
@@ -305,14 +345,199 @@ class Preferences {
|
||||
}
|
||||
}
|
||||
|
||||
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(DEFAULT_CAMERA, p, defaults.defaultCamera)
|
||||
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(BEEP_TONE_URI, p, defaults.beepToneUri)
|
||||
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_DEVICE_ID, p, defaults.sendScanDeviceId)
|
||||
json.put(SEND_SCAN_BLUETOOTH, p, defaults.sendScanBluetooth)
|
||||
json.put(SEND_SCAN_BLUETOOTH_HOST, p, defaults.sendScanBluetoothHost)
|
||||
json.put(SEND_SCAN_KDE_CONNECT, p, defaults.sendScanKdeConnect)
|
||||
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(DEFAULT_CAMERA, 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)
|
||||
putString(BEEP_TONE_URI, 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)
|
||||
putString(SEND_SCAN_DEVICE_ID, json)
|
||||
putBoolean(SEND_SCAN_BLUETOOTH, json)
|
||||
putString(SEND_SCAN_BLUETOOTH_HOST, json)
|
||||
putBoolean(SEND_SCAN_KDE_CONNECT, 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 {
|
||||
val migrated = migrateCode39ToCode39Std(migrateBarcodeFormats(it))
|
||||
if (migrated != it) {
|
||||
apply(BARCODE_FORMATS, migrated)
|
||||
}
|
||||
barcodeFormats = addFormatsOnUpdate(
|
||||
migrated,
|
||||
migrateCode39ToCode39Std(
|
||||
migrateBarcodeFormatNames(it)
|
||||
),
|
||||
BarcodeFormat.Code39Std,
|
||||
BarcodeFormat.Code39Ext,
|
||||
BarcodeFormat.Code32,
|
||||
@@ -323,7 +548,10 @@ class Preferences {
|
||||
BarcodeFormat.DataBarExp,
|
||||
BarcodeFormat.DataBarExpStk,
|
||||
BarcodeFormat.RMQRCode,
|
||||
BarcodeFormat.DXFilmEdge
|
||||
BarcodeFormat.DXFilmEdge,
|
||||
BarcodeFormat.Telepen,
|
||||
BarcodeFormat.TelepenAlpha,
|
||||
BarcodeFormat.TelepenNumeric
|
||||
)
|
||||
}
|
||||
|
||||
@@ -339,6 +567,9 @@ class Preferences {
|
||||
autoRotate = preferences.getBoolean(AUTO_ROTATE, autoRotate)
|
||||
tryHarder = preferences.getBoolean(TRY_HARDER, tryHarder)
|
||||
bulkMode = preferences.getBoolean(BULK_MODE, bulkMode)
|
||||
preferences.getString(DEFAULT_CAMERA, defaultCamera)?.also {
|
||||
defaultCamera = it
|
||||
}
|
||||
bulkModeDelay = preferences.getString(
|
||||
BULK_MODE_DELAY,
|
||||
bulkModeDelay
|
||||
@@ -355,6 +586,9 @@ class Preferences {
|
||||
preferences.getString(BEEP_STREAM_NAME, beepStreamName)?.also {
|
||||
beepStreamName = it
|
||||
}
|
||||
preferences.getString(BEEP_TONE_URI, beepToneUri)?.also {
|
||||
beepToneUri = it
|
||||
}
|
||||
useHistory = preferences.getBoolean(USE_HISTORY, useHistory)
|
||||
|
||||
// Map old setting to new one if it wasn't on default value.
|
||||
@@ -388,6 +622,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 {
|
||||
@@ -414,6 +652,9 @@ class Preferences {
|
||||
preferences.getString(SEND_SCAN_TYPE, sendScanType)?.also {
|
||||
sendScanType = it
|
||||
}
|
||||
preferences.getString(SEND_SCAN_DEVICE_ID, sendScanDeviceId)?.also {
|
||||
sendScanDeviceId = it
|
||||
}
|
||||
sendScanBluetooth = preferences.getBoolean(
|
||||
SEND_SCAN_BLUETOOTH,
|
||||
sendScanBluetooth
|
||||
@@ -424,6 +665,10 @@ class Preferences {
|
||||
)?.also {
|
||||
sendScanBluetoothHost = it
|
||||
}
|
||||
sendScanKdeConnect = preferences.getBoolean(
|
||||
SEND_SCAN_KDE_CONNECT,
|
||||
sendScanKdeConnect
|
||||
)
|
||||
preferences.getString(CUSTOM_LOCALE, customLocale)?.also {
|
||||
customLocale = it
|
||||
}
|
||||
@@ -449,8 +694,9 @@ class Preferences {
|
||||
PREVIEW_SCALE,
|
||||
previewScale
|
||||
)
|
||||
dynamicColors = preferences.getBoolean(DYNAMIC_COLORS, dynamicColors)
|
||||
automatedActions = fromJsonArray(
|
||||
preferences.getString(AUTOMATED_ACTIONS, "[]") ?: "[]"
|
||||
preferences.getString(AUTOMATED_ACTIONS, null) ?: "[]"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -467,39 +713,6 @@ class Preferences {
|
||||
fun shouldIgnoreHistoryContent(content: String, format: String): Boolean =
|
||||
ignoreCodes.any { it.matches(content, format) }
|
||||
|
||||
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 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 migrateBarcodeFormats(formats: Set<String>): Set<String> {
|
||||
return formats.mapTo(mutableSetOf()) { format ->
|
||||
format.migrateBarcodeFormatName()
|
||||
}
|
||||
}
|
||||
|
||||
fun restoreCropHandle(
|
||||
name: String,
|
||||
viewWidth: Int,
|
||||
@@ -527,12 +740,137 @@ class Preferences {
|
||||
else -> android.media.AudioManager.STREAM_MUSIC
|
||||
}
|
||||
|
||||
fun usesCustomBeepTone() = beepToneName == CUSTOM_BEEP_TONE_NAME &&
|
||||
beepToneUri.isNotEmpty()
|
||||
|
||||
fun ignoreDuplicates() = when (ignoreDuplicatesName) {
|
||||
"ignore_consecutive_duplicates" -> IgnoreDuplicates.Consecutive
|
||||
"ignore_any_duplicates" -> IgnoreDuplicates.Any
|
||||
else -> IgnoreDuplicates.Never
|
||||
}
|
||||
|
||||
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(DEFAULT_CAMERA, defaults.defaultCamera)
|
||||
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)
|
||||
putString(BEEP_TONE_URI, defaults.beepToneUri)
|
||||
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)
|
||||
putString(SEND_SCAN_DEVICE_ID, defaults.sendScanDeviceId)
|
||||
putBoolean(SEND_SCAN_BLUETOOTH, defaults.sendScanBluetooth)
|
||||
putString(SEND_SCAN_BLUETOOTH_HOST, defaults.sendScanBluetoothHost)
|
||||
putBoolean(SEND_SCAN_KDE_CONNECT, defaults.sendScanKdeConnect)
|
||||
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) {
|
||||
preferences.edit {
|
||||
putBoolean(label, value)
|
||||
@@ -575,8 +913,12 @@ 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"
|
||||
@@ -584,16 +926,19 @@ class Preferences {
|
||||
private const val AUTO_ROTATE = "auto_rotate"
|
||||
private const val TRY_HARDER = "try_harder"
|
||||
private const val BULK_MODE = "bulk_mode"
|
||||
private const val DEFAULT_CAMERA = "default_camera"
|
||||
private const val BULK_MODE_DELAY = "bulk_mode_delay"
|
||||
private const val SHOW_TOAST_IN_BULK_MODE = "show_toast_in_bulk_mode"
|
||||
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 BEEP_TONE_URI = "beep_tone_uri"
|
||||
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"
|
||||
@@ -605,8 +950,10 @@ class Preferences {
|
||||
private const val SEND_SCAN_ACTIVE = "send_scan_active"
|
||||
private const val SEND_SCAN_URL = "send_scan_url"
|
||||
private const val SEND_SCAN_TYPE = "send_scan_type"
|
||||
private const val SEND_SCAN_DEVICE_ID = "send_scan_device_id"
|
||||
private const val SEND_SCAN_BLUETOOTH = "send_scan_bluetooth"
|
||||
private const val SEND_SCAN_BLUETOOTH_HOST = "send_scan_bluetooth_host"
|
||||
private const val SEND_SCAN_KDE_CONNECT = "send_scan_kde_connect"
|
||||
private const val CUSTOM_LOCALE = "custom_locale"
|
||||
private const val INDEX_OF_LAST_SELECTED_FORMAT = "index_of_last_selected_format"
|
||||
private const val INDEX_OF_LAST_SELECTED_EC_LEVEL = "index_of_last_selected_ec_level"
|
||||
@@ -616,6 +963,48 @@ 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 DEFAULT_IGNORE_CODE_PATTERN = "^FIDO://.*"
|
||||
private const val DYNAMIC_COLORS = "dynamic_colors"
|
||||
private const val DEFAULT_IGNORE_CODE_PATTERN = "(?i)^fido:/.*"
|
||||
const val CUSTOM_BEEP_TONE_NAME = "tone_custom_file"
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.Color
|
||||
import android.os.Build
|
||||
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.content.ContextCompat
|
||||
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 {
|
||||
@@ -77,7 +79,7 @@ fun AppCompatActivity.initBars() {
|
||||
setSupportActionBar(this)
|
||||
},
|
||||
findViewById<View>(R.id.navbar)?.apply {
|
||||
setBackgroundColor(translucentPrimaryColor)
|
||||
setBackgroundColor(getTranslucentPrimaryColor(this@initBars))
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -91,18 +93,23 @@ fun unlockStatusBarColor() {
|
||||
statusBarColorLocked = false
|
||||
}
|
||||
|
||||
private var translucentPrimaryColor = 0
|
||||
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
|
||||
// System bars no longer have a background from SDK35+.
|
||||
@@ -125,6 +132,7 @@ fun colorSystemAndToolBars(
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
setBackgroundColor(translucentPrimaryColor)
|
||||
}
|
||||
}
|
||||
activity.supportActionBar?.setBackgroundDrawable(topColor.toDrawable())
|
||||
|
||||
@@ -20,40 +20,58 @@ fun setPaddingFromWindowInsets(
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||
return
|
||||
}
|
||||
// Capture the bare toolbar height so the listener can recompute the
|
||||
// padded height from scratch every time. Applying the insets only
|
||||
// once (or adding them incrementally) leaves the layout stuck with
|
||||
// stale values when the first callback reports wrong insets, which
|
||||
// is why some devices only settled after several rotations.
|
||||
val baseToolbarHeight = toolbarHeight
|
||||
mainLayout.setOnApplyWindowInsetsListener { _, insets ->
|
||||
val systemBarInsets = insets.getInsets(
|
||||
WindowInsets.Type.systemBars()
|
||||
)
|
||||
|
||||
val statusBarTop = systemBarInsets.top
|
||||
if (toolbar.paddingTop == 0 && statusBarTop > 0) {
|
||||
toolbar.setPadding(0, statusBarTop, 0, 0)
|
||||
toolbar.layoutParams.height += statusBarTop
|
||||
}
|
||||
toolbar.setPadding(0, statusBarTop, 0, 0)
|
||||
toolbar.layoutParams.height = baseToolbarHeight + statusBarTop
|
||||
|
||||
val navBarBottom = systemBarInsets.bottom
|
||||
if (navBarBottom > 0) {
|
||||
mainLayout.setPadding(0, 0, 0, navBarBottom);
|
||||
navbar?.apply {
|
||||
layoutParams.height = navBarBottom
|
||||
}
|
||||
mainLayout.setPadding(0, 0, 0, navBarBottom)
|
||||
navbar?.apply {
|
||||
layoutParams.height = navBarBottom
|
||||
}
|
||||
|
||||
insets
|
||||
}
|
||||
}
|
||||
|
||||
fun View.setPaddingFromWindowInsets() {
|
||||
doOnApplyWindowInsets { v, insets -> v.setPadding(insets) }
|
||||
fun View.setPaddingFromWindowInsets(bottom: Boolean = true) {
|
||||
doOnApplyWindowInsets { v, insets, windowInsets ->
|
||||
if (!bottom) {
|
||||
// On SDK 35+ the window is edge-to-edge and the parent isn't
|
||||
// resized for the soft keyboard, so lift this view by the IME
|
||||
// inset (above the navigation bar) here. On older versions the
|
||||
// parent's fitsSystemWindows already shrinks this view above the
|
||||
// keyboard, so adding the IME inset again would lift it twice.
|
||||
insets.bottom = if (
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM
|
||||
) {
|
||||
getImeBottomInsetWithoutSystemBars(windowInsets)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
v.setPadding(insets)
|
||||
}
|
||||
}
|
||||
|
||||
// A slight variation of the idea from Google's Chris Banes to avoid
|
||||
// adding the toolbar height for every view that needs to be inset.
|
||||
// For the original post see here:
|
||||
// https://medium.com/androiddevelopers/windowinsets-listeners-to-layouts-8f9ccc8fa4d1
|
||||
fun View.doOnApplyWindowInsets(f: (View, Rect) -> Unit) {
|
||||
fun View.doOnApplyWindowInsets(f: (View, Rect, WindowInsetsCompat) -> Unit) {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets ->
|
||||
f(v, insetsWithToolbar(insets))
|
||||
f(v, insetsWithToolbar(insets), insets)
|
||||
insets
|
||||
}
|
||||
// It's important to explicitly request the insets (again) in
|
||||
@@ -70,6 +88,11 @@ private fun insetsWithToolbar(insets: WindowInsetsCompat? = null) = Rect(
|
||||
insets?.systemWindowInsetBottom ?: 0
|
||||
)
|
||||
|
||||
private fun getImeBottomInsetWithoutSystemBars(insets: WindowInsetsCompat) =
|
||||
(insets.getInsets(WindowInsetsCompat.Type.ime()).bottom -
|
||||
insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom)
|
||||
.coerceAtLeast(0)
|
||||
|
||||
private fun View.requestApplyInsetsWhenAttached() {
|
||||
if (isAttachedToWindow) {
|
||||
ViewCompat.requestApplyInsets(this)
|
||||
|
||||
@@ -3,6 +3,7 @@ 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
|
||||
@@ -12,8 +13,9 @@ import android.os.Build
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
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
|
||||
@@ -34,18 +36,27 @@ class DetectorView : View {
|
||||
coordinatesLast = 0
|
||||
invalidate()
|
||||
}
|
||||
private val overlayColor = ColorUtils.setAlphaComponent(
|
||||
MaterialColors.getColor(
|
||||
context,
|
||||
com.google.android.material.R.attr.colorOnSurface,
|
||||
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 = ContextCompat.getColor(context, R.color.dot)
|
||||
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
|
||||
@@ -197,7 +208,7 @@ class DetectorView : View {
|
||||
val mn = min(center.x, center.y) * .8f
|
||||
handlePos.set(
|
||||
(center.x + mn).roundToInt(),
|
||||
(center.y + mn).roundToInt()
|
||||
min((center.y + mn).roundToInt(), handleHome.y)
|
||||
)
|
||||
handleActive = true
|
||||
}
|
||||
@@ -236,8 +247,8 @@ class DetectorView : View {
|
||||
viewWidth = right - left
|
||||
viewHeight = bottom - top
|
||||
center.set(
|
||||
left + (viewWidth / 2),
|
||||
top + (viewHeight / 2)
|
||||
viewWidth / 2,
|
||||
viewHeight / 2
|
||||
)
|
||||
minY = padding * 2
|
||||
maxY = viewHeight - minY
|
||||
|
||||
@@ -10,6 +10,7 @@ val oldToNewFormatNames = mapOf(
|
||||
"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",
|
||||
@@ -22,6 +23,9 @@ val oldToNewFormatNames = mapOf(
|
||||
"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",
|
||||
|
||||
@@ -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,8 +0,0 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@color/primary"/>
|
||||
<item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@drawable/logo"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -1,8 +0,0 @@
|
||||
<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>
|
||||
</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>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
<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>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
<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>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
<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>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
<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>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
<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>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
<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>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
<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="M640 680l-57-56 184-184-184-184 57-56 240 240-240 240zM80 760V600q0-83 58.5-141.5T280 400h247L383 256l57-56 240 240-240 240-57-56 144-144H280q-50 0-85 35t-35 85v160H80z"/>
|
||||
</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="M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89 0.07 0.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-0.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9zm-1 5v5l4.28 2.54 0.72-1.21-3.5-2.08V8H12z"/>
|
||||
</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="M14.54 18.49v-1.98h2.97v-2.97h1.98v2.97h2.97v1.98l-2.952 0.018-0.018 2.952h-1.98l-0.018-2.952-2.952-0.018zm-1.19-3.74L11 12.4V7h2v4.6l1.4 1.4-1.05 1.75zM3 10V4h2v2.35c0.85-1.067 1.888-1.892 3.113-2.475C9.338 3.292 10.633 3 12 3c2.5 0 4.625 0.875 6.375 2.625S21 9.5 21 12h-2c0-1.95-0.679-3.604-2.037-4.963C15.604 5.679 13.95 5 12 5c-1.15 0-2.225 0.267-3.225 0.8S6.933 7.067 6.25 8H9v2H3zm9 11c-2.3 0-4.304-0.763-6.012-2.288C4.279 17.187 3.3 15.283 3.05 13H5.1c0.233 1.733 1.004 3.167 2.313 4.3C8.721 18.433 10.25 19 12 19c0.183 0 0.354-0.004 0.513-0.013 0.158-0.008 0.32-0.029 0.487-0.062v2.025c-0.167 0.017-0.329 0.029-0.487 0.037C12.354 20.996 12.183 21 12 21z"/>
|
||||
</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="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/>
|
||||
</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.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/>
|
||||
</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="M20 4H4C2.9 4 2.01 4.9 2.01 6L2 18c0 1.1 0.9 2 2 2h16c1.1 0 2-0.9 2-2V6c0-1.1-0.9-2-2-2zm0 14H4V8l8 5 8-5v10zm-8-7L4 6h16l-8 5z"/>
|
||||
</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="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>
|
||||
</vector>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
<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="M501 460zm-21 420q-33 0-56.5-23.5T400 800h160q0 33-23.5 56.5T480 880zM160 760v-80h80V400q0-83 50-147.5T420 168v-28q0-25 17.5-42.5T480 80q25 0 42.5 17.5T540 140v28q29 7 53.5 20.5T639 220l-57 57q-21-17-47-27t-55-10q-66 0-113 47t-47 113v280h480v80H160zm496-219l-56-57q10-10 15-24t5-32q0-18-5-32.5T600 371l56-56q21 21 32.5 50t11.5 63q0 34-11.5 63T656 541zm43 43q29-29 45-70t16-86q0-45-16-86t-45-70l57-56q41 41 62.5 95.5T840 428q0 62-21.5 116.5T756 640l-57-56z"/>
|
||||
</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 4H5C3.89 4 3 4.9 3 6v12c0 1.1 0.89 2 2 2h4v-2H5V8h14v10h-4v2h4c1.1 0 2-0.9 2-2V6c0-1.1-0.89-2-2-2zm-7 6l-4 4h3v6h2v-6h3l-4-4z"/>
|
||||
</vector>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp">
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?attr/colorControlNormal" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp">
|
||||
<path android:fillColor="#FF000000" android:pathData="M14 10H2v2h12v-2zm0-4H2v2h12V6zm4 8v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zM2 16h8v-2H2v2z"/>
|
||||
</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="M6 2C4.9 2 4.01 2.9 4.01 4L4 20c0 1.1 0.89 2 1.99 2H18c1.1 0 2-0.9 2-2V8l-6-6H6zm7 7V3.5L18.5 9H13z"/>
|
||||
</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="#FF000000" android:pathData="M19.1 12.9a2.8 2.8 0 0 0 0.1-0.9 2.8 2.8 0 0 0-0.1-0.9l2.1-1.6a0.7 0.7 0 0 0 0.1-0.6l-1.9-3.4a0.7 0.7 0 0 0-0.6-0.2l-2.4 1a6.5 6.5 0 0 0-1.6-0.9l-0.4-2.6a0.5 0.5 0 0 0-0.5-0.4h-3.8a0.5 0.5 0 0 0-0.5 0.4L9.3 5.4a5.6 5.6 0 0 0-1.7 0.9l-2.4-1a0.4 0.4 0 0 0-0.5 0.2l-2 3.4c-0.1 0.2 0 0.4 0.2 0.6l2 1.6A2.8 2.8 0 0 0 4.8 12a2.8 2.8 0 0 0 0.1 0.9l-2.1 1.6a0.7 0.7 0 0 0-0.1 0.6l1.9 3.4a0.7 0.7 0 0 0 0.6 0.2l2.4-1a6.5 6.5 0 0 0 1.6 0.9l0.4 2.6a0.5 0.5 0 0 0 0.5 0.4h3.8a0.5 0.5 0 0 0 0.5-0.4l0.3-2.6a5.6 5.6 0 0 0 1.7-0.9l2.4 1a0.4 0.4 0 0 0 0.5-0.2l2-3.4c0.1-0.2 0-0.4-0.2-0.6zM12 15.6a3.6 3.6 0 1 1 3.6-3.6 3.6 3.6 0 0 1-3.6 3.6z"/>
|
||||
</vector>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="960" android:viewportWidth="960" android:width="24dp">
|
||||
<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="#fff" android:pathData="M480 480q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47zM160 800V688q0-34 17.5-62.5T224 582q62-31 126-46.5T480 520q66 0 130 15.5T736 582q29 15 46.5 43.5T800 688v112H160zm80-80h480v-32q0-11-5.5-20T700 654q-54-27-109-40.5T480 600q-56 0-111 13.5T260 654q-9 5-14.5 14t-5.5 20v32zm240-320q33 0 56.5-23.5T560 320q0-33-23.5-56.5T480 240q-33 0-56.5 23.5T400 320q0 33 23.5 56.5T480 400zm0-80zm0 400z"/>
|
||||
</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="M6 19c0 1.1 0.9 2 2 2h8c1.1 0 2-0.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>
|
||||
</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="M15.55 5.55L11 1v3.07C7.06 4.56 4 7.92 4 12s3.05 7.44 7 7.93v-2.02C8.16 17.43 6 14.97 6 12s2.16-5.43 5-5.91V10l4.55-4.45zM19.93 11c-0.17-1.39-0.72-2.73-1.62-3.89l-1.42 1.42c0.54 0.75 0.88 1.6 1.02 2.47h2.02zM13 17.9v2.02c1.39-0.17 2.74-0.71 3.9-1.61l-1.44-1.44c-0.75 0.54-1.59 0.89-2.46 1.03zm3.89-2.42l1.42 1.41c0.9-1.16 1.45-2.5 1.62-3.89h-2.02c-0.14 0.87-0.48 1.72-1.02 2.48z"/>
|
||||
</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="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 0.9-2 2v10c0 1.1 0.9 2 2 2h12c1.1 0 2-0.9 2-2V10c0-1.1-0.9-2-2-2zm-6 9c-1.1 0-2-0.9-2-2s0.9-2 2-2 2 0.9 2 2-0.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/>
|
||||
</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 17c1.1 0 2-0.9 2-2s-0.9-2-2-2-2 0.9-2 2 0.9 2 2 2zm6-9h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6h1.9c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2H6c-1.1 0-2 0.9-2 2v10c0 1.1 0.9 2 2 2h12c1.1 0 2-0.9 2-2V10c0-1.1-0.9-2-2-2zm0 12H6V10h12v10z"/>
|
||||
</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="M17 3H5C3.89 3 3 3.9 3 5v14c0 1.1 0.89 2 2 2h14c1.1 0 2-0.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z"/>
|
||||
</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="#FF000000" android:pathData="M3 11h8V3H3v8zm2-6h4v4H5V5zM3 21h8v-8H3v8zm2-6h4v4H5v-4zm8-12v8h8V3h-8zm6 6h-4V5h4v4zm0 10h2v2h-2zm-6-6h2v2h-2zm2 2h2v2h-2zm-2 2h2v2h-2zm2 2h2v2h-2zm2-2h2v2h-2zm0-4h2v2h-2zm2 2h2v2h-2z"/>
|
||||
</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="M15.5 14h-0.79l-0.28-0.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-0.59 4.23-1.57L14 14.71v0.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
|
||||
</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="M18 16.08c-0.76 0-1.44 0.3-1.96 0.77L8.91 12.7C8.96 12.47 9 12.24 9 12s-0.04-0.47-0.09-0.7l7.05-4.11C16.5 7.69 17.21 8 18 8c1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 0.24 0.04 0.47 0.09 0.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c0.79 0 1.5-0.31 2.04-0.81l7.12 4.16c-0.05 0.21-0.08 0.43-0.08 0.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z"/>
|
||||
</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="M20 2H4C2.9 2 2.01 2.9 2.01 4L2 22l4-4h14c1.1 0 2-0.9 2-2V4c0-1.1-0.9-2-2-2zM9 11H7V9h2v2zm4 0h-2V9h2v2zm4 0h-2V9h2v2z"/>
|
||||
</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="M20 4h-3.17L15 2H9L7.17 4H4C2.9 4 2 4.9 2 6v12c0 1.1 0.9 2 2 2h16c1.1 0 2-0.9 2-2V6c0-1.1-0.9-2-2-2zm-5 11.5V13H9v2.5L5.5 12 9 8.5V11h6V8.5l3.5 3.5-3.5 3.5z"/>
|
||||
</vector>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user