Show parsed content types in history
Store content types and use it in the history.
This commit is contained in:
@@ -24,21 +24,12 @@ import android.widget.LinearLayout
|
||||
import android.widget.TableLayout
|
||||
import android.widget.TableRow
|
||||
import android.widget.TextView
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.widget.TextViewCompat
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.ActionRegistry
|
||||
import de.markusfisch.android.binaryeye.actions.mail.MatMsg
|
||||
import de.markusfisch.android.binaryeye.actions.mail.MatMsgAction
|
||||
import de.markusfisch.android.binaryeye.actions.otpauth.OtpauthAction
|
||||
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
|
||||
import de.markusfisch.android.binaryeye.actions.vtype.vcard.VCardAction
|
||||
import de.markusfisch.android.binaryeye.actions.vtype.vevent.VEventAction
|
||||
import de.markusfisch.android.binaryeye.actions.web.WebAction
|
||||
import de.markusfisch.android.binaryeye.actions.wifi.WifiAction
|
||||
import de.markusfisch.android.binaryeye.actions.wifi.WifiConnector
|
||||
import de.markusfisch.android.binaryeye.adapter.prettifyFormatName
|
||||
import de.markusfisch.android.binaryeye.adapter.toFormatDescriptionResId
|
||||
import de.markusfisch.android.binaryeye.app.db
|
||||
@@ -46,13 +37,10 @@ import de.markusfisch.android.binaryeye.app.hasLocationPermission
|
||||
import de.markusfisch.android.binaryeye.app.hasWritePermission
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.content.ContentBarcode
|
||||
import de.markusfisch.android.binaryeye.content.EpcQrParser
|
||||
import de.markusfisch.android.binaryeye.content.IdlParser
|
||||
import de.markusfisch.android.binaryeye.content.SealParser
|
||||
import de.markusfisch.android.binaryeye.content.ParsedContentType
|
||||
import de.markusfisch.android.binaryeye.content.copyToClipboard
|
||||
import de.markusfisch.android.binaryeye.content.epcQrToRes
|
||||
import de.markusfisch.android.binaryeye.content.idlToRes
|
||||
import de.markusfisch.android.binaryeye.content.openUrl
|
||||
import de.markusfisch.android.binaryeye.content.parseData
|
||||
import de.markusfisch.android.binaryeye.content.shareAsFile
|
||||
import de.markusfisch.android.binaryeye.content.shareText
|
||||
import de.markusfisch.android.binaryeye.content.toBarcode
|
||||
@@ -74,11 +62,10 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
import kotlin.math.roundToInt
|
||||
import de.markusfisch.android.binaryeye.content.ParsedField as Field
|
||||
|
||||
class DecodeActivity : AbstractBaseActivity() {
|
||||
private lateinit var labelHeadlineView: TextView
|
||||
@@ -371,19 +358,20 @@ class DecodeActivity : AbstractBaseActivity() {
|
||||
isEditing: Boolean
|
||||
) {
|
||||
val items = mutableListOf<Field>()
|
||||
val parsedDataTitleResId = parseData(items, text, bytes)
|
||||
val parsedData = parseData(this@DecodeActivity, text, bytes, format, action)
|
||||
items.addAll(parsedData.fields)
|
||||
when (prefs.showChecksum) {
|
||||
"CRC4" -> items.addField(R.string.crc4, String.format("%X", crc4(bytes)))
|
||||
"MD5" -> items.addField(R.string.md5, bytes.md5().toHexString().fold())
|
||||
"SHA1" -> items.addField(R.string.sha1, bytes.sha1().toHexString().fold())
|
||||
"SHA256" -> items.addField(R.string.sha256, bytes.sha256().toHexString().fold())
|
||||
}
|
||||
if (parsedDataTitleResId != 0) {
|
||||
if (parsedData.type != ParsedContentType.UNKNOWN) {
|
||||
items.add(
|
||||
0,
|
||||
Field(
|
||||
R.string.parsed_type,
|
||||
getString(parsedDataTitleResId)
|
||||
getString(parsedData.type.resId)
|
||||
)
|
||||
)
|
||||
lastParsedDataItems = items
|
||||
@@ -399,135 +387,6 @@ class DecodeActivity : AbstractBaseActivity() {
|
||||
fillDataItems(items, false)
|
||||
}
|
||||
|
||||
private fun parseData(
|
||||
items: MutableList<Field>,
|
||||
text: String,
|
||||
bytes: ByteArray
|
||||
): Int {
|
||||
val ctx = this
|
||||
|
||||
val trackingLink = bytes.generateDpTrackingLink(format)
|
||||
if (trackingLink != null) {
|
||||
items.addField(
|
||||
getString(R.string.tracking_number),
|
||||
trackingLink.label,
|
||||
trackingLink.link,
|
||||
)
|
||||
return R.string.parsed_type_deutsche_post
|
||||
}
|
||||
|
||||
try {
|
||||
items.addField(
|
||||
R.string.formatted_json,
|
||||
JSONObject(text).toString(2)
|
||||
)
|
||||
return R.string.parsed_type_json
|
||||
} catch (_: JSONException) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
IdlParser.parse(String(bytes))?.let {
|
||||
items.addField("IIN", it.iin)
|
||||
it.elements.forEach { (id, value) ->
|
||||
items.addField(ctx.idlToRes(id), value)
|
||||
}
|
||||
return R.string.parsed_type_international_driver_license
|
||||
}
|
||||
|
||||
SealParser.parse(ctx, bytes)?.let { sealFields ->
|
||||
sealFields.forEach { vf ->
|
||||
items.addField(vf.name, vf.value)
|
||||
}
|
||||
return R.string.parsed_type_digital_seal
|
||||
}
|
||||
|
||||
EpcQrParser.parse(text)?.let {
|
||||
items.addAll(it.map { (id, value) ->
|
||||
Field(ctx.epcQrToRes(id), value)
|
||||
})
|
||||
return R.string.parsed_type_sepa_epc_qr
|
||||
}
|
||||
|
||||
when (action) {
|
||||
is MatMsgAction -> MatMsg(text).run {
|
||||
items.addField(R.string.email_to, to)
|
||||
items.addField(R.string.email_subject, sub)
|
||||
items.addField(R.string.email_body, body)
|
||||
return R.string.parsed_type_email
|
||||
}
|
||||
|
||||
is VCardAction,
|
||||
is VEventAction -> VTypeParser.parseMap(text).let { parsedMap ->
|
||||
parsedMap.forEach { item ->
|
||||
items.addField(item.key, item.value.joinToString("\n") { it.value })
|
||||
}
|
||||
return if (action is VCardAction) {
|
||||
R.string.parsed_type_contact_card
|
||||
} else {
|
||||
R.string.parsed_type_calendar_event
|
||||
}
|
||||
}
|
||||
|
||||
is WebAction -> try {
|
||||
text.toUri().run {
|
||||
items.addField(R.string.scheme, scheme)
|
||||
items.addField(R.string.host, host)
|
||||
items.addField(R.string.query, query)
|
||||
}
|
||||
return R.string.parsed_type_url
|
||||
} catch (_: Exception) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
is OtpauthAction -> try {
|
||||
text.toUri().run {
|
||||
val label = pathSegments.firstOrNull()?.let {
|
||||
Uri.decode(it)
|
||||
} ?: ""
|
||||
val colonIdx = label.indexOf(':')
|
||||
val issuerFromLabel = if (colonIdx >= 0) {
|
||||
label.substring(0, colonIdx)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val account = if (colonIdx >= 0) {
|
||||
label.substring(colonIdx + 1).trim()
|
||||
} else {
|
||||
label
|
||||
}
|
||||
val issuer = getQueryParameter("issuer") ?: issuerFromLabel
|
||||
items.addField(R.string.entry_type, host?.uppercase())
|
||||
items.addField(R.string.otp_account, account)
|
||||
items.addField(R.string.otp_issuer, issuer)
|
||||
items.addField(R.string.otp_algorithm, getQueryParameter("algorithm"))
|
||||
items.addField(R.string.otp_digits, getQueryParameter("digits"))
|
||||
if (host == "totp") {
|
||||
items.addField(R.string.otp_period, getQueryParameter("period"))
|
||||
} else {
|
||||
items.addField(R.string.otp_counter, getQueryParameter("counter"))
|
||||
}
|
||||
}
|
||||
return R.string.parsed_type_otp
|
||||
} catch (_: Exception) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
is WifiAction -> WifiConnector.parseMap(text)?.let { wifiData ->
|
||||
items.addField(R.string.entry_type, getString(R.string.wifi_network))
|
||||
items.addField(R.string.wifi_ssid, wifiData["S"])
|
||||
items.addField(R.string.wifi_password, wifiData["P"])
|
||||
items.addField(R.string.wifi_type, wifiData["T"])
|
||||
items.addField(R.string.wifi_hidden, wifiData["H"])
|
||||
items.addField(R.string.wifi_eap, wifiData["E"])
|
||||
items.addField(R.string.wifi_identity, wifiData["I"])
|
||||
items.addField(R.string.wifi_anonymous_identity, wifiData["A"])
|
||||
items.addField(R.string.wifi_phase2, wifiData["PH2"])
|
||||
return R.string.parsed_type_wifi_network
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
private fun TableLayout.fillMetaView(scan: Scan) {
|
||||
val items = mutableListOf(
|
||||
Field(R.string.error_correction_level, scan.errorCorrectionLevel),
|
||||
@@ -887,12 +746,6 @@ class DecodeActivity : AbstractBaseActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private data class Field(
|
||||
val key: Any,
|
||||
val value: CharSequence?,
|
||||
val link: String? = null
|
||||
)
|
||||
|
||||
private fun MutableList<Field>.addField(
|
||||
key: Any,
|
||||
value: CharSequence?,
|
||||
@@ -959,57 +812,6 @@ private fun hexDump(bytes: ByteArray, charsPerLine: Int = 33): String {
|
||||
|
||||
private fun Int.positiveToString() = if (this > -1) this.toString() else ""
|
||||
|
||||
private data class TrackingLink(
|
||||
val label: String,
|
||||
val link: String
|
||||
)
|
||||
|
||||
private fun ByteArray.generateDpTrackingLink(format: String): TrackingLink? {
|
||||
// Check for Deutsche Post Matrixcode stamp.
|
||||
var isStamp = false
|
||||
var rawData = this
|
||||
if (format == ZxingCpp.BarcodeFormat.DataMatrix.name &&
|
||||
toString(Charsets.ISO_8859_1).startsWith("DEA5")
|
||||
) {
|
||||
if (size == 47) {
|
||||
isStamp = true
|
||||
} else if (size > 47) {
|
||||
// Transform back to original data.
|
||||
rawData = toString(Charsets.UTF_8).toByteArray(
|
||||
Charsets.ISO_8859_1
|
||||
)
|
||||
if (rawData.size == 47) {
|
||||
isStamp = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isStamp) {
|
||||
return null
|
||||
}
|
||||
|
||||
val hex = StringBuilder()
|
||||
hex.append(String.format("%02X", rawData[9]))
|
||||
hex.append(String.format("%02X", rawData[10]))
|
||||
hex.append(String.format("%02X", rawData[11]))
|
||||
hex.append(String.format("%02X", rawData[12]))
|
||||
hex.append(String.format("%02X", rawData[13]))
|
||||
hex.append(String.format("%X", (rawData[4].toInt() and 0x0f).toByte()))
|
||||
hex.append(String.format("%02X", rawData[5]))
|
||||
hex.append(String.format("%02X", rawData[6]))
|
||||
hex.append(String.format("%02X", rawData[7]))
|
||||
hex.append(String.format("%02X", rawData[8]))
|
||||
val hexString = hex.toString()
|
||||
val trackingNumber = hexString + String.format(
|
||||
"%X",
|
||||
crc4(hexString.toByteArray(Charsets.ISO_8859_1))
|
||||
)
|
||||
return TrackingLink(
|
||||
trackingNumber,
|
||||
"https://www.deutschepost.de/de/s/sendungsverfolgung.html?piececode=$trackingNumber",
|
||||
)
|
||||
}
|
||||
|
||||
// CRC-4 with polynomial x^4 + x + 1.
|
||||
private fun crc4(input: ByteArray): Int {
|
||||
var crc = 0
|
||||
|
||||
@@ -12,6 +12,8 @@ import android.widget.ImageView
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.content.ParsedContentType
|
||||
import de.markusfisch.android.binaryeye.content.parsedContentTypeFromName
|
||||
import de.markusfisch.android.binaryeye.database.Database
|
||||
import java.text.DateFormat
|
||||
import java.text.ParseException
|
||||
@@ -29,6 +31,7 @@ class ScansAdapter(
|
||||
private val timeIndex = cursor.getColumnIndex(Database.SCANS_DATETIME)
|
||||
private val nameIndex = cursor.getColumnIndex(Database.SCANS_NAME)
|
||||
private val textIndex = cursor.getColumnIndex(Database.SCANS_TEXT)
|
||||
private val contentTypeIndex = cursor.getColumnIndex(Database.SCANS_CONTENT_TYPE)
|
||||
private val formatIndex = cursor.getColumnIndex(Database.SCANS_FORMAT)
|
||||
private val selections = mutableMapOf<Long, Int>()
|
||||
fun select(view: View, id: Long, position: Int) {
|
||||
@@ -113,7 +116,7 @@ class ScansAdapter(
|
||||
}
|
||||
|
||||
text.isNullOrEmpty() -> context.getString(
|
||||
R.string.binary_data
|
||||
cursor.getString(contentTypeIndex).toBinaryDataStringResId()
|
||||
)
|
||||
|
||||
else -> text
|
||||
@@ -201,6 +204,15 @@ fun Context.setupFormatSpinner(spinner: Spinner): List<String> {
|
||||
fun String.prettifyFormatName() = replace("_", " ")
|
||||
.replace(formatWordBoundary, " ")
|
||||
|
||||
private fun String?.toBinaryDataStringResId(): Int {
|
||||
val type = parsedContentTypeFromName(this)
|
||||
return if (type == ParsedContentType.UNKNOWN) {
|
||||
R.string.binary_data
|
||||
} else {
|
||||
type.resId
|
||||
}
|
||||
}
|
||||
|
||||
fun String.toFormatDescriptionResId(): Int = when (this) {
|
||||
"Aztec" -> R.string.format_description_aztec
|
||||
"Codabar" -> R.string.format_description_codabar
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
package de.markusfisch.android.binaryeye.content
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import de.markusfisch.android.binaryeye.R
|
||||
import de.markusfisch.android.binaryeye.actions.Action
|
||||
import de.markusfisch.android.binaryeye.actions.ActionRegistry
|
||||
import de.markusfisch.android.binaryeye.actions.mail.MatMsg
|
||||
import de.markusfisch.android.binaryeye.actions.mail.MatMsgAction
|
||||
import de.markusfisch.android.binaryeye.actions.otpauth.OtpauthAction
|
||||
import de.markusfisch.android.binaryeye.actions.vtype.VTypeParser
|
||||
import de.markusfisch.android.binaryeye.actions.vtype.vcard.VCardAction
|
||||
import de.markusfisch.android.binaryeye.actions.vtype.vevent.VEventAction
|
||||
import de.markusfisch.android.binaryeye.actions.web.WebAction
|
||||
import de.markusfisch.android.binaryeye.actions.wifi.WifiAction
|
||||
import de.markusfisch.android.binaryeye.actions.wifi.WifiConnector
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
enum class ParsedContentType(
|
||||
val resId: Int
|
||||
) {
|
||||
UNKNOWN(0),
|
||||
DEUTSCHE_POST(R.string.parsed_type_deutsche_post),
|
||||
JSON(R.string.parsed_type_json),
|
||||
INTERNATIONAL_DRIVER_LICENSE(
|
||||
R.string.parsed_type_international_driver_license
|
||||
),
|
||||
DIGITAL_SEAL(R.string.parsed_type_digital_seal),
|
||||
SEPA_EPC_QR(R.string.parsed_type_sepa_epc_qr),
|
||||
EMAIL(R.string.parsed_type_email),
|
||||
URL(R.string.parsed_type_url),
|
||||
OTP(R.string.parsed_type_otp),
|
||||
CONTACT_CARD(R.string.parsed_type_contact_card),
|
||||
CALENDAR_EVENT(R.string.parsed_type_calendar_event),
|
||||
WIFI_NETWORK(R.string.parsed_type_wifi_network);
|
||||
}
|
||||
|
||||
data class ParsedData(
|
||||
val type: ParsedContentType,
|
||||
val fields: List<ParsedField>
|
||||
)
|
||||
|
||||
data class ParsedField(
|
||||
val key: Any,
|
||||
val value: CharSequence?,
|
||||
val link: String? = null
|
||||
)
|
||||
|
||||
fun parseData(
|
||||
context: Context,
|
||||
text: String,
|
||||
bytes: ByteArray,
|
||||
format: String,
|
||||
action: Action = ActionRegistry.getAction(bytes)
|
||||
): ParsedData {
|
||||
val fields = mutableListOf<ParsedField>()
|
||||
|
||||
DeutschePostParser.parse(bytes, format)?.let {
|
||||
fields.addField(
|
||||
context.getString(R.string.tracking_number),
|
||||
it.label,
|
||||
it.link,
|
||||
)
|
||||
return ParsedData(ParsedContentType.DEUTSCHE_POST, fields)
|
||||
}
|
||||
|
||||
try {
|
||||
fields.addField(R.string.formatted_json, JSONObject(text).toString(2))
|
||||
return ParsedData(ParsedContentType.JSON, fields)
|
||||
} catch (_: JSONException) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
IdlParser.parse(String(bytes))?.let {
|
||||
fields.addField("IIN", it.iin)
|
||||
it.elements.forEach { (id, value) ->
|
||||
fields.addField(context.idlToRes(id), value)
|
||||
}
|
||||
return ParsedData(ParsedContentType.INTERNATIONAL_DRIVER_LICENSE, fields)
|
||||
}
|
||||
|
||||
SealParser.parse(context, bytes)?.let { sealFields ->
|
||||
sealFields.forEach { vf ->
|
||||
fields.addField(vf.name, vf.value)
|
||||
}
|
||||
return ParsedData(ParsedContentType.DIGITAL_SEAL, fields)
|
||||
}
|
||||
|
||||
EpcQrParser.parse(text)?.let {
|
||||
fields.addAll(it.map { (id, value) ->
|
||||
ParsedField(context.epcQrToRes(id), value)
|
||||
})
|
||||
return ParsedData(ParsedContentType.SEPA_EPC_QR, fields)
|
||||
}
|
||||
|
||||
when (action) {
|
||||
is MatMsgAction -> MatMsg(text).run {
|
||||
fields.addField(R.string.email_to, to)
|
||||
fields.addField(R.string.email_subject, sub)
|
||||
fields.addField(R.string.email_body, body)
|
||||
return ParsedData(ParsedContentType.EMAIL, fields)
|
||||
}
|
||||
|
||||
is VCardAction,
|
||||
is VEventAction -> VTypeParser.parseMap(text).let { parsedMap ->
|
||||
parsedMap.forEach { item ->
|
||||
fields.addField(item.key, item.value.joinToString("\n") { it.value })
|
||||
}
|
||||
return ParsedData(
|
||||
if (action is VCardAction) {
|
||||
ParsedContentType.CONTACT_CARD
|
||||
} else {
|
||||
ParsedContentType.CALENDAR_EVENT
|
||||
},
|
||||
fields
|
||||
)
|
||||
}
|
||||
|
||||
is WebAction -> try {
|
||||
text.toUri().run {
|
||||
fields.addField(R.string.scheme, scheme)
|
||||
fields.addField(R.string.host, host)
|
||||
fields.addField(R.string.query, query)
|
||||
}
|
||||
return ParsedData(ParsedContentType.URL, fields)
|
||||
} catch (_: Exception) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
is OtpauthAction -> try {
|
||||
text.toUri().run {
|
||||
val label = pathSegments.firstOrNull()?.let {
|
||||
Uri.decode(it)
|
||||
} ?: ""
|
||||
val colonIdx = label.indexOf(':')
|
||||
val issuerFromLabel = if (colonIdx >= 0) {
|
||||
label.substring(0, colonIdx)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val account = if (colonIdx >= 0) {
|
||||
label.substring(colonIdx + 1).trim()
|
||||
} else {
|
||||
label
|
||||
}
|
||||
val issuer = getQueryParameter("issuer") ?: issuerFromLabel
|
||||
fields.addField(R.string.entry_type, host?.uppercase())
|
||||
fields.addField(R.string.otp_account, account)
|
||||
fields.addField(R.string.otp_issuer, issuer)
|
||||
fields.addField(R.string.otp_algorithm, getQueryParameter("algorithm"))
|
||||
fields.addField(R.string.otp_digits, getQueryParameter("digits"))
|
||||
if (host == "totp") {
|
||||
fields.addField(R.string.otp_period, getQueryParameter("period"))
|
||||
} else {
|
||||
fields.addField(R.string.otp_counter, getQueryParameter("counter"))
|
||||
}
|
||||
}
|
||||
return ParsedData(ParsedContentType.OTP, fields)
|
||||
} catch (_: Exception) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
is WifiAction -> WifiConnector.parseMap(text)?.let { wifiData ->
|
||||
fields.addField(R.string.wifi_ssid, wifiData["S"])
|
||||
fields.addField(R.string.wifi_password, wifiData["P"])
|
||||
fields.addField(R.string.wifi_type, wifiData["T"])
|
||||
fields.addField(R.string.wifi_hidden, wifiData["H"])
|
||||
fields.addField(R.string.wifi_eap, wifiData["E"])
|
||||
fields.addField(R.string.wifi_identity, wifiData["I"])
|
||||
fields.addField(R.string.wifi_anonymous_identity, wifiData["A"])
|
||||
fields.addField(R.string.wifi_phase2, wifiData["PH2"])
|
||||
return ParsedData(ParsedContentType.WIFI_NETWORK, fields)
|
||||
}
|
||||
}
|
||||
return ParsedData(ParsedContentType.UNKNOWN, emptyList())
|
||||
}
|
||||
|
||||
fun parsedContentTypeFromName(
|
||||
name: String?
|
||||
): ParsedContentType = try {
|
||||
ParsedContentType.valueOf(name ?: "")
|
||||
} catch (_: IllegalArgumentException) {
|
||||
ParsedContentType.UNKNOWN
|
||||
}
|
||||
|
||||
private fun MutableList<ParsedField>.addField(
|
||||
key: Any,
|
||||
value: CharSequence?,
|
||||
link: String? = null
|
||||
) {
|
||||
add(ParsedField(key, value, link))
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package de.markusfisch.android.binaryeye.content
|
||||
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp
|
||||
|
||||
data class DeutschePostTrackingLink(
|
||||
val label: String,
|
||||
val link: String
|
||||
)
|
||||
|
||||
object DeutschePostParser {
|
||||
fun parse(
|
||||
bytes: ByteArray,
|
||||
format: String
|
||||
): DeutschePostTrackingLink? {
|
||||
var isStamp = false
|
||||
var rawData = bytes
|
||||
if (format == ZxingCpp.BarcodeFormat.DataMatrix.name &&
|
||||
bytes.toString(Charsets.ISO_8859_1).startsWith("DEA5")
|
||||
) {
|
||||
if (bytes.size == 47) {
|
||||
isStamp = true
|
||||
} else if (bytes.size > 47) {
|
||||
// Transform back to original data.
|
||||
rawData = bytes.toString(Charsets.UTF_8).toByteArray(
|
||||
Charsets.ISO_8859_1
|
||||
)
|
||||
if (rawData.size == 47) {
|
||||
isStamp = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isStamp) {
|
||||
return null
|
||||
}
|
||||
|
||||
val hex = StringBuilder()
|
||||
hex.append(String.format("%02X", rawData[9]))
|
||||
hex.append(String.format("%02X", rawData[10]))
|
||||
hex.append(String.format("%02X", rawData[11]))
|
||||
hex.append(String.format("%02X", rawData[12]))
|
||||
hex.append(String.format("%02X", rawData[13]))
|
||||
hex.append(String.format("%X", (rawData[4].toInt() and 0x0f).toByte()))
|
||||
hex.append(String.format("%02X", rawData[5]))
|
||||
hex.append(String.format("%02X", rawData[6]))
|
||||
hex.append(String.format("%02X", rawData[7]))
|
||||
hex.append(String.format("%02X", rawData[8]))
|
||||
val hexString = hex.toString()
|
||||
val trackingNumber = hexString + String.format(
|
||||
"%X",
|
||||
crc4(hexString.toByteArray(Charsets.ISO_8859_1))
|
||||
)
|
||||
return DeutschePostTrackingLink(
|
||||
trackingNumber,
|
||||
"https://www.deutschepost.de/de/s/sendungsverfolgung.html?piececode=$trackingNumber",
|
||||
)
|
||||
}
|
||||
|
||||
// CRC-4 with polynomial x^4 + x + 1.
|
||||
private fun crc4(input: ByteArray): Int {
|
||||
var crc = 0
|
||||
var i = 0
|
||||
while (i < input.size) {
|
||||
val c = input[i].toInt()
|
||||
var j = 0x80
|
||||
while (j != 0) {
|
||||
var bit = crc and 0x8
|
||||
crc = crc shl 1
|
||||
if (c and j != 0) {
|
||||
bit = bit xor 0x8
|
||||
}
|
||||
if (bit != 0) {
|
||||
crc = crc xor 0x3
|
||||
}
|
||||
j = j ushr 1
|
||||
}
|
||||
++i
|
||||
}
|
||||
crc = crc and 0xF
|
||||
return crc
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,9 @@ import android.database.Cursor
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import de.markusfisch.android.binaryeye.app.prefs
|
||||
import de.markusfisch.android.binaryeye.content.ParsedContentType
|
||||
import de.markusfisch.android.binaryeye.content.parseData
|
||||
import de.markusfisch.android.binaryeye.content.parsedContentTypeFromName
|
||||
import de.markusfisch.android.binaryeye.preference.Preferences
|
||||
import de.markusfisch.android.binaryeye.zxingcpp.migrateBarcodeFormatName
|
||||
import de.markusfisch.android.binaryeye.zxingcpp.oldToNewFormatNames
|
||||
@@ -13,9 +16,12 @@ import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||
|
||||
class Database {
|
||||
private lateinit var db: SQLiteDatabase
|
||||
private lateinit var context: Context
|
||||
|
||||
fun open(context: Context) {
|
||||
this.context = context.applicationContext
|
||||
db = OpenHelper(context).writableDatabase
|
||||
fillMissingContentTypes()
|
||||
}
|
||||
|
||||
fun getScans(query: String? = null): Cursor = db.rawQuery(
|
||||
@@ -25,6 +31,7 @@ class Database {
|
||||
$SCANS_DATETIME,
|
||||
$SCANS_NAME,
|
||||
$SCANS_TEXT,
|
||||
$SCANS_CONTENT_TYPE,
|
||||
$SCANS_FORMAT
|
||||
FROM $SCANS
|
||||
${getWhereClause(query)}
|
||||
@@ -49,7 +56,8 @@ class Database {
|
||||
$SCANS_GTIN_COUNTRY,
|
||||
$SCANS_GTIN_ADD_ON,
|
||||
$SCANS_GTIN_PRICE,
|
||||
$SCANS_GTIN_ISSUE_NUMBER
|
||||
$SCANS_GTIN_ISSUE_NUMBER,
|
||||
$SCANS_CONTENT_TYPE
|
||||
FROM $SCANS
|
||||
${getWhereClause(query)}
|
||||
ORDER BY $SCANS_PINNED DESC, $SCANS_DATETIME DESC
|
||||
@@ -93,7 +101,8 @@ class Database {
|
||||
$SCANS_GTIN_COUNTRY,
|
||||
$SCANS_GTIN_ADD_ON,
|
||||
$SCANS_GTIN_PRICE,
|
||||
$SCANS_GTIN_ISSUE_NUMBER
|
||||
$SCANS_GTIN_ISSUE_NUMBER,
|
||||
$SCANS_CONTENT_TYPE
|
||||
FROM $SCANS
|
||||
WHERE $SCANS_ID IN (${ids.joinToString(",")})
|
||||
ORDER BY $SCANS_PINNED DESC, $SCANS_DATETIME DESC
|
||||
@@ -121,7 +130,8 @@ class Database {
|
||||
$SCANS_GTIN_COUNTRY,
|
||||
$SCANS_GTIN_ADD_ON,
|
||||
$SCANS_GTIN_PRICE,
|
||||
$SCANS_GTIN_ISSUE_NUMBER
|
||||
$SCANS_GTIN_ISSUE_NUMBER,
|
||||
$SCANS_CONTENT_TYPE
|
||||
FROM $SCANS
|
||||
WHERE $SCANS_ID = ?
|
||||
""".trimMargin(), arrayOf("$id")
|
||||
@@ -151,7 +161,8 @@ class Database {
|
||||
it.getString(SCANS_DATETIME),
|
||||
it.getLong(SCANS_ID),
|
||||
it.getString(SCANS_NAME),
|
||||
it.getInt(SCANS_PINNED) != 0
|
||||
it.getInt(SCANS_PINNED) != 0,
|
||||
parsedContentTypeFromName(it.getString(SCANS_CONTENT_TYPE))
|
||||
)
|
||||
} else {
|
||||
null
|
||||
@@ -208,6 +219,10 @@ class Database {
|
||||
scan.price?.let { put(SCANS_GTIN_PRICE, it) }
|
||||
scan.issueNumber?.let { put(SCANS_GTIN_ISSUE_NUMBER, it) }
|
||||
put(SCANS_PINNED, if (scan.pinned) 1 else 0)
|
||||
put(
|
||||
SCANS_CONTENT_TYPE,
|
||||
getContentType(scan.text, scan.raw, scan.format.name).name
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -279,8 +294,52 @@ class Database {
|
||||
db.update(SCANS, cv, "$SCANS_ID = ?", arrayOf("$id"))
|
||||
}
|
||||
|
||||
private fun fillMissingContentTypes() {
|
||||
Thread {
|
||||
db.rawQuery(
|
||||
"""SELECT
|
||||
$SCANS_ID,
|
||||
$SCANS_TEXT,
|
||||
$SCANS_RAW,
|
||||
$SCANS_FORMAT
|
||||
FROM $SCANS
|
||||
WHERE $SCANS_CONTENT_TYPE IS NULL
|
||||
""".trimMargin(),
|
||||
null
|
||||
).use {
|
||||
while (it.moveToNext()) {
|
||||
val id = it.getLong(SCANS_ID)
|
||||
val type = getContentType(
|
||||
it.getString(SCANS_TEXT),
|
||||
it.getBlob(SCANS_RAW),
|
||||
it.getString(SCANS_FORMAT)
|
||||
)
|
||||
db.update(
|
||||
SCANS,
|
||||
ContentValues().apply {
|
||||
put(SCANS_CONTENT_TYPE, type.name)
|
||||
},
|
||||
"$SCANS_ID = ?",
|
||||
arrayOf("$id")
|
||||
)
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun getContentType(
|
||||
text: String,
|
||||
raw: ByteArray?,
|
||||
format: String
|
||||
) = parseData(
|
||||
context,
|
||||
text,
|
||||
raw ?: text.toByteArray(),
|
||||
format
|
||||
).type
|
||||
|
||||
private class OpenHelper(context: Context) :
|
||||
SQLiteOpenHelper(context, FILE_NAME, null, 10) {
|
||||
SQLiteOpenHelper(context, FILE_NAME, null, 11) {
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
db.createScans()
|
||||
}
|
||||
@@ -317,6 +376,9 @@ class Database {
|
||||
if (oldVersion < 10) {
|
||||
db.addPinnedColumn()
|
||||
}
|
||||
if (oldVersion < 11) {
|
||||
db.addContentTypeColumn()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,6 +413,7 @@ class Database {
|
||||
const val SCANS_GTIN_PRICE = "gtin_price"
|
||||
const val SCANS_GTIN_ISSUE_NUMBER = "gtin_issue_number"
|
||||
const val SCANS_PINNED = "pinned"
|
||||
const val SCANS_CONTENT_TYPE = "content_type"
|
||||
|
||||
private fun SQLiteDatabase.createScans() {
|
||||
execSQL("DROP TABLE IF EXISTS $SCANS".trimMargin())
|
||||
@@ -375,7 +438,8 @@ class Database {
|
||||
$SCANS_GTIN_ADD_ON TEXT,
|
||||
$SCANS_GTIN_PRICE TEXT,
|
||||
$SCANS_GTIN_ISSUE_NUMBER TEXT,
|
||||
$SCANS_PINNED INTEGER NOT NULL DEFAULT 0
|
||||
$SCANS_PINNED INTEGER NOT NULL DEFAULT 0,
|
||||
$SCANS_CONTENT_TYPE TEXT NOT NULL DEFAULT '${ParsedContentType.UNKNOWN.name}'
|
||||
)""".trimMargin()
|
||||
)
|
||||
}
|
||||
@@ -446,5 +510,9 @@ class Database {
|
||||
"ALTER TABLE $SCANS ADD COLUMN $SCANS_PINNED INTEGER NOT NULL DEFAULT 0"
|
||||
)
|
||||
}
|
||||
|
||||
private fun SQLiteDatabase.addContentTypeColumn() {
|
||||
execSQL("ALTER TABLE $SCANS ADD COLUMN $SCANS_CONTENT_TYPE TEXT")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package de.markusfisch.android.binaryeye.database
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateFormat
|
||||
import de.markusfisch.android.binaryeye.content.ParsedContentType
|
||||
import de.markusfisch.android.binaryeye.content.parsedContentTypeFromName
|
||||
import de.markusfisch.android.binaryeye.zxingcpp.migrateBarcodeFormatName
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BarcodeFormat
|
||||
import de.markusfisch.android.zxingcpp.ZxingCpp.BitMatrix
|
||||
@@ -27,7 +29,8 @@ data class Scan(
|
||||
val dateTime: String = getDateTime(),
|
||||
var id: Long = 0L,
|
||||
val label: String? = null,
|
||||
val pinned: Boolean = false
|
||||
val pinned: Boolean = false,
|
||||
val contentType: ParsedContentType = ParsedContentType.UNKNOWN
|
||||
) {
|
||||
// Needs to be overwritten manually, as ByteArray is an array and
|
||||
// this isn't handled well by Kotlin.
|
||||
@@ -59,7 +62,8 @@ data class Scan(
|
||||
price == other.price &&
|
||||
issueNumber == other.issueNumber &&
|
||||
label == other.label &&
|
||||
pinned == other.pinned
|
||||
pinned == other.pinned &&
|
||||
contentType == other.contentType
|
||||
}
|
||||
|
||||
// Needs to be overwritten manually, as ByteArray is an array and
|
||||
@@ -82,6 +86,7 @@ data class Scan(
|
||||
result = 31 * result + (issueNumber?.hashCode() ?: 0)
|
||||
result = 31 * result + (label?.hashCode() ?: 0)
|
||||
result = 31 * result + pinned.hashCode()
|
||||
result = 31 * result + contentType.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -133,6 +138,7 @@ fun Scan.toBundle() = Bundle().apply {
|
||||
putLong(SCAN_ID, id)
|
||||
putString(SCAN_LABEL, label)
|
||||
putBoolean(SCAN_PINNED, pinned)
|
||||
putString(SCAN_CONTENT_TYPE, contentType.name)
|
||||
}
|
||||
|
||||
fun Bundle.toScan(): Scan? {
|
||||
@@ -159,7 +165,8 @@ fun Bundle.toScan(): Scan? {
|
||||
dateTime = getString(SCAN_DATE_TIME) ?: getDateTime(),
|
||||
id = getLong(SCAN_ID, 0L),
|
||||
label = getString(SCAN_LABEL),
|
||||
pinned = getBoolean(SCAN_PINNED, false)
|
||||
pinned = getBoolean(SCAN_PINNED, false),
|
||||
contentType = parsedContentTypeFromName(getString(SCAN_CONTENT_TYPE))
|
||||
)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
null
|
||||
@@ -207,3 +214,4 @@ private const val SCAN_DATE_TIME = "date_time"
|
||||
private const val SCAN_ID = "id"
|
||||
private const val SCAN_LABEL = "label"
|
||||
private const val SCAN_PINNED = "pinned"
|
||||
private const val SCAN_CONTENT_TYPE = "content_type"
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
<string name="sha1">SHA1</string>
|
||||
<string name="sha256">SHA256</string>
|
||||
<string name="scan_meta_data">%1$s • %2$s</string>
|
||||
<string name="parsed_type_deutsche_post">Deutsche Post Matrix Code</string>
|
||||
<string name="parsed_type_deutsche_post">Deutsche Post Tracking Link</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user