Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27a382f2cb | ||
|
|
b0eb601e5a | ||
|
|
3a3e743575 | ||
|
|
f08efb8238 | ||
|
|
723362dd67 | ||
|
|
dde039a299 | ||
|
|
f2a056c1d0 | ||
|
|
0defe263ad | ||
|
|
9d8d33de06 | ||
|
|
738b4d0f7b | ||
|
|
80378afa84 | ||
|
|
ec07e10208 | ||
|
|
2a6cd169aa | ||
|
|
051454988a | ||
|
|
a388d7cbec | ||
|
|
fa45d9e088 | ||
|
|
41e538fa97 | ||
|
|
1753438288 | ||
|
|
4135dfbd3f | ||
|
|
4de7d5d5d7 | ||
|
|
c4856f97ef | ||
|
|
aa885b8379 | ||
|
|
eab9639242 | ||
|
|
a199d82539 | ||
|
|
469fc2ff5f | ||
|
|
b4261d8571 | ||
|
|
725817151d | ||
|
|
e434300eda | ||
|
|
6b08c422cb | ||
|
|
4bfa7b3582 | ||
|
|
d6f902ae02 | ||
|
|
94023b0671 | ||
|
|
16a9428bc1 | ||
|
|
3db5891e0f | ||
|
|
d03fd72b95 | ||
|
|
97640c2a1f | ||
|
|
3bba6cf10d | ||
|
|
aca97afc78 | ||
|
|
3b46da5571 | ||
|
|
df62881ee6 | ||
|
|
966eea4f3f | ||
|
|
eb2ed05b6d | ||
|
|
f31c1eee0e | ||
|
|
c61db34d27 | ||
|
|
3d85cef8a0 | ||
|
|
276fcccc46 | ||
|
|
caa075f422 | ||
|
|
cc629314ca | ||
|
|
c9242c3475 | ||
|
|
3a87416cdb | ||
|
|
4c4f60bddc | ||
|
|
557652b18e | ||
|
|
a1094626c4 | ||
|
|
e45dd7ff18 | ||
|
|
1c1625b6fd | ||
|
|
9c4cb0daad | ||
|
|
024dd379e8 | ||
|
|
16c4957c79 | ||
|
|
01741b83cd | ||
|
|
4200b3274a | ||
|
|
eabd14eb60 | ||
|
|
778c087930 | ||
|
|
cd646a2e90 | ||
|
|
be6576ac74 | ||
|
|
6460500a4d | ||
|
|
02474dfad2 | ||
|
|
d78fd109ff | ||
|
|
85e3f49270 | ||
|
|
59d40b944b | ||
|
|
1e82e8bd53 | ||
|
|
ab5e9b6e9b | ||
|
|
7336d8d12b | ||
|
|
1c9dd73d9a | ||
|
|
5892002a89 | ||
|
|
540290e97c | ||
|
|
f315916fb8 | ||
|
|
e75bb48360 | ||
|
|
2b1d711168 | ||
|
|
2af5a89430 | ||
|
|
b640fa1372 | ||
|
|
8e24673d08 | ||
|
|
ae54e62ec3 | ||
|
|
420a4cbad3 | ||
|
|
05ec77da27 | ||
|
|
c03abf1148 | ||
|
|
b929df4848 | ||
|
|
ec729b6624 | ||
|
|
67471ee141 |
@@ -1,5 +1,46 @@
|
||||
# Change Log
|
||||
|
||||
# 1.26.0
|
||||
* Zoom in & out by swiping vertically in camera view
|
||||
* Add a setting for swipe to zoom feature
|
||||
* Optimize vector drawables
|
||||
* Update Indonesian translation
|
||||
|
||||
# 1.25.1
|
||||
* Fix Proguard configuration
|
||||
|
||||
# 1.25.0
|
||||
* Add giving scans individual names in history listing
|
||||
* Add searching the history
|
||||
* Add a setting to search harder for barcodes
|
||||
* Add a setting to control vibration on detection
|
||||
* Add app shortcuts for launchers that support them
|
||||
* Remove saving barcodes for intents from other apps
|
||||
* Show latest possible result point in camera view
|
||||
* Suggest a WiFi network on Android Q+
|
||||
* Always put parsed WiFi password into clipboard
|
||||
* Use native image processing where available
|
||||
* Ignore consecutive duplicates by default
|
||||
* Fix saving external files on Android Q+
|
||||
* Fix order of metadata items
|
||||
|
||||
# 1.24.0
|
||||
* Show barcode metadata
|
||||
* Add a setting to show/hide metadata
|
||||
* Include metadata in CSV export
|
||||
* Update italian translation
|
||||
* Fix resetting flash button
|
||||
* Fix CSV export for Android Q
|
||||
|
||||
# 1.23.0
|
||||
* Add a setting to not save consecutive double scans
|
||||
* Add a setting to show/hide hex dump
|
||||
* Limit history items to just one line
|
||||
* Fix window insets for consecutive screens
|
||||
|
||||
# 1.22.3
|
||||
* Fix broken RenderScript built
|
||||
|
||||
# 1.22.2
|
||||
* Fix getting window insets for Android Lollipop and above
|
||||
|
||||
|
||||
@@ -18,6 +18,9 @@ infer: clean
|
||||
infer -- ./gradlew assembleDebug
|
||||
|
||||
test:
|
||||
./gradlew test
|
||||
|
||||
cat:
|
||||
./gradlew test cAT
|
||||
|
||||
install:
|
||||
@@ -33,5 +36,8 @@ uninstall:
|
||||
images:
|
||||
svg/update.sh
|
||||
|
||||
avocado:
|
||||
avocado $(shell fgrep -rl '<vector' app/src/main/res)
|
||||
|
||||
clean:
|
||||
./gradlew clean
|
||||
|
||||
@@ -10,11 +10,27 @@ comes in Material Design and can also generate barcodes.
|
||||
Binary Eye uses the [ZXing][zxing] ("Zebra Crossing") barcode scanning
|
||||
library.
|
||||
|
||||
## Screenshots
|
||||
|
||||
<img
|
||||
src="https://raw.githubusercontent.com/markusfisch/BinaryEye/gh-pages/screencap-scanning.png"
|
||||
alt="Screenshot Scanning" width="160"/>
|
||||
<img
|
||||
src="https://raw.githubusercontent.com/markusfisch/BinaryEye/gh-pages/screencap-decoded.png"
|
||||
alt="Screenshot Result" width="160"/>
|
||||
<img
|
||||
src="https://raw.githubusercontent.com/markusfisch/BinaryEye/gh-pages/screencap-compose-barcode.png"
|
||||
alt="Screenshot Compose Barcode" width="160"/>
|
||||
|
||||
## Download
|
||||
|
||||
<a href="https://f-droid.org/en/packages/de.markusfisch.android.binaryeye/"><img alt="Get it on F-Droid" src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" height="80"/></a> <a href="https://play.google.com/store/apps/details?id=de.markusfisch.android.binaryeye"><img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" height="80"/></a>
|
||||
|
||||
## Supported Barcode Formats
|
||||
|
||||
### Read
|
||||
|
||||
ZXing can _read_ the following barcode formats:
|
||||
ZXing can read the following barcode formats:
|
||||
* [AZTEC][aztec]
|
||||
* [CODABAR][codabar]
|
||||
* [CODE 39][code_39]
|
||||
@@ -35,7 +51,7 @@ ZXing can _read_ the following barcode formats:
|
||||
|
||||
### Generate
|
||||
|
||||
ZXing can _generate_ the following barcode formats:
|
||||
ZXing can generate the following barcode formats:
|
||||
* [AZTEC][aztec]
|
||||
* [CODABAR][codabar]
|
||||
* [CODE 39][code_39]
|
||||
@@ -48,22 +64,6 @@ ZXing can _generate_ the following barcode formats:
|
||||
* [QR CODE][qr_code]
|
||||
* [UPC A][upc_a]
|
||||
|
||||
## Screenshots
|
||||
|
||||
<img
|
||||
src="https://raw.githubusercontent.com/markusfisch/BinaryEye/gh-pages/screencap-scanning.png"
|
||||
alt="Screenshot Scanning" width="160"/>
|
||||
<img
|
||||
src="https://raw.githubusercontent.com/markusfisch/BinaryEye/gh-pages/screencap-decoded.png"
|
||||
alt="Screenshot Result" width="160"/>
|
||||
<img
|
||||
src="https://raw.githubusercontent.com/markusfisch/BinaryEye/gh-pages/screencap-compose-barcode.png"
|
||||
alt="Screenshot Compose Barcode" width="160"/>
|
||||
|
||||
## Download
|
||||
|
||||
<a href="https://f-droid.org/en/packages/de.markusfisch.android.binaryeye/"><img alt="Get it on F-Droid" src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" height="80"/></a> <a href="https://play.google.com/store/apps/details?id=de.markusfisch.android.binaryeye"><img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" height="80"/></a>
|
||||
|
||||
[play]: https://play.google.com/store/search?q=barcode%20scanner&c=apps
|
||||
[zxing]: https://github.com/zxing/zxing
|
||||
[kotlin]: http://kotlinlang.org/
|
||||
|
||||
@@ -9,8 +9,8 @@ android {
|
||||
minSdkVersion 9
|
||||
targetSdkVersion sdk_version
|
||||
|
||||
versionCode 54
|
||||
versionName '1.22.2'
|
||||
versionCode 60
|
||||
versionName '1.26.0'
|
||||
|
||||
// it's recommended to set this value to the lowest API level
|
||||
// able to provide all the functionality
|
||||
@@ -75,6 +75,6 @@ dependencies {
|
||||
// Do NOT upgrade ZXing because newer versions won't work on API < 24
|
||||
// https://github.com/zxing/zxing/issues/1170
|
||||
implementation 'com.google.zxing:core:3.3.3'
|
||||
implementation 'com.github.markusfisch:CameraView:1.8.4'
|
||||
implementation 'com.github.markusfisch:CameraView:1.9.1'
|
||||
implementation 'com.github.markusfisch:ScalingImageView:1.2.0'
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
-dontwarn com.google.**
|
||||
-dontwarn android.support.v7.**
|
||||
-dontwarn android.support.v8.**
|
||||
|
||||
-keep class android.support.v7.widget.SearchView { *; }
|
||||
-keep class android.support.v8.renderscript.** { *; }
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<supports-screens
|
||||
android:largeScreens="true"
|
||||
android:xlargeScreens="true"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
@@ -30,6 +31,9 @@
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts"/>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activity.CameraActivity"
|
||||
@@ -70,7 +74,8 @@
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activity.MainActivity"
|
||||
android:label="@string/app_name"/>
|
||||
android:label="@string/app_name"
|
||||
android:exported="true"/>
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
@@ -80,5 +85,8 @@
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths"/>
|
||||
</provider>
|
||||
<meta-data
|
||||
android:name="android.app.searchable"
|
||||
android:resource="@xml/searchable"/>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -13,7 +13,7 @@ import de.markusfisch.android.binaryeye.actions.wifi.WifiAction
|
||||
object ActionRegistry {
|
||||
val DEFAULT_ACTION: IAction = OpenOrSearchAction
|
||||
|
||||
val REGISTRY: Set<IAction> = setOf(
|
||||
private val REGISTRY: Set<IAction> = setOf(
|
||||
MailAction,
|
||||
OtpauthAction,
|
||||
SmsAction,
|
||||
|
||||
@@ -2,9 +2,9 @@ package de.markusfisch.android.binaryeye.actions
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
import de.markusfisch.android.binaryeye.app.execShareIntent
|
||||
import de.markusfisch.android.binaryeye.app.parseAndNormalizeUri
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
|
||||
interface IAction {
|
||||
val iconResId: Int
|
||||
@@ -18,12 +18,9 @@ abstract class IntentAction : IAction {
|
||||
abstract val errorMsg: Int
|
||||
|
||||
final override suspend fun execute(context: Context, data: ByteArray) {
|
||||
val intent =
|
||||
createIntent(context, data) ?: return Toast.makeText(
|
||||
context,
|
||||
errorMsg,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
val intent = createIntent(context, data) ?: return context.toast(
|
||||
errorMsg
|
||||
)
|
||||
execShareIntent(context, intent)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@ package de.markusfisch.android.binaryeye.actions.search
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IAction
|
||||
import de.markusfisch.android.binaryeye.app.alertDialog
|
||||
import de.markusfisch.android.binaryeye.app.execShareIntent
|
||||
import de.markusfisch.android.binaryeye.app.parseAndNormalizeUri
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import java.net.URLEncoder
|
||||
|
||||
object OpenOrSearchAction : IAction {
|
||||
@@ -28,11 +28,7 @@ object OpenOrSearchAction : IAction {
|
||||
when {
|
||||
intent.resolveActivity(context.packageManager) != null -> return intent
|
||||
search -> return getSearchIntent(context, data)
|
||||
else -> Toast.makeText(
|
||||
context,
|
||||
R.string.cannot_resolve_action,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
else -> context.toast(R.string.cannot_resolve_action)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -120,24 +120,26 @@ object VCardAction : IntentAction() {
|
||||
}
|
||||
|
||||
private val String.locationFormat: String
|
||||
get() = """^([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?)$""".toRegex().matchEntire(
|
||||
this
|
||||
)?.groupValues?.let {
|
||||
listOf(
|
||||
it[1],
|
||||
it[2],
|
||||
it[3],
|
||||
"${it[4]} ${it[6]}",
|
||||
"${it[5]} ${it[7]}"
|
||||
).filter { line -> line.isNotBlank() }
|
||||
.joinToString("\n").takeIf { location -> location.isNotBlank() }
|
||||
} ?: this
|
||||
get() = """^([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?)$""".toRegex()
|
||||
.matchEntire(
|
||||
this
|
||||
)?.groupValues?.let {
|
||||
listOf(
|
||||
it[1],
|
||||
it[2],
|
||||
it[3],
|
||||
"${it[4]} ${it[6]}",
|
||||
"${it[5]} ${it[7]}"
|
||||
).filter { line -> line.isNotBlank() }
|
||||
.joinToString("\n").takeIf { location -> location.isNotBlank() }
|
||||
} ?: this
|
||||
|
||||
private val String.nameFormat: String
|
||||
get() = """^([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?)$""".toRegex().matchEntire(this)?.groupValues?.let {
|
||||
listOf(it[4], it[2], it[3], it[1], it[5]).joinToString(" ")
|
||||
.takeIf { name -> name.isNotBlank() }
|
||||
} ?: this
|
||||
get() = """^([^;]*?);([^;]*?);([^;]*?);([^;]*?);([^;]*?)$""".toRegex()
|
||||
.matchEntire(this)?.groupValues?.let {
|
||||
listOf(it[4], it[2], it[3], it[1], it[5]).joinToString(" ")
|
||||
.takeIf { name -> name.isNotBlank() }
|
||||
} ?: this
|
||||
|
||||
private val String.mailType: Int?
|
||||
get() = when (this.toUpperCase(Locale.US)) {
|
||||
|
||||
@@ -1,90 +1,33 @@
|
||||
package de.markusfisch.android.binaryeye.actions.wifi
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Context.WIFI_SERVICE
|
||||
import android.net.wifi.WifiConfiguration
|
||||
import android.net.wifi.WifiManager
|
||||
import android.widget.Toast
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.IAction
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
|
||||
object WifiAction : IAction {
|
||||
override val iconResId = R.drawable.ic_action_wifi
|
||||
override val titleResId = R.string.connect_to_wifi
|
||||
|
||||
override fun canExecuteOn(data: ByteArray): Boolean =
|
||||
WifiConfigurationFactory.parse(String(data)) != null
|
||||
var password: String? = null
|
||||
|
||||
override suspend fun execute(context: Context, data: ByteArray) = withContext(Dispatchers.IO) {
|
||||
val wifiConfig = WifiConfigurationFactory.parse(
|
||||
override fun canExecuteOn(data: ByteArray): Boolean =
|
||||
WifiConnector.parse(String(data)) {
|
||||
password = it
|
||||
} != null
|
||||
|
||||
override suspend fun execute(
|
||||
context: Context,
|
||||
data: ByteArray
|
||||
) = withContext(Dispatchers.IO) {
|
||||
val wifiConfig = WifiConnector.parse(
|
||||
String(data)
|
||||
) ?: return@withContext
|
||||
val wifiManager = context.applicationContext.getSystemService(
|
||||
WIFI_SERVICE
|
||||
) as WifiManager
|
||||
|
||||
wifiManager.enableWifi(context)
|
||||
wifiManager.mayRemoveOldNetwork(wifiConfig)
|
||||
wifiManager.enableNewNetwork(wifiConfig)
|
||||
val message = WifiConnector.addNetwork(context, wifiConfig)
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.wifi_added,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
context.toast(message)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun WifiManager.enableWifi(context: Context): Boolean {
|
||||
if (!isWifiEnabled) {
|
||||
if (!setWifiEnabled(true)) {
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.wifi_config_failed,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
return false
|
||||
}
|
||||
// wait for wifi to really be enabled, takes some time
|
||||
withTimeoutOrNull(10000) {
|
||||
while (!isWifiEnabled) delay(500)
|
||||
} ?: if (!isWifiEnabled) {
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.wifi_config_failed,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private suspend fun WifiManager.mayRemoveOldNetwork(
|
||||
wifiConfig: WifiConfiguration
|
||||
) {
|
||||
configuredNetworks?.firstOrNull {
|
||||
it.SSID == wifiConfig.SSID &&
|
||||
it.allowedKeyManagement == wifiConfig.allowedKeyManagement
|
||||
}?.networkId?.also {
|
||||
removeNetwork(it)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun WifiManager.enableNewNetwork(
|
||||
wifiConfig: WifiConfiguration
|
||||
) {
|
||||
val id = addNetwork(wifiConfig)
|
||||
disconnect()
|
||||
enableNetwork(id, true)
|
||||
reconnect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
package de.markusfisch.android.binaryeye.actions.wifi
|
||||
|
||||
import android.content.Context
|
||||
import android.net.wifi.WifiConfiguration
|
||||
import android.net.wifi.WifiEnterpriseConfig
|
||||
import android.net.wifi.WifiManager
|
||||
import android.net.wifi.WifiNetworkSuggestion
|
||||
import android.os.Build
|
||||
import android.support.annotation.RequiresApi
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
@@ -29,11 +34,53 @@ import java.util.*
|
||||
*
|
||||
* The fields can appear in any order. Only "S:" is required.
|
||||
*/
|
||||
object WifiConfigurationFactory {
|
||||
fun parse(input: String): WifiConfiguration? {
|
||||
object WifiConnector {
|
||||
fun parse(
|
||||
input: String,
|
||||
passwordBlock: ((password: String?) -> Unit)? = null
|
||||
): Any? {
|
||||
val inputMap = parseMap(input) ?: return null
|
||||
val parsedData = SimpleDataAccessor.of(inputMap) ?: return null
|
||||
return WifiConfiguration().apply(parsedData)
|
||||
passwordBlock?.apply {
|
||||
invoke(parsedData.password)
|
||||
}
|
||||
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
// WifiConfiguration is deprecated in Android Q
|
||||
@Suppress("DEPRECATION")
|
||||
WifiConfiguration().apply(parsedData)
|
||||
} else {
|
||||
WifiNetworkSuggestion.Builder().apply(parsedData)
|
||||
}
|
||||
}
|
||||
|
||||
fun addNetwork(context: Context, config: Any): Int {
|
||||
val wifiManager = context.applicationContext.getSystemService(
|
||||
Context.WIFI_SERVICE
|
||||
) as WifiManager
|
||||
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
// WifiConfiguration is deprecated in Android Q
|
||||
@Suppress("DEPRECATION")
|
||||
val wifiConfig = config as WifiConfiguration
|
||||
if (wifiManager.enableWifi() &&
|
||||
wifiManager.removeOldNetwork(wifiConfig) &&
|
||||
wifiManager.enableNewNetwork(wifiConfig)
|
||||
) {
|
||||
R.string.wifi_added
|
||||
} else {
|
||||
R.string.wifi_config_failed
|
||||
}
|
||||
} else {
|
||||
val suggestion = (config as WifiNetworkSuggestion.Builder).build()
|
||||
val suggestions = listOf(suggestion)
|
||||
// remove previous conflicting network suggestion
|
||||
wifiManager.removeNetworkSuggestions(suggestions)
|
||||
val result = wifiManager.addNetworkSuggestions(suggestions)
|
||||
if (result == WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS) {
|
||||
R.string.wifi_added
|
||||
} else {
|
||||
R.string.wifi_config_failed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun parseMap(string: String): Map<String, String>? {
|
||||
@@ -41,9 +88,8 @@ object WifiConfigurationFactory {
|
||||
// generators don't add it
|
||||
val wifiRegex = """^WIFI:((?:.+?:(?:[^\\;]|\\.)*;)+);?$""".toRegex()
|
||||
// should be: ^(.+):((?:[^\\;,":]|\\.)*);$ but allows unescaped , "
|
||||
// and : because many qr creator doesn't escape
|
||||
// and : because many QR Code creators don't escape properly
|
||||
val pairRegex = """(.+?):((?:[^\\;]|\\.)*);""".toRegex()
|
||||
|
||||
return wifiRegex.matchEntire(
|
||||
string
|
||||
)?.groupValues?.get(1)?.let { pairs ->
|
||||
@@ -56,26 +102,19 @@ object WifiConfigurationFactory {
|
||||
internal class SimpleDataAccessor private constructor(
|
||||
private val inputMap: Map<String, String>
|
||||
) {
|
||||
private val hexRegex = """^[0-9a-f]+$""".toRegex(
|
||||
RegexOption.IGNORE_CASE
|
||||
)
|
||||
// keep possibility of wrongly not escaped \ by explicitly searching
|
||||
// for special chars
|
||||
private val escapedRegex = """\\([\\;,":])""".toRegex()
|
||||
|
||||
// this should be private but because of testing it isn't possible
|
||||
internal val ssid: String
|
||||
get() = inputMap.getValue("S").quotedUnlessHex.unescaped
|
||||
get() = inputMap.getValue("S").unescape
|
||||
internal val securityType: String
|
||||
get() = inputMap["T"]?.unescaped ?: ""
|
||||
get() = inputMap["T"]?.unescape ?: ""
|
||||
internal val password: String?
|
||||
get() = inputMap["P"]?.quotedUnlessHex?.unescaped
|
||||
get() = inputMap["P"]?.unescape
|
||||
internal val hidden: Boolean
|
||||
get() = inputMap["H"]?.unescaped == "true"
|
||||
get() = inputMap["H"]?.unescape == "true"
|
||||
internal val anonymousIdentity: String
|
||||
get() = inputMap["AI"]?.unescaped ?: ""
|
||||
get() = inputMap["AI"]?.unescape ?: ""
|
||||
internal val identity: String
|
||||
get() = inputMap["I"]?.unescaped ?: ""
|
||||
get() = inputMap["I"]?.unescape ?: ""
|
||||
internal val eapMethod: Int?
|
||||
get() = if (inputMap["E"].isNullOrEmpty()) {
|
||||
requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
@@ -110,16 +149,6 @@ object WifiConfigurationFactory {
|
||||
else -> null
|
||||
}
|
||||
|
||||
private val String.unescaped: String
|
||||
get() = this.replace(escapedRegex) { escaped ->
|
||||
escaped.groupValues[1]
|
||||
}
|
||||
|
||||
private val String.quotedUnlessHex: String
|
||||
get() = if (matches(hexRegex) || (startsWith("\"") &&
|
||||
endsWith("\""))
|
||||
) this else "\"$this\""
|
||||
|
||||
internal companion object {
|
||||
internal fun of(inputMap: Map<String, String>): SimpleDataAccessor? {
|
||||
return SimpleDataAccessor(inputMap).takeUnless {
|
||||
@@ -129,9 +158,68 @@ object WifiConfigurationFactory {
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
private fun WifiNetworkSuggestion.Builder.apply(
|
||||
data: SimpleDataAccessor
|
||||
): WifiNetworkSuggestion.Builder? {
|
||||
fun WifiNetworkSuggestion.Builder.applyCommon(
|
||||
data: SimpleDataAccessor
|
||||
): WifiNetworkSuggestion.Builder? {
|
||||
setSsid(data.ssid)
|
||||
return this
|
||||
}
|
||||
|
||||
fun WifiNetworkSuggestion.Builder.applySecurity(
|
||||
data: SimpleDataAccessor
|
||||
): WifiNetworkSuggestion.Builder? {
|
||||
when (data.securityType) {
|
||||
"WPA", "WPA2" -> {
|
||||
data.password?.let {
|
||||
setWpa2Passphrase(it)
|
||||
}
|
||||
}
|
||||
"WPA2-EAP" -> {
|
||||
data.password?.let {
|
||||
setWpa2Passphrase(it)
|
||||
}
|
||||
setWpa2EnterpriseConfig(WifiEnterpriseConfig().apply {
|
||||
identity = data.identity
|
||||
anonymousIdentity = data.anonymousIdentity
|
||||
password = data.password
|
||||
data.eapMethod?.let { eapMethod = it }
|
||||
data.phase2Method?.let { phase2Method = it }
|
||||
})
|
||||
}
|
||||
"WPA3" -> {
|
||||
data.password?.let {
|
||||
setWpa3Passphrase(it)
|
||||
}
|
||||
}
|
||||
"WPA3-EAP" -> {
|
||||
data.password?.let { setWpa3Passphrase(it) }
|
||||
setWpa3EnterpriseConfig(WifiEnterpriseConfig().apply {
|
||||
identity = data.identity
|
||||
anonymousIdentity = data.anonymousIdentity
|
||||
password = data.password
|
||||
data.eapMethod?.let { eapMethod = it }
|
||||
data.phase2Method?.let { phase2Method = it }
|
||||
})
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
return this.applyCommon(data)?.applySecurity(data)
|
||||
}
|
||||
|
||||
// WifiConfiguration is deprecated in Android Q
|
||||
@Suppress("DEPRECATION")
|
||||
private fun WifiConfiguration.apply(
|
||||
data: SimpleDataAccessor
|
||||
): WifiConfiguration? {
|
||||
val ssidWithQuotes = data.ssid.quote
|
||||
val passwordWithQuotes = data.password?.quoteUnlessHex
|
||||
|
||||
fun WifiConfiguration.applyCommon(
|
||||
data: SimpleDataAccessor
|
||||
): WifiConfiguration? {
|
||||
@@ -141,7 +229,7 @@ object WifiConfigurationFactory {
|
||||
allowedPairwiseCiphers.clear()
|
||||
allowedProtocols.clear()
|
||||
|
||||
SSID = data.ssid
|
||||
SSID = ssidWithQuotes
|
||||
hiddenSSID = data.hidden
|
||||
return this
|
||||
}
|
||||
@@ -152,7 +240,7 @@ object WifiConfigurationFactory {
|
||||
when (data.securityType) {
|
||||
"", "nopass" -> allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE)
|
||||
"WEP" -> @Suppress("DEPRECATION") /* WEP as insecure */ {
|
||||
data.password?.also {
|
||||
passwordWithQuotes?.let {
|
||||
wepKeys[0] = it
|
||||
} ?: return null
|
||||
wepTxKeyIndex = 0
|
||||
@@ -164,7 +252,7 @@ object WifiConfigurationFactory {
|
||||
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP104)
|
||||
}
|
||||
"WPA" -> {
|
||||
data.password?.also {
|
||||
passwordWithQuotes?.let {
|
||||
preSharedKey = it
|
||||
} ?: return null
|
||||
allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN)
|
||||
@@ -180,7 +268,7 @@ object WifiConfigurationFactory {
|
||||
allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP)
|
||||
}
|
||||
"WPA2-EAP" -> requireSdk(Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
data.password?.also {
|
||||
passwordWithQuotes?.let {
|
||||
preSharedKey = it
|
||||
} ?: return null
|
||||
allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN)
|
||||
@@ -203,6 +291,7 @@ object WifiConfigurationFactory {
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
return this.applyCommon(data)?.applySecurity(data)
|
||||
}
|
||||
|
||||
@@ -210,3 +299,72 @@ object WifiConfigurationFactory {
|
||||
return if (Build.VERSION.SDK_INT >= version) return block() else null
|
||||
}
|
||||
}
|
||||
|
||||
// keep possibility of wrongly unescaped \ by explicitly searching
|
||||
// for special chars
|
||||
private val escapedRegex = """\\([\\;,":])""".toRegex()
|
||||
private val String.unescape: String
|
||||
get() = this.replace(escapedRegex) { escaped ->
|
||||
escaped.groupValues[1]
|
||||
}
|
||||
|
||||
private val String.quote: String
|
||||
get() = if (startsWith("\"") && endsWith("\"")) this else "\"$this\""
|
||||
|
||||
private val String.quoteUnlessHex: String
|
||||
get() = if (isHex()) this else this.quote
|
||||
|
||||
// Unfortunately, we can never know whether a string consisting only
|
||||
// of 0-f is a hex string. So we just make this one assumption that
|
||||
// a string of 0-f that is exactly 64 characters long, is _probably_
|
||||
// a hex string for raw PSK. Of course, this may fail for any string
|
||||
// that just happens to be 64 characters long and contains just 0-f.
|
||||
private val hexRegex = """^[0-9a-f]+$""".toRegex(
|
||||
RegexOption.IGNORE_CASE
|
||||
)
|
||||
|
||||
private fun String.isHex() = length == 64 && matches(hexRegex)
|
||||
|
||||
private fun WifiManager.enableWifi(): Boolean {
|
||||
// setWifiEnabled() will always return false for Android Q
|
||||
// because Q doesn't allow apps to enable/disable Wi-Fi anymore
|
||||
@Suppress("DEPRECATION")
|
||||
return isWifiEnabled || setWifiEnabled(true)
|
||||
}
|
||||
|
||||
// WifiConfiguration is deprecated in Android Q
|
||||
@Suppress("DEPRECATION")
|
||||
private fun WifiManager.removeOldNetwork(
|
||||
wifiConfig: WifiConfiguration
|
||||
): Boolean {
|
||||
try {
|
||||
configuredNetworks?.firstOrNull {
|
||||
it.SSID == wifiConfig.SSID &&
|
||||
it.allowedKeyManagement == wifiConfig.allowedKeyManagement
|
||||
}?.networkId?.also {
|
||||
removeNetwork(it)
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
// the user didn't allow ACCESS_FINE_LOCATION which is
|
||||
// required to access configuredNetworks and that's fine
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// WifiConfiguration is deprecated in Android Q
|
||||
@Suppress("DEPRECATION")
|
||||
private fun WifiManager.enableNewNetwork(
|
||||
wifiConfig: WifiConfiguration
|
||||
): Boolean {
|
||||
val id = addNetwork(wifiConfig)
|
||||
if (id == -1) {
|
||||
return false
|
||||
}
|
||||
disconnect()
|
||||
return if (enableNetwork(id, true)) {
|
||||
reconnect()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -12,27 +12,38 @@ import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.widget.SeekBar
|
||||
import android.widget.Toast
|
||||
import com.google.zxing.Result
|
||||
import com.google.zxing.ResultMetadataType
|
||||
import com.google.zxing.ResultPointCallback
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.db
|
||||
import de.markusfisch.android.binaryeye.app.hasCameraPermission
|
||||
import de.markusfisch.android.binaryeye.app.initSystemBars
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.app.*
|
||||
import de.markusfisch.android.binaryeye.data.Scan
|
||||
import de.markusfisch.android.binaryeye.graphics.Mapping
|
||||
import de.markusfisch.android.binaryeye.graphics.frameToView
|
||||
import de.markusfisch.android.binaryeye.rs.Preprocessor
|
||||
import de.markusfisch.android.binaryeye.widget.DetectorView
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import de.markusfisch.android.binaryeye.zxing.Zxing
|
||||
import de.markusfisch.android.cameraview.widget.CameraView
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class CameraActivity : AppCompatActivity() {
|
||||
private val zxing = Zxing()
|
||||
private val zxing = Zxing(ResultPointCallback { point ->
|
||||
point?.let {
|
||||
mapping?.map(it)?.let {
|
||||
detectorView.post {
|
||||
detectorView.mark(listOf(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
private lateinit var vibrator: Vibrator
|
||||
private lateinit var cameraView: CameraView
|
||||
@@ -41,6 +52,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
private lateinit var flashFab: View
|
||||
|
||||
private var preprocessor: Preprocessor? = null
|
||||
private var mapping: Mapping? = null
|
||||
private var invert = false
|
||||
private var flash = false
|
||||
private var returnResult = false
|
||||
@@ -56,11 +68,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
REQUEST_CAMERA -> if (grantResults.isNotEmpty() &&
|
||||
grantResults[0] != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
Toast.makeText(
|
||||
this,
|
||||
R.string.no_camera_no_fun,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
toast(R.string.no_camera_no_fun)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
@@ -119,6 +127,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
System.gc()
|
||||
zxing.updateHints(prefs.tryHarder)
|
||||
returnResult = "com.google.zxing.client.android.SCAN" == intent.action
|
||||
if (hasCameraPermission(this, REQUEST_CAMERA)) {
|
||||
openCamera()
|
||||
@@ -128,6 +137,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
private fun openCamera() {
|
||||
cameraView.openAsync(
|
||||
CameraView.findCameraId(
|
||||
@Suppress("DEPRECATION")
|
||||
if (frontFacing) {
|
||||
Camera.CameraInfo.CAMERA_FACING_FRONT
|
||||
} else {
|
||||
@@ -144,6 +154,8 @@ class CameraActivity : AppCompatActivity() {
|
||||
|
||||
private fun closeCamera() {
|
||||
cameraView.close()
|
||||
// closing the camera will also shut off the flash
|
||||
flash = false
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedState: Bundle) {
|
||||
@@ -208,13 +220,15 @@ class CameraActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun openReadme() {
|
||||
startActivity(
|
||||
Intent(
|
||||
Intent.ACTION_VIEW, Uri.parse(
|
||||
"https://github.com/markusfisch/BinaryEye/blob/master/README.md"
|
||||
)
|
||||
)
|
||||
val intent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(getString(R.string.project_url))
|
||||
)
|
||||
if (intent.resolveActivity(packageManager) != null) {
|
||||
startActivity(intent)
|
||||
} else {
|
||||
toast(R.string.project_url)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSendText(intent: Intent) {
|
||||
@@ -227,7 +241,50 @@ class CameraActivity : AppCompatActivity() {
|
||||
|
||||
private fun initCameraView() {
|
||||
cameraView.setUseOrientationListener(true)
|
||||
cameraView.setTapToFocus()
|
||||
@Suppress("ClickableViewAccessibility")
|
||||
cameraView.setOnTouchListener(object : View.OnTouchListener {
|
||||
var focus = true
|
||||
var offset = -1f
|
||||
var progress = 0
|
||||
|
||||
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
|
||||
event ?: return false
|
||||
val pos = event.y
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
offset = pos
|
||||
progress = zoomBar.progress
|
||||
return true
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (prefs.zoomBySwiping) {
|
||||
v ?: return false
|
||||
val dist = offset - pos
|
||||
val maxValue = zoomBar.max
|
||||
val change = maxValue / v.height.toFloat() * 2f * dist
|
||||
zoomBar.progress = min(
|
||||
maxValue,
|
||||
max(progress + change.roundToInt(), 0)
|
||||
)
|
||||
return true
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
// stop calling focusTo() as soon as it returns false
|
||||
// to avoid throwing and catching future exceptions
|
||||
if (focus) {
|
||||
focus = cameraView.focusTo(v, event.x, event.y)
|
||||
if (focus) {
|
||||
v?.performClick()
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
})
|
||||
@Suppress("DEPRECATION")
|
||||
cameraView.setOnCameraListener(object : CameraView.OnCameraListener {
|
||||
override fun onConfigureParameters(
|
||||
parameters: Camera.Parameters
|
||||
@@ -257,11 +314,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onCameraError() {
|
||||
Toast.makeText(
|
||||
this@CameraActivity,
|
||||
R.string.camera_error,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
this@CameraActivity.toast(R.string.camera_error)
|
||||
finish()
|
||||
}
|
||||
|
||||
@@ -269,7 +322,6 @@ class CameraActivity : AppCompatActivity() {
|
||||
val frameWidth = cameraView.frameWidth
|
||||
val frameHeight = cameraView.frameHeight
|
||||
val frameOrientation = cameraView.frameOrientation
|
||||
var mapping: Mapping? = null
|
||||
var decoding = true
|
||||
camera.setPreviewCallback { frameData, _ ->
|
||||
if (decoding) {
|
||||
@@ -280,20 +332,13 @@ class CameraActivity : AppCompatActivity() {
|
||||
frameOrientation
|
||||
)
|
||||
result?.let {
|
||||
preprocessor?.let {
|
||||
mapping = mapping ?: frameToView(
|
||||
it.outWidth,
|
||||
it.outHeight,
|
||||
cameraView.previewRect
|
||||
)
|
||||
}
|
||||
cameraView.post {
|
||||
mapping?.let {
|
||||
detectorView.mark(
|
||||
it.map(result.resultPoints)
|
||||
)
|
||||
val rp = result.resultPoints
|
||||
val m = mapping
|
||||
if (m != null && rp != null && rp.isNotEmpty()) {
|
||||
detectorView.mark(m.map(rp))
|
||||
}
|
||||
vibrator.vibrate(100)
|
||||
vibrator.vibrate()
|
||||
showResult(
|
||||
this@CameraActivity,
|
||||
result,
|
||||
@@ -332,6 +377,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
})
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun setZoom(zoom: Int) {
|
||||
val camera: Camera? = cameraView.camera
|
||||
camera?.let {
|
||||
@@ -368,6 +414,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun toggleTorchMode() {
|
||||
val camera = cameraView.camera
|
||||
val parameters = camera?.parameters
|
||||
@@ -394,8 +441,7 @@ class CameraActivity : AppCompatActivity() {
|
||||
): Result? {
|
||||
frameData ?: return null
|
||||
invert = invert xor true
|
||||
val pp = preprocessor ?: Preprocessor(
|
||||
this,
|
||||
val pp = preprocessor ?: createPreprocessorAndMapping(
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
frameOrientation
|
||||
@@ -410,6 +456,25 @@ class CameraActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
private fun createPreprocessorAndMapping(
|
||||
frameWidth: Int,
|
||||
frameHeight: Int,
|
||||
frameOrientation: Int
|
||||
): Preprocessor {
|
||||
val pp = Preprocessor(
|
||||
this,
|
||||
frameWidth,
|
||||
frameHeight,
|
||||
frameOrientation
|
||||
)
|
||||
mapping = frameToView(
|
||||
pp.outWidth,
|
||||
pp.outHeight,
|
||||
cameraView.previewRect
|
||||
)
|
||||
return pp
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val REQUEST_CAMERA = 1
|
||||
private const val PICK_FILE_RESULT_CODE = 1
|
||||
@@ -423,45 +488,22 @@ fun showResult(
|
||||
result: Result,
|
||||
isResult: Boolean = false
|
||||
) {
|
||||
val rawBytes = getRawBytes(result)
|
||||
if (prefs.useHistory) {
|
||||
GlobalScope.launch {
|
||||
db.insertScan(
|
||||
System.currentTimeMillis(),
|
||||
result.text,
|
||||
rawBytes,
|
||||
result.barcodeFormat.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
val scan = Scan(result)
|
||||
if (isResult) {
|
||||
activity.setResult(Activity.RESULT_OK, getReturnIntent(result))
|
||||
activity.finish()
|
||||
} else {
|
||||
if (prefs.useHistory) {
|
||||
GlobalScope.launch {
|
||||
db.insertScan(scan)
|
||||
}
|
||||
}
|
||||
activity.startActivity(
|
||||
MainActivity.getDecodeIntent(
|
||||
activity,
|
||||
result.text,
|
||||
result.barcodeFormat,
|
||||
rawBytes
|
||||
)
|
||||
MainActivity.getDecodeIntent(activity, scan)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getRawBytes(result: Result): ByteArray? {
|
||||
val metadata = result.resultMetadata ?: return null
|
||||
val segments = metadata[ResultMetadataType.BYTE_SEGMENTS] ?: return null
|
||||
var bytes = ByteArray(0)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
for (seg in segments as Iterable<ByteArray>) {
|
||||
bytes += seg
|
||||
}
|
||||
// byte segments can never be shorter than the text.
|
||||
// Zxing cuts off content prefixes like "WIFI:"
|
||||
return if (bytes.size >= result.text.length) bytes else null
|
||||
}
|
||||
|
||||
fun getReturnIntent(result: Result): Intent {
|
||||
val intent = Intent()
|
||||
intent.putExtra("SCAN_RESULT", result.text)
|
||||
@@ -472,34 +514,29 @@ fun getReturnIntent(result: Result): Intent {
|
||||
if (result.rawBytes?.isNotEmpty() == true) {
|
||||
intent.putExtra("SCAN_RESULT_BYTES", result.rawBytes)
|
||||
}
|
||||
val metadata = result.resultMetadata
|
||||
metadata?.let {
|
||||
val orientation = metadata[ResultMetadataType.UPC_EAN_EXTENSION]
|
||||
orientation?.let {
|
||||
result.resultMetadata?.let { metadata ->
|
||||
metadata[ResultMetadataType.UPC_EAN_EXTENSION]?.let {
|
||||
intent.putExtra(
|
||||
"SCAN_RESULT_ORIENTATION",
|
||||
orientation.toString()
|
||||
it.toString()
|
||||
)
|
||||
}
|
||||
val ecLevel = metadata[ResultMetadataType.ERROR_CORRECTION_LEVEL]
|
||||
ecLevel?.let {
|
||||
metadata[ResultMetadataType.ERROR_CORRECTION_LEVEL]?.let {
|
||||
intent.putExtra(
|
||||
"SCAN_RESULT_ERROR_CORRECTION_LEVEL",
|
||||
ecLevel.toString()
|
||||
it.toString()
|
||||
)
|
||||
}
|
||||
val upcEanExtension = metadata[ResultMetadataType.UPC_EAN_EXTENSION]
|
||||
upcEanExtension?.let {
|
||||
metadata[ResultMetadataType.UPC_EAN_EXTENSION]?.let {
|
||||
intent.putExtra(
|
||||
"SCAN_RESULT_UPC_EAN_EXTENSION",
|
||||
upcEanExtension.toString()
|
||||
it.toString()
|
||||
)
|
||||
}
|
||||
val segments = metadata[ResultMetadataType.BYTE_SEGMENTS]
|
||||
segments?.let {
|
||||
metadata[ResultMetadataType.BYTE_SEGMENTS]?.let {
|
||||
var i = 0
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
for (seg in segments as Iterable<ByteArray>) {
|
||||
for (seg in it as Iterable<ByteArray>) {
|
||||
intent.putExtra("SCAN_RESULT_BYTE_SEGMENTS_$i", seg)
|
||||
++i
|
||||
}
|
||||
|
||||
@@ -4,18 +4,19 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.Toolbar
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.colorSystemAndToolBars
|
||||
import de.markusfisch.android.binaryeye.app.initSystemBars
|
||||
import de.markusfisch.android.binaryeye.app.setFragment
|
||||
import de.markusfisch.android.binaryeye.app.setupInsets
|
||||
import de.markusfisch.android.binaryeye.data.Scan
|
||||
import de.markusfisch.android.binaryeye.fragment.DecodeFragment
|
||||
import de.markusfisch.android.binaryeye.fragment.EncodeFragment
|
||||
import de.markusfisch.android.binaryeye.fragment.HistoryFragment
|
||||
import de.markusfisch.android.binaryeye.fragment.PreferencesFragment
|
||||
import de.markusfisch.android.binaryeye.view.setupInsets
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
@@ -34,7 +35,10 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
initSystemBars(this)
|
||||
val toolbar = findViewById(R.id.toolbar) as Toolbar
|
||||
setupInsets(findViewById(android.R.id.content), toolbar)
|
||||
setupInsets(
|
||||
findViewById(android.R.id.content),
|
||||
toolbar
|
||||
)
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
@@ -43,27 +47,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
if (state == null) {
|
||||
setFragment(
|
||||
supportFragmentManager, when {
|
||||
intent?.hasExtra(PREFERENCES) == true ->
|
||||
PreferencesFragment()
|
||||
intent?.hasExtra(HISTORY) == true ->
|
||||
HistoryFragment()
|
||||
intent?.hasExtra(ENCODE) == true ->
|
||||
EncodeFragment.newInstance(
|
||||
intent.getStringExtra(ENCODE) ?: ""
|
||||
)
|
||||
intent?.hasExtra(DECODED_TEXT) == true ->
|
||||
DecodeFragment.newInstance(
|
||||
intent.getStringExtra(DECODED_TEXT) ?: "",
|
||||
intent.getSerializableExtra(
|
||||
DECODED_FORMAT
|
||||
) as BarcodeFormat,
|
||||
intent.getByteArrayExtra(DECODED_RAW)
|
||||
)
|
||||
else -> DecodeFragment()
|
||||
}
|
||||
)
|
||||
setFragment(supportFragmentManager, getFragmentForIntent(intent))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,9 +55,22 @@ class MainActivity : AppCompatActivity() {
|
||||
private const val PREFERENCES = "preferences"
|
||||
private const val HISTORY = "history"
|
||||
private const val ENCODE = "encode"
|
||||
private const val DECODED_TEXT = "decoded_text"
|
||||
private const val DECODED_FORMAT = "decoded_format"
|
||||
private const val DECODED_RAW = "decoded_raw"
|
||||
private const val DECODED = "decoded"
|
||||
|
||||
private fun getFragmentForIntent(intent: Intent?): Fragment {
|
||||
intent ?: return PreferencesFragment()
|
||||
return when {
|
||||
intent.hasExtra(PREFERENCES) -> PreferencesFragment()
|
||||
intent.hasExtra(HISTORY) -> HistoryFragment()
|
||||
intent.hasExtra(ENCODE) -> EncodeFragment.newInstance(
|
||||
intent.getStringExtra(ENCODE)!!
|
||||
)
|
||||
intent.hasExtra(DECODED) -> DecodeFragment.newInstance(
|
||||
intent.getParcelableExtra(DECODED)!!
|
||||
)
|
||||
else -> PreferencesFragment()
|
||||
}
|
||||
}
|
||||
|
||||
fun getPreferencesIntent(context: Context): Intent {
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
@@ -108,18 +105,9 @@ class MainActivity : AppCompatActivity() {
|
||||
return intent
|
||||
}
|
||||
|
||||
fun getDecodeIntent(
|
||||
context: Context,
|
||||
text: String,
|
||||
format: BarcodeFormat,
|
||||
raw: ByteArray? = null
|
||||
): Intent {
|
||||
fun getDecodeIntent(context: Context, scan: Scan): Intent {
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
intent.putExtra(DECODED_TEXT, text)
|
||||
intent.putExtra(DECODED_FORMAT, format)
|
||||
if (raw != null) {
|
||||
intent.putExtra(DECODED_RAW, raw)
|
||||
}
|
||||
intent.putExtra(DECODED, scan)
|
||||
return intent
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,17 +13,18 @@ import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import com.google.zxing.Result
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.colorSystemAndToolBars
|
||||
import de.markusfisch.android.binaryeye.app.initSystemBars
|
||||
import de.markusfisch.android.binaryeye.app.setWindowInsetListener
|
||||
import de.markusfisch.android.binaryeye.app.setupInsets
|
||||
import de.markusfisch.android.binaryeye.app.vibrate
|
||||
import de.markusfisch.android.binaryeye.graphics.crop
|
||||
import de.markusfisch.android.binaryeye.graphics.loadImageUri
|
||||
import de.markusfisch.android.binaryeye.graphics.mapResult
|
||||
import de.markusfisch.android.binaryeye.view.setWindowInsetListener
|
||||
import de.markusfisch.android.binaryeye.view.setupInsets
|
||||
import de.markusfisch.android.binaryeye.widget.CropImageView
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import de.markusfisch.android.binaryeye.zxing.Zxing
|
||||
|
||||
class PickActivity : AppCompatActivity() {
|
||||
@@ -36,6 +37,7 @@ class PickActivity : AppCompatActivity() {
|
||||
super.onCreate(state)
|
||||
setContentView(R.layout.activity_pick)
|
||||
|
||||
zxing.updateHints(true)
|
||||
vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
|
||||
initSystemBars(this)
|
||||
@@ -62,11 +64,7 @@ class PickActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
if (bitmap == null) {
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
R.string.error_no_content,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
applicationContext.toast(R.string.error_no_content)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
@@ -96,7 +94,7 @@ class PickActivity : AppCompatActivity() {
|
||||
cropImageView.getBoundsRect(),
|
||||
it
|
||||
)
|
||||
vibrator.vibrate(100)
|
||||
vibrator.vibrate()
|
||||
}
|
||||
points
|
||||
}
|
||||
@@ -146,10 +144,6 @@ class PickActivity : AppCompatActivity() {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
R.string.no_barcode_found,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
applicationContext.toast(R.string.no_barcode_found)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package de.markusfisch.android.binaryeye.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -12,9 +13,25 @@ import de.markusfisch.android.binaryeye.data.Database
|
||||
|
||||
class ScansAdapter(context: Context, cursor: Cursor) :
|
||||
CursorAdapter(context, cursor, false) {
|
||||
var selectedScanId = 0L
|
||||
var selectedScanPosition = -1
|
||||
|
||||
private val idIndex = cursor.getColumnIndex(Database.SCANS_ID)
|
||||
private val timeIndex = cursor.getColumnIndex(Database.SCANS_DATETIME)
|
||||
private val nameIndex = cursor.getColumnIndex(Database.SCANS_NAME)
|
||||
private val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
|
||||
private val formatIndex = cursor.getColumnIndex(Database.SCANS_FORMAT)
|
||||
private val selectedColor = ContextCompat.getColor(context, R.color.selected_row)
|
||||
|
||||
fun select(id: Long, position: Int) {
|
||||
selectedScanId = id
|
||||
selectedScanPosition = position
|
||||
}
|
||||
|
||||
fun clearSelection() {
|
||||
selectedScanId = 0L
|
||||
selectedScanPosition = -1
|
||||
}
|
||||
|
||||
override fun newView(
|
||||
context: Context,
|
||||
@@ -30,14 +47,27 @@ class ScansAdapter(context: Context, cursor: Cursor) :
|
||||
cursor: Cursor
|
||||
) {
|
||||
val holder = getViewHolder(view)
|
||||
val content = cursor.getString(contentIndex)
|
||||
holder.timeView.text = cursor.getString(timeIndex)
|
||||
holder.contentView.text = if (content.isEmpty()) {
|
||||
context.getString(R.string.binary_data)
|
||||
} else {
|
||||
content
|
||||
val name = cursor.getString(nameIndex)
|
||||
val content = cursor.getString(contentIndex)
|
||||
var icon = 0
|
||||
holder.contentView.text = when {
|
||||
name?.isNotEmpty() == true -> {
|
||||
icon = R.drawable.ic_label
|
||||
name
|
||||
}
|
||||
content?.isEmpty() == true -> context.getString(R.string.binary_data)
|
||||
else -> content
|
||||
}
|
||||
holder.contentView.setCompoundDrawablesWithIntrinsicBounds(
|
||||
icon, 0, 0, 0
|
||||
)
|
||||
holder.formatView.text = cursor.getString(formatIndex)
|
||||
// view.isSelected needs to be put on the queue to work
|
||||
val selected = cursor.getLong(idIndex) == selectedScanId
|
||||
view.post {
|
||||
view.isSelected = selected
|
||||
}
|
||||
}
|
||||
|
||||
private fun getViewHolder(view: View): ViewHolder {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.content.Context
|
||||
import android.support.v7.app.AlertDialog
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
suspend inline fun <T : Any> alertDialog(
|
||||
context: Context,
|
||||
crossinline build: AlertDialog.Builder.(resume: (T?) -> Unit) -> Unit
|
||||
): T? = withContext(Dispatchers.Main) {
|
||||
suspendCoroutine<T?> { continuation ->
|
||||
AlertDialog.Builder(context).apply {
|
||||
setOnCancelListener {
|
||||
continuation.resume(null)
|
||||
}
|
||||
build(continuation::resume)
|
||||
show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,25 +3,26 @@ package de.markusfisch.android.binaryeye.app
|
||||
import android.app.Application
|
||||
import android.support.v8.renderscript.RenderScript
|
||||
import de.markusfisch.android.binaryeye.BuildConfig
|
||||
import de.markusfisch.android.binaryeye.data.Database
|
||||
import de.markusfisch.android.binaryeye.preference.Preferences
|
||||
import de.markusfisch.android.binaryeye.repository.DatabaseRepository
|
||||
|
||||
val db = DatabaseRepository()
|
||||
val db = Database()
|
||||
val prefs = Preferences()
|
||||
|
||||
class BinaryEyeApp : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
// Since RenderScript can no longer be compiled on macOS Catalina
|
||||
// because Catalina cannot run 32bit binaries and Google failed
|
||||
// to provide a 64bit compiler in time. Now, as if that wasn't
|
||||
// bad enough, `RenderScript.forceCompat()` makes any build compiled
|
||||
// with the new build tools that provide a 64bit compiler crash
|
||||
// so we need to exclude this if we're building on Catalina.
|
||||
// Maybe `RenderScript.forceCompat()` isn't required anymore for
|
||||
// those new builds but I don't have a device to test this.
|
||||
if (!BuildConfig.IS_CATALINA) {
|
||||
// `RenderScript.forceCompat()` will crash the app when compiled
|
||||
// on macOS Catalina with build tools 29.0.3 :( To make things
|
||||
// even more complicated, `forceCompat()` will continue to work
|
||||
// if the app is compiled on Linux with the exact same version
|
||||
// 29.0.3 of the build tools
|
||||
if (!BuildConfig.IS_CATALINA &&
|
||||
System.getProperty("os.version")?.contains(
|
||||
"lineageos", true
|
||||
) == true
|
||||
) {
|
||||
// required to make RenderScript work for Lineage 16.0
|
||||
// possibly because of a system/device bug
|
||||
RenderScript.forceCompat()
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
|
||||
fun Context.copyToClipboard(text: String) {
|
||||
val service = getSystemService(Context.CLIPBOARD_SERVICE)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
|
||||
@Suppress("DEPRECATION")
|
||||
(service as android.text.ClipboardManager).text = text
|
||||
} else {
|
||||
(service as android.content.ClipboardManager).setPrimaryClip(
|
||||
ClipData.newPlainText("simple text", text)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,9 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.app.FragmentManager
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.view.View
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
fun setFragment(fm: FragmentManager?, fragment: Fragment) {
|
||||
fm?.let { getTransaction(fm, fragment).commit() }
|
||||
@@ -25,38 +18,3 @@ private fun getTransaction(
|
||||
fm: FragmentManager,
|
||||
fragment: Fragment
|
||||
) = fm.beginTransaction().replace(R.id.content_frame, fragment)
|
||||
|
||||
suspend inline fun View.useVisibility(
|
||||
whileExecuting: Int = View.VISIBLE,
|
||||
otherwise: Int = View.GONE,
|
||||
crossinline block: suspend () -> Unit
|
||||
) {
|
||||
if (visibility == whileExecuting) {
|
||||
return
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
visibility = whileExecuting
|
||||
}
|
||||
try {
|
||||
block()
|
||||
} finally {
|
||||
withContext(Dispatchers.Main) {
|
||||
visibility = otherwise
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend inline fun <T : Any> alertDialog(
|
||||
context: Context,
|
||||
crossinline build: AlertDialog.Builder.(resume: (T?) -> Unit) -> Unit
|
||||
): T? = withContext(Dispatchers.Main) {
|
||||
suspendCoroutine<T?> { continuation ->
|
||||
AlertDialog.Builder(context).apply {
|
||||
setOnCancelListener {
|
||||
continuation.resume(null)
|
||||
}
|
||||
build(continuation::resume)
|
||||
show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.support.annotation.MainThread
|
||||
import android.widget.EditText
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
|
||||
fun addSuffixIfNotGiven(name: String, suffix: String): String {
|
||||
val trimmed = name.trim()
|
||||
@@ -36,22 +39,53 @@ suspend fun Activity.askForFileName(suffix: String = ""): String? {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun Flow<ByteArray>.writeToFile(name: String): Int {
|
||||
return try {
|
||||
val file = File(
|
||||
Environment.getExternalStoragePublicDirectory(
|
||||
Environment.DIRECTORY_DOWNLOADS
|
||||
),
|
||||
name
|
||||
)
|
||||
if (file.exists()) {
|
||||
return R.string.error_file_exists
|
||||
fun writeExternalFile(
|
||||
context: Context,
|
||||
fileName: String,
|
||||
mimeType: String,
|
||||
write: (outputStream: OutputStream) -> Any
|
||||
): Int = try {
|
||||
val outputStream = getExternalOutputStream(
|
||||
context,
|
||||
fileName,
|
||||
mimeType
|
||||
) ?: throw IOException()
|
||||
outputStream.use { write(it) }
|
||||
R.string.saved_in_downloads
|
||||
} catch (e: FileAlreadyExistsException) {
|
||||
R.string.error_file_exists
|
||||
} catch (e: IOException) {
|
||||
R.string.error_saving_binary_data
|
||||
}
|
||||
|
||||
private fun getExternalOutputStream(
|
||||
context: Context,
|
||||
fileName: String,
|
||||
mimeType: String
|
||||
): OutputStream? = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
@Suppress("DEPRECATION")
|
||||
val file = File(
|
||||
Environment.getExternalStoragePublicDirectory(
|
||||
Environment.DIRECTORY_DOWNLOADS
|
||||
),
|
||||
fileName
|
||||
)
|
||||
if (file.exists()) {
|
||||
throw FileAlreadyExistsException(file)
|
||||
}
|
||||
FileOutputStream(file)
|
||||
} else {
|
||||
val resolver = context.contentResolver
|
||||
val uri = resolver.insert(
|
||||
MediaStore.Downloads.EXTERNAL_CONTENT_URI,
|
||||
ContentValues().apply {
|
||||
put(MediaStore.Downloads.DISPLAY_NAME, fileName)
|
||||
put(MediaStore.Downloads.MIME_TYPE, mimeType)
|
||||
}
|
||||
collect { bytes ->
|
||||
file.appendBytes(bytes)
|
||||
}
|
||||
R.string.saved_in_downloads
|
||||
} catch (e: IOException) {
|
||||
R.string.error_saving_binary_data
|
||||
)
|
||||
if (uri != null) {
|
||||
resolver.openOutputStream(uri)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.scanReduce
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
fun Flow<ByteArray>.addDelimiter(delimiter: String) = addDelimiter(
|
||||
delimiter.toByteArray(),
|
||||
ByteArray::plus
|
||||
)
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
inline fun <T, R> Flow<T>.addDelimiter(
|
||||
delimiter: R,
|
||||
crossinline append: R.(T) -> T
|
||||
@@ -17,6 +20,7 @@ inline fun <T, R> Flow<T>.addDelimiter(
|
||||
delimiter.append(content)
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
infix fun <T> T.asFlowWith(other: Flow<T>) = flow<T> {
|
||||
emit(this@asFlowWith)
|
||||
emitAll(other)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
|
||||
fun Context.hideSoftKeyboard(view: View) {
|
||||
(getSystemService(
|
||||
Context.INPUT_METHOD_SERVICE
|
||||
) as InputMethodManager).hideSoftInputFromWindow(
|
||||
view.windowToken, 0
|
||||
)
|
||||
}
|
||||
@@ -6,11 +6,11 @@ import android.content.pm.PackageManager
|
||||
import android.support.v4.app.ActivityCompat
|
||||
import android.support.v4.content.ContextCompat
|
||||
|
||||
fun hasCameraPermission(activity: Activity, requestCode: Int): Boolean {
|
||||
fun hasCameraPermission(activity: Activity, requestCode: Int = 1): Boolean {
|
||||
return hasPermission(activity, Manifest.permission.CAMERA, requestCode)
|
||||
}
|
||||
|
||||
fun hasWritePermission(activity: Activity, requestCode: Int = 1): Boolean {
|
||||
fun hasWritePermission(activity: Activity, requestCode: Int = 2): Boolean {
|
||||
return hasPermission(
|
||||
activity,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
@@ -18,6 +18,14 @@ fun hasWritePermission(activity: Activity, requestCode: Int = 1): Boolean {
|
||||
)
|
||||
}
|
||||
|
||||
fun hasLocationPermission(activity: Activity, requestCode: Int = 3): Boolean {
|
||||
return hasPermission(
|
||||
activity,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
requestCode
|
||||
)
|
||||
}
|
||||
|
||||
fun hasPermission(
|
||||
activity: Activity,
|
||||
permission: String,
|
||||
|
||||
@@ -5,9 +5,9 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.support.v4.content.FileProvider
|
||||
import android.widget.Toast
|
||||
import de.markusfisch.android.binaryeye.BuildConfig
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import java.io.File
|
||||
|
||||
fun shareText(context: Context, text: String, type: String = "text/plain") {
|
||||
@@ -29,11 +29,7 @@ fun execShareIntent(context: Context, intent: Intent) {
|
||||
if (intent.resolveActivity(context.packageManager) != null) {
|
||||
context.startActivity(intent)
|
||||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.cannot_resolve_action,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
context.toast(R.string.cannot_resolve_action)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,10 @@ package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Build
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v4.view.ViewCompat
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.view.View
|
||||
import android.widget.AbsListView
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
@@ -20,13 +17,16 @@ val systemBarScrollListener = object : AbsListView.OnScrollListener {
|
||||
visibleItemCount: Int,
|
||||
totalItemCount: Int
|
||||
) {
|
||||
// give Android some time to settle down before running this;
|
||||
// give Android some time to settle down before running this,
|
||||
// not putting it on the queue makes it only work sometimes
|
||||
view.post {
|
||||
val scrolled = firstVisibleItem > 0 ||
|
||||
(totalItemCount > 0 && view.getChildAt(0).top < 0)
|
||||
val scrollable = if (scrolled) true else totalItemCount > 0 &&
|
||||
view.getChildAt(view.lastVisiblePosition).bottom >= view.height
|
||||
(totalItemCount > 0 && firstChildScrolled(view))
|
||||
val scrollable = if (scrolled) {
|
||||
true
|
||||
} else {
|
||||
totalItemCount > 0 && lastChildOutOfView(view)
|
||||
}
|
||||
colorSystemAndToolBars(view.context, scrolled, scrollable)
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,16 @@ val systemBarScrollListener = object : AbsListView.OnScrollListener {
|
||||
}
|
||||
}
|
||||
|
||||
private fun firstChildScrolled(listView: AbsListView): Boolean {
|
||||
val child = listView.getChildAt(0)
|
||||
return child != null && child.top < listView.paddingTop
|
||||
}
|
||||
|
||||
private fun lastChildOutOfView(listView: AbsListView): Boolean {
|
||||
val child = listView.getChildAt(listView.lastVisiblePosition)
|
||||
return child.bottom >= listView.height
|
||||
}
|
||||
|
||||
fun initSystemBars(activity: AppCompatActivity) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
activity.window.decorView.systemUiVisibility =
|
||||
@@ -48,28 +58,53 @@ fun initSystemBars(activity: AppCompatActivity) {
|
||||
colorSystemAndToolBars(activity)
|
||||
}
|
||||
|
||||
private var statusBarColorLocked = false
|
||||
fun lockStatusBarColor() {
|
||||
statusBarColorLocked = true
|
||||
}
|
||||
|
||||
fun unlockStatusBarColor() {
|
||||
statusBarColorLocked = false
|
||||
}
|
||||
|
||||
private var translucentPrimaryColor = 0
|
||||
private val actionBarBackground = ColorDrawable()
|
||||
fun colorSystemAndToolBars(
|
||||
context: Context,
|
||||
scrolled: Boolean = false,
|
||||
scrollable: Boolean = false
|
||||
) {
|
||||
val translucentColor = getTranslucentPrimaryColor(context)
|
||||
val topColor = if (scrolled) translucentColor else 0
|
||||
val bottomColor = if (scrolled || scrollable) translucentColor else 0
|
||||
if (translucentPrimaryColor == 0) {
|
||||
translucentPrimaryColor = ContextCompat.getColor(
|
||||
context,
|
||||
R.color.primary_translucent
|
||||
)
|
||||
}
|
||||
val topColor = if (scrolled) translucentPrimaryColor else 0
|
||||
val activity = getAppCompatActivity(context) ?: return
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
val window = activity.window
|
||||
window.statusBarColor = topColor
|
||||
window.navigationBarColor = bottomColor
|
||||
if (!statusBarColorLocked) {
|
||||
window.statusBarColor = topColor
|
||||
}
|
||||
window.navigationBarColor = if (scrolled || scrollable) {
|
||||
translucentPrimaryColor
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
activity.supportActionBar?.setBackgroundDrawable(ColorDrawable(topColor))
|
||||
activity.supportActionBar?.setBackgroundDrawable(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
||||
// avoid allocation on Honeycomb and better
|
||||
actionBarBackground.color = topColor
|
||||
actionBarBackground
|
||||
} else {
|
||||
// ColorDrawable.setColor() doesn't exist pre Honeycomb
|
||||
ColorDrawable(topColor)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun getTranslucentPrimaryColor(context: Context) = ContextCompat.getColor(
|
||||
context,
|
||||
R.color.primary
|
||||
) and 0xffffff or 0xd8000000.toInt()
|
||||
|
||||
private fun getAppCompatActivity(context: Context): AppCompatActivity? {
|
||||
var ctx = context
|
||||
while (ctx is ContextWrapper) {
|
||||
@@ -80,39 +115,3 @@ private fun getAppCompatActivity(context: Context): AppCompatActivity? {
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private var windowInsetsListener: ((insets: Rect) -> Unit)? = null
|
||||
// invoke this from Fragment.onCreateView() *or* Activity.onCreate()
|
||||
fun setWindowInsetListener(listener: (insets: Rect) -> Unit) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
listener(windowInsets)
|
||||
} else {
|
||||
windowInsetsListener = listener
|
||||
}
|
||||
}
|
||||
|
||||
private val windowInsets = Rect()
|
||||
// invoke this from Activity.onCreate() to setup listening for insets
|
||||
fun setupInsets(view: View, toolbar: Toolbar) {
|
||||
val toolBarHeight = toolbar.layoutParams.height
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
// save insets so setWindowInsetListener() can return them
|
||||
// when it's called from Fragment.onCreateView() later on
|
||||
windowInsets.set(0, toolBarHeight, 0, 0)
|
||||
windowInsetsListener?.also { it(windowInsets) }
|
||||
return
|
||||
}
|
||||
ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets ->
|
||||
// runs *after* layout and thus *after* Fragment.onCreateView()
|
||||
if (insets.hasSystemWindowInsets()) {
|
||||
windowInsets.set(
|
||||
insets.systemWindowInsetLeft,
|
||||
insets.systemWindowInsetTop + toolBarHeight,
|
||||
insets.systemWindowInsetRight,
|
||||
insets.systemWindowInsetBottom
|
||||
)
|
||||
windowInsetsListener?.also { it(windowInsets) }
|
||||
}
|
||||
insets
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package de.markusfisch.android.binaryeye.app
|
||||
|
||||
import android.os.Build
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
|
||||
private const val MILLISECONDS = 100L
|
||||
|
||||
fun Vibrator.vibrate() {
|
||||
if (!prefs.vibrate) {
|
||||
return
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
@Suppress("DEPRECATION")
|
||||
this.vibrate(MILLISECONDS)
|
||||
} else {
|
||||
this.vibrate(
|
||||
VibrationEffect.createOneShot(
|
||||
MILLISECONDS,
|
||||
VibrationEffect.DEFAULT_AMPLITUDE
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,8 @@ import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import android.text.format.DateFormat
|
||||
import de.markusfisch.android.binaryeye.app.hasNonPrintableCharacters
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
|
||||
class Database {
|
||||
private lateinit var db: SQLiteDatabase
|
||||
@@ -15,58 +15,158 @@ class Database {
|
||||
db = OpenHelper(context).writableDatabase
|
||||
}
|
||||
|
||||
fun getScans(): Cursor? = db.rawQuery(
|
||||
fun getScans(query: String? = null): Cursor? = db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID,
|
||||
$SCANS_DATETIME,
|
||||
$SCANS_NAME,
|
||||
$SCANS_CONTENT,
|
||||
$SCANS_FORMAT
|
||||
FROM $SCANS
|
||||
${getWhereClause(query)}
|
||||
ORDER BY $SCANS_DATETIME DESC
|
||||
""", null
|
||||
""", getWhereArguments(query)
|
||||
)
|
||||
|
||||
fun getScan(id: Long): Cursor? = db.rawQuery(
|
||||
fun getScansDetailed(query: String? = null): Cursor? = db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID,
|
||||
$SCANS_DATETIME,
|
||||
$SCANS_NAME,
|
||||
$SCANS_CONTENT,
|
||||
$SCANS_RAW,
|
||||
$SCANS_FORMAT
|
||||
$SCANS_FORMAT,
|
||||
$SCANS_ERROR_CORRECTION_LEVEL,
|
||||
$SCANS_ISSUE_NUMBER,
|
||||
$SCANS_ORIENTATION,
|
||||
$SCANS_OTHER_META_DATA,
|
||||
$SCANS_PDF417_EXTRA_METADATA,
|
||||
$SCANS_POSSIBLE_COUNTRY,
|
||||
$SCANS_SUGGESTED_PRICE,
|
||||
$SCANS_UPC_EAN_EXTENSION
|
||||
FROM $SCANS
|
||||
${getWhereClause(query)}
|
||||
ORDER BY $SCANS_DATETIME DESC
|
||||
""", getWhereArguments(query)
|
||||
)
|
||||
|
||||
private fun getWhereClause(query: String?) = if (query?.isNotEmpty() == true) {
|
||||
"""WHERE $SCANS_CONTENT LIKE ?
|
||||
OR $SCANS_NAME LIKE ?"""
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
private fun getWhereArguments(query: String?) = if (query?.isNotEmpty() == true) {
|
||||
val instr = "%$query%"
|
||||
arrayOf(instr, instr)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
fun getScan(id: Long): Scan? = db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID,
|
||||
$SCANS_DATETIME,
|
||||
$SCANS_NAME,
|
||||
$SCANS_CONTENT,
|
||||
$SCANS_RAW,
|
||||
$SCANS_FORMAT,
|
||||
$SCANS_ERROR_CORRECTION_LEVEL,
|
||||
$SCANS_ISSUE_NUMBER,
|
||||
$SCANS_ORIENTATION,
|
||||
$SCANS_OTHER_META_DATA,
|
||||
$SCANS_PDF417_EXTRA_METADATA,
|
||||
$SCANS_POSSIBLE_COUNTRY,
|
||||
$SCANS_SUGGESTED_PRICE,
|
||||
$SCANS_UPC_EAN_EXTENSION
|
||||
FROM $SCANS
|
||||
WHERE $SCANS_ID = ?
|
||||
""", arrayOf("$id")
|
||||
)
|
||||
)?.use {
|
||||
if (it.moveToFirst()) {
|
||||
Scan(
|
||||
it.getString(SCANS_CONTENT),
|
||||
it.getBlob(SCANS_RAW),
|
||||
it.getString(SCANS_FORMAT),
|
||||
it.getString(SCANS_ERROR_CORRECTION_LEVEL),
|
||||
it.getString(SCANS_ISSUE_NUMBER),
|
||||
it.getString(SCANS_ORIENTATION),
|
||||
it.getString(SCANS_OTHER_META_DATA),
|
||||
it.getString(SCANS_PDF417_EXTRA_METADATA),
|
||||
it.getString(SCANS_POSSIBLE_COUNTRY),
|
||||
it.getString(SCANS_SUGGESTED_PRICE),
|
||||
it.getString(SCANS_UPC_EAN_EXTENSION),
|
||||
it.getString(SCANS_DATETIME),
|
||||
it.getLong(SCANS_ID)
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun hasBinaryData(): Cursor? = db.rawQuery(
|
||||
"""SELECT
|
||||
1
|
||||
FROM $SCANS
|
||||
WHERE $SCANS_RAW IS NOT NULL
|
||||
LIMIT 1
|
||||
""", null
|
||||
)
|
||||
fun insertScan(scan: Scan): Long {
|
||||
val cv = ContentValues()
|
||||
cv.put(SCANS_DATETIME, scan.timestamp)
|
||||
val isRaw = hasNonPrintableCharacters(scan.content)
|
||||
if (isRaw) {
|
||||
cv.put(SCANS_CONTENT, "")
|
||||
cv.put(SCANS_RAW, scan.raw ?: scan.content.toByteArray())
|
||||
} else {
|
||||
cv.put(SCANS_CONTENT, scan.content)
|
||||
}
|
||||
cv.put(SCANS_FORMAT, scan.format)
|
||||
scan.errorCorrectionLevel?.let {
|
||||
cv.put(SCANS_ERROR_CORRECTION_LEVEL, it)
|
||||
}
|
||||
scan.issueNumber?.let { cv.put(SCANS_ISSUE_NUMBER, it) }
|
||||
scan.orientation?.let { cv.put(SCANS_ORIENTATION, it) }
|
||||
scan.otherMetaData?.let { cv.put(SCANS_OTHER_META_DATA, it) }
|
||||
scan.pdf417ExtraMetaData?.let { cv.put(SCANS_PDF417_EXTRA_METADATA, it) }
|
||||
scan.possibleCountry?.let { cv.put(SCANS_POSSIBLE_COUNTRY, it) }
|
||||
scan.suggestedPrice?.let { cv.put(SCANS_SUGGESTED_PRICE, it) }
|
||||
scan.upcEanExtension?.let { cv.put(SCANS_UPC_EAN_EXTENSION, it) }
|
||||
if (prefs.ignoreConsecutiveDuplicates) {
|
||||
val id = getIdOfLastScan(
|
||||
cv.get(SCANS_CONTENT) as String,
|
||||
if (isRaw) cv.get(SCANS_RAW) as ByteArray else null,
|
||||
scan.format
|
||||
)
|
||||
if (id > 0L) {
|
||||
return id
|
||||
}
|
||||
}
|
||||
return db.insert(SCANS, null, cv)
|
||||
}
|
||||
|
||||
fun insertScan(
|
||||
timestamp: Long,
|
||||
private fun getIdOfLastScan(
|
||||
content: String,
|
||||
raw: ByteArray?,
|
||||
format: String
|
||||
): Long {
|
||||
val cv = ContentValues()
|
||||
cv.put(
|
||||
SCANS_DATETIME, DateFormat.format(
|
||||
"yyyy-MM-dd HH:mm:ss",
|
||||
timestamp
|
||||
).toString()
|
||||
)
|
||||
if (hasNonPrintableCharacters(content)) {
|
||||
cv.put(SCANS_CONTENT, "")
|
||||
cv.put(SCANS_RAW, if (raw != null) raw else content.toByteArray())
|
||||
} else {
|
||||
cv.put(SCANS_CONTENT, content)
|
||||
}
|
||||
cv.put(SCANS_FORMAT, format)
|
||||
return db.insert(SCANS, null, cv)
|
||||
return db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID,
|
||||
$SCANS_CONTENT,
|
||||
$SCANS_RAW,
|
||||
$SCANS_FORMAT
|
||||
FROM $SCANS
|
||||
ORDER BY $SCANS_ID DESC
|
||||
LIMIT 1
|
||||
""", null
|
||||
)?.use {
|
||||
if (it.count > 0 &&
|
||||
it.moveToFirst() &&
|
||||
it.getString(it.getColumnIndex(SCANS_CONTENT)) == content &&
|
||||
(raw == null || it.getBlob(it.getColumnIndex(SCANS_RAW))
|
||||
?.contentEquals(raw) == true) &&
|
||||
it.getString(it.getColumnIndex(SCANS_FORMAT)) == format
|
||||
) {
|
||||
it.getLong(it.getColumnIndex(SCANS_ID))
|
||||
} else {
|
||||
0L
|
||||
}
|
||||
} ?: 0L
|
||||
}
|
||||
|
||||
fun removeScan(id: Long) {
|
||||
@@ -77,8 +177,14 @@ class Database {
|
||||
db.delete(SCANS, null, null)
|
||||
}
|
||||
|
||||
fun renameScan(id: Long, name: String) {
|
||||
val cv = ContentValues()
|
||||
cv.put(SCANS_NAME, name)
|
||||
db.update(SCANS, cv, "$SCANS_ID = ?", arrayOf("$id"))
|
||||
}
|
||||
|
||||
private class OpenHelper(context: Context) :
|
||||
SQLiteOpenHelper(context, "history.db", null, 2) {
|
||||
SQLiteOpenHelper(context, "history.db", null, 4) {
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
createScans(db)
|
||||
}
|
||||
@@ -91,6 +197,12 @@ class Database {
|
||||
if (oldVersion < 2) {
|
||||
addRawColumn(db)
|
||||
}
|
||||
if (oldVersion < 3) {
|
||||
addMetaDataColumns(db)
|
||||
}
|
||||
if (oldVersion < 4) {
|
||||
addNameColumn(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,9 +210,18 @@ class Database {
|
||||
const val SCANS = "scans"
|
||||
const val SCANS_ID = "_id"
|
||||
const val SCANS_DATETIME = "_datetime"
|
||||
const val SCANS_NAME = "name"
|
||||
const val SCANS_CONTENT = "content"
|
||||
const val SCANS_RAW = "raw"
|
||||
const val SCANS_FORMAT = "format"
|
||||
const val SCANS_ERROR_CORRECTION_LEVEL = "error_correction_level"
|
||||
const val SCANS_ISSUE_NUMBER = "issue_number"
|
||||
const val SCANS_ORIENTATION = "orientation"
|
||||
const val SCANS_OTHER_META_DATA = "other_meta_data"
|
||||
const val SCANS_PDF417_EXTRA_METADATA = "pdf417_extra_metadata"
|
||||
const val SCANS_POSSIBLE_COUNTRY = "possible_country"
|
||||
const val SCANS_SUGGESTED_PRICE = "suggested_price"
|
||||
const val SCANS_UPC_EAN_EXTENSION = "upc_ean_extension"
|
||||
|
||||
private fun createScans(db: SQLiteDatabase) {
|
||||
db.execSQL("DROP TABLE IF EXISTS $SCANS")
|
||||
@@ -108,9 +229,18 @@ class Database {
|
||||
"""CREATE TABLE $SCANS (
|
||||
$SCANS_ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
$SCANS_DATETIME DATETIME NOT NULL,
|
||||
$SCANS_NAME TEXT,
|
||||
$SCANS_CONTENT TEXT NOT NULL,
|
||||
$SCANS_RAW BLOB,
|
||||
$SCANS_FORMAT TEXT NOT NULL
|
||||
$SCANS_FORMAT TEXT NOT NULL,
|
||||
$SCANS_ERROR_CORRECTION_LEVEL TEXT,
|
||||
$SCANS_ISSUE_NUMBER INT,
|
||||
$SCANS_ORIENTATION INT,
|
||||
$SCANS_OTHER_META_DATA TEXT,
|
||||
$SCANS_PDF417_EXTRA_METADATA TEXT,
|
||||
$SCANS_POSSIBLE_COUNTRY TEXT,
|
||||
$SCANS_SUGGESTED_PRICE TEXT,
|
||||
$SCANS_UPC_EAN_EXTENSION TEXT
|
||||
)"""
|
||||
)
|
||||
}
|
||||
@@ -118,5 +248,32 @@ class Database {
|
||||
private fun addRawColumn(db: SQLiteDatabase) {
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_RAW BLOB")
|
||||
}
|
||||
|
||||
private fun addMetaDataColumns(db: SQLiteDatabase) {
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ERROR_CORRECTION_LEVEL TEXT")
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ISSUE_NUMBER INT")
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_ORIENTATION INT")
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_OTHER_META_DATA TEXT")
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_PDF417_EXTRA_METADATA TEXT")
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_POSSIBLE_COUNTRY TEXT")
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_SUGGESTED_PRICE TEXT")
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_UPC_EAN_EXTENSION TEXT")
|
||||
}
|
||||
|
||||
private fun addNameColumn(db: SQLiteDatabase) {
|
||||
db.execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_NAME TEXT")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Cursor.getString(name: String) = this.getString(
|
||||
this.getColumnIndex(name)
|
||||
)
|
||||
|
||||
private fun Cursor.getBlob(name: String) = this.getBlob(
|
||||
this.getColumnIndex(name)
|
||||
)
|
||||
|
||||
private fun Cursor.getLong(name: String) = this.getLong(
|
||||
this.getColumnIndex(name)
|
||||
)
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package de.markusfisch.android.binaryeye.data
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import de.markusfisch.android.binaryeye.app.writeExternalFile
|
||||
|
||||
fun exportCsv(
|
||||
context: Context,
|
||||
name: String,
|
||||
cursor: Cursor,
|
||||
delimiter: String
|
||||
) {
|
||||
if (!cursor.moveToFirst()) {
|
||||
return
|
||||
}
|
||||
val columns = arrayOf(
|
||||
Database.SCANS_DATETIME,
|
||||
Database.SCANS_FORMAT,
|
||||
Database.SCANS_CONTENT,
|
||||
Database.SCANS_ERROR_CORRECTION_LEVEL,
|
||||
Database.SCANS_ISSUE_NUMBER,
|
||||
Database.SCANS_ORIENTATION,
|
||||
Database.SCANS_OTHER_META_DATA,
|
||||
Database.SCANS_PDF417_EXTRA_METADATA,
|
||||
Database.SCANS_POSSIBLE_COUNTRY,
|
||||
Database.SCANS_SUGGESTED_PRICE,
|
||||
Database.SCANS_UPC_EAN_EXTENSION
|
||||
)
|
||||
val indices = columns.map {
|
||||
cursor.getColumnIndex(it)
|
||||
}
|
||||
val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
|
||||
val rawIndex = cursor.getColumnIndex(Database.SCANS_RAW)
|
||||
writeExternalFile(context, name, "text/csv") { outputStream ->
|
||||
outputStream.write(
|
||||
columns.joinToString(
|
||||
delimiter,
|
||||
postfix = "\n"
|
||||
).toByteArray()
|
||||
)
|
||||
do {
|
||||
var deviation: Pair<Int, String>? = null
|
||||
if (cursor.getString(contentIndex)?.isEmpty() == true) {
|
||||
deviation = Pair(
|
||||
contentIndex,
|
||||
cursor.getBlob(rawIndex).toHexString()
|
||||
)
|
||||
}
|
||||
outputStream.write(
|
||||
cursor.toCsvRecord(indices, delimiter, deviation)
|
||||
)
|
||||
} while (cursor.moveToNext())
|
||||
}
|
||||
}
|
||||
|
||||
private fun ByteArray.toHexString(): String {
|
||||
val hex = StringBuilder()
|
||||
for (i in this.indices) {
|
||||
hex.append(String.format("%02X", this[i]))
|
||||
}
|
||||
return hex.toString()
|
||||
}
|
||||
|
||||
private fun Cursor.toCsvRecord(
|
||||
indices: List<Int>,
|
||||
delimiter: String,
|
||||
deviation: Pair<Int, String>?
|
||||
): ByteArray {
|
||||
val sb = StringBuilder()
|
||||
indices.forEach {
|
||||
val value = if (deviation?.first == it) {
|
||||
deviation.second
|
||||
} else {
|
||||
this.getString(it)
|
||||
}
|
||||
sb.append(value?.quoteAndEscape() ?: "")
|
||||
sb.append(delimiter)
|
||||
}
|
||||
sb.append("\n")
|
||||
return sb.toString().toByteArray()
|
||||
}
|
||||
|
||||
private fun String.quoteAndEscape() = "\"${this
|
||||
.replace("\n", " ")
|
||||
.replace("\"", "\"\"")}\""
|
||||
@@ -0,0 +1,169 @@
|
||||
package de.markusfisch.android.binaryeye.data
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.text.format.DateFormat
|
||||
import com.google.zxing.Result
|
||||
import com.google.zxing.ResultMetadataType
|
||||
|
||||
data class Scan(
|
||||
val content: String,
|
||||
val raw: ByteArray?,
|
||||
val format: String,
|
||||
val errorCorrectionLevel: String?,
|
||||
val issueNumber: String?,
|
||||
val orientation: String?,
|
||||
val otherMetaData: String?,
|
||||
val pdf417ExtraMetaData: String?,
|
||||
val possibleCountry: String?,
|
||||
val suggestedPrice: String?,
|
||||
val upcEanExtension: String?,
|
||||
val timestamp: String = getDateTime(),
|
||||
val id: Long = 0L
|
||||
) : Parcelable {
|
||||
constructor(result: Result) : this(
|
||||
result.text,
|
||||
getRawBytes(result),
|
||||
result.barcodeFormat.toString(),
|
||||
result.getMetaString(ResultMetadataType.ERROR_CORRECTION_LEVEL),
|
||||
result.getMetaString(ResultMetadataType.ISSUE_NUMBER),
|
||||
result.getMetaString(ResultMetadataType.ORIENTATION),
|
||||
result.getMetaString(ResultMetadataType.OTHER),
|
||||
result.getMetaString(ResultMetadataType.PDF417_EXTRA_METADATA),
|
||||
result.getMetaString(ResultMetadataType.POSSIBLE_COUNTRY),
|
||||
result.getMetaString(ResultMetadataType.SUGGESTED_PRICE),
|
||||
result.getMetaString(ResultMetadataType.UPC_EAN_EXTENSION)
|
||||
)
|
||||
|
||||
// Needs to be overwritten manually, as ByteArray is an array and
|
||||
// this isn't handled well by Kotlin
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
if (javaClass != other?.javaClass) {
|
||||
return false
|
||||
}
|
||||
|
||||
other as Scan
|
||||
|
||||
return id == other.id &&
|
||||
timestamp == other.timestamp &&
|
||||
content == other.content &&
|
||||
((raw == null && other.raw == null) ||
|
||||
(raw != null && other.raw != null && raw.contentEquals(other.raw))) &&
|
||||
format == other.format &&
|
||||
errorCorrectionLevel == other.errorCorrectionLevel &&
|
||||
issueNumber == other.issueNumber &&
|
||||
orientation == other.orientation &&
|
||||
otherMetaData == other.otherMetaData &&
|
||||
pdf417ExtraMetaData == other.pdf417ExtraMetaData &&
|
||||
possibleCountry == other.possibleCountry &&
|
||||
suggestedPrice == other.suggestedPrice &&
|
||||
upcEanExtension == other.upcEanExtension
|
||||
}
|
||||
|
||||
// Needs to be overwritten manually, as ByteArray is an array and
|
||||
// this isn't handled well by Kotlin
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + timestamp.hashCode()
|
||||
result = 31 * result + content.hashCode()
|
||||
result = 31 * result + (raw?.contentHashCode() ?: 0)
|
||||
result = 31 * result + format.hashCode()
|
||||
result = 31 * result + (errorCorrectionLevel?.hashCode() ?: 0)
|
||||
result = 31 * result + (issueNumber?.hashCode() ?: 0)
|
||||
result = 31 * result + (orientation?.hashCode() ?: 0)
|
||||
result = 31 * result + (otherMetaData?.hashCode() ?: 0)
|
||||
result = 31 * result + (pdf417ExtraMetaData?.hashCode() ?: 0)
|
||||
result = 31 * result + (possibleCountry?.hashCode() ?: 0)
|
||||
result = 31 * result + (suggestedPrice?.hashCode() ?: 0)
|
||||
result = 31 * result + (upcEanExtension?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
private constructor(parcel: Parcel) : this(
|
||||
content = parcel.readString() ?: "",
|
||||
raw = readByteArray(parcel),
|
||||
format = parcel.readString() ?: "",
|
||||
errorCorrectionLevel = parcel.readString(),
|
||||
issueNumber = parcel.readString(),
|
||||
orientation = parcel.readString(),
|
||||
otherMetaData = parcel.readString(),
|
||||
pdf417ExtraMetaData = parcel.readString(),
|
||||
possibleCountry = parcel.readString(),
|
||||
suggestedPrice = parcel.readString(),
|
||||
upcEanExtension = parcel.readString(),
|
||||
timestamp = parcel.readString() ?: "",
|
||||
id = parcel.readLong()
|
||||
)
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeString(content)
|
||||
writeByteArray(parcel, raw)
|
||||
parcel.writeString(format)
|
||||
parcel.writeString(errorCorrectionLevel)
|
||||
parcel.writeString(issueNumber)
|
||||
parcel.writeString(orientation)
|
||||
parcel.writeString(otherMetaData)
|
||||
parcel.writeString(pdf417ExtraMetaData)
|
||||
parcel.writeString(possibleCountry)
|
||||
parcel.writeString(suggestedPrice)
|
||||
parcel.writeString(upcEanExtension)
|
||||
parcel.writeString(timestamp)
|
||||
parcel.writeLong(id)
|
||||
}
|
||||
|
||||
override fun describeContents() = 0
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val CREATOR = object : Parcelable.Creator<Scan> {
|
||||
override fun createFromParcel(parcel: Parcel) = Scan(parcel)
|
||||
override fun newArray(size: Int) = arrayOfNulls<Scan>(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDateTime(time: Long = System.currentTimeMillis()) = DateFormat.format(
|
||||
"yyyy-MM-dd HH:mm:ss",
|
||||
time
|
||||
).toString()
|
||||
|
||||
private fun getRawBytes(result: Result): ByteArray? {
|
||||
val metadata = result.resultMetadata ?: return null
|
||||
val segments = metadata[ResultMetadataType.BYTE_SEGMENTS] ?: return null
|
||||
var bytes = ByteArray(0)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
for (seg in segments as Iterable<ByteArray>) {
|
||||
bytes += seg
|
||||
}
|
||||
// byte segments can never be shorter than the text.
|
||||
// Zxing cuts off content prefixes like "WIFI:"
|
||||
return if (bytes.size >= result.text.length) bytes else null
|
||||
}
|
||||
|
||||
private fun Result.getMetaString(
|
||||
key: ResultMetadataType
|
||||
): String? = this.resultMetadata?.let {
|
||||
it[key]?.toString()
|
||||
}
|
||||
|
||||
private fun writeByteArray(parcel: Parcel, array: ByteArray?) {
|
||||
val size = array?.size ?: 0
|
||||
parcel.writeInt(size)
|
||||
if (size > 0) {
|
||||
parcel.writeByteArray(array)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readByteArray(parcel: Parcel): ByteArray? {
|
||||
val size = parcel.readInt()
|
||||
return if (size > 0) {
|
||||
val array = ByteArray(size)
|
||||
parcel.readByteArray(array)
|
||||
array
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
package de.markusfisch.android.binaryeye.data.csv
|
||||
|
||||
import de.markusfisch.android.binaryeye.app.addDelimiter
|
||||
import de.markusfisch.android.binaryeye.app.asFlowWith
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun <T> csvBuilder(config: CSVBuilder<T>.() -> Unit): CSVBuilder<T> {
|
||||
return CSVBuilder<T>().apply(config)
|
||||
}
|
||||
|
||||
class CSVBuilder<T> {
|
||||
private val columns = mutableListOf<ColumnBuilder>()
|
||||
|
||||
fun column(config: ColumnBuilder.() -> Unit): ColumnBuilder {
|
||||
return ColumnBuilder().apply(config).also {
|
||||
columns.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
inner class ColumnBuilder {
|
||||
var isBinary = false
|
||||
var name: String? = null
|
||||
internal var gettingBy: ((T) -> ByteArray)? = null
|
||||
|
||||
fun gettingBy(getter: (T) -> ByteArray) {
|
||||
gettingBy = getter
|
||||
}
|
||||
|
||||
fun gettingByString(getter: (T) -> String): Unit = gettingBy {
|
||||
getter(it).toByteArray()
|
||||
}
|
||||
}
|
||||
|
||||
fun buildWith(inputs: Flow<T>, delimiter: String): Flow<ByteArray> = flow {
|
||||
val columnsFlow = columns.asFlow()
|
||||
|
||||
val header = columnsFlow.buildHeader(delimiter)
|
||||
|
||||
val contentLines = inputs.buildContent(columnsFlow, delimiter)
|
||||
|
||||
emitAll(header)
|
||||
emit("\n".toByteArray())
|
||||
emitAll(contentLines)
|
||||
}
|
||||
|
||||
private fun Flow<ColumnBuilder>.buildHeader(delimiter: String) = map {
|
||||
it.name?.escaped(delimiter)?.toByteArray()
|
||||
?: throw IllegalStateException("You need to provide a name for csv column")
|
||||
}.addDelimiter(delimiter)
|
||||
|
||||
private fun Flow<T>.buildContent(
|
||||
columnsFlow: Flow<ColumnBuilder>,
|
||||
delimiter: String
|
||||
): Flow<ByteArray> = map { input ->
|
||||
input.getLine(columnsFlow, delimiter)
|
||||
}.addDelimiter("\n".toByteArray()) { other: Flow<ByteArray> ->
|
||||
// this: ByteArray
|
||||
this asFlowWith other
|
||||
}.flattenConcat()
|
||||
|
||||
private fun T.getLine(columnsFlow: Flow<ColumnBuilder>, delimiter: String): Flow<ByteArray> =
|
||||
columnsFlow.map { column ->
|
||||
column.gettingBy?.run {
|
||||
var result = invoke(this@getLine)
|
||||
if (!column.isBinary) {
|
||||
result = String(result).escaped(delimiter).toByteArray()
|
||||
}
|
||||
return@run result
|
||||
} ?: throw IllegalStateException("You need to provide a getter for the value")
|
||||
}.addDelimiter(delimiter)
|
||||
|
||||
private fun Any.escaped(delimiter: String): String {
|
||||
val any = this.toString()
|
||||
return when {
|
||||
any.run { contains('"') || contains('\n') || contains(delimiter) } ->
|
||||
"\"${any.replace("\"", "\"\"").replace(delimiter, "\"$delimiter")}\""
|
||||
else -> any
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,19 +4,19 @@ import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.support.v4.app.Fragment
|
||||
import android.view.*
|
||||
import android.widget.EditText
|
||||
import android.widget.Toast
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.addSuffixIfNotGiven
|
||||
import de.markusfisch.android.binaryeye.app.hasWritePermission
|
||||
import de.markusfisch.android.binaryeye.app.setWindowInsetListener
|
||||
import de.markusfisch.android.binaryeye.app.shareFile
|
||||
import de.markusfisch.android.binaryeye.app.writeExternalFile
|
||||
import de.markusfisch.android.binaryeye.view.setPadding
|
||||
import de.markusfisch.android.binaryeye.view.setWindowInsetListener
|
||||
import de.markusfisch.android.binaryeye.widget.ConfinedScalingImageView
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import de.markusfisch.android.binaryeye.zxing.Zxing
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
@@ -24,6 +24,7 @@ import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.util.*
|
||||
|
||||
class BarcodeFragment : Fragment() {
|
||||
@@ -60,7 +61,9 @@ class BarcodeFragment : Fragment() {
|
||||
if (message == null || message.isEmpty()) {
|
||||
message = getString(R.string.error_encoding_barcode)
|
||||
}
|
||||
Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
|
||||
message?.let {
|
||||
activity.toast(message)
|
||||
}
|
||||
fragmentManager.popBackStack()
|
||||
return null
|
||||
}
|
||||
@@ -131,31 +134,17 @@ class BarcodeFragment : Fragment() {
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun saveAsFile(bitmap: Bitmap, path: String) {
|
||||
private fun saveAsFile(bitmap: Bitmap, fileName: String) {
|
||||
val ac = activity ?: return
|
||||
if (!hasWritePermission(ac)) {
|
||||
return
|
||||
}
|
||||
GlobalScope.launch {
|
||||
val success = saveBitmap(
|
||||
bitmap,
|
||||
File(
|
||||
Environment.getExternalStoragePublicDirectory(
|
||||
Environment.DIRECTORY_DOWNLOADS
|
||||
),
|
||||
path
|
||||
)
|
||||
)
|
||||
val message = writeExternalFile(ac, fileName, "image/png") {
|
||||
bitmap.saveAsPng(it)
|
||||
}
|
||||
GlobalScope.launch(Main) {
|
||||
Toast.makeText(
|
||||
ac,
|
||||
if (success) {
|
||||
R.string.saved_in_downloads
|
||||
} else {
|
||||
R.string.error_saving_binary_data
|
||||
},
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
ac.toast(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,19 +155,19 @@ class BarcodeFragment : Fragment() {
|
||||
context.externalCacheDir,
|
||||
"shared_barcode.png"
|
||||
)
|
||||
val success = saveBitmap(bitmap, file)
|
||||
val success = try {
|
||||
FileOutputStream(file).use {
|
||||
bitmap.saveAsPng(it)
|
||||
}
|
||||
true
|
||||
} catch (e: IOException) {
|
||||
false
|
||||
}
|
||||
GlobalScope.launch(Main) {
|
||||
if (success) {
|
||||
shareFile(context, file, "image/png")
|
||||
} else {
|
||||
val ac = activity
|
||||
ac?.let {
|
||||
Toast.makeText(
|
||||
ac,
|
||||
R.string.error_saving_binary_data,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
activity?.toast(R.string.error_saving_binary_data)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -205,15 +194,8 @@ class BarcodeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveBitmap(bitmap: Bitmap, file: File): Boolean {
|
||||
return try {
|
||||
val fos = FileOutputStream(file)
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 90, fos)
|
||||
fos.close()
|
||||
true
|
||||
} catch (e: IOException) {
|
||||
false
|
||||
}
|
||||
private fun Bitmap.saveAsPng(outputStream: OutputStream, quality: Int = 90) {
|
||||
this.compress(Bitmap.CompressFormat.PNG, quality, outputStream)
|
||||
}
|
||||
|
||||
private val fileNameCharacters = "[^A-Za-z0-9]".toRegex()
|
||||
|
||||
@@ -1,33 +1,35 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.design.widget.FloatingActionButton
|
||||
import android.support.v4.app.Fragment
|
||||
import android.text.ClipboardManager
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.*
|
||||
import android.widget.EditText
|
||||
import android.widget.TableLayout
|
||||
import android.widget.TableRow
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.ActionRegistry
|
||||
import de.markusfisch.android.binaryeye.actions.wifi.WifiAction
|
||||
import de.markusfisch.android.binaryeye.app.*
|
||||
import de.markusfisch.android.binaryeye.data.Scan
|
||||
import de.markusfisch.android.binaryeye.view.setPadding
|
||||
import de.markusfisch.android.binaryeye.view.setWindowInsetListener
|
||||
import de.markusfisch.android.binaryeye.widget.toast
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class DecodeFragment : Fragment() {
|
||||
private lateinit var contentView: EditText
|
||||
private lateinit var formatView: TextView
|
||||
private lateinit var metaView: TableLayout
|
||||
private lateinit var hexView: TextView
|
||||
private lateinit var format: BarcodeFormat
|
||||
private lateinit var format: String
|
||||
private lateinit var fab: FloatingActionButton
|
||||
|
||||
private var action = ActionRegistry.DEFAULT_ACTION
|
||||
@@ -56,10 +58,15 @@ class DecodeFragment : Fragment() {
|
||||
false
|
||||
)
|
||||
|
||||
val inputContent = arguments?.getString(CONTENT) ?: ""
|
||||
isBinary = hasNonPrintableCharacters(inputContent) or inputContent.isEmpty()
|
||||
val raw = arguments?.getByteArray(RAW) ?: inputContent.toByteArray()
|
||||
format = arguments?.getSerializable(FORMAT) as BarcodeFormat? ?: BarcodeFormat.QR_CODE
|
||||
val scan = arguments?.getParcelable(SCAN) as Scan?
|
||||
?: throw IllegalArgumentException("DecodeFragment needs a Scan")
|
||||
|
||||
val inputContent = scan.content
|
||||
isBinary = hasNonPrintableCharacters(
|
||||
inputContent
|
||||
) or inputContent.isEmpty()
|
||||
val raw = scan.raw ?: inputContent.toByteArray()
|
||||
format = scan.format
|
||||
|
||||
contentView = view.findViewById(R.id.content)
|
||||
fab = view.findViewById(R.id.open)
|
||||
@@ -103,9 +110,23 @@ class DecodeFragment : Fragment() {
|
||||
}
|
||||
|
||||
formatView = view.findViewById(R.id.format)
|
||||
metaView = view.findViewById(R.id.meta)
|
||||
hexView = view.findViewById(R.id.hex)
|
||||
|
||||
updateViewsAndAction(raw)
|
||||
if (action is WifiAction) {
|
||||
val password = (action as WifiAction).password
|
||||
if (password != null) {
|
||||
activity?.apply {
|
||||
copyToClipboard(password)
|
||||
toast(R.string.copied_password_to_clipboard)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (prefs.showMetaData) {
|
||||
fillMetaView(metaView, scan)
|
||||
}
|
||||
|
||||
setWindowInsetListener { insets ->
|
||||
(view.findViewById(R.id.inset_layout) as View).setPadding(insets)
|
||||
@@ -123,19 +144,15 @@ class DecodeFragment : Fragment() {
|
||||
formatView.text = resources.getQuantityString(
|
||||
R.plurals.barcode_info,
|
||||
bytes.size,
|
||||
format.toString(),
|
||||
format,
|
||||
bytes.size
|
||||
)
|
||||
hexView.text = hexDump(bytes, 33)
|
||||
hexView.text = if (prefs.showHexDump) hexDump(bytes) else ""
|
||||
if (prevAction !== action) {
|
||||
fab.setImageResource(action.iconResId)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
fab.setOnLongClickListener { v ->
|
||||
Toast.makeText(
|
||||
v.context,
|
||||
action.titleResId,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
v.context.toast(action.titleResId)
|
||||
true
|
||||
}
|
||||
} else {
|
||||
@@ -144,6 +161,38 @@ class DecodeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun fillMetaView(tableLayout: TableLayout, scan: Scan) {
|
||||
val ctx = tableLayout.context
|
||||
val spaceBetween = (ctx.resources.displayMetrics.density * 16f).toInt()
|
||||
var hasMeta = false
|
||||
sortedMapOf(
|
||||
R.string.error_correction_level to scan.errorCorrectionLevel,
|
||||
R.string.issue_number to scan.issueNumber,
|
||||
R.string.orientation to scan.orientation,
|
||||
R.string.other_meta_data to scan.otherMetaData,
|
||||
R.string.pdf417_extra_metadata to scan.pdf417ExtraMetaData,
|
||||
R.string.possible_country to scan.possibleCountry,
|
||||
R.string.suggested_price to scan.suggestedPrice,
|
||||
R.string.upc_ean_extension to scan.upcEanExtension
|
||||
).forEach { item ->
|
||||
item.value?.let {
|
||||
val tr = TableRow(ctx)
|
||||
val keyView = TextView(ctx)
|
||||
keyView.setText(item.key)
|
||||
val valueView = TextView(ctx)
|
||||
valueView.setPadding(spaceBetween, 0, 0, 0)
|
||||
valueView.text = it
|
||||
tr.addView(keyView)
|
||||
tr.addView(valueView)
|
||||
tableLayout.addView(tr)
|
||||
hasMeta = true
|
||||
}
|
||||
}
|
||||
if (hasMeta) {
|
||||
tableLayout.setPadding(0, 0, 0, spaceBetween)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.fragment_decode, menu)
|
||||
if (isBinary) {
|
||||
@@ -174,36 +223,41 @@ class DecodeFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun copyToClipboard(text: String) {
|
||||
activity ?: return
|
||||
|
||||
val cm = activity.getSystemService(
|
||||
Context.CLIPBOARD_SERVICE
|
||||
) as ClipboardManager
|
||||
cm.text = text
|
||||
Toast.makeText(
|
||||
activity,
|
||||
R.string.put_into_clipboard,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
activity?.apply {
|
||||
copyToClipboard(text)
|
||||
toast(R.string.copied_to_clipboard)
|
||||
}
|
||||
}
|
||||
|
||||
private fun executeAction(content: ByteArray) {
|
||||
if (activity != null && content.isNotEmpty()) scope.launch {
|
||||
action.execute(activity, content)
|
||||
if (activity != null && content.isNotEmpty()) {
|
||||
if (action is WifiAction &&
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||
!hasLocationPermission(activity)
|
||||
) {
|
||||
return
|
||||
}
|
||||
scope.launch {
|
||||
action.execute(activity, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun askForFileNameAndSave(raw: ByteArray) {
|
||||
val ac = activity ?: return
|
||||
if (!hasWritePermission(ac)) return
|
||||
if (!hasWritePermission(ac)) {
|
||||
return
|
||||
}
|
||||
scope.launch(Dispatchers.Main) {
|
||||
val name = ac.askForFileName() ?: return@launch
|
||||
val message = flowOf(raw).writeToFile(name)
|
||||
Toast.makeText(
|
||||
val message = writeExternalFile(
|
||||
ac,
|
||||
message,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
name,
|
||||
"application/octet-stream"
|
||||
) {
|
||||
it.write(raw)
|
||||
}
|
||||
ac.toast(message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,21 +267,11 @@ class DecodeFragment : Fragment() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CONTENT = "content"
|
||||
private const val FORMAT = "format"
|
||||
private const val RAW = "raw"
|
||||
private const val SCAN = "scan"
|
||||
|
||||
fun newInstance(
|
||||
content: String,
|
||||
format: BarcodeFormat,
|
||||
raw: ByteArray? = null
|
||||
): Fragment {
|
||||
fun newInstance(scan: Scan): Fragment {
|
||||
val args = Bundle()
|
||||
args.putString(CONTENT, content)
|
||||
args.putSerializable(FORMAT, format)
|
||||
if (raw != null) {
|
||||
args.putByteArray(RAW, raw)
|
||||
}
|
||||
args.putParcelable(SCAN, scan)
|
||||
val fragment = DecodeFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
@@ -235,7 +279,7 @@ class DecodeFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun hexDump(bytes: ByteArray, charsPerLine: Int): String {
|
||||
fun hexDump(bytes: ByteArray, charsPerLine: Int = 33): String {
|
||||
if (charsPerLine < 4 || bytes.isEmpty()) {
|
||||
return ""
|
||||
}
|
||||
@@ -249,9 +293,7 @@ private fun hexDump(bytes: ByteArray, charsPerLine: Int): String {
|
||||
val ord = bytes[i]
|
||||
hex.append(String.format("%02X ", ord))
|
||||
ascii.append(if (ord > 31) ord.toChar() else " ")
|
||||
|
||||
++i
|
||||
|
||||
val posInLine = i % itemsPerLine
|
||||
val atEnd = i >= len
|
||||
var atLineEnd = posInLine == 0
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.view.*
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.*
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.addFragment
|
||||
import de.markusfisch.android.binaryeye.app.hideSoftKeyboard
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.app.setWindowInsetListener
|
||||
import de.markusfisch.android.binaryeye.view.setPadding
|
||||
import de.markusfisch.android.binaryeye.view.setWindowInsetListener
|
||||
|
||||
class EncodeFragment : Fragment() {
|
||||
private lateinit var formatView: Spinner
|
||||
@@ -53,7 +52,7 @@ class EncodeFragment : Fragment() {
|
||||
)
|
||||
|
||||
formatView = view.findViewById(R.id.format)
|
||||
formatView.adapter = ArrayAdapter<String>(
|
||||
formatView.adapter = ArrayAdapter(
|
||||
ac,
|
||||
android.R.layout.simple_list_item_1,
|
||||
writers.map { it.name }
|
||||
@@ -63,23 +62,25 @@ class EncodeFragment : Fragment() {
|
||||
sizeBarView = view.findViewById(R.id.size_bar)
|
||||
initSizeBar()
|
||||
|
||||
contentView = view.findViewById<EditText>(R.id.content)
|
||||
contentView = view.findViewById(R.id.content)
|
||||
|
||||
val args = arguments
|
||||
args?.also {
|
||||
contentView.setText(it.getString(CONTENT))
|
||||
}
|
||||
|
||||
val barcodeFormat = args?.getSerializable(FORMAT) as BarcodeFormat?
|
||||
val barcodeFormat = args?.getString(FORMAT)
|
||||
if (barcodeFormat != null) {
|
||||
formatView.setSelection(writers.indexOf(barcodeFormat))
|
||||
formatView.setSelection(
|
||||
writers.indexOf(resolveFormat(barcodeFormat))
|
||||
)
|
||||
} else if (state == null) {
|
||||
formatView.post {
|
||||
formatView.setSelection(prefs.indexOfLastSelectedFormat)
|
||||
}
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.encode).setOnClickListener { v ->
|
||||
view.findViewById<View>(R.id.encode).setOnClickListener {
|
||||
encode()
|
||||
}
|
||||
|
||||
@@ -121,7 +122,7 @@ class EncodeFragment : Fragment() {
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} else {
|
||||
hideSoftKeyboard(contentView)
|
||||
activity?.hideSoftKeyboard(contentView)
|
||||
addFragment(
|
||||
fragmentManager,
|
||||
BarcodeFragment.newInstance(content, format, size)
|
||||
@@ -154,31 +155,26 @@ class EncodeFragment : Fragment() {
|
||||
|
||||
private fun getSize(power: Int) = 128 * (power + 1)
|
||||
|
||||
private fun hideSoftKeyboard(view: View) {
|
||||
val im = activity?.getSystemService(
|
||||
Context.INPUT_METHOD_SERVICE
|
||||
) as InputMethodManager?
|
||||
im?.let {
|
||||
im.hideSoftInputFromWindow(view.windowToken, 0)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CONTENT = "content"
|
||||
private const val FORMAT = "format"
|
||||
|
||||
fun newInstance(
|
||||
content: String,
|
||||
format: BarcodeFormat? = null
|
||||
format: String? = null
|
||||
): Fragment {
|
||||
val args = Bundle()
|
||||
args.putString(CONTENT, content)
|
||||
format?.let {
|
||||
args.putSerializable(FORMAT, format)
|
||||
}
|
||||
format?.let { args.putString(FORMAT, it) }
|
||||
val fragment = EncodeFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveFormat(name: String): BarcodeFormat = try {
|
||||
BarcodeFormat.valueOf(name)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
BarcodeFormat.QR_CODE
|
||||
}
|
||||
|
||||
@@ -1,38 +1,104 @@
|
||||
package de.markusfisch.android.binaryeye.fragment
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.app.SearchManager
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.support.annotation.WorkerThread
|
||||
import android.support.v4.app.ActivityCompat
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.view.ActionMode
|
||||
import android.support.v7.widget.SearchView
|
||||
import android.support.v7.widget.SwitchCompat
|
||||
import android.view.*
|
||||
import android.widget.AbsListView
|
||||
import android.widget.EditText
|
||||
import android.widget.ListView
|
||||
import android.widget.Toast
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.adapter.ScansAdapter
|
||||
import de.markusfisch.android.binaryeye.app.*
|
||||
import de.markusfisch.android.binaryeye.data.csv.csvBuilder
|
||||
import de.markusfisch.android.binaryeye.repository.DatabaseRepository
|
||||
import de.markusfisch.android.binaryeye.data.Database
|
||||
import de.markusfisch.android.binaryeye.data.exportCsv
|
||||
import de.markusfisch.android.binaryeye.view.setPadding
|
||||
import de.markusfisch.android.binaryeye.view.setWindowInsetListener
|
||||
import de.markusfisch.android.binaryeye.view.useVisibility
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
|
||||
class HistoryFragment : Fragment() {
|
||||
private lateinit var useHistorySwitch: SwitchCompat
|
||||
private lateinit var listView: ListView
|
||||
private lateinit var fab: View
|
||||
private lateinit var progressView: View
|
||||
|
||||
private val parentJob = Job()
|
||||
private val scope = CoroutineScope(Dispatchers.IO + parentJob)
|
||||
private val actionModeCallback = object : ActionMode.Callback {
|
||||
override fun onCreateActionMode(
|
||||
mode: ActionMode,
|
||||
menu: Menu
|
||||
): Boolean {
|
||||
mode.menuInflater.inflate(
|
||||
R.menu.fragment_history_edit,
|
||||
menu
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
lockStatusBarColor()
|
||||
val ac = activity
|
||||
ac.window.statusBarColor = ContextCompat.getColor(
|
||||
ac,
|
||||
R.color.accent_dark
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(
|
||||
mode: ActionMode,
|
||||
menu: Menu
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onActionItemClicked(
|
||||
mode: ActionMode,
|
||||
item: MenuItem
|
||||
): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.edit_scan -> {
|
||||
scansAdapter?.let {
|
||||
askForName(
|
||||
activity,
|
||||
it.selectedScanId,
|
||||
getScanName(it.selectedScanPosition)
|
||||
)
|
||||
}
|
||||
closeActionMode()
|
||||
true
|
||||
}
|
||||
R.id.remove_scan -> {
|
||||
scansAdapter?.let {
|
||||
askToRemoveScan(activity, it.selectedScanId)
|
||||
}
|
||||
closeActionMode()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode) {
|
||||
closeActionMode()
|
||||
}
|
||||
}
|
||||
|
||||
private var scansAdapter: ScansAdapter? = null
|
||||
private var listViewState: Parcelable? = null
|
||||
private var actionMode: ActionMode? = null
|
||||
private var filter: String? = null
|
||||
|
||||
override fun onCreate(state: Bundle?) {
|
||||
super.onCreate(state)
|
||||
@@ -52,28 +118,24 @@ class HistoryFragment : Fragment() {
|
||||
false
|
||||
)
|
||||
|
||||
val useHistorySwitch = view.findViewById(
|
||||
useHistorySwitch = view.findViewById(
|
||||
R.id.use_history
|
||||
) as SwitchCompat
|
||||
initHistorySwitch(useHistorySwitch)
|
||||
|
||||
val headView = View(context)
|
||||
val footView = View(context)
|
||||
setWindowInsetListener { insets ->
|
||||
(view.findViewById(R.id.inset_layout) as View).setPadding(insets)
|
||||
headView.layoutParams = AbsListView.LayoutParams(0, insets.top)
|
||||
footView.layoutParams = AbsListView.LayoutParams(0, insets.bottom)
|
||||
}
|
||||
|
||||
listView = view.findViewById(R.id.scans)
|
||||
listView.addHeaderView(headView, null, false)
|
||||
listView.addFooterView(footView, null, false)
|
||||
listView.emptyView = useHistorySwitch
|
||||
listView.setOnItemClickListener { _, _, _, id ->
|
||||
showScan(id)
|
||||
}
|
||||
listView.setOnItemLongClickListener { _, v, _, id ->
|
||||
askToRemoveScan(v.context, id)
|
||||
listView.setOnItemLongClickListener { _, v, position, id ->
|
||||
v.isSelected = true
|
||||
scansAdapter?.select(id, position)
|
||||
val ac = activity
|
||||
if (actionMode == null && ac is AppCompatActivity) {
|
||||
actionMode = ac.delegate.startSupportActionMode(
|
||||
actionModeCallback
|
||||
)
|
||||
}
|
||||
true
|
||||
}
|
||||
listView.setOnScrollListener(systemBarScrollListener)
|
||||
@@ -85,6 +147,13 @@ class HistoryFragment : Fragment() {
|
||||
|
||||
progressView = view.findViewById(R.id.progress_view)
|
||||
|
||||
setWindowInsetListener { insets ->
|
||||
(view.findViewById(R.id.inset_layout) as View).setPadding(insets)
|
||||
listView.setPadding(insets)
|
||||
}
|
||||
|
||||
update()
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
@@ -94,11 +163,6 @@ class HistoryFragment : Fragment() {
|
||||
parentJob.cancel()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
update(context)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
listViewState = listView.onSaveInstanceState()
|
||||
@@ -106,6 +170,35 @@ class HistoryFragment : Fragment() {
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.fragment_history, menu)
|
||||
initSearchView(menu.findItem(R.id.search))
|
||||
menu.setGroupVisible(R.id.scans_available, scansAdapter?.count != 0)
|
||||
}
|
||||
|
||||
private fun initSearchView(item: MenuItem?) {
|
||||
item ?: return
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
|
||||
item.isVisible = false
|
||||
} else {
|
||||
val ac = activity
|
||||
val searchView = item.actionView as SearchView
|
||||
val searchManager = ac.getSystemService(
|
||||
Context.SEARCH_SERVICE
|
||||
) as SearchManager
|
||||
searchView.setSearchableInfo(
|
||||
searchManager.getSearchableInfo(ac.componentName)
|
||||
)
|
||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String): Boolean {
|
||||
update(query)
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(query: String): Boolean {
|
||||
update(query)
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
@@ -131,19 +224,33 @@ class HistoryFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun update(context: Context) {
|
||||
private fun updateAndClearFilter() {
|
||||
filter = null
|
||||
update()
|
||||
}
|
||||
|
||||
private fun update(query: String? = null) {
|
||||
query?.let { filter = it }
|
||||
scope.launch {
|
||||
val cursor = db.getScansCursor()
|
||||
val cursor = db.getScans(filter)
|
||||
withContext(Dispatchers.Main) {
|
||||
fab.visibility = if (cursor != null && cursor.count > 0) {
|
||||
val ac = activity ?: return@withContext
|
||||
val hasScans = cursor != null && cursor.count > 0
|
||||
if (filter == null) {
|
||||
if (!hasScans) {
|
||||
listView.emptyView = useHistorySwitch
|
||||
}
|
||||
ActivityCompat.invalidateOptionsMenu(ac)
|
||||
}
|
||||
fab.visibility = if (hasScans) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
cursor?.let {
|
||||
cursor?.let { cursor ->
|
||||
// close previous cursor
|
||||
scansAdapter?.also { it.changeCursor(null) }
|
||||
scansAdapter = ScansAdapter(context, it)
|
||||
scansAdapter = ScansAdapter(ac, cursor)
|
||||
listView.adapter = scansAdapter
|
||||
listViewState?.also {
|
||||
listView.onRestoreInstanceState(it)
|
||||
@@ -153,27 +260,60 @@ class HistoryFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun closeActionMode() {
|
||||
unlockStatusBarColor()
|
||||
scansAdapter?.clearSelection()
|
||||
actionMode?.finish()
|
||||
actionMode = null
|
||||
scansAdapter?.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private fun showScan(id: Long) = db.getScan(id)?.also { scan ->
|
||||
closeActionMode()
|
||||
try {
|
||||
addFragment(
|
||||
fragmentManager,
|
||||
DecodeFragment.newInstance(
|
||||
scan.content,
|
||||
BarcodeFormat.valueOf(scan.format),
|
||||
scan.raw
|
||||
)
|
||||
DecodeFragment.newInstance(scan)
|
||||
)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// shouldn't ever happen
|
||||
// ignore, can never happen
|
||||
}
|
||||
}
|
||||
|
||||
private fun getScanName(position: Int): String? {
|
||||
val cursor = scansAdapter?.getItem(position) as Cursor?
|
||||
return cursor?.getString(cursor.getColumnIndex(Database.SCANS_NAME))
|
||||
}
|
||||
|
||||
// dialogs don't have a parent layout
|
||||
@SuppressLint("InflateParams")
|
||||
private fun askForName(context: Context, id: Long, text: String?) {
|
||||
val view = LayoutInflater.from(context).inflate(
|
||||
R.layout.dialog_enter_name, null
|
||||
)
|
||||
val nameView = view.findViewById<EditText>(R.id.name)
|
||||
nameView.setText(text)
|
||||
AlertDialog.Builder(context)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val name = nameView.text.toString()
|
||||
db.renameScan(id, name)
|
||||
update()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun askToRemoveScan(context: Context, id: Long) {
|
||||
AlertDialog.Builder(context)
|
||||
.setMessage(R.string.really_remove_scan)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
db.removeScan(id)
|
||||
update(context)
|
||||
if (scansAdapter?.count == 1) {
|
||||
updateAndClearFilter()
|
||||
} else {
|
||||
update()
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
}
|
||||
@@ -185,7 +325,7 @@ class HistoryFragment : Fragment() {
|
||||
.setMessage(R.string.really_remove_all_scans)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
db.removeScans()
|
||||
update(context)
|
||||
updateAndClearFilter()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
}
|
||||
@@ -197,21 +337,6 @@ class HistoryFragment : Fragment() {
|
||||
if (!hasWritePermission(activity)) {
|
||||
return@useVisibility
|
||||
}
|
||||
val (getBinaries, scans) = getScans {
|
||||
alertDialog(context) { resume ->
|
||||
setTitle(R.string.csv_allow_binary)
|
||||
setMessage(R.string.csv_allow_binary_message)
|
||||
setPositiveButton(R.string.no) { _, _ ->
|
||||
resume(false)
|
||||
}
|
||||
setNegativeButton(android.R.string.yes) { _, _ ->
|
||||
resume(true)
|
||||
}
|
||||
setNeutralButton(android.R.string.cancel) { _, _ ->
|
||||
resume(null)
|
||||
}
|
||||
}
|
||||
} ?: return@useVisibility
|
||||
val delimiters = context.resources.getStringArray(
|
||||
R.array.csv_delimiters_values
|
||||
)
|
||||
@@ -222,45 +347,14 @@ class HistoryFragment : Fragment() {
|
||||
}
|
||||
} ?: return@useVisibility
|
||||
val name = withContext(Dispatchers.Main) {
|
||||
activity.askForFileName(suffix = "csv")
|
||||
activity.askForFileName(suffix = ".csv")
|
||||
} ?: return@useVisibility
|
||||
val csv = scans.toCSV(delimiter, getBinaries)
|
||||
val toastMessage = csv.writeToFile(name)
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
toastMessage,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
db.getScansDetailed(filter)?.use { cursor ->
|
||||
exportCsv(context, name, cursor, delimiter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Flow<DatabaseRepository.Scan>.toCSV(
|
||||
delimiter: String,
|
||||
allowBinary: Boolean
|
||||
): Flow<ByteArray> {
|
||||
return csvBuilder<DatabaseRepository.Scan> {
|
||||
column {
|
||||
name = "DATE"
|
||||
gettingByString { it.timestamp }
|
||||
}
|
||||
column {
|
||||
name = "TYPE"
|
||||
gettingByString { it.format }
|
||||
}
|
||||
column {
|
||||
name = "CONTENT"
|
||||
gettingByString { it.content }
|
||||
}
|
||||
if (allowBinary) column {
|
||||
isBinary = true
|
||||
name = "BINARY_CONTENT"
|
||||
gettingBy { it.raw ?: ByteArray(0) }
|
||||
}
|
||||
}.buildWith(this, delimiter)
|
||||
}
|
||||
|
||||
private fun pickListSeparatorAndShare(context: Context) {
|
||||
val separators = context.resources.getStringArray(
|
||||
R.array.list_separators_values
|
||||
@@ -277,9 +371,17 @@ class HistoryFragment : Fragment() {
|
||||
scope.launch {
|
||||
progressView.useVisibility {
|
||||
val sb = StringBuilder()
|
||||
getScans { false }?.second?.collect {
|
||||
sb.append(it.content)
|
||||
sb.append(separator)
|
||||
db.getScans(filter)?.use { cursor ->
|
||||
val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
val content = cursor.getString(contentIndex)
|
||||
if (content?.isNotEmpty() == true) {
|
||||
sb.append(content)
|
||||
sb.append(separator)
|
||||
}
|
||||
} while (cursor.moveToNext())
|
||||
}
|
||||
}
|
||||
val text = sb.toString()
|
||||
if (text.isNotEmpty()) withContext(Dispatchers.Main) {
|
||||
@@ -288,18 +390,4 @@ class HistoryFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private suspend inline fun getScans(
|
||||
crossinline binaryData: suspend () -> Boolean?
|
||||
): Pair<Boolean, Flow<DatabaseRepository.Scan>>? {
|
||||
val getBinaries: Boolean = db.hasBinaryData() && binaryData() ?: return null
|
||||
return getBinaries to db.getScans().mapNotNull {
|
||||
when {
|
||||
it.content.isNotEmpty() -> db.getScan(it.id)
|
||||
getBinaries -> db.getScan(it.id)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,18 @@ import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.app.setWindowInsetListener
|
||||
import de.markusfisch.android.binaryeye.view.setPadding
|
||||
import de.markusfisch.android.binaryeye.view.setWindowInsetListener
|
||||
|
||||
class PreferencesFragment : Fragment() {
|
||||
private lateinit var zoomBySwipingSwitch: SwitchCompat
|
||||
private lateinit var openImmediatelySwitch: SwitchCompat
|
||||
private lateinit var vibrateSwitch: SwitchCompat
|
||||
private lateinit var useHistorySwitch: SwitchCompat
|
||||
private lateinit var showMetaDataSwitch: SwitchCompat
|
||||
private lateinit var showHexDumpSwitch: SwitchCompat
|
||||
private lateinit var tryHarderSwitch: SwitchCompat
|
||||
private lateinit var ignoreConsecutiveDuplicatesSwitch: SwitchCompat
|
||||
private lateinit var openWithUrlInput: EditText
|
||||
|
||||
override fun onCreateView(
|
||||
@@ -30,16 +36,48 @@ class PreferencesFragment : Fragment() {
|
||||
false
|
||||
)
|
||||
|
||||
zoomBySwipingSwitch = view.findViewById(R.id.zoom_by_swiping)
|
||||
if (prefs.zoomBySwiping) {
|
||||
zoomBySwipingSwitch.toggle()
|
||||
}
|
||||
|
||||
openImmediatelySwitch = view.findViewById(R.id.open_immediately)
|
||||
if (prefs.openImmediately) {
|
||||
openImmediatelySwitch.toggle()
|
||||
}
|
||||
|
||||
vibrateSwitch = view.findViewById(R.id.vibrate)
|
||||
if (prefs.vibrate) {
|
||||
vibrateSwitch.toggle()
|
||||
}
|
||||
|
||||
useHistorySwitch = view.findViewById(R.id.use_history)
|
||||
if (prefs.useHistory) {
|
||||
useHistorySwitch.toggle()
|
||||
}
|
||||
|
||||
ignoreConsecutiveDuplicatesSwitch = view.findViewById(
|
||||
R.id.ignore_consecutive_duplicates
|
||||
)
|
||||
if (prefs.ignoreConsecutiveDuplicates) {
|
||||
ignoreConsecutiveDuplicatesSwitch.toggle()
|
||||
}
|
||||
|
||||
showMetaDataSwitch = view.findViewById(R.id.show_meta_data)
|
||||
if (prefs.showMetaData) {
|
||||
showMetaDataSwitch.toggle()
|
||||
}
|
||||
|
||||
showHexDumpSwitch = view.findViewById(R.id.show_hex_dump)
|
||||
if (prefs.showHexDump) {
|
||||
showHexDumpSwitch.toggle()
|
||||
}
|
||||
|
||||
tryHarderSwitch = view.findViewById(R.id.try_harder)
|
||||
if (prefs.tryHarder) {
|
||||
tryHarderSwitch.toggle()
|
||||
}
|
||||
|
||||
openWithUrlInput = view.findViewById(R.id.open_with_url)
|
||||
openWithUrlInput.setText(prefs.openWithUrl)
|
||||
|
||||
@@ -52,8 +90,14 @@ class PreferencesFragment : Fragment() {
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
prefs.zoomBySwiping = zoomBySwipingSwitch.isChecked
|
||||
prefs.openImmediately = openImmediatelySwitch.isChecked
|
||||
prefs.vibrate = vibrateSwitch.isChecked
|
||||
prefs.useHistory = useHistorySwitch.isChecked
|
||||
prefs.ignoreConsecutiveDuplicates = ignoreConsecutiveDuplicatesSwitch.isChecked
|
||||
prefs.showMetaData = showMetaDataSwitch.isChecked
|
||||
prefs.showHexDump = showHexDumpSwitch.isChecked
|
||||
prefs.tryHarder = tryHarderSwitch.isChecked
|
||||
prefs.openWithUrl = openWithUrlInput.text.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,7 @@ fun loadImageUri(cr: ContentResolver, uri: Uri): Bitmap? = try {
|
||||
BitmapFactory.decodeStream(it, null, options)
|
||||
options.inSampleSize = calculateInSampleSize(
|
||||
options.outWidth,
|
||||
options.outHeight,
|
||||
1024,
|
||||
1024
|
||||
options.outHeight
|
||||
)
|
||||
options.inJustDecodeBounds = false
|
||||
}
|
||||
@@ -32,8 +30,8 @@ fun loadImageUri(cr: ContentResolver, uri: Uri): Bitmap? = try {
|
||||
private fun calculateInSampleSize(
|
||||
width: Int,
|
||||
height: Int,
|
||||
reqWidth: Int,
|
||||
reqHeight: Int
|
||||
reqWidth: Int = 1024,
|
||||
reqHeight: Int = 1024
|
||||
): Int {
|
||||
var inSampleSize = 1
|
||||
if (height > reqHeight || width > reqWidth) {
|
||||
|
||||
@@ -4,12 +4,8 @@ import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Point
|
||||
import android.graphics.Rect
|
||||
import android.support.v4.content.ContextCompat
|
||||
import com.google.zxing.Result
|
||||
import com.google.zxing.ResultPoint
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class Candidates(context: Context) {
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
@@ -34,33 +30,3 @@ class Candidates(context: Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun mapResult(
|
||||
width: Int,
|
||||
height: Int,
|
||||
viewRect: Rect,
|
||||
result: Result
|
||||
): List<Point> = frameToView(width, height, viewRect).map(result.resultPoints)
|
||||
|
||||
fun frameToView(width: Int, height: Int, viewRect: Rect) = Mapping(
|
||||
viewRect.width().toFloat() / width.toFloat(),
|
||||
viewRect.height().toFloat() / height.toFloat(),
|
||||
viewRect.left,
|
||||
viewRect.top
|
||||
)
|
||||
|
||||
data class Mapping(
|
||||
val ratioX: Float,
|
||||
val ratioY: Float,
|
||||
val offsetX: Int,
|
||||
val offsetY: Int
|
||||
) {
|
||||
fun map(points: Array<ResultPoint>): List<Point> {
|
||||
return points.map {
|
||||
Point(
|
||||
(it.x * ratioX).roundToInt() + offsetX,
|
||||
(it.y * ratioY).roundToInt() + offsetY
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package de.markusfisch.android.binaryeye.graphics
|
||||
|
||||
import android.graphics.Point
|
||||
import android.graphics.Rect
|
||||
import com.google.zxing.Result
|
||||
import com.google.zxing.ResultPoint
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun mapResult(
|
||||
width: Int,
|
||||
height: Int,
|
||||
viewRect: Rect,
|
||||
result: Result
|
||||
): List<Point> = frameToView(width, height, viewRect).map(result.resultPoints)
|
||||
|
||||
fun frameToView(width: Int, height: Int, viewRect: Rect) = Mapping(
|
||||
viewRect.width().toFloat() / width.toFloat(),
|
||||
viewRect.height().toFloat() / height.toFloat(),
|
||||
viewRect.left,
|
||||
viewRect.top
|
||||
)
|
||||
|
||||
data class Mapping(
|
||||
val ratioX: Float,
|
||||
val ratioY: Float,
|
||||
val offsetX: Int,
|
||||
val offsetY: Int
|
||||
) {
|
||||
fun map(point: ResultPoint) = Point(
|
||||
(point.x * ratioX).roundToInt() + offsetX,
|
||||
(point.y * ratioY).roundToInt() + offsetY
|
||||
)
|
||||
|
||||
fun map(points: Array<ResultPoint?>): List<Point> =
|
||||
// because ZXing apparently returns null in this array sometimes
|
||||
points.filterNotNull().map { map(it) }
|
||||
}
|
||||
@@ -7,16 +7,46 @@ import android.preference.PreferenceManager
|
||||
class Preferences {
|
||||
lateinit var preferences: SharedPreferences
|
||||
|
||||
var zoomBySwiping = true
|
||||
set(value) {
|
||||
setBoolean(ZOOM_BY_SWIPING, value)
|
||||
field = value
|
||||
}
|
||||
var openImmediately = false
|
||||
set(value) {
|
||||
setBoolean(OPEN_IMMEDIATELY, value)
|
||||
field = value
|
||||
}
|
||||
var vibrate = true
|
||||
set(value) {
|
||||
setBoolean(VIBRATE, value)
|
||||
field = value
|
||||
}
|
||||
var useHistory = false
|
||||
set(value) {
|
||||
setBoolean(USE_HISTORY, value)
|
||||
field = value
|
||||
}
|
||||
var ignoreConsecutiveDuplicates = true
|
||||
set(value) {
|
||||
setBoolean(IGNORE_CONSECUTIVE_DUPLICATES, value)
|
||||
field = value
|
||||
}
|
||||
var showMetaData = true
|
||||
set(value) {
|
||||
setBoolean(SHOW_META_DATA, value)
|
||||
field = value
|
||||
}
|
||||
var showHexDump = true
|
||||
set(value) {
|
||||
setBoolean(SHOW_HEX_DUMP, value)
|
||||
field = value
|
||||
}
|
||||
var tryHarder = false
|
||||
set(value) {
|
||||
setBoolean(TRY_HARDER, value)
|
||||
field = value
|
||||
}
|
||||
var openWithUrl: String = ""
|
||||
set(value) {
|
||||
setString(OPEN_WITH_URL, value)
|
||||
@@ -34,18 +64,27 @@ class Preferences {
|
||||
}
|
||||
|
||||
fun update() {
|
||||
zoomBySwiping = preferences.getBoolean(ZOOM_BY_SWIPING, zoomBySwiping)
|
||||
openImmediately = preferences.getBoolean(
|
||||
OPEN_IMMEDIATELY,
|
||||
openImmediately
|
||||
)
|
||||
vibrate = preferences.getBoolean(VIBRATE, vibrate)
|
||||
useHistory = preferences.getBoolean(USE_HISTORY, useHistory)
|
||||
ignoreConsecutiveDuplicates = preferences.getBoolean(
|
||||
IGNORE_CONSECUTIVE_DUPLICATES,
|
||||
ignoreConsecutiveDuplicates
|
||||
)
|
||||
showMetaData = preferences.getBoolean(SHOW_META_DATA, showMetaData)
|
||||
showHexDump = preferences.getBoolean(SHOW_HEX_DUMP, showHexDump)
|
||||
tryHarder = preferences.getBoolean(TRY_HARDER, tryHarder)
|
||||
preferences.getString(OPEN_WITH_URL, openWithUrl)?.also {
|
||||
openWithUrl = it
|
||||
}
|
||||
indexOfLastSelectedFormat = preferences.getInt(
|
||||
INDEX_OF_LAST_SELECTED_FORMAT,
|
||||
indexOfLastSelectedFormat
|
||||
)
|
||||
preferences.getString(OPEN_WITH_URL, openWithUrl)?.also {
|
||||
openWithUrl = it
|
||||
}
|
||||
}
|
||||
|
||||
private fun setBoolean(label: String, value: Boolean) {
|
||||
@@ -67,8 +106,14 @@ class Preferences {
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ZOOM_BY_SWIPING = "zoom_by_swiping"
|
||||
const val OPEN_IMMEDIATELY = "open_immediately"
|
||||
const val VIBRATE = "vibrate"
|
||||
const val USE_HISTORY = "use_history"
|
||||
const val IGNORE_CONSECUTIVE_DUPLICATES = "ignore_consecutive_duplicates"
|
||||
const val SHOW_HEX_DUMP = "show_hex_dump"
|
||||
const val SHOW_META_DATA = "show_meta_data"
|
||||
const val TRY_HARDER = "try_harder"
|
||||
const val OPEN_WITH_URL = "open_with_url"
|
||||
const val INDEX_OF_LAST_SELECTED_FORMAT = "index_of_last_selected_format"
|
||||
}
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
package de.markusfisch.android.binaryeye.repository
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import de.markusfisch.android.binaryeye.data.Database
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class DatabaseRepository {
|
||||
private val db = Database()
|
||||
|
||||
fun open(applicationContext: Context) {
|
||||
db.open(applicationContext)
|
||||
}
|
||||
|
||||
fun getScan(id: Long): Scan? = getScan(id) { _, cursor ->
|
||||
val timeIndex = cursor.getColumnIndex(Database.SCANS_DATETIME)
|
||||
val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
|
||||
val rawIndex = cursor.getColumnIndex(Database.SCANS_RAW)
|
||||
val formatIndex = cursor.getColumnIndex(Database.SCANS_FORMAT)
|
||||
Scan(
|
||||
id = id,
|
||||
timestamp = cursor.getString(timeIndex),
|
||||
content = cursor.getString(contentIndex),
|
||||
raw = cursor.getBlob(rawIndex),
|
||||
format = cursor.getString(formatIndex)
|
||||
)
|
||||
}
|
||||
|
||||
fun <T> getScan(id: Long, map: (id: Long, Cursor) -> T): T? =
|
||||
db.getScan(id)?.use { map(id, it.apply { moveToFirst() }) }
|
||||
|
||||
fun getScans(): Flow<SimpleScan> = getScans { cursor ->
|
||||
val idIndex = cursor.getColumnIndex(Database.SCANS_ID)
|
||||
val timeIndex = cursor.getColumnIndex(Database.SCANS_DATETIME)
|
||||
val contentIndex = cursor.getColumnIndex(Database.SCANS_CONTENT)
|
||||
val formatIndex = cursor.getColumnIndex(Database.SCANS_FORMAT)
|
||||
SimpleScan(
|
||||
id = cursor.getLong(idIndex),
|
||||
timestamp = cursor.getString(timeIndex),
|
||||
content = cursor.getString(contentIndex),
|
||||
format = cursor.getString(formatIndex)
|
||||
)
|
||||
}
|
||||
|
||||
fun <T> getScans(map: suspend (Cursor) -> T): Flow<T> = flow {
|
||||
db.getScans()?.use { results ->
|
||||
results.asIterable.forEach {
|
||||
emit(it)
|
||||
}
|
||||
}
|
||||
}.map(map).flowOn(Dispatchers.IO)
|
||||
|
||||
fun getScansCursor(): Cursor? = db.getScans()
|
||||
|
||||
fun hasBinaryData() = db.hasBinaryData()?.use { it.count > 0 } ?: false
|
||||
|
||||
fun insertScan(
|
||||
timestamp: Long,
|
||||
content: String,
|
||||
raw: ByteArray?,
|
||||
format: String
|
||||
): Long = db.insertScan(timestamp, content, raw, format)
|
||||
|
||||
fun removeScan(id: Long) = db.removeScan(id)
|
||||
|
||||
fun removeScans() = db.removeScans()
|
||||
|
||||
data class SimpleScan(
|
||||
val id: Long,
|
||||
val timestamp: String,
|
||||
val content: String,
|
||||
val format: String
|
||||
)
|
||||
|
||||
data class Scan(
|
||||
val id: Long,
|
||||
val timestamp: String,
|
||||
val content: String,
|
||||
val raw: ByteArray?,
|
||||
val format: String
|
||||
) {
|
||||
// Needed to be overwritten manually, as ByteArray is an array and this isn't handled well by Kotlin
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Scan
|
||||
|
||||
if (id != other.id) return false
|
||||
if (timestamp != other.timestamp) return false
|
||||
if (content != other.content) return false
|
||||
if (raw != null) {
|
||||
if (other.raw == null) return false
|
||||
if (!raw.contentEquals(other.raw)) return false
|
||||
} else if (other.raw != null) return false
|
||||
if (format != other.format) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Needs to be overwritten manually, as ByteArray is an array and this
|
||||
// isn't handled well by Kotlin
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + timestamp.hashCode()
|
||||
result = 31 * result + content.hashCode()
|
||||
result = 31 * result + (raw?.contentHashCode() ?: 0)
|
||||
result = 31 * result + format.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val Cursor.asIterable: Iterable<Cursor>
|
||||
get() = object : Iterable<Cursor> {
|
||||
override fun iterator(): Iterator<Cursor> = object : Iterator<Cursor> {
|
||||
override fun hasNext(): Boolean = position + 1 < count
|
||||
override fun next(): Cursor = if (moveToNext()) {
|
||||
this@asIterable
|
||||
} else {
|
||||
throw IllegalStateException("You can't access more items, then there are")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,28 @@ package de.markusfisch.android.binaryeye.view
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
suspend inline fun View.useVisibility(
|
||||
whileExecuting: Int = View.VISIBLE,
|
||||
otherwise: Int = View.GONE,
|
||||
crossinline block: suspend () -> Unit
|
||||
) {
|
||||
if (visibility == whileExecuting) {
|
||||
return
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
visibility = whileExecuting
|
||||
}
|
||||
try {
|
||||
block()
|
||||
} finally {
|
||||
withContext(Dispatchers.Main) {
|
||||
visibility = otherwise
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun View.setPadding(rect: Rect) {
|
||||
this.setPadding(
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package de.markusfisch.android.binaryeye.view
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.support.v4.view.ViewCompat
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.view.View
|
||||
|
||||
private val windowInsets = Rect()
|
||||
private var windowInsetsSet = false
|
||||
private var windowInsetsListener: ((insets: Rect) -> Unit)? = null
|
||||
|
||||
// invoke this from Fragment.onCreateView() *or* Activity.onCreate()
|
||||
fun setWindowInsetListener(listener: (insets: Rect) -> Unit) {
|
||||
if (windowInsetsSet) {
|
||||
// invoke listener for previously set window insets because
|
||||
// setOnApplyWindowInsetsListener() won't fire again if we
|
||||
// change Fragments
|
||||
listener(windowInsets)
|
||||
}
|
||||
// *always* set the listener in case there is a configuration change
|
||||
windowInsetsListener = listener
|
||||
}
|
||||
|
||||
// invoke this from Activity.onCreate() to setup listening for insets
|
||||
fun setupInsets(view: View, toolbar: Toolbar) {
|
||||
val toolBarHeight = toolbar.layoutParams.height
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
// save insets so setWindowInsetListener() can return them
|
||||
// when it's called from Fragment.onCreateView() later on
|
||||
windowInsets.set(0, toolBarHeight, 0, 0)
|
||||
windowInsetsSet = true
|
||||
windowInsetsListener?.also { it(windowInsets) }
|
||||
return
|
||||
}
|
||||
ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets ->
|
||||
// runs *after* layout and thus *after* Fragment.onCreateView()
|
||||
if (insets.hasSystemWindowInsets()) {
|
||||
windowInsets.set(
|
||||
insets.systemWindowInsetLeft,
|
||||
insets.systemWindowInsetTop + toolBarHeight,
|
||||
insets.systemWindowInsetRight,
|
||||
insets.systemWindowInsetBottom
|
||||
)
|
||||
windowInsetsSet = true
|
||||
windowInsetsListener?.also { it(windowInsets) }
|
||||
}
|
||||
insets
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ class ConfinedScrollView : ScrollView {
|
||||
) {
|
||||
super.onLayout(changed, left, top, right, bottom)
|
||||
if (changed) {
|
||||
// give Android some time to settle down before running this;
|
||||
// give Android some time to settle down before running this,
|
||||
// not putting it on the queue makes it only work sometimes
|
||||
post {
|
||||
getChildAt(0)?.also { child ->
|
||||
|
||||
@@ -10,6 +10,10 @@ import de.markusfisch.android.binaryeye.graphics.Candidates
|
||||
|
||||
class DetectorView : View {
|
||||
private val candidates = Candidates(context)
|
||||
private val invalidateRunnable: Runnable = Runnable {
|
||||
marks = null
|
||||
invalidate()
|
||||
}
|
||||
private var marks: List<Point>? = null
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) :
|
||||
@@ -21,10 +25,8 @@ class DetectorView : View {
|
||||
fun mark(points: List<Point>) {
|
||||
marks = points
|
||||
invalidate()
|
||||
postDelayed({
|
||||
marks = null
|
||||
invalidate()
|
||||
}, 500)
|
||||
removeCallbacks(invalidateRunnable)
|
||||
postDelayed(invalidateRunnable, 500)
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package de.markusfisch.android.binaryeye.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
|
||||
fun Context.toast(message: Int) = Toast.makeText(
|
||||
this,
|
||||
message,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
|
||||
fun Context.toast(message: String) = Toast.makeText(
|
||||
this,
|
||||
message,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
@@ -5,7 +5,8 @@ import com.google.zxing.*
|
||||
import com.google.zxing.common.HybridBinarizer
|
||||
import java.util.*
|
||||
|
||||
class Zxing {
|
||||
class Zxing(possibleResultPoint: ResultPointCallback? = null) {
|
||||
private val hints = EnumMap<DecodeHintType, Any>(DecodeHintType::class.java)
|
||||
private val multiFormatReader = MultiFormatReader()
|
||||
|
||||
init {
|
||||
@@ -35,11 +36,18 @@ class Zxing {
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val hints = EnumMap<DecodeHintType, Any>(DecodeHintType::class.java)
|
||||
hints[DecodeHintType.POSSIBLE_FORMATS] = decodeFormats
|
||||
possibleResultPoint?.let {
|
||||
hints[DecodeHintType.NEED_RESULT_POINT_CALLBACK] = it
|
||||
}
|
||||
}
|
||||
|
||||
multiFormatReader.setHints(hints)
|
||||
fun updateHints(tryHarder: Boolean) {
|
||||
if (tryHarder) {
|
||||
hints[DecodeHintType.TRY_HARDER] = java.lang.Boolean.TRUE
|
||||
} else {
|
||||
hints.remove(DecodeHintType.TRY_HARDER)
|
||||
}
|
||||
}
|
||||
|
||||
fun decode(
|
||||
@@ -61,21 +69,15 @@ class Zxing {
|
||||
return decodeLuminanceSource(source, invert)
|
||||
}
|
||||
|
||||
fun decodePositiveNegative(bitmap: Bitmap): Result? {
|
||||
val result = decode(bitmap, false)
|
||||
return if (result != null) {
|
||||
result
|
||||
} else {
|
||||
decode(bitmap, true)
|
||||
}
|
||||
}
|
||||
fun decodePositiveNegative(bitmap: Bitmap): Result? =
|
||||
decode(bitmap, false) ?: decode(bitmap, true)
|
||||
|
||||
fun decode(bitmap: Bitmap, invert: Boolean = false): Result? {
|
||||
private fun decode(bitmap: Bitmap, invert: Boolean = false): Result? {
|
||||
val pixels = IntArray(bitmap.width * bitmap.height)
|
||||
return decode(pixels, bitmap, invert)
|
||||
}
|
||||
|
||||
fun decode(
|
||||
private fun decode(
|
||||
pixels: IntArray,
|
||||
bitmap: Bitmap,
|
||||
invert: Boolean = false
|
||||
@@ -109,7 +111,7 @@ class Zxing {
|
||||
private fun decodeLuminanceSource(source: LuminanceSource): Result? {
|
||||
val bitmap = BinaryBitmap(HybridBinarizer(source))
|
||||
return try {
|
||||
multiFormatReader.decodeWithState(bitmap)
|
||||
multiFormatReader.decode(bitmap, hints)
|
||||
} catch (e: ReaderException) {
|
||||
null
|
||||
} finally {
|
||||
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 899 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
@@ -1,5 +0,0 @@
|
||||
<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">
|
||||
<path android:fillColor="#FF000000" android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2L18,7L6,7v12zM8.46,11.88l1.41,-1.41L12,12.59l2.12,-2.12 1.41,1.41L13.41,14l2.12,2.12 -1.41,1.41L12,15.41l-2.12,2.12 -1.41,-1.41L10.59,14l-2.13,-2.12zM15.5,4l-1,-1h-5l-1,1L5,4v2h14L19,4z"/>
|
||||
</vector>
|
||||
@@ -1,9 +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">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"/>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" 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,9 +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">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0">
|
||||
<path android:fillColor="#FFFFFFFF" android:pathData="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||
</vector>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector 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">
|
||||
<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>
|
||||
@@ -0,0 +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">
|
||||
<path android:fillColor="#FF000000" android:pathData="M9 3L5 6.99h3V14h2V6.99h3L9 3zm7 14.01V10h-2v7.01h-3L15 21l4-3.99h-3z"/>
|
||||
</vector>
|
||||
@@ -1,9 +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">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M7,2v11h3v9l7,-12h-4l4,-8z"/>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" 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,5 +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">
|
||||
<path android:fillColor="#FF000000" android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-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,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z"/>
|
||||
<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">
|
||||
<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,9 +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">
|
||||
<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,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"/>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" 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,6 +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">
|
||||
<path android:fillColor="#FF000000"
|
||||
android:pathData="M20,4L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM20,18L4,18L4,8l8,5 8,-5v10zM12,11L4,6h16l-8,5z"/>
|
||||
<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">
|
||||
<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,5 +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">
|
||||
<path android:fillColor="#FF000000" android:pathData="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z"/>
|
||||
<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">
|
||||
<path android:fillColor="#FF000000" android:pathData="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>
|
||||
</vector>
|
||||
|
||||
@@ -1,9 +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">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M19,4L5,4c-1.11,0 -2,0.9 -2,2v12c0,1.1 0.89,2 2,2h4v-2L5,18L5,8h14v10h-4v2h4c1.1,0 2,-0.9 2,-2L21,6c0,-1.1 -0.89,-2 -2,-2zM12,10l-4,4h3v6h2v-6h3l-4,-4z"/>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" 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,9 +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">
|
||||
<path android:fillColor="#FF000000"
|
||||
android:pathData="M14,10L2,10v2h12v-2zM14,6L2,6v2h12L14,6zM18,14v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zM2,16h8v-2L2,14v2z"/>
|
||||
<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">
|
||||
<path android:fillColor="#FF000000" android:pathData="M14 10H2v2h12v-2zm0-4H2v2h12V6zm4 8v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zM2 16h8v-2H2v2z"/>
|
||||
</vector>
|
||||
|
||||
@@ -1,5 +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">
|
||||
<path android:fillColor="#FF000000" android:pathData="M6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6L6,2zM13,9L13,3.5L18.5,9L13,9z"/>
|
||||
<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">
|
||||
<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,5 +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">
|
||||
<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.6L19.4,5.5a0.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.4H10.1a0.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,-0.1 0.9,2.8 2.8,0 0,0 0.1,0.9L2.8,14.5a0.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,15.6 12,3.6 3.6,0 0,1 12,15.6Z"/>
|
||||
<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">
|
||||
<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,5 +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">
|
||||
<path android:fillColor="#FF000000" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13L7,13v-2h10v2z"/>
|
||||
<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">
|
||||
<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,5 +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">
|
||||
<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.02c-2.84,-0.48 -5,-2.94 -5,-5.91s2.16,-5.43 5,-5.91L11,10l4.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.03zM16.89,15.48l1.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 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">
|
||||
<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,5 +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">
|
||||
<path android:fillColor="#FF000000" android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/>
|
||||
<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">
|
||||
<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,5 +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">
|
||||
<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.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.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 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">
|
||||
<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,6 +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">
|
||||
<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.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.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 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">
|
||||
<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,9 +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">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.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 xmlns:android="http://schemas.android.com/apk/res/android" 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,6 +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">
|
||||
<path android:fillColor="#FF000000"
|
||||
android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM9,11L7,11L7,9h2v2zM13,11h-2L11,9h2v2zM17,11h-2L15,9h2v2z"/>
|
||||
<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">
|
||||
<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,5 +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">
|
||||
<path android:fillColor="#FF000000" android:pathData="M20,4h-3.17L15,2L9,2L7.17,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM15,15.5L15,13L9,13v2.5L5.5,12 9,8.5L9,11h6L15,8.5l3.5,3.5 -3.5,3.5z"/>
|
||||
<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">
|
||||
<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>
|
||||
|
||||
@@ -1,6 +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">
|
||||
<path android:fillColor="#FF000000"
|
||||
android:pathData="M6.62,10.79c1.44,2.83 3.76,5.14 6.59,6.59l2.2,-2.2c0.27,-0.27 0.67,-0.36 1.02,-0.24 1.12,0.37 2.33,0.57 3.57,0.57 0.55,0 1,0.45 1,1V20c0,0.55 -0.45,1 -1,1 -9.39,0 -17,-7.61 -17,-17 0,-0.55 0.45,-1 1,-1h3.5c0.55,0 1,0.45 1,1 0,1.25 0.2,2.45 0.57,3.57 0.11,0.35 0.03,0.74 -0.25,1.02l-2.2,2.2z"/>
|
||||
<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">
|
||||
<path android:fillColor="#FF000000" android:pathData="M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c0.27-0.27 0.67-0.36 1.02-0.24 1.12 0.37 2.33 0.57 3.57 0.57 0.55 0 1 0.45 1 1V20c0 0.55-0.45 1-1 1-9.39 0-17-7.61-17-17 0-0.55 0.45-1 1-1h3.5c0.55 0 1 0.45 1 1 0 1.25 0.2 2.45 0.57 3.57 0.11 0.35 0.03 0.74-0.25 1.02l-2.2 2.2z"/>
|
||||
</vector>
|
||||
|
||||
@@ -1,6 +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">
|
||||
<path android:fillColor="#FF000000"
|
||||
android:pathData="M19,3h-1L18,1h-2v2L8,3L8,1L6,1v2L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM12,6c1.66,0 3,1.34 3,3s-1.34,3 -3,3 -3,-1.34 -3,-3 1.34,-3 3,-3zM18,18L6,18v-1c0,-2 4,-3.1 6,-3.1s6,1.1 6,3.1v1z"/>
|
||||
<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">
|
||||
<path android:fillColor="#FF000000" android:pathData="M19 3h-1V1h-2v2H8V1H6v2H5C3.89 3 3 3.9 3 5v14c0 1.1 0.89 2 2 2h14c1.1 0 2-0.9 2-2V5c0-1.1-0.9-2-2-2zm-7 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm6 12H6v-1c0-2 4-3.1 6-3.1s6 1.1 6 3.1v1z"/>
|
||||
</vector>
|
||||
|
||||
@@ -1,6 +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">
|
||||
<path android:fillColor="#FF000000"
|
||||
android:pathData="M17,12h-5v5h5v-5zM16,1v2L8,3L8,1L6,1v2L5,3c-1.11,0 -1.99,0.9 -1.99,2L3,19c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2h-1L18,1h-2zM19,19L5,19L5,8h14v11z"/>
|
||||
<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">
|
||||
<path android:fillColor="#FF000000" android:pathData="M17 12h-5v5h5v-5zM16 1v2H8V1H6v2H5C3.89 3 3.01 3.9 3.01 5L3 19c0 1.1 0.89 2 2 2h14c1.1 0 2-0.9 2-2V5c0-1.1-0.9-2-2-2h-1V1h-2zm3 18H5V8h14v11z"/>
|
||||
</vector>
|
||||
|
||||
@@ -1,5 +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">
|
||||
<path android:fillColor="#FF000000" android:pathData="M1,9l2,2c4.97,-4.97 13.03,-4.97 18,0l2,-2C16.93,2.93 7.08,2.93 1,9zM9,17l3,3 3,-3c-1.65,-1.66 -4.34,-1.66 -6,0zM5,13l2,2c2.76,-2.76 7.24,-2.76 10,0l2,-2C15.14,9.14 8.87,9.14 5,13z"/>
|
||||
<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">
|
||||
<path android:fillColor="#FF000000" android:pathData="M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.08 2.93 1 9zm8 8l3 3 3-3c-1.65-1.66-4.34-1.66-6 0zm-4-4l2 2c2.76-2.76 7.24-2.76 10 0l2-2C15.14 9.14 8.87 9.14 5 13z"/>
|
||||
</vector>
|
||||
|
||||
@@ -0,0 +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">
|
||||
<path android:fillColor="#FF000000" android:pathData="M17.63 5.84C17.27 5.33 16.67 5 16 5L5 5.01C3.9 5.01 3 5.9 3 7v10c0 1.1 0.9 1.99 2 1.99L16 19c0.67 0 1.27-0.33 1.63-0.84L22 12l-4.37-6.16z"/>
|
||||
</vector>
|
||||
@@ -1,70 +1,14 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M32.002,-24.974h3.127v7.656h-3.127z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M52,-884.362h29v71h-29z"/>
|
||||
<path
|
||||
android:pathData="m54.826,27.893c-6.851,0 -13.701,2.581 -18.924,7.745 -10.445,10.328 -10.413,27.062 0,37.423l36.218,36.036 37.879,0.902 -0.07,-38.363 -36.18,-35.998c-5.207,-5.18 -12.073,-7.745 -18.924,-7.745z"
|
||||
android:strokeAlpha="1"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="8.60725021"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillType="nonZero"
|
||||
android:fillAlpha="0.1254902"
|
||||
android:strokeLineCap="butt"/>
|
||||
<path
|
||||
android:pathData="M28.001,54.118a26.525,26.227 0,1 0,53.049 0a26.525,26.227 0,1 0,-53.049 0z"
|
||||
android:strokeAlpha="1"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="4"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillAlpha="1"
|
||||
android:strokeLineCap="butt"/>
|
||||
<path
|
||||
android:pathData="M39.26,54.988a14.509,14.346 0,1 0,29.019 0a14.509,14.346 0,1 0,-29.019 0z"
|
||||
android:strokeAlpha="1"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="4"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillAlpha="1"
|
||||
android:strokeLineCap="butt"/>
|
||||
<path
|
||||
android:pathData="M54.034,44.513a9.535,9.428 0,1 0,19.07 0a9.535,9.428 0,1 0,-19.07 0z"
|
||||
android:strokeAlpha="1"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="10"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillAlpha="1"
|
||||
android:strokeLineCap="butt"/>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="108dp" android:height="108dp" android:viewportWidth="108" android:viewportHeight="108">
|
||||
<path android:fillColor="#FF000000" android:pathData="M52-884.362h29v71H52z"/>
|
||||
<path android:fillColor="#FF000000" android:pathData="M52-884.362h29v71H52z"/>
|
||||
<path android:fillColor="#FF000000" android:pathData="M52-884.362h29v71H52zM32.002-24.974h3.127v7.656h-3.127z"/>
|
||||
<path android:fillColor="#FF000000" android:pathData="M52-884.362h29v71H52z"/>
|
||||
<path android:fillColor="#FF000000" android:pathData="M52-884.362h29v71H52z"/>
|
||||
<path android:fillColor="#FF000000" android:pathData="M52-884.362h29v71H52z"/>
|
||||
<path android:fillColor="#FF000000" android:pathData="M52-884.362h29v71H52z"/>
|
||||
<path android:fillColor="#FF000000" android:pathData="M52-884.362h29v71H52z"/>
|
||||
<path android:pathData="M54.826 27.893c-6.851 0-13.701 2.581-18.924 7.745-10.445 10.328-10.413 27.062 0 37.423l36.218 36.036 37.879 0.902-0.07-38.363-36.18-35.998c-5.207-5.18-12.073-7.745-18.924-7.745z" android:strokeWidth="8.60725021" android:fillColor="#000000" android:fillAlpha="0.1254902"/>
|
||||
<path android:pathData="M28.001 54.118a26.525 26.227 0 1 0 53.049 0 26.525 26.227 0 1 0-53.049 0z" android:strokeWidth="4" android:fillColor="#ffffff"/>
|
||||
<path android:pathData="M39.26 54.988a14.509 14.346 0 1 0 29.019 0 14.509 14.346 0 1 0-29.019 0z" android:strokeWidth="4" android:fillColor="#000000"/>
|
||||
<path android:pathData="M54.034 44.513a9.535 9.428 0 1 0 19.07 0 9.535 9.428 0 1 0-19.07 0z" android:strokeWidth="10" android:fillColor="#ffffff"/>
|
||||
</vector>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@color/selected_row"
|
||||
android:state_pressed="true"/>
|
||||
<item android:drawable="@color/selected_row"
|
||||
android:state_selected="true"/>
|
||||
<item android:drawable="@android:color/transparent"/>
|
||||
</selector>
|
||||
@@ -0,0 +1,22 @@
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:text="@string/enter_name"/>
|
||||
<EditText
|
||||
android:id="@+id/name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textShortMessage|textAutoCorrect|textCapSentences"
|
||||
android:imeOptions="actionDone"
|
||||
android:hint="@string/enter_name_hint"
|
||||
android:maxLines="1"
|
||||
android:maxLength="1024"
|
||||
android:importantForAutofill="no"/>
|
||||
</LinearLayout>
|
||||
@@ -36,12 +36,21 @@
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:gravity="end|top" />
|
||||
android:gravity="end|top"/>
|
||||
<TableLayout
|
||||
android:id="@+id/meta"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/format"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginRight="16dp"/>
|
||||
<TextView
|
||||
android:id="@+id/hex"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/format"
|
||||
android:layout_below="@+id/meta"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
android:layout_height="match_parent">
|
||||
<ListView
|
||||
android:id="@+id/scans"
|
||||
android:choiceMode="singleChoice"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:choiceMode="singleChoice"
|
||||
android:clipToPadding="false"
|
||||
android:divider="@color/separator"
|
||||
android:dividerHeight="1px"/>
|
||||
<android.support.design.widget.CoordinatorLayout
|
||||
|
||||
@@ -10,18 +10,54 @@
|
||||
android:layout_height="match_parent"
|
||||
android:padding="16dp"
|
||||
android:orientation="vertical">
|
||||
<android.support.v7.widget.SwitchCompat
|
||||
android:id="@+id/zoom_by_swiping"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:text="@string/zoom_by_swiping"/>
|
||||
<android.support.v7.widget.SwitchCompat
|
||||
android:id="@+id/open_immediately"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:text="@string/open_immediately"/>
|
||||
<android.support.v7.widget.SwitchCompat
|
||||
android:id="@+id/vibrate"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:text="@string/vibrate"/>
|
||||
<android.support.v7.widget.SwitchCompat
|
||||
android:id="@+id/use_history"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:text="@string/use_history"/>
|
||||
<android.support.v7.widget.SwitchCompat
|
||||
android:id="@+id/ignore_consecutive_duplicates"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:text="@string/ignore_consecutive_duplicates"/>
|
||||
<android.support.v7.widget.SwitchCompat
|
||||
android:id="@+id/show_meta_data"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:text="@string/show_meta_data"/>
|
||||
<android.support.v7.widget.SwitchCompat
|
||||
android:id="@+id/show_hex_dump"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:text="@string/show_hex_dump"/>
|
||||
<android.support.v7.widget.SwitchCompat
|
||||
android:id="@+id/try_harder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:text="@string/try_harder"/>
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:background="@drawable/list_selector"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="16dp">
|
||||
@@ -28,6 +29,9 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/time"
|
||||
android:layout_marginTop="4dp"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:drawablePadding="16dp"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:typeface="monospace"/>
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:material="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
<group android:id="@+id/scans_available"
|
||||
android:visible="false">
|
||||
<item
|
||||
android:id="@+id/search"
|
||||
android:icon="@drawable/ic_action_search"
|
||||
android:title="@string/search_scan"
|
||||
material:actionViewClass="android.support.v7.widget.SearchView"
|
||||
material:showAsAction="collapseActionView|ifRoom"/>
|
||||
<item
|
||||
android:id="@+id/export_history"
|
||||
android:icon="@drawable/ic_action_export"
|
||||
android:title="@string/export_to_file"
|
||||
android:icon="@drawable/ic_action_save"
|
||||
material:showAsAction="ifRoom"/>
|
||||
<item
|
||||
<item
|
||||
android:id="@+id/clear"
|
||||
android:icon="@drawable/ic_action_remove"
|
||||
android:title="@string/clear_history"
|
||||
android:icon="@drawable/ic_action_clear"
|
||||
material:showAsAction="ifRoom"/>
|
||||
</group>
|
||||
</menu>
|
||||
|
||||