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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

## Added

- #2184 Add a display resolution constraint. Pick from the display's supported resolutions or enter a
custom width and height.
- #2163 Add ringer mode constraints (Ring, Vibrate, Silent).
- #2140 Add monochrome app icon layer for themed icon support on Android 13+.
- #2174 Add "Do not remap by default" preference to the default options settings page.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ fun ChooseConstraintScreen(modifier: Modifier = Modifier, viewModel: ChooseConst
val query by viewModel.searchQuery.collectAsStateWithLifecycle()

TimeConstraintBottomSheet(viewModel)
DisplayResolutionConstraintBottomSheet(viewModel)

ChooseConstraintScreen(
modifier = modifier,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import io.github.sds100.keymapper.base.utils.ui.compose.SimpleListItemModel
import io.github.sds100.keymapper.base.utils.ui.showDialog
import io.github.sds100.keymapper.common.utils.Orientation
import io.github.sds100.keymapper.common.utils.PhysicalOrientation
import io.github.sds100.keymapper.common.utils.SizeKM
import io.github.sds100.keymapper.common.utils.State
import io.github.sds100.keymapper.system.camera.CameraLens
import io.github.sds100.keymapper.system.volume.RingerMode
Expand Down Expand Up @@ -95,6 +96,9 @@ class ChooseConstraintViewModel @Inject constructor(

var timeConstraintState: ConstraintData.Time? by mutableStateOf(null)

var displayResolutionState: DisplayResolutionSheetState? by mutableStateOf(null)
private set

init {
viewModelScope.launch {
returnResult.collect { constraintData ->
Expand All @@ -112,6 +116,49 @@ class ChooseConstraintViewModel @Inject constructor(
}
}

fun onSelectDisplayResolution(resolution: SizeKM) {
displayResolutionState = displayResolutionState?.copy(
isCustom = false,
selectedResolution = resolution,
)
}

fun onSelectCustomDisplayResolution() {
displayResolutionState = displayResolutionState?.copy(isCustom = true)
}

fun onDisplayResolutionWidthChange(width: String) {
displayResolutionState = displayResolutionState?.copy(
widthText = width.filter(Char::isDigit),
)
}

fun onDisplayResolutionHeightChange(height: String) {
displayResolutionState = displayResolutionState?.copy(
heightText = height.filter(Char::isDigit),
)
}

fun onDismissDisplayResolution() {
displayResolutionState = null
}

fun onDoneConfigDisplayResolutionClick() {
val state = displayResolutionState ?: return

val resolution = state.resolvedResolution ?: return

viewModelScope.launch {
returnResult.emit(
ConstraintData.DisplayResolution(
width = resolution.width,
height = resolution.height,
),
)
displayResolutionState = null
}
}

fun onNavigateBack() {
viewModelScope.launch {
popBackStack()
Expand All @@ -126,6 +173,7 @@ class ChooseConstraintViewModel @Inject constructor(
onSelectDisplayOrientationConstraint()
return@launch
}

PHYSICAL_ORIENTATION_LIST_ITEM_ID -> {
onSelectPhysicalOrientationConstraint()
return@launch
Expand All @@ -140,6 +188,7 @@ class ChooseConstraintViewModel @Inject constructor(
-> onSelectAppConstraint(constraintType)

ConstraintId.MEDIA_PLAYING -> returnResult.emit(ConstraintData.MediaPlaying)

ConstraintId.MEDIA_NOT_PLAYING -> returnResult.emit(ConstraintData.NoMediaPlaying)

ConstraintId.BT_DEVICE_CONNECTED,
Expand All @@ -152,6 +201,10 @@ class ChooseConstraintViewModel @Inject constructor(

ConstraintId.SCREEN_OFF -> returnResult.emit(ConstraintData.ScreenOff)

ConstraintId.DISPLAY_RESOLUTION -> {
displayResolutionState = buildDisplayResolutionState()
}

ConstraintId.DISPLAY_ORIENTATION_PORTRAIT ->
returnResult.emit(ConstraintData.OrientationPortrait)

Expand Down Expand Up @@ -217,6 +270,7 @@ class ChooseConstraintViewModel @Inject constructor(
}

ConstraintId.WIFI_ON -> returnResult.emit(ConstraintData.WifiOn)

ConstraintId.WIFI_OFF -> returnResult.emit(ConstraintData.WifiOff)

ConstraintId.WIFI_CONNECTED,
Expand Down Expand Up @@ -295,6 +349,25 @@ class ChooseConstraintViewModel @Inject constructor(
}
}

private fun buildDisplayResolutionState(): DisplayResolutionSheetState {
val supportedResolutions = useCase.getSupportedResolutions()
val currentResolution = useCase.getCurrentResolution()

val matchingResolution = supportedResolutions.firstOrNull {
it.matchesIgnoringOrientation(currentResolution)
}

return DisplayResolutionSheetState(
supportedResolutions = supportedResolutions.sortedBy { it.width },
// Show the text fields immediately when there is nothing meaningful to pick
// from or when the current resolution isn't one of the supported modes.
isCustom = supportedResolutions.size <= 1 || matchingResolution == null,
selectedResolution = matchingResolution ?: supportedResolutions.firstOrNull(),
widthText = currentResolution.width.toString(),
heightText = currentResolution.height.toString(),
)
}

private suspend fun chooseFlashlightLens(): CameraLens? {
val items = useCase.getFlashlightLenses().map { lens ->
val label = when (lens) {
Expand Down Expand Up @@ -336,15 +409,21 @@ class ChooseConstraintViewModel @Inject constructor(

val constraintData = when (selectedOrientation) {
ConstraintId.DISPLAY_ORIENTATION_PORTRAIT -> ConstraintData.OrientationPortrait

ConstraintId.DISPLAY_ORIENTATION_LANDSCAPE -> ConstraintData.OrientationLandscape

ConstraintId.DISPLAY_ORIENTATION_0 ->
ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_0)

ConstraintId.DISPLAY_ORIENTATION_90 ->
ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_90)

ConstraintId.DISPLAY_ORIENTATION_180 ->
ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_180)

ConstraintId.DISPLAY_ORIENTATION_270 ->
ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_270)

else -> return
}

Expand Down Expand Up @@ -598,3 +677,51 @@ class ChooseConstraintViewModel @Inject constructor(
returnResult.emit(constraintData)
}
}

/**
* State for the display resolution bottom sheet.
*
* @param supportedResolutions the resolutions the display reports as supported.
* @param isCustom whether the user is entering a custom resolution instead of picking a chip.
* @param selectedResolution the currently selected supported resolution, if any.
* @param widthText the custom width input.
* @param heightText the custom height input.
*/
data class DisplayResolutionSheetState(
val supportedResolutions: List<SizeKM>,
val isCustom: Boolean,
val selectedResolution: SizeKM?,
val widthText: String,
val heightText: String,
) {
private val customWidth: Int? get() = widthText.toIntOrNull()
private val customHeight: Int? get() = heightText.toIntOrNull()

/**
* The resolution that will be saved, or null when the current input is not valid.
*/
val resolvedResolution: SizeKM?
get() = if (isCustom) {
val width = customWidth
val height = customHeight

if (width != null && width > 0 && height != null && height > 0) {
SizeKM(width, height)
} else {
null
}
} else {
selectedResolution
}

val isValid: Boolean get() = resolvedResolution != null
}

/**
* Compares two resolutions ignoring orientation so that e.g. 1080x1920 and 1920x1080
* are treated as the same resolution.
*/
private fun SizeKM.matchesIgnoringOrientation(other: SizeKM): Boolean {
return (width == other.width && height == other.height) ||
(width == other.height && height == other.width)
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ sealed class ConstraintData {
}
}

@Serializable
data class DisplayResolution(val width: Int, val height: Int) : ConstraintData() {
override val id: ConstraintId = ConstraintId.DISPLAY_RESOLUTION
}

@Serializable
data class FlashlightOn(val lens: CameraLens) : ConstraintData() {
override val id: ConstraintId = ConstraintId.FLASHLIGHT_ON
Expand Down Expand Up @@ -302,6 +307,12 @@ object ConstraintEntityMapper {
return extraValue
}

fun getResolutionWidth(): Int =
entity.extras.getData(ConstraintEntity.EXTRA_RESOLUTION_WIDTH).valueOrNull()!!.toInt()

fun getResolutionHeight(): Int =
entity.extras.getData(ConstraintEntity.EXTRA_RESOLUTION_HEIGHT).valueOrNull()!!.toInt()

val constraintData = when (entity.type) {
ConstraintEntity.APP_FOREGROUND -> ConstraintData.AppInForeground(
getPackageName(),
Expand Down Expand Up @@ -362,6 +373,11 @@ object ConstraintEntityMapper {
ConstraintEntity.PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED ->
ConstraintData.PhysicalOrientation(PhysicalOrientation.LANDSCAPE_INVERTED)

ConstraintEntity.DISPLAY_RESOLUTION -> ConstraintData.DisplayResolution(
width = getResolutionWidth(),
height = getResolutionHeight(),
)

ConstraintEntity.SCREEN_OFF -> ConstraintData.ScreenOff
ConstraintEntity.SCREEN_ON -> ConstraintData.ScreenOn

Expand Down Expand Up @@ -575,6 +591,19 @@ object ConstraintEntityMapper {
)
}

is ConstraintData.DisplayResolution -> ConstraintEntity(
uid = constraint.uid,
ConstraintEntity.DISPLAY_RESOLUTION,
EntityExtra(
ConstraintEntity.EXTRA_RESOLUTION_WIDTH,
constraint.data.width.toString(),
),
EntityExtra(
ConstraintEntity.EXTRA_RESOLUTION_HEIGHT,
constraint.data.height.toString(),
),
)

is ConstraintData.ScreenOff -> ConstraintEntity(
uid = constraint.uid,
ConstraintEntity.SCREEN_OFF,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ enum class ConstraintDependency {
CHARGING_STATE,
HINGE_STATE,
NOTIFICATION_PANEL_STATE,
DISPLAY_RESOLUTIONS,
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ enum class ConstraintId {
PHYSICAL_ORIENTATION_PORTRAIT_INVERTED,
PHYSICAL_ORIENTATION_LANDSCAPE_INVERTED,

DISPLAY_RESOLUTION,

FLASHLIGHT_ON,
FLASHLIGHT_OFF,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.os.Build
import io.github.sds100.keymapper.base.system.accessibility.IAccessibilityService
import io.github.sds100.keymapper.common.utils.Orientation
import io.github.sds100.keymapper.common.utils.PhysicalOrientation
import io.github.sds100.keymapper.common.utils.SizeKM
import io.github.sds100.keymapper.common.utils.firstBlocking
import io.github.sds100.keymapper.system.bluetooth.BluetoothDeviceInfo
import io.github.sds100.keymapper.system.camera.CameraAdapter
Expand Down Expand Up @@ -51,6 +52,7 @@ class LazyConstraintSnapshot(
displayAdapter.cachedPhysicalOrientation
}
private val isScreenOn: Boolean by lazy { displayAdapter.isScreenOn.firstBlocking() }
private val displaySize: SizeKM by lazy { displayAdapter.size }
private val appsPlayingMedia: List<String> by lazy {
mediaAdapter.getActiveMediaSessionPackages()
}
Expand Down Expand Up @@ -134,6 +136,19 @@ class LazyConstraintSnapshot(

is ConstraintData.ScreenOff -> !isScreenOn
is ConstraintData.ScreenOn -> isScreenOn

// Compare the resolution regardless of orientation so that the constraint
// holds whether the device is in portrait or landscape.
is ConstraintData.DisplayResolution ->
(
displaySize.width == constraint.data.width &&
displaySize.height == constraint.data.height
) ||
(
displaySize.width == constraint.data.height &&
displaySize.height == constraint.data.width
)

is ConstraintData.FlashlightOff -> !cameraAdapter.isFlashlightOn(constraint.data.lens)
is ConstraintData.FlashlightOn -> cameraAdapter.isFlashlightOn(constraint.data.lens)
is ConstraintData.WifiConnected -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ class ConstraintUiHelper(
is ConstraintData.ScreenOn ->
getString(R.string.constraint_screen_on_description)

is ConstraintData.DisplayResolution -> getString(
R.string.constraint_display_resolution_description,
arrayOf(constraint.data.width, constraint.data.height),
)

is ConstraintData.FlashlightOff -> if (constraint.data.lens == CameraLens.FRONT) {
getString(R.string.constraint_front_flashlight_off_description)
} else {
Expand Down
Loading
Loading