From e3aed3d01f4cb61c7c06157e3b48e14d4ae19472 Mon Sep 17 00:00:00 2001 From: James Bottomley Date: Mon, 7 Mar 2022 11:44:11 -0500 Subject: [PATCH 1/3] make BoardingPass scanner more robust At the moment it throws an exception on various format errors. Make it more defensive about the requirements as well as return null in every situation so the barcode scan still completes even if it isn't recognized as a boarding pass. Signed-off-by: James Bottomley --- .../model/schema/BoardingPass.kt | 150 ++++++++++-------- 1 file changed, 83 insertions(+), 67 deletions(-) diff --git a/app/src/main/java/com/example/barcodescanner/model/schema/BoardingPass.kt b/app/src/main/java/com/example/barcodescanner/model/schema/BoardingPass.kt index ecc72aa1..c79dd609 100644 --- a/app/src/main/java/com/example/barcodescanner/model/schema/BoardingPass.kt +++ b/app/src/main/java/com/example/barcodescanner/model/schema/BoardingPass.kt @@ -16,6 +16,7 @@ class BoardingPass( val carrier: String? = null, val flight: String? = null, val date: String? = null, + val dateJ: Int = 0, val cabin: String? = null, val seat: String? = null, val seq: String? = null, @@ -32,84 +33,99 @@ class BoardingPass( private val DATE_FORMATTER by unsafeLazy { SimpleDateFormat("d MMMM", Locale.ENGLISH) } fun parse(text: String): BoardingPass? { + try { - // M1 means single leg barcode - if (text.startsWithIgnoreCase("M1").not()) { - return null - } - // E means electronic ticket - if (text[22] != 'E') { - return null - } - val fieldSize: Int = text.slice(58..59).toInt(16) - // > is the marker for the airline specific optional fields - if (fieldSize != 0 && text[60] != '>') { - return null - } - // ^ is the mandatory security marker - if (text[60+fieldSize] != '^') { - return null - } - - val name: String = text.slice(2..21).trim() - val pnr: String = text.slice(23..29).trim() - val from: String = text.slice(30..32) - val to: String = text.slice(33..35) - val carrier: String = text.slice(36..38).trim() - val flight: String = text.slice(39..43).trim() - val dateJ: String = text.slice(44..46) - val cabin: String = text.slice(47..47) - val seat: String = text.slice(48..51).trim() - val seq: String = text.slice(52..56).trim() - // 57 is status - ignore - - val today = Calendar.getInstance() - today.set(Calendar.DAY_OF_YEAR, dateJ.toInt()) - val date: String = DATE_FORMATTER.format(today.getTime()) - var selectee : String = "" - var ticket : String = "" - var ffAirline : String = "" - var ffNo : String = "" - var fasttrack: String = "" + if (text.length < 60) { + return null + } - if (fieldSize != 0) { - val size: Int = text.slice(62..63).toInt(16) - if (size != 0 && size != 24) { + // M1 means single leg barcode + if (text.startsWithIgnoreCase("M1").not()) { + return null + } + // E means electronic ticket + if (text[22] != 'E') { + return null + } + val fieldSize: Int = text.slice(58..59).toInt(16) + // > is the marker for the airline specific optional fields + if (fieldSize != 0 && text[60] != '>') { return null } - // don't really care about the first optional field - // it's mostly baggage and checkin information - val size1: Int = text.slice(64+size..65+size).toInt(16) - // European boarding passes are 42 to have appended fasttrack - // US boarding passes are size 41 with no fasttrack - if (size1 != 0 && size1 != 41 && size1 != 42) { + // ^ is the security marker; sometimes missing on paper passes + if (text.length > 60 + fieldSize && text[60+fieldSize] != '^') { return null - } else { - ticket = text.slice(66+size..78+size).trim() - // TSA field: - // blank for not US flights - // 0 - normal cleared - // 1 - no fly - // 2 - selected for enhanced security - // 3 - precheck - selectee = text.slice(79+size..79+size) - ffAirline = text.slice(84+size..86+size).trim() - ffNo = text.slice(87+size..102+size).trim() - if (size1 == 42) { - // Y - fasttrack eligible - fasttrack = text.slice(107+size..107+size) + } + + val name = text.slice(2..21).trim() + val pnr = text.slice(23..29).trim() + val from = text.slice(30..32) + val to = text.slice(33..35) + val carrier = text.slice(36..38).trim() + val flight = text.slice(39..43).trim() + val dateJ = text.slice(44..46).toInt() + val cabin = text.slice(47..47) + val seat = text.slice(48..51).trim() + val seq = text.slice(52..56) + // 57 is status - ignore + + val today = Calendar.getInstance() + today.set(Calendar.DAY_OF_YEAR, dateJ) + val date: String = DATE_FORMATTER.format(today.getTime()) + var selectee : String? = null + var ticket : String? = null + var ffAirline : String? = null + var ffNo : String? = null + var fasttrack: String? = null + + if (fieldSize != 0) { + // don't actually use version but it must parse as an Int + @Suppress("UNUSED_VARIABLE") + val version: Int = text.slice(61..61).toInt() + val size: Int = text.slice(62..63).toInt(16) + + if (size != 0 && size < 11) { + return null + } + // don't really care about the first optional field + // it's mostly baggage and checkin information + val size1: Int = text.slice(64+size..65+size).toInt(16) + // European boarding passes are 42 to have appended fasttrack + // US boarding passes are size 41 with no fasttrack + if (size1 != 0 && (size1 < 37 || size1 > 42)) { + return null + } else { + ticket = text.slice(66+size..78+size).trim() + // TSA field: + // blank for not US flights + // 0 - normal cleared + // 1 - no fly + // 2 - selected for enhanced security + // 3 - precheck + selectee = text.slice(79+size..79+size) + ffAirline = text.slice(84+size..86+size).trim() + ffNo = text.slice(87+size..102+size).trim() + if (size1 == 42) { + // Y - fasttrack eligible + fasttrack = text.slice(107+size..107+size) + } } } - } - return BoardingPass(name, pnr, from, to, carrier, flight, date, - cabin, seat, seq, ticket, selectee, - ffAirline, ffNo, fasttrack, - text) + return BoardingPass(name, pnr, from, to, carrier, flight, date, + dateJ, cabin, seat, seq, ticket, selectee, + ffAirline, ffNo, fasttrack, + text) + } catch(e: Exception) { + // mostly number format and parse past end of string errors + return null + } } } override val schema = BarcodeSchema.BOARDINGPASS override fun toFormattedText(): String = listOf(name, pnr, "$from->$to", "$carrier$flight", date, cabin, seat, seq, ticket, selectee, "$ffAirline$ffNo", fasttrack).joinToStringNotNullOrBlankWithLineSeparator() - override fun toBarcodeText(): String = "$blob" + override fun toBarcodeText(): String { + return blob ?: "" + } } \ No newline at end of file From c8ad24023cdbf388327fa337406476a434744459 Mon Sep 17 00:00:00 2001 From: James Bottomley Date: Mon, 7 Mar 2022 11:51:03 -0500 Subject: [PATCH 2/3] Add pkpass save/share for boarding pass scan This allows a scanned boarding pass to be converted to a pkpass file and then shared with any application that can make use of it. Note that because of the limitations of what the boarding pass barcode contains (no flight time, no frequent flyer status etc), the exported pkpass is somewhat incomplete (so the user will have to add information to certain fields). This functionality is most useful with PassAndroid which allows pass editing so you can fill in the missing information. Signed-off-by: James Bottomley --- app/src/main/assets/img/icon.png | Bin 0 -> 866 bytes .../feature/barcode/BarcodeActivity.kt | 40 +++ .../usecase/BarcodePkpassSaver.kt | 263 ++++++++++++++++++ app/src/main/res/layout/activity_barcode.xml | 22 +- app/src/main/res/values/strings.xml | 2 + 5 files changed, 326 insertions(+), 1 deletion(-) create mode 100644 app/src/main/assets/img/icon.png create mode 100644 app/src/main/java/com/example/barcodescanner/usecase/BarcodePkpassSaver.kt diff --git a/app/src/main/assets/img/icon.png b/app/src/main/assets/img/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..07f95b0a63049d3f8ce740aa0a54055a7f3d621c GIT binary patch literal 866 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTCmUKs7M+SzC{oH>NS%G|oWRD45dJguM!v-tY$DUh!@P+6=(yLU`z6LcLCBs@Y8vBJ&@uo z@Q5r1(g|SvA=~LZ0|Qfvr;B4q#=W;QY%@e0CEE6%)|>u`5igX+eVEQ?7%|h6~tSbdwZsM0BuOdkVS+?tGEFv3kR~x9`u~ z@niZtt^EF;`=8&{Znxg7|tPI}FUd{D~>F%yq zv(K{0J?PHgr}QK7f#%I$bGDW(aGE?>$w*5{-!&yr^XqNFKOG%gTvGh~ZtC7vOK;fU zsr`w&f-#1Xk5St9VdL{B*?bzc3op-_bNRvMFN{05=dhMBt~#(iBZoom#-Z=b(;MOr?7*IDHP;S2E<4paV}k^=2P37^5! L)z4*}Q$iB}^RH#h literal 0 HcmV?d00001 diff --git a/app/src/main/java/com/example/barcodescanner/feature/barcode/BarcodeActivity.kt b/app/src/main/java/com/example/barcodescanner/feature/barcode/BarcodeActivity.kt index c91db46d..91f3feb1 100644 --- a/app/src/main/java/com/example/barcodescanner/feature/barcode/BarcodeActivity.kt +++ b/app/src/main/java/com/example/barcodescanner/feature/barcode/BarcodeActivity.kt @@ -30,6 +30,7 @@ import com.example.barcodescanner.model.SearchEngine import com.example.barcodescanner.model.schema.BarcodeSchema import com.example.barcodescanner.model.schema.OtpAuth import com.example.barcodescanner.usecase.Logger +import com.example.barcodescanner.usecase.BarcodePkpassSaver import com.example.barcodescanner.usecase.save import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable @@ -200,6 +201,8 @@ class BarcodeActivity : BaseActivity(), DeleteConfirmationDialogFragment.Listene button_open_otp.setOnClickListener { openOtpInOtherApp() } button_open_bitcoin_uri.setOnClickListener { openBitcoinUrl() } button_open_link.setOnClickListener { openLink() } + button_save_as_pkpass.setOnClickListener { savePkpass() } + button_share_as_pkpass.setOnClickListener { sharePkpass() } button_save_bookmark.setOnClickListener { saveBookmark() } button_call_phone_1.setOnClickListener { callPhone(barcode.phone) } @@ -455,6 +458,41 @@ class BarcodeActivity : BaseActivity(), DeleteConfirmationDialogFragment.Listene startActivityIfExists(intent) } + private fun savePkpass() { + BarcodePkpassSaver.saveBarcodeAsPkpass(this, originalBarcode, null) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + Toast.makeText(this, R.string.activity_save_barcode_as_text_file_name_saved, Toast.LENGTH_LONG).show() + }, + { error -> + showError(error) + } + ) + .addTo(disposable) + } + + private fun sharePkpass() { + val uri = Array(1, init={i:Int -> null}) + BarcodePkpassSaver.saveBarcodeAsPkpass(this, originalBarcode, uri) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + val intent = Intent(Intent.ACTION_VIEW).apply { + setDataAndType(uri[0], "application/vnd.apple.pkpass") + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + startActivityIfExists(intent) + }, + { error -> + showError(error) + } + ) + .addTo(disposable) + } + private fun shareBarcodeAsText() { val intent = Intent(Intent.ACTION_SEND).apply { type = "text/plain" @@ -694,6 +732,8 @@ class BarcodeActivity : BaseActivity(), DeleteConfirmationDialogFragment.Listene button_open_bitcoin_uri.isVisible = barcode.bitcoinUri.isNullOrEmpty().not() button_open_link.isVisible = barcode.url.isNullOrEmpty().not() button_save_bookmark.isVisible = barcode.schema == BarcodeSchema.BOOKMARK + button_save_as_pkpass.isVisible = barcode.schema == BarcodeSchema.BOARDINGPASS + button_share_as_pkpass.isVisible = barcode.schema == BarcodeSchema.BOARDINGPASS } private fun showButtonText() { diff --git a/app/src/main/java/com/example/barcodescanner/usecase/BarcodePkpassSaver.kt b/app/src/main/java/com/example/barcodescanner/usecase/BarcodePkpassSaver.kt new file mode 100644 index 00000000..4cdfbca6 --- /dev/null +++ b/app/src/main/java/com/example/barcodescanner/usecase/BarcodePkpassSaver.kt @@ -0,0 +1,263 @@ +package com.example.barcodescanner.usecase + +import android.content.ContentValues +import android.content.Context +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.MediaStore +import androidx.annotation.RequiresApi +import com.example.barcodescanner.model.Barcode +import com.example.barcodescanner.model.schema.BoardingPass +import com.example.barcodescanner.extension.unsafeLazy +import io.reactivex.Completable +import org.json.JSONArray +import org.json.JSONObject +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.OutputStream +import java.security.MessageDigest +import java.text.SimpleDateFormat +import java.util.* +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + +import android.util.Log + +object BarcodePkpassSaver { + private const val MIME_TYPE = "application/vnd.apple.pkpass" + private const val FILE_EXTENSION = ".pkpass" + private const val TAG = "BarcodePkpassSaver" + + private val DATE_FORMATTER by unsafeLazy { SimpleDateFormat("YYYY-MM-dd\'T\'HH:MMZZZZZ", Locale.ENGLISH) } + + fun saveBarcodeAsPkpass(context: Context, barcode: Barcode, uri: Array?): Completable { + Log.i(TAG, "saving barcode") + return Completable.create { emitter -> + try { + val bp = BoardingPass.parse(barcode.text) + saveToDownloads(context, barcode, bp!!, FILE_EXTENSION, MIME_TYPE, uri) + emitter.onComplete() + } catch (ex: Exception) { + Log.e(TAG, "Save zipfile", ex) + emitter.onError(ex) + } + } + } + + private fun airportTrim(name: String) : String { + return name.removeSuffix("Airport") + .removeSuffix("airport") + .trim() + .removeSuffix("International") + .removeSuffix("international") + .trim() + } + + private fun JSONArray.label(key: String, label: String, + value: String?) : JSONArray { + if (value == null) { + return this + } + + return this.put(JSONObject() + .put("key", key) + .put("label", label) + .put("value", value) + ) + } + private fun removeLeadingZeros(value: String?) : String? { + when(value) { + null -> return null + "" -> return "" + } + + for (i in 0 until value!!.length) { + if (value[i] != '0') { + return value.slice(i..value.length-1) + } + } + return "0" + } + + private fun alternate(bp: BoardingPass) : String { + if (bp.fasttrack == "Y" && bp.selectee == "3") { + return "FAST TRACK|TSA PRECHK" + } else if (bp.fasttrack == "Y") { + return "FAST TRACK" + } else if (bp.selectee == "3") { + return "TSA PRECHK" + } else { + return "" + } + } + + private fun cabin(bp: BoardingPass) : String? { + if (bp.cabin == null) { + return null + } + when (bp.cabin) { + in "R","P" -> return "Premium First" + in "F","A" -> return "First" + in "J","C","D","I","Z" -> return "Business" + "W" -> return "Premium Economy" + in "Y","B","M","S","H","K","L","N","Q","T","V","X" -> return "Economy" + } + return null + } + + private fun convertToJson(bp : BoardingPass): JSONObject { + val flight = removeLeadingZeros(bp.flight) + val date = Calendar.getInstance() + var from = bp.from!! + var to = bp.to!! + val locations = JSONArray() + val seq = if (bp.seq.isNullOrBlank()) { + null + } else { + removeLeadingZeros(bp.seq) + } + val ticket = if (bp.ticket.isNullOrBlank()) { + null + } else { + bp.ticket + } + val ff = if (bp.ffNo.isNullOrBlank()) { + null + } else { + "${bp.ffAirline} ${bp.ffNo}" + } + date.set(Calendar.DAY_OF_YEAR, bp.dateJ) + // cheat: we don't know the time, so set to current time on boarding day + val relevantDate: String = DATE_FORMATTER.format(date.getTime()) + return JSONObject() + .put("barcode", JSONObject() + .put("altText", alternate(bp)) + .put("format", "PkBarcodeFormatAztec") + .put("message", bp.blob) + .put("messageEncoding", "iso-8859-1") + ) + .put("description", "${bp.carrier} Flight ${flight} on ${bp.date} departing ${from}") + .put("locations", locations) + .put("boardingPass", JSONObject() + .put("primaryFields", JSONArray() + .label("depart", "Depart", from) + .label("arrive", "Arrive", to) + ) + .put("secondaryFields", JSONArray() + .label("passenger", "", bp.name) + .label("priorityaccess", "Cabin", cabin(bp)) + ) + .put("headerFields", JSONArray() + .label("flight", "Flight", "${bp.carrier}${flight}") + .label("gate", "Gate", "") + ) + .put("auxiliaryFields", JSONArray() + .label("group", "Group", "") + .label("seat", "Seat", removeLeadingZeros(bp.seat)) + .label("status", "Status", "On Time") + .label("terminal", "Terminal", "") + .label("boardingTime", "Departs", "") + ) + .put("backFields", JSONArray() + .label("date", "Date", bp.date) + .label("gateBoardingTime", "Boarding Time", "") + .label("record_locator", "Record Locator", bp.pnr) + .label("seq", "Sequence", seq) + .label("ff_number", "Frequent Flyer", ff) + .label("ticket_number", "Ticket", ticket) + ) + .put("transitType", "PKTransitTypeAir") + ) + .put("serialNumber", UUID.randomUUID()) + .put("organizationName", "QRAndBarcodeScanner") + .put("passTypeIdentifier", "com.example.barcodescanner") + .put("formatVersion", 1) + .put("backgroundColor", "#ff0000ff") + .put("relevantDate", relevantDate) + .put("voided", false) + } + + private fun ByteArray.toHex(): String = joinToString(separator = "") { + eachByte -> "%02x".format(eachByte) + } + + private fun ZipOutputStream.addManifest(manifest: JSONObject, fileName: String, content: ByteArray) { + val md = MessageDigest.getInstance("SHA-1") + this.putNextEntry(ZipEntry(fileName)) + this.write(content) + this.closeEntry() + md.update(content) + manifest.put(fileName, md.digest().toHex()) + } + + + private fun saveToDownloads(context: Context, barcode: Barcode, bp: BoardingPass, extension: String, mimeType: String, uri: Array?) { + val fileName = "BoardingPass_${barcode.date}$extension" + Log.i(TAG, "constructed filename $fileName") + val main = convertToJson(bp).toString() + val out = openFileOutputStream(context, fileName, mimeType, uri) + val zipout = ZipOutputStream(out) + val manifest = JSONObject() + val assets = context.getAssets() + + val icon = try { + assets.open("img/${bp.carrier}/icon.png").readBytes() + } catch(e: Exception) { + assets.open("img/icon.png").readBytes() + } + + zipout.addManifest(manifest, "pass.json", main.toByteArray()) + zipout.addManifest(manifest, "icon.png", icon) + try { + val thumbnail = assets.open("img/${bp.carrier}/thumbnail.png").readBytes() + zipout.addManifest(manifest, "thumbnail.png", thumbnail) + } catch(e: Exception) { } + try { + val logo = assets.open("img/${bp.carrier}/logo.png").readBytes() + zipout.addManifest(manifest, "logo.png", logo) + } catch(e: Exception) { } + zipout.putNextEntry(ZipEntry("manifest.json")) + zipout.write(manifest.toString().toByteArray()) + zipout.close() + out.close() + } + + private fun openFileOutputStream(context: Context, fileName: String, mimeType: String, uri: Array?): OutputStream { + return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + openFileOutputStreamOldSdk(fileName, uri) + } else { + openFileOutputStreamNewSdk(context, fileName, mimeType, uri) + } + } + + @Suppress("DEPRECATION") + private fun openFileOutputStreamOldSdk(fileName: String, uri: Array?): OutputStream { + val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + val file = File(dir, fileName) + if (file.exists()) { + file.delete() + } + if (uri != null) { + uri[0] = Uri.fromFile(file) + file.deleteOnExit() + } + return FileOutputStream(file) + } + + @RequiresApi(Build.VERSION_CODES.Q) + private fun openFileOutputStreamNewSdk(context: Context, fileName: String, mimeType: String, uri: Array?): OutputStream { + val resolver = context.contentResolver + val values = ContentValues().apply { + put(MediaStore.Downloads.DISPLAY_NAME, fileName) + put(MediaStore.Downloads.MIME_TYPE, mimeType) + } + val uris = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values) ?: throw IOException() + if (uri != null) { + Log.i(TAG, "URI is " + uris) + uri[0] = uris + } + return resolver.openOutputStream(uris) ?: throw IOException() + } +} diff --git a/app/src/main/res/layout/activity_barcode.xml b/app/src/main/res/layout/activity_barcode.xml index a5541427..cc04588a 100644 --- a/app/src/main/res/layout/activity_barcode.xml +++ b/app/src/main/res/layout/activity_barcode.xml @@ -357,6 +357,26 @@ android:visibility="gone" tools:visibility="visible" /> + + - \ No newline at end of file + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 05b6ed48..f28f0bf9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -315,6 +315,8 @@ Copied to clipboard No app found for this action Cancel + Save as pkpass + Share as pkpass Increase brightness From 6fdd683b44debb61dd7a554fc8de94b362f8a6bd Mon Sep 17 00:00:00 2001 From: James Bottomley Date: Mon, 24 Apr 2023 16:17:02 -0400 Subject: [PATCH 3/3] Add TSAPre to PKPASS export If you have TSAPre in the US it shows in the barcode, so if this is present export the PKPASS with a TSAPre green check at the bottom so you can use the Pre lanes without getting stopped. Signed-off-by: James Bottomley --- app/src/main/assets/img/tsapre.png | Bin 0 -> 4474 bytes .../barcodescanner/usecase/BarcodePkpassSaver.kt | 4 ++++ 2 files changed, 4 insertions(+) create mode 100644 app/src/main/assets/img/tsapre.png diff --git a/app/src/main/assets/img/tsapre.png b/app/src/main/assets/img/tsapre.png new file mode 100644 index 0000000000000000000000000000000000000000..16c05be5695efbaf18b075e39ae4aff50ec4f99c GIT binary patch literal 4474 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 literal 0 HcmV?d00001 diff --git a/app/src/main/java/com/example/barcodescanner/usecase/BarcodePkpassSaver.kt b/app/src/main/java/com/example/barcodescanner/usecase/BarcodePkpassSaver.kt index 4cdfbca6..26e9e271 100644 --- a/app/src/main/java/com/example/barcodescanner/usecase/BarcodePkpassSaver.kt +++ b/app/src/main/java/com/example/barcodescanner/usecase/BarcodePkpassSaver.kt @@ -210,6 +210,10 @@ object BarcodePkpassSaver { zipout.addManifest(manifest, "pass.json", main.toByteArray()) zipout.addManifest(manifest, "icon.png", icon) + if (bp.selectee == "3") { + val footer = assets.open("img/tsapre.png").readBytes() + zipout.addManifest(manifest, "footer.png", footer) + } try { val thumbnail = assets.open("img/${bp.carrier}/thumbnail.png").readBytes() zipout.addManifest(manifest, "thumbnail.png", thumbnail)