From 6e0a000eb30cfde7247a4dc6eae940e92f1ce076 Mon Sep 17 00:00:00 2001 From: darken Date: Tue, 4 Jan 2022 23:49:36 +0100 Subject: [PATCH] Add autopause and autoplay --- .../eu/darken/capod/common/MediaControl.kt | 58 +++++++++++++++++++ .../capod/common/bluetooth/BleScanner.kt | 2 +- .../capod/common/dagger/AndroidModule.kt | 6 ++ .../darken/capod/main/core/GeneralSettings.kt | 6 ++ .../main/ui/overview/OverviewFragmentVM.kt | 25 ++++---- .../cards/pods/SingleApplePodsCardVH.kt | 2 +- .../darken/capod/monitor/core/PodMonitor.kt | 9 ++- .../monitor/core/worker/MonitorWorker.kt | 25 ++++++-- .../capod/monitor/ui/MonitorNotifications.kt | 2 +- .../darken/capod/pods/core/DualPodDevice.kt | 10 ---- .../darken/capod/pods/core/HasEarDetection.kt | 7 +++ .../capod/pods/core/apple/DualApplePods.kt | 16 +++-- .../capod/pods/core/apple/SingleApplePods.kt | 8 ++- .../drawable/ic_baseline_play_circle_24.xml | 10 ++++ app/src/main/res/layout/main_fragment.xml | 11 +--- app/src/main/res/layout/main_toggle_item.xml | 41 ------------- app/src/main/res/values/strings.xml | 6 +- app/src/main/res/xml/preferences_general.xml | 6 ++ .../pods/core/apple/SingleApplePodsTest.kt | 2 +- .../pods/core/apple/airpods/AirPodsMaxTest.kt | 4 +- 20 files changed, 164 insertions(+), 92 deletions(-) create mode 100644 app/src/main/java/eu/darken/capod/common/MediaControl.kt create mode 100644 app/src/main/java/eu/darken/capod/pods/core/HasEarDetection.kt create mode 100644 app/src/main/res/drawable/ic_baseline_play_circle_24.xml delete mode 100644 app/src/main/res/layout/main_toggle_item.xml diff --git a/app/src/main/java/eu/darken/capod/common/MediaControl.kt b/app/src/main/java/eu/darken/capod/common/MediaControl.kt new file mode 100644 index 00000000..8e2e7a19 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/common/MediaControl.kt @@ -0,0 +1,58 @@ +package eu.darken.capod.common + +import android.media.AudioManager +import android.os.SystemClock +import android.view.KeyEvent +import eu.darken.capod.common.debug.logging.log +import eu.darken.capod.common.debug.logging.logTag +import kotlinx.coroutines.delay +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class MediaControl @Inject constructor( + private val audioManager: AudioManager, +) { + + val isPlaying: Boolean + get() = audioManager.isMusicActive + + suspend fun sendPlay() { + log(TAG) { "sendPlay()" } + if (audioManager.isMusicActive) { + log(TAG) { "Music is already playing, not sending play" } + return + } + sendKey(KeyEvent.KEYCODE_MEDIA_PLAY) + } + + suspend fun sendPause() { + log(TAG) { "sendPause()" } + if (!audioManager.isMusicActive) { + log(TAG) { "Music is not playing, not sending pause" } + return + } + sendKey(KeyEvent.KEYCODE_MEDIA_PAUSE) + } + + suspend fun sendPlayPause() { + log(TAG) { "sendPlayPause()" } + if (audioManager.isMusicActive) { + sendPause() + } else { + sendPlay() + } + } + + private suspend fun sendKey(keyCode: Int) { + log(TAG) { "Sending up+down KeyEvent: $keyCode" } + val eventTime = SystemClock.uptimeMillis() + audioManager.dispatchMediaKeyEvent(KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, keyCode, 0)) + delay(100) + audioManager.dispatchMediaKeyEvent(KeyEvent(eventTime + 200, eventTime + 200, KeyEvent.ACTION_UP, keyCode, 0)) + } + + companion object { + private val TAG = logTag("MediaControl") + } +} diff --git a/app/src/main/java/eu/darken/capod/common/bluetooth/BleScanner.kt b/app/src/main/java/eu/darken/capod/common/bluetooth/BleScanner.kt index 76de1497..e485edcb 100644 --- a/app/src/main/java/eu/darken/capod/common/bluetooth/BleScanner.kt +++ b/app/src/main/java/eu/darken/capod/common/bluetooth/BleScanner.kt @@ -27,7 +27,7 @@ class BleScanner @Inject constructor( fun scan( filter: Set = ProximityPairing.getBleScanFilter(), mode: Int = ScanSettings.SCAN_MODE_LOW_POWER, - delay: Long = 1, + delay: Long = 100, ): Flow> = callbackFlow { val scanner = bluetoothManager.adapter.bluetoothLeScanner diff --git a/app/src/main/java/eu/darken/capod/common/dagger/AndroidModule.kt b/app/src/main/java/eu/darken/capod/common/dagger/AndroidModule.kt index 5ade1bf9..57905e34 100644 --- a/app/src/main/java/eu/darken/capod/common/dagger/AndroidModule.kt +++ b/app/src/main/java/eu/darken/capod/common/dagger/AndroidModule.kt @@ -4,6 +4,7 @@ import android.app.Application import android.app.NotificationManager import android.bluetooth.BluetoothManager import android.content.Context +import android.media.AudioManager import androidx.work.WorkManager import dagger.Module import dagger.Provides @@ -33,4 +34,9 @@ class AndroidModule { @Singleton fun workerManager(context: Context): WorkManager = WorkManager.getInstance(context) + + @Provides + @Singleton + fun audioManager(context: Context): AudioManager = + context.getSystemService(Context.AUDIO_SERVICE) as AudioManager } diff --git a/app/src/main/java/eu/darken/capod/main/core/GeneralSettings.kt b/app/src/main/java/eu/darken/capod/main/core/GeneralSettings.kt index 5afcc2a8..1e441cc1 100644 --- a/app/src/main/java/eu/darken/capod/main/core/GeneralSettings.kt +++ b/app/src/main/java/eu/darken/capod/main/core/GeneralSettings.kt @@ -39,6 +39,11 @@ class GeneralSettings @Inject constructor( false ) + val autoPlay = preferences.createFlowPreference( + "core.eardetection.autoplay.enabled", + false + ) + val showAll = preferences.createFlowPreference( "core.showall.enabled", false @@ -48,6 +53,7 @@ class GeneralSettings @Inject constructor( monitorMode, scannerMode, autoPause, + autoPlay, showAll, debugSettings.isDebugModeEnabled, debugSettings.isAutoReportEnabled diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragmentVM.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragmentVM.kt index fea45e0e..21c0ba70 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragmentVM.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragmentVM.kt @@ -14,6 +14,7 @@ import eu.darken.capod.common.permissions.Permission import eu.darken.capod.common.permissions.isGrantedOrNotRequired import eu.darken.capod.common.uix.ViewModel3 import eu.darken.capod.main.core.GeneralSettings +import eu.darken.capod.main.core.MonitorMode import eu.darken.capod.main.ui.overview.cards.PermissionCardVH import eu.darken.capod.main.ui.overview.cards.pods.BasicSingleApplePodsCardVH import eu.darken.capod.main.ui.overview.cards.pods.DualApplePodsCardVH @@ -50,6 +51,11 @@ class OverviewFragmentVM @Inject constructor( delay(1000) } } + + val showManualRefresh = generalSettings.monitorMode.flow + .map { it == MonitorMode.MANUAL } + .asLiveData2() + private val permissionCheckTrigger = MutableStateFlow(UUID.randomUUID()) private val requiredPermissions: Flow> = permissionCheckTrigger .map { @@ -66,17 +72,16 @@ class OverviewFragmentVM @Inject constructor( private val pods: Flow> = requiredPermissions .flatMapLatest { permissions -> if (permissions.isEmpty()) { - combine(podMonitor.pods, generalSettings.showAll.flow) { pods, showAll -> - if (showAll) { - pods - .sortedByDescending { it.rssi } - } else { - pods - .maxByOrNull { it.rssi } - ?.let { listOf(it) } - ?: emptyList() + generalSettings.showAll.flow + .flatMapLatest { showAll -> + if (showAll) { + podMonitor.devices + } else { + podMonitor.mainDevice.map { mainDevice -> + mainDevice?.let { listOf(it) } ?: emptyList() + } + } } - } } else { channelFlow { send(emptyList()) diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/SingleApplePodsCardVH.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/SingleApplePodsCardVH.kt index 077741c5..ee122ca7 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/SingleApplePodsCardVH.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/SingleApplePodsCardVH.kt @@ -41,7 +41,7 @@ class SingleApplePodsCardVH(parent: ViewGroup) : sb.append("\n").append(device.getBatteryLevelHeadset(context)) when { device.isHeadsetBeingCharged -> sb.append("\n").append("Charging") - device.isHeadsetBeingWorn -> sb.append("\n").append("In ear") + device.isHeadphonesBeingWorn -> sb.append("\n").append("In ear") else -> {} } text = sb diff --git a/app/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt b/app/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt index 434662c3..569a7c73 100644 --- a/app/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt +++ b/app/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt @@ -23,7 +23,7 @@ class PodMonitor @Inject constructor( private val generalSettings: GeneralSettings, ) { - val pods: Flow> = generalSettings.scannerMode.flow + val devices: Flow> = generalSettings.scannerMode.flow .flatMapLatest { bleScanner.scan( mode = when (it) { @@ -80,6 +80,13 @@ class PodMonitor @Inject constructor( pods } + val mainDevice: Flow + get() = devices.map { devices -> + devices.maxByOrNull { it.rssi }?.let { + if (it.rssi > -85) it else null + } + } + companion object { private val TAG = logTag("Monitor", "PodMonitor") } diff --git a/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorWorker.kt b/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorWorker.kt index 3ba134d0..f3024b6e 100644 --- a/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorWorker.kt +++ b/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorWorker.kt @@ -8,12 +8,14 @@ import androidx.work.WorkerParameters import dagger.assisted.Assisted import dagger.assisted.AssistedInject import dagger.hilt.EntryPoints +import eu.darken.capod.common.MediaControl import eu.darken.capod.common.debug.Bugs import eu.darken.capod.common.debug.logging.Logging.Priority.* import eu.darken.capod.common.debug.logging.asLog import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.flow.setupCommonEventHandlers +import eu.darken.capod.common.flow.withPrevious import eu.darken.capod.common.permissions.Permission import eu.darken.capod.common.permissions.isGrantedOrNotRequired import eu.darken.capod.main.core.GeneralSettings @@ -21,6 +23,7 @@ import eu.darken.capod.main.core.MonitorMode import eu.darken.capod.monitor.core.MonitorComponent import eu.darken.capod.monitor.core.MonitorCoroutineScope import eu.darken.capod.monitor.ui.MonitorNotifications +import eu.darken.capod.pods.core.HasEarDetection import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol import kotlinx.coroutines.cancel import kotlinx.coroutines.cancelChildren @@ -36,6 +39,7 @@ class MonitorWorker @AssistedInject constructor( private val monitorNotifications: MonitorNotifications, private val notificationManager: NotificationManager, private val generalSettings: GeneralSettings, + private val mediaControl: MediaControl, ) : CoroutineWorker(context, params) { private val workerScope = MonitorCoroutineScope() @@ -105,18 +109,31 @@ class MonitorWorker @AssistedInject constructor( } .launchIn(workerScope) - - val monitorJob = podMonitor.pods + val monitorJob = podMonitor.mainDevice .setupCommonEventHandlers(TAG) { "PodMonitor" } .onStart { setForeground(monitorNotifications.getForegroundInfo(null)) } - .onEach { + .onEach { currentDevice -> notificationManager.notify( MonitorNotifications.NOTIFICATION_ID, - monitorNotifications.getNotification(it.firstOrNull()) + monitorNotifications.getNotification(currentDevice) ) } + .withPrevious() + .onEach { (previous, current) -> + if (previous is HasEarDetection && current is HasEarDetection) { + log(TAG) { "previous=${previous.isBeingWorn}, current=${current.isBeingWorn}" } + log(TAG) { "previous-id=${previous.identifier}, current-id=${current.identifier}" } + if (previous.identifier == current.identifier && previous.isBeingWorn != current.isBeingWorn) { + if (generalSettings.autoPlay.value && !mediaControl.isPlaying) { + mediaControl.sendPlay() + } else if (generalSettings.autoPause.value && mediaControl.isPlaying) { + mediaControl.sendPause() + } + } + } + } .launchIn(workerScope) log(TAG, VERBOSE) { "Monitor job is active" } diff --git a/app/src/main/java/eu/darken/capod/monitor/ui/MonitorNotifications.kt b/app/src/main/java/eu/darken/capod/monitor/ui/MonitorNotifications.kt index ee609a2d..9726594d 100644 --- a/app/src/main/java/eu/darken/capod/monitor/ui/MonitorNotifications.kt +++ b/app/src/main/java/eu/darken/capod/monitor/ui/MonitorNotifications.kt @@ -60,7 +60,7 @@ class MonitorNotifications @Inject constructor( fun getBuilder(device: PodDevice?): NotificationCompat.Builder { if (device == null) { - builder.setContentTitle(context.getString(R.string.device_status_loading_message)) + builder.setContentTitle(context.getString(R.string.device_none_label)) builder.setSmallIcon(R.drawable.ic_device_generic_earbuds) return builder } diff --git a/app/src/main/java/eu/darken/capod/pods/core/DualPodDevice.kt b/app/src/main/java/eu/darken/capod/pods/core/DualPodDevice.kt index 1d634b0b..f1bf3c7a 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/DualPodDevice.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/DualPodDevice.kt @@ -12,14 +12,4 @@ interface DualPodDevice : PodDevice { val batteryRightPodPercent: Float? val batteryCasePercent: Float? - - val isLeftPodInEar: Boolean - - val isRightPodInEar: Boolean - - val isCaseCharging: Boolean - - val isLeftPodCharging: Boolean - - val isRightPodCharging: Boolean } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/HasEarDetection.kt b/app/src/main/java/eu/darken/capod/pods/core/HasEarDetection.kt new file mode 100644 index 00000000..326c4c94 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/pods/core/HasEarDetection.kt @@ -0,0 +1,7 @@ +package eu.darken.capod.pods.core + +interface HasEarDetection : PodDevice { + + val isBeingWorn: Boolean + +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/DualApplePods.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/DualApplePods.kt index 00f5c239..3cdb8e72 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/DualApplePods.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/DualApplePods.kt @@ -9,8 +9,9 @@ import eu.darken.capod.common.lowerNibble import eu.darken.capod.common.upperNibble import eu.darken.capod.pods.core.DualPodDevice import eu.darken.capod.pods.core.DualPodDevice.Pod +import eu.darken.capod.pods.core.HasEarDetection -interface DualApplePods : ApplePods, DualPodDevice { +interface DualApplePods : ApplePods, DualPodDevice, HasEarDetection { val microPhonePod: Pod get() = when (rawStatus.isBitSet(5)) { @@ -63,28 +64,31 @@ interface DualApplePods : ApplePods, DualPodDevice { } } - override val isLeftPodInEar: Boolean + val isLeftPodInEar: Boolean get() = when (microPhonePod) { Pod.LEFT -> rawStatus.isBitSet(1) Pod.RIGHT -> rawStatus.isBitSet(3) } - override val isRightPodInEar: Boolean + val isRightPodInEar: Boolean get() = when (microPhonePod) { Pod.LEFT -> rawStatus.isBitSet(3) Pod.RIGHT -> rawStatus.isBitSet(1) } - override val isCaseCharging: Boolean + override val isBeingWorn: Boolean + get() = isLeftPodInEar && isRightPodInEar + + val isCaseCharging: Boolean get() = rawCaseBattery.upperNibble.isBitSet(2) - override val isLeftPodCharging: Boolean + val isLeftPodCharging: Boolean get() = when (microPhonePod) { Pod.LEFT -> rawCaseBattery.upperNibble.isBitSet(0) Pod.RIGHT -> rawCaseBattery.upperNibble.isBitSet(1) } - override val isRightPodCharging: Boolean + val isRightPodCharging: Boolean get() = when (microPhonePod) { Pod.LEFT -> rawCaseBattery.upperNibble.isBitSet(1) Pod.RIGHT -> rawCaseBattery.upperNibble.isBitSet(0) diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/SingleApplePods.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/SingleApplePods.kt index 5de84e21..a8995227 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/SingleApplePods.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/SingleApplePods.kt @@ -4,8 +4,9 @@ import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.isBitSet import eu.darken.capod.common.lowerNibble import eu.darken.capod.common.upperNibble +import eu.darken.capod.pods.core.HasEarDetection -interface SingleApplePods : BasicSingleApplePods { +interface SingleApplePods : BasicSingleApplePods, HasEarDetection { val batteryCasePercent: Float? get() = when (val value = rawCaseBattery.lowerNibble.toInt()) { @@ -18,7 +19,7 @@ interface SingleApplePods : BasicSingleApplePods { } } - val isHeadsetBeingWorn: Boolean + val isHeadphonesBeingWorn: Boolean get() = rawStatus.isBitSet(1) val isCaseCharging: Boolean @@ -26,4 +27,7 @@ interface SingleApplePods : BasicSingleApplePods { val isHeadsetBeingCharged: Boolean get() = rawCaseBattery.upperNibble.isBitSet(0) + + override val isBeingWorn: Boolean + get() = isHeadphonesBeingWorn } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_play_circle_24.xml b/app/src/main/res/drawable/ic_baseline_play_circle_24.xml new file mode 100644 index 00000000..56565cf9 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_play_circle_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/main_fragment.xml b/app/src/main/res/layout/main_fragment.xml index 48bfafa9..f59de350 100644 --- a/app/src/main/res/layout/main_fragment.xml +++ b/app/src/main/res/layout/main_fragment.xml @@ -22,17 +22,8 @@ android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" - tools:listitem="@layout/main_toggle_item" + tools:listitem="@layout/overview_pods_apple_dual_item" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/toolbar" /> - - \ No newline at end of file diff --git a/app/src/main/res/layout/main_toggle_item.xml b/app/src/main/res/layout/main_toggle_item.xml deleted file mode 100644 index 16f55e98..00000000 --- a/app/src/main/res/layout/main_toggle_item.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 990e90fe..fc8ab23b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,7 +2,6 @@ CAPod CAPod Pro - Loading device status Device status Error @@ -82,6 +81,7 @@ Automatic bug reports Automatically reports issues, e.g. details on an app crash so I can figure out how to fix it. Unknown device + No device Headphones Debug mode Show additional information to troubleshoot issues. @@ -96,7 +96,9 @@ Balanced Low latency Auto pause - Pause music when removing the device from your ear (if your device supports it). + Pause music when removing the device from your ear (if supported). Show all devices Show other people\'s devices that are near you. + Auto play + Start music playback music when wearing the device (if supported). \ No newline at end of file diff --git a/app/src/main/res/xml/preferences_general.xml b/app/src/main/res/xml/preferences_general.xml index 29def366..84f487f1 100644 --- a/app/src/main/res/xml/preferences_general.xml +++ b/app/src/main/res/xml/preferences_general.xml @@ -13,6 +13,12 @@ android:summary="@string/settings_scanner_mode_description" android:title="@string/settings_scanner_mode_label" /> + +