Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ class ChooseConstraintViewModel @Inject constructor(
ConstraintId.CHARGING,
ConstraintId.DISCHARGING,

ConstraintId.HINGE_CLOSED,
ConstraintId.HINGE_OPEN,

ConstraintId.TIME,
)
}
Expand Down Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()!!
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ enum class ConstraintDependency {
LOCK_SCREEN_SHOWING,
PHONE_STATE,
CHARGING_STATE,
HINGE_STATE,
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,8 @@ enum class ConstraintId {
CHARGING,
DISCHARGING,

HINGE_CLOSED,
HINGE_OPEN,

TIME,
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<BluetoothDeviceInfo> by lazy { devicesAdapter.connectedBluetoothDevices.value }
Expand Down Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -50,6 +52,7 @@ class DetectConstraintsUseCaseImpl @AssistedInject constructor(
lockScreenAdapter,
phoneAdapter,
powerAdapter,
foldableAdapter,
)

override fun onDependencyChanged(dependency: ConstraintDependency): Flow<ConstraintDependency> {
Expand Down Expand Up @@ -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 }
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions base/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,10 @@

<string name="constraint_charging">Charging</string>
<string name="constraint_discharging">Discharging</string>
<string name="constraint_hinge_closed">Hinge closed</string>
<string name="constraint_hinge_open">Hinge open</string>
<string name="constraint_hinge_closed_description">Hinge is closed</string>
<string name="constraint_hinge_open_description">Hinge is open</string>

<string name="orientation_0">Portrait (0°)</string>
<string name="orientation_90">Landscape (90°)</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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>(HingeState.Unavailable)
override val hingeState: StateFlow<HingeState> = _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
}
}
}
Original file line number Diff line number Diff line change
@@ -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<HingeState>
}
Loading