diff --git a/app/src/main/java/com/artemchep/pocketmode/sensors/FlowOfKeyguard.kt b/app/src/main/java/com/artemchep/pocketmode/sensors/FlowOfKeyguard.kt new file mode 100644 index 0000000..5d2f585 --- /dev/null +++ b/app/src/main/java/com/artemchep/pocketmode/sensors/FlowOfKeyguard.kt @@ -0,0 +1,29 @@ +package com.artemchep.pocketmode.sensors + +import android.content.Context +import com.artemchep.pocketmode.ext.isKeyguardLocked +import com.artemchep.pocketmode.models.Keyguard +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn + +private const val KEYGUARD_CHECK_INTERVAL = 300L + +/** + * @author Artem Chepurnoy + */ +fun flowOfKeyguard( + context: Context +): Flow = flow { + while (true) { + val keyguard = when (context.isKeyguardLocked()) { + true -> Keyguard.Locked + false -> Keyguard.Unlocked + } + emit(keyguard) + delay(KEYGUARD_CHECK_INTERVAL) + } +} + .flowOn(Dispatchers.Main) diff --git a/app/src/main/java/com/artemchep/pocketmode/sensors/LockScreenEventFlow.kt b/app/src/main/java/com/artemchep/pocketmode/sensors/FlowOfLockScreen.kt similarity index 86% rename from app/src/main/java/com/artemchep/pocketmode/sensors/LockScreenEventFlow.kt rename to app/src/main/java/com/artemchep/pocketmode/sensors/FlowOfLockScreen.kt index cde3ac4..f90ebee 100644 --- a/app/src/main/java/com/artemchep/pocketmode/sensors/LockScreenEventFlow.kt +++ b/app/src/main/java/com/artemchep/pocketmode/sensors/FlowOfLockScreen.kt @@ -18,24 +18,21 @@ import kotlinx.coroutines.flow.* /** * @author Artem Chepurnoy */ -@Suppress("FunctionName") -fun LockScreenEventFlow( - proximityLiveData: LiveData, - screenLiveData: LiveData, +fun flowOfLockScreen( + proximityFlow: Flow, + screenFlow: Flow, phoneCallLiveData: LiveData, - keyguardLiveData: LiveData + keyguardFlow: Flow ): Flow { - val proximityFlow = proximityLiveData.asFlow().filterNotNull().distinctUntilChanged() - val screenFlow = screenLiveData.asFlow().filterNotNull().distinctUntilChanged() val phoneCallFlow = phoneCallLiveData.asFlow().distinctUntilChanged() - val keyguardFlow = keyguardLiveData.asFlow().distinctUntilChanged() return screenFlow + .distinctUntilChanged() // If the screen is on -> subscribe to the // keyguard flow. .flatMapLatest { screen -> when (screen) { is Screen.On -> combine( - keyguardFlow, + keyguardFlow.distinctUntilChanged(), phoneCallFlow, ) { keyguard, phoneCall -> when { @@ -54,9 +51,11 @@ fun LockScreenEventFlow( } } .flatMapLatest { it } + .distinctUntilChanged() .flatMapLatest { isActive -> if (isActive) { proximityFlow + .distinctUntilChanged() .flatMapLatest { proximity -> when (proximity) { is Proximity.Far -> flowOf(Idle) diff --git a/app/src/main/java/com/artemchep/pocketmode/sensors/FlowOfProximity.kt b/app/src/main/java/com/artemchep/pocketmode/sensors/FlowOfProximity.kt new file mode 100644 index 0000000..e2aec64 --- /dev/null +++ b/app/src/main/java/com/artemchep/pocketmode/sensors/FlowOfProximity.kt @@ -0,0 +1,48 @@ +package com.artemchep.pocketmode.sensors + +import android.content.Context +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import androidx.core.content.getSystemService +import androidx.lifecycle.LiveData +import androidx.lifecycle.asLiveData +import com.artemchep.pocketmode.util.observerFlow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMap + +fun flowOfProximity( + context: Context, +): Flow = observerFlow { callback -> + val sensorManager = context.getSystemService() + ?: return@observerFlow {} + + val sensorProximity = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY) + val sensorListener = object : SensorEventListener { + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { + // Unused. + } + + override fun onSensorChanged(event: SensorEvent) { + val distance = event.values[0] + callback(distance) + } + } + + val delay = SensorManager.SENSOR_DELAY_NORMAL + sensorManager.registerListener(sensorListener, sensorProximity, delay) + + return@observerFlow { + sensorManager.unregisterListener(sensorListener) + } +} + +@Deprecated( + message = "Flow analogue is 'flowOfProximity'", + replaceWith = ReplaceWith("flowOfProximity(context)"), +) +@Suppress("FunctionName") +fun ProximityLiveData( + context: Context +): LiveData = flowOfProximity(context).asLiveData() diff --git a/app/src/main/java/com/artemchep/pocketmode/sensors/ProximityBinaryLiveData.kt b/app/src/main/java/com/artemchep/pocketmode/sensors/FlowOfProximityBinary.kt similarity index 58% rename from app/src/main/java/com/artemchep/pocketmode/sensors/ProximityBinaryLiveData.kt rename to app/src/main/java/com/artemchep/pocketmode/sensors/FlowOfProximityBinary.kt index facb0d6..c96f556 100644 --- a/app/src/main/java/com/artemchep/pocketmode/sensors/ProximityBinaryLiveData.kt +++ b/app/src/main/java/com/artemchep/pocketmode/sensors/FlowOfProximityBinary.kt @@ -7,27 +7,45 @@ import androidx.core.content.getSystemService import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations import com.artemchep.pocketmode.models.Proximity +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map /** * @author Artem Chepurnoy */ @Suppress("FunctionName") +fun flowOfBinaryProximity( + context: Context, + proximitySensor: Flow = flowOfProximity(context), +): Flow { + val proximityBinaryTransformation = proximityBinaryTransformationFactory(context) + return proximitySensor + .map { + proximityBinaryTransformation(it) + } +} + +@Deprecated( + message = "Flow analogue is 'flowOfBinaryProximity'", + replaceWith = ReplaceWith("flowOfBinaryProximity(context, proximitySensor)"), +) +@Suppress("FunctionName") fun ProximityBinaryLiveData( context: Context, - proximitySensor: LiveData, -): LiveData { + proximitySensor: LiveData, +): LiveData { val proximityBinaryTransformation = proximityBinaryTransformationFactory(context) return Transformations.map(proximitySensor, proximityBinaryTransformation) } private fun proximityBinaryTransformationFactory( context: Context, -): (distance: Float?) -> Proximity? { +): (distance: Float) -> Proximity { val sensorProximityMaxRange = context.getSystemService() ?.getDefaultSensor(Sensor.TYPE_PROXIMITY) ?.maximumRange ?: 1.0f return transform@{ distance -> - distance?.let { proximityBinaryTransformationFactory(it, sensorProximityMaxRange) } + proximityBinaryTransformationFactory(distance, sensorProximityMaxRange) } } diff --git a/app/src/main/java/com/artemchep/pocketmode/sensors/FlowOfScreen.kt b/app/src/main/java/com/artemchep/pocketmode/sensors/FlowOfScreen.kt new file mode 100644 index 0000000..706bf35 --- /dev/null +++ b/app/src/main/java/com/artemchep/pocketmode/sensors/FlowOfScreen.kt @@ -0,0 +1,66 @@ +package com.artemchep.pocketmode.sensors + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import com.artemchep.pocketmode.ext.isScreenOn +import com.artemchep.pocketmode.models.Screen +import com.artemchep.pocketmode.util.observerFlow +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* + +private const val SCREEN_ON_CHECK_INTERVAL = 600L + +fun flowOfScreen( + context: Context, +): Flow = observerFlow { callback -> + val broadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val screen = isScreenOn(context) + callback(screen) + } + } + val intentFilter = IntentFilter() + .apply { + addAction(Intent.ACTION_SCREEN_OFF) + addAction(Intent.ACTION_SCREEN_ON) + } + context.registerReceiver(broadcastReceiver, intentFilter) + + val screen = isScreenOn(context) + callback(screen) + return@observerFlow { + context.unregisterReceiver(broadcastReceiver) + } +} + .flatMapLatest { screen -> + when (screen) { + is Screen.On -> flow { + emit(screen) + + // While the screen is on, send the update every + // few seconds. This is needed because of the + // Always On mode which may not send the 'screen is off' + // broadcast. + while (true) { + delay(SCREEN_ON_CHECK_INTERVAL) + val newScreen = isScreenOn(context) + emit(newScreen) + } + } + else -> flowOf(screen) + } + } + .flowOn(Dispatchers.Main) + .distinctUntilChanged() + +/** + * It does not store the screen state, it + * retrieves it every time. + */ +private fun isScreenOn(context: Context): Screen = + when (context.isScreenOn()) { + true -> Screen.On + false -> Screen.Off + } diff --git a/app/src/main/java/com/artemchep/pocketmode/sensors/KeyguardLiveData.kt b/app/src/main/java/com/artemchep/pocketmode/sensors/KeyguardLiveData.kt deleted file mode 100644 index f371362..0000000 --- a/app/src/main/java/com/artemchep/pocketmode/sensors/KeyguardLiveData.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.artemchep.pocketmode.sensors - -import android.content.Context -import androidx.lifecycle.LiveData -import com.artemchep.pocketmode.ext.isKeyguardLocked -import com.artemchep.pocketmode.models.Keyguard -import kotlinx.coroutines.* - -/** - * @author Artem Chepurnoy - */ -class KeyguardLiveData( - private val context: Context -) : LiveData() { - companion object { - private const val PERIOD = 200L - } - - private lateinit var observeKeyguardJob: Job - - override fun onActive() { - super.onActive() - observeKeyguardJob = GlobalScope.launch(Dispatchers.Main) { - while (isActive) { - delay(PERIOD) - updateKeyguardState() - } - } - - // Immediately fire a current state. - updateKeyguardState() - } - - override fun onInactive() { - observeKeyguardJob.cancel() - super.onInactive() - } - - private fun updateKeyguardState() { - val keyguard = when (context.isKeyguardLocked()) { - true -> Keyguard.Locked - false -> Keyguard.Unlocked - } - postValue(keyguard) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/artemchep/pocketmode/sensors/ProximityLiveData.kt b/app/src/main/java/com/artemchep/pocketmode/sensors/ProximityLiveData.kt deleted file mode 100644 index 5e4ae3c..0000000 --- a/app/src/main/java/com/artemchep/pocketmode/sensors/ProximityLiveData.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.artemchep.pocketmode.sensors - -import android.content.Context -import android.hardware.Sensor -import android.hardware.SensorEvent -import android.hardware.SensorEventListener -import android.hardware.SensorManager -import androidx.core.content.getSystemService -import androidx.lifecycle.LiveData - -/** - * @author Artem Chepurnoy - */ -class ProximityLiveData( - private val context: Context -) : LiveData() { - private val sensorManager by lazy { context.getSystemService() } - - private val sensorProximity by lazy { sensorManager?.getDefaultSensor(Sensor.TYPE_PROXIMITY) } - - private val sensorListener = object : SensorEventListener { - override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { - } - - override fun onSensorChanged(event: SensorEvent) { - val distance = event.values[0] - postValue(distance) - } - } - - override fun onActive() { - super.onActive() - - // Register the proximity sensor if it is - // available - if (sensorManager != null && sensorProximity != null) { - val delay = SensorManager.SENSOR_DELAY_NORMAL - sensorManager!!.registerListener(sensorListener, sensorProximity, delay) - } - } - - override fun onInactive() { - sensorManager?.unregisterListener(sensorListener) - super.onInactive() - value = null - } -} \ No newline at end of file diff --git a/app/src/main/java/com/artemchep/pocketmode/sensors/ScreenLiveData.kt b/app/src/main/java/com/artemchep/pocketmode/sensors/ScreenLiveData.kt deleted file mode 100644 index 923b586..0000000 --- a/app/src/main/java/com/artemchep/pocketmode/sensors/ScreenLiveData.kt +++ /dev/null @@ -1,78 +0,0 @@ -package com.artemchep.pocketmode.sensors - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.os.Handler -import androidx.lifecycle.LiveData -import com.artemchep.pocketmode.ext.isScreenOn -import com.artemchep.pocketmode.models.Screen - -/** - * @author Artem Chepurnoy - */ -class ScreenLiveData( - private val context: Context -) : LiveData() { - companion object { - private const val SCREEN_CHECK_INTERVAL = 100L - } - - private val handler = Handler() - - private val updateScreenStateRunnable = Runnable { - updateScreenState() - } - - private val broadcastReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - updateScreenState() - } - } - - override fun onActive() { - super.onActive() - - // Register an observer. - val intentFilter = IntentFilter() - .apply { - addAction(Intent.ACTION_SCREEN_OFF) - addAction(Intent.ACTION_SCREEN_ON) - } - context.registerReceiver(broadcastReceiver, intentFilter) - - // Immediately fire a current state. - updateScreenState() - } - - override fun onInactive() { - context.unregisterReceiver(broadcastReceiver) - handler.removeCallbacksAndMessages(null) - super.onInactive() - value = null - } - - private fun updateScreenState() { - val screen = isScreenOn() - postValue(screen) - - // While the screen is on, send the update every - // few seconds. This is needed because of the - // Always On mode which may not send the 'screen is off' - // broadcast. - if (screen is Screen.On && hasActiveObservers()) { - handler.postDelayed(updateScreenStateRunnable, SCREEN_CHECK_INTERVAL) - } - } - - /** - * It does not store the screen state, it - * retrieves it every time. - */ - private fun isScreenOn(): Screen = - when (context.isScreenOn()) { - true -> Screen.On - false -> Screen.Off - } -} \ No newline at end of file diff --git a/app/src/main/java/com/artemchep/pocketmode/util/ObserverFlow.kt b/app/src/main/java/com/artemchep/pocketmode/util/ObserverFlow.kt new file mode 100644 index 0000000..67c8d26 --- /dev/null +++ b/app/src/main/java/com/artemchep/pocketmode/util/ObserverFlow.kt @@ -0,0 +1,38 @@ +package com.artemchep.pocketmode.util + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext +import kotlin.coroutines.resume + +inline fun observerFlow( + crossinline register: ((T) -> Unit) -> () -> Unit, +): Flow = channelFlow { + var unregister: (() -> Unit)? = null + try { + unregister = withContext(Dispatchers.Main) { + val setter = { value: T -> + try { + offer(value) + } catch (e: Exception) { + // Do nothing. + } + } + register(setter) + } + suspendCancellableCoroutine { cont -> + invokeOnClose { + cont.resume(Unit) + } + } + } finally { + withContext(Dispatchers.Main + NonCancellable) { + // Unregister the broadcast receiver that we have + // previously registered. + unregister?.invoke() + } + } +} diff --git a/app/src/main/java/com/artemchep/pocketmode/viewmodels/MainViewModel.kt b/app/src/main/java/com/artemchep/pocketmode/viewmodels/MainViewModel.kt index edd6d93..71d3c2d 100644 --- a/app/src/main/java/com/artemchep/pocketmode/viewmodels/MainViewModel.kt +++ b/app/src/main/java/com/artemchep/pocketmode/viewmodels/MainViewModel.kt @@ -30,7 +30,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { val lockScreenDelayLiveData = ConfigLockScreenDelayLiveData() - val proximityLiveData: LiveData = ProximityLiveData(context) + val proximityLiveData: LiveData = ProximityLiveData(context) val appInfoLiveData: LiveData = MutableLiveData() .apply { @@ -41,7 +41,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { value = "$name v$versionName+$versionCode ($flavor)" } - val proximityBinaryLiveData: LiveData = + val proximityBinaryLiveData: LiveData = ProximityBinaryLiveData(context, proximityLiveData) .distinctUntilChanged() diff --git a/app/src/main/java/com/artemchep/pocketmode/viewmodels/PocketViewModel.kt b/app/src/main/java/com/artemchep/pocketmode/viewmodels/PocketViewModel.kt index 110e506..9629d6d 100644 --- a/app/src/main/java/com/artemchep/pocketmode/viewmodels/PocketViewModel.kt +++ b/app/src/main/java/com/artemchep/pocketmode/viewmodels/PocketViewModel.kt @@ -1,11 +1,9 @@ package com.artemchep.pocketmode.viewmodels import android.content.Context -import android.os.PowerManager import androidx.lifecycle.LiveData -import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.asFlow -import androidx.lifecycle.distinctUntilChanged +import androidx.lifecycle.map import com.artemchep.pocketmode.models.Keyguard import com.artemchep.pocketmode.models.PhoneCall import com.artemchep.pocketmode.models.Proximity @@ -13,7 +11,6 @@ import com.artemchep.pocketmode.models.Screen import com.artemchep.pocketmode.models.`fun`.Either import com.artemchep.pocketmode.models.events.Idle import com.artemchep.pocketmode.models.events.LockScreenEvent -import com.artemchep.pocketmode.models.issues.NoReadPhoneStatePermissionGranted import com.artemchep.pocketmode.sensors.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flatMapLatest @@ -24,39 +21,20 @@ import kotlinx.coroutines.flow.map * @author Artem Chepurnoy */ class PocketViewModel(context: Context) { - private val proximityLiveData: LiveData = ProximityLiveData(context) + private val proximityBinaryFlow: Flow = flowOfBinaryProximity(context) - private val proximityBinaryLiveData: LiveData = - ProximityBinaryLiveData(context, proximityLiveData) - .distinctUntilChanged() + private val screenFlow: Flow = flowOfScreen(context) - /** - * Holds the wake lock while being - * observed. - */ - private val wakeLockLiveData: LiveData = WakeLockLiveData(context) { - PowerManager.PARTIAL_WAKE_LOCK - } - - private val screenLiveData: LiveData = ScreenLiveData(context) + private val keyguardFlow: Flow = flowOfKeyguard(context) - private val phoneCallLiveData: LiveData> = - PhoneCallLiveData(context) - - private val phoneCallSoloLiveData = MediatorLiveData() - .apply { - addSource(phoneCallLiveData) { state -> - val value = when (state) { - is Either.Left -> PhoneCall.Idle - is Either.Right -> state.b - } - postValue(value) + private val phoneCallLiveData: LiveData = PhoneCallLiveData(context) + .map { state -> + when (state) { + is Either.Left -> PhoneCall.Idle + is Either.Right -> state.b } } - private val keyguardLiveData: LiveData = KeyguardLiveData(context) - .distinctUntilChanged() - private val overlayBeforeLockingSwitchIsCheckedLiveData = ConfigOverlayBeforeIsCheckedLiveData() /** @@ -64,11 +42,11 @@ class PocketViewModel(context: Context) { * the pocket. */ val lockScreenEventFlow: Flow = - LockScreenEventFlow( - proximityLiveData = proximityBinaryLiveData, - screenLiveData = screenLiveData, - phoneCallLiveData = phoneCallSoloLiveData, - keyguardLiveData = keyguardLiveData + flowOfLockScreen( + proximityFlow = proximityBinaryFlow, + screenFlow = screenFlow, + keyguardFlow = keyguardFlow, + phoneCallLiveData = phoneCallLiveData, ) val overlayFlow: Flow = overlayBeforeLockingSwitchIsCheckedLiveData