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" />
+