diff --git a/.gitmodules b/.gitmodules
index c6c8d4eb..4efe2e93 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -5,3 +5,6 @@
[submodule "pretixscan/libpretixui-repo"]
path = pretixscan/libpretixui-repo
url = https://github.com/pretix/libpretixui-android.git
+[submodule "pretixscan/libpretixnfc-repo"]
+ path = pretixscan/libpretixnfc-repo
+ url = https://github.com/pretix/libpretixnfc.git
diff --git a/pretixscan/app/build.gradle b/pretixscan/app/build.gradle
index baaeda3f..c2858241 100644
--- a/pretixscan/app/build.gradle
+++ b/pretixscan/app/build.gradle
@@ -172,6 +172,17 @@ dependencies {
implementation(project(':libpretixsync')) {
transitive = false
}
+
+ // libpretixnfc
+ implementation(project(':libpretixnfc')) {
+ transitive = true
+ }
+
+ // libpretixnfc-android
+ implementation(project(':libpretixnfc-android')) {
+ transitive = true
+ }
+
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}
diff --git a/pretixscan/app/src/main/AndroidManifest.xml b/pretixscan/app/src/main/AndroidManifest.xml
index 44c0140a..30e95ffb 100644
--- a/pretixscan/app/src/main/AndroidManifest.xml
+++ b/pretixscan/app/src/main/AndroidManifest.xml
@@ -102,6 +102,8 @@
android:exported="false"
android:theme="@style/AppTheme.NoActionBar" />
+
+
Unit)? = null
+ private var nfcHandler: NfcHandler? = null
+
companion object {
const val PERMISSIONS_REQUEST_CAMERA = 1337
}
@@ -206,9 +218,15 @@ class MainActivity : AppCompatActivity(), ReloadableActivity, ScannerView.Result
}
lastScanTime = System.currentTimeMillis()
lastScanCode = result
+ lastScanSourceType = ReusableMediaType.BARCODE
lastIgnoreUnpaid = false
lastScanResult = null
- handleScan(result, null, !conf.unpaidAsk)
+ handleScan(
+ result,
+ lastScanSourceType.serverName,
+ null,
+ !conf.unpaidAsk
+ )
}
})
@@ -271,6 +289,8 @@ class MainActivity : AppCompatActivity(), ReloadableActivity, ScannerView.Result
}
view_data.configDetails.set(confdetails.trim())
view_data.isOffline.set(conf.offlineMode)
+
+ reloadNfcHandler()
}
private fun setSearchFilter(f: String) {
@@ -290,10 +310,16 @@ class MainActivity : AppCompatActivity(), ReloadableActivity, ScannerView.Result
override fun onSearchResultClicked(res: TicketCheckProvider.SearchResult) {
lastScanTime = System.currentTimeMillis()
lastScanCode = res.secret!!
+ lastScanSourceType = ReusableMediaType.BARCODE
lastScanResult = null
lastIgnoreUnpaid = false
hideSearchCard()
- handleScan(res.secret!!, null, !conf.unpaidAsk)
+ handleScan(
+ res.secret!!,
+ lastScanSourceType.serverName,
+ null,
+ !conf.unpaidAsk
+ )
}
})
runOnUiThread {
@@ -565,7 +591,9 @@ class MainActivity : AppCompatActivity(), ReloadableActivity, ScannerView.Result
Build.VERSION.RELEASE,
"pretixSCAN Android",
BuildConfig.VERSION_NAME,
- null,
+ try {
+ conf.keyStore.getOrCreateRsaPubKey("device")?.toString(Charset.defaultCharset())
+ } catch (e: NotImplementedError) { null },
null,
(application as PretixScan).connectivityHelper
)
@@ -966,6 +994,7 @@ class MainActivity : AppCompatActivity(), ReloadableActivity, ScannerView.Result
override fun onPause() {
handler.removeCallbacks(syncRunnable)
(application as PretixScan).connectivityHelper.removeListener(this)
+ nfcHandler?.stop()
super.onPause()
if (conf.useCamera) {
binding.scannerView.stopCamera()
@@ -984,7 +1013,12 @@ class MainActivity : AppCompatActivity(), ReloadableActivity, ScannerView.Result
super.onStop()
}
- fun handleScan(raw_result: String, answers: MutableList?, ignore_unpaid: Boolean = false) {
+ fun handleScan(
+ raw_result: String,
+ source_type: String,
+ answers: MutableList?,
+ ignore_unpaid: Boolean = false
+ ) {
if (conf.kioskMode && conf.requiresPin("settings") && conf.verifyPin(raw_result)) {
supportActionBar?.show()
return
@@ -1032,7 +1066,7 @@ class MainActivity : AppCompatActivity(), ReloadableActivity, ScannerView.Result
val provider = (application as PretixScan).getCheckProvider(conf)
val startedAt = System.currentTimeMillis()
try {
- checkResult = provider.check(conf.eventSelectionToMap(), result, "barcode", answers, ignore_unpaid, conf.printBadges, when (conf.scanType) {
+ checkResult = provider.check(conf.eventSelectionToMap(), result, source_type, answers, ignore_unpaid, conf.printBadges, when (conf.scanType) {
"exit" -> TicketCheckProvider.CheckInType.EXIT
else -> TicketCheckProvider.CheckInType.ENTRY
}, allowQuestions = !conf.ignoreQuestions)
@@ -1061,9 +1095,13 @@ class MainActivity : AppCompatActivity(), ReloadableActivity, ScannerView.Result
}
}
- fun showQuestionsDialog(res: TicketCheckProvider.CheckResult, secret: String, ignore_unpaid: Boolean,
- values: Map?, isResumed: Boolean,
- retryHandler: ((String, MutableList, Boolean) -> Unit)): QuestionsDialogInterface {
+ fun showQuestionsDialog(res: TicketCheckProvider.CheckResult,
+ secret: String,
+ sourceType: ReusableMediaType,
+ ignore_unpaid: Boolean,
+ values: Map?,
+ isResumed: Boolean,
+ retryHandler: ((String, ReusableMediaType, MutableList, Boolean) -> Unit)): QuestionsDialogInterface {
val questions = res.requiredAnswers!!.map { it.question.toModel() }
for (q in questions) {
@@ -1105,7 +1143,7 @@ class MainActivity : AppCompatActivity(), ReloadableActivity, ScannerView.Result
values_,
null,
null,
- { answers -> retryHandler(secret, answers, ignore_unpaid) },
+ { answers -> retryHandler(secret, sourceType, answers, ignore_unpaid) },
null,
attendeeName,
attendeeDOB,
@@ -1160,9 +1198,14 @@ class MainActivity : AppCompatActivity(), ReloadableActivity, ScannerView.Result
startHidingTimer()
if (result.type == TicketCheckProvider.CheckResult.Type.ANSWERS_REQUIRED) {
view_data.resultState.set(DIALOG)
- dialog = showQuestionsDialog(result, lastScanCode, ignore_unpaid, null, false) { secret, answers, ignore_unpaid ->
+ dialog = showQuestionsDialog(result, lastScanCode, lastScanSourceType, ignore_unpaid, null, false) { secret, sourceType, answers, ignore_unpaid ->
stopHidingTimer()
- handleScan(secret, answers, ignore_unpaid)
+ handleScan(
+ secret,
+ sourceType.serverName,
+ answers,
+ ignore_unpaid
+ )
}
dialog!!.setOnCancelListener(DialogInterface.OnCancelListener { hideCard() })
view_data.setLed(this, view_data.resultState.get()!!, true)
@@ -1170,9 +1213,14 @@ class MainActivity : AppCompatActivity(), ReloadableActivity, ScannerView.Result
}
if (result.type == TicketCheckProvider.CheckResult.Type.UNPAID && result.isCheckinAllowed) {
view_data.resultState.set(DIALOG)
- dialog = showUnpaidDialog(this, result, lastScanCode, answers) { secret, answers, ignore_unpaid ->
+ dialog = showUnpaidDialog(this, result, lastScanCode, lastScanSourceType, answers) { secret, sourceType, answers, ignore_unpaid ->
stopHidingTimer()
- handleScan(secret, answers, ignore_unpaid)
+ handleScan(
+ secret,
+ sourceType.serverName,
+ answers,
+ ignore_unpaid
+ )
}
dialog!!.setOnCancelListener(DialogInterface.OnCancelListener { hideCard() })
view_data.setLed(this, view_data.resultState.get()!!, true)
@@ -1375,9 +1423,15 @@ class MainActivity : AppCompatActivity(), ReloadableActivity, ScannerView.Result
}
lastScanTime = System.currentTimeMillis()
lastScanCode = s
+ lastScanSourceType = ReusableMediaType.BARCODE
lastScanResult = null
lastIgnoreUnpaid = false
- handleScan(s, null, !conf.unpaidAsk)
+ handleScan(
+ s,
+ lastScanSourceType.serverName,
+ null,
+ !conf.unpaidAsk
+ )
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
@@ -1391,9 +1445,15 @@ class MainActivity : AppCompatActivity(), ReloadableActivity, ScannerView.Result
}
lastScanTime = System.currentTimeMillis()
lastScanCode = keyboardBuffer
+ lastScanSourceType = ReusableMediaType.BARCODE
lastScanResult = null
lastIgnoreUnpaid = false
- handleScan(keyboardBuffer, null, !conf.unpaidAsk)
+ handleScan(
+ keyboardBuffer,
+ lastScanSourceType.serverName,
+ null,
+ !conf.unpaidAsk
+ )
keyboardBuffer = ""
true
}
@@ -1557,6 +1617,7 @@ class MainActivity : AppCompatActivity(), ReloadableActivity, ScannerView.Result
view_data.resultState.set(DIALOG)
lastScanCode = savedInstanceState.getString("lastScanCode", null)
+ lastScanSourceType = ReusableMediaType.entries.firstOrNull { it.serverName == savedInstanceState.getString("lastScanType", ReusableMediaType.BARCODE.serverName) } ?: ReusableMediaType.BARCODE
lastIgnoreUnpaid = savedInstanceState.getBoolean("ignore_unpaid")
lastScanResult = om.readValue(savedInstanceState.getString("result"), TicketCheckProvider.CheckResult::class.java)
@@ -1574,9 +1635,14 @@ class MainActivity : AppCompatActivity(), ReloadableActivity, ScannerView.Result
}
}
- dialog = showQuestionsDialog(lastScanResult!!, lastScanCode, lastIgnoreUnpaid, values, true) { secret, answers, ignore_unpaid ->
+ dialog = showQuestionsDialog(lastScanResult!!, lastScanCode, lastScanSourceType, lastIgnoreUnpaid, values, true) { secret, sourceType, answers, ignore_unpaid ->
stopHidingTimer()
- handleScan(secret, answers, ignore_unpaid)
+ handleScan(
+ secret,
+ sourceType.serverName,
+ answers,
+ ignore_unpaid
+ )
}
dialog!!.onRestoreInstanceState(answers)
dialog!!.setOnCancelListener(DialogInterface.OnCancelListener { hideCard() })
@@ -1601,9 +1667,102 @@ class MainActivity : AppCompatActivity(), ReloadableActivity, ScannerView.Result
outState.putString("result_state", "DIALOG")
outState.putString("lastScanCode", lastScanCode)
+ outState.putString("lastScanType", lastScanSourceType.serverName)
outState.putBoolean("ignore_unpaid", lastIgnoreUnpaid)
outState.putBundle("answers", dialog!!.onSaveInstanceState())
outState.putString("result", om.writeValueAsString(lastScanResult))
}
}
+
+ override fun chipReadSuccessfully(identifier: String, mediaType: ReusableMediaType) {
+ if (identifier.startsWith("08")) {
+ runOnUiThread {
+ showLoadingCard()
+ displayScanResult(
+ TicketCheckProvider.CheckResult(TicketCheckProvider.CheckResult.Type.ERROR, getString(R.string.nfc_random_uid), false),
+ null
+ )
+ }
+ return
+ }
+
+ lastScanTime = System.currentTimeMillis()
+ lastScanCode = identifier
+ lastScanSourceType = mediaType
+ lastScanResult = null
+ lastIgnoreUnpaid = false
+
+ handleScan(
+ identifier,
+ mediaType.serverName,
+ null,
+ !conf.unpaidAsk
+ )
+ }
+
+ override fun chipReadError(error: ChipReadError, identifier: String?) {
+ val error = when (error) {
+ ChipReadError.IO_ERROR -> getString(R.string.nfc_read_error)
+ ChipReadError.UNKNOWN_CHIP_TYPE -> getString(R.string.nfc_unknown_chip_type)
+ ChipReadError.FOREIGN_CHIP -> getString(R.string.nfc_foreign_chip)
+ ChipReadError.EMPTY_CHIP -> getString(R.string.nfc_empty_chip)
+ else -> error.toString()
+ }
+
+ runOnUiThread {
+ showLoadingCard()
+ displayScanResult(
+ TicketCheckProvider.CheckResult(TicketCheckProvider.CheckResult.Type.ERROR, error, false),
+ null
+ )
+ }
+ }
+
+ private fun reloadNfcHandler() {
+ if (conf.synchronizedEvents.isNotEmpty()) {
+ val eventSlug = conf.synchronizedEvents.first()
+ val settingsManager = SettingsManager(application)
+ val useRandomIdForNewTags = settingsManager.getBySlug(eventSlug)?.json?.optBoolean("reusable_media_type_nfc_mf0aes_random_uid", false) ?: false
+ val activeMediaTypes = getActiveMediaTypes(
+ settingsManager,
+ eventSlug
+ )
+ if (!activeMediaTypes.any { it.isNfcBased }) {
+ if (nfcHandler?.isRunning() == true) {
+ nfcHandler?.stop()
+ }
+ return
+ }
+ if (nfcHandler?.isRunning() != true || activeMediaTypes.toSet() != nfcHandler?.getMediaTypes()?.toSet()) {
+ nfcHandler?.stop()
+ val keySets = (this.applicationContext as PretixScan).db.mediumKeySetQueries.selectAll()
+ .executeAsList()
+ .map { it.toModel() }
+ .map {
+ Mf0aesKeySet(
+ it.publicId,
+ it.organizer == conf.organizerSlug && it.active,
+ conf.keyStore.decryptRsa(
+ "device",
+ eu.pretix.libpretixsync.utils.codec.binary.Base64.decodeBase64(it.uidKey.toByteArray(Charset.defaultCharset()))
+ ),
+ conf.keyStore.decryptRsa(
+ "device",
+ eu.pretix.libpretixsync.utils.codec.binary.Base64.decodeBase64(it.diversificationKey.toByteArray(Charset.defaultCharset()))
+ ),
+ )
+ }
+
+ nfcHandler = getNfcHandler(this, keySets, useRandomIdForNewTags, nfcReaderType = conf.nfcReaderType)
+ nfcHandler!!.setOnChipReadListener(this)
+ try {
+ nfcHandler!!.start(activeMediaTypes)
+ } catch (_: NfcUnsupported) {
+ // silently ignore, else users on non-nfc-devices are warned all the time
+ } catch (_: NfcDisabled) {
+ // silently ignore, we may want to show an unobtrusive warning in the future
+ }
+ }
+ }
+ }
}
diff --git a/pretixscan/app/src/main/java/eu/pretix/pretixscan/droid/ui/UnpaidOrderDialogHelper.kt b/pretixscan/app/src/main/java/eu/pretix/pretixscan/droid/ui/UnpaidOrderDialogHelper.kt
index 183a9498..dd7ac07b 100644
--- a/pretixscan/app/src/main/java/eu/pretix/pretixscan/droid/ui/UnpaidOrderDialogHelper.kt
+++ b/pretixscan/app/src/main/java/eu/pretix/pretixscan/droid/ui/UnpaidOrderDialogHelper.kt
@@ -1,25 +1,25 @@
package eu.pretix.pretixscan.droid.ui
import android.app.Activity
-import android.app.Dialog
import android.content.DialogInterface
import android.content.Intent
import android.view.WindowManager
import androidx.appcompat.app.AlertDialog
import eu.pretix.libpretixsync.check.TicketCheckProvider
import eu.pretix.libpretixsync.db.Answer
+import eu.pretix.libpretixsync.db.ReusableMediaType
import eu.pretix.libpretixui.android.questions.QuestionsDialogInterface
import eu.pretix.pretixscan.droid.R
-class UnpaidDialog(ctx: Activity, val secret: String, val answers: MutableList?,
- val retryHandler: ((String, MutableList?, Boolean) -> Unit)) : AlertDialog(ctx), QuestionsDialogInterface {
+class UnpaidDialog(ctx: Activity, val secret: String, val sourceType: ReusableMediaType, val answers: MutableList?,
+ val retryHandler: ((String, ReusableMediaType, MutableList?, Boolean) -> Unit)) : AlertDialog(ctx), QuestionsDialogInterface {
init {
setTitle(R.string.dialog_unpaid_title)
setMessage(ctx.getString(R.string.dialog_unpaid_text))
setButton(DialogInterface.BUTTON_POSITIVE, ctx.getString(R.string.dialog_unpaid_retry)) { p0, p1 ->
dismiss()
- retryHandler(secret, answers, true)
+ retryHandler(secret, sourceType, answers, true)
}
setButton(DialogInterface.BUTTON_NEGATIVE, ctx.getString(eu.pretix.libpretixui.android.R.string.cancel)) { p0, p1 ->
cancel()
@@ -31,10 +31,13 @@ class UnpaidDialog(ctx: Activity, val secret: String, val answers: MutableList?,
- retryHandler: ((String, MutableList?, Boolean) -> Unit)): QuestionsDialogInterface {
- val dialog = UnpaidDialog(ctx, secret, answers, retryHandler)
+fun showUnpaidDialog(ctx: Activity,
+ res: TicketCheckProvider.CheckResult,
+ secret: String,
+ sourceType: ReusableMediaType,
+ answers: MutableList?,
+ retryHandler: ((String, ReusableMediaType, MutableList?, Boolean) -> Unit)): QuestionsDialogInterface {
+ val dialog = UnpaidDialog(ctx, secret, sourceType, answers, retryHandler)
dialog.show()
return dialog
}
diff --git a/pretixscan/app/src/main/java/eu/pretix/pretixscan/utils/Settings.kt b/pretixscan/app/src/main/java/eu/pretix/pretixscan/utils/Settings.kt
new file mode 100644
index 00000000..a40c65e9
--- /dev/null
+++ b/pretixscan/app/src/main/java/eu/pretix/pretixscan/utils/Settings.kt
@@ -0,0 +1,13 @@
+package eu.pretix.pretixscan.utils
+
+import android.app.Application
+import eu.pretix.libpretixsync.models.Settings
+import eu.pretix.libpretixsync.models.db.toModel
+import eu.pretix.libpretixsync.utils.SettingsManager
+import eu.pretix.pretixscan.droid.PretixScan
+
+class SettingsManager(private val application: Application): SettingsManager {
+ override fun getBySlug(eventSlug: String): Settings? {
+ return (application as PretixScan).db.settingsQueries.selectBySlug(eventSlug).executeAsOneOrNull()?.toModel()
+ }
+}
\ No newline at end of file
diff --git a/pretixscan/libpretixnfc-repo b/pretixscan/libpretixnfc-repo
new file mode 160000
index 00000000..d2a04c79
--- /dev/null
+++ b/pretixscan/libpretixnfc-repo
@@ -0,0 +1 @@
+Subproject commit d2a04c79ecb6d8b7abf4b8f1a59a609b879b0c39
diff --git a/pretixscan/libpretixsync-repo b/pretixscan/libpretixsync-repo
index c4011dc8..1edc026e 160000
--- a/pretixscan/libpretixsync-repo
+++ b/pretixscan/libpretixsync-repo
@@ -1 +1 @@
-Subproject commit c4011dc8c6604a50664bd64ddeec395d745cb589
+Subproject commit 1edc026ebe40b4c68b7cc5f5f5383883b933f231
diff --git a/pretixscan/libpretixui-repo b/pretixscan/libpretixui-repo
index 4136af04..8bb21189 160000
--- a/pretixscan/libpretixui-repo
+++ b/pretixscan/libpretixui-repo
@@ -1 +1 @@
-Subproject commit 4136af0492dff9610b724d3b718ed3189132aa39
+Subproject commit 8bb211896fd68e3888d187ce6c60f0b982a9317e
diff --git a/pretixscan/settings.gradle b/pretixscan/settings.gradle
index ffa3386a..b9a52074 100644
--- a/pretixscan/settings.gradle
+++ b/pretixscan/settings.gradle
@@ -12,9 +12,11 @@ plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0'
}
-include ':app', ':libpretixsync', ':libpretixui-android', ':android-libserenegiantcommon', ':android-libusbcameracommon', ':android-libuvccamera'
+include ':app', ':libpretixsync', ':libpretixui-android', ':libpretixnfc', ':libpretixnfc-android', ':android-libserenegiantcommon', ':android-libusbcameracommon', ':android-libuvccamera'
project(':libpretixsync').projectDir = new File('libpretixsync-repo/libpretixsync')
project(':libpretixui-android').projectDir = new File('libpretixui-repo/libpretixui-android')
+project(':libpretixnfc').projectDir = new File('libpretixnfc-repo/libpretixnfc')
+project(':libpretixnfc-android').projectDir = new File('libpretixnfc-repo/libpretixnfc-android')
project(':android-libserenegiantcommon').projectDir = new File('libpretixui-repo/android-libserenegiantcommon')
project(':android-libusbcameracommon').projectDir = new File('libpretixui-repo/android-libusbcameracommon')
project(':android-libuvccamera').projectDir = new File('libpretixui-repo/android-libuvccamera')