diff --git a/pretixscan/app/src/main/AndroidManifest.xml b/pretixscan/app/src/main/AndroidManifest.xml index e189d564..c7f5d745 100644 --- a/pretixscan/app/src/main/AndroidManifest.xml +++ b/pretixscan/app/src/main/AndroidManifest.xml @@ -9,6 +9,8 @@ + + diff --git a/pretixscan/app/src/main/java/eu/pretix/pretixscan/droid/AppConfig.kt b/pretixscan/app/src/main/java/eu/pretix/pretixscan/droid/AppConfig.kt index fb058f1c..afafbe20 100644 --- a/pretixscan/app/src/main/java/eu/pretix/pretixscan/droid/AppConfig.kt +++ b/pretixscan/app/src/main/java/eu/pretix/pretixscan/droid/AppConfig.kt @@ -353,6 +353,10 @@ class AppConfig(ctx: Context) : ConfigStore { get() = default_prefs.getBoolean(PREFS_KEY_SOUNDS, true) set(value) = default_prefs.edit().putBoolean(PREFS_KEY_SOUNDS, value).apply() + var haptics: Boolean + get() = default_prefs.getBoolean(PREFS_KEY_HAPTICS, false) + set(value) = default_prefs.edit().putBoolean(PREFS_KEY_HAPTICS, value).apply() + var proxyMode: Boolean get() = default_prefs.getBoolean(PREFS_KEY_SCAN_PROXY, false) set(value) = default_prefs.edit().putBoolean(PREFS_KEY_SCAN_PROXY, value).apply() @@ -447,6 +451,7 @@ class AppConfig(ctx: Context) : ConfigStore { val PREFS_KEY_IGNORE_QUESTIONS = "pref_ignore_questions" val PREFS_KEY_SCAN_TYPE = "pref_scan_type" val PREFS_KEY_SOUNDS = "pref_sounds" + val PREFS_KEY_HAPTICS = "pref_haptics" val PREFS_KEY_HIDE_NAMES = "pref_hide_names" val PREFS_KEY_SEARCH_DISABLE = "pref_search_disable" val PREFS_KEY_KIOSK_MODE = "pref_kiosk_mode" diff --git a/pretixscan/app/src/main/java/eu/pretix/pretixscan/droid/ui/MainActivity.kt b/pretixscan/app/src/main/java/eu/pretix/pretixscan/droid/ui/MainActivity.kt index f4d3ad7e..e204f26c 100644 --- a/pretixscan/app/src/main/java/eu/pretix/pretixscan/droid/ui/MainActivity.kt +++ b/pretixscan/app/src/main/java/eu/pretix/pretixscan/droid/ui/MainActivity.kt @@ -22,6 +22,9 @@ import android.os.Build import android.os.Bundle import android.os.Handler import android.os.ResultReceiver +import android.os.VibrationEffect +import android.os.Vibrator +import android.os.VibratorManager import android.text.SpannableString import android.text.SpannableStringBuilder import android.util.Base64 @@ -255,6 +258,22 @@ class MainActivity : AppCompatActivity(), ReloadableActivity, ScannerView.Result view_data.isOffline.set(conf.offlineMode) } + private fun vibrate(success: Boolean) { + val vibrator: Vibrator + if (Build.VERSION.SDK_INT >= 31) { + val vb = this.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager + vibrator = vb.defaultVibrator + } else { + vibrator = this.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + } + + if (Build.VERSION.SDK_INT >= 26) { + vibrator.vibrate(VibrationEffect.createOneShot(if (success) 50L else 200L, VibrationEffect.DEFAULT_AMPLITUDE)) + } else { + vibrator.vibrate(if (success) 50L else 200L) + } + } + private fun setSearchFilter(f: String) { binding.cardSearch.visibility = View.VISIBLE view_data.searchState.set(LOADING) @@ -957,8 +976,9 @@ class MainActivity : AppCompatActivity(), ReloadableActivity, ScannerView.Result showLoadingCard() hideSearchCard() - if (answers == null && !ignore_unpaid && !conf.offlineMode && conf.sounds) { - mediaPlayers[R.raw.beep]?.start() + if (answers == null && !ignore_unpaid && !conf.offlineMode) { + if (conf.sounds) mediaPlayers[R.raw.beep]?.start() + if (conf.haptics) vibrate(true) } bgScope.launch { @@ -1054,30 +1074,40 @@ class MainActivity : AppCompatActivity(), ReloadableActivity, ScannerView.Result lastScanResult = result lastIgnoreUnpaid = ignore_unpaid - if (conf.sounds) - when (result.type) { + when (result.type) { TicketCheckProvider.CheckResult.Type.VALID -> when (result.scanType) { TicketCheckProvider.CheckInType.ENTRY -> if (result.isRequireAttention) { mediaPlayers[R.raw.attention]?.start() + if (conf.haptics) vibrate(false) } else { - mediaPlayers[R.raw.enter]?.start() + if (conf.sounds) mediaPlayers[R.raw.enter]?.start() + if (conf.haptics) vibrate(true) } - TicketCheckProvider.CheckInType.EXIT -> mediaPlayers[R.raw.exit]?.start() + TicketCheckProvider.CheckInType.EXIT -> { + if (conf.sounds) mediaPlayers[R.raw.exit]?.start() + if (conf.haptics) vibrate(true) + } + } + TicketCheckProvider.CheckResult.Type.INVALID, + TicketCheckProvider.CheckResult.Type.ERROR, + TicketCheckProvider.CheckResult.Type.UNPAID, + TicketCheckProvider.CheckResult.Type.CANCELED, + TicketCheckProvider.CheckResult.Type.PRODUCT, + TicketCheckProvider.CheckResult.Type.RULES, + TicketCheckProvider.CheckResult.Type.AMBIGUOUS, + TicketCheckProvider.CheckResult.Type.REVOKED, + TicketCheckProvider.CheckResult.Type.UNAPPROVED, + TicketCheckProvider.CheckResult.Type.BLOCKED, + TicketCheckProvider.CheckResult.Type.INVALID_TIME, + TicketCheckProvider.CheckResult.Type.USED -> { + if (conf.sounds) mediaPlayers[R.raw.error]?.start() + if (conf.haptics) vibrate(false) + } + TicketCheckProvider.CheckResult.Type.ANSWERS_REQUIRED -> { + if (conf.sounds) mediaPlayers[R.raw.attention]?.start() + if (conf.haptics) vibrate(false) } - TicketCheckProvider.CheckResult.Type.INVALID -> mediaPlayers[R.raw.error]?.start() - TicketCheckProvider.CheckResult.Type.ERROR -> mediaPlayers[R.raw.error]?.start() - TicketCheckProvider.CheckResult.Type.UNPAID -> mediaPlayers[R.raw.error]?.start() - TicketCheckProvider.CheckResult.Type.CANCELED -> mediaPlayers[R.raw.error]?.start() - TicketCheckProvider.CheckResult.Type.PRODUCT -> mediaPlayers[R.raw.error]?.start() - TicketCheckProvider.CheckResult.Type.RULES -> mediaPlayers[R.raw.error]?.start() - TicketCheckProvider.CheckResult.Type.AMBIGUOUS -> mediaPlayers[R.raw.error]?.start() - TicketCheckProvider.CheckResult.Type.REVOKED -> mediaPlayers[R.raw.error]?.start() - TicketCheckProvider.CheckResult.Type.UNAPPROVED -> mediaPlayers[R.raw.error]?.start() - TicketCheckProvider.CheckResult.Type.BLOCKED -> mediaPlayers[R.raw.error]?.start() - TicketCheckProvider.CheckResult.Type.INVALID_TIME -> mediaPlayers[R.raw.error]?.start() - TicketCheckProvider.CheckResult.Type.USED -> mediaPlayers[R.raw.error]?.start() - TicketCheckProvider.CheckResult.Type.ANSWERS_REQUIRED -> mediaPlayers[R.raw.attention]?.start() else -> { } } diff --git a/pretixscan/app/src/main/res/values/strings.xml b/pretixscan/app/src/main/res/values/strings.xml index 3b40bbae..27458961 100644 --- a/pretixscan/app/src/main/res/values/strings.xml +++ b/pretixscan/app/src/main/res/values/strings.xml @@ -109,6 +109,7 @@ When scanning offline, your device will verify all input with its internal database instead with the server and synchronize its database with the server occasionally. This is more reliable, but allows a ticket to be scanned twice if you scan with multiple devices. Play sounds + Enable Haptic Feedback Hide names and other personal information Does not affect search, useful e.g. in combination with kiosk mode Badges diff --git a/pretixscan/app/src/main/res/xml/preferences.xml b/pretixscan/app/src/main/res/xml/preferences.xml index 4a4a1668..fa015e84 100644 --- a/pretixscan/app/src/main/res/xml/preferences.xml +++ b/pretixscan/app/src/main/res/xml/preferences.xml @@ -40,6 +40,10 @@ android:defaultValue="true" android:key="pref_sounds" android:title="@string/settings_label_sounds" /> +