diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt index 22620cd6a1..f12378beeb 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ChooseConstraintViewModel.kt @@ -89,6 +89,9 @@ class ChooseConstraintViewModel @Inject constructor( ConstraintId.CHARGING, ConstraintId.DISCHARGING, + ConstraintId.HINGE_CLOSED, + ConstraintId.HINGE_OPEN, + ConstraintId.TIME, ) } @@ -214,6 +217,12 @@ class ChooseConstraintViewModel @Inject constructor( ConstraintId.DISCHARGING -> returnResult.emit(ConstraintData.Discharging) + ConstraintId.HINGE_CLOSED -> + returnResult.emit(ConstraintData.HingeClosed) + + ConstraintId.HINGE_OPEN -> + returnResult.emit(ConstraintData.HingeOpen) + ConstraintId.LOCK_SCREEN_SHOWING -> returnResult.emit(ConstraintData.LockScreenShowing) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt index 0e3744def1..2b34237dc5 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/Constraint.kt @@ -200,6 +200,16 @@ sealed class ConstraintData { override val id: ConstraintId = ConstraintId.DISCHARGING } + @Serializable + data object HingeClosed : ConstraintData() { + override val id: ConstraintId = ConstraintId.HINGE_CLOSED + } + + @Serializable + data object HingeOpen : ConstraintData() { + override val id: ConstraintId = ConstraintId.HINGE_OPEN + } + @Serializable data class Time( val startHour: Int, @@ -364,6 +374,9 @@ object ConstraintEntityMapper { ConstraintEntity.CHARGING -> ConstraintData.Charging ConstraintEntity.DISCHARGING -> ConstraintData.Discharging + ConstraintEntity.HINGE_CLOSED -> ConstraintData.HingeClosed + ConstraintEntity.HINGE_OPEN -> ConstraintData.HingeOpen + ConstraintEntity.TIME -> { val startTime = entity.extras.getData(ConstraintEntity.EXTRA_START_TIME).valueOrNull()!! @@ -628,6 +641,16 @@ object ConstraintEntityMapper { ConstraintEntity.DISCHARGING, ) + is ConstraintData.HingeClosed -> ConstraintEntity( + uid = constraint.uid, + ConstraintEntity.HINGE_CLOSED, + ) + + is ConstraintData.HingeOpen -> ConstraintEntity( + uid = constraint.uid, + ConstraintEntity.HINGE_OPEN, + ) + is ConstraintData.Time -> ConstraintEntity( uid = constraint.uid, type = ConstraintEntity.TIME, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintDependency.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintDependency.kt index 916eec0ae6..2036fe2d97 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintDependency.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintDependency.kt @@ -15,4 +15,5 @@ enum class ConstraintDependency { LOCK_SCREEN_SHOWING, PHONE_STATE, CHARGING_STATE, + HINGE_STATE, } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintId.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintId.kt index 6ff99f2b54..57cf603867 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintId.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintId.kt @@ -49,5 +49,8 @@ enum class ConstraintId { CHARGING, DISCHARGING, + HINGE_CLOSED, + HINGE_OPEN, + TIME, } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt index f50b49ccdd..7b4bbe1439 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintSnapshot.kt @@ -8,6 +8,8 @@ import io.github.sds100.keymapper.system.bluetooth.BluetoothDeviceInfo import io.github.sds100.keymapper.system.camera.CameraAdapter import io.github.sds100.keymapper.system.devices.DevicesAdapter import io.github.sds100.keymapper.system.display.DisplayAdapter +import io.github.sds100.keymapper.system.hinge.FoldableAdapter +import io.github.sds100.keymapper.system.hinge.HingeState import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter import io.github.sds100.keymapper.system.lock.LockScreenAdapter import io.github.sds100.keymapper.system.media.MediaAdapter @@ -31,6 +33,7 @@ class LazyConstraintSnapshot( lockScreenAdapter: LockScreenAdapter, phoneAdapter: PhoneAdapter, powerAdapter: PowerAdapter, + private val foldableAdapter: FoldableAdapter, ) : ConstraintSnapshot { private val appInForeground: String? by lazy { accessibilityService.rootNode?.packageName } private val connectedBluetoothDevices: Set by lazy { devicesAdapter.connectedBluetoothDevices.value } @@ -141,6 +144,20 @@ class LazyConstraintSnapshot( is ConstraintData.Charging -> isCharging is ConstraintData.Discharging -> !isCharging + is ConstraintData.HingeClosed -> { + when (val state = foldableAdapter.hingeState.value) { + is HingeState.Available -> state.angle < 30f + is HingeState.Unavailable -> false + } + } + + is ConstraintData.HingeOpen -> { + when (val state = foldableAdapter.hingeState.value) { + is HingeState.Available -> state.angle >= 150f + is HingeState.Unavailable -> false + } + } + // The keyguard manager still reports the lock screen as showing if you are in // an another activity like the camera app while the phone is locked. is ConstraintData.LockScreenShowing -> isLockscreenShowing && appInForeground == "com.android.systemui" diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt index 817519fc20..ea75532c72 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUiHelper.kt @@ -142,6 +142,8 @@ class ConstraintUiHelper( is ConstraintData.PhoneRinging -> getString(R.string.constraint_phone_ringing) is ConstraintData.Charging -> getString(R.string.constraint_charging) is ConstraintData.Discharging -> getString(R.string.constraint_discharging) + is ConstraintData.HingeClosed -> getString(R.string.constraint_hinge_closed_description) + is ConstraintData.HingeOpen -> getString(R.string.constraint_hinge_open_description) is ConstraintData.LockScreenShowing -> getString(R.string.constraint_lock_screen_showing) is ConstraintData.LockScreenNotShowing -> getString(R.string.constraint_lock_screen_not_showing) is ConstraintData.Time -> getString( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt index 8ea3d1f352..39b75792ed 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConstraintUtils.kt @@ -77,6 +77,10 @@ object ConstraintUtils { ConstraintId.CHARGING -> ComposeIconInfo.Vector(Icons.Outlined.BatteryChargingFull) ConstraintId.DISCHARGING -> ComposeIconInfo.Vector(Icons.Outlined.Battery2Bar) + + ConstraintId.HINGE_CLOSED -> ComposeIconInfo.Vector(Icons.Outlined.StayCurrentPortrait) + ConstraintId.HINGE_OPEN -> ComposeIconInfo.Vector(Icons.Outlined.StayCurrentLandscape) + ConstraintId.LOCK_SCREEN_SHOWING -> ComposeIconInfo.Vector(Icons.Outlined.ScreenLockPortrait) ConstraintId.LOCK_SCREEN_NOT_SHOWING -> ComposeIconInfo.Vector(Icons.Outlined.LockOpen) ConstraintId.TIME -> ComposeIconInfo.Vector(Icons.Outlined.Timer) @@ -114,6 +118,8 @@ object ConstraintUtils { ConstraintId.PHONE_RINGING -> R.string.constraint_phone_ringing ConstraintId.CHARGING -> R.string.constraint_charging ConstraintId.DISCHARGING -> R.string.constraint_discharging + ConstraintId.HINGE_CLOSED -> R.string.constraint_hinge_closed + ConstraintId.HINGE_OPEN -> R.string.constraint_hinge_open ConstraintId.LOCK_SCREEN_SHOWING -> R.string.constraint_lock_screen_showing ConstraintId.LOCK_SCREEN_NOT_SHOWING -> R.string.constraint_lock_screen_not_showing ConstraintId.TIME -> R.string.constraint_time diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/CreateConstraintUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/CreateConstraintUseCase.kt index a735e7ddd9..d6236f2ccf 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/CreateConstraintUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/CreateConstraintUseCase.kt @@ -1,6 +1,7 @@ package io.github.sds100.keymapper.base.constraints import android.content.pm.PackageManager +import android.os.Build import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.repositories.PreferenceRepository @@ -30,6 +31,11 @@ class CreateConstraintUseCaseImpl @Inject constructor( return KMError.SystemFeatureNotSupported(PackageManager.FEATURE_CAMERA_FLASH) } } + ConstraintId.HINGE_CLOSED, ConstraintId.HINGE_OPEN -> { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + return KMError.SdkVersionTooLow(Build.VERSION_CODES.R) + } + } else -> Unit } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/DetectConstraintsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/DetectConstraintsUseCase.kt index b8fbc93d6e..f1fdd46f73 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/DetectConstraintsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/DetectConstraintsUseCase.kt @@ -8,6 +8,7 @@ import io.github.sds100.keymapper.system.camera.CameraAdapter import io.github.sds100.keymapper.system.camera.CameraLens import io.github.sds100.keymapper.system.devices.DevicesAdapter import io.github.sds100.keymapper.system.display.DisplayAdapter +import io.github.sds100.keymapper.system.hinge.FoldableAdapter import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter import io.github.sds100.keymapper.system.lock.LockScreenAdapter import io.github.sds100.keymapper.system.media.MediaAdapter @@ -30,6 +31,7 @@ class DetectConstraintsUseCaseImpl @AssistedInject constructor( private val lockScreenAdapter: LockScreenAdapter, private val phoneAdapter: PhoneAdapter, private val powerAdapter: PowerAdapter, + private val foldableAdapter: FoldableAdapter, ) : DetectConstraintsUseCase { @AssistedFactory @@ -50,6 +52,7 @@ class DetectConstraintsUseCaseImpl @AssistedInject constructor( lockScreenAdapter, phoneAdapter, powerAdapter, + foldableAdapter, ) override fun onDependencyChanged(dependency: ConstraintDependency): Flow { @@ -83,6 +86,7 @@ class DetectConstraintsUseCaseImpl @AssistedInject constructor( ConstraintDependency.PHONE_STATE -> phoneAdapter.callStateFlow.map { dependency } ConstraintDependency.CHARGING_STATE -> powerAdapter.isCharging.map { dependency } + ConstraintDependency.HINGE_STATE -> foldableAdapter.hingeState.map { dependency } } } } diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 0b3f3507a5..5d37edfd36 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -286,6 +286,10 @@ Charging Discharging + Hinge closed + Hinge open + Hinge is closed + Hinge is open Portrait (0°) Landscape (90°) diff --git a/data/src/main/java/io/github/sds100/keymapper/data/entities/ConstraintEntity.kt b/data/src/main/java/io/github/sds100/keymapper/data/entities/ConstraintEntity.kt index 6690875809..45367f5b7f 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/entities/ConstraintEntity.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/entities/ConstraintEntity.kt @@ -81,6 +81,9 @@ data class ConstraintEntity( const val CHARGING = "charging" const val DISCHARGING = "discharging" + const val HINGE_CLOSED = "hinge_closed" + const val HINGE_OPEN = "hinge_open" + const val TIME = "time" const val EXTRA_PACKAGE_NAME = "extra_package_name" diff --git a/system/src/main/java/io/github/sds100/keymapper/system/SystemHiltModule.kt b/system/src/main/java/io/github/sds100/keymapper/system/SystemHiltModule.kt index cce2c461be..7a2a38f0cd 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/SystemHiltModule.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/SystemHiltModule.kt @@ -22,6 +22,8 @@ import io.github.sds100.keymapper.system.display.AndroidDisplayAdapter import io.github.sds100.keymapper.system.display.DisplayAdapter import io.github.sds100.keymapper.system.files.AndroidFileAdapter import io.github.sds100.keymapper.system.files.FileAdapter +import io.github.sds100.keymapper.system.hinge.AndroidFoldableAdapter +import io.github.sds100.keymapper.system.hinge.FoldableAdapter import io.github.sds100.keymapper.system.inputmethod.AndroidInputMethodAdapter import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter import io.github.sds100.keymapper.system.intents.IntentAdapter @@ -106,6 +108,10 @@ abstract class SystemHiltModule { @Binds abstract fun provideFileAdapter(impl: AndroidFileAdapter): FileAdapter + @Singleton + @Binds + abstract fun provideFoldableAdapter(impl: AndroidFoldableAdapter): FoldableAdapter + @Singleton @Binds abstract fun provideInputMethodAdapter(impl: AndroidInputMethodAdapter): InputMethodAdapter diff --git a/system/src/main/java/io/github/sds100/keymapper/system/hinge/AndroidFoldableAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/hinge/AndroidFoldableAdapter.kt new file mode 100644 index 0000000000..1b9654bd4c --- /dev/null +++ b/system/src/main/java/io/github/sds100/keymapper/system/hinge/AndroidFoldableAdapter.kt @@ -0,0 +1,68 @@ +package io.github.sds100.keymapper.system.hinge + +import android.content.Context +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.core.content.getSystemService +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@RequiresApi(Build.VERSION_CODES.R) +@Singleton +class AndroidFoldableAdapter @Inject constructor( + @ApplicationContext private val context: Context, +) : FoldableAdapter { + + private val _hingeState = MutableStateFlow(HingeState.Unavailable) + override val hingeState: StateFlow = _hingeState.asStateFlow() + + private val sensorManager: SensorManager? = context.getSystemService() + private val hingeSensor: Sensor? = sensorManager?.getDefaultSensor(Sensor.TYPE_HINGE_ANGLE) + + private val sensorEventListener = object : SensorEventListener { + override fun onSensorChanged(event: SensorEvent?) { + event?.let { + if (it.sensor.type == Sensor.TYPE_HINGE_ANGLE && it.values.isNotEmpty()) { + val angle = it.values[0] + _hingeState.value = HingeState.Available(angle) + } + } + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { + // Not needed for hinge angle sensor + } + } + + init { + startMonitoring() + } + + private fun startMonitoring() { + if (hingeSensor != null) { + try { + sensorManager?.registerListener( + sensorEventListener, + hingeSensor, + SensorManager.SENSOR_DELAY_NORMAL, + ) + Timber.d("Hinge angle sensor monitoring started") + } catch (e: Exception) { + Timber.e(e, "Failed to start hinge angle sensor monitoring") + _hingeState.value = HingeState.Unavailable + } + } else { + Timber.d("Hinge angle sensor not available on this device") + _hingeState.value = HingeState.Unavailable + } + } +} diff --git a/system/src/main/java/io/github/sds100/keymapper/system/hinge/FoldableAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/hinge/FoldableAdapter.kt new file mode 100644 index 0000000000..fbe28ff42d --- /dev/null +++ b/system/src/main/java/io/github/sds100/keymapper/system/hinge/FoldableAdapter.kt @@ -0,0 +1,31 @@ +package io.github.sds100.keymapper.system.hinge + +import androidx.annotation.RequiresApi +import android.os.Build +import kotlinx.coroutines.flow.StateFlow + +/** + * Represents the state of a foldable device hinge. + */ +sealed class HingeState { + /** + * Hinge sensor is not available on this device. + */ + data object Unavailable : HingeState() + + /** + * Hinge sensor is available and reporting angle. + * @param angle The angle in degrees of the hinge. + * 0 degrees means the device is closed/flat. + * 180 degrees means the device is fully open. + */ + data class Available(val angle: Float) : HingeState() +} + +@RequiresApi(Build.VERSION_CODES.R) +interface FoldableAdapter { + /** + * StateFlow that emits the current hinge state. + */ + val hingeState: StateFlow +}