Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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,7 @@ 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.inputmethod.InputMethodAdapter
import io.github.sds100.keymapper.system.lock.LockScreenAdapter
import io.github.sds100.keymapper.system.media.MediaAdapter
Expand All @@ -31,6 +32,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 +143,16 @@ class LazyConstraintSnapshot(
is ConstraintData.Charging -> isCharging
is ConstraintData.Discharging -> !isCharging

is ConstraintData.HingeClosed -> {
val hingeState = foldableAdapter.getCachedHingeState()
hingeState.isAvailable && hingeState.angle != null && hingeState.angle < 30f
}

is ConstraintData.HingeOpen -> {
val hingeState = foldableAdapter.getCachedHingeState()
hingeState.isAvailable && hingeState.angle != null && hingeState.angle >= 150f
}

// 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
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,70 @@
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 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

@Singleton
class AndroidFoldableAdapter @Inject constructor(
@ApplicationContext private val context: Context,
) : FoldableAdapter {

private val _hingeState = MutableStateFlow(HingeState(isAvailable = false, angle = null))
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(
isAvailable = true,
angle = 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(isAvailable = false, angle = null)
}
} else {
Timber.d("Hinge angle sensor not available on this device")
_hingeState.value = HingeState(isAvailable = false, angle = null)
}
}

override fun getCachedHingeState(): HingeState = _hingeState.value
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.github.sds100.keymapper.system.hinge

import kotlinx.coroutines.flow.Flow

/**
* Represents the state of a foldable device hinge.
*/
data class HingeState(
/**
* True if the device has a hinge and it is currently available.
*/
val isAvailable: Boolean,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create separate sealed class for Unavailable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created sealed class for HingeState with Unavailable and Available(angle: Float) states. Updated ConstraintSnapshot to use pattern matching with the sealed class. (faa66c9)

/**
* The angle in degrees of the hinge. Null if hinge is not available.
* 0 degrees means the device is closed/flat.
* 180 degrees means the device is fully open.
*/
val angle: Float?,
)

interface FoldableAdapter {
/**
* Flow that emits the current hinge state.
*/
val hingeState: Flow<HingeState>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Just make one StateFlow instead of a separate cached method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed getCachedHingeState() method. Now using hingeState.value directly from the StateFlow. (faa66c9)


/**
* Get the current cached hinge state.
*/
fun getCachedHingeState(): HingeState
}