diff --git a/CHANGELOG.md b/CHANGELOG.md index 1494219777..ad610e5975 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,28 @@ +## [4.0.0 Beta 1](https://github.com/sds100/KeyMapper/releases/tag/v4.0.0-beta.01) + +#### TO BE RELEASED + +## Added + +- #761 Detect keys with scancodes. Key Mapper will do this automatically if the key code is unknown + or you record different physical keys from the same device with the same key code. +- Redesign the Settings screen + +## Removed + +- The key event relay service is now also used on all Android versions below Android 14. The + broadcast receiver method is no longer used. +- Minimum supported Android version is now 8.0. Less than 1% of users are on older versions than + this and dropping support simplifies the codebase and maintenance. +- Dropped support for showing a keyboard picker notification and automatically showing it when a + device connects. This is only supported on Android 8.1 and is extra work to maintain it. +- Dropped support for rerouting key events on Android 11. This was a workaround for a specific bug + in Android 11 which fewer than 10% of users are using and less are probably using that feature. + +## Fixed + +- Restoring subgroups works and does not freeze Key Mapper + ## [3.2.1](https://github.com/sds100/KeyMapper/releases/tag/v3.2.1) #### 03 Sept 2025 diff --git a/CREDITS.md b/CREDITS.md index 8d7b22f638..874d1a432c 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -13,6 +13,8 @@ Many thanks to... - @[MFlisar](https://github.com/MFlisar) for their [drag and select](https://github.com/MFlisar/DragSelectRecyclerView) library. - @[RikkaApps](https://github.com/RikkaApps) for Shizuku! It is amazing. - @[canopas](https://github.com/canopas) for their Jetpack Compose Tap Target library https://github.com/canopas/compose-intro-showcase. +- @[topjohnwu](https://github.com/topjohnwu) for Magisk + and [libsu](https://github.com/topjohnwu/libsu). [salomonbrys]: https://github.com/salomonbrys [Kotson]: https://github.com/salomonbrys/Kotson diff --git a/api/src/main/java/io/github/sds100/keymapper/api/KeyEventRelayService.kt b/api/src/main/java/io/github/sds100/keymapper/api/KeyEventRelayService.kt index 58c49939e5..db3c7350b5 100644 --- a/api/src/main/java/io/github/sds100/keymapper/api/KeyEventRelayService.kt +++ b/api/src/main/java/io/github/sds100/keymapper/api/KeyEventRelayService.kt @@ -21,7 +21,10 @@ import java.util.concurrent.ConcurrentHashMap * * This was originally implemented in issue #850 for the action to answer phone calls * because Android doesn't pass volume down key events to the accessibility service - * when the phone is ringing or it is in a phone call. + * when the phone is ringing or it is in a phone call. Later, in Android 14 this relay must be + * used because they also introduced a 1-second delay to context-registered broadcast receivers. + * And who knows what other restrictions will be added in the future :) + * * The accessibility service registers a callback and the input method service * sends the key events. */ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e5df922030..77c8450f1b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -141,6 +141,7 @@ dependencies { implementation(project(":base")) implementation(project(":api")) implementation(project(":data")) + implementation(project(":sysbridge")) implementation(project(":system")) compileOnly(project(":systemstubs")) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index bbf10eaad0..050f28f590 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -98,4 +98,136 @@ -keep class com.google.gson.reflect.TypeToken -keep class * extends com.google.gson.reflect.TypeToken --keep public class * implements java.lang.reflect.Type \ No newline at end of file +-keep public class * implements java.lang.reflect.Type + +-keep class io.github.sds100.keymapper.sysbridge.service.SystemBridge { + public ; + native ; + static ; + (...); +} + +# Keep all AIDL interface classes and their methods +-keep class io.github.sds100.keymapper.sysbridge.ISystemBridge** { *; } +-keep class io.github.sds100.keymapper.sysbridge.IEvdevCallback** { *; } +-keep class io.github.sds100.keymapper.sysbridge.IShizukuStarterService** { *; } + +# Keep binder provider classes +-keep class io.github.sds100.keymapper.sysbridge.provider.** { *; } + +# Keep classes accessed via reflection or from system services +-keep class io.github.sds100.keymapper.sysbridge.utils.** { *; } + +# Keep native method signatures +-keepclasseswithmembernames class * { + native ; +} + +# Keep classes that might be accessed via ContentProvider +-keep class io.github.sds100.keymapper.sysbridge.** extends android.content.ContentProvider { *; } + +# Keep parcelable classes used in AIDL +-keep class io.github.sds100.keymapper.common.models.EvdevDeviceHandle { *; } + +# Keep all rikka.hidden classes and interfaces as they contain AIDL files +-keep class rikka.hidden.** { *; } +-keep interface rikka.hidden.** { *; } + +# Keep Android system API classes and interfaces that rikka.hidden depends on +# android.app package classes +-keep class android.app.ActivityManagerNative { *; } +-keep class android.app.ActivityTaskManager$RootTaskInfo { *; } +-keep class android.app.ContentProviderHolder { *; } +-keep class android.app.IActivityManager** { *; } +-keep class android.app.IApplicationThread** { *; } +-keep class android.app.IProcessObserver** { *; } +-keep class android.app.ITaskStackListener** { *; } +-keep class android.app.IUidObserver** { *; } +-keep class android.app.ProfilerInfo { *; } + +# android.content package classes +-keep class android.content.IContentProvider** { *; } +-keep class android.content.IIntentReceiver** { *; } + +# android.content.pm package classes +-keep class android.content.pm.IPackageManager** { *; } +-keep class android.content.pm.IPackageInstaller** { *; } +-keep class android.content.pm.ILauncherApps** { *; } +-keep class android.content.pm.IOnAppsChangedListener** { *; } +-keep class android.content.pm.IPackageInstallerCallback** { *; } +-keep class android.content.pm.ParceledListSlice { *; } +-keep class android.content.pm.UserInfo { *; } + +# android.hardware package classes +-keep class android.hardware.input.IInputManager** { *; } +-keep class android.hardware.display.IDisplayManager** { *; } +-keep class android.hardware.display.IDisplayManagerCallback** { *; } + +# android.os package classes +-keep class android.os.BatteryProperty { *; } +-keep class android.os.IBatteryPropertiesRegistrar** { *; } +-keep class android.os.IDeviceIdleController { *; } +-keep class android.os.IDeviceIdleController** { *; } +-keep class android.os.IUserManager { *; } +-keep class android.os.IUserManager** { *; } +-keep class android.os.RemoteCallback** { *; } +-keep class android.os.ServiceManager { *; } + +# android.view package classes +-keep class android.view.DisplayInfo { *; } +-keep class android.view.IWindowManager** { *; } + +# android.permission package classes +-keep class android.permission.IPermissionManager** { *; } + +# android.net package classes +-keep class android.net.wifi.IWifiManager** { *; } + +# com.android.internal package classes +-keep class com.android.internal.app.IAppOpsActiveCallback** { *; } +-keep class com.android.internal.app.IAppOpsNotedCallback** { *; } +-keep class com.android.internal.app.IAppOpsService** { *; } +-keep class com.android.internal.policy.IKeyguardLockedStateListener** { *; } + +# Keep all Android AIDL interfaces (they implement IInterface) +-keep class android.** implements android.os.IInterface { *; } + +# Keep Android system service stubs and natives +-keep class android.**Native { *; } +-keep class android.**$Stub** { *; } +-keep class android.**$Proxy** { *; } + +# Keep Android hidden/internal classes that might be accessed via reflection +-dontwarn android.app.ActivityManagerNative +-dontwarn android.app.ActivityTaskManager$** +-dontwarn android.app.ContentProviderHolder +-dontwarn android.app.IActivityManager** +-dontwarn android.app.IApplicationThread** +-dontwarn android.app.IProcessObserver** +-dontwarn android.app.ITaskStackListener** +-dontwarn android.app.IUidObserver** +-dontwarn android.app.ProfilerInfo +-dontwarn android.app.AppOpsManager$** +-dontwarn android.content.IContentProvider** +-dontwarn android.content.IIntentReceiver** +-dontwarn android.content.pm.ILauncherApps** +-dontwarn android.content.pm.IOnAppsChangedListener** +-dontwarn android.content.pm.IPackageInstallerCallback** +-dontwarn android.content.pm.ParceledListSlice +-dontwarn android.content.pm.UserInfo +-dontwarn android.content.pm.PackageManagerHidden +-dontwarn android.hardware.display.IDisplayManager** +-dontwarn android.hardware.display.IDisplayManagerCallback** +-dontwarn android.os.BatteryProperty +-dontwarn android.os.IBatteryPropertiesRegistrar** +-dontwarn android.os.IDeviceIdleController +-dontwarn android.os.IDeviceIdleController** +-dontwarn android.os.IUserManager +-dontwarn android.os.IUserManager** +-dontwarn android.os.RemoteCallback** +-dontwarn android.os.ServiceManager +-dontwarn android.os.UserHandle$** +-dontwarn android.view.DisplayInfo +-dontwarn android.view.IWindowManager** +-dontwarn com.android.internal.app.** +-dontwarn com.android.internal.policy.** \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fd3a99ee47..c4e87ba265 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,10 +5,16 @@ + + + + diff --git a/app/src/main/java/io/github/sds100/keymapper/MainFragment.kt b/app/src/main/java/io/github/sds100/keymapper/MainFragment.kt index ccd1e61eee..1ef6d00efd 100644 --- a/app/src/main/java/io/github/sds100/keymapper/MainFragment.kt +++ b/app/src/main/java/io/github/sds100/keymapper/MainFragment.kt @@ -24,20 +24,24 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import dagger.hilt.android.AndroidEntryPoint import io.github.sds100.keymapper.base.BaseMainNavHost -import io.github.sds100.keymapper.base.actions.ChooseActionScreen -import io.github.sds100.keymapper.base.actions.ChooseActionViewModel +import io.github.sds100.keymapper.base.actions.ActionsScreen +import io.github.sds100.keymapper.base.actions.ConfigActionsViewModel import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import io.github.sds100.keymapper.base.constraints.ConfigConstraintsViewModel +import io.github.sds100.keymapper.base.constraints.ConstraintsScreen import io.github.sds100.keymapper.base.databinding.FragmentComposeBinding import io.github.sds100.keymapper.base.home.HomeKeyMapListScreen +import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapScreen +import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapViewModel +import io.github.sds100.keymapper.base.keymaps.KeyMapOptionsScreen import io.github.sds100.keymapper.base.utils.navigation.NavDestination import io.github.sds100.keymapper.base.utils.navigation.NavigationProviderImpl import io.github.sds100.keymapper.base.utils.navigation.SetupNavigation import io.github.sds100.keymapper.base.utils.navigation.handleRouteArgs import io.github.sds100.keymapper.base.utils.navigation.setupFragmentNavigation -import io.github.sds100.keymapper.base.utils.ui.DialogProviderImpl import io.github.sds100.keymapper.home.HomeViewModel -import io.github.sds100.keymapper.keymaps.ConfigKeyMapScreen -import io.github.sds100.keymapper.keymaps.ConfigKeyMapViewModel +import io.github.sds100.keymapper.trigger.ConfigTriggerViewModel +import io.github.sds100.keymapper.trigger.TriggerScreen import javax.inject.Inject @AndroidEntryPoint @@ -46,9 +50,6 @@ class MainFragment : Fragment() { @Inject lateinit var navigationProvider: NavigationProviderImpl - @Inject - lateinit var dialogProvider: DialogProviderImpl - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -107,45 +108,84 @@ class MainFragment : Fragment() { } composable { backStackEntry -> - val viewModel: ConfigKeyMapViewModel = hiltViewModel() + val keyMapViewModel: ConfigKeyMapViewModel = hiltViewModel() + val triggerViewModel: ConfigTriggerViewModel = hiltViewModel() + val actionsViewModel: ConfigActionsViewModel = hiltViewModel() + val constraintsViewModel: ConfigConstraintsViewModel = hiltViewModel() + val snackbarHostState = remember { SnackbarHostState() } backStackEntry.handleRouteArgs { args -> - viewModel.loadNewKeyMap(groupUid = args.groupUid) + keyMapViewModel.loadNewKeyMap(groupUid = args.groupUid) if (args.showAdvancedTriggers) { - viewModel.configTriggerViewModel.showAdvancedTriggersBottomSheet = true + triggerViewModel.showAdvancedTriggersBottomSheet = true } } ConfigKeyMapScreen( modifier = Modifier.fillMaxSize(), - viewModel = viewModel, + snackbarHostState = snackbarHostState, + keyMapViewModel = keyMapViewModel, + triggerScreen = { + TriggerScreen(Modifier.fillMaxSize(), triggerViewModel) + }, + actionsScreen = { + ActionsScreen(Modifier.fillMaxSize(), actionsViewModel) + }, + constraintsScreen = { + ConstraintsScreen( + Modifier.fillMaxSize(), + constraintsViewModel, + snackbarHostState, + ) + }, + optionsScreen = { + KeyMapOptionsScreen( + Modifier.fillMaxSize(), + triggerViewModel.optionsViewModel, + ) + }, ) } composable { backStackEntry -> - val viewModel: ConfigKeyMapViewModel = hiltViewModel() + val keyMapViewModel: ConfigKeyMapViewModel = hiltViewModel() + val triggerViewModel: ConfigTriggerViewModel = hiltViewModel() + val actionsViewModel: ConfigActionsViewModel = hiltViewModel() + val constraintsViewModel: ConfigConstraintsViewModel = hiltViewModel() + val snackbarHostState = remember { SnackbarHostState() } backStackEntry.handleRouteArgs { args -> - viewModel.loadKeyMap(uid = args.keyMapUid) + keyMapViewModel.loadKeyMap(uid = args.keyMapUid) if (args.showAdvancedTriggers) { - viewModel.configTriggerViewModel.showAdvancedTriggersBottomSheet = true + triggerViewModel.showAdvancedTriggersBottomSheet = true } } ConfigKeyMapScreen( modifier = Modifier.fillMaxSize(), - viewModel = viewModel, - ) - } - - composable { - val viewModel: ChooseActionViewModel = hiltViewModel() - - ChooseActionScreen( - modifier = Modifier.fillMaxSize(), - viewModel = viewModel, + snackbarHostState = snackbarHostState, + keyMapViewModel = keyMapViewModel, + triggerScreen = { + TriggerScreen(Modifier.fillMaxSize(), triggerViewModel) + }, + actionsScreen = { + ActionsScreen(Modifier.fillMaxSize(), actionsViewModel) + }, + constraintsScreen = { + ConstraintsScreen( + Modifier.fillMaxSize(), + constraintsViewModel, + snackbarHostState, + ) + }, + optionsScreen = { + KeyMapOptionsScreen( + Modifier.fillMaxSize(), + triggerViewModel.optionsViewModel, + ) + }, ) } } diff --git a/app/src/main/java/io/github/sds100/keymapper/home/HomeViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/home/HomeViewModel.kt index 0c2f3dc906..4ebfcfd6fc 100644 --- a/app/src/main/java/io/github/sds100/keymapper/home/HomeViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/home/HomeViewModel.kt @@ -3,8 +3,8 @@ package io.github.sds100.keymapper.home import dagger.hilt.android.lifecycle.HiltViewModel import io.github.sds100.keymapper.base.backup.BackupRestoreMappingsUseCase import io.github.sds100.keymapper.base.home.BaseHomeViewModel +import io.github.sds100.keymapper.base.home.ListKeyMapsUseCase import io.github.sds100.keymapper.base.home.ShowHomeScreenAlertsUseCase -import io.github.sds100.keymapper.base.keymaps.ListKeyMapsUseCase import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase import io.github.sds100.keymapper.base.sorting.SortKeyMapsUseCase diff --git a/app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapScreen.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapScreen.kt deleted file mode 100644 index 61e36a3624..0000000000 --- a/app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapScreen.kt +++ /dev/null @@ -1,81 +0,0 @@ -package io.github.sds100.keymapper.keymaps - -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import io.github.sds100.keymapper.base.actions.ActionsScreen -import io.github.sds100.keymapper.base.constraints.ConstraintsScreen -import io.github.sds100.keymapper.base.keymaps.BaseConfigKeyMapScreen -import io.github.sds100.keymapper.base.keymaps.KeyMapOptionsScreen -import io.github.sds100.keymapper.base.utils.ui.UnsavedChangesDialog -import io.github.sds100.keymapper.trigger.TriggerScreen - -@Composable -fun ConfigKeyMapScreen( - modifier: Modifier = Modifier, - viewModel: ConfigKeyMapViewModel, -) { - val isKeyMapEnabled by viewModel.isEnabled.collectAsStateWithLifecycle() - val showActionTapTarget by viewModel.showActionsTapTarget.collectAsStateWithLifecycle() - val showConstraintTapTarget by viewModel.showConstraintsTapTarget.collectAsStateWithLifecycle() - - val snackbarHostState = remember { SnackbarHostState() } - - var showBackDialog by rememberSaveable { mutableStateOf(false) } - - if (showBackDialog) { - UnsavedChangesDialog( - onDismiss = { showBackDialog = false }, - onDiscardClick = { - showBackDialog = false - viewModel.onBackClick() - }, - ) - } - - BaseConfigKeyMapScreen( - modifier = modifier, - isKeyMapEnabled = isKeyMapEnabled, - onKeyMapEnabledChange = viewModel::onEnabledChanged, - triggerScreen = { - TriggerScreen(Modifier.fillMaxSize(), viewModel.configTriggerViewModel) - }, - actionScreen = { - ActionsScreen(Modifier.fillMaxSize(), viewModel.configActionsViewModel) - }, - constraintsScreen = { - ConstraintsScreen( - Modifier.fillMaxSize(), - viewModel.configConstraintsViewModel, - snackbarHostState, - ) - }, - optionsScreen = { - KeyMapOptionsScreen( - Modifier.fillMaxSize(), - viewModel.configTriggerViewModel.optionsViewModel, - ) - }, - onBackClick = { - if (viewModel.isKeyMapEdited) { - showBackDialog = true - } else { - viewModel.onBackClick() - } - }, - onDoneClick = viewModel::onDoneClick, - snackbarHostState = snackbarHostState, - showActionTapTarget = showActionTapTarget, - onActionTapTargetCompleted = viewModel::onActionTapTargetCompleted, - showConstraintTapTarget = showConstraintTapTarget, - onConstraintTapTargetCompleted = viewModel::onConstraintTapTargetCompleted, - onSkipTutorialClick = viewModel::onSkipTutorialClick, - ) -} diff --git a/app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapViewModel.kt deleted file mode 100644 index 046f5cee1c..0000000000 --- a/app/src/main/java/io/github/sds100/keymapper/keymaps/ConfigKeyMapViewModel.kt +++ /dev/null @@ -1,81 +0,0 @@ -package io.github.sds100.keymapper.keymaps - -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import io.github.sds100.keymapper.base.actions.ConfigActionsViewModel -import io.github.sds100.keymapper.base.actions.CreateActionUseCase -import io.github.sds100.keymapper.base.actions.TestActionUseCase -import io.github.sds100.keymapper.base.constraints.ConfigConstraintsViewModel -import io.github.sds100.keymapper.base.keymaps.BaseConfigKeyMapViewModel -import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapUseCase -import io.github.sds100.keymapper.base.keymaps.CreateKeyMapShortcutUseCase -import io.github.sds100.keymapper.base.keymaps.DisplayKeyMapUseCase -import io.github.sds100.keymapper.base.keymaps.FingerprintGesturesSupportedUseCase -import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase -import io.github.sds100.keymapper.base.purchasing.PurchasingManager -import io.github.sds100.keymapper.base.trigger.RecordTriggerUseCase -import io.github.sds100.keymapper.base.trigger.SetupGuiKeyboardUseCase -import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider -import io.github.sds100.keymapper.base.utils.ui.DialogProvider -import io.github.sds100.keymapper.base.utils.ui.ResourceProvider -import io.github.sds100.keymapper.trigger.ConfigTriggerViewModel -import javax.inject.Inject - -@HiltViewModel -class ConfigKeyMapViewModel @Inject constructor( - display: DisplayKeyMapUseCase, - config: ConfigKeyMapUseCase, - onboarding: OnboardingUseCase, - createActionUseCase: CreateActionUseCase, - testActionUseCase: TestActionUseCase, - recordTriggerUseCase: RecordTriggerUseCase, - createKeyMapShortcutUseCase: CreateKeyMapShortcutUseCase, - purchasingManager: PurchasingManager, - setupGuiKeyboardUseCase: SetupGuiKeyboardUseCase, - fingerprintGesturesSupportedUseCase: FingerprintGesturesSupportedUseCase, - resourceProvider: ResourceProvider, - navigationProvider: NavigationProvider, - dialogProvider: DialogProvider, -) : BaseConfigKeyMapViewModel( - config = config, - onboarding = onboarding, - navigationProvider = navigationProvider, - dialogProvider = dialogProvider, -) { - override val configActionsViewModel: ConfigActionsViewModel = ConfigActionsViewModel( - coroutineScope = viewModelScope, - displayAction = display, - createAction = createActionUseCase, - testAction = testActionUseCase, - config = config, - onboarding = onboarding, - resourceProvider = resourceProvider, - navigationProvider = navigationProvider, - dialogProvider = dialogProvider, - ) - - override val configTriggerViewModel: ConfigTriggerViewModel = ConfigTriggerViewModel( - coroutineScope = viewModelScope, - onboarding = onboarding, - config = config, - recordTrigger = recordTriggerUseCase, - createKeyMapShortcut = createKeyMapShortcutUseCase, - displayKeyMap = display, - purchasingManager = purchasingManager, - setupGuiKeyboard = setupGuiKeyboardUseCase, - fingerprintGesturesSupported = fingerprintGesturesSupportedUseCase, - resourceProvider = resourceProvider, - navigationProvider = navigationProvider, - dialogProvider = dialogProvider, - ) - - override val configConstraintsViewModel: ConfigConstraintsViewModel = - ConfigConstraintsViewModel( - coroutineScope = viewModelScope, - config = config, - displayConstraint = display, - resourceProvider = resourceProvider, - navigationProvider = navigationProvider, - dialogProvider = dialogProvider, - ) -} diff --git a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt index 6bc72f0bbe..3989a01ffd 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/accessibility/AccessibilityServiceController.kt @@ -5,41 +5,44 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.github.sds100.keymapper.base.actions.PerformActionsUseCaseImpl import io.github.sds100.keymapper.base.constraints.DetectConstraintsUseCaseImpl +import io.github.sds100.keymapper.base.detection.DetectKeyMapsUseCaseImpl +import io.github.sds100.keymapper.base.input.InputEventHub import io.github.sds100.keymapper.base.keymaps.FingerprintGesturesSupportedUseCase import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase -import io.github.sds100.keymapper.base.keymaps.detection.DetectKeyMapsUseCaseImpl -import io.github.sds100.keymapper.base.reroutekeyevents.RerouteKeyEventsController +import io.github.sds100.keymapper.base.promode.SystemBridgeSetupAssistantController import io.github.sds100.keymapper.base.system.accessibility.AccessibilityNodeRecorder import io.github.sds100.keymapper.base.system.accessibility.BaseAccessibilityServiceController +import io.github.sds100.keymapper.base.trigger.RecordTriggerController import io.github.sds100.keymapper.data.repositories.PreferenceRepository -import io.github.sds100.keymapper.system.devices.DevicesAdapter -import io.github.sds100.keymapper.system.root.SuAdapter +import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapper class AccessibilityServiceController @AssistedInject constructor( @Assisted private val service: MyAccessibilityService, - rerouteKeyEventsControllerFactory: RerouteKeyEventsController.Factory, accessibilityNodeRecorderFactory: AccessibilityNodeRecorder.Factory, performActionsUseCaseFactory: PerformActionsUseCaseImpl.Factory, detectKeyMapsUseCaseFactory: DetectKeyMapsUseCaseImpl.Factory, detectConstraintsUseCaseFactory: DetectConstraintsUseCaseImpl.Factory, fingerprintGesturesSupported: FingerprintGesturesSupportedUseCase, pauseKeyMapsUseCase: PauseKeyMapsUseCase, - devicesAdapter: DevicesAdapter, - suAdapter: SuAdapter, settingsRepository: PreferenceRepository, + keyEventRelayServiceWrapper: KeyEventRelayServiceWrapper, + inputEventHub: InputEventHub, + recordTriggerController: RecordTriggerController, + setupAssistantControllerFactory: SystemBridgeSetupAssistantController.Factory, ) : BaseAccessibilityServiceController( service = service, - rerouteKeyEventsControllerFactory = rerouteKeyEventsControllerFactory, accessibilityNodeRecorderFactory = accessibilityNodeRecorderFactory, performActionsUseCaseFactory = performActionsUseCaseFactory, detectKeyMapsUseCaseFactory = detectKeyMapsUseCaseFactory, detectConstraintsUseCaseFactory = detectConstraintsUseCaseFactory, fingerprintGesturesSupported = fingerprintGesturesSupported, pauseKeyMapsUseCase = pauseKeyMapsUseCase, - devicesAdapter = devicesAdapter, - suAdapter = suAdapter, settingsRepository = settingsRepository, + keyEventRelayServiceWrapper = keyEventRelayServiceWrapper, + inputEventHub = inputEventHub, + recordTriggerController = recordTriggerController, + setupAssistantControllerFactory = setupAssistantControllerFactory, ) { @AssistedFactory interface Factory { diff --git a/app/src/main/java/io/github/sds100/keymapper/trigger/ConfigTriggerViewModel.kt b/app/src/main/java/io/github/sds100/keymapper/trigger/ConfigTriggerViewModel.kt index 1654023097..b1dd42b073 100644 --- a/app/src/main/java/io/github/sds100/keymapper/trigger/ConfigTriggerViewModel.kt +++ b/app/src/main/java/io/github/sds100/keymapper/trigger/ConfigTriggerViewModel.kt @@ -1,25 +1,25 @@ package io.github.sds100.keymapper.trigger -import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapUseCase -import io.github.sds100.keymapper.base.keymaps.CreateKeyMapShortcutUseCase +import dagger.hilt.android.lifecycle.HiltViewModel import io.github.sds100.keymapper.base.keymaps.DisplayKeyMapUseCase import io.github.sds100.keymapper.base.keymaps.FingerprintGesturesSupportedUseCase import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase import io.github.sds100.keymapper.base.purchasing.PurchasingManager +import io.github.sds100.keymapper.base.shortcuts.CreateKeyMapShortcutUseCase import io.github.sds100.keymapper.base.trigger.BaseConfigTriggerViewModel -import io.github.sds100.keymapper.base.trigger.RecordTriggerUseCase +import io.github.sds100.keymapper.base.trigger.ConfigTriggerUseCase +import io.github.sds100.keymapper.base.trigger.RecordTriggerController import io.github.sds100.keymapper.base.trigger.SetupGuiKeyboardUseCase import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider import io.github.sds100.keymapper.base.utils.ui.DialogProvider import io.github.sds100.keymapper.base.utils.ui.ResourceProvider -import kotlinx.coroutines.CoroutineScope import javax.inject.Inject +@HiltViewModel class ConfigTriggerViewModel @Inject constructor( - private val coroutineScope: CoroutineScope, private val onboarding: OnboardingUseCase, - private val config: ConfigKeyMapUseCase, - private val recordTrigger: RecordTriggerUseCase, + private val config: ConfigTriggerUseCase, + private val recordTrigger: RecordTriggerController, private val createKeyMapShortcut: CreateKeyMapShortcutUseCase, private val displayKeyMap: DisplayKeyMapUseCase, private val purchasingManager: PurchasingManager, @@ -29,7 +29,6 @@ class ConfigTriggerViewModel @Inject constructor( navigationProvider: NavigationProvider, dialogProvider: DialogProvider, ) : BaseConfigTriggerViewModel( - coroutineScope = coroutineScope, onboarding = onboarding, config = config, recordTrigger = recordTrigger, diff --git a/app/version.properties b/app/version.properties index be3fe652ab..c0cb36037e 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,3 +1,3 @@ -VERSION_NAME=3.2.1 +VERSION_NAME=4.0.0 VERSION_CODE=132 VERSION_NUM=0 \ No newline at end of file diff --git a/base/build.gradle.kts b/base/build.gradle.kts index 86e093e95d..b247f287b7 100644 --- a/base/build.gradle.kts +++ b/base/build.gradle.kts @@ -73,6 +73,7 @@ android { dependencies { implementation(project(":common")) implementation(project(":data")) + implementation(project(":sysbridge")) implementation(project(":system")) implementation(project(":systemstubs")) @@ -89,7 +90,6 @@ dependencies { kapt(libs.airbnb.epoxy.processor) implementation(libs.jakewharton.timber) implementation(libs.anggrayudi.storage) - implementation(libs.github.mflisar.dragselectrecyclerview) implementation(libs.google.flexbox) implementation(libs.squareup.okhttp) coreLibraryDesugaring(libs.desugar.jdk.libs) diff --git a/base/src/main/AndroidManifest.xml b/base/src/main/AndroidManifest.xml index b14e766002..bb55b6b0fe 100644 --- a/base/src/main/AndroidManifest.xml +++ b/base/src/main/AndroidManifest.xml @@ -48,7 +48,7 @@ diff --git a/base/src/main/java/io/github/sds100/keymapper/base/ActivityViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/ActivityViewModel.kt index a8b48bb2b9..b734c2c94b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/ActivityViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/ActivityViewModel.kt @@ -3,8 +3,9 @@ package io.github.sds100.keymapper.base import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import io.github.sds100.keymapper.base.utils.navigation.NavDestination import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider -import io.github.sds100.keymapper.base.utils.navigation.NavigationProviderImpl +import io.github.sds100.keymapper.base.utils.navigation.navigate import io.github.sds100.keymapper.base.utils.ui.DialogProvider import io.github.sds100.keymapper.base.utils.ui.ResourceProvider import io.github.sds100.keymapper.base.utils.ui.ViewModelHelper @@ -15,12 +16,12 @@ import javax.inject.Inject class ActivityViewModel @Inject constructor( resourceProvider: ResourceProvider, dialogProvider: DialogProvider, + navigationProvider: NavigationProvider, ) : ViewModel(), ResourceProvider by resourceProvider, DialogProvider by dialogProvider, - NavigationProvider by NavigationProviderImpl() { + NavigationProvider by navigationProvider { - var handledActivityLaunchIntent: Boolean = false var previousNightMode: Int? = null fun onCantFindAccessibilitySettings() { @@ -31,4 +32,10 @@ class ActivityViewModel @Inject constructor( ) } } + + fun launchProModeSetup() { + viewModelScope.launch { + navigate("pro_mode_setup", NavDestination.ProModeSetup) + } + } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt index 62d85ba466..c4afd7d072 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseKeyMapperApp.kt @@ -14,7 +14,8 @@ import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.ProcessLifecycleOwner import androidx.multidex.MultiDexApplication import io.github.sds100.keymapper.base.logging.KeyMapperLoggingTree -import io.github.sds100.keymapper.base.settings.ThemeUtils +import io.github.sds100.keymapper.base.promode.SystemBridgeAutoStarter +import io.github.sds100.keymapper.base.settings.Theme import io.github.sds100.keymapper.base.system.accessibility.AccessibilityServiceAdapterImpl import io.github.sds100.keymapper.base.system.inputmethod.AutoSwitchImeController import io.github.sds100.keymapper.base.system.notifications.NotificationController @@ -22,9 +23,12 @@ import io.github.sds100.keymapper.base.system.permissions.AutoGrantPermissionCon import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.entities.LogEntryEntity import io.github.sds100.keymapper.data.repositories.LogRepository -import io.github.sds100.keymapper.data.repositories.SettingsPreferenceRepository +import io.github.sds100.keymapper.data.repositories.PreferenceRepositoryImpl +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManagerImpl +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState import io.github.sds100.keymapper.system.apps.AndroidPackageManagerAdapter import io.github.sds100.keymapper.system.devices.AndroidDevicesAdapter +import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapperImpl import io.github.sds100.keymapper.system.permissions.AndroidPermissionAdapter import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.system.root.SuAdapterImpl @@ -74,11 +78,20 @@ abstract class BaseKeyMapperApp : MultiDexApplication() { lateinit var loggingTree: KeyMapperLoggingTree @Inject - lateinit var settingsRepository: SettingsPreferenceRepository + lateinit var settingsRepository: PreferenceRepositoryImpl @Inject lateinit var logRepository: LogRepository + @Inject + lateinit var keyEventRelayServiceWrapper: KeyEventRelayServiceWrapperImpl + + @Inject + lateinit var systemBridgeAutoStarter: SystemBridgeAutoStarter + + @Inject + lateinit var systemBridgeConnectionManager: SystemBridgeConnectionManagerImpl + private val processLifecycleOwner by lazy { ProcessLifecycleOwner.get() } private val userManager: UserManager? by lazy { getSystemService() } @@ -138,8 +151,8 @@ abstract class BaseKeyMapperApp : MultiDexApplication() { .map { it?.toIntOrNull() } .map { when (it) { - ThemeUtils.DARK -> AppCompatDelegate.MODE_NIGHT_YES - ThemeUtils.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO + Theme.DARK.value -> AppCompatDelegate.MODE_NIGHT_YES + Theme.LIGHT.value -> AppCompatDelegate.MODE_NIGHT_NO else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM } } @@ -184,6 +197,19 @@ abstract class BaseKeyMapperApp : MultiDexApplication() { }.launchIn(appCoroutineScope) autoGrantPermissionController.start() + keyEventRelayServiceWrapper.bind() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + systemBridgeAutoStarter.init() + + appCoroutineScope.launch { + systemBridgeConnectionManager.connectionState.collect { state -> + if (state is SystemBridgeConnectionState.Connected) { + settingsRepository.set(Keys.isSystemBridgeUsed, true) + } + } + } + } } abstract fun getMainActivityClass(): Class<*> diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt index 6c6dae42eb..bd74f28121 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainActivity.kt @@ -26,17 +26,22 @@ import com.anggrayudi.storage.extension.openInputStream import com.anggrayudi.storage.extension.openOutputStream import com.anggrayudi.storage.extension.toDocumentFile import io.github.sds100.keymapper.base.compose.ComposeColors +import io.github.sds100.keymapper.base.input.InputEventDetectionSource +import io.github.sds100.keymapper.base.input.InputEventHubImpl import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase import io.github.sds100.keymapper.base.system.accessibility.AccessibilityServiceAdapterImpl import io.github.sds100.keymapper.base.system.permissions.RequestPermissionDelegate -import io.github.sds100.keymapper.base.trigger.RecordTriggerController +import io.github.sds100.keymapper.base.trigger.RecordTriggerControllerImpl import io.github.sds100.keymapper.base.utils.ui.ResourceProviderImpl -import io.github.sds100.keymapper.base.utils.ui.launchRepeatOnLifecycle import io.github.sds100.keymapper.common.BuildConfigProvider +import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupControllerImpl +import io.github.sds100.keymapper.system.devices.AndroidDevicesAdapter import io.github.sds100.keymapper.system.files.FileUtils -import io.github.sds100.keymapper.system.inputevents.MyMotionEvent +import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent +import io.github.sds100.keymapper.system.network.AndroidNetworkAdapter import io.github.sds100.keymapper.system.notifications.NotificationReceiverAdapterImpl import io.github.sds100.keymapper.system.permissions.AndroidPermissionAdapter +import io.github.sds100.keymapper.system.root.SuAdapterImpl import io.github.sds100.keymapper.system.shizuku.ShizukuAdapter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn @@ -56,6 +61,9 @@ abstract class BaseMainActivity : AppCompatActivity() { const val ACTION_SAVE_FILE = "${BuildConfig.LIBRARY_PACKAGE_NAME}.ACTION_SAVE_FILE" const val EXTRA_FILE_URI = "${BuildConfig.LIBRARY_PACKAGE_NAME}.EXTRA_FILE_URI" + + const val ACTION_START_SYSTEM_BRIDGE = + "${BuildConfig.LIBRARY_PACKAGE_NAME}.ACTION_START_SYSTEM_BRIDGE" } @Inject @@ -71,7 +79,7 @@ abstract class BaseMainActivity : AppCompatActivity() { lateinit var onboardingUseCase: OnboardingUseCase @Inject - lateinit var recordTriggerController: RecordTriggerController + lateinit var recordTriggerController: RecordTriggerControllerImpl @Inject lateinit var notificationReceiverAdapter: NotificationReceiverAdapterImpl @@ -82,6 +90,21 @@ abstract class BaseMainActivity : AppCompatActivity() { @Inject lateinit var buildConfigProvider: BuildConfigProvider + @Inject + lateinit var systemBridgeSetupController: SystemBridgeSetupControllerImpl + + @Inject + lateinit var suAdapter: SuAdapterImpl + + @Inject + lateinit var devicesAdapter: AndroidDevicesAdapter + + @Inject + lateinit var networkAdapter: AndroidNetworkAdapter + + @Inject + lateinit var inputEventHub: InputEventHubImpl + private lateinit var requestPermissionDelegate: RequestPermissionDelegate private val currentNightMode: Int @@ -155,21 +178,6 @@ abstract class BaseMainActivity : AppCompatActivity() { } .launchIn(lifecycleScope) - // Must launch when the activity is resumed - // so the nav controller can be found - launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - if (viewModel.handledActivityLaunchIntent) { - return@launchRepeatOnLifecycle - } - - when (intent?.action) { - ACTION_SHOW_ACCESSIBILITY_SETTINGS_NOT_FOUND_DIALOG -> { - viewModel.onCantFindAccessibilitySettings() - viewModel.handledActivityLaunchIntent = true - } - } - } - IntentFilter().apply { addAction(ACTION_SAVE_FILE) @@ -180,6 +188,8 @@ abstract class BaseMainActivity : AppCompatActivity() { ContextCompat.RECEIVER_EXPORTED, ) } + + handleIntent(intent) } override fun onResume() { @@ -193,6 +203,9 @@ abstract class BaseMainActivity : AppCompatActivity() { // the activities have not necessarily resumed at that point. permissionAdapter.onPermissionsChanged() serviceAdapter.invalidateState() + suAdapter.invalidateIsRooted() + systemBridgeSetupController.invalidateSettings() + networkAdapter.invalidateState() } override fun onDestroy() { @@ -203,11 +216,19 @@ abstract class BaseMainActivity : AppCompatActivity() { super.onDestroy() } + /** + * Process motion events from the activity so that DPAD buttons can be recorded + * even when the Key Mapper IME is not being used. DO NOT record the key events because + * these are sent from the joy sticks. + */ override fun onGenericMotionEvent(event: MotionEvent?): Boolean { event ?: return super.onGenericMotionEvent(event) - val consume = - recordTriggerController.onActivityMotionEvent(MyMotionEvent.fromMotionEvent(event)) + val gamepadEvent = KMGamePadEvent.fromMotionEvent(event) ?: return false + val consume = inputEventHub.onInputEvent( + gamepadEvent, + detectionSource = InputEventDetectionSource.INPUT_METHOD, + ) return if (consume) { true @@ -217,6 +238,29 @@ abstract class BaseMainActivity : AppCompatActivity() { } } + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + + handleIntent(intent) + } + + private fun handleIntent(intent: Intent?) { + when (intent?.action) { + ACTION_SHOW_ACCESSIBILITY_SETTINGS_NOT_FOUND_DIALOG -> { + viewModel.onCantFindAccessibilitySettings() + // Only clear the intent if it is handled in case it is used elsewhere + this.intent = null + } + + ACTION_START_SYSTEM_BRIDGE -> { + viewModel.launchProModeSetup() + + // Only clear the intent if it is handled in case it is used elsewhere + this.intent = null + } + } + } + private fun saveFile(originalFile: Uri, targetFile: Uri) { lifecycleScope.launch(Dispatchers.IO) { targetFile.openOutputStream(this@BaseMainActivity)?.use { output -> diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt index 4589823952..1fce8dbf81 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseMainNavHost.kt @@ -1,7 +1,14 @@ package io.github.sds100.keymapper.base import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.add +import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel @@ -9,10 +16,19 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import io.github.sds100.keymapper.base.actions.ChooseActionScreen +import io.github.sds100.keymapper.base.actions.ChooseActionViewModel import io.github.sds100.keymapper.base.actions.uielement.InteractUiElementScreen import io.github.sds100.keymapper.base.actions.uielement.InteractUiElementViewModel import io.github.sds100.keymapper.base.constraints.ChooseConstraintScreen import io.github.sds100.keymapper.base.constraints.ChooseConstraintViewModel +import io.github.sds100.keymapper.base.logging.LogScreen +import io.github.sds100.keymapper.base.promode.ProModeScreen +import io.github.sds100.keymapper.base.promode.ProModeSetupScreen +import io.github.sds100.keymapper.base.settings.AutomaticChangeImeSettingsScreen +import io.github.sds100.keymapper.base.settings.DefaultOptionsSettingsScreen +import io.github.sds100.keymapper.base.settings.SettingsScreen +import io.github.sds100.keymapper.base.settings.SettingsViewModel import io.github.sds100.keymapper.base.utils.navigation.NavDestination import io.github.sds100.keymapper.base.utils.navigation.handleRouteArgs import kotlinx.serialization.json.Json @@ -54,6 +70,68 @@ fun BaseMainNavHost( ) } + composable { + val viewModel: ChooseActionViewModel = hiltViewModel() + + ChooseActionScreen( + modifier = Modifier.fillMaxSize(), + viewModel = viewModel, + ) + } + + composable { + val viewModel: SettingsViewModel = hiltViewModel() + + SettingsScreen( + modifier = Modifier.fillMaxSize(), + viewModel = viewModel, + ) + } + + composable { + val viewModel: SettingsViewModel = hiltViewModel() + + DefaultOptionsSettingsScreen( + modifier = Modifier.fillMaxSize(), + viewModel = viewModel, + ) + } + + composable { + val viewModel: SettingsViewModel = hiltViewModel() + + AutomaticChangeImeSettingsScreen( + modifier = Modifier.fillMaxSize(), + viewModel = viewModel, + ) + } + + composable { + ProModeScreen( + modifier = Modifier + .fillMaxSize() + .windowInsetsPadding( + WindowInsets.systemBars.only(sides = WindowInsetsSides.Horizontal) + .add(WindowInsets.displayCutout.only(sides = WindowInsetsSides.Horizontal)), + ), + viewModel = hiltViewModel(), + ) + } + + composable { + ProModeSetupScreen( + viewModel = hiltViewModel(), + ) + } + + composable { + LogScreen( + modifier = Modifier.fillMaxSize(), + viewModel = hiltViewModel(), + onBackClick = { navController.popBackStack() }, + ) + } + composableDestinations() } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseSingletonHiltModule.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseSingletonHiltModule.kt index 55a0ffabfe..5c0783bdc1 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseSingletonHiltModule.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseSingletonHiltModule.kt @@ -14,10 +14,14 @@ import io.github.sds100.keymapper.base.backup.BackupManager import io.github.sds100.keymapper.base.backup.BackupManagerImpl import io.github.sds100.keymapper.base.constraints.GetConstraintErrorUseCase import io.github.sds100.keymapper.base.constraints.GetConstraintErrorUseCaseImpl -import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapUseCase -import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapUseCaseController +import io.github.sds100.keymapper.base.input.InputEventHub +import io.github.sds100.keymapper.base.input.InputEventHubImpl +import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapState +import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapStateImpl import io.github.sds100.keymapper.base.keymaps.FingerprintGesturesSupportedUseCase import io.github.sds100.keymapper.base.keymaps.FingerprintGesturesSupportedUseCaseImpl +import io.github.sds100.keymapper.base.keymaps.GetDefaultKeyMapOptionsUseCase +import io.github.sds100.keymapper.base.keymaps.GetDefaultKeyMapOptionsUseCaseImpl import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCaseImpl import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase @@ -25,6 +29,8 @@ import io.github.sds100.keymapper.base.onboarding.OnboardingUseCaseImpl import io.github.sds100.keymapper.base.system.accessibility.AccessibilityServiceAdapterImpl import io.github.sds100.keymapper.base.system.accessibility.ControlAccessibilityServiceUseCase import io.github.sds100.keymapper.base.system.accessibility.ControlAccessibilityServiceUseCaseImpl +import io.github.sds100.keymapper.base.system.inputmethod.ImeInputEventInjector +import io.github.sds100.keymapper.base.system.inputmethod.ImeInputEventInjectorImpl import io.github.sds100.keymapper.base.system.inputmethod.ShowHideInputMethodUseCase import io.github.sds100.keymapper.base.system.inputmethod.ShowHideInputMethodUseCaseImpl import io.github.sds100.keymapper.base.system.inputmethod.ShowInputMethodPickerUseCase @@ -35,7 +41,7 @@ import io.github.sds100.keymapper.base.system.notifications.AndroidNotificationA import io.github.sds100.keymapper.base.system.notifications.ManageNotificationsUseCase import io.github.sds100.keymapper.base.system.notifications.ManageNotificationsUseCaseImpl import io.github.sds100.keymapper.base.trigger.RecordTriggerController -import io.github.sds100.keymapper.base.trigger.RecordTriggerUseCase +import io.github.sds100.keymapper.base.trigger.RecordTriggerControllerImpl import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider import io.github.sds100.keymapper.base.utils.navigation.NavigationProviderImpl import io.github.sds100.keymapper.base.utils.ui.DialogProvider @@ -45,6 +51,8 @@ import io.github.sds100.keymapper.base.utils.ui.ResourceProviderImpl import io.github.sds100.keymapper.common.utils.DefaultUuidGenerator import io.github.sds100.keymapper.common.utils.UuidGenerator import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceAdapter +import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapper +import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapperImpl import io.github.sds100.keymapper.system.notifications.NotificationAdapter import javax.inject.Singleton @@ -101,11 +109,7 @@ abstract class BaseSingletonHiltModule { @Binds @Singleton - abstract fun bindConfigKeyMapUseCase(impl: ConfigKeyMapUseCaseController): ConfigKeyMapUseCase - - @Binds - @Singleton - abstract fun bindRecordTriggerUseCase(impl: RecordTriggerController): RecordTriggerUseCase + abstract fun bindRecordTriggerUseCase(impl: RecordTriggerControllerImpl): RecordTriggerController @Binds @Singleton @@ -134,4 +138,24 @@ abstract class BaseSingletonHiltModule { @Binds @Singleton abstract fun bindDialogProvider(impl: DialogProviderImpl): DialogProvider + + @Binds + @Singleton + abstract fun bindInputEventHub(impl: InputEventHubImpl): InputEventHub + + @Binds + @Singleton + abstract fun keyEventRelayServiceWrapper(impl: KeyEventRelayServiceWrapperImpl): KeyEventRelayServiceWrapper + + @Binds + @Singleton + abstract fun imeInputEvenInjector(impl: ImeInputEventInjectorImpl): ImeInputEventInjector + + @Binds + @Singleton + abstract fun bindConfigKeyMapState(impl: ConfigKeyMapStateImpl): ConfigKeyMapState + + @Binds + @Singleton + abstract fun bindGetDefaultKeyMapOptionsUseCas(impl: GetDefaultKeyMapOptionsUseCaseImpl): GetDefaultKeyMapOptionsUseCase } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/BaseViewModelHiltModule.kt b/base/src/main/java/io/github/sds100/keymapper/base/BaseViewModelHiltModule.kt index 526b59f6e7..b31a3833c4 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/BaseViewModelHiltModule.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/BaseViewModelHiltModule.kt @@ -5,8 +5,11 @@ import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.android.components.ViewModelComponent import dagger.hilt.android.scopes.ViewModelScoped +import io.github.sds100.keymapper.base.actions.ConfigActionsUseCase +import io.github.sds100.keymapper.base.actions.ConfigActionsUseCaseImpl import io.github.sds100.keymapper.base.actions.CreateActionUseCase import io.github.sds100.keymapper.base.actions.CreateActionUseCaseImpl +import io.github.sds100.keymapper.base.actions.DisplayActionUseCase import io.github.sds100.keymapper.base.actions.TestActionUseCase import io.github.sds100.keymapper.base.actions.TestActionUseCaseImpl import io.github.sds100.keymapper.base.actions.keyevent.ConfigKeyEventUseCase @@ -15,20 +18,25 @@ import io.github.sds100.keymapper.base.actions.sound.ChooseSoundFileUseCase import io.github.sds100.keymapper.base.actions.sound.ChooseSoundFileUseCaseImpl import io.github.sds100.keymapper.base.backup.BackupRestoreMappingsUseCase import io.github.sds100.keymapper.base.backup.BackupRestoreMappingsUseCaseImpl +import io.github.sds100.keymapper.base.constraints.ConfigConstraintsUseCase +import io.github.sds100.keymapper.base.constraints.ConfigConstraintsUseCaseImpl import io.github.sds100.keymapper.base.constraints.CreateConstraintUseCase import io.github.sds100.keymapper.base.constraints.CreateConstraintUseCaseImpl +import io.github.sds100.keymapper.base.constraints.DisplayConstraintUseCase +import io.github.sds100.keymapper.base.home.ListKeyMapsUseCase +import io.github.sds100.keymapper.base.home.ListKeyMapsUseCaseImpl import io.github.sds100.keymapper.base.home.ShowHomeScreenAlertsUseCase import io.github.sds100.keymapper.base.home.ShowHomeScreenAlertsUseCaseImpl -import io.github.sds100.keymapper.base.keymaps.CreateKeyMapShortcutUseCase -import io.github.sds100.keymapper.base.keymaps.CreateKeyMapShortcutUseCaseImpl import io.github.sds100.keymapper.base.keymaps.DisplayKeyMapUseCase import io.github.sds100.keymapper.base.keymaps.DisplayKeyMapUseCaseImpl -import io.github.sds100.keymapper.base.keymaps.ListKeyMapsUseCase -import io.github.sds100.keymapper.base.keymaps.ListKeyMapsUseCaseImpl import io.github.sds100.keymapper.base.logging.DisplayLogUseCase import io.github.sds100.keymapper.base.logging.DisplayLogUseCaseImpl +import io.github.sds100.keymapper.base.promode.SystemBridgeSetupUseCase +import io.github.sds100.keymapper.base.promode.SystemBridgeSetupUseCaseImpl import io.github.sds100.keymapper.base.settings.ConfigSettingsUseCase import io.github.sds100.keymapper.base.settings.ConfigSettingsUseCaseImpl +import io.github.sds100.keymapper.base.shortcuts.CreateKeyMapShortcutUseCase +import io.github.sds100.keymapper.base.shortcuts.CreateKeyMapShortcutUseCaseImpl import io.github.sds100.keymapper.base.sorting.SortKeyMapsUseCase import io.github.sds100.keymapper.base.sorting.SortKeyMapsUseCaseImpl import io.github.sds100.keymapper.base.system.apps.DisplayAppShortcutsUseCase @@ -37,6 +45,8 @@ import io.github.sds100.keymapper.base.system.apps.DisplayAppsUseCase import io.github.sds100.keymapper.base.system.apps.DisplayAppsUseCaseImpl import io.github.sds100.keymapper.base.system.bluetooth.ChooseBluetoothDeviceUseCase import io.github.sds100.keymapper.base.system.bluetooth.ChooseBluetoothDeviceUseCaseImpl +import io.github.sds100.keymapper.base.trigger.ConfigTriggerUseCase +import io.github.sds100.keymapper.base.trigger.ConfigTriggerUseCaseImpl import io.github.sds100.keymapper.base.trigger.SetupGuiKeyboardUseCase import io.github.sds100.keymapper.base.trigger.SetupGuiKeyboardUseCaseImpl @@ -47,6 +57,14 @@ abstract class BaseViewModelHiltModule { @ViewModelScoped abstract fun bindDisplayKeyMapUseCase(impl: DisplayKeyMapUseCaseImpl): DisplayKeyMapUseCase + @Binds + @ViewModelScoped + abstract fun bindDisplayActionUseCase(impl: DisplayKeyMapUseCaseImpl): DisplayActionUseCase + + @Binds + @ViewModelScoped + abstract fun bindDisplayConstraintUseCase(impl: DisplayKeyMapUseCaseImpl): DisplayConstraintUseCase + @Binds @ViewModelScoped abstract fun bindListKeyMapsUseCase(impl: ListKeyMapsUseCaseImpl): ListKeyMapsUseCase @@ -110,4 +128,20 @@ abstract class BaseViewModelHiltModule { @Binds @ViewModelScoped abstract fun bindCreateConstraintUseCase(impl: CreateConstraintUseCaseImpl): CreateConstraintUseCase + + @Binds + @ViewModelScoped + abstract fun bindProModeSetupUseCase(impl: SystemBridgeSetupUseCaseImpl): SystemBridgeSetupUseCase + + @Binds + @ViewModelScoped + abstract fun bindConfigConstraintsUseCase(impl: ConfigConstraintsUseCaseImpl): ConfigConstraintsUseCase + + @Binds + @ViewModelScoped + abstract fun bindConfigActionsUseCase(impl: ConfigActionsUseCaseImpl): ConfigActionsUseCase + + @Binds + @ViewModelScoped + abstract fun bindConfigTriggerUseCase(impl: ConfigTriggerUseCaseImpl): ConfigTriggerUseCase } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionData.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionData.kt index 3923f4d70b..69ea62085f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionData.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionData.kt @@ -48,7 +48,6 @@ sealed class ActionData : Comparable { data class InputKeyEvent( val keyCode: Int, val metaState: Int = 0, - val useShell: Boolean = false, val device: Device? = null, ) : ActionData() { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionDataEntityMapper.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionDataEntityMapper.kt index d9c0dd80cc..8466da44d6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionDataEntityMapper.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionDataEntityMapper.kt @@ -79,11 +79,6 @@ object ActionDataEntityMapper { entity.extras.getData(ActionEntity.EXTRA_KEY_EVENT_DEVICE_NAME) .valueOrNull() ?: "" - val useShell = - entity.extras.getData(ActionEntity.EXTRA_KEY_EVENT_USE_SHELL).then { - (it == "true").success() - }.valueOrNull() ?: false - val device = if (deviceDescriptor != null) { ActionData.InputKeyEvent.Device(deviceDescriptor, deviceName) } else { @@ -93,7 +88,6 @@ object ActionDataEntityMapper { ActionData.InputKeyEvent( keyCode = entity.data.toInt(), metaState = metaState, - useShell = useShell, device = device, ) } @@ -724,15 +718,6 @@ object ActionDataEntityMapper { ) is ActionData.InputKeyEvent -> sequence { - if (data.useShell) { - val string = if (data.useShell) { - "true" - } else { - "false" - } - yield(EntityExtra(ActionEntity.EXTRA_KEY_EVENT_USE_SHELL, string)) - } - if (data.metaState != 0) { yield( EntityExtra( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt index fb2d13bde6..3f9fc360de 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionErrorSnapshot.kt @@ -1,12 +1,19 @@ package io.github.sds100.keymapper.base.actions +import android.os.Build import io.github.sds100.keymapper.base.actions.sound.SoundsManager import io.github.sds100.keymapper.base.system.inputmethod.KeyMapperImeHelper import io.github.sds100.keymapper.common.BuildConfigProvider import io.github.sds100.keymapper.common.utils.KMError +import io.github.sds100.keymapper.common.utils.firstBlocking import io.github.sds100.keymapper.common.utils.onFailure import io.github.sds100.keymapper.common.utils.onSuccess import io.github.sds100.keymapper.common.utils.valueOrNull +import io.github.sds100.keymapper.data.Keys +import io.github.sds100.keymapper.data.repositories.PreferenceRepository +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState +import io.github.sds100.keymapper.sysbridge.utils.SystemBridgeError import io.github.sds100.keymapper.system.SystemError import io.github.sds100.keymapper.system.apps.PackageManagerAdapter import io.github.sds100.keymapper.system.camera.CameraAdapter @@ -17,7 +24,6 @@ import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.system.permissions.PermissionAdapter import io.github.sds100.keymapper.system.permissions.SystemFeatureAdapter import io.github.sds100.keymapper.system.ringtones.RingtoneAdapter -import io.github.sds100.keymapper.system.shizuku.ShizukuAdapter class LazyActionErrorSnapshot( private val packageManager: PackageManagerAdapter, @@ -26,9 +32,10 @@ class LazyActionErrorSnapshot( systemFeatureAdapter: SystemFeatureAdapter, cameraAdapter: CameraAdapter, private val soundsManager: SoundsManager, - shizukuAdapter: ShizukuAdapter, private val ringtoneAdapter: RingtoneAdapter, private val buildConfigProvider: BuildConfigProvider, + private val systemBridgeConnectionManager: SystemBridgeConnectionManager, + private val preferenceRepository: PreferenceRepository, ) : ActionErrorSnapshot, IsActionSupportedUseCase by IsActionSupportedUseCaseImpl( systemFeatureAdapter, @@ -40,8 +47,6 @@ class LazyActionErrorSnapshot( private val isCompatibleImeEnabled by lazy { keyMapperImeHelper.isCompatibleImeEnabled() } private val isCompatibleImeChosen by lazy { keyMapperImeHelper.isCompatibleImeChosen() } - private val isShizukuInstalled by lazy { shizukuAdapter.isInstalled.value } - private val isShizukuStarted by lazy { shizukuAdapter.isStarted.value } private val isVoiceAssistantInstalled by lazy { packageManager.isVoiceAssistantInstalled() } private val grantedPermissions: MutableMap = mutableMapOf() private val flashLenses by lazy { @@ -56,6 +61,22 @@ class LazyActionErrorSnapshot( } } + private val isSystemBridgeConnected: Boolean by lazy { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + systemBridgeConnectionManager.connectionState.value is SystemBridgeConnectionState.Connected + } else { + false + } + } + + private val isSystemBridgeUsed: Boolean by lazy { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + preferenceRepository.get(Keys.isSystemBridgeUsed).firstBlocking() ?: false + } else { + false + } + } + override fun getErrors(actions: List): Map { // Fixes #797 and #1719 // Store which input method would be selected if the actions run successfully. @@ -96,15 +117,10 @@ class LazyActionErrorSnapshot( return isSupportedError } - if (action.canUseShizukuToPerform() && isShizukuInstalled) { - if (!(action.canUseImeToPerform() && isCompatibleImeChosen)) { - when { - !isShizukuStarted -> return KMError.ShizukuNotStarted - - !isPermissionGranted(Permission.SHIZUKU) -> return SystemError.PermissionDenied( - Permission.SHIZUKU, - ) - } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && action.canUseSystemBridgeToPerform() && isSystemBridgeUsed) { + // Only throw an error if they aren't using another compatible back up option + if (!(action.canUseImeToPerform() && isCompatibleImeChosen) && !isSystemBridgeConnected) { + return SystemBridgeError.Disconnected } } else if (action.canUseImeToPerform()) { if (!isCompatibleImeEnabled) { @@ -116,6 +132,13 @@ class LazyActionErrorSnapshot( } } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && + ActionUtils.isSystemBridgeRequired(action.id) && + !isSystemBridgeConnected + ) { + return SystemBridgeError.Disconnected + } + for (permission in ActionUtils.getRequiredPermissions(action.id)) { if (!isPermissionGranted(permission)) { return SystemError.PermissionDenied(permission) @@ -133,13 +156,6 @@ class LazyActionErrorSnapshot( return getAppError(action.packageName) } - is ActionData.InputKeyEvent -> - if ( - action.useShell && !isPermissionGranted(Permission.ROOT) - ) { - return SystemError.PermissionDenied(Permission.ROOT) - } - is ActionData.Sound.SoundFile -> { soundsManager.getSound(action.soundUid).onFailure { error -> return error diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUiHelper.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUiHelper.kt index 64cd9d1d3a..e3b76dd85d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUiHelper.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUiHelper.kt @@ -7,20 +7,20 @@ import androidx.compose.material.icons.outlined.Android import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.keymaps.KeyMap import io.github.sds100.keymapper.base.utils.DndModeStrings -import io.github.sds100.keymapper.base.utils.InputEventStrings +import io.github.sds100.keymapper.base.utils.KeyCodeStrings import io.github.sds100.keymapper.base.utils.RingerModeStrings import io.github.sds100.keymapper.base.utils.VolumeStreamStrings import io.github.sds100.keymapper.base.utils.ui.IconInfo import io.github.sds100.keymapper.base.utils.ui.ResourceProvider import io.github.sds100.keymapper.base.utils.ui.TintType import io.github.sds100.keymapper.base.utils.ui.compose.ComposeIconInfo +import io.github.sds100.keymapper.common.utils.InputDeviceUtils import io.github.sds100.keymapper.common.utils.Orientation import io.github.sds100.keymapper.common.utils.PinchScreenType import io.github.sds100.keymapper.common.utils.handle import io.github.sds100.keymapper.common.utils.hasFlag import io.github.sds100.keymapper.common.utils.toPercentString import io.github.sds100.keymapper.system.camera.CameraLens -import io.github.sds100.keymapper.system.devices.InputDeviceUtils import io.github.sds100.keymapper.system.intents.IntentTarget class ActionUiHelper( @@ -46,46 +46,43 @@ class ActionUiHelper( } // only a key code can be inputted through the shell - if (action.useShell) { - getString(R.string.description_keyevent_through_shell, keyCodeString) - } else { - val metaStateString = buildString { - for (label in InputEventStrings.MODIFIER_LABELS.entries) { - val modifier = label.key - val labelRes = label.value - if (action.metaState.hasFlag(modifier)) { - append("${getString(labelRes)} + ") - } - } - } + val metaStateString = buildString { + for (label in KeyCodeStrings.MODIFIER_LABELS.entries) { + val modifier = label.key + val labelRes = label.value - if (action.device != null) { - val name = if (action.device.name.isBlank()) { - getString(R.string.unknown_device_name) - } else { - action.device.name + if (action.metaState.hasFlag(modifier)) { + append("${getString(labelRes)} + ") } + } + } - val nameToShow = if (showDeviceDescriptors) { - InputDeviceUtils.appendDeviceDescriptorToName( - action.device.descriptor, - name, - ) - } else { - name - } + if (action.device != null) { + val name = if (action.device.name.isBlank()) { + getString(R.string.unknown_device_name) + } else { + action.device.name + } - getString( - R.string.description_keyevent_from_device, - arrayOf(metaStateString, keyCodeString, nameToShow), + val nameToShow = if (showDeviceDescriptors) { + InputDeviceUtils.appendDeviceDescriptorToName( + action.device.descriptor, + name, ) } else { - getString( - R.string.description_keyevent, - args = arrayOf(metaStateString, keyCodeString), - ) + name } + + getString( + R.string.description_keyevent_from_device, + arrayOf(metaStateString, keyCodeString, nameToShow), + ) + } else { + getString( + R.string.description_keyevent, + args = arrayOf(metaStateString, keyCodeString), + ) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt index 903c562c44..c995e13c39 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ActionUtils.kt @@ -3,6 +3,7 @@ package io.github.sds100.keymapper.base.actions import android.content.pm.PackageManager import android.os.Build import androidx.annotation.DrawableRes +import androidx.annotation.RequiresApi import androidx.annotation.StringRes import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowBack @@ -86,6 +87,8 @@ import io.github.sds100.keymapper.system.permissions.Permission object ActionUtils { + val isSystemBridgeSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + @StringRes fun getCategoryLabel(category: ActionCategory): Int = when (category) { ActionCategory.NAVIGATION -> R.string.action_cat_navigation @@ -525,11 +528,6 @@ object ActionUtils { // The global action still fails even though the API exists in SDK 34. ActionId.COLLAPSE_STATUS_BAR -> Build.VERSION_CODES.TIRAMISU - ActionId.ENABLE_BLUETOOTH, - ActionId.DISABLE_BLUETOOTH, - ActionId.TOGGLE_BLUETOOTH, - -> Build.VERSION_CODES.S_V2 - // See https://issuetracker.google.com/issues/225186417. The global action // is not marked as deprecated even though it doesn't work. ActionId.TOGGLE_SPLIT_SCREEN -> Build.VERSION_CODES.S @@ -551,6 +549,15 @@ object ActionUtils { ActionId.DISABLE_WIFI, -> listOf(PackageManager.FEATURE_WIFI) + ActionId.TOGGLE_MOBILE_DATA, + ActionId.ENABLE_MOBILE_DATA, + ActionId.DISABLE_MOBILE_DATA, + -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + listOf(PackageManager.FEATURE_TELEPHONY_DATA) + } else { + listOf(PackageManager.FEATURE_TELEPHONY) + } + ActionId.TOGGLE_NFC, ActionId.ENABLE_NFC, ActionId.DISABLE_NFC, @@ -570,19 +577,50 @@ object ActionUtils { else -> emptyList() } - fun getRequiredPermissions(id: ActionId): List { - when (id) { - ActionId.TOGGLE_WIFI, + @RequiresApi(Build.VERSION_CODES.Q) + fun isSystemBridgeRequired(id: ActionId): Boolean { + return when (id) { ActionId.ENABLE_WIFI, ActionId.DISABLE_WIFI, - -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - return listOf(Permission.ROOT) - } + ActionId.TOGGLE_WIFI, + -> true + + ActionId.TOGGLE_MOBILE_DATA, + ActionId.ENABLE_MOBILE_DATA, + ActionId.DISABLE_MOBILE_DATA, + -> true + + ActionId.ENABLE_NFC, + ActionId.DISABLE_NFC, + ActionId.TOGGLE_NFC, + -> true + + ActionId.TOGGLE_AIRPLANE_MODE, + ActionId.ENABLE_AIRPLANE_MODE, + ActionId.DISABLE_AIRPLANE_MODE, + -> true + + ActionId.TOGGLE_BLUETOOTH, + ActionId.ENABLE_BLUETOOTH, + ActionId.DISABLE_BLUETOOTH, + -> Build.VERSION.SDK_INT >= Build.VERSION_CODES.S_V2 + + ActionId.POWER_ON_OFF_DEVICE -> true + else -> false + } + } + + fun getRequiredPermissions(id: ActionId): List { + when (id) { ActionId.TOGGLE_MOBILE_DATA, ActionId.ENABLE_MOBILE_DATA, ActionId.DISABLE_MOBILE_DATA, - -> return listOf(Permission.ROOT) + -> return if (isSystemBridgeSupported) { + emptyList() + } else { + listOf(Permission.ROOT) + } ActionId.PLAY_PAUSE_MEDIA_PACKAGE, ActionId.PAUSE_MEDIA_PACKAGE, @@ -634,7 +672,11 @@ object ActionUtils { ActionId.ENABLE_NFC, ActionId.DISABLE_NFC, ActionId.TOGGLE_NFC, - -> return listOf(Permission.ROOT) + -> return if (isSystemBridgeSupported) { + emptyList() + } else { + listOf(Permission.ROOT) + } ActionId.SHOW_KEYBOARD_PICKER -> if (Build.VERSION.SDK_INT in Build.VERSION_CODES.O_MR1..Build.VERSION_CODES.P) { @@ -648,7 +690,11 @@ object ActionUtils { ActionId.TOGGLE_AIRPLANE_MODE, ActionId.ENABLE_AIRPLANE_MODE, ActionId.DISABLE_AIRPLANE_MODE, - -> return listOf(Permission.ROOT) + -> return if (isSystemBridgeSupported) { + emptyList() + } else { + listOf(Permission.ROOT) + } ActionId.SCREENSHOT -> if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { return listOf(Permission.ROOT) @@ -659,7 +705,11 @@ object ActionUtils { } ActionId.SECURE_LOCK_DEVICE -> return listOf(Permission.DEVICE_ADMIN) - ActionId.POWER_ON_OFF_DEVICE -> return listOf(Permission.ROOT) + ActionId.POWER_ON_OFF_DEVICE -> return if (isSystemBridgeSupported) { + emptyList() + } else { + listOf(Permission.ROOT) + } ActionId.DISMISS_ALL_NOTIFICATIONS, ActionId.DISMISS_MOST_RECENT_NOTIFICATION, @@ -672,7 +722,8 @@ object ActionUtils { ActionId.PHONE_CALL -> return listOf(Permission.CALL_PHONE) ActionId.ENABLE_BLUETOOTH, ActionId.DISABLE_BLUETOOTH, ActionId.TOGGLE_BLUETOOTH -> - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // On S_V2 and newer, the system bridge is used which means no permissions are required + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.S) { return listOf(Permission.FIND_NEARBY_DEVICES) } @@ -802,18 +853,17 @@ object ActionUtils { } fun ActionData.canBeHeldDown(): Boolean = when (this) { - is ActionData.InputKeyEvent -> !useShell - is ActionData.TapScreen -> Build.VERSION.SDK_INT >= Build.VERSION_CODES.O + is ActionData.InputKeyEvent -> true else -> false } fun ActionData.canUseImeToPerform(): Boolean = when (this) { - is ActionData.InputKeyEvent -> !useShell + is ActionData.InputKeyEvent -> true is ActionData.Text -> true else -> false } -fun ActionData.canUseShizukuToPerform(): Boolean = when (this) { +fun ActionData.canUseSystemBridgeToPerform(): Boolean = when (this) { is ActionData.InputKeyEvent -> true else -> false } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseActionScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseActionScreen.kt index fce1f2f42c..490ae30a1c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseActionScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseActionScreen.kt @@ -2,7 +2,6 @@ package io.github.sds100.keymapper.base.actions import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding @@ -20,10 +19,12 @@ import androidx.compose.material.icons.rounded.Bluetooth import androidx.compose.material.icons.rounded.Wifi import androidx.compose.material3.BottomAppBar import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment @@ -71,6 +72,7 @@ fun ChooseActionScreen( ) } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun ChooseActionScreen( modifier: Modifier = Modifier, @@ -83,6 +85,11 @@ private fun ChooseActionScreen( ) { Scaffold( modifier = modifier.displayCutoutPadding(), + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.choose_action_title)) }, + ) + }, bottomBar = { BottomAppBar( modifier = Modifier.imePadding(), @@ -113,33 +120,20 @@ private fun ChooseActionScreen( ), ) { - Column { - Text( - modifier = Modifier.padding( - start = 16.dp, - end = 16.dp, - top = 16.dp, - bottom = 8.dp, - ), - text = stringResource(R.string.choose_action_title), - style = MaterialTheme.typography.titleLarge, - ) - - when (state) { - State.Loading -> LoadingScreen(modifier = Modifier.fillMaxSize()) + when (state) { + State.Loading -> LoadingScreen(modifier = Modifier.fillMaxSize()) - is State.Data -> { - if (state.data.isEmpty()) { - EmptyScreen( - modifier = Modifier.fillMaxSize(), - ) - } else { - ListScreen( - modifier = Modifier.fillMaxSize(), - groups = state.data, - onClickAction = onClickAction, - ) - } + is State.Data -> { + if (state.data.isEmpty()) { + EmptyScreen( + modifier = Modifier.fillMaxSize(), + ) + } else { + ListScreen( + modifier = Modifier.fillMaxSize(), + groups = state.data, + onClickAction = onClickAction, + ) } } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseActionViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseActionViewModel.kt index 5961652602..6fcc481422 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseActionViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ChooseActionViewModel.kt @@ -211,7 +211,6 @@ class ChooseActionViewModel @Inject constructor( -> R.string.action_toggle_keyboard_message ActionId.SECURE_LOCK_DEVICE -> R.string.action_secure_lock_device_message - ActionId.POWER_ON_OFF_DEVICE -> R.string.action_power_on_off_device_message else -> null } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCase.kt new file mode 100644 index 0000000000..c5212a6585 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCase.kt @@ -0,0 +1,284 @@ +package io.github.sds100.keymapper.base.actions + +import dagger.hilt.android.scopes.ViewModelScoped +import io.github.sds100.keymapper.base.constraints.ConfigConstraintsUseCase +import io.github.sds100.keymapper.base.constraints.Constraint +import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapState +import io.github.sds100.keymapper.base.keymaps.GetDefaultKeyMapOptionsUseCase +import io.github.sds100.keymapper.base.keymaps.KeyMap +import io.github.sds100.keymapper.base.trigger.KeyEventTriggerKey +import io.github.sds100.keymapper.common.utils.State +import io.github.sds100.keymapper.common.utils.moveElement +import io.github.sds100.keymapper.data.Keys +import io.github.sds100.keymapper.data.repositories.PreferenceRepository +import io.github.sds100.keymapper.system.inputevents.KeyEventUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import java.util.LinkedList +import javax.inject.Inject + +@ViewModelScoped +class ConfigActionsUseCaseImpl @Inject constructor( + private val state: ConfigKeyMapState, + private val preferenceRepository: PreferenceRepository, + private val configConstraints: ConfigConstraintsUseCase, + defaultKeyMapOptionsUseCase: GetDefaultKeyMapOptionsUseCase, +) : ConfigActionsUseCase, GetDefaultKeyMapOptionsUseCase by defaultKeyMapOptionsUseCase { + + override val keyMap: StateFlow> = state.keyMap + + /** + * The most recently used is first. + */ + override val recentlyUsedActions: Flow> = + preferenceRepository.get(Keys.recentlyUsedActions) + .map(::getActionShortcuts) + .map { it.take(5) } + + override fun addAction(data: ActionData) { + state.update { keyMap -> + val newActionList = keyMap.actionList.toMutableList().apply { + add(createAction(keyMap, data)) + } + + preferenceRepository.update( + Keys.recentlyUsedActions, + { old -> + val oldList: List = if (old == null) { + emptyList() + } else { + Json.decodeFromString>(old) + } + + val newShortcuts = LinkedList(oldList) + .also { it.addFirst(data) } + .distinct() + + Json.encodeToString(newShortcuts) + }, + ) + + keyMap.copy(actionList = newActionList) + } + } + + override fun moveAction(fromIndex: Int, toIndex: Int) { + updateActionList { actionList -> + actionList.toMutableList().apply { + moveElement(fromIndex, toIndex) + } + } + } + + override fun removeAction(uid: String) { + updateActionList { actionList -> + actionList.toMutableList().apply { + removeAll { it.uid == uid } + } + } + } + + override fun setActionData(uid: String, data: ActionData) { + updateActionList { actionList -> + actionList.map { action -> + if (action.uid == uid) { + action.copy(data = data) + } else { + action + } + } + } + } + + override fun setActionRepeatEnabled(uid: String, repeat: Boolean) { + setActionOption(uid) { action -> action.copy(repeat = repeat) } + } + + override fun setActionRepeatRate(uid: String, repeatRate: Int) { + setActionOption(uid) { action -> + if (repeatRate == defaultRepeatRate.value) { + action.copy(repeatRate = null) + } else { + action.copy(repeatRate = repeatRate) + } + } + } + + override fun setActionRepeatDelay(uid: String, repeatDelay: Int) { + setActionOption(uid) { action -> + if (repeatDelay == defaultRepeatDelay.value) { + action.copy(repeatDelay = null) + } else { + action.copy(repeatDelay = repeatDelay) + } + } + } + + override fun setActionRepeatLimit(uid: String, repeatLimit: Int) { + setActionOption(uid) { action -> + if (action.repeatMode == RepeatMode.LIMIT_REACHED) { + if (repeatLimit == 1) { + action.copy(repeatLimit = null) + } else { + action.copy(repeatLimit = repeatLimit) + } + } else { + if (repeatLimit == Int.MAX_VALUE) { + action.copy(repeatLimit = null) + } else { + action.copy(repeatLimit = repeatLimit) + } + } + } + } + + override fun setActionHoldDownEnabled(uid: String, holdDown: Boolean) = + setActionOption(uid) { it.copy(holdDown = holdDown) } + + override fun setActionHoldDownDuration(uid: String, holdDownDuration: Int) { + setActionOption(uid) { action -> + if (holdDownDuration == defaultHoldDownDuration.value) { + action.copy(holdDownDuration = null) + } else { + action.copy(holdDownDuration = holdDownDuration) + } + } + } + + override fun setActionStopRepeatingWhenTriggerPressedAgain(uid: String) = + setActionOption(uid) { it.copy(repeatMode = RepeatMode.TRIGGER_PRESSED_AGAIN) } + + override fun setActionStopRepeatingWhenLimitReached(uid: String) = + setActionOption(uid) { it.copy(repeatMode = RepeatMode.LIMIT_REACHED) } + + override fun setActionStopRepeatingWhenTriggerReleased(uid: String) = + setActionOption(uid) { it.copy(repeatMode = RepeatMode.TRIGGER_RELEASED) } + + override fun setActionStopHoldingDownWhenTriggerPressedAgain(uid: String, enabled: Boolean) = + setActionOption(uid) { it.copy(stopHoldDownWhenTriggerPressedAgain = enabled) } + + override fun setActionMultiplier(uid: String, multiplier: Int) { + setActionOption(uid) { action -> + if (multiplier == 1) { + action.copy(multiplier = null) + } else { + action.copy(multiplier = multiplier) + } + } + } + + override fun setDelayBeforeNextAction(uid: String, delay: Int) { + setActionOption(uid) { action -> + if (delay == 0) { + action.copy(delayBeforeNextAction = null) + } else { + action.copy(delayBeforeNextAction = delay) + } + } + } + + private suspend fun getActionShortcuts(json: String?): List { + if (json == null) { + return emptyList() + } + + try { + return withContext(Dispatchers.Default) { + val list = Json.decodeFromString>(json) + + list.distinct() + } + } catch (_: Exception) { + preferenceRepository.set(Keys.recentlyUsedActions, null) + return emptyList() + } + } + + private fun createAction(keyMap: KeyMap, data: ActionData): Action { + var holdDown = false + var repeat = false + + if (data is ActionData.InputKeyEvent) { + val containsDpadKey: Boolean = + keyMap.trigger.keys + .mapNotNull { it as? KeyEventTriggerKey } + .any { KeyEventUtils.isDpadKeyCode(it.keyCode) } + + if (KeyEventUtils.isModifierKey(data.keyCode) || containsDpadKey) { + holdDown = true + repeat = false + } else { + repeat = true + } + } + + if (data is ActionData.Volume.Down || data is ActionData.Volume.Up || data is ActionData.Volume.Stream) { + repeat = true + } + + if (data is ActionData.AnswerCall) { + configConstraints.addConstraint(Constraint.PhoneRinging()) + } + + if (data is ActionData.EndCall) { + configConstraints.addConstraint(Constraint.InPhoneCall()) + } + + return Action( + data = data, + repeat = repeat, + holdDown = holdDown, + ) + } + + private fun updateActionList(block: (actionList: List) -> List) { + state.update { it.copy(actionList = block(it.actionList)) } + } + + private fun setActionOption( + uid: String, + block: (action: Action) -> Action, + ) { + state.update { keyMap -> + val newActionList = keyMap.actionList.map { action -> + if (action.uid == uid) { + block.invoke(action) + } else { + action + } + } + + keyMap.copy( + actionList = newActionList, + ) + } + } +} + +interface ConfigActionsUseCase : GetDefaultKeyMapOptionsUseCase { + val keyMap: StateFlow> + + fun addAction(data: ActionData) + fun moveAction(fromIndex: Int, toIndex: Int) + fun removeAction(uid: String) + + val recentlyUsedActions: Flow> + fun setActionData(uid: String, data: ActionData) + fun setActionMultiplier(uid: String, multiplier: Int) + fun setDelayBeforeNextAction(uid: String, delay: Int) + fun setActionRepeatRate(uid: String, repeatRate: Int) + fun setActionRepeatLimit(uid: String, repeatLimit: Int) + fun setActionStopRepeatingWhenTriggerPressedAgain(uid: String) + fun setActionStopRepeatingWhenLimitReached(uid: String) + fun setActionRepeatEnabled(uid: String, repeat: Boolean) + fun setActionRepeatDelay(uid: String, repeatDelay: Int) + fun setActionHoldDownEnabled(uid: String, holdDown: Boolean) + fun setActionHoldDownDuration(uid: String, holdDownDuration: Int) + fun setActionStopRepeatingWhenTriggerReleased(uid: String) + + fun setActionStopHoldingDownWhenTriggerPressedAgain(uid: String, enabled: Boolean) +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt index 0e1a4ca5d2..f0b45b975e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/ConfigActionsViewModel.kt @@ -1,7 +1,9 @@ package io.github.sds100.keymapper.base.actions +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel import io.github.sds100.keymapper.base.R -import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapUseCase import io.github.sds100.keymapper.base.keymaps.KeyMap import io.github.sds100.keymapper.base.keymaps.ShortcutModel import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase @@ -26,7 +28,6 @@ import io.github.sds100.keymapper.common.utils.mapData import io.github.sds100.keymapper.common.utils.onFailure import io.github.sds100.keymapper.system.SystemError import io.github.sds100.keymapper.system.permissions.Permission -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -39,24 +40,26 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import javax.inject.Inject -class ConfigActionsViewModel( - private val coroutineScope: CoroutineScope, +@HiltViewModel +class ConfigActionsViewModel @Inject constructor( private val displayAction: DisplayActionUseCase, private val createAction: CreateActionUseCase, private val testAction: TestActionUseCase, - private val config: ConfigKeyMapUseCase, + private val config: ConfigActionsUseCase, private val onboarding: OnboardingUseCase, resourceProvider: ResourceProvider, navigationProvider: NavigationProvider, dialogProvider: DialogProvider, -) : ActionOptionsBottomSheetCallback, +) : ViewModel(), + ActionOptionsBottomSheetCallback, ResourceProvider by resourceProvider, DialogProvider by dialogProvider, NavigationProvider by navigationProvider { val createActionDelegate = - CreateActionDelegate(coroutineScope, createAction, this, this, this) + CreateActionDelegate(viewModelScope, createAction, this, this, this) private val uiHelper = ActionUiHelper(displayAction, resourceProvider) private val _state = MutableStateFlow>(State.Loading) @@ -65,15 +68,15 @@ class ConfigActionsViewModel( private val shortcuts: StateFlow>> = config.recentlyUsedActions.map { actions -> actions.map(::buildShortcut).toSet() - }.stateIn(coroutineScope, SharingStarted.Lazily, emptySet()) + }.stateIn(viewModelScope, SharingStarted.Lazily, emptySet()) val actionOptionsUid = MutableStateFlow(null) val actionOptionsState: StateFlow = combine(config.keyMap, actionOptionsUid, transform = ::buildOptionsState) - .stateIn(coroutineScope, SharingStarted.Lazily, null) + .stateIn(viewModelScope, SharingStarted.Lazily, null) private val actionErrorSnapshot: StateFlow = - displayAction.actionErrorSnapshot.stateIn(coroutineScope, SharingStarted.Lazily, null) + displayAction.actionErrorSnapshot.stateIn(viewModelScope, SharingStarted.Lazily, null) init { combine( @@ -85,9 +88,9 @@ class ConfigActionsViewModel( _state.value = keyMapState.mapData { keyMap -> buildState(keyMap, shortcuts, errorSnapshot, showDeviceDescriptors) } - }.launchIn(coroutineScope) + }.launchIn(viewModelScope) - coroutineScope.launch { + viewModelScope.launch { createActionDelegate.actionResult.filterNotNull().collect { action -> val actionUid = actionOptionsUid.value ?: return@collect config.setActionData(actionUid, action) @@ -101,19 +104,19 @@ class ConfigActionsViewModel( } fun onClickShortcut(action: ActionData) { - coroutineScope.launch { + viewModelScope.launch { config.addAction(action) } } fun onFixError(actionUid: String) { - coroutineScope.launch { + viewModelScope.launch { val actionData = getActionData(actionUid) ?: return@launch val error = actionErrorSnapshot.filterNotNull().first().getError(actionData) ?: return@launch if (error == SystemError.PermissionDenied(Permission.ACCESS_NOTIFICATION_POLICY)) { - coroutineScope.launch { + viewModelScope.launch { ViewModelHelper.showDialogExplainingDndAccessBeingUnavailable( resourceProvider = this@ConfigActionsViewModel, dialogProvider = this@ConfigActionsViewModel, @@ -134,17 +137,13 @@ class ConfigActionsViewModel( } fun onAddActionClick() { - coroutineScope.launch { + viewModelScope.launch { val actionData = navigate("add_action", NavDestination.ChooseAction) ?: return@launch - val showInstallShizukuPrompt = onboarding.showInstallShizukuPrompt(actionData) val showInstallGuiKeyboardPrompt = onboarding.showInstallGuiKeyboardPrompt(actionData) when { - showInstallShizukuPrompt && showInstallGuiKeyboardPrompt -> - promptToInstallShizukuOrGuiKeyboard() - showInstallGuiKeyboardPrompt -> promptToInstallGuiKeyboard() } @@ -165,7 +164,7 @@ class ConfigActionsViewModel( } fun onTestClick(actionUid: String) { - coroutineScope.launch { + viewModelScope.launch { val actionData = getActionData(actionUid) ?: return@launch attemptTestAction(actionData) } @@ -173,7 +172,7 @@ class ConfigActionsViewModel( override fun onEditClick() { val actionUid = actionOptionsUid.value ?: return - coroutineScope.launch { + viewModelScope.launch { val keyMap = config.keyMap.first().dataOrNull() ?: return@launch val oldAction = keyMap.actionList.find { it.uid == actionUid } ?: return@launch @@ -183,11 +182,12 @@ class ConfigActionsViewModel( override fun onReplaceClick() { val actionUid = actionOptionsUid.value ?: return - coroutineScope.launch { + viewModelScope.launch { + actionOptionsUid.update { null } + val newActionData = navigate("replace_action", NavDestination.ChooseAction) ?: return@launch - actionOptionsUid.update { null } config.setActionData(actionUid, newActionData) } } @@ -311,102 +311,6 @@ class ConfigActionsViewModel( } } - private suspend fun promptToInstallShizukuOrGuiKeyboard() { - if (onboarding.isTvDevice()) { - val chooseSolutionDialog = DialogModel.Alert( - title = getText(R.string.dialog_title_install_shizuku_or_leanback_keyboard), - message = getText(R.string.dialog_message_install_shizuku_or_leanback_keyboard), - positiveButtonText = getString(R.string.dialog_button_install_shizuku), - negativeButtonText = getString(R.string.dialog_button_install_leanback_keyboard), - neutralButtonText = getString(R.string.dialog_button_install_nothing), - ) - - val chooseSolutionResponse = - showDialog("choose_solution", chooseSolutionDialog) ?: return - - when (chooseSolutionResponse) { - // install shizuku - DialogResponse.POSITIVE -> { - navigate("shizuku", NavDestination.ShizukuSettings) - onboarding.neverShowGuiKeyboardPromptsAgain() - - return - } - // do nothing - DialogResponse.NEUTRAL -> { - onboarding.neverShowGuiKeyboardPromptsAgain() - return - } - - // download leanback keyboard - DialogResponse.NEGATIVE -> { - val chooseAppStoreDialog = DialogModel.ChooseAppStore( - title = getString(R.string.dialog_title_choose_download_leanback_keyboard), - message = getString(R.string.dialog_message_choose_download_leanback_keyboard), - model = ChooseAppStoreModel( - githubLink = getString(R.string.url_github_keymapper_leanback_keyboard), - ), - positiveButtonText = getString(R.string.pos_never_show_again), - negativeButtonText = getString(R.string.neg_cancel), - ) - - val response = showDialog("install_leanback_keyboard", chooseAppStoreDialog) - - if (response == DialogResponse.POSITIVE) { - onboarding.neverShowGuiKeyboardPromptsAgain() - } - } - } - } else { - val chooseSolutionDialog = DialogModel.Alert( - title = getText(R.string.dialog_title_install_shizuku_or_gui_keyboard), - message = getText(R.string.dialog_message_install_shizuku_or_gui_keyboard), - positiveButtonText = getString(R.string.dialog_button_install_shizuku), - negativeButtonText = getString(R.string.dialog_button_install_gui_keyboard), - neutralButtonText = getString(R.string.dialog_button_install_nothing), - ) - - val chooseSolutionResponse = - showDialog("choose_solution", chooseSolutionDialog) ?: return - - when (chooseSolutionResponse) { - // install shizuku - DialogResponse.POSITIVE -> { - navigate("shizuku_error", NavDestination.ShizukuSettings) - onboarding.neverShowGuiKeyboardPromptsAgain() - - return - } - // do nothing - DialogResponse.NEUTRAL -> { - onboarding.neverShowGuiKeyboardPromptsAgain() - return - } - - // download gui keyboard - DialogResponse.NEGATIVE -> { - val chooseAppStoreDialog = DialogModel.ChooseAppStore( - title = getString(R.string.dialog_title_choose_download_gui_keyboard), - message = getString(R.string.dialog_message_choose_download_gui_keyboard), - model = ChooseAppStoreModel( - playStoreLink = getString(R.string.url_play_store_keymapper_gui_keyboard), - fdroidLink = getString(R.string.url_fdroid_keymapper_gui_keyboard), - githubLink = getString(R.string.url_github_keymapper_gui_keyboard), - ), - positiveButtonText = getString(R.string.pos_never_show_again), - negativeButtonText = getString(R.string.neg_cancel), - ) - - val response = showDialog("install_gui_keyboard", chooseAppStoreDialog) - - if (response == DialogResponse.POSITIVE) { - onboarding.neverShowGuiKeyboardPromptsAgain() - } - } - } - } - } - private fun buildShortcut(action: ActionData): ShortcutModel { return ShortcutModel( icon = uiHelper.getIcon(action), diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCase.kt index d165b5025b..eec3255131 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCase.kt @@ -1,18 +1,22 @@ package io.github.sds100.keymapper.base.actions +import android.os.Build import io.github.sds100.keymapper.base.actions.sound.SoundsManager import io.github.sds100.keymapper.common.BuildConfigProvider +import io.github.sds100.keymapper.data.Keys +import io.github.sds100.keymapper.data.repositories.PreferenceRepository +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager import io.github.sds100.keymapper.system.apps.PackageManagerAdapter import io.github.sds100.keymapper.system.camera.CameraAdapter import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter import io.github.sds100.keymapper.system.permissions.PermissionAdapter import io.github.sds100.keymapper.system.permissions.SystemFeatureAdapter import io.github.sds100.keymapper.system.ringtones.RingtoneAdapter -import io.github.sds100.keymapper.system.shizuku.ShizukuAdapter import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import javax.inject.Inject @@ -26,9 +30,10 @@ class GetActionErrorUseCaseImpl @Inject constructor( private val systemFeatureAdapter: SystemFeatureAdapter, private val cameraAdapter: CameraAdapter, private val soundsManager: SoundsManager, - private val shizukuAdapter: ShizukuAdapter, private val ringtoneAdapter: RingtoneAdapter, private val buildConfigProvider: BuildConfigProvider, + private val systemBridgeConnectionManager: SystemBridgeConnectionManager, + private val preferenceRepository: PreferenceRepository, ) : GetActionErrorUseCase { private val invalidateActionErrors = merge( @@ -37,9 +42,15 @@ class GetActionErrorUseCaseImpl @Inject constructor( inputMethodAdapter.inputMethods.drop(1).map { }, permissionAdapter.onPermissionsUpdate, soundsManager.soundFiles.drop(1).map { }, - shizukuAdapter.isStarted.drop(1).map { }, - shizukuAdapter.isInstalled.drop(1).map { }, packageManagerAdapter.onPackagesChanged, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + merge( + systemBridgeConnectionManager.connectionState.drop(1).map { }, + preferenceRepository.get(Keys.isSystemBridgeUsed), + ) + } else { + emptyFlow() + }, ) override val actionErrorSnapshot: Flow = channelFlow { @@ -50,17 +61,20 @@ class GetActionErrorUseCaseImpl @Inject constructor( } } - private fun createSnapshot(): ActionErrorSnapshot = LazyActionErrorSnapshot( - packageManagerAdapter, - inputMethodAdapter, - permissionAdapter, - systemFeatureAdapter, - cameraAdapter, - soundsManager, - shizukuAdapter, - ringtoneAdapter, - buildConfigProvider, - ) + private fun createSnapshot(): ActionErrorSnapshot { + return LazyActionErrorSnapshot( + packageManagerAdapter, + inputMethodAdapter, + permissionAdapter, + systemFeatureAdapter, + cameraAdapter, + soundsManager, + ringtoneAdapter, + buildConfigProvider, + systemBridgeConnectionManager, + preferenceRepository, + ) + } } interface GetActionErrorUseCase { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt index 690635ed06..ce156b0299 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCase.kt @@ -10,6 +10,8 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.actions.sound.SoundsManager +import io.github.sds100.keymapper.base.input.InjectKeyEventModel +import io.github.sds100.keymapper.base.input.InputEventHub import io.github.sds100.keymapper.base.system.accessibility.AccessibilityNodeAction import io.github.sds100.keymapper.base.system.accessibility.AccessibilityNodeModel import io.github.sds100.keymapper.base.system.accessibility.IAccessibilityService @@ -17,7 +19,7 @@ import io.github.sds100.keymapper.base.system.inputmethod.ImeInputEventInjector import io.github.sds100.keymapper.base.system.navigation.OpenMenuHelper import io.github.sds100.keymapper.base.utils.getFullMessage import io.github.sds100.keymapper.base.utils.ui.ResourceProvider -import io.github.sds100.keymapper.common.utils.InputEventType +import io.github.sds100.keymapper.common.utils.InputEventAction import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Orientation @@ -35,6 +37,8 @@ import io.github.sds100.keymapper.common.utils.withFlag import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.PreferenceDefaults import io.github.sds100.keymapper.data.repositories.PreferenceRepository +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState import io.github.sds100.keymapper.system.airplanemode.AirplaneModeAdapter import io.github.sds100.keymapper.system.apps.AppShortcutAdapter import io.github.sds100.keymapper.system.apps.PackageManagerAdapter @@ -44,8 +48,8 @@ import io.github.sds100.keymapper.system.devices.DevicesAdapter import io.github.sds100.keymapper.system.display.DisplayAdapter import io.github.sds100.keymapper.system.files.FileAdapter import io.github.sds100.keymapper.system.files.FileUtils -import io.github.sds100.keymapper.system.inputevents.InputEventUtils -import io.github.sds100.keymapper.system.inputmethod.InputKeyModel +import io.github.sds100.keymapper.system.inputevents.KeyEventUtils +import io.github.sds100.keymapper.system.inputevents.Scancode import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter import io.github.sds100.keymapper.system.intents.IntentAdapter import io.github.sds100.keymapper.system.intents.IntentTarget @@ -55,30 +59,22 @@ import io.github.sds100.keymapper.system.network.NetworkAdapter import io.github.sds100.keymapper.system.nfc.NfcAdapter import io.github.sds100.keymapper.system.notifications.NotificationReceiverAdapter import io.github.sds100.keymapper.system.notifications.NotificationServiceEvent -import io.github.sds100.keymapper.system.permissions.Permission -import io.github.sds100.keymapper.system.permissions.PermissionAdapter import io.github.sds100.keymapper.system.phone.PhoneAdapter import io.github.sds100.keymapper.system.popup.ToastAdapter import io.github.sds100.keymapper.system.ringtones.RingtoneAdapter import io.github.sds100.keymapper.system.root.SuAdapter import io.github.sds100.keymapper.system.shell.ShellAdapter -import io.github.sds100.keymapper.system.shizuku.ShizukuInputEventInjector import io.github.sds100.keymapper.system.url.OpenUrlAdapter import io.github.sds100.keymapper.system.volume.RingerMode import io.github.sds100.keymapper.system.volume.VolumeAdapter import io.github.sds100.keymapper.system.volume.VolumeStream -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn import timber.log.Timber class PerformActionsUseCaseImpl @AssistedInject constructor( - private val appCoroutineScope: CoroutineScope, @Assisted private val service: IAccessibilityService, private val inputMethodAdapter: InputMethodAdapter, @@ -87,7 +83,6 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( private val shell: ShellAdapter, private val intentAdapter: IntentAdapter, private val getActionErrorUseCase: GetActionErrorUseCase, - @Assisted private val keyMapperImeMessenger: ImeInputEventInjector, private val packageManagerAdapter: PackageManagerAdapter, private val appShortcutAdapter: AppShortcutAdapter, @@ -106,42 +101,30 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( private val openUrlAdapter: OpenUrlAdapter, private val resourceProvider: ResourceProvider, private val soundsManager: SoundsManager, - private val permissionAdapter: PermissionAdapter, private val notificationReceiverAdapter: NotificationReceiverAdapter, private val ringtoneAdapter: RingtoneAdapter, private val settingsRepository: PreferenceRepository, + private val inputEventHub: InputEventHub, + private val systemBridgeConnectionManager: SystemBridgeConnectionManager, ) : PerformActionsUseCase { @AssistedFactory interface Factory { fun create( accessibilityService: IAccessibilityService, - imeInputEventInjector: ImeInputEventInjector, ): PerformActionsUseCaseImpl } - private val shizukuInputEventInjector: ShizukuInputEventInjector = ShizukuInputEventInjector() - private val openMenuHelper by lazy { OpenMenuHelper( - suAdapter, service, - shizukuInputEventInjector, - permissionAdapter, - appCoroutineScope, + inputEventHub, ) } - /** - * Cache this so we aren't checking every time a key event must be inputted. - */ - private val inputKeyEventsWithShizuku: StateFlow = - permissionAdapter.isGrantedFlow(Permission.SHIZUKU) - .stateIn(appCoroutineScope, SharingStarted.Eagerly, false) - override suspend fun perform( action: ActionData, - inputEventType: InputEventType, + inputEventAction: InputEventAction, keyMetaState: Int, ) { /** @@ -167,32 +150,32 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( // See issue #1683. Some apps ignore key events which do not have a source. val source = when { - InputEventUtils.isDpadKeyCode(action.keyCode) -> InputDevice.SOURCE_DPAD - InputEventUtils.isGamepadButton(action.keyCode) -> InputDevice.SOURCE_GAMEPAD + KeyEventUtils.isDpadKeyCode(action.keyCode) -> InputDevice.SOURCE_DPAD + KeyEventUtils.isGamepadButton(action.keyCode) -> InputDevice.SOURCE_GAMEPAD else -> InputDevice.SOURCE_KEYBOARD } - val model = InputKeyModel( + val firstInputAction = if (inputEventAction == InputEventAction.UP) { + KeyEvent.ACTION_UP + } else { + KeyEvent.ACTION_DOWN + } + + val model = InjectKeyEventModel( keyCode = action.keyCode, - inputType = inputEventType, + action = firstInputAction, metaState = keyMetaState.withFlag(action.metaState), deviceId = deviceId, source = source, + repeatCount = 0, + scanCode = 0, ) - result = when { - inputKeyEventsWithShizuku.value -> { - shizukuInputEventInjector.inputKeyEvent(model) - Success(Unit) - } - - action.useShell -> suAdapter.execute("input keyevent ${model.keyCode}") - - else -> { - keyMapperImeMessenger.inputKeyEvent(model) - - Success(Unit) - } + if (inputEventAction == InputEventAction.DOWN_UP) { + result = inputEventHub.injectKeyEvent(model) + .then { inputEventHub.injectKeyEvent(model.copy(action = KeyEvent.ACTION_UP)) } + } else { + result = inputEventHub.injectKeyEvent(model) } } @@ -335,7 +318,7 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( } is ActionData.TapScreen -> { - result = service.tapScreen(action.x, action.y, inputEventType) + result = service.tapScreen(action.x, action.y, inputEventAction) } is ActionData.SwipeScreen -> { @@ -346,7 +329,7 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( action.yEnd, action.fingerCount, action.duration, - inputEventType, + inputEventAction, ) } @@ -358,7 +341,7 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( action.pinchType, action.fingerCount, action.duration, - inputEventType, + inputEventAction, ) } @@ -805,7 +788,22 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( } is ActionData.ScreenOnOff -> { - result = suAdapter.execute("input keyevent ${KeyEvent.KEYCODE_POWER}") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && + systemBridgeConnectionManager.connectionState.value is SystemBridgeConnectionState.Connected + ) { + val model = InjectKeyEventModel( + keyCode = KeyEvent.KEYCODE_POWER, + action = KeyEvent.ACTION_DOWN, + metaState = 0, + deviceId = -1, + scanCode = Scancode.KEY_POWER, + source = InputDevice.SOURCE_UNKNOWN, + ) + result = inputEventHub.injectKeyEvent(model) + .then { inputEventHub.injectKeyEvent(model.copy(action = KeyEvent.ACTION_UP)) } + } else { + result = suAdapter.execute("input keyevent ${KeyEvent.KEYCODE_POWER}") + } } is ActionData.SecureLock -> { @@ -881,9 +879,9 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( } when (result) { - is Success -> Timber.d("Performed action $action, input event type: $inputEventType, key meta state: $keyMetaState") + is Success -> Timber.d("Performed action $action, input event type: $inputEventAction, key meta state: $keyMetaState") is KMError -> Timber.d( - "Failed to perform action $action, reason: ${result.getFullMessage(resourceProvider)}, action: $action, input event type: $inputEventType, key meta state: $keyMetaState", + "Failed to perform action $action, reason: ${result.getFullMessage(resourceProvider)}, action: $action, input event type: $inputEventAction, key meta state: $keyMetaState", ) } @@ -913,7 +911,7 @@ class PerformActionsUseCaseImpl @AssistedInject constructor( if (action.device?.descriptor == null) { // automatically select a game controller as the input device for game controller key events - if (InputEventUtils.isGamepadKeyCode(action.keyCode)) { + if (KeyEventUtils.isGamepadKeyCode(action.keyCode)) { devicesAdapter.connectedInputDevices.value.ifIsData { inputDevices -> val device = inputDevices.find { it.isGameController } @@ -1022,7 +1020,7 @@ interface PerformActionsUseCase { suspend fun perform( action: ActionData, - inputEventType: InputEventType = InputEventType.DOWN_UP, + inputEventAction: InputEventAction = InputEventAction.DOWN_UP, keyMetaState: Int = 0, ) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/ChooseKeyCodeViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/ChooseKeyCodeViewModel.kt index 67987c742a..0502fc93ca 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/ChooseKeyCodeViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/ChooseKeyCodeViewModel.kt @@ -8,7 +8,7 @@ import io.github.sds100.keymapper.base.utils.filterByQuery import io.github.sds100.keymapper.base.utils.ui.DefaultSimpleListItem import io.github.sds100.keymapper.base.utils.ui.SimpleListItemOld import io.github.sds100.keymapper.common.utils.State -import io.github.sds100.keymapper.system.inputevents.InputEventUtils +import io.github.sds100.keymapper.system.inputevents.KeyEventUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -31,7 +31,7 @@ class ChooseKeyCodeViewModel @Inject constructor() : ViewModel() { private val allListItems = flow { withContext(Dispatchers.Default) { - InputEventUtils.getKeyCodes().sorted().map { keyCode -> + KeyEventUtils.getKeyCodes().sorted().map { keyCode -> DefaultSimpleListItem( id = keyCode.toString(), title = "$keyCode \t\t ${KeyEvent.keyCodeToString(keyCode)}", diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/ConfigKeyEventActionViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/ConfigKeyEventActionViewModel.kt index 02fd7545a0..d8d1e89a70 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/ConfigKeyEventActionViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/ConfigKeyEventActionViewModel.kt @@ -7,7 +7,7 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.actions.ActionData -import io.github.sds100.keymapper.base.utils.InputEventStrings +import io.github.sds100.keymapper.base.utils.KeyCodeStrings import io.github.sds100.keymapper.base.utils.getFullMessage import io.github.sds100.keymapper.base.utils.navigation.NavDestination import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider @@ -15,6 +15,8 @@ import io.github.sds100.keymapper.base.utils.navigation.NavigationProviderImpl import io.github.sds100.keymapper.base.utils.navigation.navigate import io.github.sds100.keymapper.base.utils.ui.CheckBoxListItem import io.github.sds100.keymapper.base.utils.ui.ResourceProvider +import io.github.sds100.keymapper.common.utils.InputDeviceInfo +import io.github.sds100.keymapper.common.utils.InputDeviceUtils import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success @@ -26,8 +28,6 @@ import io.github.sds100.keymapper.common.utils.minusFlag import io.github.sds100.keymapper.common.utils.success import io.github.sds100.keymapper.common.utils.valueOrNull import io.github.sds100.keymapper.common.utils.withFlag -import io.github.sds100.keymapper.system.devices.InputDeviceInfo -import io.github.sds100.keymapper.system.devices.InputDeviceUtils import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -105,7 +105,6 @@ class ConfigKeyEventActionViewModel @Inject constructor( keyEventState.value = KeyEventState( Success(action.keyCode), inputDevice, - useShell = action.useShell, metaState = action.metaState, ) } @@ -121,10 +120,6 @@ class ConfigKeyEventActionViewModel @Inject constructor( keyEventState.value = keyEventState.value.copy(keyCode = keyCodeState) } - fun setUseShell(checked: Boolean) { - keyEventState.value = keyEventState.value.copy(useShell = checked) - } - @SuppressLint("NullSafeMutableLiveData") fun chooseNoDevice() { keyEventState.value = keyEventState.value.copy(chosenDevice = null) @@ -157,7 +152,6 @@ class ConfigKeyEventActionViewModel @Inject constructor( ActionData.InputKeyEvent( keyCode = keyCode, metaState = keyEventState.value.metaState, - useShell = keyEventState.value.useShell, device = device, ), ) @@ -171,7 +165,6 @@ class ConfigKeyEventActionViewModel @Inject constructor( ): ConfigKeyEventUiState { val keyCode = state.keyCode val metaState = state.metaState - val useShell = state.useShell val chosenDevice = state.chosenDevice val keyCodeString = when (keyCode) { @@ -190,7 +183,7 @@ class ConfigKeyEventActionViewModel @Inject constructor( onError = { "" }, ) - val modifierListItems = InputEventStrings.MODIFIER_LABELS.map { (modifier, label) -> + val modifierListItems = KeyCodeStrings.MODIFIER_LABELS.map { (modifier, label) -> CheckBoxListItem( id = modifier.toString(), label = getString(label), @@ -226,9 +219,8 @@ class ConfigKeyEventActionViewModel @Inject constructor( keyCodeErrorMessage = keyCode.errorOrNull()?.getFullMessage(this), keyCodeLabel = keyCodeLabel, showKeyCodeLabel = keyCode.isSuccess, - isUseShellChecked = useShell, - isDevicePickerShown = !useShell, - isModifierListShown = !useShell, + isDevicePickerShown = true, + isModifierListShown = true, modifierListItems = modifierListItems, isDoneButtonEnabled = keyCode.isSuccess, deviceListItems = deviceListItems, @@ -239,7 +231,6 @@ class ConfigKeyEventActionViewModel @Inject constructor( private data class KeyEventState( val keyCode: KMResult = KMError.EmptyText, val chosenDevice: InputDeviceInfo? = null, - val useShell: Boolean = false, val metaState: Int = 0, ) } @@ -249,7 +240,6 @@ data class ConfigKeyEventUiState( val keyCodeErrorMessage: String?, val keyCodeLabel: String, val showKeyCodeLabel: Boolean, - val isUseShellChecked: Boolean, val isDevicePickerShown: Boolean, val isModifierListShown: Boolean, val modifierListItems: List, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/ConfigKeyEventUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/ConfigKeyEventUseCase.kt index a304220fab..417ebb3066 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/ConfigKeyEventUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/actions/keyevent/ConfigKeyEventUseCase.kt @@ -1,10 +1,10 @@ package io.github.sds100.keymapper.base.actions.keyevent +import io.github.sds100.keymapper.common.utils.InputDeviceInfo import io.github.sds100.keymapper.common.utils.State import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.system.devices.DevicesAdapter -import io.github.sds100.keymapper.system.devices.InputDeviceInfo import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import javax.inject.Inject diff --git a/base/src/main/java/io/github/sds100/keymapper/base/backup/BackupManager.kt b/base/src/main/java/io/github/sds100/keymapper/base/backup/BackupManager.kt index 25427c4000..0b72a2ceaf 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/backup/BackupManager.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/backup/BackupManager.kt @@ -25,7 +25,7 @@ import io.github.sds100.keymapper.common.utils.State import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.common.utils.TreeNode import io.github.sds100.keymapper.common.utils.UuidGenerator -import io.github.sds100.keymapper.common.utils.breadFirstTraversal +import io.github.sds100.keymapper.common.utils.breadthFirstTraversal import io.github.sds100.keymapper.common.utils.onFailure import io.github.sds100.keymapper.common.utils.then import io.github.sds100.keymapper.data.Keys @@ -465,16 +465,25 @@ class BackupManagerImpl @Inject constructor( .map { it.uid } .toSet() - val groupUids = backupContent.groups.map { it.uid }.toMutableSet() + val groupUids = + backupContent.groups.map { it.uid }.toMutableSet().plus(existingGroupUids) - groupUids.addAll(existingGroupUids) + // Set the parents to null of any groups that have a missing parent. + val backupGroups = backupContent.groups.map { group -> + if (groupUids.contains(group.parentUid)) { + group + } else { + group.copy(parentUid = null) + } + } - // Group parents must be restored first so an SqliteConstraintException - // is not thrown when restoring a child group. - val groupRestoreTrees = buildGroupTrees(backupContent.groups) + val groupRestoreTrees = buildGroupTrees(backupGroups) for (tree in groupRestoreTrees) { - tree.breadFirstTraversal { group -> + // Do breadth first traversal because group parents must be restored + // first so an SqliteConstraintException + // is not thrown when restoring a child group. + tree.breadthFirstTraversal { group -> restoreGroup(group, currentTime, groupUids, existingGroupUids) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/compose/ComposeColors.kt b/base/src/main/java/io/github/sds100/keymapper/base/compose/ComposeColors.kt index 7746f70135..0a2c8c7518 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/compose/ComposeColors.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/compose/ComposeColors.kt @@ -45,6 +45,14 @@ object ComposeColors { val onGreenLight = Color(0xFFFFFFFF) val greenContainerLight = Color(0xFFBCF0B4) val onGreenContainerLight = Color(0xFF235024) + val magiskTealLight = Color(0xFF008072) + val onMagiskTealLight = Color(0xFFFFFFFF) + val shizukuBlueLight = Color(0xFF4556B7) + val onShizukuBlueLight = Color(0xFFFFFFFF) + val orangeLight = Color(0xFF8B5000) + val onOrangeLight = Color(0xFFFFFFFF) + val orangeContainerLight = Color(0xFFFFA643) + val onOrangeContainerLight = Color(0xFF452500) val primaryDark = Color(0xFFAAC7FF) val onPrimaryDark = Color(0xFF0A305F) @@ -87,4 +95,12 @@ object ComposeColors { val onGreenDark = Color(0xFF0A390F) val greenContainerDark = Color(0xFF235024) val onGreenContainerDark = Color(0xFFBCF0B4) + val magiskTealDark = Color(0xFF009B8C) + val onMagiskTealDark = Color(0xFFFFFFFF) + val shizukuBlueDark = Color(0xFFB7C4F4) + val onShizukuBlueDark = Color(0xFF0A305F) + val orangeDark = Color(0xFFFFCA97) + val onOrangeDark = Color(0xFF4A2800) + val orangeContainerDark = Color(0xFFF69300) + val onOrangeContainerDark = Color(0xFF331A00) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/compose/ComposeCustomColors.kt b/base/src/main/java/io/github/sds100/keymapper/base/compose/ComposeCustomColors.kt index 49d7638df8..3e9dc7ddc2 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/compose/ComposeCustomColors.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/compose/ComposeCustomColors.kt @@ -1,7 +1,19 @@ package io.github.sds100.keymapper.base.compose +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable import androidx.compose.ui.graphics.Color +import io.github.sds100.keymapper.base.compose.ComposeColors.onOrangeContainerDark +import io.github.sds100.keymapper.base.compose.ComposeColors.onOrangeContainerLight +import io.github.sds100.keymapper.base.compose.ComposeColors.onOrangeDark +import io.github.sds100.keymapper.base.compose.ComposeColors.onOrangeLight +import io.github.sds100.keymapper.base.compose.ComposeColors.orangeContainerDark +import io.github.sds100.keymapper.base.compose.ComposeColors.orangeContainerLight +import io.github.sds100.keymapper.base.compose.ComposeColors.orangeDark +import io.github.sds100.keymapper.base.compose.ComposeColors.orangeLight /** * Stores the custom colors in a palette that changes @@ -17,6 +29,14 @@ data class ComposeCustomColors( val onGreen: Color = Color.Unspecified, val greenContainer: Color = Color.Unspecified, val onGreenContainer: Color = Color.Unspecified, + val magiskTeal: Color = Color.Unspecified, + val onMagiskTeal: Color = Color.Unspecified, + val shizukuBlue: Color = Color.Unspecified, + val onShizukuBlue: Color = Color.Unspecified, + val orange: Color = Color.Unspecified, + val onOrange: Color = Color.Unspecified, + val orangeContainer: Color = Color.Unspecified, + val onOrangeContainer: Color = Color.Unspecified, ) { companion object { val LightPalette = ComposeCustomColors( @@ -26,6 +46,14 @@ data class ComposeCustomColors( onGreen = ComposeColors.onGreenLight, greenContainer = ComposeColors.greenContainerLight, onGreenContainer = ComposeColors.onGreenContainerLight, + magiskTeal = ComposeColors.magiskTealLight, + onMagiskTeal = ComposeColors.onMagiskTealLight, + shizukuBlue = ComposeColors.shizukuBlueLight, + onShizukuBlue = ComposeColors.onShizukuBlueLight, + orange = orangeLight, + onOrange = onOrangeLight, + orangeContainer = orangeContainerLight, + onOrangeContainer = onOrangeContainerLight, ) val DarkPalette = ComposeCustomColors( @@ -35,6 +63,27 @@ data class ComposeCustomColors( onGreen = ComposeColors.onGreenDark, greenContainer = ComposeColors.greenContainerDark, onGreenContainer = ComposeColors.onGreenContainerDark, + magiskTeal = ComposeColors.magiskTealDark, + onMagiskTeal = ComposeColors.onMagiskTealDark, + shizukuBlue = ComposeColors.shizukuBlueDark, + onShizukuBlue = ComposeColors.onShizukuBlueDark, + orange = orangeDark, + onOrange = onOrangeDark, + orangeContainer = orangeContainerDark, + onOrangeContainer = onOrangeContainerDark, ) } + + @Composable + @Stable + fun contentColorFor(color: Color): Color { + return when (color) { + red -> onRed + green -> onGreen + greenContainer -> onGreenContainer + magiskTeal -> onMagiskTeal + shizukuBlue -> onShizukuBlue + else -> MaterialTheme.colorScheme.contentColorFor(color) + } + } } 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 6bf2901ee9..cb7f9f2985 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 @@ -148,8 +148,9 @@ class ChooseConstraintViewModel @Inject constructor( constraintType, ) - ConstraintId.SCREEN_ON -> onSelectScreenOnConstraint() - ConstraintId.SCREEN_OFF -> onSelectScreenOffConstraint() + ConstraintId.SCREEN_ON -> returnResult.emit(Constraint.ScreenOn()) + + ConstraintId.SCREEN_OFF -> returnResult.emit(Constraint.ScreenOff()) ConstraintId.ORIENTATION_PORTRAIT -> returnResult.emit(Constraint.OrientationPortrait()) @@ -353,28 +354,6 @@ class ChooseConstraintViewModel @Inject constructor( } } - private suspend fun onSelectScreenOnConstraint() { - val response = showDialog( - "screen_on_constraint_limitation", - DialogModel.Ok(getString(R.string.dialog_message_screen_constraints_limitation)), - ) - - response ?: return - - returnResult.emit(Constraint.ScreenOn()) - } - - private suspend fun onSelectScreenOffConstraint() { - val response = showDialog( - "screen_on_constraint_limitation", - DialogModel.Ok(getString(R.string.dialog_message_screen_constraints_limitation)), - ) - - response ?: return - - returnResult.emit(Constraint.ScreenOff()) - } - private suspend fun onSelectBluetoothConstraint(type: ConstraintId) { val response = showDialog( "bluetooth_device_constraint_limitation", diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConfigConstraintsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConfigConstraintsUseCase.kt new file mode 100644 index 0000000000..ebcd303914 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConfigConstraintsUseCase.kt @@ -0,0 +1,124 @@ +package io.github.sds100.keymapper.base.constraints + +import dagger.hilt.android.scopes.ViewModelScoped +import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapState +import io.github.sds100.keymapper.base.keymaps.KeyMap +import io.github.sds100.keymapper.common.utils.State +import io.github.sds100.keymapper.data.Keys +import io.github.sds100.keymapper.data.repositories.PreferenceRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import java.util.LinkedList +import javax.inject.Inject + +@ViewModelScoped +class ConfigConstraintsUseCaseImpl @Inject constructor( + private val state: ConfigKeyMapState, + private val preferenceRepository: PreferenceRepository, +) : ConfigConstraintsUseCase { + + override val keyMap: StateFlow> = state.keyMap + + /** + * The most recently used is first. + */ + override val recentlyUsedConstraints: Flow> = + combine( + preferenceRepository.get(Keys.recentlyUsedConstraints).map(::getConstraintShortcuts), + keyMap.filterIsInstance>(), + ) { shortcuts, keyMap -> + + // Do not include constraints that the key map already contains. + shortcuts + .filter { !keyMap.data.constraintState.constraints.contains(it) } + .take(5) + } + + override fun addConstraint(constraint: Constraint): Boolean { + var containsConstraint = false + + updateConstraintState { oldState -> + containsConstraint = oldState.constraints.contains(constraint) + oldState.copy(constraints = oldState.constraints.plus(constraint)) + } + + preferenceRepository.update( + Keys.recentlyUsedConstraints, + { old -> + val oldList: List = if (old == null) { + emptyList() + } else { + Json.decodeFromString>(old) + } + + val newShortcuts = LinkedList(oldList) + .also { it.addFirst(constraint) } + .distinct() + + Json.encodeToString(newShortcuts) + }, + ) + + return !containsConstraint + } + + override fun removeConstraint(id: String) { + updateConstraintState { oldState -> + val newList = oldState.constraints.toMutableSet().apply { + removeAll { it.uid == id } + } + oldState.copy(constraints = newList) + } + } + + override fun setAndMode() { + updateConstraintState { oldState -> + oldState.copy(mode = ConstraintMode.AND) + } + } + + override fun setOrMode() { + updateConstraintState { oldState -> + oldState.copy(mode = ConstraintMode.OR) + } + } + + private fun updateConstraintState(block: (ConstraintState) -> ConstraintState) { + state.update { keyMap -> + keyMap.copy(constraintState = block(keyMap.constraintState)) + } + } + + private suspend fun getConstraintShortcuts(json: String?): List { + if (json == null) { + return emptyList() + } + + try { + return withContext(Dispatchers.Default) { + val list = Json.decodeFromString>(json) + + list.distinct() + } + } catch (_: Exception) { + preferenceRepository.set(Keys.recentlyUsedConstraints, null) + return emptyList() + } + } +} + +interface ConfigConstraintsUseCase { + val keyMap: StateFlow> + + val recentlyUsedConstraints: Flow> + fun addConstraint(constraint: Constraint): Boolean + fun removeConstraint(id: String) + fun setAndMode() + fun setOrMode() +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConfigConstraintsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConfigConstraintsViewModel.kt index a2d941bbbd..3f2b67d5f7 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConfigConstraintsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/constraints/ConfigConstraintsViewModel.kt @@ -3,7 +3,9 @@ package io.github.sds100.keymapper.base.constraints import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapUseCase +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel import io.github.sds100.keymapper.base.keymaps.ShortcutModel import io.github.sds100.keymapper.base.utils.getFullMessage import io.github.sds100.keymapper.base.utils.isFixable @@ -20,7 +22,6 @@ import io.github.sds100.keymapper.common.utils.dataOrNull import io.github.sds100.keymapper.common.utils.mapData import io.github.sds100.keymapper.system.SystemError import io.github.sds100.keymapper.system.permissions.Permission -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -33,15 +34,17 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import javax.inject.Inject -class ConfigConstraintsViewModel( - private val coroutineScope: CoroutineScope, - private val config: ConfigKeyMapUseCase, +@HiltViewModel +class ConfigConstraintsViewModel @Inject constructor( + private val config: ConfigConstraintsUseCase, private val displayConstraint: DisplayConstraintUseCase, resourceProvider: ResourceProvider, navigationProvider: NavigationProvider, dialogProvider: DialogProvider, -) : ResourceProvider by resourceProvider, +) : ViewModel(), + ResourceProvider by resourceProvider, DialogProvider by dialogProvider, NavigationProvider by navigationProvider { @@ -54,11 +57,11 @@ class ConfigConstraintsViewModel( private val shortcuts: StateFlow>> = config.recentlyUsedConstraints.map { actions -> actions.map(::buildShortcut).toSet() - }.stateIn(coroutineScope, SharingStarted.Lazily, emptySet()) + }.stateIn(viewModelScope, SharingStarted.Lazily, emptySet()) private val constraintErrorSnapshot: StateFlow = displayConstraint.constraintErrorSnapshot.stateIn( - coroutineScope, + viewModelScope, SharingStarted.Lazily, null, ) @@ -74,11 +77,11 @@ class ConfigConstraintsViewModel( _state.value = keyMapState.mapData { keyMap -> buildState(keyMap.constraintState, shortcuts, errorSnapshot) } - }.launchIn(coroutineScope) + }.launchIn(viewModelScope) } fun onClickShortcut(constraint: Constraint) { - coroutineScope.launch { + viewModelScope.launch { config.addConstraint(constraint) } } @@ -93,7 +96,7 @@ class ConfigConstraintsViewModel( } fun onFixError(constraintUid: String) { - coroutineScope.launch { + viewModelScope.launch { val constraint = config.keyMap .firstOrNull() ?.dataOrNull() @@ -106,7 +109,7 @@ class ConfigConstraintsViewModel( ?: return@launch if (error == SystemError.PermissionDenied(Permission.ACCESS_NOTIFICATION_POLICY)) { - coroutineScope.launch { + viewModelScope.launch { ViewModelHelper.showDialogExplainingDndAccessBeingUnavailable( resourceProvider = this@ConfigConstraintsViewModel, dialogProvider = this@ConfigConstraintsViewModel, @@ -127,7 +130,7 @@ class ConfigConstraintsViewModel( } fun addConstraint() { - coroutineScope.launch { + viewModelScope.launch { val constraint = navigate("add_constraint", NavDestination.ChooseConstraint) ?: return@launch 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 e5803e9b0a..39fb58faf4 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 @@ -1,7 +1,6 @@ package io.github.sds100.keymapper.base.constraints import android.media.AudioManager -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.firstBlocking @@ -41,25 +40,17 @@ class LazyConstraintSnapshot( private val appsPlayingMedia: List by lazy { mediaAdapter.getActiveMediaSessionPackages() } private val audioVolumeStreams: Set by lazy { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - mediaAdapter.getActiveAudioVolumeStreams() - } else { - emptySet() - } + mediaAdapter.getActiveAudioVolumeStreams() } private val isWifiEnabled: Boolean by lazy { networkAdapter.isWifiEnabled() } - private val connectedWifiSSID: String? by lazy { networkAdapter.connectedWifiSSID } + private val connectedWifiSSID: String? by lazy { networkAdapter.connectedWifiSSIDFlow.firstBlocking() } private val chosenImeId: String? by lazy { inputMethodAdapter.chosenIme.value?.id } private val callState: CallState by lazy { phoneAdapter.getCallState() } private val isCharging: Boolean by lazy { powerAdapter.isCharging.value } private val isLocked: Boolean by lazy { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { - lockScreenAdapter.isLocked() - } else { - false - } + lockScreenAdapter.isLocked() } private val isLockscreenShowing: Boolean by lazy { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/DetectKeyMapModel.kt similarity index 80% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapModel.kt rename to base/src/main/java/io/github/sds100/keymapper/base/detection/DetectKeyMapModel.kt index 127f2021dd..321bce0664 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/DetectKeyMapModel.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.base.keymaps.detection +package io.github.sds100.keymapper.base.detection import io.github.sds100.keymapper.base.constraints.ConstraintState import io.github.sds100.keymapper.base.keymaps.KeyMap diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/DetectKeyMapsUseCase.kt similarity index 78% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt rename to base/src/main/java/io/github/sds100/keymapper/base/detection/DetectKeyMapsUseCase.kt index fb3fc72e1f..ebe1f12199 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectKeyMapsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/DetectKeyMapsUseCase.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.base.keymaps.detection +package io.github.sds100.keymapper.base.detection import android.accessibilityservice.AccessibilityService import android.os.SystemClock @@ -11,14 +11,14 @@ import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.constraints.ConstraintState import io.github.sds100.keymapper.base.groups.Group import io.github.sds100.keymapper.base.groups.GroupEntityMapper +import io.github.sds100.keymapper.base.input.InjectKeyEventModel +import io.github.sds100.keymapper.base.input.InputEventHub import io.github.sds100.keymapper.base.keymaps.KeyMap import io.github.sds100.keymapper.base.keymaps.KeyMapEntityMapper import io.github.sds100.keymapper.base.system.accessibility.IAccessibilityService -import io.github.sds100.keymapper.base.system.inputmethod.ImeInputEventInjector import io.github.sds100.keymapper.base.system.navigation.OpenMenuHelper import io.github.sds100.keymapper.base.trigger.FingerprintTriggerKey import io.github.sds100.keymapper.base.utils.ui.ResourceProvider -import io.github.sds100.keymapper.common.utils.InputEventType import io.github.sds100.keymapper.common.utils.State import io.github.sds100.keymapper.common.utils.dataOrNull import io.github.sds100.keymapper.data.Keys @@ -27,13 +27,7 @@ import io.github.sds100.keymapper.data.repositories.FloatingButtonRepository import io.github.sds100.keymapper.data.repositories.GroupRepository import io.github.sds100.keymapper.data.repositories.KeyMapRepository import io.github.sds100.keymapper.data.repositories.PreferenceRepository -import io.github.sds100.keymapper.system.display.DisplayAdapter -import io.github.sds100.keymapper.system.inputmethod.InputKeyModel -import io.github.sds100.keymapper.system.permissions.Permission -import io.github.sds100.keymapper.system.permissions.PermissionAdapter import io.github.sds100.keymapper.system.popup.ToastAdapter -import io.github.sds100.keymapper.system.root.SuAdapter -import io.github.sds100.keymapper.system.shizuku.ShizukuInputEventInjector import io.github.sds100.keymapper.system.vibrator.VibratorAdapter import io.github.sds100.keymapper.system.volume.VolumeAdapter import kotlinx.coroutines.CoroutineScope @@ -42,28 +36,22 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import timber.log.Timber class DetectKeyMapsUseCaseImpl @AssistedInject constructor( - @Assisted - private val imeInputEventInjector: ImeInputEventInjector, @Assisted private val accessibilityService: IAccessibilityService, private val keyMapRepository: KeyMapRepository, private val floatingButtonRepository: FloatingButtonRepository, private val groupRepository: GroupRepository, private val preferenceRepository: PreferenceRepository, - private val suAdapter: SuAdapter, - private val displayAdapter: DisplayAdapter, private val volumeAdapter: VolumeAdapter, private val toastAdapter: ToastAdapter, - private val permissionAdapter: PermissionAdapter, private val resourceProvider: ResourceProvider, private val vibrator: VibratorAdapter, @Assisted private val coroutineScope: CoroutineScope, + private val inputEventHub: InputEventHub, ) : DetectKeyMapsUseCase { @AssistedFactory @@ -71,7 +59,6 @@ class DetectKeyMapsUseCaseImpl @AssistedInject constructor( fun create( accessibilityService: IAccessibilityService, coroutineScope: CoroutineScope, - imeInputEventInjector: ImeInputEventInjector, ): DetectKeyMapsUseCaseImpl } @@ -148,14 +135,6 @@ class DetectKeyMapsUseCaseImpl @AssistedInject constructor( keyMapList.filter { it.keyMap.trigger.triggerFromOtherApps }.map { it.keyMap } }.flowOn(Dispatchers.Default) - override val detectScreenOffTriggers: Flow = - combine( - allKeyMapList, - suAdapter.isGranted, - ) { keyMapList, isRootPermissionGranted -> - keyMapList.any { it.keyMap.trigger.screenOffTrigger } && isRootPermissionGranted - }.flowOn(Dispatchers.Default) - override val defaultLongPressDelay: Flow = preferenceRepository.get(Keys.defaultLongPressDelay) .map { it ?: PreferenceDefaults.LONG_PRESS_DELAY } @@ -174,14 +153,9 @@ class DetectKeyMapsUseCaseImpl @AssistedInject constructor( override val currentTime: Long get() = SystemClock.elapsedRealtime() - private val shizukuInputEventInjector = ShizukuInputEventInjector() - private val openMenuHelper = OpenMenuHelper( - suAdapter, accessibilityService, - shizukuInputEventInjector, - permissionAdapter, - coroutineScope, + inputEventHub, ) override val forceVibrate: Flow = @@ -200,29 +174,26 @@ class DetectKeyMapsUseCaseImpl @AssistedInject constructor( vibrator.vibrate(duration) } - override fun imitateButtonPress( + override fun imitateKeyEvent( keyCode: Int, metaState: Int, deviceId: Int, - inputEventType: InputEventType, + action: Int, scanCode: Int, source: Int, ) { - val model = InputKeyModel( - keyCode, - inputEventType, - metaState, - deviceId, - scanCode, + val model = InjectKeyEventModel( + keyCode = keyCode, + action = action, + metaState = metaState, + deviceId = deviceId, + scanCode = scanCode, source = source, ) - if (permissionAdapter.isGranted(Permission.SHIZUKU)) { - Timber.d("Imitate button press ${KeyEvent.keyCodeToString(keyCode)} with Shizuku, key code: $keyCode, device id: $deviceId, meta state: $metaState, scan code: $scanCode") - - coroutineScope.launch { - shizukuInputEventInjector.inputKeyEvent(model) - } + if (inputEventHub.isSystemBridgeConnected()) { + Timber.d("Imitate button press ${KeyEvent.keyCodeToString(keyCode)} with system bridge, key code: $keyCode, device id: $deviceId, meta state: $metaState, scan code: $scanCode") + inputEventHub.injectKeyEventAsync(model) } else { Timber.d("Imitate button press ${KeyEvent.keyCodeToString(keyCode)}, key code: $keyCode, device id: $deviceId, meta state: $metaState, scan code: $scanCode") @@ -239,21 +210,25 @@ class DetectKeyMapsUseCaseImpl @AssistedInject constructor( KeyEvent.KEYCODE_MENU -> openMenuHelper.openMenu() - else -> runBlocking { - imeInputEventInjector.inputKeyEvent(model) - } + else -> inputEventHub.injectKeyEventAsync(model) } } } - override val isScreenOn: Flow = displayAdapter.isScreenOn + override fun imitateEvdevEvent(devicePath: String, type: Int, code: Int, value: Int) { + if (inputEventHub.isSystemBridgeConnected()) { + Timber.d("Imitate evdev event, device path: $devicePath, type: $type, code: $code, value: $value") + inputEventHub.injectEvdevEvent(devicePath, type, code, value) + } else { + Timber.w("Cannot imitate evdev event without system bridge connected. Device path: $devicePath, type: $type, code: $code, value: $value") + } + } } interface DetectKeyMapsUseCase { val allKeyMapList: Flow> val requestFingerprintGestureDetection: Flow val keyMapsToTriggerFromOtherApps: Flow> - val detectScreenOffTriggers: Flow val defaultLongPressDelay: Flow val defaultDoublePressDelay: Flow @@ -267,14 +242,19 @@ interface DetectKeyMapsUseCase { val currentTime: Long - fun imitateButtonPress( + fun imitateKeyEvent( keyCode: Int, metaState: Int = 0, deviceId: Int = 0, - inputEventType: InputEventType = InputEventType.DOWN_UP, + action: Int, scanCode: Int = 0, source: Int = InputDevice.SOURCE_UNKNOWN, ) - val isScreenOn: Flow + fun imitateEvdevEvent( + devicePath: String, + type: Int, + code: Int, + value: Int, + ) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DpadMotionEventTracker.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/DpadMotionEventTracker.kt similarity index 86% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DpadMotionEventTracker.kt rename to base/src/main/java/io/github/sds100/keymapper/base/detection/DpadMotionEventTracker.kt index 0da1229d11..0f51a065a9 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DpadMotionEventTracker.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/DpadMotionEventTracker.kt @@ -1,11 +1,11 @@ -package io.github.sds100.keymapper.base.keymaps.detection +package io.github.sds100.keymapper.base.detection import android.view.InputDevice import android.view.KeyEvent -import io.github.sds100.keymapper.system.devices.InputDeviceInfo -import io.github.sds100.keymapper.system.inputevents.InputEventUtils -import io.github.sds100.keymapper.system.inputevents.MyKeyEvent -import io.github.sds100.keymapper.system.inputevents.MyMotionEvent +import io.github.sds100.keymapper.common.utils.InputDeviceInfo +import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent +import io.github.sds100.keymapper.system.inputevents.KMKeyEvent +import io.github.sds100.keymapper.system.inputevents.KeyEventUtils /** * See https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input#dpad @@ -32,10 +32,10 @@ class DpadMotionEventTracker { * * @return whether to consume the key event. */ - fun onKeyEvent(event: MyKeyEvent): Boolean { + fun onKeyEvent(event: KMKeyEvent): Boolean { val device = event.device ?: return false - if (!InputEventUtils.isDpadKeyCode(event.keyCode)) { + if (!KeyEventUtils.isDpadKeyCode(event.keyCode)) { return false } @@ -63,7 +63,7 @@ class DpadMotionEventTracker { * * @return An array of key events. Empty if no DPAD buttons changed. */ - fun convertMotionEvent(event: MyMotionEvent): List { + fun convertMotionEvent(event: KMGamePadEvent): List { val oldState = dpadState[event.device.getDescriptor()] ?: 0 val newState = eventToDpadState(event) val diff = oldState xor newState @@ -101,7 +101,7 @@ class DpadMotionEventTracker { } return keyCodes.map { - MyKeyEvent( + KMKeyEvent( it, action, metaState = event.metaState, @@ -109,6 +109,7 @@ class DpadMotionEventTracker { device = event.device, repeatCount = 0, source = InputDevice.SOURCE_DPAD, + eventTime = event.eventTime, ) } } @@ -121,7 +122,7 @@ class DpadMotionEventTracker { return this?.descriptor ?: "" } - private fun eventToDpadState(event: MyMotionEvent): Int { + private fun eventToDpadState(event: KMGamePadEvent): Int { var state = 0 if (event.axisHatX == -1.0f) { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapController.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt similarity index 84% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapController.kt rename to base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt index b6325a601a..809266c0e6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyMapController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapAlgorithm.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.base.keymaps.detection +package io.github.sds100.keymapper.base.detection import android.view.KeyEvent import androidx.collection.SparseArrayCompat @@ -15,31 +15,34 @@ import io.github.sds100.keymapper.base.keymaps.ClickType import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType import io.github.sds100.keymapper.base.trigger.AssistantTriggerKey import io.github.sds100.keymapper.base.trigger.AssistantTriggerType +import io.github.sds100.keymapper.base.trigger.EvdevTriggerKey import io.github.sds100.keymapper.base.trigger.FingerprintTriggerKey import io.github.sds100.keymapper.base.trigger.FloatingButtonKey import io.github.sds100.keymapper.base.trigger.KeyCodeTriggerKey -import io.github.sds100.keymapper.base.trigger.KeyEventDetectionSource +import io.github.sds100.keymapper.base.trigger.KeyEventTriggerDevice +import io.github.sds100.keymapper.base.trigger.KeyEventTriggerKey import io.github.sds100.keymapper.base.trigger.Trigger import io.github.sds100.keymapper.base.trigger.TriggerKey -import io.github.sds100.keymapper.base.trigger.TriggerKeyDevice import io.github.sds100.keymapper.base.trigger.TriggerMode -import io.github.sds100.keymapper.common.utils.InputEventType +import io.github.sds100.keymapper.base.trigger.detectWithScancode +import io.github.sds100.keymapper.common.models.EvdevDeviceInfo import io.github.sds100.keymapper.common.utils.minusFlag import io.github.sds100.keymapper.common.utils.withFlag import io.github.sds100.keymapper.data.PreferenceDefaults -import io.github.sds100.keymapper.system.inputevents.InputEventUtils -import io.github.sds100.keymapper.system.inputevents.MyKeyEvent -import io.github.sds100.keymapper.system.inputevents.MyMotionEvent +import io.github.sds100.keymapper.system.inputevents.KMEvdevEvent +import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent +import io.github.sds100.keymapper.system.inputevents.KMInputEvent +import io.github.sds100.keymapper.system.inputevents.KMKeyEvent +import io.github.sds100.keymapper.system.inputevents.KeyEventUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -class KeyMapController( +class KeyMapAlgorithm( private val coroutineScope: CoroutineScope, private val useCase: DetectKeyMapsUseCase, private val performActionsUseCase: PerformActionsUseCase, @@ -65,7 +68,192 @@ class KeyMapController( trigger.mode is TriggerMode.Parallel } - private fun loadKeyMaps(value: List) { + private var detectKeyMaps: Boolean = false + private var detectInternalEvents: Boolean = false + private var detectExternalEvents: Boolean = false + private var detectSequenceLongPresses: Boolean = false + private var detectSequenceDoublePresses: Boolean = false + + /** + * All sequence events that have the long press click type. + */ + private var longPressSequenceTriggerKeys: Array = arrayOf() + + /** + * All double press keys and the index of their corresponding trigger. first is the event and second is + * the trigger index. + */ + private var doublePressTriggerKeys: Array = arrayOf() + + /** + * order matches with [doublePressTriggerKeys] + */ + private var doublePressEventStates: IntArray = intArrayOf() + + /** + * The user has an amount of time to double press a key before it is registered as a double press. + * The order matches with [doublePressTriggerKeys]. This array stores the time when the corresponding trigger will + * timeout. If the key isn't waiting to timeout, the value is -1. + */ + private var doublePressTimeoutTimes = longArrayOf() + + private var actionMap: SparseArrayCompat = SparseArrayCompat() + var triggers: Array = emptyArray() + private set + + /** + * The events to detect for each sequence trigger. + */ + private var sequenceTriggers: IntArray = intArrayOf() + + /** + * Sequence triggers timeout after the first key has been pressed. + * This map stores the time when the corresponding trigger will timeout. If the trigger in + * isn't waiting to timeout, the value is -1. + * The index of a trigger matches with the index in [triggers] + */ + private var sequenceTriggersTimeoutTimes: MutableMap = mutableMapOf() + + /** + * The indexes of triggers that overlap after the first element with each trigger in [sequenceTriggers] + */ + private var sequenceTriggersOverlappingSequenceTriggers: Array = arrayOf() + + private var sequenceTriggersOverlappingParallelTriggers: Array = arrayOf() + + /** + * An array of the index of the last matched event in each trigger. + */ + private var lastMatchedEventIndices: IntArray = intArrayOf() + + /** + * An array of the constraints for every trigger + */ + private var triggerConstraints: Array> = arrayOf() + + /** + * The events to detect for each parallel trigger. + */ + private var parallelTriggers: IntArray = intArrayOf() + + /** + * The actions to perform when each trigger is detected. The order matches with + * [triggers]. + */ + private var triggerActions: Array = arrayOf() + + /** + * Stores whether each event in each parallel trigger need to be released after being held down. + * The index of a trigger matches with the index in [triggers] + */ + private var parallelTriggerEventsAwaitingRelease: Array = emptyArray() + + /** + * Whether each parallel trigger is awaiting to be released after performing an action. + * This is only set to true if the trigger has been successfully triggered and *all* the keys + * have not been released. + * The index of a trigger matches with the index in [triggers] + */ + private var parallelTriggersAwaitingReleaseAfterBeingTriggered: BooleanArray = booleanArrayOf() + + private var parallelTriggerModifierKeyIndices: Array> = arrayOf() + + /** + * The indexes of triggers that overlap after the first element with each trigger in [parallelTriggers] + */ + private var parallelTriggersOverlappingParallelTriggers = arrayOf() + + private var modifierKeyEventActions: Boolean = false + private var notModifierKeyEventActions: Boolean = false + private var keyCodesToImitateUpAction: MutableSet = mutableSetOf() + private var metaStateFromActions: Int = 0 + private var metaStateFromKeyEvent: Int = 0 + + private val eventDownTimeMap: MutableMap = mutableMapOf() + + /** + * This solves issue #1386. This stores the jobs that will wait until the sequence trigger + * times out and check whether the overlapping sequence trigger was indeed triggered. + */ + private val performActionsAfterSequenceTriggerTimeout: MutableMap = mutableMapOf() + + /** + * The indexes of parallel triggers that didn't have their actions performed because there is a matching trigger but + * for a long-press. These actions should only be performed if the long-press fails, otherwise when the user + * holds down the trigger keys for the long-press trigger, actions from both triggers will be performed. + */ + private val performActionsOnFailedLongPress: MutableSet = mutableSetOf() + + /** + * The indexes of parallel triggers that didn't have their actions performed because there is a matching trigger but + * for a double-press. These actions should only be performed if the double-press fails, otherwise each time the user + * presses the keys for the double press, actions from both triggers will be performed. + */ + private val performActionsOnFailedDoublePress: MutableSet = mutableSetOf() + + /** + * Maps jobs to perform an action after a long press to their corresponding parallel trigger index + */ + private val parallelTriggerLongPressJobs: SparseArrayCompat = SparseArrayCompat() + + /** + * Keys that are detected through an input method will potentially send multiple DOWN key events + * with incremented repeatCounts, such as DPAD buttons. These repeated DOWN key events must + * all be consumed and ignored because the UP key event is only sent once at the end. The action + * must not be executed for each repeat. The user may potentially have many hundreds + * of trigger keys so to reduce latency this set caches which keys + * will be affected by this behavior. + * + * NOTE: This only contains the trigger keys that are flagged to consume the key event. + */ + private var triggerKeysThatSendRepeatedKeyEvents: Set = emptySet() + + private var parallelTriggerActionPerformers: Map = + emptyMap() + private var sequenceTriggerActionPerformers: Map = + emptyMap() + + private val currentTime: Long + get() = useCase.currentTime + + private val defaultVibrateDuration: StateFlow = + useCase.defaultVibrateDuration.stateIn( + coroutineScope, + SharingStarted.Eagerly, + PreferenceDefaults.VIBRATION_DURATION.toLong(), + ) + + private val defaultSequenceTriggerTimeout: StateFlow = + useCase.defaultSequenceTriggerTimeout.stateIn( + coroutineScope, + SharingStarted.Eagerly, + PreferenceDefaults.SEQUENCE_TRIGGER_TIMEOUT.toLong(), + ) + + private val defaultLongPressDelay: StateFlow = + useCase.defaultLongPressDelay.stateIn( + coroutineScope, + SharingStarted.Eagerly, + PreferenceDefaults.LONG_PRESS_DELAY.toLong(), + ) + + private val defaultDoublePressDelay: StateFlow = + useCase.defaultDoublePressDelay.stateIn( + coroutineScope, + SharingStarted.Eagerly, + PreferenceDefaults.DOUBLE_PRESS_DELAY.toLong(), + ) + + private val forceVibrate: StateFlow = + useCase.forceVibrate.stateIn( + coroutineScope, + SharingStarted.Eagerly, + PreferenceDefaults.FORCE_VIBRATE, + ) + + private val dpadMotionEventTracker: DpadMotionEventTracker = DpadMotionEventTracker() + + fun loadKeyMaps(value: List) { actionMap.clear() // If there are no keymaps with actions then keys don't need to be detected. @@ -102,7 +290,7 @@ class KeyMapController( val parallelTriggerActionPerformers = mutableMapOf() val parallelTriggerModifierKeyIndices = mutableListOf>() - val triggerKeysThatSendRepeatedKeyEvents = mutableSetOf() + val triggerKeysThatSendRepeatedKeyEvents = mutableSetOf() // Only process key maps that can be triggered val validKeyMaps = value.filter { @@ -112,8 +300,8 @@ class KeyMapController( for ((triggerIndex, model) in validKeyMaps.withIndex()) { val keyMap = model.keyMap // TRIGGER STUFF - keyMap.trigger.keys.forEachIndexed { keyIndex, key -> - if (key is KeyCodeTriggerKey && key.detectionSource == KeyEventDetectionSource.INPUT_METHOD && key.consumeEvent) { + for ((keyIndex, key) in keyMap.trigger.keys.withIndex()) { + if (key is KeyEventTriggerKey && key.requiresIme && key.consumeEvent) { triggerKeysThatSendRepeatedKeyEvents.add(key) } @@ -133,21 +321,26 @@ class KeyMapController( } when (key) { - is KeyCodeTriggerKey -> when (key.device) { - TriggerKeyDevice.Internal -> { + is KeyEventTriggerKey -> when (key.device) { + KeyEventTriggerDevice.Internal -> { detectInternalEvents = true } - TriggerKeyDevice.Any -> { + KeyEventTriggerDevice.Any -> { detectInternalEvents = true detectExternalEvents = true } - is TriggerKeyDevice.External -> { + is KeyEventTriggerDevice.External -> { detectExternalEvents = true } } + is EvdevTriggerKey -> { + detectInternalEvents = true + detectExternalEvents = true + } + else -> {} } } @@ -327,15 +520,13 @@ class KeyMapController( for (triggerIndex in parallelTriggers) { val trigger = triggers[triggerIndex] - trigger.keys.forEachIndexed { keyIndex, key -> + for ((keyIndex, key) in trigger.keys.withIndex()) { if (key is KeyCodeTriggerKey && isModifierKey(key.keyCode)) { parallelTriggerModifierKeyIndices.add(triggerIndex to keyIndex) } } } - reset() - this.triggers = triggers.toTypedArray() this.triggerActions = triggerActions.toTypedArray() this.triggerConstraints = triggerConstraints.toTypedArray() @@ -376,200 +567,7 @@ class KeyMapController( } } - private var detectKeyMaps: Boolean = false - private var detectInternalEvents: Boolean = false - private var detectExternalEvents: Boolean = false - private var detectSequenceLongPresses: Boolean = false - private var detectSequenceDoublePresses: Boolean = false - - /** - * All sequence events that have the long press click type. - */ - private var longPressSequenceTriggerKeys: Array = arrayOf() - - /** - * All double press keys and the index of their corresponding trigger. first is the event and second is - * the trigger index. - */ - private var doublePressTriggerKeys: Array = arrayOf() - - /** - * order matches with [doublePressTriggerKeys] - */ - private var doublePressEventStates: IntArray = intArrayOf() - - /** - * The user has an amount of time to double press a key before it is registered as a double press. - * The order matches with [doublePressTriggerKeys]. This array stores the time when the corresponding trigger will - * timeout. If the key isn't waiting to timeout, the value is -1. - */ - private var doublePressTimeoutTimes = longArrayOf() - - private var actionMap: SparseArrayCompat = SparseArrayCompat() - private var triggers: Array = emptyArray() - - /** - * The events to detect for each sequence trigger. - */ - private var sequenceTriggers: IntArray = intArrayOf() - - /** - * Sequence triggers timeout after the first key has been pressed. - * This map stores the time when the corresponding trigger will timeout. If the trigger in - * isn't waiting to timeout, the value is -1. - * The index of a trigger matches with the index in [triggers] - */ - private var sequenceTriggersTimeoutTimes: MutableMap = mutableMapOf() - - /** - * The indexes of triggers that overlap after the first element with each trigger in [sequenceTriggers] - */ - private var sequenceTriggersOverlappingSequenceTriggers: Array = arrayOf() - - private var sequenceTriggersOverlappingParallelTriggers: Array = arrayOf() - - /** - * An array of the index of the last matched event in each trigger. - */ - private var lastMatchedEventIndices: IntArray = intArrayOf() - - /** - * An array of the constraints for every trigger - */ - private var triggerConstraints: Array> = arrayOf() - - /** - * The events to detect for each parallel trigger. - */ - private var parallelTriggers: IntArray = intArrayOf() - - /** - * The actions to perform when each trigger is detected. The order matches with - * [triggers]. - */ - private var triggerActions: Array = arrayOf() - - /** - * Stores whether each event in each parallel trigger need to be released after being held down. - * The index of a trigger matches with the index in [triggers] - */ - private var parallelTriggerEventsAwaitingRelease: Array = emptyArray() - - /** - * Whether each parallel trigger is awaiting to be released after performing an action. - * This is only set to true if the trigger has been successfully triggered and *all* the keys - * have not been released. - * The index of a trigger matches with the index in [triggers] - */ - private var parallelTriggersAwaitingReleaseAfterBeingTriggered: BooleanArray = booleanArrayOf() - - private var parallelTriggerModifierKeyIndices: Array> = arrayOf() - - /** - * The indexes of triggers that overlap after the first element with each trigger in [parallelTriggers] - */ - private var parallelTriggersOverlappingParallelTriggers = arrayOf() - - private var modifierKeyEventActions: Boolean = false - private var notModifierKeyEventActions: Boolean = false - private var keyCodesToImitateUpAction: MutableSet = mutableSetOf() - private var metaStateFromActions: Int = 0 - private var metaStateFromKeyEvent: Int = 0 - - private val eventDownTimeMap: MutableMap = mutableMapOf() - - /** - * This solves issue #1386. This stores the jobs that will wait until the sequence trigger - * times out and check whether the overlapping sequence trigger was indeed triggered. - */ - private val performActionsAfterSequenceTriggerTimeout: MutableMap = mutableMapOf() - - /** - * The indexes of parallel triggers that didn't have their actions performed because there is a matching trigger but - * for a long-press. These actions should only be performed if the long-press fails, otherwise when the user - * holds down the trigger keys for the long-press trigger, actions from both triggers will be performed. - */ - private val performActionsOnFailedLongPress: MutableSet = mutableSetOf() - - /** - * The indexes of parallel triggers that didn't have their actions performed because there is a matching trigger but - * for a double-press. These actions should only be performed if the double-press fails, otherwise each time the user - * presses the keys for the double press, actions from both triggers will be performed. - */ - private val performActionsOnFailedDoublePress: MutableSet = mutableSetOf() - - /** - * Maps jobs to perform an action after a long press to their corresponding parallel trigger index - */ - private val parallelTriggerLongPressJobs: SparseArrayCompat = SparseArrayCompat() - - /** - * Keys that are detected through an input method will potentially send multiple DOWN key events - * with incremented repeatCounts, such as DPAD buttons. These repeated DOWN key events must - * all be consumed and ignored because the UP key event is only sent once at the end. The action - * must not be executed for each repeat. The user may potentially have many hundreds - * of trigger keys so to reduce latency this set caches which keys - * will be affected by this behavior. - * - * NOTE: This only contains the trigger keys that are flagged to consume the key event. - */ - private var triggerKeysThatSendRepeatedKeyEvents: Set = emptySet() - - private var parallelTriggerActionPerformers: Map = - emptyMap() - private var sequenceTriggerActionPerformers: Map = - emptyMap() - - private val currentTime: Long - get() = useCase.currentTime - - private val defaultVibrateDuration: StateFlow = - useCase.defaultVibrateDuration.stateIn( - coroutineScope, - SharingStarted.Eagerly, - PreferenceDefaults.VIBRATION_DURATION.toLong(), - ) - - private val defaultSequenceTriggerTimeout: StateFlow = - useCase.defaultSequenceTriggerTimeout.stateIn( - coroutineScope, - SharingStarted.Eagerly, - PreferenceDefaults.SEQUENCE_TRIGGER_TIMEOUT.toLong(), - ) - - private val defaultLongPressDelay: StateFlow = - useCase.defaultLongPressDelay.stateIn( - coroutineScope, - SharingStarted.Eagerly, - PreferenceDefaults.LONG_PRESS_DELAY.toLong(), - ) - - private val defaultDoublePressDelay: StateFlow = - useCase.defaultDoublePressDelay.stateIn( - coroutineScope, - SharingStarted.Eagerly, - PreferenceDefaults.DOUBLE_PRESS_DELAY.toLong(), - ) - - private val forceVibrate: StateFlow = - useCase.forceVibrate.stateIn( - coroutineScope, - SharingStarted.Eagerly, - PreferenceDefaults.FORCE_VIBRATE, - ) - - private val dpadMotionEventTracker: DpadMotionEventTracker = DpadMotionEventTracker() - - init { - coroutineScope.launch { - useCase.allKeyMapList.collectLatest { keyMapList -> - reset() - loadKeyMaps(keyMapList) - } - } - } - - fun onMotionEvent(event: MyMotionEvent): Boolean { + fun onMotionEvent(event: KMGamePadEvent): Boolean { if (!detectKeyMaps) return false // See https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input#dpad @@ -594,26 +592,28 @@ class KeyMapController( /** * @return whether to consume the [KeyEvent]. */ - fun onKeyEvent(keyEvent: MyKeyEvent): Boolean { + fun onInputEvent(event: KMInputEvent): Boolean { if (!detectKeyMaps) return false - if (dpadMotionEventTracker.onKeyEvent(keyEvent)) { - return true - } + if (event is KMKeyEvent) { + if (dpadMotionEventTracker.onKeyEvent(event)) { + return true + } - val device = keyEvent.device + val device = event.device - if (device != null) { if ((device.isExternal && !detectExternalEvents) || (!device.isExternal && !detectInternalEvents)) { return false } } - return onKeyEventPostFilter(keyEvent) + return onKeyEventPostFilter(event) } - private fun onKeyEventPostFilter(keyEvent: MyKeyEvent): Boolean { - metaStateFromKeyEvent = keyEvent.metaState + private fun onKeyEventPostFilter(inputEvent: KMInputEvent): Boolean { + if (inputEvent is KMKeyEvent) { + metaStateFromKeyEvent = inputEvent.metaState + } // remove the metastate from any modifier keys that remapped and are pressed down for ((triggerIndex, eventIndex) in parallelTriggerModifierKeyIndices) { @@ -625,49 +625,65 @@ class KeyMapController( if (parallelTriggerEventsAwaitingRelease[triggerIndex][eventIndex]) { metaStateFromKeyEvent = - metaStateFromKeyEvent.minusFlag(InputEventUtils.modifierKeycodeToMetaState(key.keyCode)) + metaStateFromKeyEvent.minusFlag(KeyEventUtils.modifierKeycodeToMetaState(key.keyCode)) } } - val device = keyEvent.device - - val event = if (device != null && device.isExternal) { - KeyCodeEvent( - keyCode = keyEvent.keyCode, - clickType = null, - descriptor = device.descriptor, - deviceId = device.id, - scanCode = keyEvent.scanCode, - repeatCount = keyEvent.repeatCount, - source = keyEvent.source, - ) - } else { - KeyCodeEvent( - keyCode = keyEvent.keyCode, - clickType = null, - descriptor = null, - deviceId = device?.id ?: 0, - scanCode = keyEvent.scanCode, - repeatCount = keyEvent.repeatCount, - source = keyEvent.source, - ) - } + when (inputEvent) { + is KMEvdevEvent -> { + val event = EvdevEventAlgo( + keyCode = inputEvent.androidCode, + clickType = null, + devicePath = inputEvent.device.path, + device = EvdevDeviceInfo( + name = inputEvent.device.name, + bus = inputEvent.device.bus, + vendor = inputEvent.device.vendor, + product = inputEvent.device.product, + ), + scanCode = inputEvent.code, + ) + + if (inputEvent.isDownEvent) { + return onKeyDown(event) + } else if (inputEvent.isUpEvent) { + return onKeyUp(event) + } + } + + is KMKeyEvent -> { + val device = inputEvent.device + + val event = KeyEventAlgo( + keyCode = inputEvent.keyCode, + clickType = null, + descriptor = device.descriptor, + deviceId = device.id, + scanCode = inputEvent.scanCode, + repeatCount = inputEvent.repeatCount, + source = inputEvent.source, + isExternal = device.isExternal, + ) + + when (inputEvent.action) { + KeyEvent.ACTION_DOWN -> return onKeyDown(event) + KeyEvent.ACTION_UP -> return onKeyUp(event) + } + } - when (keyEvent.action) { - KeyEvent.ACTION_DOWN -> return onKeyDown(event) - KeyEvent.ACTION_UP -> return onKeyUp(event) + is KMGamePadEvent -> throw IllegalArgumentException("GamePad events are not supported by the key map algorithm") } return false } /** - * @return whether to consume the [KeyEvent]. + * @return whether to consume the event. */ - private fun onKeyDown(event: Event): Boolean { + private fun onKeyDown(event: AlgoEvent): Boolean { // Must come before saving the event down time because // there is no corresponding up key event for key events with a repeat count > 0 - if (event is KeyCodeEvent && event.repeatCount > 0) { + if (event is KeyEventAlgo && event.repeatCount > 0) { val matchingTriggerKey = triggerKeysThatSendRepeatedKeyEvents.any { it.matchesEvent(event.withShortPress) || it.matchesEvent(event.withLongPress) || @@ -682,7 +698,7 @@ class KeyMapController( eventDownTimeMap[event] = currentTime var consumeEvent = false - val isModifierKeyCode = event is KeyCodeEvent && isModifierKey(event.keyCode) + val isModifierKeyCode = event is KeyEventAlgo && isModifierKey(event.keyCode) var mappedToParallelTriggerAction = false val constraintSnapshot: ConstraintSnapshot by lazy { detectConstraints.getSnapshot() } @@ -734,7 +750,7 @@ class KeyMapController( consumeEvent = true } - key is KeyCodeTriggerKey && event is KeyCodeEvent -> + key is KeyCodeTriggerKey && event is KeyEventAlgo -> if (key.keyCode == event.keyCode && key.consumeEvent) { consumeEvent = true } @@ -759,7 +775,7 @@ class KeyMapController( val doublePressEvent = triggers[eventLocation.triggerIndex].keys[eventLocation.keyIndex] - triggers[triggerIndex].keys.forEachIndexed { eventIndex, event -> + for ((eventIndex, event) in triggers[triggerIndex].keys.withIndex()) { if (event == doublePressEvent && triggers[triggerIndex].keys[eventIndex].consumeEvent ) { @@ -879,7 +895,7 @@ class KeyMapController( if (isModifierKey(actionKeyCode)) { val actionMetaState = - InputEventUtils.modifierKeycodeToMetaState(actionKeyCode) + KeyEventUtils.modifierKeycodeToMetaState(actionKeyCode) metaStateFromActions = metaStateFromActions.withFlag(actionMetaState) } @@ -926,16 +942,16 @@ class KeyMapController( !isModifierKeyCode && metaStateFromActions != 0 && !mappedToParallelTriggerAction && - event is KeyCodeEvent + event is KeyEventAlgo ) { consumeEvent = true keyCodesToImitateUpAction.add(event.keyCode) - useCase.imitateButtonPress( + useCase.imitateKeyEvent( keyCode = event.keyCode, metaState = metaStateFromKeyEvent.withFlag(metaStateFromActions), deviceId = event.deviceId, - inputEventType = InputEventType.DOWN, + action = KeyEvent.ACTION_DOWN, scanCode = event.scanCode, source = event.source, ) @@ -1048,7 +1064,7 @@ class KeyMapController( /** * @return whether to consume the event. */ - private fun onKeyUp(event: Event): Boolean { + private fun onKeyUp(event: AlgoEvent): Boolean { val downTime = eventDownTimeMap[event] ?: currentTime eventDownTimeMap.remove(event) @@ -1074,7 +1090,7 @@ class KeyMapController( var metaStateFromActionsToRemove = 0 - if (event is KeyCodeEvent) { + if (event is KeyEventAlgo) { if (keyCodesToImitateUpAction.contains(event.keyCode)) { consumeEvent = true imitateUpKeyEvent = true @@ -1153,7 +1169,12 @@ class KeyMapController( if ((currentTime - downTime) >= longPressDelay(trigger)) { successfulLongPressTrigger = true } else if (detectSequenceLongPresses && - longPressSequenceTriggerKeys.any { it.matchesEvent(event.withLongPress) } + longPressSequenceTriggerKeys.any { key -> + when (key) { + is EvdevTriggerKey -> key.matchesEvent(event.withLongPress) + is KeyEventTriggerKey -> key.matchesEvent(event.withLongPress) + } + } ) { imitateDownUpKeyEvent = true } @@ -1259,10 +1280,7 @@ class KeyMapController( // short press if (keyAwaitingRelease && - trigger.matchingEventAtIndex( - event.withShortPress, - keyIndex, - ) + trigger.matchingEventAtIndex(event.withShortPress, keyIndex) ) { if (isSingleKeyTrigger) { shortPressSingleKeyTriggerJustReleased = true @@ -1274,12 +1292,11 @@ class KeyMapController( if (modifierKeyEventActions) { val actionKeys = triggerActions[triggerIndex] - actionKeys.forEach { actionKey -> - + for (actionKey in actionKeys) { actionMap[actionKey]?.let { action -> if (action.data is ActionData.InputKeyEvent && isModifierKey(action.data.keyCode)) { val actionMetaState = - InputEventUtils.modifierKeycodeToMetaState(action.data.keyCode) + KeyEventUtils.modifierKeycodeToMetaState(action.data.keyCode) metaStateFromActionsToRemove = metaStateFromActionsToRemove.withFlag(actionMetaState) @@ -1419,13 +1436,34 @@ class KeyMapController( return@launch } - if (event is KeyCodeEvent) { - useCase.imitateButtonPress( + if (event is KeyEventAlgo) { + useCase.imitateKeyEvent( + event.keyCode, + action = KeyEvent.ACTION_DOWN, + scanCode = event.scanCode, + source = event.source, + ) + + useCase.imitateKeyEvent( event.keyCode, - inputEventType = InputEventType.DOWN_UP, + action = KeyEvent.ACTION_UP, scanCode = event.scanCode, source = event.source, ) + } else if (event is EvdevEventAlgo) { + useCase.imitateEvdevEvent( + devicePath = event.devicePath, + KMEvdevEvent.TYPE_KEY_EVENT, + event.scanCode, + KMEvdevEvent.VALUE_DOWN, + ) + + useCase.imitateEvdevEvent( + devicePath = event.devicePath, + KMEvdevEvent.TYPE_KEY_EVENT, + event.scanCode, + KMEvdevEvent.VALUE_UP, + ) } } } @@ -1434,25 +1472,61 @@ class KeyMapController( detectedSequenceTriggerIndexes.isEmpty() && detectedParallelTriggerIndexes.isEmpty() && !shortPressSingleKeyTriggerJustReleased && - !mappedToDoublePress && - event is KeyCodeEvent + !mappedToDoublePress ) { - val keyEventAction = if (imitateUpKeyEvent) { - InputEventType.UP - } else { - InputEventType.DOWN_UP + if (event is KeyEventAlgo) { + if (imitateUpKeyEvent) { + useCase.imitateKeyEvent( + keyCode = event.keyCode, + metaState = metaStateFromKeyEvent.withFlag(metaStateFromActions), + deviceId = event.deviceId, + action = KeyEvent.ACTION_UP, + scanCode = event.scanCode, + source = event.source, + ) + } else { + useCase.imitateKeyEvent( + keyCode = event.keyCode, + metaState = metaStateFromKeyEvent.withFlag(metaStateFromActions), + deviceId = event.deviceId, + action = KeyEvent.ACTION_DOWN, + scanCode = event.scanCode, + source = event.source, + ) + useCase.imitateKeyEvent( + keyCode = event.keyCode, + metaState = metaStateFromKeyEvent.withFlag(metaStateFromActions), + deviceId = event.deviceId, + action = KeyEvent.ACTION_UP, + scanCode = event.scanCode, + source = event.source, + ) + } + keyCodesToImitateUpAction.remove(event.keyCode) + } else if (event is EvdevEventAlgo) { + if (imitateUpKeyEvent) { + useCase.imitateEvdevEvent( + devicePath = event.devicePath, + type = KMEvdevEvent.TYPE_KEY_EVENT, + code = event.scanCode, + value = KMEvdevEvent.VALUE_UP, + ) + } else { + useCase.imitateEvdevEvent( + devicePath = event.devicePath, + type = KMEvdevEvent.TYPE_KEY_EVENT, + code = event.scanCode, + value = KMEvdevEvent.VALUE_DOWN, + ) + useCase.imitateEvdevEvent( + devicePath = event.devicePath, + type = KMEvdevEvent.TYPE_KEY_EVENT, + code = event.scanCode, + value = KMEvdevEvent.VALUE_UP, + ) + } + keyCodesToImitateUpAction.remove(event.keyCode) } - - useCase.imitateButtonPress( - keyCode = event.keyCode, - metaState = metaStateFromKeyEvent.withFlag(metaStateFromActions), - deviceId = event.deviceId, - inputEventType = keyEventAction, - scanCode = event.scanCode, - source = event.source, - ) - - keyCodesToImitateUpAction.remove(event.keyCode) } return consumeEvent @@ -1516,7 +1590,7 @@ class KeyMapController( /** * @return whether any actions were performed. */ - private fun performActionsOnFailedDoublePress(event: Event): Boolean { + private fun performActionsOnFailedDoublePress(event: AlgoEvent): Boolean { var showToast = false val detectedTriggerIndexes = mutableListOf() val vibrateDurations = mutableListOf() @@ -1586,11 +1660,11 @@ class KeyMapController( delay(400) while (keyCodesToImitateUpAction.contains(keyCode)) { - useCase.imitateButtonPress( + useCase.imitateKeyEvent( keyCode = keyCode, metaState = metaStateFromKeyEvent.withFlag(metaStateFromActions), deviceId = deviceId, - inputEventType = InputEventType.DOWN, + action = KeyEvent.ACTION_DOWN, scanCode = scanCode, source = source, ) // use down action because this is what Android does @@ -1656,27 +1730,38 @@ class KeyMapController( } } - private fun Trigger.matchingEventAtIndex(event: Event, index: Int): Boolean { + private fun Trigger.matchingEventAtIndex(event: AlgoEvent, index: Int): Boolean { if (index >= this.keys.size) return false return this.keys[index].matchesEvent(event) } - private fun TriggerKey.matchesEvent(event: Event): Boolean { - if (this is KeyCodeTriggerKey && event is KeyCodeEvent) { + private fun TriggerKey.matchesEvent(event: AlgoEvent): Boolean { + if (this is KeyEventTriggerKey && event is KeyEventAlgo) { + val codeMatches = if (this.detectWithScancode()) { + this.scanCode == event.scanCode + } else { + this.keyCode == event.keyCode + } + return when (this.device) { - TriggerKeyDevice.Any -> this.keyCode == event.keyCode && this.clickType == event.clickType - is TriggerKeyDevice.External -> - this.keyCode == event.keyCode && - event.descriptor != null && - event.descriptor == this.device.descriptor && - this.clickType == event.clickType + KeyEventTriggerDevice.Any -> codeMatches && this.clickType == event.clickType + is KeyEventTriggerDevice.External -> + event.isExternal && codeMatches && event.descriptor == this.device.descriptor && this.clickType == event.clickType - TriggerKeyDevice.Internal -> - this.keyCode == event.keyCode && - event.descriptor == null && + KeyEventTriggerDevice.Internal -> + !event.isExternal && + codeMatches && this.clickType == event.clickType } + } else if (this is EvdevTriggerKey && event is EvdevEventAlgo) { + val codeMatches = if (this.detectWithScancode()) { + this.scanCode == event.scanCode + } else { + this.keyCode == event.keyCode + } + + return codeMatches && this.clickType == event.clickType && this.device == event.device } else if (this is AssistantTriggerKey && event is AssistantEvent) { return if (this.type == AssistantTriggerType.ANY || event.type == AssistantTriggerType.ANY) { this.clickType == event.clickType @@ -1693,22 +1778,36 @@ class KeyMapController( } private fun TriggerKey.matchesWithOtherKey(otherKey: TriggerKey): Boolean { - if (this is KeyCodeTriggerKey && otherKey is KeyCodeTriggerKey) { + if (this is KeyEventTriggerKey && otherKey is KeyEventTriggerKey) { + val codeMatches = if (this.detectWithScancode()) { + otherKey.detectWithScancode() && this.scanCode == otherKey.scanCode + } else { + this.keyCode == otherKey.keyCode + } + return when (this.device) { - TriggerKeyDevice.Any -> - this.keyCode == otherKey.keyCode && + KeyEventTriggerDevice.Any -> + codeMatches && this.clickType == otherKey.clickType - is TriggerKeyDevice.External -> - this.keyCode == otherKey.keyCode && + is KeyEventTriggerDevice.External -> + codeMatches && this.device == otherKey.device && this.clickType == otherKey.clickType - TriggerKeyDevice.Internal -> - this.keyCode == otherKey.keyCode && - otherKey.device == TriggerKeyDevice.Internal && + KeyEventTriggerDevice.Internal -> + codeMatches && + otherKey.device == KeyEventTriggerDevice.Internal && this.clickType == otherKey.clickType } + } else if (this is EvdevTriggerKey && otherKey is EvdevTriggerKey) { + val codeMatches = if (this.detectWithScancode()) { + otherKey.detectWithScancode() && this.scanCode == otherKey.scanCode + } else { + this.keyCode == otherKey.keyCode + } + + return codeMatches && this.clickType == otherKey.clickType && this.device == otherKey.device } else if (this is AssistantTriggerKey && otherKey is AssistantTriggerKey) { return this.type == otherKey.type && this.clickType == otherKey.clickType } else if (this is FloatingButtonKey && otherKey is FloatingButtonKey) { @@ -1763,56 +1862,74 @@ class KeyMapController( else -> false } - private val Event.withShortPress: Event + private val AlgoEvent.withShortPress: AlgoEvent get() = setClickType(clickType = ClickType.SHORT_PRESS) - private val Event.withLongPress: Event + private val AlgoEvent.withLongPress: AlgoEvent get() = setClickType(clickType = ClickType.LONG_PRESS) - private val Event.withDoublePress: Event + private val AlgoEvent.withDoublePress: AlgoEvent get() = setClickType(clickType = ClickType.DOUBLE_PRESS) + private val TriggerKey.consumeEvent: Boolean + get() { + return when (this) { + is AssistantTriggerKey -> true + is EvdevTriggerKey -> consumeEvent + is FingerprintTriggerKey -> true + is FloatingButtonKey -> true + is KeyEventTriggerKey -> consumeEvent + } + } + /** * Represents the kind of event a trigger key is expecting to happen. */ - private sealed class Event { + private sealed class AlgoEvent { abstract val clickType: ClickType? - fun setClickType(clickType: ClickType?): Event = when (this) { - is KeyCodeEvent -> this.copy(clickType = clickType) + fun setClickType(clickType: ClickType?): AlgoEvent = when (this) { + is EvdevEventAlgo -> this.copy(clickType = clickType) + is KeyEventAlgo -> this.copy(clickType = clickType) is AssistantEvent -> this.copy(clickType = clickType) is FloatingButtonEvent -> this.copy(clickType = clickType) is FingerprintGestureEvent -> this.copy(clickType = clickType) } } - private data class KeyCodeEvent( + private data class EvdevEventAlgo( + val devicePath: String, + val device: EvdevDeviceInfo, + val scanCode: Int, val keyCode: Int, override val clickType: ClickType?, - /** - * null if not an external device - */ - val descriptor: String?, + ) : AlgoEvent() + + private data class KeyEventAlgo( + val keyCode: Int, + override val clickType: ClickType?, + val descriptor: String, val deviceId: Int, + val isExternal: Boolean, val scanCode: Int, val repeatCount: Int, val source: Int, - ) : Event() + ) : AlgoEvent() private data class AssistantEvent( val type: AssistantTriggerType, override val clickType: ClickType?, - ) : Event() + ) : AlgoEvent() private data class FingerprintGestureEvent( val type: FingerprintGestureType, override val clickType: ClickType?, - ) : Event() + ) : AlgoEvent() private data class FloatingButtonEvent( val buttonUid: String, override val clickType: ClickType?, - ) : Event() + ) : AlgoEvent() private data class TriggerKeyLocation(val triggerIndex: Int, val keyIndex: Int) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapDetectionController.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapDetectionController.kt new file mode 100644 index 0000000000..955023690b --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyMapDetectionController.kt @@ -0,0 +1,119 @@ +package io.github.sds100.keymapper.base.detection + +import io.github.sds100.keymapper.base.actions.PerformActionsUseCase +import io.github.sds100.keymapper.base.constraints.DetectConstraintsUseCase +import io.github.sds100.keymapper.base.input.InputEventDetectionSource +import io.github.sds100.keymapper.base.input.InputEventHub +import io.github.sds100.keymapper.base.input.InputEventHubCallback +import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase +import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType +import io.github.sds100.keymapper.base.trigger.AssistantTriggerType +import io.github.sds100.keymapper.base.trigger.EvdevTriggerKey +import io.github.sds100.keymapper.base.trigger.RecordTriggerController +import io.github.sds100.keymapper.base.trigger.RecordTriggerState +import io.github.sds100.keymapper.base.trigger.Trigger +import io.github.sds100.keymapper.system.inputevents.KMEvdevEvent +import io.github.sds100.keymapper.system.inputevents.KMInputEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import timber.log.Timber + +class KeyMapDetectionController( + private val coroutineScope: CoroutineScope, + private val detectUseCase: DetectKeyMapsUseCase, + private val performActionsUseCase: PerformActionsUseCase, + private val detectConstraints: DetectConstraintsUseCase, + private val inputEventHub: InputEventHub, + private val pauseKeyMapsUseCase: PauseKeyMapsUseCase, + private val recordTriggerController: RecordTriggerController, +) : InputEventHubCallback { + companion object { + private const val INPUT_EVENT_HUB_ID = "key_map_controller" + } + + private val algorithm: KeyMapAlgorithm = + KeyMapAlgorithm(coroutineScope, detectUseCase, performActionsUseCase, detectConstraints) + + private val isPaused: StateFlow = + pauseKeyMapsUseCase.isPaused.stateIn(coroutineScope, SharingStarted.Eagerly, false) + + init { + // Must first register before collecting anything that may call reset() + inputEventHub.registerClient(INPUT_EVENT_HUB_ID, this, listOf(KMEvdevEvent.TYPE_KEY_EVENT)) + + coroutineScope.launch { + combine(detectUseCase.allKeyMapList, isPaused) { keyMapList, isPaused -> + algorithm.reset() + + if (isPaused) { + algorithm.loadKeyMaps(emptyList()) + inputEventHub.setGrabbedEvdevDevices(INPUT_EVENT_HUB_ID, emptyList()) + } else { + algorithm.loadKeyMaps(keyMapList) + // Only grab the triggers that are actually being listened to by the algorithm + grabEvdevDevicesForTriggers(algorithm.triggers) + } + }.launchIn(coroutineScope) + } + } + + override fun onInputEvent( + event: KMInputEvent, + detectionSource: InputEventDetectionSource, + ): Boolean { + if (isPaused.value) { + return false + } + + if (recordTriggerController.state.value is RecordTriggerState.CountingDown) { + return false + } + + return algorithm.onInputEvent(event) + } + + fun onFingerprintGesture(type: FingerprintGestureType) { + algorithm.onFingerprintGesture(type) + } + + fun onFloatingButtonDown(button: String) { + algorithm.onFloatingButtonDown(button) + } + + fun onFloatingButtonUp(button: String) { + algorithm.onFloatingButtonUp(button) + } + + fun onAssistantEvent(event: AssistantTriggerType) { + algorithm.onAssistantEvent(event) + } + + fun teardown() { + reset() + inputEventHub.unregisterClient(INPUT_EVENT_HUB_ID) + } + + private fun grabEvdevDevicesForTriggers(triggers: Array) { + val evdevDevices = triggers + .flatMap { trigger -> trigger.keys.filterIsInstance() } + .map { it.device } + .distinct() + .toList() + + Timber.i("Grab evdev devices for key map detection: ${evdevDevices.joinToString()}") + inputEventHub.setGrabbedEvdevDevices( + INPUT_EVENT_HUB_ID, + evdevDevices, + ) + } + + private fun reset() { + algorithm.reset() + inputEventHub.setGrabbedEvdevDevices(INPUT_EVENT_HUB_ID, emptyList()) + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyPressedCallback.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyPressedCallback.kt similarity index 62% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyPressedCallback.kt rename to base/src/main/java/io/github/sds100/keymapper/base/detection/KeyPressedCallback.kt index cada80083c..3b8377b56a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/KeyPressedCallback.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/KeyPressedCallback.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.base.keymaps.detection +package io.github.sds100.keymapper.base.detection interface KeyPressedCallback { fun onDownEvent(button: T) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/ParallelTriggerActionPerformer.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/ParallelTriggerActionPerformer.kt similarity index 84% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/ParallelTriggerActionPerformer.kt rename to base/src/main/java/io/github/sds100/keymapper/base/detection/ParallelTriggerActionPerformer.kt index dcdac06267..228b9a038f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/ParallelTriggerActionPerformer.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/ParallelTriggerActionPerformer.kt @@ -1,12 +1,12 @@ -package io.github.sds100.keymapper.base.keymaps.detection +package io.github.sds100.keymapper.base.detection import io.github.sds100.keymapper.base.actions.Action import io.github.sds100.keymapper.base.actions.ActionData import io.github.sds100.keymapper.base.actions.PerformActionsUseCase import io.github.sds100.keymapper.base.actions.RepeatMode -import io.github.sds100.keymapper.common.utils.InputEventType +import io.github.sds100.keymapper.common.utils.InputEventAction import io.github.sds100.keymapper.data.PreferenceDefaults -import io.github.sds100.keymapper.system.inputevents.InputEventUtils +import io.github.sds100.keymapper.system.inputevents.KeyEventUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -75,13 +75,13 @@ class ParallelTriggerActionPerformer( actionIsHeldDown[actionIndex] = true } - val actionInputEventType = when { - performUpAction -> InputEventType.UP - action.holdDown -> InputEventType.DOWN - else -> InputEventType.DOWN_UP + val actionInputEventAction = when { + performUpAction -> InputEventAction.UP + action.holdDown -> InputEventAction.DOWN + else -> InputEventAction.DOWN_UP } - performAction(action, actionInputEventType, metaState) + performAction(action, actionInputEventAction, metaState) if (action.repeat && action.holdDown) { delay(action.holdDownDuration?.toLong() ?: defaultHoldDownDuration.value) @@ -112,7 +112,7 @@ class ParallelTriggerActionPerformer( continue } - if (action.data is ActionData.InputKeyEvent && InputEventUtils.isModifierKey(action.data.keyCode)) { + if (action.data is ActionData.InputKeyEvent && KeyEventUtils.isModifierKey(action.data.keyCode)) { continue } @@ -124,13 +124,13 @@ class ParallelTriggerActionPerformer( while (isActive && continueRepeating) { if (action.holdDown) { - performAction(action, InputEventType.DOWN, metaState) + performAction(action, InputEventAction.DOWN, metaState) delay( action.holdDownDuration?.toLong() ?: defaultHoldDownDuration.value, ) - performAction(action, InputEventType.UP, metaState) + performAction(action, InputEventAction.UP, metaState) } else { - performAction(action, InputEventType.DOWN_UP, metaState) + performAction(action, InputEventAction.DOWN_UP, metaState) } repeatCount++ @@ -159,7 +159,7 @@ class ParallelTriggerActionPerformer( if (actionIsHeldDown[actionIndex]) { actionIsHeldDown[actionIndex] = false - performAction(action, InputEventType.UP, metaState) + performAction(action, InputEventAction.UP, metaState) } } } @@ -173,7 +173,7 @@ class ParallelTriggerActionPerformer( coroutineScope.launch { for ((index, isHeldDown) in actionIsHeldDown.withIndex()) { if (isHeldDown) { - performAction(actionList[index], inputEventType = InputEventType.UP, 0) + performAction(actionList[index], inputEventAction = InputEventAction.UP, 0) } } } @@ -190,11 +190,11 @@ class ParallelTriggerActionPerformer( private suspend fun performAction( action: Action, - inputEventType: InputEventType, + inputEventAction: InputEventAction, metaState: Int, ) { repeat(action.multiplier ?: 1) { - useCase.perform(action.data, inputEventType, metaState) + useCase.perform(action.data, inputEventAction, metaState) } } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/SequenceTriggerActionPerformer.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/SequenceTriggerActionPerformer.kt similarity index 85% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/SequenceTriggerActionPerformer.kt rename to base/src/main/java/io/github/sds100/keymapper/base/detection/SequenceTriggerActionPerformer.kt index a040f7f860..dc7d40816b 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/SequenceTriggerActionPerformer.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/SequenceTriggerActionPerformer.kt @@ -1,8 +1,8 @@ -package io.github.sds100.keymapper.base.keymaps.detection +package io.github.sds100.keymapper.base.detection import io.github.sds100.keymapper.base.actions.Action import io.github.sds100.keymapper.base.actions.PerformActionsUseCase -import io.github.sds100.keymapper.common.utils.InputEventType +import io.github.sds100.keymapper.common.utils.InputEventAction import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -37,7 +37,7 @@ class SequenceTriggerActionPerformer( private suspend fun performAction(action: Action, metaState: Int) { repeat(action.multiplier ?: 1) { - useCase.perform(action.data, InputEventType.DOWN_UP, metaState) + useCase.perform(action.data, InputEventAction.DOWN_UP, metaState) } } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/SimpleMappingController.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/SimpleMappingController.kt similarity index 88% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/SimpleMappingController.kt rename to base/src/main/java/io/github/sds100/keymapper/base/detection/SimpleMappingController.kt index c0bb27f2ef..872ca159d0 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/SimpleMappingController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/SimpleMappingController.kt @@ -1,12 +1,12 @@ -package io.github.sds100.keymapper.base.keymaps +package io.github.sds100.keymapper.base.detection import io.github.sds100.keymapper.base.actions.Action import io.github.sds100.keymapper.base.actions.PerformActionsUseCase import io.github.sds100.keymapper.base.actions.RepeatMode import io.github.sds100.keymapper.base.constraints.DetectConstraintsUseCase import io.github.sds100.keymapper.base.constraints.isSatisfied -import io.github.sds100.keymapper.base.keymaps.detection.DetectKeyMapsUseCase -import io.github.sds100.keymapper.common.utils.InputEventType +import io.github.sds100.keymapper.base.keymaps.KeyMap +import io.github.sds100.keymapper.common.utils.InputEventAction import io.github.sds100.keymapper.data.PreferenceDefaults import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart @@ -30,27 +30,27 @@ abstract class SimpleMappingController( private val defaultRepeatRate: StateFlow = performActionsUseCase.defaultRepeatRate.stateIn( coroutineScope, - SharingStarted.Eagerly, + SharingStarted.Companion.Eagerly, PreferenceDefaults.REPEAT_RATE.toLong(), ) private val forceVibrate: StateFlow = detectMappingUseCase.forceVibrate.stateIn( coroutineScope, - SharingStarted.Eagerly, + SharingStarted.Companion.Eagerly, PreferenceDefaults.FORCE_VIBRATE, ) private val defaultHoldDownDuration: StateFlow = performActionsUseCase.defaultHoldDownDuration.stateIn( coroutineScope, - SharingStarted.Eagerly, + SharingStarted.Companion.Eagerly, PreferenceDefaults.HOLD_DOWN_DURATION.toLong(), ) private val defaultVibrateDuration: StateFlow = detectMappingUseCase.defaultVibrateDuration.stateIn( coroutineScope, - SharingStarted.Eagerly, + SharingStarted.Companion.Eagerly, PreferenceDefaults.VIBRATION_DURATION.toLong(), ) @@ -97,9 +97,9 @@ abstract class SimpleMappingController( val alreadyBeingHeldDown = actionsBeingHeldDown.any { action.uid == it.uid } val keyEventAction = when { - action.holdDown && !alreadyBeingHeldDown -> InputEventType.DOWN - alreadyBeingHeldDown -> InputEventType.UP - else -> InputEventType.DOWN_UP + action.holdDown && !alreadyBeingHeldDown -> InputEventAction.DOWN + alreadyBeingHeldDown -> InputEventAction.UP + else -> InputEventAction.DOWN_UP } when { @@ -129,10 +129,10 @@ abstract class SimpleMappingController( private suspend fun performAction( action: Action, - inputEventType: InputEventType = InputEventType.DOWN_UP, + inputEventAction: InputEventAction = InputEventAction.DOWN_UP, ) { repeat(action.multiplier ?: 1) { - performActionsUseCase.perform(action.data, inputEventType) + performActionsUseCase.perform(action.data, inputEventAction) } } @@ -149,15 +149,15 @@ abstract class SimpleMappingController( while (continueRepeating) { val keyEventAction = when { - holdDown -> InputEventType.DOWN - else -> InputEventType.DOWN_UP + holdDown -> InputEventAction.DOWN + else -> InputEventAction.DOWN_UP } performAction(action, keyEventAction) if (holdDown) { delay(holdDownDuration) - performAction(action, InputEventType.UP) + performAction(action, InputEventAction.UP) } repeatCount++ @@ -186,7 +186,7 @@ abstract class SimpleMappingController( coroutineScope.launch { for (it in actionsBeingHeldDown) { - performAction(it, InputEventType.UP) + performAction(it, InputEventAction.UP) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/TriggerKeyMapFromOtherAppsController.kt b/base/src/main/java/io/github/sds100/keymapper/base/detection/TriggerKeyMapFromOtherAppsController.kt similarity index 91% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/TriggerKeyMapFromOtherAppsController.kt rename to base/src/main/java/io/github/sds100/keymapper/base/detection/TriggerKeyMapFromOtherAppsController.kt index c9d04b1f0d..9f0b8101e1 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/TriggerKeyMapFromOtherAppsController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/detection/TriggerKeyMapFromOtherAppsController.kt @@ -1,9 +1,8 @@ -package io.github.sds100.keymapper.base.keymaps.detection +package io.github.sds100.keymapper.base.detection import io.github.sds100.keymapper.base.actions.PerformActionsUseCase import io.github.sds100.keymapper.base.constraints.DetectConstraintsUseCase import io.github.sds100.keymapper.base.keymaps.KeyMap -import io.github.sds100.keymapper.base.keymaps.SimpleMappingController import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch diff --git a/base/src/main/java/io/github/sds100/keymapper/base/home/BaseHomeViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/BaseHomeViewModel.kt index 47e7d65a73..a1cc19ffde 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/home/BaseHomeViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/BaseHomeViewModel.kt @@ -4,8 +4,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.backup.BackupRestoreMappingsUseCase -import io.github.sds100.keymapper.base.keymaps.KeyMapListViewModel -import io.github.sds100.keymapper.base.keymaps.ListKeyMapsUseCase import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase import io.github.sds100.keymapper.base.sorting.SortKeyMapsUseCase diff --git a/base/src/main/java/io/github/sds100/keymapper/base/home/HomeKeyMapListScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/HomeKeyMapListScreen.kt index 1b4175e7c8..a739247f72 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/home/HomeKeyMapListScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/HomeKeyMapListScreen.kt @@ -59,9 +59,6 @@ import io.github.sds100.keymapper.base.backup.RestoreType import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.constraints.ConstraintMode import io.github.sds100.keymapper.base.groups.GroupListItemModel -import io.github.sds100.keymapper.base.keymaps.KeyMapAppBarState -import io.github.sds100.keymapper.base.keymaps.KeyMapList -import io.github.sds100.keymapper.base.keymaps.KeyMapListViewModel import io.github.sds100.keymapper.base.onboarding.OnboardingTapTarget import io.github.sds100.keymapper.base.sorting.SortBottomSheet import io.github.sds100.keymapper.base.trigger.DpadTriggerSetupBottomSheet diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapAppBarState.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapAppBarState.kt similarity index 86% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapAppBarState.kt rename to base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapAppBarState.kt index d2b56debf0..3d61ec8460 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapAppBarState.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapAppBarState.kt @@ -1,9 +1,7 @@ -package io.github.sds100.keymapper.base.keymaps +package io.github.sds100.keymapper.base.home import io.github.sds100.keymapper.base.constraints.ConstraintMode import io.github.sds100.keymapper.base.groups.GroupListItemModel -import io.github.sds100.keymapper.base.home.HomeWarningListItem -import io.github.sds100.keymapper.base.home.SelectedKeyMapsEnabled import io.github.sds100.keymapper.base.utils.ui.compose.ComposeChipModel sealed class KeyMapAppBarState { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapGroup.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapGroup.kt similarity index 72% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapGroup.kt rename to base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapGroup.kt index 8ddf927570..79d33b8888 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapGroup.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapGroup.kt @@ -1,6 +1,7 @@ -package io.github.sds100.keymapper.base.keymaps +package io.github.sds100.keymapper.base.home import io.github.sds100.keymapper.base.groups.Group +import io.github.sds100.keymapper.base.keymaps.KeyMap import io.github.sds100.keymapper.common.utils.State data class KeyMapGroup( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListAppBar.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListAppBar.kt index b85c8e1c4e..23b20e240a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListAppBar.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListAppBar.kt @@ -99,7 +99,6 @@ import io.github.sds100.keymapper.base.groups.GroupBreadcrumbRow import io.github.sds100.keymapper.base.groups.GroupConstraintRow import io.github.sds100.keymapper.base.groups.GroupListItemModel import io.github.sds100.keymapper.base.groups.GroupRow -import io.github.sds100.keymapper.base.keymaps.KeyMapAppBarState import io.github.sds100.keymapper.base.utils.ui.compose.ComposeChipModel import io.github.sds100.keymapper.base.utils.ui.compose.ComposeIconInfo import io.github.sds100.keymapper.base.utils.ui.compose.RadioButtonText diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListItemCreator.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListItemCreator.kt similarity index 86% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListItemCreator.kt rename to base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListItemCreator.kt index 31fceffd98..5d0267ad1e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListItemCreator.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListItemCreator.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.base.keymaps +package io.github.sds100.keymapper.base.home import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowForward @@ -9,24 +9,28 @@ import io.github.sds100.keymapper.base.actions.ActionUiHelper import io.github.sds100.keymapper.base.constraints.ConstraintErrorSnapshot import io.github.sds100.keymapper.base.constraints.ConstraintState import io.github.sds100.keymapper.base.constraints.ConstraintUiHelper +import io.github.sds100.keymapper.base.keymaps.ClickType +import io.github.sds100.keymapper.base.keymaps.DisplayKeyMapUseCase +import io.github.sds100.keymapper.base.keymaps.KeyMap import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType import io.github.sds100.keymapper.base.trigger.AssistantTriggerKey import io.github.sds100.keymapper.base.trigger.AssistantTriggerType +import io.github.sds100.keymapper.base.trigger.EvdevTriggerKey import io.github.sds100.keymapper.base.trigger.FingerprintTriggerKey import io.github.sds100.keymapper.base.trigger.FloatingButtonKey -import io.github.sds100.keymapper.base.trigger.KeyEventDetectionSource +import io.github.sds100.keymapper.base.trigger.KeyEventTriggerDevice +import io.github.sds100.keymapper.base.trigger.KeyEventTriggerKey import io.github.sds100.keymapper.base.trigger.KeyMapListItemModel import io.github.sds100.keymapper.base.trigger.Trigger import io.github.sds100.keymapper.base.trigger.TriggerErrorSnapshot -import io.github.sds100.keymapper.base.trigger.TriggerKeyDevice import io.github.sds100.keymapper.base.trigger.TriggerMode -import io.github.sds100.keymapper.base.utils.InputEventStrings +import io.github.sds100.keymapper.base.trigger.getCodeLabel import io.github.sds100.keymapper.base.utils.isFixable import io.github.sds100.keymapper.base.utils.ui.ResourceProvider import io.github.sds100.keymapper.base.utils.ui.compose.ComposeChipModel import io.github.sds100.keymapper.base.utils.ui.compose.ComposeIconInfo +import io.github.sds100.keymapper.common.utils.InputDeviceUtils import io.github.sds100.keymapper.common.utils.KMError -import io.github.sds100.keymapper.system.devices.InputDeviceUtils class KeyMapListItemCreator( private val displayMapping: DisplayKeyMapUseCase, @@ -56,12 +60,14 @@ class KeyMapListItemCreator( val triggerKeys = keyMap.trigger.keys.map { key -> when (key) { is AssistantTriggerKey -> assistantTriggerKeyName(key) - is io.github.sds100.keymapper.base.trigger.KeyCodeTriggerKey -> keyCodeTriggerKeyName( + is KeyEventTriggerKey -> keyEventTriggerKeyName( key, showDeviceDescriptors, ) + is FloatingButtonKey -> floatingButtonKeyName(key) is FingerprintTriggerKey -> fingerprintKeyName(key) + is EvdevTriggerKey -> evdevTriggerKeyName(key) } } @@ -243,8 +249,8 @@ class KeyMapListItemCreator( } } - private fun keyCodeTriggerKeyName( - key: io.github.sds100.keymapper.base.trigger.KeyCodeTriggerKey, + private fun keyEventTriggerKeyName( + key: KeyEventTriggerKey, showDeviceDescriptors: Boolean, ): String = buildString { when (key.clickType) { @@ -253,12 +259,12 @@ class KeyMapListItemCreator( else -> Unit } - append(InputEventStrings.keyCodeToString(key.keyCode)) + append(key.getCodeLabel(this@KeyMapListItemCreator)) val deviceName = when (key.device) { - is TriggerKeyDevice.Internal -> null - is TriggerKeyDevice.Any -> getString(R.string.any_device) - is TriggerKeyDevice.External -> { + is KeyEventTriggerDevice.Internal -> null + is KeyEventTriggerDevice.Any -> getString(R.string.any_device) + is KeyEventTriggerDevice.External -> { if (showDeviceDescriptors) { InputDeviceUtils.appendDeviceDescriptorToName( key.device.descriptor, @@ -272,8 +278,8 @@ class KeyMapListItemCreator( val parts = mutableListOf() - if (deviceName != null || key.detectionSource == KeyEventDetectionSource.INPUT_METHOD || !key.consumeEvent) { - if (key.detectionSource == KeyEventDetectionSource.INPUT_METHOD) { + if (deviceName != null || key.requiresIme || !key.consumeEvent) { + if (key.requiresIme) { parts.add(getString(R.string.flag_detect_from_input_method)) } @@ -293,6 +299,31 @@ class KeyMapListItemCreator( } } + private fun evdevTriggerKeyName(key: EvdevTriggerKey): String = buildString { + when (key.clickType) { + ClickType.LONG_PRESS -> append(longPressString).append(" ") + ClickType.DOUBLE_PRESS -> append(doublePressString).append(" ") + else -> Unit + } + + append(key.getCodeLabel(this@KeyMapListItemCreator)) + + val parts = buildList { + add("PRO") + add(key.device.name) + + if (!key.consumeEvent) { + add(getString(R.string.flag_dont_override_default_action)) + } + } + + if (parts.isNotEmpty()) { + append(" (") + append(parts.joinToString(separator = " $midDot ")) + append(")") + } + } + private fun assistantTriggerKeyName(key: AssistantTriggerKey): String = buildString { when (key.clickType) { ClickType.DOUBLE_PRESS -> append(doublePressString).append(" ") @@ -331,10 +362,6 @@ class KeyMapListItemCreator( labels.add(getString(R.string.flag_long_press_double_vibration)) } - if (trigger.isDetectingWhenScreenOffAllowed() && trigger.screenOffTrigger) { - labels.add(getString(R.string.flag_detect_triggers_screen_off)) - } - if (trigger.triggerFromOtherApps) { labels.add(getString(R.string.flag_trigger_from_other_apps)) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListScreen.kt similarity index 99% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListScreen.kt rename to base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListScreen.kt index c18452592d..965f6798d2 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListScreen.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.base.keymaps +package io.github.sds100.keymapper.base.home import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement @@ -483,7 +483,6 @@ private fun ActionConstraintChip( private fun getTriggerErrorMessage(error: TriggerError): String { return when (error) { TriggerError.DND_ACCESS_DENIED -> stringResource(R.string.trigger_error_dnd_access_denied) - TriggerError.SCREEN_OFF_ROOT_DENIED -> stringResource(R.string.trigger_error_screen_off_root_permission_denied) TriggerError.CANT_DETECT_IN_PHONE_CALL -> stringResource(R.string.trigger_error_cant_detect_in_phone_call) TriggerError.ASSISTANT_TRIGGER_NOT_PURCHASED -> stringResource(R.string.trigger_error_assistant_not_purchased) TriggerError.DPAD_IME_NOT_SELECTED -> stringResource(R.string.trigger_error_dpad_ime_not_selected) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListState.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListState.kt similarity index 86% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListState.kt rename to base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListState.kt index 16189cadba..6c5425e0a6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListState.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListState.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.base.keymaps +package io.github.sds100.keymapper.base.home import io.github.sds100.keymapper.base.trigger.KeyMapListItemModel import io.github.sds100.keymapper.common.utils.State diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListViewModel.kt similarity index 99% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListViewModel.kt rename to base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListViewModel.kt index 90c49ecea1..e25e99118c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapListViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/KeyMapListViewModel.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.base.keymaps +package io.github.sds100.keymapper.base.home import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -14,9 +14,8 @@ import io.github.sds100.keymapper.base.constraints.ConstraintUiHelper import io.github.sds100.keymapper.base.groups.Group import io.github.sds100.keymapper.base.groups.GroupFamily import io.github.sds100.keymapper.base.groups.GroupListItemModel -import io.github.sds100.keymapper.base.home.HomeWarningListItem -import io.github.sds100.keymapper.base.home.SelectedKeyMapsEnabled -import io.github.sds100.keymapper.base.home.ShowHomeScreenAlertsUseCase +import io.github.sds100.keymapper.base.keymaps.KeyMap +import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase import io.github.sds100.keymapper.base.onboarding.OnboardingTapTarget import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase import io.github.sds100.keymapper.base.sorting.SortKeyMapsUseCase diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ListKeyMapsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/home/ListKeyMapsUseCase.kt similarity index 98% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/ListKeyMapsUseCase.kt rename to base/src/main/java/io/github/sds100/keymapper/base/home/ListKeyMapsUseCase.kt index d1dc791061..2ff0ebd8dd 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ListKeyMapsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/home/ListKeyMapsUseCase.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.base.keymaps +package io.github.sds100.keymapper.base.home import android.database.sqlite.SQLiteConstraintException import io.github.sds100.keymapper.base.R @@ -12,6 +12,9 @@ import io.github.sds100.keymapper.base.constraints.ConstraintModeEntityMapper import io.github.sds100.keymapper.base.groups.Group import io.github.sds100.keymapper.base.groups.GroupEntityMapper import io.github.sds100.keymapper.base.groups.GroupFamily +import io.github.sds100.keymapper.base.keymaps.DisplayKeyMapUseCase +import io.github.sds100.keymapper.base.keymaps.KeyMap +import io.github.sds100.keymapper.base.keymaps.KeyMapEntityMapper import io.github.sds100.keymapper.base.utils.ui.ResourceProvider import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.State diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevHandleCache.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevHandleCache.kt new file mode 100644 index 0000000000..5d7147abe3 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/EvdevHandleCache.kt @@ -0,0 +1,79 @@ +package io.github.sds100.keymapper.base.input + +import android.os.Build +import androidx.annotation.RequiresApi +import io.github.sds100.keymapper.common.models.EvdevDeviceHandle +import io.github.sds100.keymapper.common.models.EvdevDeviceInfo +import io.github.sds100.keymapper.common.utils.onFailure +import io.github.sds100.keymapper.common.utils.valueIfFailure +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState +import io.github.sds100.keymapper.system.devices.DevicesAdapter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import timber.log.Timber +import java.util.concurrent.ConcurrentHashMap + +@RequiresApi(Build.VERSION_CODES.Q) +class EvdevHandleCache( + private val coroutineScope: CoroutineScope, + private val devicesAdapter: DevicesAdapter, + private val systemBridgeConnectionManager: SystemBridgeConnectionManager, +) { + private val devicesByPath: MutableMap = ConcurrentHashMap() + + init { + coroutineScope.launch { + combine( + devicesAdapter.connectedInputDevices, + systemBridgeConnectionManager.connectionState, + ) { _, connectionState -> + if (connectionState !is SystemBridgeConnectionState.Connected) { + devicesByPath.clear() + } else { + invalidate() + } + }.collect() + } + } + + fun getDevices(): List { + return devicesByPath.values.map { device -> + EvdevDeviceInfo( + name = device.name, + bus = device.bus, + vendor = device.vendor, + product = device.product, + ) + } + } + + fun getByPath(path: String): EvdevDeviceHandle? { + return devicesByPath[path] + } + + fun getByInfo(deviceInfo: EvdevDeviceInfo): EvdevDeviceHandle? { + return devicesByPath.values.firstOrNull { + it.name == deviceInfo.name && + it.bus == deviceInfo.bus && + it.vendor == deviceInfo.vendor && + it.product == deviceInfo.product + } + } + + suspend fun invalidate() { + // Do it on a separate thread in case there is deadlock + val newDevices = withContext(Dispatchers.IO) { + systemBridgeConnectionManager.run { bridge -> bridge.evdevInputDevices.associateBy { it.path } } + }.onFailure { error -> + Timber.e("Failed to get evdev input devices from system bridge $error") + }.valueIfFailure { emptyMap() } + + devicesByPath.clear() + devicesByPath.putAll(newDevices) + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InjectKeyEventModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InjectKeyEventModel.kt new file mode 100644 index 0000000000..276c6b4bb5 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InjectKeyEventModel.kt @@ -0,0 +1,30 @@ +package io.github.sds100.keymapper.base.input + +import android.os.SystemClock +import android.view.KeyEvent + +data class InjectKeyEventModel( + val keyCode: Int, + val action: Int, + val metaState: Int, + val deviceId: Int, + val scanCode: Int, + val source: Int, + val repeatCount: Int = 0, +) { + fun toAndroidKeyEvent(flags: Int = 0): KeyEvent { + val eventTime = SystemClock.uptimeMillis() + return KeyEvent( + eventTime, + eventTime, + action, + keyCode, + repeatCount, + metaState, + deviceId, + scanCode, + flags, + source, + ) + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventDetectionSource.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventDetectionSource.kt new file mode 100644 index 0000000000..8b06b10e01 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventDetectionSource.kt @@ -0,0 +1,7 @@ +package io.github.sds100.keymapper.base.input + +enum class InputEventDetectionSource { + ACCESSIBILITY_SERVICE, + INPUT_METHOD, + EVDEV, +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt new file mode 100644 index 0000000000..36530c8340 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHub.kt @@ -0,0 +1,423 @@ +package io.github.sds100.keymapper.base.input + +import android.os.Build +import android.view.KeyEvent +import androidx.annotation.RequiresApi +import io.github.sds100.keymapper.base.BuildConfig +import io.github.sds100.keymapper.base.system.inputmethod.ImeInputEventInjector +import io.github.sds100.keymapper.common.models.EvdevDeviceInfo +import io.github.sds100.keymapper.common.utils.KMError +import io.github.sds100.keymapper.common.utils.KMResult +import io.github.sds100.keymapper.common.utils.Success +import io.github.sds100.keymapper.common.utils.firstBlocking +import io.github.sds100.keymapper.common.utils.onFailure +import io.github.sds100.keymapper.common.utils.onSuccess +import io.github.sds100.keymapper.common.utils.then +import io.github.sds100.keymapper.common.utils.valueOrNull +import io.github.sds100.keymapper.data.Keys +import io.github.sds100.keymapper.data.repositories.PreferenceRepository +import io.github.sds100.keymapper.sysbridge.IEvdevCallback +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState +import io.github.sds100.keymapper.system.devices.DevicesAdapter +import io.github.sds100.keymapper.system.inputevents.KMEvdevEvent +import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent +import io.github.sds100.keymapper.system.inputevents.KMInputEvent +import io.github.sds100.keymapper.system.inputevents.KMKeyEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import timber.log.Timber +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class InputEventHubImpl @Inject constructor( + private val coroutineScope: CoroutineScope, + private val systemBridgeConnManager: SystemBridgeConnectionManager, + private val imeInputEventInjector: ImeInputEventInjector, + private val preferenceRepository: PreferenceRepository, + private val devicesAdapter: DevicesAdapter, +) : InputEventHub, IEvdevCallback.Stub() { + + companion object { + private const val INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2 + } + + private val clients: ConcurrentHashMap = ConcurrentHashMap() + + // Event queue for processing key events asynchronously in order + private val keyEventQueue = Channel(capacity = 100) + + @RequiresApi(Build.VERSION_CODES.Q) + private val evdevHandlesCache: EvdevHandleCache = EvdevHandleCache( + coroutineScope, + devicesAdapter, + systemBridgeConnManager, + ) + + private val logInputEventsEnabled: StateFlow = + preferenceRepository.get(Keys.log).map { isLogEnabled -> + if (isLogEnabled == true) { + isLogEnabled + } else { + BuildConfig.DEBUG + } + }.stateIn(coroutineScope, SharingStarted.Eagerly, false) + + private val invalidateGrabbedDevicesChannel: Channel = Channel(capacity = 10) + + init { + startKeyEventProcessingLoop() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + coroutineScope.launch { + systemBridgeConnManager.connectionState + .filterIsInstance() + .collect { + systemBridgeConnManager.run { bridge -> + bridge.registerEvdevCallback(this@InputEventHubImpl) + } + } + } + + coroutineScope.launch { + invalidateGrabbedDevicesChannel + .consumeAsFlow() + .map { + clients.values.flatMap { it.grabbedEvdevDevices }.toSet().toList() + } + .flowOn(Dispatchers.Default) + .collectLatest { devices -> + invalidateGrabbedEvdevDevices(devices) + } + } + } + } + + /** + * Starts a coroutine that processes key events from the queue in order on the IO thread.= + **/ + private fun startKeyEventProcessingLoop() { + coroutineScope.launch(Dispatchers.IO) { + for (event in keyEventQueue) { + try { + injectKeyEvent(event) + } catch (e: Exception) { + Timber.e(e, "Error processing key event: $event") + } + } + } + } + + @RequiresApi(Build.VERSION_CODES.Q) + override fun isSystemBridgeConnected(): Boolean { + return systemBridgeConnManager.connectionState.firstBlocking() is SystemBridgeConnectionState.Connected + } + + override fun onEvdevEventLoopStarted() { + Timber.i("On evdev event loop started") + + coroutineScope.launch { + invalidateGrabbedDevicesChannel.send(Unit) + } + } + + @RequiresApi(Build.VERSION_CODES.Q) + override fun onEvdevEvent( + devicePath: String?, + timeSec: Long, + timeUsec: Long, + type: Int, + code: Int, + value: Int, + androidCode: Int, + ): Boolean { + devicePath ?: return false + + val handle = evdevHandlesCache.getByPath(devicePath) ?: return false + val evdevEvent = KMEvdevEvent(handle, type, code, value, androidCode, timeSec, timeUsec) + + return onInputEvent(evdevEvent, InputEventDetectionSource.EVDEV) + } + + override fun onInputEvent( + event: KMInputEvent, + detectionSource: InputEventDetectionSource, + ): Boolean { + if (logInputEventsEnabled.value) { + logInputEvent(event) + } + + var consume = false + + for (clientContext in clients.values) { + if (event is KMEvdevEvent) { + if (!clientContext.evdevEventTypes.contains(event.type) || + clientContext.grabbedEvdevDevices.isEmpty() + ) { + continue + } + + val deviceInfo = EvdevDeviceInfo( + event.device.name, + event.device.bus, + event.device.vendor, + event.device.product, + ) + + // Only send events from evdev devices to the client if they grabbed it + if (!clientContext.grabbedEvdevDevices.contains(deviceInfo)) { + continue + } + + // Lazy evaluation may not execute this if its inlined + val result = clientContext.callback.onInputEvent(event, detectionSource) + consume = consume || result + } else { + // Lazy evaluation may not execute this if its inlined + val result = clientContext.callback.onInputEvent(event, detectionSource) + consume = consume || result + } + } + + if (logInputEventsEnabled.value) { + Timber.d("Consumed: $consume") + } + + return consume + } + + private fun logInputEvent(event: KMInputEvent) { + when (event) { + is KMEvdevEvent -> { + Timber.d( + "Evdev event: devicePath=${event.device.path}, deviceName=${event.device.name}, type=${event.type}, code=${event.code}, value=${event.value}", + ) + } + + is KMGamePadEvent -> { + Timber.d( + "GamePad event: deviceId=${event.deviceId}, axisHatX=${event.axisHatX}, axisHatY=${event.axisHatY}", + ) + } + + is KMKeyEvent -> { + when (event.action) { + KeyEvent.ACTION_DOWN -> { + Timber.d("Key down ${KeyEvent.keyCodeToString(event.keyCode)}: keyCode=${event.keyCode}, scanCode=${event.scanCode}, deviceId=${event.deviceId}, metaState=${event.metaState}, source=${event.source}") + } + + KeyEvent.ACTION_UP -> { + Timber.d("Key up ${KeyEvent.keyCodeToString(event.keyCode)}: keyCode=${event.keyCode}, scanCode=${event.scanCode}, deviceId=${event.deviceId}, metaState=${event.metaState}, source=${event.source}") + } + + else -> { + Timber.w("Unknown key event action: ${event.action}") + } + } + } + } + } + + override fun registerClient( + clientId: String, + callback: InputEventHubCallback, + eventTypes: List, + ) { + Timber.d("InputEventHub: Registering client $clientId") + if (clients.containsKey(clientId)) { + throw IllegalArgumentException("This client already has a callback registered!") + } + + clients[clientId] = ClientContext(callback, emptySet(), eventTypes.toSet()) + } + + override fun unregisterClient(clientId: String) { + Timber.d("InputEventHub: Unregistering client $clientId") + clients.remove(clientId) + invalidateGrabbedDevicesChannel.trySend(Unit) + } + + override fun setGrabbedEvdevDevices(clientId: String, devices: List) { + if (!clients.containsKey(clientId)) { + throw IllegalArgumentException("This client $clientId is not registered when trying to grab devices!") + } + + clients[clientId] = clients[clientId]!!.copy(grabbedEvdevDevices = devices.toSet()) + + invalidateGrabbedDevicesChannel.trySend(Unit) + } + + @RequiresApi(Build.VERSION_CODES.Q) + override fun grabAllEvdevDevices(clientId: String) { + if (!clients.containsKey(clientId)) { + throw IllegalArgumentException("This client $clientId is not registered when trying to grab devices!") + } + + val devices = evdevHandlesCache.getDevices().toSet() + clients[clientId] = clients[clientId]!!.copy(grabbedEvdevDevices = devices) + + invalidateGrabbedDevicesChannel.trySend(Unit) + } + + @RequiresApi(Build.VERSION_CODES.Q) + private suspend fun invalidateGrabbedEvdevDevices(evdevDevices: List) { + // Invalidate the cache first to make sure it is up to date. + evdevHandlesCache.invalidate() + + // Grabbing can block if there are other grabbing or event loop start/stop operations happening. + systemBridgeConnManager.run { bridge -> bridge.ungrabAllEvdevDevices() } + .onSuccess { Timber.i("Ungrabbed all evdev devices: $it") } + .then { + val handles: Array = + evdevDevices.mapNotNull { evdevHandlesCache.getByInfo(it)?.path }.toTypedArray() + + val result = + systemBridgeConnManager.run { bridge -> bridge.grabEvdevDeviceArray(handles) } + + if (result.valueOrNull() == true) { + Success(Unit) + } else { + KMError.Exception(Exception("Failed to grab")) + } + } + .onSuccess { result -> Timber.i("Grabbed evdev devices [${evdevDevices.joinToString { it.name }}]") } + .onFailure { + Timber.e("Failed to grab evdev devices.") + } + } + + @RequiresApi(Build.VERSION_CODES.Q) + override fun injectEvdevEvent( + devicePath: String, + type: Int, + code: Int, + value: Int, + ): KMResult { + return systemBridgeConnManager.run { bridge -> + bridge.writeEvdevEvent( + devicePath, + type, + code, + value, + ) + }.onSuccess { + Timber.d("Injected evdev event: $it") + } + } + + override suspend fun injectKeyEvent(event: InjectKeyEventModel): KMResult { + val isSysBridgeConnected = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && + systemBridgeConnManager.connectionState.value is SystemBridgeConnectionState.Connected + + if (isSysBridgeConnected) { + val androidKeyEvent = event.toAndroidKeyEvent(flags = KeyEvent.FLAG_FROM_SYSTEM) + + if (logInputEventsEnabled.value) { + Timber.d("Injecting key event $androidKeyEvent with system bridge") + } + + return withContext(Dispatchers.IO) { + // All injected events have their device id set to -1 (VIRTUAL_KEYBOARD_ID) + // in InputDispatcher.cpp injectInputEvent. + systemBridgeConnManager.run { bridge -> + bridge.injectInputEvent( + androidKeyEvent, + INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH, + ) + }.then { Success(Unit) } + } + } else { + imeInputEventInjector.inputKeyEvent(event) + return Success(Unit) + } + } + + override fun injectKeyEventAsync(event: InjectKeyEventModel): KMResult { + return try { + // Send the event to the queue for processing + if (keyEventQueue.trySend(event).isSuccess) { + Success(Unit) + } else { + KMError.Exception(Exception("Failed to queue key event")) + } + } catch (e: Exception) { + KMError.Exception(e) + } + } + + override fun onEmergencyKillSystemBridge() { + preferenceRepository.set(Keys.isSystemBridgeEmergencyKilled, true) + // Wait for it to be persisted. This method is a synchronous Binder call + // so the system bridge will not be killed until this returns + preferenceRepository.get(Keys.isSystemBridgeEmergencyKilled).filter { it == true } + .firstBlocking() + } + + private data class ClientContext( + val callback: InputEventHubCallback, + /** + * The evdev devices that this client wants to grab. + */ + val grabbedEvdevDevices: Set, + val evdevEventTypes: Set, + ) +} + +interface InputEventHub { + /** + * Register a client that will receive input events through the [callback]. The same [clientId] + * must be used for any requests to other methods in this class. The input events will either + * come from the key event relay service, accessibility service, or system bridge + * depending on the type of event and Key Mapper's permissions. + */ + fun registerClient( + clientId: String, + callback: InputEventHubCallback, + eventTypes: List, + ) + + fun unregisterClient(clientId: String) + + fun setGrabbedEvdevDevices(clientId: String, devices: List) + fun grabAllEvdevDevices(clientId: String) + + /** + * Inject a key event. This may either use the key event relay service or the system + * bridge depending on the permissions granted to Key Mapper. + * + * Must be suspend so injecting to the systembridge can happen on another thread. + */ + suspend fun injectKeyEvent(event: InjectKeyEventModel): KMResult + + /** + * Some callers don't care about the result from injecting and it isn't critical + * for it to be synchronous so calls to this + * will be put in a queue and processed. + */ + fun injectKeyEventAsync(event: InjectKeyEventModel): KMResult + + fun injectEvdevEvent(devicePath: String, type: Int, code: Int, value: Int): KMResult + + /** + * Send an input event to the connected clients. + * + * @return whether the input event was consumed by a client. + */ + fun onInputEvent(event: KMInputEvent, detectionSource: InputEventDetectionSource): Boolean + + fun isSystemBridgeConnected(): Boolean +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHubCallback.kt b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHubCallback.kt new file mode 100644 index 0000000000..68e6806eee --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/input/InputEventHubCallback.kt @@ -0,0 +1,10 @@ +package io.github.sds100.keymapper.base.input + +import io.github.sds100.keymapper.system.inputevents.KMInputEvent + +interface InputEventHubCallback { + /** + * @return whether to consume the event. + */ + fun onInputEvent(event: KMInputEvent, detectionSource: InputEventDetectionSource): Boolean +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/BaseConfigKeyMapScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/BaseConfigKeyMapScreen.kt index 06517591d7..abb4d7dbf6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/BaseConfigKeyMapScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/BaseConfigKeyMapScreen.kt @@ -66,7 +66,7 @@ fun BaseConfigKeyMapScreen( isKeyMapEnabled: Boolean, onKeyMapEnabledChange: (Boolean) -> Unit = {}, triggerScreen: @Composable () -> Unit, - actionScreen: @Composable () -> Unit, + actionsScreen: @Composable () -> Unit, constraintsScreen: @Composable () -> Unit, optionsScreen: @Composable () -> Unit, onBackClick: () -> Unit = {}, @@ -202,7 +202,7 @@ fun BaseConfigKeyMapScreen( ) { pageIndex -> when (tabs[pageIndex]) { ConfigKeyMapTab.TRIGGER -> triggerScreen() - ConfigKeyMapTab.ACTIONS -> actionScreen() + ConfigKeyMapTab.ACTIONS -> actionsScreen() ConfigKeyMapTab.CONSTRAINTS -> constraintsScreen() ConfigKeyMapTab.OPTIONS -> optionsScreen() ConfigKeyMapTab.TRIGGER_AND_ACTIONS -> { @@ -213,7 +213,7 @@ fun BaseConfigKeyMapScreen( topScreen = triggerScreen, bottomTitle = stringResource(R.string.tab_actions), bottomHelpUrl = actionsHelpUrl, - bottomScreen = actionScreen, + bottomScreen = actionsScreen, ) } else { HorizontalTwoScreens( @@ -222,7 +222,7 @@ fun BaseConfigKeyMapScreen( leftScreen = triggerScreen, rightTitle = stringResource(R.string.tab_actions), rightHelpUrl = actionsHelpUrl, - rightScreen = actionScreen, + rightScreen = actionsScreen, ) } } @@ -255,7 +255,7 @@ fun BaseConfigKeyMapScreen( topLeftScreen = triggerScreen, topRightTitle = stringResource(R.string.tab_actions), topRightHelpUrl = actionsHelpUrl, - topRightScreen = actionScreen, + topRightScreen = actionsScreen, bottomLeftTitle = stringResource(R.string.tab_constraints), bottomLeftHelpUrl = constraintsHelpUrl, bottomLeftScreen = constraintsScreen, @@ -557,7 +557,7 @@ private fun SmallScreenPreview() { modifier = Modifier.fillMaxSize(), isKeyMapEnabled = false, triggerScreen = {}, - actionScreen = {}, + actionsScreen = {}, constraintsScreen = {}, optionsScreen = {}, ) @@ -572,7 +572,7 @@ private fun MediumScreenPreview() { modifier = Modifier.fillMaxSize(), isKeyMapEnabled = true, triggerScreen = {}, - actionScreen = {}, + actionsScreen = {}, constraintsScreen = {}, optionsScreen = {}, ) @@ -587,7 +587,7 @@ private fun MediumScreenLandscapePreview() { modifier = Modifier.fillMaxSize(), isKeyMapEnabled = true, triggerScreen = {}, - actionScreen = {}, + actionsScreen = {}, constraintsScreen = {}, optionsScreen = {}, ) @@ -602,7 +602,7 @@ private fun LargeScreenPreview() { modifier = Modifier.fillMaxSize(), isKeyMapEnabled = true, triggerScreen = {}, - actionScreen = {}, + actionsScreen = {}, constraintsScreen = {}, optionsScreen = {}, ) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapOptionsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapOptionsViewModel.kt index 9067eabe14..63657c3fab 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapOptionsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapOptionsViewModel.kt @@ -4,6 +4,8 @@ import android.graphics.Color import android.graphics.drawable.Drawable import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.actions.ActionUiHelper +import io.github.sds100.keymapper.base.shortcuts.CreateKeyMapShortcutUseCase +import io.github.sds100.keymapper.base.trigger.ConfigTriggerUseCase import io.github.sds100.keymapper.base.utils.getFullMessage import io.github.sds100.keymapper.base.utils.ui.DialogModel import io.github.sds100.keymapper.base.utils.ui.DialogProvider @@ -25,7 +27,7 @@ import kotlinx.coroutines.launch class ConfigKeyMapOptionsViewModel( private val coroutineScope: CoroutineScope, - private val config: ConfigKeyMapUseCase, + private val config: ConfigTriggerUseCase, private val displayUseCase: DisplayKeyMapUseCase, private val createKeyMapShortcut: CreateKeyMapShortcutUseCase, private val dialogProvider: DialogProvider, @@ -64,10 +66,6 @@ class ConfigKeyMapOptionsViewModel( config.setLongPressDoubleVibrationEnabled(checked) } - override fun onScreenOffTriggerChanged(checked: Boolean) { - config.setTriggerWhenScreenOff(checked) - } - override fun onShowToastChanged(checked: Boolean) { config.setShowToastEnabled(checked) } @@ -164,9 +162,6 @@ class ConfigKeyMapOptionsViewModel( showLongPressDoubleVibration = keyMap.trigger.isLongPressDoubleVibrationAllowed(), longPressDoubleVibration = keyMap.trigger.longPressDoubleVibration, - showScreenOffTrigger = keyMap.trigger.isDetectingWhenScreenOffAllowed(), - screenOffTrigger = keyMap.trigger.screenOffTrigger, - triggerFromOtherApps = keyMap.trigger.triggerFromOtherApps, keyMapUid = keyMap.uid, isLauncherShortcutButtonEnabled = createKeyMapShortcut.isSupported, @@ -199,9 +194,6 @@ data class KeyMapOptionsState( val showLongPressDoubleVibration: Boolean, val longPressDoubleVibration: Boolean, - val showScreenOffTrigger: Boolean, - val screenOffTrigger: Boolean, - val triggerFromOtherApps: Boolean, val keyMapUid: String, val isLauncherShortcutButtonEnabled: Boolean, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapScreen.kt new file mode 100644 index 0000000000..e178a384ca --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapScreen.kt @@ -0,0 +1,61 @@ +package io.github.sds100.keymapper.base.keymaps + +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.sds100.keymapper.base.utils.ui.UnsavedChangesDialog + +@Composable +fun ConfigKeyMapScreen( + modifier: Modifier = Modifier, + snackbarHostState: SnackbarHostState, + keyMapViewModel: ConfigKeyMapViewModel, + triggerScreen: @Composable () -> Unit, + actionsScreen: @Composable () -> Unit, + constraintsScreen: @Composable () -> Unit, + optionsScreen: @Composable () -> Unit, +) { + val isKeyMapEnabled by keyMapViewModel.isEnabled.collectAsStateWithLifecycle() + val showActionTapTarget by keyMapViewModel.showActionsTapTarget.collectAsStateWithLifecycle() + val showConstraintTapTarget by keyMapViewModel.showConstraintsTapTarget.collectAsStateWithLifecycle() + var showBackDialog by rememberSaveable { mutableStateOf(false) } + + if (showBackDialog) { + UnsavedChangesDialog( + onDismiss = { showBackDialog = false }, + onDiscardClick = { + showBackDialog = false + keyMapViewModel.onBackClick() + }, + ) + } + + BaseConfigKeyMapScreen( + modifier = modifier, + isKeyMapEnabled = isKeyMapEnabled, + onKeyMapEnabledChange = keyMapViewModel::onEnabledChanged, + triggerScreen = triggerScreen, + actionsScreen = actionsScreen, + constraintsScreen = constraintsScreen, + optionsScreen = optionsScreen, + onBackClick = { + if (keyMapViewModel.isKeyMapEdited) { + showBackDialog = true + } else { + keyMapViewModel.onBackClick() + } + }, + onDoneClick = keyMapViewModel::onDoneClick, + snackbarHostState = snackbarHostState, + showActionTapTarget = showActionTapTarget, + onActionTapTargetCompleted = keyMapViewModel::onActionTapTargetCompleted, + showConstraintTapTarget = showConstraintTapTarget, + onConstraintTapTargetCompleted = keyMapViewModel::onConstraintTapTargetCompleted, + onSkipTutorialClick = keyMapViewModel::onSkipTutorialClick, + ) +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapState.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapState.kt new file mode 100644 index 0000000000..8db0a071b5 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapState.kt @@ -0,0 +1,122 @@ +package io.github.sds100.keymapper.base.keymaps + +import android.database.sqlite.SQLiteConstraintException +import io.github.sds100.keymapper.common.utils.State +import io.github.sds100.keymapper.common.utils.dataOrNull +import io.github.sds100.keymapper.common.utils.mapData +import io.github.sds100.keymapper.data.entities.FloatingButtonEntityWithLayout +import io.github.sds100.keymapper.data.repositories.FloatingButtonRepository +import io.github.sds100.keymapper.data.repositories.KeyMapRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ConfigKeyMapStateImpl @Inject constructor( + private val coroutineScope: CoroutineScope, + private val keyMapRepository: KeyMapRepository, + private val floatingButtonRepository: FloatingButtonRepository, +) : ConfigKeyMapState { + private var originalKeyMap: KeyMap? = null + + private val _keyMap: MutableStateFlow> = MutableStateFlow(State.Loading) + override val keyMap: StateFlow> = _keyMap.asStateFlow() + + override val floatingButtonToUse: MutableStateFlow = MutableStateFlow(null) + + init { + // Update button data in the key map whenever the floating buttons changes. + coroutineScope.launch { + floatingButtonRepository.buttonsList + .filterIsInstance>>() + .map { it.data } + .collectLatest(::updateFloatingButtonTriggerKeys) + } + } + + private fun updateFloatingButtonTriggerKeys(buttons: List) { + update { keyMap -> + keyMap.copy(trigger = keyMap.trigger.updateFloatingButtonData(buttons)) + } + } + + /** + * Whether any changes were made to the key map. + */ + override val isEdited: Boolean + get() = when (val keyMap = keyMap.value) { + is State.Data -> originalKeyMap?.let { it != keyMap.data } ?: false + State.Loading -> false + } + + override suspend fun loadKeyMap(uid: String) { + _keyMap.update { State.Loading } + val entity = keyMapRepository.get(uid) ?: return + val floatingButtons = floatingButtonRepository.buttonsList + .filterIsInstance>>() + .map { it.data } + .first() + + val keyMap = KeyMapEntityMapper.fromEntity(entity, floatingButtons) + _keyMap.update { State.Data(keyMap) } + originalKeyMap = keyMap + } + + override fun loadNewKeyMap(groupUid: String?) { + val keyMap = KeyMap(groupUid = groupUid) + _keyMap.update { State.Data(keyMap) } + originalKeyMap = keyMap + } + + // Useful for testing + fun setKeyMap(keyMap: KeyMap) { + _keyMap.update { State.Data(keyMap) } + originalKeyMap = keyMap + } + + override fun save() { + val keyMap = keyMap.value.dataOrNull() ?: return + + if (keyMap.dbId == null) { + val entity = KeyMapEntityMapper.toEntity(keyMap, 0) + try { + keyMapRepository.insert(entity) + } catch (e: SQLiteConstraintException) { + keyMapRepository.update(entity) + } + } else { + keyMapRepository.update(KeyMapEntityMapper.toEntity(keyMap, keyMap.dbId)) + } + } + + override fun restoreState(keyMap: KeyMap) { + _keyMap.update { State.Data(keyMap) } + } + + override fun update(block: (keyMap: KeyMap) -> KeyMap) { + _keyMap.update { value -> value.mapData { block.invoke(it) } } + } +} + +interface ConfigKeyMapState { + val keyMap: StateFlow> + val isEdited: Boolean + + fun update(block: (keyMap: KeyMap) -> KeyMap) + fun save() + + fun restoreState(keyMap: KeyMap) + suspend fun loadKeyMap(uid: String) + fun loadNewKeyMap(groupUid: String?) + + val floatingButtonToUse: MutableStateFlow +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapUseCase.kt deleted file mode 100644 index 5d87b95a92..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapUseCase.kt +++ /dev/null @@ -1,1046 +0,0 @@ -package io.github.sds100.keymapper.base.keymaps - -import android.database.sqlite.SQLiteConstraintException -import io.github.sds100.keymapper.base.actions.Action -import io.github.sds100.keymapper.base.actions.ActionData -import io.github.sds100.keymapper.base.actions.RepeatMode -import io.github.sds100.keymapper.base.constraints.Constraint -import io.github.sds100.keymapper.base.constraints.ConstraintMode -import io.github.sds100.keymapper.base.constraints.ConstraintState -import io.github.sds100.keymapper.base.floating.FloatingButtonEntityMapper -import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType -import io.github.sds100.keymapper.base.trigger.AssistantTriggerKey -import io.github.sds100.keymapper.base.trigger.AssistantTriggerType -import io.github.sds100.keymapper.base.trigger.FingerprintTriggerKey -import io.github.sds100.keymapper.base.trigger.FloatingButtonKey -import io.github.sds100.keymapper.base.trigger.KeyCodeTriggerKey -import io.github.sds100.keymapper.base.trigger.KeyEventDetectionSource -import io.github.sds100.keymapper.base.trigger.Trigger -import io.github.sds100.keymapper.base.trigger.TriggerKey -import io.github.sds100.keymapper.base.trigger.TriggerKeyDevice -import io.github.sds100.keymapper.base.trigger.TriggerMode -import io.github.sds100.keymapper.common.utils.KMResult -import io.github.sds100.keymapper.common.utils.State -import io.github.sds100.keymapper.common.utils.dataOrNull -import io.github.sds100.keymapper.common.utils.firstBlocking -import io.github.sds100.keymapper.common.utils.ifIsData -import io.github.sds100.keymapper.common.utils.moveElement -import io.github.sds100.keymapper.data.Keys -import io.github.sds100.keymapper.data.entities.FloatingButtonEntityWithLayout -import io.github.sds100.keymapper.data.repositories.FloatingButtonRepository -import io.github.sds100.keymapper.data.repositories.FloatingLayoutRepository -import io.github.sds100.keymapper.data.repositories.KeyMapRepository -import io.github.sds100.keymapper.data.repositories.PreferenceRepository -import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceAdapter -import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceEvent -import io.github.sds100.keymapper.system.devices.DevicesAdapter -import io.github.sds100.keymapper.system.devices.InputDeviceUtils -import io.github.sds100.keymapper.system.inputevents.InputEventUtils -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlinx.serialization.json.Json -import java.util.LinkedList -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class ConfigKeyMapUseCaseController @Inject constructor( - private val coroutineScope: CoroutineScope, - private val keyMapRepository: KeyMapRepository, - private val devicesAdapter: DevicesAdapter, - private val preferenceRepository: PreferenceRepository, - private val floatingLayoutRepository: FloatingLayoutRepository, - private val floatingButtonRepository: FloatingButtonRepository, - private val serviceAdapter: AccessibilityServiceAdapter, -) : ConfigKeyMapUseCase, - GetDefaultKeyMapOptionsUseCase by GetDefaultKeyMapOptionsUseCaseImpl( - coroutineScope, - preferenceRepository, - ) { - - private var originalKeyMap: KeyMap? = null - override val keyMap = MutableStateFlow>(State.Loading) - - override val floatingButtonToUse: MutableStateFlow = MutableStateFlow(null) - - private val showDeviceDescriptors: Flow = - preferenceRepository.get(Keys.showDeviceDescriptors).map { it == true } - - /** - * The most recently used is first. - */ - override val recentlyUsedActions: StateFlow> = - preferenceRepository.get(Keys.recentlyUsedActions) - .map(::getActionShortcuts) - .map { it.take(5) } - .stateIn( - coroutineScope, - SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000), - emptyList(), - ) - - /** - * The most recently used is first. - */ - override val recentlyUsedConstraints: StateFlow> = - combine( - preferenceRepository.get(Keys.recentlyUsedConstraints).map(::getConstraintShortcuts), - keyMap.filterIsInstance>(), - ) { shortcuts, keyMap -> - - // Do not include constraints that the key map already contains. - shortcuts - .filter { !keyMap.data.constraintState.constraints.contains(it) } - .take(5) - }.stateIn( - coroutineScope, - SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000), - emptyList(), - ) - - /** - * Whether any changes were made to the key map. - */ - override val isEdited: Boolean - get() = when (val keyMap = keyMap.value) { - is State.Data -> originalKeyMap?.let { it != keyMap.data } ?: false - State.Loading -> false - } - - init { - // Update button data in the key map whenever the floating buttons changes. - coroutineScope.launch { - floatingButtonRepository.buttonsList - .filterIsInstance>>() - .map { it.data } - .collectLatest(::updateFloatingButtonTriggerKeys) - } - } - - private fun updateFloatingButtonTriggerKeys(buttons: List) { - keyMap.update { keyMapState -> - if (keyMapState is State.Data) { - val trigger = keyMapState.data.trigger - val newKeyMap = - keyMapState.data.copy(trigger = trigger.updateFloatingButtonData(buttons)) - - State.Data(newKeyMap) - } else { - keyMapState - } - } - } - - override fun addConstraint(constraint: Constraint): Boolean { - var containsConstraint = false - - keyMap.value.ifIsData { mapping -> - val oldState = mapping.constraintState - - containsConstraint = oldState.constraints.contains(constraint) - val newState = oldState.copy(constraints = oldState.constraints.plus(constraint)) - - setConstraintState(newState) - } - - preferenceRepository.update( - Keys.recentlyUsedConstraints, - { old -> - val oldList: List = if (old == null) { - emptyList() - } else { - Json.decodeFromString>(old) - } - - val newShortcuts = LinkedList(oldList) - .also { it.addFirst(constraint) } - .distinct() - - Json.encodeToString(newShortcuts as List) - }, - ) - - return !containsConstraint - } - - override fun removeConstraint(id: String) { - keyMap.value.ifIsData { mapping -> - val newList = mapping.constraintState.constraints.toMutableSet().apply { - removeAll { it.uid == id } - } - - setConstraintState(mapping.constraintState.copy(constraints = newList)) - } - } - - override fun setAndMode() { - keyMap.value.ifIsData { mapping -> - setConstraintState(mapping.constraintState.copy(mode = ConstraintMode.AND)) - } - } - - override fun setOrMode() { - keyMap.value.ifIsData { mapping -> - setConstraintState(mapping.constraintState.copy(mode = ConstraintMode.OR)) - } - } - - override fun addAction(data: ActionData) = keyMap.value.ifIsData { mapping -> - mapping.actionList.toMutableList().apply { - add(createAction(data)) - setActionList(this) - } - - preferenceRepository.update( - Keys.recentlyUsedActions, - { old -> - val oldList: List = if (old == null) { - emptyList() - } else { - Json.decodeFromString>(old) - } - - val newShortcuts = LinkedList(oldList) - .also { it.addFirst(data) } - .distinct() - - Json.encodeToString(newShortcuts as List) - }, - ) - } - - override fun moveAction(fromIndex: Int, toIndex: Int) { - keyMap.value.ifIsData { mapping -> - mapping.actionList.toMutableList().apply { - moveElement(fromIndex, toIndex) - setActionList(this) - } - } - } - - override fun removeAction(uid: String) { - keyMap.value.ifIsData { mapping -> - mapping.actionList.toMutableList().apply { - removeAll { it.uid == uid } - setActionList(this) - } - } - } - - override suspend fun addFloatingButtonTriggerKey(buttonUid: String) { - floatingButtonToUse.update { null } - - editTrigger { trigger -> - val clickType = when (trigger.mode) { - is TriggerMode.Parallel -> trigger.mode.clickType - TriggerMode.Sequence -> ClickType.SHORT_PRESS - TriggerMode.Undefined -> ClickType.SHORT_PRESS - } - - // Check whether the trigger already contains the key because if so - // then it must be converted to a sequence trigger. - val containsKey = trigger.keys - .mapNotNull { it as? FloatingButtonKey } - .any { keyToCompare -> keyToCompare.buttonUid == buttonUid } - - val button = floatingButtonRepository.get(buttonUid) - ?.let { entity -> - FloatingButtonEntityMapper.fromEntity( - entity.button, - entity.layout.name, - ) - } - - val triggerKey = FloatingButtonKey( - buttonUid = buttonUid, - button = button, - clickType = clickType, - ) - - var newKeys = trigger.keys.plus(triggerKey) - - val newMode = when { - trigger.mode != TriggerMode.Sequence && containsKey -> TriggerMode.Sequence - newKeys.size <= 1 -> TriggerMode.Undefined - - /* Automatically make it a parallel trigger when the user makes a trigger with more than one key - because this is what most users are expecting when they make a trigger with multiple keys */ - newKeys.size == 2 && !containsKey -> { - newKeys = newKeys.map { it.setClickType(triggerKey.clickType) } - TriggerMode.Parallel(triggerKey.clickType) - } - - else -> trigger.mode - } - - trigger.copy(keys = newKeys, mode = newMode) - } - } - - override fun addAssistantTriggerKey(type: AssistantTriggerType) = editTrigger { trigger -> - val clickType = when (trigger.mode) { - is TriggerMode.Parallel -> trigger.mode.clickType - TriggerMode.Sequence -> ClickType.SHORT_PRESS - TriggerMode.Undefined -> ClickType.SHORT_PRESS - } - - // Check whether the trigger already contains the key because if so - // then it must be converted to a sequence trigger. - val containsAssistantKey = trigger.keys.any { it is AssistantTriggerKey } - - val triggerKey = AssistantTriggerKey(type = type, clickType = clickType) - - val newKeys = trigger.keys.plus(triggerKey).map { it.setClickType(ClickType.SHORT_PRESS) } - - val newMode = when { - trigger.mode != TriggerMode.Sequence && containsAssistantKey -> TriggerMode.Sequence - newKeys.size <= 1 -> TriggerMode.Undefined - - /* Automatically make it a parallel trigger when the user makes a trigger with more than one key - because this is what most users are expecting when they make a trigger with multiple keys. - - It must be a short press because long pressing the assistant key isn't supported. - */ - !containsAssistantKey -> TriggerMode.Parallel(ClickType.SHORT_PRESS) - else -> trigger.mode - } - - trigger.copy(keys = newKeys, mode = newMode) - } - - override fun addFingerprintGesture(type: FingerprintGestureType) = editTrigger { trigger -> - val clickType = when (trigger.mode) { - is TriggerMode.Parallel -> trigger.mode.clickType - TriggerMode.Sequence -> ClickType.SHORT_PRESS - TriggerMode.Undefined -> ClickType.SHORT_PRESS - } - - // Check whether the trigger already contains the key because if so - // then it must be converted to a sequence trigger. - val containsFingerprintGesture = trigger.keys.any { it is FingerprintTriggerKey } - - val triggerKey = FingerprintTriggerKey(type = type, clickType = clickType) - - val newKeys = trigger.keys.plus(triggerKey).map { it.setClickType(ClickType.SHORT_PRESS) } - - val newMode = when { - trigger.mode != TriggerMode.Sequence && containsFingerprintGesture -> TriggerMode.Sequence - newKeys.size <= 1 -> TriggerMode.Undefined - - /* Automatically make it a parallel trigger when the user makes a trigger with more than one key - because this is what most users are expecting when they make a trigger with multiple keys. - - It must be a short press because long pressing the assistant key isn't supported. - */ - !containsFingerprintGesture -> TriggerMode.Parallel(ClickType.SHORT_PRESS) - else -> trigger.mode - } - - trigger.copy(keys = newKeys, mode = newMode) - } - - override fun addKeyCodeTriggerKey( - keyCode: Int, - device: TriggerKeyDevice, - detectionSource: KeyEventDetectionSource, - ) = editTrigger { trigger -> - val clickType = when (trigger.mode) { - is TriggerMode.Parallel -> trigger.mode.clickType - TriggerMode.Sequence -> ClickType.SHORT_PRESS - TriggerMode.Undefined -> ClickType.SHORT_PRESS - } - - // Check whether the trigger already contains the key because if so - // then it must be converted to a sequence trigger. - val containsKey = trigger.keys - .mapNotNull { it as? KeyCodeTriggerKey } - .any { keyToCompare -> - keyToCompare.keyCode == keyCode && keyToCompare.device.isSameDevice(device) - } - - var consumeKeyEvent = true - - // Issue #753 - if (InputEventUtils.isModifierKey(keyCode)) { - consumeKeyEvent = false - } - - val triggerKey = KeyCodeTriggerKey( - keyCode = keyCode, - device = device, - clickType = clickType, - consumeEvent = consumeKeyEvent, - detectionSource = detectionSource, - ) - - var newKeys = trigger.keys.plus(triggerKey) - - val newMode = when { - trigger.mode != TriggerMode.Sequence && containsKey -> TriggerMode.Sequence - newKeys.size <= 1 -> TriggerMode.Undefined - - /* Automatically make it a parallel trigger when the user makes a trigger with more than one key - because this is what most users are expecting when they make a trigger with multiple keys */ - newKeys.size == 2 && !containsKey -> { - newKeys = newKeys.map { it.setClickType(triggerKey.clickType) } - TriggerMode.Parallel(triggerKey.clickType) - } - - else -> trigger.mode - } - - trigger.copy(keys = newKeys, mode = newMode) - } - - override fun removeTriggerKey(uid: String) = editTrigger { trigger -> - val newKeys = trigger.keys.toMutableList().apply { - removeAll { it.uid == uid } - } - - val newMode = when { - newKeys.size <= 1 -> TriggerMode.Undefined - else -> trigger.mode - } - - trigger.copy(keys = newKeys, mode = newMode) - } - - override fun moveTriggerKey(fromIndex: Int, toIndex: Int) = editTrigger { trigger -> - trigger.copy( - keys = trigger.keys.toMutableList().apply { - add(toIndex, removeAt(fromIndex)) - }, - ) - } - - override fun getTriggerKey(uid: String): TriggerKey? { - return keyMap.value.dataOrNull()?.trigger?.keys?.find { it.uid == uid } - } - - override fun setParallelTriggerMode() = editTrigger { trigger -> - if (trigger.mode is TriggerMode.Parallel) { - return@editTrigger trigger - } - - // undefined mode only allowed if one or no keys - if (trigger.keys.size <= 1) { - return@editTrigger trigger.copy(mode = TriggerMode.Undefined) - } - - val oldKeys = trigger.keys - var newKeys = oldKeys - - // set all the keys to a short press if coming from a non-parallel trigger - // because they must all be the same click type and can't all be double pressed - newKeys = newKeys - .map { key -> key.setClickType(clickType = ClickType.SHORT_PRESS) } - // remove duplicates of keys that have the same keycode and device id - .distinctBy { key -> - when (key) { - // You can't mix assistant trigger types in a parallel trigger because there is no notion of a "down" key event, which means they can't be pressed at the same time - is AssistantTriggerKey, is FingerprintTriggerKey -> 0 - is KeyCodeTriggerKey -> Pair( - key.keyCode, - key.device, - ) - - is FloatingButtonKey -> key.buttonUid - } - } - - val newMode = if (newKeys.size <= 1) { - TriggerMode.Undefined - } else { - TriggerMode.Parallel(newKeys[0].clickType) - } - - trigger.copy(keys = newKeys, mode = newMode) - } - - override fun setSequenceTriggerMode() = editTrigger { trigger -> - if (trigger.mode == TriggerMode.Sequence) return@editTrigger trigger - // undefined mode only allowed if one or no keys - if (trigger.keys.size <= 1) { - return@editTrigger trigger.copy(mode = TriggerMode.Undefined) - } - - trigger.copy(mode = TriggerMode.Sequence) - } - - override fun setUndefinedTriggerMode() = editTrigger { trigger -> - if (trigger.mode == TriggerMode.Undefined) return@editTrigger trigger - - // undefined mode only allowed if one or no keys - if (trigger.keys.size > 1) { - return@editTrigger trigger - } - - trigger.copy(mode = TriggerMode.Undefined) - } - - override fun setTriggerShortPress() { - editTrigger { oldTrigger -> - if (oldTrigger.mode == TriggerMode.Sequence) { - return@editTrigger oldTrigger - } - - val newKeys = oldTrigger.keys.map { it.setClickType(clickType = ClickType.SHORT_PRESS) } - val newMode = if (newKeys.size <= 1) { - TriggerMode.Undefined - } else { - TriggerMode.Parallel(ClickType.SHORT_PRESS) - } - oldTrigger.copy(keys = newKeys, mode = newMode) - } - } - - override fun setTriggerLongPress() { - editTrigger { trigger -> - if (trigger.mode == TriggerMode.Sequence) { - return@editTrigger trigger - } - - // You can't set the trigger to a long press if it contains a key - // that isn't detected with key codes. This is because there aren't - // separate key events for the up and down press that can be timed. - if (trigger.keys.any { !it.allowedLongPress }) { - return@editTrigger trigger - } - - val newKeys = trigger.keys.map { it.setClickType(clickType = ClickType.LONG_PRESS) } - val newMode = if (newKeys.size <= 1) { - TriggerMode.Undefined - } else { - TriggerMode.Parallel(ClickType.LONG_PRESS) - } - - trigger.copy(keys = newKeys, mode = newMode) - } - } - - override fun setTriggerDoublePress() { - editTrigger { trigger -> - if (trigger.mode != TriggerMode.Undefined) { - return@editTrigger trigger - } - - if (trigger.keys.any { !it.allowedDoublePress }) { - return@editTrigger trigger - } - - val newKeys = trigger.keys.map { it.setClickType(clickType = ClickType.DOUBLE_PRESS) } - val newMode = TriggerMode.Undefined - - trigger.copy(keys = newKeys, mode = newMode) - } - } - - override fun setTriggerKeyClickType(keyUid: String, clickType: ClickType) { - editTriggerKey(keyUid) { key -> - key.setClickType(clickType = clickType) - } - } - - override fun setTriggerKeyDevice(keyUid: String, device: TriggerKeyDevice) { - editTriggerKey(keyUid) { key -> - if (key is KeyCodeTriggerKey) { - key.copy(device = device) - } else { - key - } - } - } - - override fun setTriggerKeyConsumeKeyEvent(keyUid: String, consumeKeyEvent: Boolean) { - editTriggerKey(keyUid) { key -> - if (key is KeyCodeTriggerKey) { - key.copy(consumeEvent = consumeKeyEvent) - } else { - key - } - } - } - - override fun setAssistantTriggerKeyType(keyUid: String, type: AssistantTriggerType) { - editTriggerKey(keyUid) { key -> - if (key is AssistantTriggerKey) { - key.copy(type = type) - } else { - key - } - } - } - - override fun setFingerprintGestureType(keyUid: String, type: FingerprintGestureType) { - editTriggerKey(keyUid) { key -> - if (key is FingerprintTriggerKey) { - key.copy(type = type) - } else { - key - } - } - } - - override fun setVibrateEnabled(enabled: Boolean) = editTrigger { it.copy(vibrate = enabled) } - - override fun setVibrationDuration(duration: Int) = editTrigger { trigger -> - if (duration == defaultVibrateDuration.value) { - trigger.copy(vibrateDuration = null) - } else { - trigger.copy(vibrateDuration = duration) - } - } - - override fun setLongPressDelay(delay: Int) = editTrigger { trigger -> - if (delay == defaultLongPressDelay.value) { - trigger.copy(longPressDelay = null) - } else { - trigger.copy(longPressDelay = delay) - } - } - - override fun setDoublePressDelay(delay: Int) { - editTrigger { trigger -> - if (delay == defaultDoublePressDelay.value) { - trigger.copy(doublePressDelay = null) - } else { - trigger.copy(doublePressDelay = delay) - } - } - } - - override fun setSequenceTriggerTimeout(delay: Int) { - editTrigger { trigger -> - if (delay == defaultSequenceTriggerTimeout.value) { - trigger.copy(sequenceTriggerTimeout = null) - } else { - trigger.copy(sequenceTriggerTimeout = delay) - } - } - } - - override fun setLongPressDoubleVibrationEnabled(enabled: Boolean) { - editTrigger { it.copy(longPressDoubleVibration = enabled) } - } - - override fun setTriggerWhenScreenOff(enabled: Boolean) { - editTrigger { it.copy(screenOffTrigger = enabled) } - } - - override fun setTriggerFromOtherAppsEnabled(enabled: Boolean) { - editTrigger { it.copy(triggerFromOtherApps = enabled) } - } - - override fun setShowToastEnabled(enabled: Boolean) { - editTrigger { it.copy(showToast = enabled) } - } - - override fun getAvailableTriggerKeyDevices(): List { - val externalTriggerKeyDevices = sequence { - val inputDevices = - devicesAdapter.connectedInputDevices.value.dataOrNull() ?: emptyList() - - val showDeviceDescriptors = showDeviceDescriptors.firstBlocking() - - inputDevices.forEach { device -> - - if (device.isExternal) { - val name = if (showDeviceDescriptors) { - InputDeviceUtils.appendDeviceDescriptorToName( - device.descriptor, - device.name, - ) - } else { - device.name - } - - yield(TriggerKeyDevice.External(device.descriptor, name)) - } - } - } - - return sequence { - yield(TriggerKeyDevice.Internal) - yield(TriggerKeyDevice.Any) - yieldAll(externalTriggerKeyDevices) - }.toList() - } - - override fun setEnabled(enabled: Boolean) { - editKeyMap { it.copy(isEnabled = enabled) } - } - - override fun setActionData(uid: String, data: ActionData) { - editKeyMap { keyMap -> - val newActionList = keyMap.actionList.map { action -> - if (action.uid == uid) { - action.copy(data = data) - } else { - action - } - } - - keyMap.copy( - actionList = newActionList, - ) - } - } - - override fun setActionRepeatEnabled(uid: String, repeat: Boolean) { - setActionOption(uid) { action -> action.copy(repeat = repeat) } - } - - override fun setActionRepeatRate(uid: String, repeatRate: Int) { - setActionOption(uid) { action -> - if (repeatRate == defaultRepeatRate.value) { - action.copy(repeatRate = null) - } else { - action.copy(repeatRate = repeatRate) - } - } - } - - override fun setActionRepeatDelay(uid: String, repeatDelay: Int) { - setActionOption(uid) { action -> - if (repeatDelay == defaultRepeatDelay.value) { - action.copy(repeatDelay = null) - } else { - action.copy(repeatDelay = repeatDelay) - } - } - } - - override fun setActionRepeatLimit(uid: String, repeatLimit: Int) { - setActionOption(uid) { action -> - if (action.repeatMode == RepeatMode.LIMIT_REACHED) { - if (repeatLimit == 1) { - action.copy(repeatLimit = null) - } else { - action.copy(repeatLimit = repeatLimit) - } - } else { - if (repeatLimit == Int.MAX_VALUE) { - action.copy(repeatLimit = null) - } else { - action.copy(repeatLimit = repeatLimit) - } - } - } - } - - override fun setActionHoldDownEnabled(uid: String, holdDown: Boolean) = - setActionOption(uid) { it.copy(holdDown = holdDown) } - - override fun setActionHoldDownDuration(uid: String, holdDownDuration: Int) { - setActionOption(uid) { action -> - if (holdDownDuration == defaultHoldDownDuration.value) { - action.copy(holdDownDuration = null) - } else { - action.copy(holdDownDuration = holdDownDuration) - } - } - } - - override fun setActionStopRepeatingWhenTriggerPressedAgain(uid: String) = - setActionOption(uid) { it.copy(repeatMode = RepeatMode.TRIGGER_PRESSED_AGAIN) } - - override fun setActionStopRepeatingWhenLimitReached(uid: String) = - setActionOption(uid) { it.copy(repeatMode = RepeatMode.LIMIT_REACHED) } - - override fun setActionStopRepeatingWhenTriggerReleased(uid: String) = - setActionOption(uid) { it.copy(repeatMode = RepeatMode.TRIGGER_RELEASED) } - - override fun setActionStopHoldingDownWhenTriggerPressedAgain(uid: String, enabled: Boolean) = - setActionOption(uid) { it.copy(stopHoldDownWhenTriggerPressedAgain = enabled) } - - override fun setActionMultiplier(uid: String, multiplier: Int) { - setActionOption(uid) { action -> - if (multiplier == 1) { - action.copy(multiplier = null) - } else { - action.copy(multiplier = multiplier) - } - } - } - - override fun setDelayBeforeNextAction(uid: String, delay: Int) { - setActionOption(uid) { action -> - if (delay == 0) { - action.copy(delayBeforeNextAction = null) - } else { - action.copy(delayBeforeNextAction = delay) - } - } - } - - private fun createAction(data: ActionData): Action { - var holdDown = false - var repeat = false - - if (data is ActionData.InputKeyEvent) { - val trigger = keyMap.value.dataOrNull()?.trigger - - val containsDpadKey: Boolean = if (trigger == null) { - false - } else { - trigger.keys - .mapNotNull { it as? KeyCodeTriggerKey } - .any { InputEventUtils.isDpadKeyCode(it.keyCode) } - } - - if (InputEventUtils.isModifierKey(data.keyCode) || containsDpadKey) { - holdDown = true - repeat = false - } else { - repeat = true - } - } - - if (data is ActionData.Volume.Down || data is ActionData.Volume.Up || data is ActionData.Volume.Stream) { - repeat = true - } - - if (data is ActionData.AnswerCall) { - addConstraint(Constraint.PhoneRinging()) - } - - if (data is ActionData.EndCall) { - addConstraint(Constraint.InPhoneCall()) - } - - return Action( - data = data, - repeat = repeat, - holdDown = holdDown, - ) - } - - private fun setActionList(actionList: List) { - editKeyMap { it.copy(actionList = actionList) } - } - - private fun setConstraintState(constraintState: ConstraintState) { - editKeyMap { it.copy(constraintState = constraintState) } - } - - override suspend fun loadKeyMap(uid: String) { - keyMap.update { State.Loading } - val entity = keyMapRepository.get(uid) ?: return - val floatingButtons = floatingButtonRepository.buttonsList - .filterIsInstance>>() - .map { it.data } - .first() - - val keyMap = KeyMapEntityMapper.fromEntity(entity, floatingButtons) - this.keyMap.update { State.Data(keyMap) } - originalKeyMap = keyMap - } - - override fun loadNewKeyMap(groupUid: String?) { - val keyMap = KeyMap(groupUid = groupUid) - this.keyMap.update { State.Data(keyMap) } - originalKeyMap = keyMap - } - - override fun save() { - val keyMap = keyMap.value.dataOrNull() ?: return - - if (keyMap.dbId == null) { - val entity = KeyMapEntityMapper.toEntity(keyMap, 0) - try { - keyMapRepository.insert(entity) - } catch (e: SQLiteConstraintException) { - keyMapRepository.update(entity) - } - } else { - keyMapRepository.update(KeyMapEntityMapper.toEntity(keyMap, keyMap.dbId)) - } - } - - override fun restoreState(keyMap: KeyMap) { - this.keyMap.value = State.Data(keyMap) - } - - override suspend fun getFloatingLayoutCount(): Int { - return floatingLayoutRepository.count() - } - - override suspend fun sendServiceEvent(event: AccessibilityServiceEvent): KMResult<*> { - return serviceAdapter.send(event) - } - - private fun setActionOption( - uid: String, - block: (action: Action) -> Action, - ) { - editKeyMap { keyMap -> - val newActionList = keyMap.actionList.map { action -> - if (action.uid == uid) { - block.invoke(action) - } else { - action - } - } - - keyMap.copy( - actionList = newActionList, - ) - } - } - - private suspend fun getActionShortcuts(json: String?): List { - if (json == null) { - return emptyList() - } - - try { - return withContext(Dispatchers.Default) { - val list = Json.decodeFromString>(json) - - list.distinct() - } - } catch (_: Exception) { - preferenceRepository.set(Keys.recentlyUsedActions, null) - return emptyList() - } - } - - private suspend fun getConstraintShortcuts(json: String?): List { - if (json == null) { - return emptyList() - } - - try { - return withContext(Dispatchers.Default) { - val list = Json.decodeFromString>(json) - - list.distinct() - } - } catch (_: Exception) { - preferenceRepository.set(Keys.recentlyUsedConstraints, null) - return emptyList() - } - } - - private inline fun editTrigger(block: (trigger: Trigger) -> Trigger) { - editKeyMap { keyMap -> - val newTrigger = block(keyMap.trigger) - - keyMap.copy(trigger = newTrigger) - } - } - - private fun editTriggerKey(uid: String, block: (key: TriggerKey) -> TriggerKey) { - editTrigger { oldTrigger -> - val newKeys = oldTrigger.keys.map { - if (it.uid == uid) { - block.invoke(it) - } else { - it - } - } - - oldTrigger.copy(keys = newKeys) - } - } - - private inline fun editKeyMap(block: (keymap: KeyMap) -> KeyMap) { - keyMap.value.ifIsData { keyMap.value = State.Data(block.invoke(it)) } - } -} - -interface ConfigKeyMapUseCase : GetDefaultKeyMapOptionsUseCase { - val keyMap: Flow> - val isEdited: Boolean - - fun save() - - fun setEnabled(enabled: Boolean) - - fun addAction(data: ActionData) - fun moveAction(fromIndex: Int, toIndex: Int) - fun removeAction(uid: String) - - val recentlyUsedActions: Flow> - fun setActionData(uid: String, data: ActionData) - fun setActionMultiplier(uid: String, multiplier: Int) - fun setDelayBeforeNextAction(uid: String, delay: Int) - fun setActionRepeatRate(uid: String, repeatRate: Int) - fun setActionRepeatLimit(uid: String, repeatLimit: Int) - fun setActionStopRepeatingWhenTriggerPressedAgain(uid: String) - fun setActionStopRepeatingWhenLimitReached(uid: String) - fun setActionRepeatEnabled(uid: String, repeat: Boolean) - fun setActionRepeatDelay(uid: String, repeatDelay: Int) - fun setActionHoldDownEnabled(uid: String, holdDown: Boolean) - fun setActionHoldDownDuration(uid: String, holdDownDuration: Int) - fun setActionStopRepeatingWhenTriggerReleased(uid: String) - - fun setActionStopHoldingDownWhenTriggerPressedAgain(uid: String, enabled: Boolean) - - val recentlyUsedConstraints: Flow> - fun addConstraint(constraint: Constraint): Boolean - fun removeConstraint(id: String) - fun setAndMode() - fun setOrMode() - suspend fun sendServiceEvent(event: AccessibilityServiceEvent): KMResult<*> - - // trigger - fun addKeyCodeTriggerKey( - keyCode: Int, - device: TriggerKeyDevice, - detectionSource: KeyEventDetectionSource, - ) - - suspend fun addFloatingButtonTriggerKey(buttonUid: String) - fun addAssistantTriggerKey(type: AssistantTriggerType) - fun addFingerprintGesture(type: FingerprintGestureType) - fun removeTriggerKey(uid: String) - fun getTriggerKey(uid: String): TriggerKey? - fun moveTriggerKey(fromIndex: Int, toIndex: Int) - - fun restoreState(keyMap: KeyMap) - suspend fun loadKeyMap(uid: String) - fun loadNewKeyMap(groupUid: String?) - - fun setParallelTriggerMode() - fun setSequenceTriggerMode() - fun setUndefinedTriggerMode() - - fun setTriggerShortPress() - fun setTriggerLongPress() - fun setTriggerDoublePress() - - fun setTriggerKeyClickType(keyUid: String, clickType: ClickType) - fun setTriggerKeyDevice(keyUid: String, device: TriggerKeyDevice) - fun setTriggerKeyConsumeKeyEvent(keyUid: String, consumeKeyEvent: Boolean) - fun setAssistantTriggerKeyType(keyUid: String, type: AssistantTriggerType) - fun setFingerprintGestureType(keyUid: String, type: FingerprintGestureType) - - fun setVibrateEnabled(enabled: Boolean) - fun setVibrationDuration(duration: Int) - fun setLongPressDelay(delay: Int) - fun setDoublePressDelay(delay: Int) - fun setSequenceTriggerTimeout(delay: Int) - fun setLongPressDoubleVibrationEnabled(enabled: Boolean) - fun setTriggerWhenScreenOff(enabled: Boolean) - fun setTriggerFromOtherAppsEnabled(enabled: Boolean) - fun setShowToastEnabled(enabled: Boolean) - - fun getAvailableTriggerKeyDevices(): List - - val floatingButtonToUse: MutableStateFlow - suspend fun getFloatingLayoutCount(): Int -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/BaseConfigKeyMapViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapViewModel.kt similarity index 76% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/BaseConfigKeyMapViewModel.kt rename to base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapViewModel.kt index 7dd8d9d1f4..88c7e0af5c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/BaseConfigKeyMapViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/ConfigKeyMapViewModel.kt @@ -2,11 +2,10 @@ package io.github.sds100.keymapper.base.keymaps import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import io.github.sds100.keymapper.base.actions.ConfigActionsViewModel -import io.github.sds100.keymapper.base.constraints.ConfigConstraintsViewModel +import dagger.hilt.android.lifecycle.HiltViewModel import io.github.sds100.keymapper.base.onboarding.OnboardingTapTarget import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase -import io.github.sds100.keymapper.base.trigger.BaseConfigTriggerViewModel +import io.github.sds100.keymapper.base.trigger.ConfigTriggerUseCase import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider import io.github.sds100.keymapper.base.utils.ui.DialogProvider import io.github.sds100.keymapper.common.utils.dataOrNull @@ -16,9 +15,12 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import javax.inject.Inject -abstract class BaseConfigKeyMapViewModel( - private val config: ConfigKeyMapUseCase, +@HiltViewModel +class ConfigKeyMapViewModel @Inject constructor( + private val configKeyMapState: ConfigKeyMapState, + private val configTrigger: ConfigTriggerUseCase, private val onboarding: OnboardingUseCase, navigationProvider: NavigationProvider, dialogProvider: DialogProvider, @@ -26,21 +28,17 @@ abstract class BaseConfigKeyMapViewModel( NavigationProvider by navigationProvider, DialogProvider by dialogProvider { - abstract val configActionsViewModel: ConfigActionsViewModel - abstract val configTriggerViewModel: BaseConfigTriggerViewModel - abstract val configConstraintsViewModel: ConfigConstraintsViewModel + val isKeyMapEdited: Boolean + get() = configKeyMapState.isEdited - val isEnabled: StateFlow = config.keyMap + val isEnabled: StateFlow = configTrigger.keyMap .map { state -> state.dataOrNull()?.isEnabled ?: true } .stateIn(viewModelScope, SharingStarted.Eagerly, true) - val isKeyMapEdited: Boolean - get() = config.isEdited - val showActionsTapTarget: StateFlow = combine( onboarding.showTapTarget(OnboardingTapTarget.CHOOSE_ACTION), - config.keyMap, + configKeyMapState.keyMap, ) { showTapTarget, keyMapState -> // Show the choose action tap target if they have recorded a key. showTapTarget && keyMapState.dataOrNull()?.trigger?.keys?.isNotEmpty() ?: false @@ -49,14 +47,14 @@ abstract class BaseConfigKeyMapViewModel( val showConstraintsTapTarget: StateFlow = combine( onboarding.showTapTarget(OnboardingTapTarget.CHOOSE_CONSTRAINT), - config.keyMap, + configKeyMapState.keyMap, ) { showTapTarget, keyMapState -> // Show the choose constraint tap target if they have added an action. showTapTarget && keyMapState.dataOrNull()?.actionList?.isNotEmpty() ?: false }.stateIn(viewModelScope, SharingStarted.Lazily, false) fun onDoneClick() { - config.save() + configKeyMapState.save() viewModelScope.launch { popBackStack() @@ -64,17 +62,17 @@ abstract class BaseConfigKeyMapViewModel( } fun loadNewKeyMap(floatingButtonUid: String? = null, groupUid: String?) { - config.loadNewKeyMap(groupUid) + configKeyMapState.loadNewKeyMap(groupUid) if (floatingButtonUid != null) { viewModelScope.launch { - config.addFloatingButtonTriggerKey(floatingButtonUid) + configTrigger.addFloatingButtonTriggerKey(floatingButtonUid) } } } fun loadKeyMap(uid: String) { viewModelScope.launch { - config.loadKeyMap(uid) + configKeyMapState.loadKeyMap(uid) } } @@ -84,10 +82,6 @@ abstract class BaseConfigKeyMapViewModel( } } - fun onEnabledChanged(enabled: Boolean) { - config.setEnabled(enabled) - } - fun onActionTapTargetCompleted() { onboarding.completedTapTarget(OnboardingTapTarget.CHOOSE_ACTION) } @@ -99,4 +93,8 @@ abstract class BaseConfigKeyMapViewModel( fun onSkipTutorialClick() { onboarding.skipTapTargetOnboarding() } + + fun onEnabledChanged(enabled: Boolean) { + configTrigger.setEnabled(enabled) + } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/DisplayKeyMapUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/DisplayKeyMapUseCase.kt index d4632d4e2b..9f41439665 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/DisplayKeyMapUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/DisplayKeyMapUseCase.kt @@ -12,6 +12,9 @@ import io.github.sds100.keymapper.base.purchasing.PurchasingManager import io.github.sds100.keymapper.base.system.inputmethod.KeyMapperImeHelper import io.github.sds100.keymapper.base.trigger.TriggerError import io.github.sds100.keymapper.base.trigger.TriggerErrorSnapshot +import io.github.sds100.keymapper.base.utils.navigation.NavDestination +import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider +import io.github.sds100.keymapper.base.utils.navigation.navigate import io.github.sds100.keymapper.common.BuildConfigProvider import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult @@ -23,6 +26,7 @@ import io.github.sds100.keymapper.common.utils.then import io.github.sds100.keymapper.common.utils.valueIfFailure import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.repositories.PreferenceRepository +import io.github.sds100.keymapper.sysbridge.utils.SystemBridgeError import io.github.sds100.keymapper.system.SystemError import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceAdapter import io.github.sds100.keymapper.system.apps.PackageManagerAdapter @@ -54,6 +58,7 @@ class DisplayKeyMapUseCaseImpl @Inject constructor( private val getActionErrorUseCase: GetActionErrorUseCase, private val getConstraintErrorUseCase: GetConstraintErrorUseCase, private val buildConfigProvider: BuildConfigProvider, + private val navigationProvider: NavigationProvider, ) : DisplayKeyMapUseCase, GetActionErrorUseCase by getActionErrorUseCase, GetConstraintErrorUseCase by getConstraintErrorUseCase { @@ -133,11 +138,6 @@ class DisplayKeyMapUseCaseImpl @Inject constructor( override suspend fun fixTriggerError(error: TriggerError) { when (error) { TriggerError.DND_ACCESS_DENIED -> fixError(SystemError.PermissionDenied(Permission.ACCESS_NOTIFICATION_POLICY)) - TriggerError.SCREEN_OFF_ROOT_DENIED -> fixError( - SystemError.PermissionDenied( - Permission.ROOT, - ), - ) TriggerError.CANT_DETECT_IN_PHONE_CALL -> fixError(KMError.CantDetectKeyEventsInPhoneCall) TriggerError.ASSISTANT_TRIGGER_NOT_PURCHASED -> fixError( @@ -158,9 +158,11 @@ class DisplayKeyMapUseCaseImpl @Inject constructor( } } - override fun getAppName(packageName: String): KMResult = packageManagerAdapter.getAppName(packageName) + override fun getAppName(packageName: String): KMResult = + packageManagerAdapter.getAppName(packageName) - override fun getAppIcon(packageName: String): KMResult = packageManagerAdapter.getAppIcon(packageName) + override fun getAppIcon(packageName: String): KMResult = + packageManagerAdapter.getAppIcon(packageName) override fun getInputMethodLabel(imeId: String): KMResult = inputMethodAdapter.getInfoById(imeId).then { Success(it.label) } @@ -191,6 +193,11 @@ class DisplayKeyMapUseCaseImpl @Inject constructor( } } + is SystemBridgeError.Disconnected -> navigationProvider.navigate( + "fix_system_bridge", + NavDestination.ProMode, + ) + else -> Unit } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/GetDefaultKeyMapOptionsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/GetDefaultKeyMapOptionsUseCase.kt index 11e3b89199..b7db27f9af 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/GetDefaultKeyMapOptionsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/GetDefaultKeyMapOptionsUseCase.kt @@ -8,8 +8,11 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject +import javax.inject.Singleton -class GetDefaultKeyMapOptionsUseCaseImpl( +@Singleton +class GetDefaultKeyMapOptionsUseCaseImpl @Inject constructor( coroutineScope: CoroutineScope, preferenceRepository: PreferenceRepository, ) : GetDefaultKeyMapOptionsUseCase { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMap.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMap.kt index c0e05384aa..633055dba5 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMap.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMap.kt @@ -8,7 +8,7 @@ import io.github.sds100.keymapper.base.actions.canBeHeldDown import io.github.sds100.keymapper.base.constraints.ConstraintEntityMapper import io.github.sds100.keymapper.base.constraints.ConstraintModeEntityMapper import io.github.sds100.keymapper.base.constraints.ConstraintState -import io.github.sds100.keymapper.base.keymaps.detection.KeyMapController +import io.github.sds100.keymapper.base.detection.KeyMapAlgorithm import io.github.sds100.keymapper.base.trigger.Trigger import io.github.sds100.keymapper.base.trigger.TriggerEntityMapper import io.github.sds100.keymapper.base.trigger.TriggerKey @@ -37,13 +37,14 @@ data class KeyMap( val vibrateDuration: Int? get() = trigger.vibrateDuration - fun isRepeatingActionsAllowed(): Boolean = KeyMapController.performActionOnDown(trigger) + fun isRepeatingActionsAllowed(): Boolean = KeyMapAlgorithm.performActionOnDown(trigger) fun isChangingActionRepeatRateAllowed(action: Action): Boolean = action.repeat && isRepeatingActionsAllowed() fun isChangingActionRepeatDelayAllowed(action: Action): Boolean = action.repeat && isRepeatingActionsAllowed() - fun isHoldingDownActionAllowed(action: Action): Boolean = KeyMapController.performActionOnDown(trigger) && action.data.canBeHeldDown() + fun isHoldingDownActionAllowed(action: Action): Boolean = + KeyMapAlgorithm.performActionOnDown(trigger) && action.data.canBeHeldDown() fun isHoldingDownActionBeforeRepeatingAllowed(action: Action): Boolean = action.repeat && action.holdDown @@ -67,7 +68,7 @@ fun KeyMap.requiresImeKeyEventForwarding(): Boolean { actionList.any { it.data is ActionData.AnswerCall || it.data is ActionData.EndCall } val hasVolumeKeys = trigger.keys - .mapNotNull { it as? io.github.sds100.keymapper.base.trigger.KeyCodeTriggerKey } + .mapNotNull { it as? io.github.sds100.keymapper.base.trigger.KeyEventTriggerKey } .any { it.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || it.keyCode == KeyEvent.KEYCODE_VOLUME_UP @@ -83,7 +84,7 @@ fun KeyMap.requiresImeKeyEventForwarding(): Boolean { * is incoming. */ fun KeyMap.requiresImeKeyEventForwardingInPhoneCall(triggerKey: TriggerKey): Boolean { - if (triggerKey !is io.github.sds100.keymapper.base.trigger.KeyCodeTriggerKey) { + if (triggerKey !is io.github.sds100.keymapper.base.trigger.KeyEventTriggerKey) { return false } @@ -91,7 +92,7 @@ fun KeyMap.requiresImeKeyEventForwardingInPhoneCall(triggerKey: TriggerKey): Boo actionList.any { it.data is ActionData.AnswerCall || it.data is ActionData.EndCall } val hasVolumeKeys = trigger.keys - .mapNotNull { it as? io.github.sds100.keymapper.base.trigger.KeyCodeTriggerKey } + .mapNotNull { it as? io.github.sds100.keymapper.base.trigger.KeyEventTriggerKey } .any { it.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || it.keyCode == KeyEvent.KEYCODE_VOLUME_UP diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapOptionsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapOptionsScreen.kt index e2a06d926d..e4681beac9 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapOptionsScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/KeyMapOptionsScreen.kt @@ -114,18 +114,6 @@ private fun Loaded( Spacer(Modifier.height(8.dp)) - if (state.showScreenOffTrigger) { - CheckBoxText( - modifier = Modifier - .padding(horizontal = 8.dp) - .fillMaxWidth(), - text = stringResource(R.string.flag_detect_triggers_screen_off), - isChecked = state.screenOffTrigger, - onCheckedChange = callback::onScreenOffTriggerChanged, - ) - Spacer(Modifier.height(8.dp)) - } - CheckBoxText( modifier = Modifier .padding(horizontal = 8.dp) @@ -352,7 +340,6 @@ interface KeyMapOptionsCallback { fun onVibrateDurationChanged(duration: Int) = run { } fun onVibrateChanged(checked: Boolean) = run { } fun onLongPressDoubleVibrationChanged(checked: Boolean) = run { } - fun onScreenOffTriggerChanged(checked: Boolean) = run { } fun onShowToastChanged(checked: Boolean) = run { } fun onTriggerFromOtherAppsChanged(checked: Boolean) = run {} fun onCreateShortcutClick() = run { } @@ -388,9 +375,6 @@ private fun Preview() { showLongPressDoubleVibration = true, longPressDoubleVibration = false, - showScreenOffTrigger = true, - screenOffTrigger = false, - triggerFromOtherApps = true, keyMapUid = "00000-00000-00000-0000000000000000000000000000000000", isLauncherShortcutButtonEnabled = false, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectScreenOffKeyEventsController.kt b/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectScreenOffKeyEventsController.kt deleted file mode 100644 index bd56ca6ef0..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/detection/DetectScreenOffKeyEventsController.kt +++ /dev/null @@ -1,134 +0,0 @@ -package io.github.sds100.keymapper.base.keymaps.detection - -import android.view.InputDevice -import android.view.KeyEvent -import io.github.sds100.keymapper.common.utils.State -import io.github.sds100.keymapper.common.utils.valueOrNull -import io.github.sds100.keymapper.system.devices.DevicesAdapter -import io.github.sds100.keymapper.system.devices.InputDeviceInfo -import io.github.sds100.keymapper.system.inputevents.InputEventUtils -import io.github.sds100.keymapper.system.inputevents.MyKeyEvent -import io.github.sds100.keymapper.system.root.SuAdapter -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import timber.log.Timber - -class DetectScreenOffKeyEventsController( - private val suAdapter: SuAdapter, - private val devicesAdapter: DevicesAdapter, - private val onKeyEvent: suspend (event: MyKeyEvent) -> Unit, -) { - - companion object { - private const val REGEX_GET_DEVICE_LOCATION = "/.*(?=:)" - private const val REGEX_KEY_EVENT_ACTION = "(?<= )(DOWN|UP)" - } - - private var job: Job? = null - - /** - * @return whether it successfully started listening. - */ - fun startListening(scope: CoroutineScope): Boolean { - try { - job = scope.launch(Dispatchers.IO) { - val devicesInputStream = - suAdapter.getCommandOutput("getevent -i").valueOrNull() ?: return@launch - - val getEventDevices: String = devicesInputStream.bufferedReader().readText() - devicesInputStream.close() - - val deviceLocationToDeviceMap = mutableMapOf() - - val inputDevices = - devicesAdapter.connectedInputDevices.first { it is State.Data } as State.Data - - inputDevices.data.forEach { device -> - val deviceLocation = - getDeviceLocation(getEventDevices, device.name) ?: return@forEach - - deviceLocationToDeviceMap[deviceLocation] = device - } - - val deviceLocationRegex = Regex(REGEX_GET_DEVICE_LOCATION) - val actionRegex = Regex(REGEX_KEY_EVENT_ACTION) - - // use -q option to not initially output the list of devices - val inputStream = - suAdapter.getCommandOutput("getevent -lq").valueOrNull() ?: return@launch - - var line: String? - - while (inputStream.bufferedReader().readLine() - .also { line = it } != null && - isActive - ) { - line ?: continue - - InputEventUtils.GET_EVENT_LABEL_TO_KEYCODE.forEach { (label, keyCode) -> - if (line!!.contains(label)) { - val deviceLocation = - deviceLocationRegex.find(line!!)?.value ?: return@forEach - - val device = deviceLocationToDeviceMap[deviceLocation] ?: return@forEach - - val actionString = actionRegex.find(line!!)?.value ?: return@forEach - - when (actionString) { - "UP" -> { - onKeyEvent.invoke( - MyKeyEvent( - keyCode = keyCode, - action = KeyEvent.ACTION_UP, - device = device, - scanCode = 0, - metaState = 0, - repeatCount = 0, - source = InputDevice.SOURCE_UNKNOWN, - ), - ) - } - - "DOWN" -> { - onKeyEvent.invoke( - MyKeyEvent( - keyCode = keyCode, - action = KeyEvent.ACTION_DOWN, - device = device, - scanCode = 0, - metaState = 0, - repeatCount = 0, - source = InputDevice.SOURCE_UNKNOWN, - ), - ) - } - } - } - } - } - - inputStream.close() - } - } catch (e: Exception) { - Timber.e(e) - job?.cancel() - return false - } - - return true - } - - fun stopListening() { - job?.cancel() - job = null - } - - private fun getDeviceLocation(getEventDeviceOutput: String, deviceName: String): String? { - val regex = Regex("(/.*)(?=(\\n.*){5}\"$deviceName\")") - return regex.find(getEventDeviceOutput)?.value - } -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/logging/DisplayLogUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/logging/DisplayLogUseCase.kt index 0ffde83f82..de3afe7ede 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/logging/DisplayLogUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/logging/DisplayLogUseCase.kt @@ -2,9 +2,7 @@ package io.github.sds100.keymapper.base.logging import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.utils.ui.ResourceProvider -import io.github.sds100.keymapper.common.utils.State -import io.github.sds100.keymapper.common.utils.ifIsData -import io.github.sds100.keymapper.common.utils.mapData +import io.github.sds100.keymapper.data.entities.LogEntryEntity import io.github.sds100.keymapper.data.repositories.LogRepository import io.github.sds100.keymapper.system.clipboard.ClipboardAdapter import io.github.sds100.keymapper.system.files.FileAdapter @@ -13,6 +11,9 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale import javax.inject.Inject class DisplayLogUseCaseImpl @Inject constructor( @@ -21,41 +22,43 @@ class DisplayLogUseCaseImpl @Inject constructor( private val clipboardAdapter: ClipboardAdapter, private val fileAdapter: FileAdapter, ) : DisplayLogUseCase { - override val log: Flow>> = repository.log - .map { state -> - state.mapData { entityList -> entityList.map { LogEntryEntityMapper.fromEntity(it) } } - } + private val dateFormat = SimpleDateFormat("MM/dd HH:mm:ss.SSS", Locale.getDefault()) + private val severityString: Map = mapOf( + LogEntryEntity.SEVERITY_ERROR to "ERROR", + LogEntryEntity.SEVERITY_WARNING to "WARN", + LogEntryEntity.SEVERITY_INFO to "INFO", + LogEntryEntity.SEVERITY_DEBUG to "DEBUG", + ) + + override val log: Flow> = repository.log + .map { entityList -> entityList.map { LogEntryEntityMapper.fromEntity(it) } } .flowOn(Dispatchers.Default) override fun clearLog() { repository.deleteAll() } - override suspend fun copyToClipboard(entryId: Set) { - repository.log.first().ifIsData { logEntries -> - val logText = LogUtils.createLogText(logEntries.filter { it.id in entryId }) + override suspend fun copyToClipboard() { + val logEntries = repository.log.first() + val logText = createLogText(logEntries) - clipboardAdapter.copy( - label = resourceProvider.getString(R.string.clip_key_mapper_log), - logText, - ) - } + clipboardAdapter.copy( + label = resourceProvider.getString(R.string.clip_key_mapper_log), + logText, + ) } - override suspend fun saveToFile(uri: String, entryId: Set) { - val file = fileAdapter.getFileFromUri(uri) - - repository.log.first().ifIsData { logEntries -> - val logText = LogUtils.createLogText(logEntries.filter { it.id in entryId }) + private fun createLogText(logEntries: List): String { + return logEntries.joinToString(separator = "\n") { entry -> + val date = dateFormat.format(Date(entry.time)) - file.outputStream()!!.bufferedWriter().use { it.write(logText) } + return@joinToString "$date ${severityString[entry.severity]} ${entry.message}" } } } interface DisplayLogUseCase { - val log: Flow>> + val log: Flow> fun clearLog() - suspend fun copyToClipboard(entryId: Set) - suspend fun saveToFile(uri: String, entryId: Set) + suspend fun copyToClipboard() } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/logging/KeyMapperLoggingTree.kt b/base/src/main/java/io/github/sds100/keymapper/base/logging/KeyMapperLoggingTree.kt index 861b6522df..a5df61eecb 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/logging/KeyMapperLoggingTree.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/logging/KeyMapperLoggingTree.kt @@ -44,8 +44,8 @@ class KeyMapperLoggingTree @Inject constructor( } override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { - // error and info logs should always log even if the user setting is turned off - if (!logEverything.value && priority != Log.ERROR && priority != Log.INFO) { + // error, warn, and info logs should always log even if the user setting is turned off + if (!logEverything.value && priority != Log.ERROR && priority != Log.WARN && priority != Log.INFO) { return } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogEntryEntityMapper.kt b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogEntryEntityMapper.kt index 0e3d6f301d..ccb11093d3 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogEntryEntityMapper.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogEntryEntityMapper.kt @@ -8,6 +8,7 @@ object LogEntryEntityMapper { LogSeverity.ERROR -> LogEntryEntity.SEVERITY_ERROR LogSeverity.DEBUG -> LogEntryEntity.SEVERITY_DEBUG LogSeverity.INFO -> LogEntryEntity.SEVERITY_INFO + LogSeverity.WARNING -> LogEntryEntity.SEVERITY_WARNING } return LogEntryEntity( @@ -23,6 +24,7 @@ object LogEntryEntityMapper { LogEntryEntity.SEVERITY_ERROR -> LogSeverity.ERROR LogEntryEntity.SEVERITY_DEBUG -> LogSeverity.DEBUG LogEntryEntity.SEVERITY_INFO -> LogSeverity.INFO + LogEntryEntity.SEVERITY_WARNING -> LogSeverity.WARNING else -> LogSeverity.DEBUG } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogEntryListItem.kt b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogEntryListItem.kt deleted file mode 100644 index 5dcd1551d8..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogEntryListItem.kt +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.sds100.keymapper.base.logging - -import io.github.sds100.keymapper.base.utils.ui.TintType - -data class LogEntryListItem( - val id: Int, - val time: String, - val textTint: TintType, - val message: String, - val isSelected: Boolean, -) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogFragment.kt b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogFragment.kt deleted file mode 100644 index 5c84b9c89e..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogFragment.kt +++ /dev/null @@ -1,182 +0,0 @@ -package io.github.sds100.keymapper.base.logging - -import android.content.Intent -import android.os.Bundle -import android.view.View -import android.view.ViewTreeObserver -import androidx.activity.result.contract.ActivityResultContracts.CreateDocument -import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle -import androidx.navigation.fragment.findNavController -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.airbnb.epoxy.EpoxyRecyclerView -import com.airbnb.epoxy.TypedEpoxyController -import com.michaelflisar.dragselectrecyclerview.DragSelectTouchListener -import com.michaelflisar.dragselectrecyclerview.DragSelectionProcessor -import dagger.hilt.android.AndroidEntryPoint -import io.github.sds100.keymapper.base.R -import io.github.sds100.keymapper.base.databinding.FragmentSimpleRecyclerviewBinding -import io.github.sds100.keymapper.base.logEntry -import io.github.sds100.keymapper.base.utils.ui.SimpleRecyclerViewFragment -import io.github.sds100.keymapper.base.utils.ui.launchRepeatOnLifecycle -import io.github.sds100.keymapper.common.utils.State -import io.github.sds100.keymapper.system.files.FileUtils -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collectLatest - -@AndroidEntryPoint -class LogFragment : SimpleRecyclerViewFragment() { - - private val viewModel: LogViewModel by viewModels() - - override val listItems: Flow>> - get() = viewModel.listItems - - override val appBarMenu: Int = R.menu.menu_log - override var isAppBarVisible = true - - private val recyclerViewController by lazy { RecyclerViewController() } - - private val saveLogToFileLauncher = - registerForActivityResult(CreateDocument(FileUtils.MIME_TYPE_TEXT)) { - it ?: return@registerForActivityResult - - viewModel.onPickFileToSaveTo(it.toString()) - - val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION - - requireContext().contentResolver.takePersistableUriPermission(it, takeFlags) - } - - private lateinit var dragSelectTouchListener: DragSelectTouchListener - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - getBottomAppBar(binding)?.setOnMenuItemClickListener { menuItem -> - viewModel.onMenuItemClick(menuItem.itemId) - true - } - - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.pickFileToSaveTo.collectLatest { - saveLogToFileLauncher.launch(LogUtils.createLogFileName()) - } - } - } - - override fun subscribeUi(binding: FragmentSimpleRecyclerviewBinding) { - super.subscribeUi(binding) - - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.appBarState.collectLatest { appBarState -> - when (appBarState) { - LogAppBarState.MULTI_SELECTING -> { - binding.appBar.setNavigationIcon(R.drawable.ic_outline_clear_24) - } - - LogAppBarState.NORMAL -> { - binding.appBar.setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) - } - } - } - } - - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.goBack.collectLatest { - findNavController().navigateUp() - } - } - - val dragSelectionProcessor = DragSelectionProcessor(viewModel.dragSelectionHandler) - .withMode(DragSelectionProcessor.Mode.Simple) - - dragSelectTouchListener = DragSelectTouchListener() - .withSelectListener(dragSelectionProcessor) - - binding.epoxyRecyclerView.setController(recyclerViewController) - } - - override fun onBackPressed() { - viewModel.onBackPressed() - } - - override fun populateList(recyclerView: EpoxyRecyclerView, listItems: List) { - recyclerViewController.setData(listItems) - } - - private inner class RecyclerViewController : TypedEpoxyController>() { - private var scrollToBottom = false - private var scrolledToBottomInitially = false - private var recyclerView: RecyclerView? = null - - init { - addModelBuildListener { - currentData?.also { currentData -> - if (!scrolledToBottomInitially) { - recyclerView?.viewTreeObserver?.addOnGlobalLayoutListener(object : - ViewTreeObserver.OnGlobalLayoutListener { - override fun onGlobalLayout() { - recyclerView?.scrollToPosition(currentData.size - 1) - recyclerView?.viewTreeObserver?.removeOnGlobalLayoutListener(this) - } - }) - - scrolledToBottomInitially = true - } else if (scrollToBottom) { - recyclerView?.smoothScrollToPosition(currentData.size) - } - } - } - } - - override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { - this.recyclerView = recyclerView - super.onAttachedToRecyclerView(recyclerView) - } - - override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { - this.recyclerView = null - super.onDetachedFromRecyclerView(recyclerView) - } - - override fun buildModels(data: List?) { - if (data == null) { - return - } - - if (recyclerView?.scrollState != RecyclerView.SCROLL_STATE_SETTLING) { - // only automatically scroll to the bottom if the recyclerview is already scrolled to the button - val layoutManager = recyclerView?.layoutManager as LinearLayoutManager? - - if (layoutManager != null) { - val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition() - - if (lastVisibleItemPosition == RecyclerView.NO_POSITION) { - scrollToBottom = false - } else { - scrollToBottom = lastVisibleItemPosition == layoutManager.itemCount - 1 - } - } - } - - data.forEachIndexed { index, model -> - logEntry { - id(model.id) - model(model) - - onClick { _ -> - viewModel.onListItemClick(model.id) - } - - onLongClick { _ -> - dragSelectTouchListener.startDragSelection(index) - true - } - } - } - } - } -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogListItem.kt b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogListItem.kt new file mode 100644 index 0000000000..1709b82cdd --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogListItem.kt @@ -0,0 +1,8 @@ +package io.github.sds100.keymapper.base.logging + +data class LogListItem( + val id: Int, + val time: String, + val severity: LogSeverity, + val message: String, +) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogScreen.kt new file mode 100644 index 0000000000..ca68e3ad0b --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogScreen.kt @@ -0,0 +1,212 @@ +package io.github.sds100.keymapper.base.logging + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.displayCutoutPadding +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material.icons.outlined.ContentCopy +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import io.github.sds100.keymapper.base.compose.LocalCustomColorsPalette + +@Composable +fun LogScreen( + modifier: Modifier = Modifier, + viewModel: LogViewModel = hiltViewModel(), + onBackClick: () -> Unit, +) { + val log = viewModel.log.collectAsStateWithLifecycle().value + + LogScreen( + modifier = modifier, + onBackClick = onBackClick, + onCopyToClipboardClick = viewModel::onCopyToClipboardClick, + onClearLogClick = viewModel::onClearLogClick, + content = { + Content( + modifier = Modifier.fillMaxSize(), + logListItems = log, + ) + }, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun LogScreen( + modifier: Modifier = Modifier, + onBackClick: () -> Unit = {}, + onCopyToClipboardClick: () -> Unit = {}, + onClearLogClick: () -> Unit = {}, + content: @Composable () -> Unit, +) { + Scaffold( + modifier = modifier.displayCutoutPadding(), + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.title_pref_view_and_share_log)) }, + actions = { + OutlinedButton( + modifier = Modifier.padding(horizontal = 16.dp), + onClick = onClearLogClick, + ) { + Text(stringResource(R.string.action_clear_log)) + } + }, + ) + }, + bottomBar = { + BottomAppBar { + IconButton(onClick = onBackClick) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = stringResource(R.string.action_go_back), + ) + } + Spacer(Modifier.weight(1f)) + IconButton(onClick = onCopyToClipboardClick) { + Icon( + imageVector = Icons.Outlined.ContentCopy, + contentDescription = stringResource(R.string.action_copy_log), + ) + } + } + }, + ) { innerPadding -> + val layoutDirection = LocalLayoutDirection.current + val startPadding = innerPadding.calculateStartPadding(layoutDirection) + val endPadding = innerPadding.calculateEndPadding(layoutDirection) + + Surface( + modifier = Modifier + .fillMaxSize() + .padding( + top = innerPadding.calculateTopPadding(), + bottom = innerPadding.calculateBottomPadding(), + start = startPadding, + end = endPadding, + ), + ) { + content() + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun Content( + modifier: Modifier = Modifier, + logListItems: List, +) { + val listState = rememberLazyListState() + + // Scroll to the bottom when a new item is added + LaunchedEffect(logListItems) { + if (logListItems.isNotEmpty()) { + listState.animateScrollToItem(logListItems.size - 1) + } + } + + Column(modifier = modifier) { + LazyColumn( + state = listState, + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(8.dp), + ) { + items(logListItems, key = { it.id }) { item -> + val color = when (item.severity) { + LogSeverity.ERROR -> MaterialTheme.colorScheme.error + LogSeverity.WARNING -> LocalCustomColorsPalette.current.orange + LogSeverity.INFO -> LocalCustomColorsPalette.current.green + else -> LocalContentColor.current + } + + Row { + Text( + text = item.time, + color = color, + style = MaterialTheme.typography.bodySmall, + overflow = TextOverflow.Ellipsis, + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = item.message, + color = color, + style = MaterialTheme.typography.bodySmall, + overflow = TextOverflow.Ellipsis, + ) + } + } + } + } +} + +@Preview +@Composable +private fun Preview() { + KeyMapperTheme { + LogScreen( + content = { + Content( + logListItems = listOf( + LogListItem(1, "12:34:56.789", LogSeverity.INFO, "This is an info message"), + LogListItem( + 2, + "12:34:57.123", + LogSeverity.WARNING, + "This is a warning message", + ), + LogListItem( + 3, + "12:34:58.456", + LogSeverity.ERROR, + "This is an error message. It is a bit long to see how it overflows inside the available space.", + ), + LogListItem(4, "12:34:59.000", LogSeverity.INFO, "Another info message"), + LogListItem( + 5, + "12:35:00.000", + LogSeverity.ERROR, + "Error recording trigger", + ), + LogListItem(6, "12:35:01.000", LogSeverity.WARNING, "I am a warning"), + LogListItem(7, "12:35:02.000", LogSeverity.INFO, "I am some info..."), + LogListItem(8, "12:35:03.000", LogSeverity.INFO, "This more info"), + ), + ) + }, + ) + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogSeverity.kt b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogSeverity.kt index e90e7993da..91d594e028 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogSeverity.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogSeverity.kt @@ -2,6 +2,7 @@ package io.github.sds100.keymapper.base.logging enum class LogSeverity { ERROR, + WARNING, INFO, DEBUG, } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogUtils.kt deleted file mode 100644 index 93b19ffaad..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogUtils.kt +++ /dev/null @@ -1,27 +0,0 @@ -package io.github.sds100.keymapper.base.logging - -import io.github.sds100.keymapper.data.entities.LogEntryEntity -import io.github.sds100.keymapper.system.files.FileUtils -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale - -object LogUtils { - val DATE_FORMAT - get() = SimpleDateFormat("MM/dd HH:mm:ss.SSS", Locale.getDefault()) - - fun createLogFileName(): String { - val formattedDate = FileUtils.createFileDate() - return "key_mapper_log_$formattedDate.txt" - } - - fun createLogText(logEntries: List): String { - val dateFormat = DATE_FORMAT - - return logEntries.joinToString(separator = "\n") { entry -> - val date = dateFormat.format(Date(entry.time)) - - return@joinToString "$date ${entry.message}" - } - } -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogViewModel.kt index 61959f6990..3eedcaaf36 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/logging/LogViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/logging/LogViewModel.kt @@ -1,204 +1,49 @@ package io.github.sds100.keymapper.base.logging +import android.annotation.SuppressLint import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.michaelflisar.dragselectrecyclerview.DragSelectionProcessor import dagger.hilt.android.lifecycle.HiltViewModel -import io.github.sds100.keymapper.base.R -import io.github.sds100.keymapper.base.utils.ui.DialogModel -import io.github.sds100.keymapper.base.utils.ui.DialogProvider -import io.github.sds100.keymapper.base.utils.ui.MultiSelectProvider -import io.github.sds100.keymapper.base.utils.ui.ResourceProvider -import io.github.sds100.keymapper.base.utils.ui.SelectionState -import io.github.sds100.keymapper.base.utils.ui.TintType -import io.github.sds100.keymapper.base.utils.ui.showDialog -import io.github.sds100.keymapper.common.utils.State -import io.github.sds100.keymapper.common.utils.ifIsData -import io.github.sds100.keymapper.common.utils.mapData -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import java.util.Date +import java.text.SimpleDateFormat +import java.util.Locale import javax.inject.Inject @HiltViewModel class LogViewModel @Inject constructor( - private val useCase: DisplayLogUseCase, - resourceProvider: ResourceProvider, - dialogProvider: DialogProvider, -) : ViewModel(), - DialogProvider by dialogProvider, - ResourceProvider by resourceProvider { - private val multiSelectProvider: MultiSelectProvider = MultiSelectProvider() - - private val _listItems = MutableStateFlow>>(State.Loading) - val listItems = _listItems.asStateFlow() - - private val dateFormat = LogUtils.DATE_FORMAT - - private val _pickFileToSaveTo = MutableSharedFlow() - val pickFileToSaveTo = _pickFileToSaveTo.asSharedFlow() - - val appBarState: StateFlow = multiSelectProvider.state - .map { selectionState -> - when (selectionState) { - is SelectionState.Selecting -> LogAppBarState.MULTI_SELECTING - else -> LogAppBarState.NORMAL - } - } - .stateIn(viewModelScope, SharingStarted.Lazily, LogAppBarState.NORMAL) - - private val showShortMessages = MutableStateFlow(true) - - private val _goBack = MutableSharedFlow() - val goBack = _goBack.asSharedFlow() - - val dragSelectionHandler = object : DragSelectionProcessor.ISelectionHandler { - override fun getSelection(): MutableSet = multiSelectProvider.getSelectedIds().map { it.toInt() }.toMutableSet() - - override fun isSelected(index: Int): Boolean { - listItems.value.ifIsData { - val id = it.getOrNull(index)?.id ?: return false - - return multiSelectProvider.isSelected(id.toString()) - } - - return false - } - - override fun updateSelection( - start: Int, - end: Int, - isSelected: Boolean, - calledFromOnStart: Boolean, - ) { - listItems.value.ifIsData { listItems -> - val selectedListItems = listItems.slice(start..end) - val selectedIds = selectedListItems.map { it.id.toString() }.toTypedArray() - - if (calledFromOnStart) { - multiSelectProvider.startSelecting() - } - - if (isSelected) { - multiSelectProvider.select(*selectedIds) - } else { - multiSelectProvider.deselect(*selectedIds) - } + private val displayLogUseCase: DisplayLogUseCase, +) : ViewModel() { + @SuppressLint("ConstantLocale") + private val dateFormat = SimpleDateFormat("MM/dd HH:mm:ss.SSS", Locale.getDefault()) + + val log: StateFlow> = displayLogUseCase.log + .map { list -> + list.map { + LogListItem( + id = it.id, + time = dateFormat.format(it.time), + message = it.message, + severity = it.severity, + ) } } - } - - init { - combine( - useCase.log, - showShortMessages, - multiSelectProvider.state, - ) { log, showShortMessages, selectionState -> - _listItems.value = log.mapData { logEntries -> - logEntries.map { entry -> - val isSelected = if (selectionState is SelectionState.Selecting) { - selectionState.selectedIds.contains(entry.id.toString()) - } else { - false - } - - createListItem(entry, showShortMessages, isSelected) - } - } - }.flowOn(Dispatchers.Default).launchIn(viewModelScope) - } - - fun onMenuItemClick(itemId: Int) { - viewModelScope.launch { - when (itemId) { - R.id.action_clear -> useCase.clearLog() - R.id.action_copy -> { - useCase.copyToClipboard(getSelectedLogEntries()) - showDialog("copied", DialogModel.Toast(getString(R.string.toast_copied_log))) - } - - R.id.action_short_messages -> { - showShortMessages.value = !showShortMessages.value - } - - R.id.action_save -> - _pickFileToSaveTo.emit(Unit) - } - } - } - - fun onListItemClick(id: Int) { - multiSelectProvider.toggleSelection(id.toString()) - } - - fun onBackPressed() { - if (multiSelectProvider.isSelecting()) { - multiSelectProvider.stopSelecting() - } else { - viewModelScope.launch { - _goBack.emit(Unit) - } - } - } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = emptyList(), + ) - fun onPickFileToSaveTo(uri: String) { + fun onCopyToClipboardClick() { viewModelScope.launch { - useCase.saveToFile(uri, getSelectedLogEntries()) + displayLogUseCase.copyToClipboard() } } - private suspend fun getSelectedLogEntries(): Set = if (multiSelectProvider.isSelecting()) { - multiSelectProvider.getSelectedIds().map { it.toInt() }.toSet() - } else { - val logState = useCase.log.first() - - if (logState is State.Data) { - logState.data.map { it.id }.toSet() - } else { - emptySet() - } + fun onClearLogClick() { + displayLogUseCase.clearLog() } - - private fun createListItem( - logEntry: LogEntry, - shortMessage: Boolean, - isSelected: Boolean, - ): LogEntryListItem { - val textTint = if (logEntry.severity == LogSeverity.ERROR) { - TintType.Error - } else { - TintType.OnSurface - } - - val message: String = if (shortMessage) { - logEntry.message.split(',').getOrElse(0) { logEntry.message } - } else { - logEntry.message - } - - return LogEntryListItem( - id = logEntry.id, - time = dateFormat.format(Date(logEntry.time)), - textTint = textTint, - message = message, - isSelected = isSelected, - ) - } -} - -enum class LogAppBarState { - MULTI_SELECTING, - NORMAL, } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingUseCase.kt index d01fa193b0..b0d670a105 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/onboarding/OnboardingUseCase.kt @@ -3,7 +3,6 @@ package io.github.sds100.keymapper.base.onboarding import androidx.datastore.preferences.core.Preferences import io.github.sds100.keymapper.base.actions.ActionData import io.github.sds100.keymapper.base.actions.canUseImeToPerform -import io.github.sds100.keymapper.base.actions.canUseShizukuToPerform import io.github.sds100.keymapper.base.purchasing.ProductId import io.github.sds100.keymapper.base.purchasing.PurchasingManager import io.github.sds100.keymapper.base.system.inputmethod.KeyMapperImeHelper @@ -23,7 +22,6 @@ import io.github.sds100.keymapper.system.leanback.LeanbackAdapter import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.system.permissions.PermissionAdapter import io.github.sds100.keymapper.system.shizuku.ShizukuAdapter -import io.github.sds100.keymapper.system.shizuku.ShizukuUtils import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterIsInstance @@ -61,10 +59,6 @@ class OnboardingUseCaseImpl @Inject constructor( action.canUseImeToPerform() } - override suspend fun showInstallShizukuPrompt(action: ActionData): Boolean = !shizukuAdapter.isInstalled.value && - ShizukuUtils.isRecommendedForSdkVersion() && - action.canUseShizukuToPerform() - override fun neverShowGuiKeyboardPromptsAgain() { settingsRepository.set(Keys.acknowledgedGuiKeyboard, true) } @@ -77,11 +71,6 @@ class OnboardingUseCaseImpl @Inject constructor( Keys.shownSequenceTriggerExplanation, false, ) - override var shownKeyCodeToScanCodeTriggerExplanation by PrefDelegate( - Keys.shownKeyCodeToScanCodeTriggerExplanation, - false, - ) - override val showWhatsNew = get(Keys.lastInstalledVersionCodeHomeScreen) .map { (it ?: -1) < buildConfigProvider.versionCode } @@ -240,18 +229,11 @@ interface OnboardingUseCase { */ suspend fun showInstallGuiKeyboardPrompt(action: ActionData): Boolean - /** - * @return whether to prompt the user to install Shizuku after adding - * this action - */ - suspend fun showInstallShizukuPrompt(action: ActionData): Boolean - fun isTvDevice(): Boolean fun neverShowGuiKeyboardPromptsAgain() var shownParallelTriggerOrderExplanation: Boolean var shownSequenceTriggerExplanation: Boolean - var shownKeyCodeToScanCodeTriggerExplanation: Boolean val showFloatingButtonFeatureNotification: Flow fun showedFloatingButtonFeatureNotification() diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt new file mode 100644 index 0000000000..0ba677bdfd --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeScreen.kt @@ -0,0 +1,783 @@ +package io.github.sds100.keymapper.base.promode + +import android.os.Build +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.displayCutoutPadding +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material.icons.automirrored.rounded.HelpOutline +import androidx.compose.material.icons.rounded.Check +import androidx.compose.material.icons.rounded.Checklist +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material.icons.rounded.Info +import androidx.compose.material.icons.rounded.Notifications +import androidx.compose.material.icons.rounded.Numbers +import androidx.compose.material.icons.rounded.RestartAlt +import androidx.compose.material.icons.rounded.Tune +import androidx.compose.material.icons.rounded.WarningAmber +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import io.github.sds100.keymapper.base.compose.LocalCustomColorsPalette +import io.github.sds100.keymapper.base.utils.ui.compose.OptionsHeaderRow +import io.github.sds100.keymapper.base.utils.ui.compose.SwitchPreferenceCompose +import io.github.sds100.keymapper.base.utils.ui.compose.icons.FakeShizuku +import io.github.sds100.keymapper.base.utils.ui.compose.icons.KeyMapperIcon +import io.github.sds100.keymapper.base.utils.ui.compose.icons.KeyMapperIcons +import io.github.sds100.keymapper.common.utils.State + +@Composable +fun ProModeScreen( + modifier: Modifier = Modifier, + viewModel: ProModeViewModel, +) { + val proModeWarningState by viewModel.warningState.collectAsStateWithLifecycle() + val proModeSetupState by viewModel.setupState.collectAsStateWithLifecycle() + val autoStartBootEnabled by viewModel.autoStartBootEnabled.collectAsStateWithLifecycle() + + ProModeScreen( + modifier = modifier, + onBackClick = viewModel::onBackClick, + onHelpClick = { viewModel.showInfoCard() }, + showHelpIcon = !viewModel.showInfoCard, + ) { + Content( + warningState = proModeWarningState, + setupState = proModeSetupState, + showInfoCard = viewModel.showInfoCard, + onInfoCardDismiss = { viewModel.hideInfoCard() }, + onWarningButtonClick = viewModel::onWarningButtonClick, + onStopServiceClick = viewModel::onStopServiceClick, + onShizukuButtonClick = viewModel::onShizukuButtonClick, + onRootButtonClick = viewModel::onRootButtonClick, + onSetupWithKeyMapperClick = viewModel::onSetupWithKeyMapperClick, + onRequestNotificationPermissionClick = viewModel::onRequestNotificationPermissionClick, + autoStartAtBoot = autoStartBootEnabled, + onAutoStartAtBootToggled = { viewModel.onAutoStartBootToggled() }, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ProModeScreen( + modifier: Modifier = Modifier, + onBackClick: () -> Unit = {}, + onHelpClick: () -> Unit = {}, + showHelpIcon: Boolean = false, + content: @Composable () -> Unit, +) { + Scaffold( + modifier = modifier.displayCutoutPadding(), + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.pro_mode_app_bar_title)) }, + actions = { + AnimatedVisibility( + visible = showHelpIcon, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically(), + ) { + IconButton(onClick = onHelpClick) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.HelpOutline, + contentDescription = stringResource(R.string.pro_mode_info_card_show_content_description), + ) + } + } + }, + ) + }, + bottomBar = { + BottomAppBar { + IconButton(onClick = onBackClick) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = stringResource(R.string.action_go_back), + ) + } + } + }, + ) { innerPadding -> + val layoutDirection = LocalLayoutDirection.current + val startPadding = innerPadding.calculateStartPadding(layoutDirection) + val endPadding = innerPadding.calculateEndPadding(layoutDirection) + + Surface( + modifier = Modifier + .fillMaxSize() + .padding( + top = innerPadding.calculateTopPadding(), + bottom = innerPadding.calculateBottomPadding(), + start = startPadding, + end = endPadding, + ), + ) { + content() + } + } +} + +@Composable +private fun Content( + modifier: Modifier = Modifier, + warningState: ProModeWarningState, + setupState: State, + showInfoCard: Boolean, + onInfoCardDismiss: () -> Unit = {}, + onWarningButtonClick: () -> Unit = {}, + onShizukuButtonClick: () -> Unit = {}, + onStopServiceClick: () -> Unit = {}, + onRootButtonClick: () -> Unit = {}, + onSetupWithKeyMapperClick: () -> Unit = {}, + onRequestNotificationPermissionClick: () -> Unit = {}, + autoStartAtBoot: Boolean, + onAutoStartAtBootToggled: (Boolean) -> Unit = {}, +) { + Column(modifier = modifier.verticalScroll(rememberScrollState())) { + AnimatedVisibility( + visible = showInfoCard, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically(), + ) { + ProModeInfoCard( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + onDismiss = onInfoCardDismiss, + ) + } + + if (showInfoCard) { + Spacer(modifier = Modifier.height(8.dp)) + } + + WarningCard( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + state = warningState, + onButtonClick = onWarningButtonClick, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + if (warningState is ProModeWarningState.Understood) { + when (setupState) { + is State.Loading -> { + CircularProgressIndicator(modifier = Modifier.align(Alignment.CenterHorizontally)) + } + + is State.Data -> { + LoadedContent( + modifier = Modifier.fillMaxWidth(), + state = setupState.data, + onShizukuButtonClick = onShizukuButtonClick, + onStopServiceClick = onStopServiceClick, + onRootButtonClick = onRootButtonClick, + onSetupWithKeyMapperClick = onSetupWithKeyMapperClick, + onRequestNotificationPermissionClick = onRequestNotificationPermissionClick, + autoStartAtBoot = autoStartAtBoot, + onAutoStartAtBootToggled = onAutoStartAtBootToggled, + ) + } + } + } else { + Text( + modifier = Modifier.padding(horizontal = 32.dp), + text = stringResource(R.string.pro_mode_settings_unavailable_text), + textAlign = TextAlign.Center, + ) + } + } +} + +@Composable +private fun LoadedContent( + modifier: Modifier, + state: ProModeState, + onRootButtonClick: () -> Unit = {}, + onShizukuButtonClick: () -> Unit, + onStopServiceClick: () -> Unit, + onSetupWithKeyMapperClick: () -> Unit, + onRequestNotificationPermissionClick: () -> Unit = {}, + autoStartAtBoot: Boolean, + onAutoStartAtBootToggled: (Boolean) -> Unit = {}, +) { + Column(modifier) { + OptionsHeaderRow( + modifier = Modifier.padding(horizontal = 16.dp), + icon = Icons.Rounded.Checklist, + text = stringResource(R.string.pro_mode_set_up_title), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // Show notification permission warning if permission not granted + if (state is ProModeState.Stopped && !state.isNotificationPermissionGranted) { + SetupCard( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + color = MaterialTheme.colorScheme.errorContainer, + icon = { + Icon( + imageVector = Icons.Rounded.Notifications, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + ) + }, + title = stringResource(R.string.pro_mode_setup_wizard_enable_notification_permission_title), + content = { + Text( + text = stringResource(R.string.pro_mode_setup_wizard_enable_notification_permission_description), + style = MaterialTheme.typography.bodyMedium, + ) + }, + buttonText = stringResource(R.string.pro_mode_setup_wizard_enable_notification_permission_button), + onButtonClick = onRequestNotificationPermissionClick, + ) + Spacer(modifier = Modifier.height(8.dp)) + } + + when (state) { + ProModeState.Started -> { + EmergencyTipCard( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + ProModeStartedCard( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + onStopClick = onStopServiceClick, + ) + } + + is ProModeState.Stopped -> { + if (state.isRootGranted) { + SetupCard( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + color = LocalCustomColorsPalette.current.magiskTeal, + icon = { + Icon( + imageVector = Icons.Rounded.Numbers, + contentDescription = null, + tint = LocalCustomColorsPalette.current.magiskTeal, + ) + }, + title = stringResource(R.string.pro_mode_root_detected_title), + content = { + Text( + text = stringResource(R.string.pro_mode_root_detected_text), + style = MaterialTheme.typography.bodyMedium, + ) + }, + buttonText = stringResource(R.string.pro_mode_root_detected_button_start_service), + onButtonClick = onRootButtonClick, + enabled = state.isNotificationPermissionGranted, + ) + + Spacer(modifier = Modifier.height(8.dp)) + } + + val shizukuButtonText: String? = when (state.shizukuSetupState) { + ShizukuSetupState.INSTALLED -> stringResource(R.string.pro_mode_shizuku_detected_button_start) + ShizukuSetupState.STARTED -> stringResource(R.string.pro_mode_shizuku_detected_button_request_permission) + ShizukuSetupState.PERMISSION_GRANTED -> stringResource(R.string.pro_mode_shizuku_detected_button_start_service) + ShizukuSetupState.NOT_FOUND -> null + } + + if (shizukuButtonText != null) { + SetupCard( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + color = LocalCustomColorsPalette.current.shizukuBlue, + icon = { + Image( + imageVector = KeyMapperIcons.FakeShizuku, + contentDescription = null, + ) + }, + title = stringResource(R.string.pro_mode_shizuku_detected_title), + content = { + Text( + text = stringResource(R.string.pro_mode_shizuku_detected_text), + style = MaterialTheme.typography.bodyMedium, + ) + }, + buttonText = shizukuButtonText, + onButtonClick = onShizukuButtonClick, + enabled = state.isNotificationPermissionGranted, + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + val setupKeyMapperText: String = when { + Build.VERSION.SDK_INT < Build.VERSION_CODES.R -> stringResource(R.string.pro_mode_set_up_with_key_mapper_button_incompatible) + else -> stringResource(R.string.pro_mode_set_up_with_key_mapper_button) + } + + SetupCard( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + color = MaterialTheme.colorScheme.primaryContainer, + icon = { + Image( + modifier = Modifier.padding(2.dp), + imageVector = KeyMapperIcons.KeyMapperIcon, + contentDescription = null, + ) + }, + title = stringResource(R.string.pro_mode_set_up_with_key_mapper_title), + content = {}, + buttonText = setupKeyMapperText, + onButtonClick = onSetupWithKeyMapperClick, + enabled = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && state.isNotificationPermissionGranted, + ) + } + } + + // Options section + Spacer(modifier = Modifier.height(16.dp)) + + OptionsHeaderRow( + modifier = Modifier.padding(horizontal = 16.dp), + icon = Icons.Rounded.Tune, + text = stringResource(R.string.pro_mode_options_title), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + SwitchPreferenceCompose( + modifier = Modifier.padding(horizontal = 8.dp), + title = stringResource(R.string.title_pref_pro_mode_auto_start_at_boot), + text = stringResource(R.string.summary_pref_pro_mode_auto_start_at_boot), + icon = Icons.Rounded.RestartAlt, + isChecked = autoStartAtBoot, + onCheckedChange = onAutoStartAtBootToggled, + ) + } +} + +@Composable +private fun WarningCard( + modifier: Modifier = Modifier, + state: ProModeWarningState, + onButtonClick: () -> Unit = {}, +) { + val borderStroke = if (state is ProModeWarningState.Understood) { + CardDefaults.outlinedCardBorder() + } else { + BorderStroke(1.dp, MaterialTheme.colorScheme.error) + } + + OutlinedCard( + modifier = modifier, + border = borderStroke, + elevation = CardDefaults.elevatedCardElevation(), + ) { + Spacer(modifier = Modifier.height(16.dp)) + Row(modifier = Modifier.padding(horizontal = 16.dp)) { + Icon( + imageVector = Icons.Rounded.WarningAmber, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = stringResource(R.string.pro_mode_warning_title), + style = MaterialTheme.typography.titleMedium, + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = stringResource(R.string.pro_mode_warning_text), + style = MaterialTheme.typography.bodyMedium, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + FilledTonalButton( + modifier = Modifier + .align(Alignment.End) + .padding(horizontal = 16.dp), + onClick = onButtonClick, + enabled = state is ProModeWarningState.Idle, + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.error, + contentColor = MaterialTheme.colorScheme.onError, + ), + ) { + if (state is ProModeWarningState.Understood) { + Icon(imageVector = Icons.Rounded.Check, contentDescription = null) + + Spacer(modifier = Modifier.width(8.dp)) + } + + val text = when (state) { + is ProModeWarningState.CountingDown -> stringResource( + R.string.pro_mode_warning_understand_button_countdown, + state.seconds, + ) + + ProModeWarningState.Idle -> stringResource(R.string.pro_mode_warning_understand_button_not_completed) + ProModeWarningState.Understood -> stringResource(R.string.pro_mode_warning_understand_button_completed) + } + + Text(text) + } + + Spacer(modifier = Modifier.height(16.dp)) + } +} + +@Composable +private fun ProModeStartedCard( + modifier: Modifier = Modifier, + onStopClick: () -> Unit = {}, +) { + OutlinedCard(modifier) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(modifier = Modifier.width(16.dp)) + + Icon( + imageVector = Icons.Rounded.Check, + contentDescription = null, + tint = LocalCustomColorsPalette.current.green, + ) + + Spacer(modifier = Modifier.width(16.dp)) + + Text( + modifier = Modifier + .weight(1f) + .padding(vertical = 8.dp), + text = stringResource(R.string.pro_mode_service_started), + style = MaterialTheme.typography.titleMedium, + ) + + Spacer(modifier = Modifier.width(16.dp)) + + TextButton( + onClick = onStopClick, + colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error), + ) { + Text(stringResource(R.string.pro_mode_stop_service_button)) + } + + Spacer(modifier = Modifier.width(16.dp)) + } + } +} + +@Composable +private fun SetupCard( + modifier: Modifier = Modifier, + color: Color, + icon: @Composable () -> Unit, + title: String, + content: @Composable () -> Unit, + buttonText: String, + onButtonClick: () -> Unit = {}, + enabled: Boolean = true, +) { + OutlinedCard(modifier = modifier) { + Spacer(modifier = Modifier.height(16.dp)) + Row(modifier = Modifier.padding(horizontal = 16.dp)) { + Box(Modifier.size(24.dp)) { + icon() + } + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) { + content() + } + + Spacer(modifier = Modifier.height(8.dp)) + + FilledTonalButton( + modifier = Modifier + .align(Alignment.End) + .padding(horizontal = 16.dp), + onClick = onButtonClick, + enabled = enabled, + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = color, + contentColor = LocalCustomColorsPalette.current.contentColorFor(color), + ), + ) { + Text(buttonText) + } + + Spacer(modifier = Modifier.height(16.dp)) + } +} + +@Composable +private fun EmergencyTipCard( + modifier: Modifier = Modifier, +) { + OutlinedCard( + modifier = modifier, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary), + elevation = CardDefaults.elevatedCardElevation(), + ) { + Spacer(modifier = Modifier.height(16.dp)) + Row(modifier = Modifier.padding(horizontal = 16.dp)) { + Icon( + imageVector = Icons.Rounded.Info, + contentDescription = null, + tint = MaterialTheme.colorScheme.tertiary, + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = stringResource(R.string.pro_mode_emergency_tip_title), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface, + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = stringResource(R.string.pro_mode_emergency_tip_text), + style = MaterialTheme.typography.bodyMedium, + ) + + Spacer(modifier = Modifier.height(16.dp)) + } +} + +@Composable +private fun ProModeInfoCard( + modifier: Modifier = Modifier, + onDismiss: () -> Unit = {}, +) { + OutlinedCard( + modifier = modifier, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.primary), + elevation = CardDefaults.elevatedCardElevation(), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.Top, + ) { + Column(modifier = Modifier.weight(1f)) { + Row { + Icon( + imageVector = Icons.AutoMirrored.Rounded.HelpOutline, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = stringResource(R.string.pro_mode_info_card_title), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface, + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = stringResource(R.string.pro_mode_info_card_description), + style = MaterialTheme.typography.bodyMedium, + ) + } + + Spacer(modifier = Modifier.width(8.dp)) + + IconButton( + onClick = onDismiss, + modifier = Modifier.size(24.dp), + ) { + Icon( + imageVector = Icons.Rounded.Close, + contentDescription = stringResource(R.string.pro_mode_info_card_dismiss_content_description), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } +} + +@Preview +@Composable +private fun Preview() { + KeyMapperTheme { + ProModeScreen { + Content( + warningState = ProModeWarningState.Understood, + setupState = State.Data( + ProModeState.Stopped( + isRootGranted = false, + shizukuSetupState = ShizukuSetupState.PERMISSION_GRANTED, + isNotificationPermissionGranted = true, + ), + ), + showInfoCard = true, + onInfoCardDismiss = {}, + autoStartAtBoot = false, + onAutoStartAtBootToggled = {}, + ) + } + } +} + +@Preview +@Composable +private fun PreviewDark() { + KeyMapperTheme(darkTheme = true) { + ProModeScreen { + Content( + warningState = ProModeWarningState.Understood, + setupState = State.Data(ProModeState.Started), + showInfoCard = false, + onInfoCardDismiss = {}, + autoStartAtBoot = true, + onAutoStartAtBootToggled = {}, + ) + } + } +} + +@Preview +@Composable +private fun PreviewCountingDown() { + KeyMapperTheme { + ProModeScreen { + Content( + warningState = ProModeWarningState.CountingDown( + seconds = 5, + ), + setupState = State.Loading, + showInfoCard = true, + onInfoCardDismiss = {}, + autoStartAtBoot = false, + onAutoStartAtBootToggled = {}, + ) + } + } +} + +@Preview +@Composable +private fun PreviewStarted() { + KeyMapperTheme { + ProModeScreen { + Content( + warningState = ProModeWarningState.Understood, + setupState = State.Data(ProModeState.Started), + showInfoCard = false, + onInfoCardDismiss = {}, + autoStartAtBoot = false, + onAutoStartAtBootToggled = {}, + ) + } + } +} + +@Preview +@Composable +private fun PreviewNotificationPermissionNotGranted() { + KeyMapperTheme { + ProModeScreen { + Content( + warningState = ProModeWarningState.Understood, + setupState = State.Data( + ProModeState.Stopped( + isRootGranted = true, + shizukuSetupState = ShizukuSetupState.PERMISSION_GRANTED, + isNotificationPermissionGranted = false, + ), + ), + showInfoCard = false, + onInfoCardDismiss = {}, + autoStartAtBoot = false, + onAutoStartAtBootToggled = {}, + ) + } + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt new file mode 100644 index 0000000000..1e597b1581 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupScreen.kt @@ -0,0 +1,537 @@ +package io.github.sds100.keymapper.base.promode + +import android.content.res.Configuration +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.EaseInOut +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material.icons.rounded.Accessibility +import androidx.compose.material.icons.rounded.BugReport +import androidx.compose.material.icons.rounded.Build +import androidx.compose.material.icons.rounded.CheckCircleOutline +import androidx.compose.material.icons.rounded.Link +import androidx.compose.material.icons.rounded.Notifications +import androidx.compose.material.icons.rounded.PlayArrow +import androidx.compose.material3.Button +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import io.github.sds100.keymapper.base.compose.LocalCustomColorsPalette +import io.github.sds100.keymapper.base.utils.ui.compose.icons.KeyMapperIcons +import io.github.sds100.keymapper.base.utils.ui.compose.icons.SignalWifiNotConnected +import io.github.sds100.keymapper.common.utils.State +import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupStep + +@Composable +fun ProModeSetupScreen( + viewModel: ProModeSetupViewModel, +) { + val state by viewModel.setupState.collectAsStateWithLifecycle() + + ProModeSetupScreen( + state = state, + onStepButtonClick = viewModel::onStepButtonClick, + onAssistantClick = viewModel::onAssistantClick, + onWatchTutorialClick = { }, + onBackClick = viewModel::onBackClick, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ProModeSetupScreen( + state: State, + onBackClick: () -> Unit = {}, + onStepButtonClick: () -> Unit = {}, + onAssistantClick: () -> Unit = {}, + onWatchTutorialClick: () -> Unit = {}, +) { + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.pro_mode_setup_wizard_title)) }, + navigationIcon = { + IconButton(onClick = onBackClick) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = stringResource(id = R.string.action_go_back), + ) + } + }, + ) + }, + ) { paddingValues -> + when (state) { + State.Loading -> { + Box( + Modifier + .padding(paddingValues) + .fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator() + } + } + + is State.Data -> { + val stepContent = getStepContent(state.data.step) + + // Create animated progress for entrance and updates + val progressAnimatable = remember { Animatable(0f) } + val targetProgress = state.data.stepNumber.toFloat() / (state.data.stepCount) + + // Animate progress when it changes + LaunchedEffect(targetProgress) { + progressAnimatable.animateTo( + targetValue = targetProgress, + animationSpec = tween( + durationMillis = 800, + easing = EaseInOut, + ), + ) + } + + // Animate entrance when screen opens + LaunchedEffect(Unit) { + progressAnimatable.animateTo( + targetValue = targetProgress, + animationSpec = tween( + durationMillis = 1000, + easing = EaseInOut, + ), + ) + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(vertical = 16.dp, horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth(), + progress = { progressAnimatable.value }, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource( + R.string.pro_mode_setup_wizard_step_n, + state.data.stepNumber, + state.data.stepCount, + ), + style = MaterialTheme.typography.titleLarge, + ) + Text( + text = stringResource(R.string.pro_mode_app_bar_title), + style = MaterialTheme.typography.titleLarge, + ) + } + Spacer(modifier = Modifier.height(16.dp)) + + AssistantCheckBoxRow( + modifier = Modifier.fillMaxWidth(), + isEnabled = state.data.isSetupAssistantButtonEnabled, + isChecked = state.data.isSetupAssistantChecked, + onAssistantClick = onAssistantClick, + ) + + val iconTint = if (state.data.step == SystemBridgeSetupStep.STARTED) { + LocalCustomColorsPalette.current.green + } else { + MaterialTheme.colorScheme.onSurface + } + + StepContent( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .padding(horizontal = 16.dp), + stepContent, + onWatchTutorialClick, + onStepButtonClick, + iconTint = iconTint, + ) + } + } + } + } +} + +@Composable +private fun StepContent( + modifier: Modifier = Modifier, + stepContent: StepContent, + onWatchTutorialClick: () -> Unit, + onButtonClick: () -> Unit, + iconTint: Color = Color.Unspecified, +) { + Column( + modifier, + ) { + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .fillMaxWidth() + .weight(1f), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + modifier = Modifier.size(64.dp), + imageVector = stepContent.icon, + contentDescription = null, + tint = iconTint, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stepContent.title, + style = MaterialTheme.typography.headlineSmall, + textAlign = TextAlign.Center, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stepContent.message, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyMedium, + ) + } + + Spacer(Modifier.height(32.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { +// TextButton(onClick = onWatchTutorialClick) { +// Text(text = stringResource(R.string.pro_mode_setup_wizard_watch_tutorial_button)) +// } + Button(onClick = onButtonClick) { + Text(text = stepContent.buttonText) + } + } + } +} + +@Composable +private fun AssistantCheckBoxRow( + modifier: Modifier, + isEnabled: Boolean, + isChecked: Boolean, + onAssistantClick: () -> Unit, +) { + Surface( + modifier = modifier, + shape = MaterialTheme.shapes.medium, + enabled = isEnabled, + onClick = onAssistantClick, + ) { + val contentColor = if (isEnabled) { + LocalContentColor.current + } else { + LocalContentColor.current.copy(alpha = 0.5f) + } + + CompositionLocalProvider(LocalContentColor provides contentColor) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Checkbox( + enabled = isEnabled, + checked = isChecked, + onCheckedChange = { onAssistantClick() }, + ) + Column { + Text( + text = stringResource(R.string.pro_mode_setup_wizard_use_assistant), + style = MaterialTheme.typography.titleMedium, + ) + + val text = if (isEnabled) { + stringResource(R.string.pro_mode_setup_wizard_use_assistant_description) + } else { + stringResource(R.string.pro_mode_setup_wizard_use_assistant_enable_accessibility_service) + } + + Text( + text = text, + style = MaterialTheme.typography.bodyMedium, + ) + } + } + } + } +} + +@Composable +private fun getStepContent(step: SystemBridgeSetupStep): StepContent { + return when (step) { + SystemBridgeSetupStep.ACCESSIBILITY_SERVICE -> StepContent( + title = stringResource(R.string.pro_mode_setup_wizard_enable_accessibility_service_title), + message = stringResource(R.string.pro_mode_setup_wizard_enable_accessibility_service_description), + icon = Icons.Rounded.Accessibility, + buttonText = stringResource(R.string.pro_mode_setup_wizard_enable_accessibility_service_button), + ) + + SystemBridgeSetupStep.NOTIFICATION_PERMISSION -> StepContent( + title = stringResource(R.string.pro_mode_setup_wizard_enable_notification_permission_title), + message = stringResource(R.string.pro_mode_setup_wizard_enable_notification_permission_description), + icon = Icons.Rounded.Notifications, + buttonText = stringResource(R.string.pro_mode_setup_wizard_enable_notification_permission_button), + ) + + SystemBridgeSetupStep.DEVELOPER_OPTIONS -> StepContent( + title = stringResource(R.string.pro_mode_setup_wizard_enable_developer_options_title), + message = stringResource(R.string.pro_mode_setup_wizard_enable_developer_options_description), + icon = Icons.Rounded.Build, + buttonText = stringResource(R.string.pro_mode_setup_wizard_go_to_settings_button), + ) + + SystemBridgeSetupStep.WIFI_NETWORK -> StepContent( + title = stringResource(R.string.pro_mode_setup_wizard_connect_wifi_title), + message = stringResource(R.string.pro_mode_setup_wizard_connect_wifi_description), + icon = KeyMapperIcons.SignalWifiNotConnected, + buttonText = stringResource(R.string.pro_mode_setup_wizard_go_to_settings_button), + ) + + SystemBridgeSetupStep.WIRELESS_DEBUGGING -> StepContent( + title = stringResource(R.string.pro_mode_setup_wizard_enable_wireless_debugging_title), + message = stringResource(R.string.pro_mode_setup_wizard_enable_wireless_debugging_description), + icon = Icons.Rounded.BugReport, + buttonText = stringResource(R.string.pro_mode_setup_wizard_go_to_settings_button), + ) + + SystemBridgeSetupStep.ADB_PAIRING -> StepContent( + title = stringResource(R.string.pro_mode_setup_wizard_pair_wireless_debugging_title), + message = stringResource(R.string.pro_mode_setup_wizard_pair_wireless_debugging_description), + icon = Icons.Rounded.Link, + buttonText = stringResource(R.string.pro_mode_setup_wizard_go_to_settings_button), + ) + + SystemBridgeSetupStep.START_SERVICE -> StepContent( + title = stringResource(R.string.pro_mode_setup_wizard_start_service_title), + message = stringResource(R.string.pro_mode_setup_wizard_start_service_description), + icon = Icons.Rounded.PlayArrow, + buttonText = stringResource(R.string.pro_mode_root_detected_button_start_service), + ) + + SystemBridgeSetupStep.STARTED -> StepContent( + title = stringResource(R.string.pro_mode_setup_wizard_complete_title), + message = stringResource(R.string.pro_mode_setup_wizard_complete_text), + icon = Icons.Rounded.CheckCircleOutline, + buttonText = stringResource(R.string.pro_mode_setup_wizard_complete_button), + ) + } +} + +private data class StepContent( + val title: String, + val message: String, + val icon: ImageVector, + val buttonText: String, +) + +@Preview(name = "Accessibility Service Step") +@Composable +private fun ProModeSetupScreenAccessibilityServicePreview() { + KeyMapperTheme { + ProModeSetupScreen( + state = State.Data( + ProModeSetupState( + stepNumber = 1, + stepCount = 6, + step = SystemBridgeSetupStep.ACCESSIBILITY_SERVICE, + isSetupAssistantChecked = false, + isSetupAssistantButtonEnabled = false, + ), + ), + ) + } +} + +@Preview(name = "Notification Permission Step") +@Composable +private fun ProModeSetupScreenNotificationPermissionPreview() { + KeyMapperTheme { + ProModeSetupScreen( + state = State.Data( + ProModeSetupState( + stepNumber = 2, + stepCount = 6, + step = SystemBridgeSetupStep.NOTIFICATION_PERMISSION, + isSetupAssistantChecked = false, + isSetupAssistantButtonEnabled = true, + ), + ), + ) + } +} + +@Preview(name = "Developer Options Step") +@Composable +private fun ProModeSetupScreenDeveloperOptionsPreview() { + KeyMapperTheme { + ProModeSetupScreen( + state = State.Data( + ProModeSetupState( + stepNumber = 2, + stepCount = 6, + step = SystemBridgeSetupStep.DEVELOPER_OPTIONS, + isSetupAssistantChecked = false, + isSetupAssistantButtonEnabled = true, + ), + ), + ) + } +} + +@Preview(name = "WiFi Network Step") +@Composable +private fun ProModeSetupScreenWifiNetworkPreview() { + KeyMapperTheme { + ProModeSetupScreen( + state = State.Data( + ProModeSetupState( + stepNumber = 3, + stepCount = 6, + step = SystemBridgeSetupStep.WIFI_NETWORK, + isSetupAssistantChecked = false, + isSetupAssistantButtonEnabled = true, + ), + ), + ) + } +} + +@Preview(name = "Wireless Debugging Step") +@Composable +private fun ProModeSetupScreenWirelessDebuggingPreview() { + KeyMapperTheme { + ProModeSetupScreen( + state = State.Data( + ProModeSetupState( + stepNumber = 4, + stepCount = 6, + step = SystemBridgeSetupStep.WIRELESS_DEBUGGING, + isSetupAssistantChecked = false, + isSetupAssistantButtonEnabled = true, + ), + ), + ) + } +} + +@Preview(name = "ADB Pairing Step", widthDp = 400, heightDp = 400) +@Composable +private fun ProModeSetupScreenAdbPairingPreview() { + KeyMapperTheme { + ProModeSetupScreen( + state = State.Data( + ProModeSetupState( + stepNumber = 5, + stepCount = 6, + step = SystemBridgeSetupStep.ADB_PAIRING, + isSetupAssistantChecked = true, + isSetupAssistantButtonEnabled = true, + ), + ), + ) + } +} + +@Preview(name = "Start Service Step", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun ProModeSetupScreenStartServicePreview() { + KeyMapperTheme { + ProModeSetupScreen( + state = State.Data( + ProModeSetupState( + stepNumber = 6, + stepCount = 6, + step = SystemBridgeSetupStep.START_SERVICE, + isSetupAssistantChecked = true, + isSetupAssistantButtonEnabled = true, + ), + ), + ) + } +} + +@Preview(name = "Started", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun ProModeSetupScreenStartedPreview() { + KeyMapperTheme { + ProModeSetupScreen( + state = State.Data( + ProModeSetupState( + stepNumber = 8, + stepCount = 8, + step = SystemBridgeSetupStep.STARTED, + isSetupAssistantChecked = true, + isSetupAssistantButtonEnabled = true, + ), + ), + ) + } +} + +@Preview(name = "Loading", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun ProModeSetupScreenLoadingPreview() { + KeyMapperTheme { + ProModeSetupScreen( + state = State.Loading, + ) + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupViewModel.kt new file mode 100644 index 0000000000..442089f9f9 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeSetupViewModel.kt @@ -0,0 +1,88 @@ +package io.github.sds100.keymapper.base.promode + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider +import io.github.sds100.keymapper.base.utils.ui.ResourceProvider +import io.github.sds100.keymapper.common.utils.State +import io.github.sds100.keymapper.common.utils.dataOrNull +import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupStep +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class ProModeSetupViewModel @Inject constructor( + private val useCase: SystemBridgeSetupUseCase, + navigationProvider: NavigationProvider, + resourceProvider: ResourceProvider, +) : ViewModel(), NavigationProvider by navigationProvider, ResourceProvider by resourceProvider { + val setupState: StateFlow> = + combine(useCase.nextSetupStep, useCase.isSetupAssistantEnabled, ::buildState).stateIn( + viewModelScope, + SharingStarted.Eagerly, + State.Loading, + ) + + fun onStepButtonClick() { + // Do not check the latest value in the use case because there is significant latency + // when it is checking whether it is paired + val currentStep = setupState.value.dataOrNull()?.step ?: return + + when (currentStep) { + SystemBridgeSetupStep.ACCESSIBILITY_SERVICE -> useCase.enableAccessibilityService() + SystemBridgeSetupStep.NOTIFICATION_PERMISSION -> useCase.requestNotificationPermission() + SystemBridgeSetupStep.DEVELOPER_OPTIONS -> useCase.enableDeveloperOptions() + SystemBridgeSetupStep.WIFI_NETWORK -> useCase.connectWifiNetwork() + SystemBridgeSetupStep.WIRELESS_DEBUGGING -> useCase.enableWirelessDebugging() + SystemBridgeSetupStep.ADB_PAIRING -> useCase.pairWirelessAdb() + SystemBridgeSetupStep.START_SERVICE -> viewModelScope.launch { useCase.startSystemBridgeWithAdb() } + SystemBridgeSetupStep.STARTED -> viewModelScope.launch { popBackStack() } + } + } + + fun onBackClick() { + viewModelScope.launch { + popBackStack() + } + } + + fun onAssistantClick() { + useCase.toggleSetupAssistant() + } + + private fun buildState( + step: SystemBridgeSetupStep, + isSetupAssistantUserEnabled: Boolean, + ): State.Data { + // Uncheck the setup assistant if the accessibility service is disabled since it is + // required for the setup assistant to work + val isSetupAssistantChecked = if (step == SystemBridgeSetupStep.ACCESSIBILITY_SERVICE) { + false + } else { + isSetupAssistantUserEnabled + } + + return State.Data( + ProModeSetupState( + stepNumber = step.stepIndex + 1, + stepCount = SystemBridgeSetupStep.entries.size, + step = step, + isSetupAssistantChecked = isSetupAssistantChecked, + isSetupAssistantButtonEnabled = step != SystemBridgeSetupStep.ACCESSIBILITY_SERVICE && step != SystemBridgeSetupStep.STARTED, + ), + ) + } +} + +data class ProModeSetupState( + val stepNumber: Int, + val stepCount: Int, + val step: SystemBridgeSetupStep, + val isSetupAssistantChecked: Boolean, + val isSetupAssistantButtonEnabled: Boolean, +) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt new file mode 100644 index 0000000000..70fb145df6 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ProModeViewModel.kt @@ -0,0 +1,186 @@ +package io.github.sds100.keymapper.base.promode + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import io.github.sds100.keymapper.base.utils.navigation.NavDestination +import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider +import io.github.sds100.keymapper.base.utils.navigation.navigate +import io.github.sds100.keymapper.base.utils.ui.DialogProvider +import io.github.sds100.keymapper.base.utils.ui.ResourceProvider +import io.github.sds100.keymapper.common.utils.State +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class ProModeViewModel @Inject constructor( + private val useCase: SystemBridgeSetupUseCase, + resourceProvider: ResourceProvider, + dialogProvider: DialogProvider, + navigationProvider: NavigationProvider, +) : ViewModel(), + ResourceProvider by resourceProvider, + DialogProvider by dialogProvider, + NavigationProvider by navigationProvider { + + companion object { + private const val WARNING_COUNT_DOWN_SECONDS = 5 + } + + @OptIn(ExperimentalCoroutinesApi::class) + val warningState: StateFlow = + useCase.isWarningUnderstood + .flatMapLatest { isUnderstood -> createWarningStateFlow(isUnderstood) } + .stateIn( + viewModelScope, + SharingStarted.Eagerly, + ProModeWarningState.CountingDown( + WARNING_COUNT_DOWN_SECONDS, + ), + ) + + val setupState: StateFlow> = + combine( + useCase.isSystemBridgeConnected, + useCase.isRootGranted, + useCase.shizukuSetupState, + useCase.isNotificationPermissionGranted, + ::buildSetupState, + ).stateIn(viewModelScope, SharingStarted.Eagerly, State.Loading) + + val autoStartBootEnabled: StateFlow = + useCase.isAutoStartBootEnabled + .stateIn(viewModelScope, SharingStarted.Eagerly, false) + + var showInfoCard by mutableStateOf(!useCase.isInfoDismissed()) + private set + + fun hideInfoCard() { + showInfoCard = false + // Save that they've dismissed the card so it is not shown by default the next + // time they visit the PRO mode page. + useCase.dismissInfo() + } + + fun showInfoCard() { + showInfoCard = true + } + + private fun createWarningStateFlow(isUnderstood: Boolean): Flow = + if (isUnderstood) { + flowOf(ProModeWarningState.Understood) + } else { + flow { + repeat(WARNING_COUNT_DOWN_SECONDS) { + emit(ProModeWarningState.CountingDown(WARNING_COUNT_DOWN_SECONDS - it)) + delay(1000L) + } + + emit(ProModeWarningState.Idle) + } + } + + fun onWarningButtonClick() { + useCase.onUnderstoodWarning() + } + + fun onStopServiceClick() { + useCase.stopSystemBridge() + } + + fun onRootButtonClick() { + useCase.startSystemBridgeWithRoot() + } + + fun onShizukuButtonClick() { + viewModelScope.launch { + val shizukuState = useCase.shizukuSetupState.first() + when (shizukuState) { + ShizukuSetupState.NOT_FOUND -> { + // Do nothing + } + + ShizukuSetupState.INSTALLED -> { + useCase.openShizukuApp() + } + + ShizukuSetupState.STARTED -> { + useCase.requestShizukuPermission() + } + + ShizukuSetupState.PERMISSION_GRANTED -> { + useCase.startSystemBridgeWithShizuku() + } + } + } + } + + fun onBackClick() { + viewModelScope.launch { + popBackStack() + } + } + + fun onSetupWithKeyMapperClick() { + viewModelScope.launch { + navigate("setup_pro_mode_with_key_mapper", NavDestination.ProModeSetup) + } + } + + fun onRequestNotificationPermissionClick() { + useCase.requestNotificationPermission() + } + + fun onAutoStartBootToggled() { + useCase.toggleAutoStartBoot() + } + + private fun buildSetupState( + isSystemBridgeConnected: Boolean, + isRootGranted: Boolean, + shizukuSetupState: ShizukuSetupState, + isNotificationPermissionGranted: Boolean, + ): State { + if (isSystemBridgeConnected) { + return State.Data(ProModeState.Started) + } else { + return State.Data( + ProModeState.Stopped( + isRootGranted = isRootGranted, + shizukuSetupState = shizukuSetupState, + isNotificationPermissionGranted = isNotificationPermissionGranted, + ), + ) + } + } +} + +sealed class ProModeWarningState { + data class CountingDown(val seconds: Int) : ProModeWarningState() + data object Idle : ProModeWarningState() + data object Understood : ProModeWarningState() +} + +sealed class ProModeState { + data class Stopped( + val isRootGranted: Boolean, + val shizukuSetupState: ShizukuSetupState, + val isNotificationPermissionGranted: Boolean, + ) : ProModeState() + + data object Started : ProModeState() +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/ShizukuSetupState.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/ShizukuSetupState.kt new file mode 100644 index 0000000000..1c9814a519 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/ShizukuSetupState.kt @@ -0,0 +1,8 @@ +package io.github.sds100.keymapper.base.promode + +enum class ShizukuSetupState { + NOT_FOUND, + INSTALLED, + STARTED, + PERMISSION_GRANTED, +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeAutoStarter.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeAutoStarter.kt new file mode 100644 index 0000000000..5c7225683b --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeAutoStarter.kt @@ -0,0 +1,252 @@ +package io.github.sds100.keymapper.base.promode + +import android.os.Build +import android.os.SystemClock +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat +import io.github.sds100.keymapper.base.BaseMainActivity +import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.system.notifications.NotificationController.Companion.CHANNEL_SETUP_ASSISTANT +import io.github.sds100.keymapper.base.system.notifications.NotificationController.Companion.ID_SYSTEM_BRIDGE_STATUS +import io.github.sds100.keymapper.base.utils.ui.ResourceProvider +import io.github.sds100.keymapper.common.notifications.KMNotificationAction +import io.github.sds100.keymapper.data.Keys +import io.github.sds100.keymapper.data.PreferenceDefaults +import io.github.sds100.keymapper.data.repositories.PreferenceRepository +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState +import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController +import io.github.sds100.keymapper.system.network.NetworkAdapter +import io.github.sds100.keymapper.system.notifications.NotificationAdapter +import io.github.sds100.keymapper.system.notifications.NotificationModel +import io.github.sds100.keymapper.system.permissions.Permission +import io.github.sds100.keymapper.system.permissions.PermissionAdapter +import io.github.sds100.keymapper.system.root.SuAdapter +import io.github.sds100.keymapper.system.shizuku.ShizukuAdapter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +/** + * This class handles auto starting the system bridge when Key Mapper is launched and when + * the System Bridge is killed not due to the user. + */ +@RequiresApi(Build.VERSION_CODES.Q) +@Singleton +class SystemBridgeAutoStarter @Inject constructor( + private val coroutineScope: CoroutineScope, + private val suAdapter: SuAdapter, + private val shizukuAdapter: ShizukuAdapter, + private val connectionManager: SystemBridgeConnectionManager, + private val setupController: SystemBridgeSetupController, + private val preferences: PreferenceRepository, + private val networkAdapter: NetworkAdapter, + private val permissionAdapter: PermissionAdapter, + private val notificationAdapter: NotificationAdapter, + private val resourceProvider: ResourceProvider, +) : ResourceProvider by resourceProvider { + enum class AutoStartType { + ADB, + SHIZUKU, + ROOT, + } + + // Use flatMapLatest so that any calls to ADB are only done if strictly necessary. + @OptIn(ExperimentalCoroutinesApi::class) + private val autoStartTypeFlow: Flow = + suAdapter.isRootGranted.flatMapLatest { isRooted -> + if (isRooted) { + flowOf(AutoStartType.ROOT) + } else { + shizukuAdapter.isStarted.flatMapLatest { isShizukuStarted -> + if (isShizukuStarted) { + flowOf(AutoStartType.SHIZUKU) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + combine( + permissionAdapter.isGrantedFlow(Permission.WRITE_SECURE_SETTINGS), + networkAdapter.isWifiConnected, + ) { isWriteSecureSettingsGranted, isWifiConnected -> + isWriteSecureSettingsGranted && isWifiConnected && setupController.isAdbPaired() + }.distinctUntilChanged() + .map { isAdbAutoStartAllowed -> + if (isAdbAutoStartAllowed) AutoStartType.ADB else null + }.filterNotNull() + } else { + flowOf(null) + } + } + } + } + + /** + * This emits values when the system bridge needs restarting after it being killed. + */ + @OptIn(ExperimentalCoroutinesApi::class) + private val restartFlow: Flow = + connectionManager.connectionState.flatMapLatest { connectionState -> + // Do not autostart if it is connected or it was killed from the user + if (connectionState !is SystemBridgeConnectionState.Disconnected || connectionState.isExpected) { + flowOf(null) + } else { + // Do not autostart if the system bridge was killed shortly after. + // This prevents infinite loops happening. + if (lastAutoStartTime != null && connectionState.time - lastAutoStartTime!! < 30000) { + Timber.w("Not auto starting the system bridge because it was last auto started less than 30 secs ago") + showSystemBridgeKilledNotification(getString(R.string.system_bridge_died_notification_not_restarting_text)) + flowOf(null) + } else { + autoStartTypeFlow + } + } + } + + private var lastAutoStartTime: Long? = null + + /** + * This must only be called once in the application lifecycle + */ + @OptIn(FlowPreview::class) + fun init() { + coroutineScope.launch { + // The Key Mapper process may not necessarily be started on boot due to the + // on boot receiver so assume if it is started within 30 seconds of boot that + // it should be auto started. + val isBoot = SystemClock.uptimeMillis() < 30000 + + val isBootAutoStartEnabled = preferences.get(Keys.isProModeAutoStartBootEnabled) + .map { it ?: PreferenceDefaults.PRO_MODE_AUTOSTART_BOOT } + .first() + + // Wait 5 seconds for the system bridge to potentially connect itself to Key Mapper + // before starting it. + delay(5000) + + val connectionState = connectionManager.connectionState.value + + if (isBoot && isBootAutoStartEnabled && connectionState !is SystemBridgeConnectionState.Connected) { + val autoStartType = autoStartTypeFlow.first() + + if (autoStartType != null) { + autoStart(autoStartType) + } + } + + // Only start collecting the restart flow after potentially auto starting it for the first time. + restartFlow + .distinctUntilChanged() // Must come before the filterNotNull + .filterNotNull() + .collectLatest { type -> + autoStart(type) + } + } + } + + private suspend fun autoStart(type: AutoStartType) { + if (isSystemBridgeEmergencyKilled()) { + Timber.w("Not auto starting the system bridge because it was emergency killed by the user") + return + } + + lastAutoStartTime = SystemClock.elapsedRealtime() + + when (type) { + AutoStartType.ADB -> { + Timber.i("Auto starting system bridge with ADB") + showAutoStartNotification(getString(R.string.pro_mode_setup_notification_auto_start_system_bridge_adb_text)) + + setupController.autoStartWithAdb() + } + + AutoStartType.SHIZUKU -> { + Timber.i("Auto starting system bridge with Shizuku") + showAutoStartNotification(getString(R.string.pro_mode_setup_notification_auto_start_system_bridge_shizuku_text)) + connectionManager.startWithShizuku() + } + + AutoStartType.ROOT -> { + Timber.i("Auto starting system bridge with root") + showAutoStartNotification(getString(R.string.pro_mode_setup_notification_auto_start_system_bridge_root_text)) + connectionManager.startWithRoot() + } + } + + // Wait 30 seconds for it to start, and if not then show failed notification. + try { + withTimeout(30000L) { + connectionManager.connectionState.first { it is SystemBridgeConnectionState.Connected } + } + } catch (_: TimeoutCancellationException) { + showAutoStartFailedNotification() + } + } + + private suspend fun isSystemBridgeEmergencyKilled(): Boolean { + return preferences.get(Keys.isSystemBridgeEmergencyKilled).first() == true + } + + private fun showSystemBridgeKilledNotification(text: String) { + val model = NotificationModel( + id = ID_SYSTEM_BRIDGE_STATUS, + channel = CHANNEL_SETUP_ASSISTANT, + title = getString(R.string.system_bridge_died_notification_title), + text = text, + icon = R.drawable.pro_mode, + showOnLockscreen = true, + onGoing = false, + priority = NotificationCompat.PRIORITY_MAX, + autoCancel = true, + onClickAction = KMNotificationAction.Activity.MainActivity(BaseMainActivity.ACTION_START_SYSTEM_BRIDGE), + bigTextStyle = true, + ) + notificationAdapter.showNotification(model) + } + + private fun showAutoStartNotification(text: String) { + val model = NotificationModel( + id = ID_SYSTEM_BRIDGE_STATUS, + title = getString(R.string.pro_mode_setup_notification_auto_start_system_bridge_title), + text = text, + channel = CHANNEL_SETUP_ASSISTANT, + icon = R.drawable.pro_mode, + priority = NotificationCompat.PRIORITY_MAX, + onGoing = true, + showIndeterminateProgress = true, + showOnLockscreen = false, + ) + + notificationAdapter.showNotification(model) + } + + private fun showAutoStartFailedNotification() { + val model = NotificationModel( + id = ID_SYSTEM_BRIDGE_STATUS, + title = getString(R.string.pro_mode_setup_notification_start_system_bridge_failed_title), + text = getString(R.string.pro_mode_setup_notification_start_system_bridge_failed_text), + channel = CHANNEL_SETUP_ASSISTANT, + icon = R.drawable.pro_mode, + onGoing = false, + showOnLockscreen = false, + autoCancel = true, + priority = NotificationCompat.PRIORITY_MAX, + onClickAction = KMNotificationAction.Activity.MainActivity(BaseMainActivity.ACTION_START_SYSTEM_BRIDGE), + ) + + notificationAdapter.showNotification(model) + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt new file mode 100644 index 0000000000..059c165387 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupAssistantController.kt @@ -0,0 +1,368 @@ +package io.github.sds100.keymapper.base.promode + +import android.app.ActivityManager +import android.os.Build +import android.view.accessibility.AccessibilityEvent +import android.view.accessibility.AccessibilityNodeInfo +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.getSystemService +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.github.sds100.keymapper.base.BaseMainActivity +import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.system.accessibility.BaseAccessibilityService +import io.github.sds100.keymapper.base.system.accessibility.findNodeRecursively +import io.github.sds100.keymapper.base.system.notifications.ManageNotificationsUseCase +import io.github.sds100.keymapper.base.system.notifications.NotificationController +import io.github.sds100.keymapper.base.utils.ui.ResourceProvider +import io.github.sds100.keymapper.common.KeyMapperClassProvider +import io.github.sds100.keymapper.common.notifications.KMNotificationAction +import io.github.sds100.keymapper.common.utils.onFailure +import io.github.sds100.keymapper.common.utils.onSuccess +import io.github.sds100.keymapper.data.Keys +import io.github.sds100.keymapper.data.PreferenceDefaults +import io.github.sds100.keymapper.data.repositories.PreferenceRepository +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState +import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController +import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupStep +import io.github.sds100.keymapper.system.notifications.NotificationChannelModel +import io.github.sds100.keymapper.system.notifications.NotificationModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import timber.log.Timber + +@Suppress("KotlinConstantConditions") +@RequiresApi(Build.VERSION_CODES.Q) +class SystemBridgeSetupAssistantController @AssistedInject constructor( + @Assisted + private val coroutineScope: CoroutineScope, + @Assisted + private val accessibilityService: BaseAccessibilityService, + private val manageNotifications: ManageNotificationsUseCase, + private val setupController: SystemBridgeSetupController, + private val preferenceRepository: PreferenceRepository, + private val systemBridgeConnectionManager: SystemBridgeConnectionManager, + private val keyMapperClassProvider: KeyMapperClassProvider, + resourceProvider: ResourceProvider, +) : ResourceProvider by resourceProvider { + @AssistedFactory + interface Factory { + fun create( + coroutineScope: CoroutineScope, + accessibilityService: BaseAccessibilityService, + ): SystemBridgeSetupAssistantController + } + + companion object { + /** + * The max time to spend searching for an accessibility node. + */ + const val INTERACTION_TIMEOUT = 10000L + + private val PAIRING_CODE_REGEX = Regex("^\\d{6}$") + private val IPV4_REGEX = + Regex("^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$") + } + + private enum class InteractionStep { + // Do not automatically turn on the wireless debugging switch. When the user turns it on, + // Key Mapper will automatically pair. + PAIR_DEVICE, + } + + private val activityManager: ActivityManager = accessibilityService.getSystemService()!! + + private val isInteractive: StateFlow = + preferenceRepository.get(Keys.isProModeInteractiveSetupAssistantEnabled) + .map { it ?: PreferenceDefaults.PRO_MODE_INTERACTIVE_SETUP_ASSISTANT } + .stateIn( + coroutineScope, + SharingStarted.Eagerly, + PreferenceDefaults.PRO_MODE_INTERACTIVE_SETUP_ASSISTANT, + ) + + private var interactionStep: InteractionStep? = null + + /** + * This job will wait for the interaction timeout and then + * ask the user to do the steps manually if it failed to do them automatically. + */ + private var interactionTimeoutJob: Job? = null + + // Store the pairing code so only one request to pair is sent per pairing code. + private var foundPairingCode: String? = null + + fun onServiceConnected() { + createNotificationChannel() + + coroutineScope.launch { + setupController.setupAssistantStep.collect { step -> + if (step == null) { + stopInteracting() + dismissNotification() + } else { + startSetupStep(step) + } + } + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + coroutineScope.launch { + manageNotifications.onNotificationTextInput + .filter { it.intentAction == KMNotificationAction.IntentAction.PAIRING_CODE_REPLY } + .collect { textInput -> + Timber.i("Receive pairing code text input: $textInput") + + val pairingCode: String = textInput.text.trim() + onPairingCodeFound(pairingCode) + } + } + } + } + + private fun createNotificationChannel() { + val notificationChannel = NotificationChannelModel( + id = NotificationController.Companion.CHANNEL_SETUP_ASSISTANT, + name = getString(R.string.pro_mode_setup_assistant_notification_channel), + importance = NotificationManagerCompat.IMPORTANCE_MAX, + ) + manageNotifications.createChannel(notificationChannel) + } + + fun teardown() { + dismissNotification() + stopInteracting() + } + + fun onAccessibilityEvent(event: AccessibilityEvent) { + // Do not do anything if there is no node to find. + if (interactionStep == null) { + return + } + + // Do not do anything if the interactive setup assistant is disabled + if (!isInteractive.value) { + return + } + + if (event.eventType == AccessibilityEvent.TYPE_WINDOWS_CHANGED) { + val step = interactionStep ?: return + val rootNode = accessibilityService.rootInActiveWindow ?: return + + if (rootNode.packageName != "com.android.settings") { + return + } + + doInteractiveStep(step, rootNode) + } + } + + private fun doInteractiveStep(step: InteractionStep, rootNode: AccessibilityNodeInfo) { + when (step) { + InteractionStep.PAIR_DEVICE -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + doPairingInteractiveStep(rootNode) + } + } + } + } + + @RequiresApi(Build.VERSION_CODES.R) + private fun doPairingInteractiveStep(rootNode: AccessibilityNodeInfo) { + val pairingCodeText = findPairingCodeText(rootNode) + + if (pairingCodeText == null) { + clickPairWithCodeButton(rootNode) + } else { + val pairingCode = pairingCodeText.trim() + + coroutineScope.launch { + onPairingCodeFound(pairingCode) + } + } + } + + @RequiresApi(Build.VERSION_CODES.R) + private suspend fun onPairingCodeFound(pairingCode: String) { + // Only pair once per pairing code. + if (foundPairingCode == pairingCode) { + return + } + + foundPairingCode = pairingCode + + Timber.i("Pairing code found $pairingCode. Pairing ADB...") + setupController.pairWirelessAdb(pairingCode).onSuccess { + onPairingSuccess() + }.onFailure { + Timber.e("Failed to pair with wireless ADB: $it") + stopInteracting() + + showNotification( + getString(R.string.pro_mode_setup_notification_invalid_pairing_code_title), + getString(R.string.pro_mode_setup_notification_invalid_pairing_code_text), + actions = listOf(KMNotificationAction.RemoteInput.PairingCode to getString(R.string.pro_mode_setup_notification_action_input_pairing_code)), + ) + } + } + + private suspend fun onPairingSuccess() { + setupController.startWithAdb() + + stopInteracting() + + val isStarted = try { + withTimeout(10000L) { + systemBridgeConnectionManager.connectionState + .filterIsInstance() + .first() + } + + true + } catch (_: TimeoutCancellationException) { + false + } + + if (isStarted) { + getKeyMapperAppTask()?.moveToFront() + } else { + Timber.e("Failed to start system bridge after pairing.") + showNotification( + getString(R.string.pro_mode_setup_notification_start_system_bridge_failed_title), + getString(R.string.pro_mode_setup_notification_start_system_bridge_failed_text), + onClickAction = KMNotificationAction.Activity.MainActivity(BaseMainActivity.ACTION_START_SYSTEM_BRIDGE), + ) + } + } + + private fun clickPairWithCodeButton(rootNode: AccessibilityNodeInfo) { + rootNode + .findNodeRecursively { it.className == "androidx.recyclerview.widget.RecyclerView" } + ?.takeIf { recyclerView -> + // There are many settings screens with RecyclerViews so make sure + // the correct page is showing before clicking. It is not as simple + // as checking the words on the screen due to different languages. + val ipAddressPortText: CharSequence? = + runCatching { + // RecyclerView -> LinearLayout -> RelativeLayout -> TextView + recyclerView.getChild(1).getChild(0).getChild(1) + }.getOrNull()?.text + + val ipText = ipAddressPortText?.split(":")?.firstOrNull() + ipText != null && IPV4_REGEX.matches(ipText) + } + ?.runCatching { getChild(3) } + ?.getOrNull() + ?.performAction(AccessibilityNodeInfo.ACTION_CLICK) + } + + private fun showNotification( + title: String, + text: String, + onClickAction: KMNotificationAction? = null, + actions: List> = emptyList(), + ) { + val notification = NotificationModel( + // Use the same notification id for all so they overwrite each other. + id = NotificationController.Companion.ID_SETUP_ASSISTANT, + channel = NotificationController.Companion.CHANNEL_SETUP_ASSISTANT, + title = title, + text = text, + icon = R.drawable.pro_mode, + onGoing = false, + showOnLockscreen = false, + autoCancel = true, + onClickAction = onClickAction, + bigTextStyle = true, + // Must not be silent so it is shown as a heads up notification + silent = false, + actions = actions, + ) + manageNotifications.show(notification) + } + + private fun dismissNotification() { + manageNotifications.dismiss(NotificationController.Companion.ID_SETUP_ASSISTANT) + } + + private fun findPairingCodeText(rootNode: AccessibilityNodeInfo): String? { + return rootNode.findNodeRecursively { + it.text != null && PAIRING_CODE_REGEX.matches(it.text) + }?.text?.toString() + } + + private fun startSetupStep(step: SystemBridgeSetupStep) { + Timber.i("Starting setup assistant step: $step") + when (step) { + SystemBridgeSetupStep.DEVELOPER_OPTIONS -> { + showNotification( + getString(R.string.pro_mode_setup_notification_tap_build_number_title), + getString(R.string.pro_mode_setup_notification_tap_build_number_text), + ) + } + + SystemBridgeSetupStep.ADB_PAIRING -> { + showNotification( + getString(R.string.pro_mode_setup_notification_pairing_title), + getString(R.string.pro_mode_setup_notification_pairing_text), + ) + + interactionStep = InteractionStep.PAIR_DEVICE + } + + else -> return // Do not start interaction timeout job + } + + startInteractionTimeoutJob() + } + + private fun startInteractionTimeoutJob() { + interactionTimeoutJob?.cancel() + interactionTimeoutJob = coroutineScope.launch { + delay(INTERACTION_TIMEOUT) + + if (interactionStep == InteractionStep.PAIR_DEVICE) { + Timber.i("Interaction timed out. Asking user to input pairing code manually.") + + showNotification( + title = getString(R.string.pro_mode_setup_notification_pairing_button_not_found_title), + text = getString(R.string.pro_mode_setup_notification_pairing_button_not_found_text), + actions = listOf(KMNotificationAction.RemoteInput.PairingCode to getString(R.string.pro_mode_setup_notification_action_input_pairing_code)), + ) + + // Give the user 30 seconds to input the pairing code and then dismiss the notification. + delay(30000) + } + + dismissNotification() + + interactionStep = null + } + } + + private fun stopInteracting() { + interactionStep = null + interactionTimeoutJob?.cancel() + interactionTimeoutJob = null + } + + private fun getKeyMapperAppTask(): ActivityManager.AppTask? { + val task = activityManager.appTasks + .firstOrNull { it.taskInfo.topActivity?.className == keyMapperClassProvider.getMainActivity().name } + return task + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt new file mode 100644 index 0000000000..11a2ff26a0 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/promode/SystemBridgeSetupUseCase.kt @@ -0,0 +1,252 @@ +package io.github.sds100.keymapper.base.promode + +import android.os.Build +import androidx.annotation.RequiresApi +import dagger.hilt.android.scopes.ViewModelScoped +import io.github.sds100.keymapper.common.utils.firstBlocking +import io.github.sds100.keymapper.data.Keys +import io.github.sds100.keymapper.data.PreferenceDefaults +import io.github.sds100.keymapper.data.repositories.PreferenceRepository +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState +import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController +import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupStep +import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceAdapter +import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceState +import io.github.sds100.keymapper.system.network.NetworkAdapter +import io.github.sds100.keymapper.system.permissions.Permission +import io.github.sds100.keymapper.system.permissions.PermissionAdapter +import io.github.sds100.keymapper.system.root.SuAdapter +import io.github.sds100.keymapper.system.shizuku.ShizukuAdapter +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +@RequiresApi(Build.VERSION_CODES.Q) +@ViewModelScoped +class SystemBridgeSetupUseCaseImpl @Inject constructor( + private val preferences: PreferenceRepository, + private val suAdapter: SuAdapter, + private val systemBridgeSetupController: SystemBridgeSetupController, + private val systemBridgeConnectionManager: SystemBridgeConnectionManager, + private val shizukuAdapter: ShizukuAdapter, + private val permissionAdapter: PermissionAdapter, + private val accessibilityServiceAdapter: AccessibilityServiceAdapter, + private val networkAdapter: NetworkAdapter, +) : SystemBridgeSetupUseCase { + override val isWarningUnderstood: Flow = + preferences.get(Keys.isProModeWarningUnderstood).map { it ?: false } + + private val isAdbAutoStartAllowed: Flow = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + combine( + permissionAdapter.isGrantedFlow(Permission.WRITE_SECURE_SETTINGS), + networkAdapter.isWifiConnected, + ) { isWriteSecureSettingsGranted, isWifiConnected -> + isWriteSecureSettingsGranted && isWifiConnected && systemBridgeSetupController.isAdbPaired() + }.flowOn(Dispatchers.IO) + } else { + flowOf(false) + } + + override fun onUnderstoodWarning() { + preferences.set(Keys.isProModeWarningUnderstood, true) + } + + override val isSetupAssistantEnabled: Flow = + preferences.get(Keys.isProModeInteractiveSetupAssistantEnabled).map { + it ?: PreferenceDefaults.PRO_MODE_INTERACTIVE_SETUP_ASSISTANT + } + + override fun toggleSetupAssistant() { + preferences.update(Keys.isProModeInteractiveSetupAssistantEnabled) { + if (it == null) { + !PreferenceDefaults.PRO_MODE_INTERACTIVE_SETUP_ASSISTANT + } else { + !it + } + } + } + + override val isSystemBridgeConnected: Flow = + systemBridgeConnectionManager.connectionState + .map { it is SystemBridgeConnectionState.Connected } + + override val isNotificationPermissionGranted: Flow = + permissionAdapter.isGrantedFlow(Permission.POST_NOTIFICATIONS) + + @OptIn(ExperimentalCoroutinesApi::class) + @RequiresApi(Build.VERSION_CODES.R) + override val nextSetupStep: Flow = + isSystemBridgeConnected.flatMapLatest { isConnected -> + if (isConnected) { + flowOf(SystemBridgeSetupStep.STARTED) + } else { + isAdbAutoStartAllowed.flatMapLatest { isAdbAutoStartAllowed -> + if (isAdbAutoStartAllowed) { + flowOf(SystemBridgeSetupStep.START_SERVICE) + } else { + combine( + accessibilityServiceAdapter.state, + isNotificationPermissionGranted, + systemBridgeSetupController.isDeveloperOptionsEnabled, + networkAdapter.isWifiConnected, + systemBridgeSetupController.isWirelessDebuggingEnabled, + ::getNextStep, + ) + } + } + } + } + + override val isRootGranted: Flow = suAdapter.isRootGranted + + override val shizukuSetupState: Flow = combine( + shizukuAdapter.isInstalled, + shizukuAdapter.isStarted, + permissionAdapter.isGrantedFlow(Permission.SHIZUKU), + ) { isInstalled, isStarted, isPermissionGranted -> + when { + isPermissionGranted -> ShizukuSetupState.PERMISSION_GRANTED + isStarted -> ShizukuSetupState.STARTED + isInstalled -> ShizukuSetupState.INSTALLED + else -> ShizukuSetupState.NOT_FOUND + } + } + + override fun openShizukuApp() { + shizukuAdapter.openShizukuApp() + } + + override fun requestShizukuPermission() { + permissionAdapter.request(Permission.SHIZUKU) + } + + override fun requestNotificationPermission() { + permissionAdapter.request(Permission.POST_NOTIFICATIONS) + } + + override fun stopSystemBridge() { + systemBridgeConnectionManager.stopSystemBridge() + } + + override fun enableAccessibilityService() { + accessibilityServiceAdapter.start() + } + + override fun enableDeveloperOptions() { + systemBridgeSetupController.enableDeveloperOptions() + } + + override fun connectWifiNetwork() { + networkAdapter.connectWifiNetwork() + } + + override fun enableWirelessDebugging() { + systemBridgeSetupController.enableWirelessDebugging() + } + + @RequiresApi(Build.VERSION_CODES.R) + override fun pairWirelessAdb() { + systemBridgeSetupController.launchPairingAssistant() + } + + override fun startSystemBridgeWithRoot() { + preferences.set(Keys.isSystemBridgeEmergencyKilled, false) + systemBridgeSetupController.startWithRoot() + } + + override fun startSystemBridgeWithShizuku() { + preferences.set(Keys.isSystemBridgeEmergencyKilled, false) + systemBridgeSetupController.startWithShizuku() + } + + override suspend fun startSystemBridgeWithAdb() { + preferences.set(Keys.isSystemBridgeEmergencyKilled, false) + if (isAdbAutoStartAllowed.first()) { + systemBridgeSetupController.autoStartWithAdb() + } else { + systemBridgeSetupController.startWithAdb() + } + } + + override fun isInfoDismissed(): Boolean { + return preferences.get(Keys.isProModeInfoDismissed).map { it ?: false }.firstBlocking() + } + + override fun dismissInfo() { + preferences.set(Keys.isProModeInfoDismissed, true) + } + + override val isAutoStartBootEnabled: Flow = + preferences.get(Keys.isProModeAutoStartBootEnabled) + .map { it ?: PreferenceDefaults.PRO_MODE_AUTOSTART_BOOT } + + override fun toggleAutoStartBoot() { + preferences.update(Keys.isProModeAutoStartBootEnabled) { + !(it ?: PreferenceDefaults.PRO_MODE_AUTOSTART_BOOT) + } + } + + @RequiresApi(Build.VERSION_CODES.R) + private fun getNextStep( + accessibilityServiceState: AccessibilityServiceState, + isNotificationPermissionGranted: Boolean, + isDeveloperOptionsEnabled: Boolean, + isWifiConnected: Boolean, + isWirelessDebuggingEnabled: Boolean, + ): SystemBridgeSetupStep { + return when { + accessibilityServiceState != AccessibilityServiceState.ENABLED -> SystemBridgeSetupStep.ACCESSIBILITY_SERVICE + !isNotificationPermissionGranted -> SystemBridgeSetupStep.NOTIFICATION_PERMISSION + !isDeveloperOptionsEnabled -> SystemBridgeSetupStep.DEVELOPER_OPTIONS + !isWifiConnected -> SystemBridgeSetupStep.WIFI_NETWORK + !isWirelessDebuggingEnabled -> SystemBridgeSetupStep.WIRELESS_DEBUGGING + isWirelessDebuggingEnabled -> SystemBridgeSetupStep.ADB_PAIRING + else -> SystemBridgeSetupStep.START_SERVICE + } + } +} + +interface SystemBridgeSetupUseCase { + val isWarningUnderstood: Flow + fun onUnderstoodWarning() + + fun isInfoDismissed(): Boolean + fun dismissInfo() + + val isAutoStartBootEnabled: Flow + fun toggleAutoStartBoot() + + val isSetupAssistantEnabled: Flow + fun toggleSetupAssistant() + + val isSystemBridgeConnected: Flow + val nextSetupStep: Flow + + val isRootGranted: Flow + + val shizukuSetupState: Flow + fun openShizukuApp() + fun requestShizukuPermission() + + val isNotificationPermissionGranted: Flow + fun requestNotificationPermission() + + fun stopSystemBridge() + fun enableAccessibilityService() + fun enableDeveloperOptions() + fun connectWifiNetwork() + fun enableWirelessDebugging() + fun pairWirelessAdb() + fun startSystemBridgeWithRoot() + fun startSystemBridgeWithShizuku() + suspend fun startSystemBridgeWithAdb() +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsController.kt b/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsController.kt deleted file mode 100644 index 05331b8032..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsController.kt +++ /dev/null @@ -1,130 +0,0 @@ -package io.github.sds100.keymapper.base.reroutekeyevents - -import android.view.KeyEvent -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import io.github.sds100.keymapper.base.system.inputmethod.ImeInputEventInjector -import io.github.sds100.keymapper.common.utils.InputEventType -import io.github.sds100.keymapper.system.devices.InputDeviceInfo -import io.github.sds100.keymapper.system.inputevents.MyKeyEvent -import io.github.sds100.keymapper.system.inputmethod.InputKeyModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch - -/** - * This is used for the feature created in issue #618 to fix the device IDs of key events - * on Android 11. There was a bug in the system where enabling an accessibility service - * would reset the device ID of key events to -1. - */ -class RerouteKeyEventsController @AssistedInject constructor( - @Assisted - private val coroutineScope: CoroutineScope, - @Assisted - private val keyMapperImeMessenger: ImeInputEventInjector, - private val useCaseFactory: RerouteKeyEventsUseCaseImpl.Factory, -) { - @AssistedFactory - interface Factory { - fun create( - coroutineScope: CoroutineScope, - keyMapperImeMessenger: ImeInputEventInjector, - ): RerouteKeyEventsController - } - - private val useCase = useCaseFactory.create(keyMapperImeMessenger) - - /** - * The job of the key that should be repeating. This should be a down key event for the last - * key that has been pressed down. - * The old job should be cancelled whenever the key has been released - * or a new key has been pressed down - */ - private var repeatJob: Job? = null - - fun onKeyEvent(event: MyKeyEvent): Boolean { - if (!useCase.shouldRerouteKeyEvent(event.device?.descriptor)) { - return false - } - - return when (event.action) { - KeyEvent.ACTION_DOWN -> onKeyDown( - event.keyCode, - event.device, - event.metaState, - event.scanCode, - ) - - KeyEvent.ACTION_UP -> onKeyUp( - event.keyCode, - event.device, - event.metaState, - event.scanCode, - ) - - else -> false - } - } - - /** - * @return whether to consume the key event. - */ - private fun onKeyDown( - keyCode: Int, - device: InputDeviceInfo?, - metaState: Int, - scanCode: Int = 0, - ): Boolean { - val inputKeyModel = InputKeyModel( - keyCode = keyCode, - inputType = InputEventType.DOWN, - metaState = metaState, - deviceId = device?.id ?: 0, - scanCode = scanCode, - repeat = 0, - ) - - useCase.inputKeyEvent(inputKeyModel) - - repeatJob?.cancel() - - repeatJob = coroutineScope.launch { - delay(400) - - var repeatCount = 1 - - while (isActive) { - useCase.inputKeyEvent(inputKeyModel.copy(repeat = repeatCount)) - delay(50) - repeatCount++ - } - } - - return true - } - - private fun onKeyUp( - keyCode: Int, - device: InputDeviceInfo?, - metaState: Int, - scanCode: Int = 0, - ): Boolean { - repeatJob?.cancel() - - val inputKeyModel = InputKeyModel( - keyCode = keyCode, - inputType = InputEventType.UP, - metaState = metaState, - deviceId = device?.id ?: 0, - scanCode = scanCode, - repeat = 0, - ) - - useCase.inputKeyEvent(inputKeyModel) - - return true - } -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsUseCase.kt deleted file mode 100644 index 83e7af7e95..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/reroutekeyevents/RerouteKeyEventsUseCase.kt +++ /dev/null @@ -1,76 +0,0 @@ -package io.github.sds100.keymapper.base.reroutekeyevents - -import android.os.Build -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import io.github.sds100.keymapper.base.system.inputmethod.ImeInputEventInjector -import io.github.sds100.keymapper.base.system.inputmethod.KeyMapperImeHelper -import io.github.sds100.keymapper.common.BuildConfigProvider -import io.github.sds100.keymapper.common.utils.firstBlocking -import io.github.sds100.keymapper.data.Keys -import io.github.sds100.keymapper.data.repositories.PreferenceRepository -import io.github.sds100.keymapper.system.inputmethod.InputKeyModel -import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.runBlocking - -/** - * This is used for the feature created in issue #618 to fix the device IDs of key events - * on Android 11. There was a bug in the system where enabling an accessibility service - * would reset the device ID of key events to -1. - */ -class RerouteKeyEventsUseCaseImpl @AssistedInject constructor( - @Assisted - private val keyMapperImeMessenger: ImeInputEventInjector, - private val inputMethodAdapter: InputMethodAdapter, - private val preferenceRepository: PreferenceRepository, - private val buildConfigProvider: BuildConfigProvider, -) : RerouteKeyEventsUseCase { - - @AssistedFactory - interface Factory { - fun create( - keyMapperImeMessenger: ImeInputEventInjector, - ): RerouteKeyEventsUseCaseImpl - } - - private val rerouteKeyEvents = - preferenceRepository.get(Keys.rerouteKeyEvents).map { it ?: false } - - private val devicesToRerouteKeyEvents = - preferenceRepository.get(Keys.devicesToRerouteKeyEvents).map { it ?: emptyList() } - - private val imeHelper by lazy { - KeyMapperImeHelper( - inputMethodAdapter, - buildConfigProvider.packageName, - ) - } - - override fun shouldRerouteKeyEvent(descriptor: String?): Boolean { - if (Build.VERSION.SDK_INT != Build.VERSION_CODES.R) { - return false - } - - return rerouteKeyEvents.firstBlocking() && - imeHelper.isCompatibleImeChosen() && - ( - descriptor != null && - devicesToRerouteKeyEvents.firstBlocking() - .contains(descriptor) - ) - } - - override fun inputKeyEvent(keyModel: InputKeyModel) { - // It is safe to run the ime injector on the main thread. - runBlocking { - keyMapperImeMessenger.inputKeyEvent(keyModel) - } - } -} - -interface RerouteKeyEventsUseCase { - fun shouldRerouteKeyEvent(descriptor: String?): Boolean - fun inputKeyEvent(keyModel: InputKeyModel) -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/Android11BugWorkaroundSettingsFragment.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/Android11BugWorkaroundSettingsFragment.kt deleted file mode 100644 index f82b8f2e84..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/Android11BugWorkaroundSettingsFragment.kt +++ /dev/null @@ -1,192 +0,0 @@ -package io.github.sds100.keymapper.base.settings - -import android.os.Bundle -import android.view.View -import androidx.lifecycle.Lifecycle -import androidx.preference.Preference -import androidx.preference.SwitchPreference -import androidx.preference.isEmpty -import dagger.hilt.android.AndroidEntryPoint -import io.github.sds100.keymapper.base.R -import io.github.sds100.keymapper.base.utils.ui.ChooseAppStoreModel -import io.github.sds100.keymapper.base.utils.ui.DialogModel -import io.github.sds100.keymapper.base.utils.ui.drawable -import io.github.sds100.keymapper.base.utils.ui.launchRepeatOnLifecycle -import io.github.sds100.keymapper.base.utils.ui.showDialog -import io.github.sds100.keymapper.base.utils.ui.str -import io.github.sds100.keymapper.base.utils.ui.viewLifecycleScope -import io.github.sds100.keymapper.data.Keys -import io.github.sds100.keymapper.system.leanback.LeanbackUtils -import io.github.sds100.keymapper.system.url.UrlUtils -import kotlinx.coroutines.flow.collectLatest - -@AndroidEntryPoint -class Android11BugWorkaroundSettingsFragment : BaseSettingsFragment() { - - companion object { - private const val KEY_ENABLE_COMPATIBLE_IME = "pref_key_enable_compatible_ime" - private const val KEY_CHOSE_COMPATIBLE_IME = "pref_key_chose_compatible_ime" - } - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - preferenceManager.preferenceDataStore = viewModel.sharedPrefsDataStoreWrapper - addPreferencesFromResource(R.xml.preferences_empty) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - viewLifecycleScope.launchWhenResumed { - if (preferenceScreen.isEmpty()) { - populatePreferenceScreen() - } - } - } - - private fun populatePreferenceScreen() = preferenceScreen.apply { - val isTvDevice = LeanbackUtils.isTvDevice(requireContext()) - - SwitchPreference(requireContext()).apply { - key = Keys.rerouteKeyEvents.name - setDefaultValue(false) - - setTitle(R.string.title_pref_reroute_keyevents) - setSummary(R.string.summary_pref_reroute_keyevents) - isSingleLineTitle = false - - addPreference(this) - } - - Preference(requireContext()).apply { - setTitle(R.string.title_pref_devices_to_reroute_keyevents_guide) - setOnPreferenceClickListener { - UrlUtils.openUrl( - requireContext(), - str(R.string.url_android_11_bug_reset_id_work_around_setting_guide), - ) - - true - } - - addPreference(this) - } - - Preference(requireContext()).apply { - if (isTvDevice) { - setTitle(R.string.title_pref_devices_to_reroute_keyevents_install_leanback_keyboard) - } else { - setTitle(R.string.title_pref_devices_to_reroute_keyevents_install_gui_keyboard) - } - - isSingleLineTitle = false - - setOnPreferenceClickListener { - viewLifecycleScope.launchWhenResumed { - if (isTvDevice) { - val chooseAppStoreDialog = DialogModel.ChooseAppStore( - title = getString(R.string.dialog_title_choose_download_leanback_keyboard), - message = getString(R.string.dialog_message_choose_download_leanback_keyboard), - model = ChooseAppStoreModel( - githubLink = getString(R.string.url_github_keymapper_leanback_keyboard), - ), - negativeButtonText = str(R.string.neg_cancel), - ) - - viewModel.showDialog("download_leanback_ime", chooseAppStoreDialog) - } else { - val chooseAppStoreDialog = DialogModel.ChooseAppStore( - title = getString(R.string.dialog_title_choose_download_gui_keyboard), - message = getString(R.string.dialog_message_choose_download_gui_keyboard), - model = ChooseAppStoreModel( - playStoreLink = getString(R.string.url_play_store_keymapper_gui_keyboard), - fdroidLink = getString(R.string.url_fdroid_keymapper_gui_keyboard), - githubLink = getString(R.string.url_github_keymapper_gui_keyboard), - ), - negativeButtonText = str(R.string.neg_cancel), - ) - - viewModel.showDialog("download_gui_keyboard", chooseAppStoreDialog) - } - } - - true - } - - addPreference(this) - } - - Preference(requireContext()).apply { - key = KEY_ENABLE_COMPATIBLE_IME - - if (isTvDevice) { - setTitle(R.string.title_pref_devices_to_reroute_keyevents_enable_ime_leanback) - } else { - setTitle(R.string.title_pref_devices_to_reroute_keyevents_enable_ime_gui) - } - - isSingleLineTitle = false - - setOnPreferenceClickListener { - viewModel.onEnableCompatibleImeClick() - - true - } - - addPreference(this) - - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.isCompatibleImeEnabled.collectLatest { isCompatibleImeEnabled -> - icon = if (isCompatibleImeEnabled) { - drawable(R.drawable.ic_outline_check_circle_outline_24) - } else { - drawable(R.drawable.ic_baseline_error_outline_24) - } - } - } - } - - Preference(requireContext()).apply { - key = KEY_CHOSE_COMPATIBLE_IME - setTitle(R.string.title_pref_devices_to_reroute_keyevents_choose_ime) - isSingleLineTitle = false - setOnPreferenceClickListener { - viewModel.onChooseCompatibleImeClick() - - true - } - - addPreference(this) - - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.isCompatibleImeChosen.collectLatest { isCompatibleImeChosen -> - icon = if (isCompatibleImeChosen) { - drawable(R.drawable.ic_outline_check_circle_outline_24) - } else { - drawable(R.drawable.ic_baseline_error_outline_24) - } - } - } - } - - addPreference( - SettingsUtils.createChooseDevicesPreference( - requireContext(), - viewModel, - Keys.devicesToRerouteKeyEvents, - R.string.title_pref_devices_to_reroute_keyevents_choose_devices, - ), - ) - - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.rerouteKeyEvents.collectLatest { enabled -> - for (i in 0 until preferenceCount) { - getPreference(i).apply { - if (this.key != Keys.rerouteKeyEvents.name) { - this.isVisible = enabled - } - } - } - } - } - } -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/AutomaticChangeImeSettingsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/AutomaticChangeImeSettingsScreen.kt new file mode 100644 index 0000000000..f9c3af7d16 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/AutomaticChangeImeSettingsScreen.kt @@ -0,0 +1,220 @@ +package io.github.sds100.keymapper.base.settings + +import android.os.Build +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.displayCutoutPadding +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material.icons.outlined.Notifications +import androidx.compose.material.icons.rounded.Devices +import androidx.compose.material.icons.rounded.Notifications +import androidx.compose.material.icons.rounded.SwapHoriz +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import io.github.sds100.keymapper.base.utils.ui.compose.OptionPageButton +import io.github.sds100.keymapper.base.utils.ui.compose.OptionsHeaderRow +import io.github.sds100.keymapper.base.utils.ui.compose.SwitchPreferenceCompose + +@Composable +fun AutomaticChangeImeSettingsScreen(modifier: Modifier = Modifier, viewModel: SettingsViewModel) { + val state by viewModel.automaticChangeImeSettingsState.collectAsStateWithLifecycle() + val snackbarHostState = SnackbarHostState() + + AutomaticChangeImeSettingsScreen( + modifier, + onBackClick = viewModel::onBackClick, + snackbarHostState = snackbarHostState, + ) { + Content( + state = state, + onShowToastWhenAutoChangingImeToggled = viewModel::onShowToastWhenAutoChangingImeToggled, + onChangeImeOnInputFocusToggled = viewModel::onChangeImeOnInputFocusToggled, + onChangeImeOnDeviceConnectToggled = viewModel::onChangeImeOnDeviceConnectToggled, + onDevicesThatChangeImeClick = viewModel::onDevicesThatChangeImeClick, + onToggleKeyboardOnToggleKeymapsToggled = viewModel::onToggleKeyboardOnToggleKeymapsToggled, + onShowToggleKeyboardNotificationClick = viewModel::onShowToggleKeyboardNotificationClick, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun AutomaticChangeImeSettingsScreen( + modifier: Modifier = Modifier, + onBackClick: () -> Unit = {}, + snackbarHostState: SnackbarHostState = SnackbarHostState(), + content: @Composable () -> Unit, +) { + Scaffold( + modifier = modifier.displayCutoutPadding(), + snackbarHost = { SnackbarHost(snackbarHostState) }, + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.title_pref_automatically_change_ime)) }, + ) + }, + bottomBar = { + BottomAppBar { + IconButton(onClick = onBackClick) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = stringResource(R.string.action_go_back), + ) + } + } + }, + ) { innerPadding -> + val layoutDirection = LocalLayoutDirection.current + val startPadding = innerPadding.calculateStartPadding(layoutDirection) + val endPadding = innerPadding.calculateEndPadding(layoutDirection) + + Surface( + modifier = Modifier + .fillMaxSize() + .padding( + top = innerPadding.calculateTopPadding(), + bottom = innerPadding.calculateBottomPadding(), + start = startPadding, + end = endPadding, + ), + ) { + content() + } + } +} + +@Composable +private fun Content( + modifier: Modifier = Modifier, + state: AutomaticChangeImeSettingsState, + onShowToastWhenAutoChangingImeToggled: (Boolean) -> Unit = { }, + onChangeImeOnInputFocusToggled: (Boolean) -> Unit = { }, + onChangeImeOnDeviceConnectToggled: (Boolean) -> Unit = { }, + onDevicesThatChangeImeClick: () -> Unit = { }, + onToggleKeyboardOnToggleKeymapsToggled: (Boolean) -> Unit = { }, + onShowToggleKeyboardNotificationClick: () -> Unit = { }, +) { + Column( + modifier + .verticalScroll(rememberScrollState()) + .padding(horizontal = 16.dp), + ) { + Spacer(modifier = Modifier.height(8.dp)) + + SwitchPreferenceCompose( + title = stringResource(R.string.title_pref_auto_change_ime_on_input_focus), + text = stringResource(R.string.summary_pref_auto_change_ime_on_input_focus), + icon = Icons.Rounded.SwapHoriz, + isChecked = state.changeImeOnInputFocus, + onCheckedChange = onChangeImeOnInputFocusToggled, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + SwitchPreferenceCompose( + title = stringResource(R.string.title_pref_auto_change_ime_on_connection), + text = stringResource(R.string.summary_pref_auto_change_ime_on_connection), + icon = Icons.Rounded.SwapHoriz, + isChecked = state.changeImeOnDeviceConnect, + onCheckedChange = onChangeImeOnDeviceConnectToggled, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OptionPageButton( + title = stringResource(R.string.title_pref_automatically_change_ime_choose_devices), + text = stringResource(R.string.summary_pref_automatically_change_ime_choose_devices), + icon = Icons.Rounded.Devices, + onClick = onDevicesThatChangeImeClick, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + SwitchPreferenceCompose( + title = stringResource(R.string.title_pref_toggle_keyboard_on_toggle_keymaps), + text = stringResource(R.string.summary_pref_toggle_keyboard_on_toggle_keymaps), + icon = Icons.Rounded.SwapHoriz, + isChecked = state.toggleKeyboardOnToggleKeymaps, + onCheckedChange = onToggleKeyboardOnToggleKeymapsToggled, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OptionsHeaderRow( + modifier = Modifier.fillMaxWidth(), + icon = Icons.Outlined.Notifications, + text = stringResource(R.string.settings_section_notifications), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + SwitchPreferenceCompose( + title = stringResource(R.string.title_pref_show_toast_when_auto_changing_ime), + text = stringResource(R.string.summary_pref_show_toast_when_auto_changing_ime), + icon = Icons.Rounded.Notifications, + isChecked = state.showToastWhenAutoChangingIme, + onCheckedChange = onShowToastWhenAutoChangingImeToggled, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + OptionPageButton( + title = stringResource(R.string.title_pref_show_toggle_keyboard_notification), + text = stringResource(R.string.summary_pref_show_toggle_keyboard_notification), + icon = Icons.Rounded.Notifications, + onClick = onShowToggleKeyboardNotificationClick, + ) + } else { + // For older Android versions, this would be a switch but since we're targeting newer versions + // we'll just show the notification settings button + OptionPageButton( + title = stringResource(R.string.title_pref_show_toggle_keyboard_notification), + text = stringResource(R.string.summary_pref_show_toggle_keyboard_notification), + icon = Icons.Rounded.Notifications, + onClick = onShowToggleKeyboardNotificationClick, + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + } +} + +@Preview +@Composable +private fun Preview() { + KeyMapperTheme { + AutomaticChangeImeSettingsScreen(modifier = Modifier.fillMaxSize(), onBackClick = {}) { + Content( + state = AutomaticChangeImeSettingsState(), + ) + } + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/AutomaticallyChangeImeSettings.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/AutomaticallyChangeImeSettings.kt deleted file mode 100644 index a52487e9f3..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/AutomaticallyChangeImeSettings.kt +++ /dev/null @@ -1,136 +0,0 @@ -package io.github.sds100.keymapper.base.settings - -import android.os.Build -import android.os.Bundle -import android.view.View -import androidx.annotation.RequiresApi -import androidx.preference.Preference -import androidx.preference.SwitchPreferenceCompat -import androidx.preference.isEmpty -import io.github.sds100.keymapper.base.R -import io.github.sds100.keymapper.base.system.notifications.NotificationController -import io.github.sds100.keymapper.base.utils.ui.viewLifecycleScope -import io.github.sds100.keymapper.data.Keys -import io.github.sds100.keymapper.data.PreferenceDefaults -import io.github.sds100.keymapper.system.notifications.NotificationUtils - -class AutomaticallyChangeImeSettings : BaseSettingsFragment() { - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - preferenceManager.preferenceDataStore = viewModel.sharedPrefsDataStoreWrapper - addPreferencesFromResource(R.xml.preferences_empty) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - viewLifecycleScope.launchWhenResumed { - if (preferenceScreen.isEmpty()) { - populatePreferenceScreen() - } - } - } - - private fun populatePreferenceScreen() = preferenceScreen.apply { - // show on-screen messages when changing keyboards - SwitchPreferenceCompat(requireContext()).apply { - key = Keys.showToastWhenAutoChangingIme.name - - setDefaultValue(PreferenceDefaults.SHOW_TOAST_WHEN_AUTO_CHANGE_IME) - isSingleLineTitle = false - setTitle(R.string.title_pref_show_toast_when_auto_changing_ime) - - addPreference(this) - } - - // automatically change ime on input focus - SwitchPreferenceCompat(requireContext()).apply { - key = Keys.changeImeOnInputFocus.name - - setDefaultValue(PreferenceDefaults.CHANGE_IME_ON_INPUT_FOCUS) - isSingleLineTitle = false - setTitle(R.string.title_pref_auto_change_ime_on_input_focus) - setSummary(R.string.summary_pref_auto_change_ime_on_input_focus) - - addPreference(this) - } - - // automatically change the keyboard when a bluetooth device (dis)connects - SwitchPreferenceCompat(requireContext()).apply { - key = Keys.changeImeOnDeviceConnect.name - setDefaultValue(false) - - isSingleLineTitle = false - setTitle(R.string.title_pref_auto_change_ime_on_connection) - setSummary(R.string.summary_pref_auto_change_ime_on_connection) - - addPreference(this) - } - - addPreference( - SettingsUtils.createChooseDevicesPreference( - requireContext(), - viewModel, - Keys.devicesThatChangeIme, - ), - ) - - // toggle keyboard when toggling key maps - SwitchPreferenceCompat(requireContext()).apply { - key = Keys.toggleKeyboardOnToggleKeymaps.name - setDefaultValue(false) - - isSingleLineTitle = false - setTitle(R.string.title_pref_toggle_keyboard_on_toggle_keymaps) - setSummary(R.string.summary_pref_toggle_keyboard_on_toggle_keymaps) - - addPreference(this) - } - - // toggle keyboard notification - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // show a preference linking to the notification management screen - Preference(requireContext()).apply { - key = Keys.showToggleKeyboardNotification.name - - setTitle(R.string.title_pref_show_toggle_keyboard_notification) - isSingleLineTitle = false - setSummary(R.string.summary_pref_show_toggle_keyboard_notification) - - setOnPreferenceClickListener { - onToggleKeyboardNotificationClick() - - true - } - - addPreference(this) - } - } else { - SwitchPreferenceCompat(requireContext()).apply { - key = Keys.showToggleKeyboardNotification.name - setDefaultValue(true) - - setTitle(R.string.title_pref_show_toggle_keyboard_notification) - isSingleLineTitle = false - setSummary(R.string.summary_pref_show_toggle_keyboard_notification) - - addPreference(this) - } - } - } - - @RequiresApi(Build.VERSION_CODES.O) - private fun onToggleKeyboardNotificationClick() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && - !viewModel.isNotificationPermissionGranted() - ) { - viewModel.requestNotificationsPermission() - return - } - - NotificationUtils.openChannelSettings( - ctx = requireContext(), - channelId = NotificationController.CHANNEL_TOGGLE_KEYBOARD, - ) - } -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/BaseSettingsFragment.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/BaseSettingsFragment.kt deleted file mode 100644 index 8bdfdbcbec..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/BaseSettingsFragment.kt +++ /dev/null @@ -1,60 +0,0 @@ -package io.github.sds100.keymapper.base.settings - -import android.os.Bundle -import android.view.View -import androidx.activity.addCallback -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.updatePadding -import androidx.fragment.app.viewModels -import androidx.navigation.fragment.findNavController -import androidx.preference.PreferenceFragmentCompat -import com.google.android.material.bottomappbar.BottomAppBar -import dagger.hilt.android.AndroidEntryPoint -import io.github.sds100.keymapper.base.R -import io.github.sds100.keymapper.base.utils.ui.str -import io.github.sds100.keymapper.system.url.UrlUtils - -@AndroidEntryPoint -abstract class BaseSettingsFragment : PreferenceFragmentCompat() { - - protected val viewModel: SettingsViewModel by viewModels() - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets -> - val insets = - insets.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout() or WindowInsetsCompat.Type.ime()) - v.updatePadding(insets.left, insets.top, insets.right, insets.bottom) - WindowInsetsCompat.CONSUMED - } - - requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) { - onBackPressed() - } - - view.findViewById(R.id.appBar).apply { - replaceMenu(R.menu.menu_settings) - - setNavigationOnClickListener { - onBackPressed() - } - - setOnMenuItemClickListener { - when (it.itemId) { - R.id.action_help -> { - UrlUtils.openUrl(requireContext(), str(R.string.url_settings_guide)) - true - } - - else -> false - } - } - } - } - - private fun onBackPressed() { - findNavController().navigateUp() - } -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt index bc67b9be08..eb274210bc 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/ConfigSettingsUseCase.kt @@ -5,6 +5,7 @@ import io.github.sds100.keymapper.base.actions.sound.SoundFileInfo import io.github.sds100.keymapper.base.actions.sound.SoundsManager import io.github.sds100.keymapper.base.system.inputmethod.KeyMapperImeHelper import io.github.sds100.keymapper.common.BuildConfigProvider +import io.github.sds100.keymapper.common.utils.InputDeviceInfo import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.State import io.github.sds100.keymapper.data.Keys @@ -12,9 +13,9 @@ import io.github.sds100.keymapper.data.PreferenceDefaults import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.system.apps.PackageManagerAdapter import io.github.sds100.keymapper.system.devices.DevicesAdapter -import io.github.sds100.keymapper.system.devices.InputDeviceInfo import io.github.sds100.keymapper.system.inputmethod.ImeInfo import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter +import io.github.sds100.keymapper.system.notifications.NotificationAdapter import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.system.permissions.PermissionAdapter import io.github.sds100.keymapper.system.root.SuAdapter @@ -37,6 +38,7 @@ class ConfigSettingsUseCaseImpl @Inject constructor( private val shizukuAdapter: ShizukuAdapter, private val devicesAdapter: DevicesAdapter, private val buildConfigProvider: BuildConfigProvider, + private val notificationAdapter: NotificationAdapter, ) : ConfigSettingsUseCase { private val imeHelper by lazy { @@ -46,7 +48,12 @@ class ConfigSettingsUseCaseImpl @Inject constructor( ) } - override val isRootGranted: Flow = suAdapter.isGranted + override val theme: Flow = + preferences.get(Keys.darkTheme).map { it ?: PreferenceDefaults.DARK_THEME }.map { value -> + Theme.entries.single { it.value == value.toInt() } + } + + override val isRootGranted: Flow = suAdapter.isRootGranted override val isWriteSecureSettingsGranted: Flow = channelFlow { send(permissionAdapter.isGranted(Permission.WRITE_SECURE_SETTINGS)) @@ -72,9 +79,6 @@ class ConfigSettingsUseCaseImpl @Inject constructor( } } - override val rerouteKeyEvents: Flow = - preferences.get(Keys.rerouteKeyEvents).map { it ?: false } - override val isCompatibleImeChosen: Flow = inputMethodAdapter.chosenIme.map { imeHelper.isCompatibleImeChosen() } @@ -90,16 +94,18 @@ class ConfigSettingsUseCaseImpl @Inject constructor( imeHelper.enableCompatibleInputMethods() } - override suspend fun chooseCompatibleIme(): KMResult = imeHelper.chooseCompatibleInputMethod() + override suspend fun chooseCompatibleIme(): KMResult = + imeHelper.chooseCompatibleInputMethod() - override suspend fun showImePicker(): KMResult<*> = inputMethodAdapter.showImePicker(fromForeground = true) + override suspend fun showImePicker(): KMResult<*> = + inputMethodAdapter.showImePicker(fromForeground = true) override fun getPreference(key: Preferences.Key) = preferences.get(key) override fun setPreference(key: Preferences.Key, value: T?) = preferences.set(key, value) override val automaticBackupLocation = - preferences.get(Keys.automaticBackupLocation).map { it ?: "" } + preferences.get(Keys.automaticBackupLocation) override fun setAutomaticBackupLocation(uri: String) { preferences.set(Keys.automaticBackupLocation, uri) @@ -162,7 +168,12 @@ class ConfigSettingsUseCaseImpl @Inject constructor( permissionAdapter.request(Permission.POST_NOTIFICATIONS) } - override fun isNotificationsPermissionGranted(): Boolean = permissionAdapter.isGranted(Permission.POST_NOTIFICATIONS) + override fun requestRootPermission() { + suAdapter.requestPermission() + } + + override fun isNotificationsPermissionGranted(): Boolean = + permissionAdapter.isGranted(Permission.POST_NOTIFICATIONS) override fun getSoundFiles(): List = soundsManager.soundFiles.value @@ -175,24 +186,30 @@ class ConfigSettingsUseCaseImpl @Inject constructor( override fun resetAllSettings() { preferences.deleteAll() } + + override fun openNotificationChannelSettings(channelId: String) { + notificationAdapter.openChannelSettings(channelId) + } } interface ConfigSettingsUseCase { fun getPreference(key: Preferences.Key): Flow fun setPreference(key: Preferences.Key, value: T?) - val automaticBackupLocation: Flow + + val theme: Flow + val automaticBackupLocation: Flow fun setAutomaticBackupLocation(uri: String) fun disableAutomaticBackup() val isRootGranted: Flow - val isWriteSecureSettingsGranted: Flow + fun requestRootPermission() + val isWriteSecureSettingsGranted: Flow val isShizukuInstalled: Flow val isShizukuStarted: Flow val isShizukuPermissionGranted: Flow fun downloadShizuku() - fun openShizukuApp() - val rerouteKeyEvents: Flow + fun openShizukuApp() val isCompatibleImeChosen: Flow val isCompatibleImeEnabled: Flow suspend fun enableCompatibleIme() @@ -204,17 +221,18 @@ interface ConfigSettingsUseCase { val defaultRepeatDelay: Flow val defaultSequenceTriggerTimeout: Flow val defaultVibrateDuration: Flow - val defaultRepeatRate: Flow + val defaultRepeatRate: Flow fun getSoundFiles(): List fun deleteSoundFiles(uid: List) fun resetDefaultMappingOptions() fun requestWriteSecureSettingsPermission() fun requestNotificationsPermission() fun isNotificationsPermissionGranted(): Boolean + fun openNotificationChannelSettings(channelId: String) + fun requestShizukuPermission() val connectedInputDevices: StateFlow>> - fun resetAllSettings() } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsFragment.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsFragment.kt deleted file mode 100644 index 051ba20271..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsFragment.kt +++ /dev/null @@ -1,206 +0,0 @@ -package io.github.sds100.keymapper.base.settings - -import android.os.Bundle -import android.view.View -import androidx.lifecycle.Lifecycle -import androidx.preference.Preference -import androidx.preference.SeekBarPreference -import androidx.preference.isEmpty -import io.github.sds100.keymapper.base.R -import io.github.sds100.keymapper.base.utils.ui.SliderMaximums -import io.github.sds100.keymapper.base.utils.ui.SliderMinimums -import io.github.sds100.keymapper.base.utils.ui.launchRepeatOnLifecycle -import io.github.sds100.keymapper.base.utils.ui.viewLifecycleScope -import io.github.sds100.keymapper.data.Keys -import io.github.sds100.keymapper.data.PreferenceDefaults -import kotlinx.coroutines.flow.collectLatest - -class DefaultOptionsSettingsFragment : BaseSettingsFragment() { - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - preferenceManager.preferenceDataStore = viewModel.sharedPrefsDataStoreWrapper - addPreferencesFromResource(R.xml.preferences_empty) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - viewLifecycleScope.launchWhenResumed { - if (preferenceScreen.isEmpty()) { - populatePreferenceScreen() - } - } - - // these must all start after the preference screen has been populated so that findPreference works. - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.defaultLongPressDelay.collectLatest { value -> - val preference = findPreference(Keys.defaultLongPressDelay.name) - ?: return@collectLatest - - if (preference.value != value) { - preference.value = value - } - } - } - - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.defaultDoublePressDelay.collectLatest { value -> - val preference = - findPreference(Keys.defaultDoublePressDelay.name) - ?: return@collectLatest - - if (preference.value != value) { - preference.value = value - } - } - } - - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.defaultSequenceTriggerTimeout.collectLatest { value -> - val preference = - findPreference(Keys.defaultSequenceTriggerTimeout.name) - ?: return@collectLatest - - if (preference.value != value) { - preference.value = value - } - } - } - - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.defaultRepeatRate.collectLatest { value -> - val preference = findPreference(Keys.defaultRepeatRate.name) - ?: return@collectLatest - - if (preference.value != value) { - preference.value = value - } - } - } - - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.defaultRepeatDelay.collectLatest { value -> - val preference = findPreference(Keys.defaultRepeatDelay.name) - ?: return@collectLatest - - if (preference.value != value) { - preference.value = value - } - } - } - - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.defaultVibrateDuration.collectLatest { value -> - val preference = findPreference(Keys.defaultVibrateDuration.name) - ?: return@collectLatest - - if (preference.value != value) { - preference.value = value - } - } - } - } - - private fun populatePreferenceScreen() = preferenceScreen.apply { - // long press delay - SeekBarPreference(requireContext()).apply { - key = Keys.defaultLongPressDelay.name - setDefaultValue(PreferenceDefaults.LONG_PRESS_DELAY) - - setTitle(R.string.title_pref_long_press_delay) - isSingleLineTitle = false - setSummary(R.string.summary_pref_long_press_delay) - min = SliderMinimums.TRIGGER_LONG_PRESS_DELAY - max = 5000 - showSeekBarValue = true - - addPreference(this) - } - - // double press delay - SeekBarPreference(requireContext()).apply { - key = Keys.defaultDoublePressDelay.name - setDefaultValue(PreferenceDefaults.DOUBLE_PRESS_DELAY) - - setTitle(R.string.title_pref_double_press_delay) - isSingleLineTitle = false - setSummary(R.string.summary_pref_double_press_delay) - min = SliderMinimums.TRIGGER_DOUBLE_PRESS_DELAY - max = 5000 - showSeekBarValue = true - - addPreference(this) - } - - // vibration duration - SeekBarPreference(requireContext()).apply { - key = Keys.defaultVibrateDuration.name - setDefaultValue(PreferenceDefaults.VIBRATION_DURATION) - - setTitle(R.string.title_pref_vibration_duration) - isSingleLineTitle = false - setSummary(R.string.summary_pref_vibration_duration) - min = SliderMinimums.VIBRATION_DURATION - max = 1000 - showSeekBarValue = true - - addPreference(this) - } - - // repeat delay - SeekBarPreference(requireContext()).apply { - key = Keys.defaultRepeatDelay.name - setDefaultValue(PreferenceDefaults.REPEAT_DELAY) - - setTitle(R.string.title_pref_repeat_delay) - isSingleLineTitle = false - setSummary(R.string.summary_pref_repeat_delay) - min = SliderMinimums.ACTION_REPEAT_DELAY - max = SliderMaximums.ACTION_REPEAT_DELAY - showSeekBarValue = true - - addPreference(this) - } - - // repeat rate - SeekBarPreference(requireContext()).apply { - key = Keys.defaultRepeatRate.name - setDefaultValue(PreferenceDefaults.REPEAT_RATE) - - setTitle(R.string.title_pref_repeat_rate) - isSingleLineTitle = false - setSummary(R.string.summary_pref_repeat_rate) - min = SliderMinimums.ACTION_REPEAT_RATE - max = SliderMaximums.ACTION_REPEAT_RATE - showSeekBarValue = true - - addPreference(this) - } - - // sequence trigger timeout - SeekBarPreference(requireContext()).apply { - key = Keys.defaultSequenceTriggerTimeout.name - setDefaultValue(PreferenceDefaults.SEQUENCE_TRIGGER_TIMEOUT) - - setTitle(R.string.title_pref_sequence_trigger_timeout) - isSingleLineTitle = false - setSummary(R.string.summary_pref_sequence_trigger_timeout) - min = SliderMinimums.TRIGGER_SEQUENCE_TRIGGER_TIMEOUT - max = 5000 - showSeekBarValue = true - - addPreference(this) - } - - Preference(requireContext()).apply { - setTitle(R.string.title_pref_reset_defaults) - - setOnPreferenceClickListener { - viewModel.resetDefaultMappingOptions() - true - } - - addPreference(this) - } - } -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsScreen.kt new file mode 100644 index 0000000000..bdad749bad --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/DefaultOptionsSettingsScreen.kt @@ -0,0 +1,222 @@ +package io.github.sds100.keymapper.base.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.displayCutoutPadding +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import io.github.sds100.keymapper.base.utils.ui.SliderMaximums +import io.github.sds100.keymapper.base.utils.ui.SliderMinimums +import io.github.sds100.keymapper.base.utils.ui.SliderStepSizes +import io.github.sds100.keymapper.base.utils.ui.compose.SliderOptionText + +@Composable +fun DefaultOptionsSettingsScreen(modifier: Modifier = Modifier, viewModel: SettingsViewModel) { + val state by viewModel.defaultSettingsScreenState.collectAsStateWithLifecycle() + + DefaultOptionsSettingsScreen( + modifier, + onBackClick = viewModel::onBackClick, + ) { + Content( + state = state, + callback = viewModel, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DefaultOptionsSettingsScreen( + modifier: Modifier = Modifier, + onBackClick: () -> Unit = {}, + content: @Composable () -> Unit, +) { + Scaffold( + modifier = modifier.displayCutoutPadding(), + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.title_pref_default_options)) }, + ) + }, + bottomBar = { + BottomAppBar { + IconButton(onClick = onBackClick) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = stringResource(R.string.action_go_back), + ) + } + } + }, + ) { innerPadding -> + val layoutDirection = LocalLayoutDirection.current + val startPadding = innerPadding.calculateStartPadding(layoutDirection) + val endPadding = innerPadding.calculateEndPadding(layoutDirection) + + Surface( + modifier = Modifier + .fillMaxSize() + .padding( + top = innerPadding.calculateTopPadding(), + bottom = innerPadding.calculateBottomPadding(), + start = startPadding, + end = endPadding, + ), + ) { + content() + } + } +} + +@Composable +private fun Content( + modifier: Modifier = Modifier, + state: DefaultSettingsState, + callback: DefaultOptionsSettingsCallback = object : DefaultOptionsSettingsCallback {}, +) { + Column( + modifier = modifier + .verticalScroll(rememberScrollState()) + .fillMaxWidth(), + ) { + Spacer(modifier = Modifier.height(8.dp)) + + // Long press delay + SliderOptionText( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + title = stringResource(R.string.title_pref_long_press_delay), + defaultValue = state.defaultLongPressDelay.toFloat(), + value = state.longPressDelay.toFloat(), + valueText = { "${it.toInt()} ms" }, + onValueChange = { callback.onLongPressDelayChanged(it.toInt()) }, + valueRange = SliderMinimums.TRIGGER_LONG_PRESS_DELAY.toFloat()..SliderMaximums.TRIGGER_LONG_PRESS_DELAY.toFloat(), + stepSize = SliderStepSizes.TRIGGER_LONG_PRESS_DELAY, + ) + Spacer(Modifier.height(8.dp)) + + // Double press delay + SliderOptionText( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + title = stringResource(R.string.title_pref_double_press_delay), + defaultValue = state.defaultDoublePressDelay.toFloat(), + value = state.doublePressDelay.toFloat(), + valueText = { "${it.toInt()} ms" }, + onValueChange = { callback.onDoublePressDelayChanged(it.toInt()) }, + valueRange = SliderMinimums.TRIGGER_DOUBLE_PRESS_DELAY.toFloat()..SliderMaximums.TRIGGER_DOUBLE_PRESS_DELAY.toFloat(), + stepSize = SliderStepSizes.TRIGGER_DOUBLE_PRESS_DELAY, + ) + Spacer(Modifier.height(8.dp)) + + // Vibrate duration + SliderOptionText( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + title = stringResource(R.string.title_pref_vibration_duration), + defaultValue = state.defaultVibrateDuration.toFloat(), + value = state.vibrateDuration.toFloat(), + valueText = { "${it.toInt()} ms" }, + onValueChange = { callback.onVibrateDurationChanged(it.toInt()) }, + valueRange = SliderMinimums.VIBRATION_DURATION.toFloat()..SliderMaximums.VIBRATION_DURATION.toFloat(), + stepSize = SliderStepSizes.VIBRATION_DURATION, + ) + Spacer(Modifier.height(8.dp)) + + // Repeat delay + SliderOptionText( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + title = stringResource(R.string.title_pref_repeat_delay), + defaultValue = state.defaultRepeatDelay.toFloat(), + value = state.repeatDelay.toFloat(), + valueText = { "${it.toInt()} ms" }, + onValueChange = { callback.onRepeatDelayChanged(it.toInt()) }, + valueRange = SliderMinimums.ACTION_REPEAT_DELAY.toFloat()..SliderMaximums.ACTION_REPEAT_DELAY.toFloat(), + stepSize = SliderStepSizes.ACTION_REPEAT_DELAY, + ) + Spacer(Modifier.height(8.dp)) + + // Repeat rate + SliderOptionText( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + title = stringResource(R.string.title_pref_repeat_rate), + defaultValue = state.defaultRepeatRate.toFloat(), + value = state.repeatRate.toFloat(), + valueText = { "${it.toInt()} ms" }, + onValueChange = { callback.onRepeatRateChanged(it.toInt()) }, + valueRange = SliderMinimums.ACTION_REPEAT_RATE.toFloat()..SliderMaximums.ACTION_REPEAT_RATE.toFloat(), + stepSize = SliderStepSizes.ACTION_REPEAT_RATE, + ) + Spacer(Modifier.height(8.dp)) + + // Sequence trigger timeout + SliderOptionText( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + title = stringResource(R.string.title_pref_sequence_trigger_timeout), + defaultValue = state.defaultSequenceTriggerTimeout.toFloat(), + value = state.sequenceTriggerTimeout.toFloat(), + valueText = { "${it.toInt()} ms" }, + onValueChange = { callback.onSequenceTriggerTimeoutChanged(it.toInt()) }, + valueRange = SliderMinimums.TRIGGER_SEQUENCE_TRIGGER_TIMEOUT.toFloat()..SliderMaximums.TRIGGER_SEQUENCE_TRIGGER_TIMEOUT.toFloat(), + stepSize = SliderStepSizes.TRIGGER_SEQUENCE_TRIGGER_TIMEOUT, + ) + Spacer(Modifier.height(8.dp)) + } +} + +interface DefaultOptionsSettingsCallback { + fun onLongPressDelayChanged(delay: Int) = run { } + fun onDoublePressDelayChanged(delay: Int) = run { } + fun onVibrateDurationChanged(duration: Int) = run { } + fun onRepeatDelayChanged(delay: Int) = run { } + fun onRepeatRateChanged(rate: Int) = run { } + fun onSequenceTriggerTimeoutChanged(timeout: Int) = run { } +} + +@Preview +@Composable +private fun Preview() { + KeyMapperTheme { + DefaultOptionsSettingsScreen(modifier = Modifier.fillMaxSize(), onBackClick = {}) { + Content( + state = DefaultSettingsState(), + ) + } + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/ImePickerSettingsFragment.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/ImePickerSettingsFragment.kt deleted file mode 100644 index 84a58b17ba..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/ImePickerSettingsFragment.kt +++ /dev/null @@ -1,100 +0,0 @@ -package io.github.sds100.keymapper.base.settings - -import android.os.Build -import android.os.Bundle -import android.view.View -import androidx.annotation.RequiresApi -import androidx.preference.Preference -import androidx.preference.SwitchPreferenceCompat -import androidx.preference.isEmpty -import io.github.sds100.keymapper.base.R -import io.github.sds100.keymapper.base.system.notifications.NotificationController -import io.github.sds100.keymapper.base.utils.ui.viewLifecycleScope -import io.github.sds100.keymapper.data.Keys -import io.github.sds100.keymapper.system.notifications.NotificationUtils - -class ImePickerSettingsFragment : BaseSettingsFragment() { - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - preferenceManager.preferenceDataStore = viewModel.sharedPrefsDataStoreWrapper - addPreferencesFromResource(R.xml.preferences_empty) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - viewLifecycleScope.launchWhenResumed { - if (preferenceScreen.isEmpty()) { - populatePreferenceScreen() - } - } - } - - private fun populatePreferenceScreen() = preferenceScreen.apply { - // show keyboard picker notification - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // show a preference linking to the notification management screen - Preference(requireContext()).apply { - key = Keys.showImePickerNotification.name - - setTitle(R.string.title_pref_show_ime_picker_notification) - isSingleLineTitle = false - setSummary(R.string.summary_pref_show_ime_picker_notification) - - setOnPreferenceClickListener { - onImePickerNotificationClick() - - true - } - - addPreference(this) - } - } else { - SwitchPreferenceCompat(requireContext()).apply { - key = Keys.showImePickerNotification.name - setDefaultValue(false) - - setTitle(R.string.title_pref_show_ime_picker_notification) - isSingleLineTitle = false - setSummary(R.string.summary_pref_show_ime_picker_notification) - - addPreference(this) - } - } - - // auto show keyboard picker - SwitchPreferenceCompat(requireContext()).apply { - key = Keys.showImePickerOnDeviceConnect.name - setDefaultValue(false) - - setTitle(R.string.title_pref_auto_show_ime_picker) - isSingleLineTitle = false - setSummary(R.string.summary_pref_auto_show_ime_picker) - - addPreference(this) - } - - addPreference( - SettingsUtils.createChooseDevicesPreference( - requireContext(), - viewModel, - Keys.devicesThatShowImePicker, - ), - ) - } - - @RequiresApi(Build.VERSION_CODES.O) - private fun onImePickerNotificationClick() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && - !viewModel.isNotificationPermissionGranted() - ) { - viewModel.requestNotificationsPermission() - return - } - - NotificationUtils.openChannelSettings( - requireContext(), - NotificationController.CHANNEL_IME_PICKER, - ) - } -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/MainSettingsFragment.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/MainSettingsFragment.kt deleted file mode 100644 index 072e8cedb7..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/MainSettingsFragment.kt +++ /dev/null @@ -1,543 +0,0 @@ -package io.github.sds100.keymapper.base.settings - -import android.annotation.SuppressLint -import android.content.ActivityNotFoundException -import android.content.Intent -import android.os.Build -import android.os.Bundle -import android.view.View -import androidx.activity.result.contract.ActivityResultContracts.CreateDocument -import androidx.annotation.RequiresApi -import androidx.lifecycle.Lifecycle -import androidx.navigation.fragment.findNavController -import androidx.preference.DropDownPreference -import androidx.preference.Preference -import androidx.preference.PreferenceCategory -import androidx.preference.SwitchPreferenceCompat -import androidx.preference.isEmpty -import io.github.sds100.keymapper.base.R -import io.github.sds100.keymapper.base.backup.BackupUtils -import io.github.sds100.keymapper.base.system.notifications.NotificationController -import io.github.sds100.keymapper.base.utils.ui.launchRepeatOnLifecycle -import io.github.sds100.keymapper.base.utils.ui.str -import io.github.sds100.keymapper.base.utils.ui.strArray -import io.github.sds100.keymapper.base.utils.ui.viewLifecycleScope -import io.github.sds100.keymapper.common.utils.firstBlocking -import io.github.sds100.keymapper.data.Keys -import io.github.sds100.keymapper.data.PreferenceDefaults -import io.github.sds100.keymapper.system.files.FileUtils -import io.github.sds100.keymapper.system.notifications.NotificationUtils -import io.github.sds100.keymapper.system.shizuku.ShizukuUtils -import kotlinx.coroutines.flow.collectLatest -import splitties.alertdialog.appcompat.alertDialog -import splitties.alertdialog.appcompat.messageResource -import splitties.alertdialog.appcompat.negativeButton -import splitties.alertdialog.appcompat.positiveButton - -class MainSettingsFragment : BaseSettingsFragment() { - - companion object { - private const val KEY_GRANT_WRITE_SECURE_SETTINGS = "pref_key_grant_write_secure_settings" - private const val CATEGORY_KEY_GRANT_WRITE_SECURE_SETTINGS = - "category_key_grant_write_secure_settings" - private const val KEY_GRANT_SHIZUKU = "pref_key_grant_shizuku" - private const val KEY_AUTOMATICALLY_CHANGE_IME_LINK = - "pref_automatically_change_ime_link" - } - - private val chooseAutomaticBackupLocationLauncher = - registerForActivityResult(CreateDocument(FileUtils.MIME_TYPE_ZIP)) { - it ?: return@registerForActivityResult - - viewModel.setAutomaticBackupLocation(it.toString()) - - val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION - - requireContext().contentResolver.takePersistableUriPermission(it, takeFlags) - } - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - preferenceManager.preferenceDataStore = viewModel.sharedPrefsDataStoreWrapper - addPreferencesFromResource(R.xml.preferences_empty) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - viewLifecycleScope.launchWhenResumed { - if (preferenceScreen.isEmpty()) { - populatePreferenceScreen() - } - } - - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.automaticBackupLocation.collectLatest { backupLocation -> - val preference = - findPreference(Keys.automaticBackupLocation.name) - ?: return@collectLatest - preference.summary = if (backupLocation.isBlank()) { - str(R.string.summary_pref_automatic_backup_location_disabled) - } else { - backupLocation - } - } - } - - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.isWriteSecureSettingsPermissionGranted.collectLatest { isGranted -> - val writeSecureSettingsCategory = - findPreference(CATEGORY_KEY_GRANT_WRITE_SECURE_SETTINGS) - - findPreference(KEY_GRANT_WRITE_SECURE_SETTINGS)?.apply { - isEnabled = !isGranted - - if (isGranted) { - setTitle(R.string.title_pref_grant_write_secure_settings_granted) - setIcon(R.drawable.ic_outline_check_circle_outline_24) - } else { - setTitle(R.string.title_pref_grant_write_secure_settings_not_granted) - setIcon(R.drawable.ic_baseline_error_outline_24) - } - } - - writeSecureSettingsCategory - ?.findPreference(KEY_AUTOMATICALLY_CHANGE_IME_LINK)?.apply { - isEnabled = isGranted - } - } - } - - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.isShizukuPermissionGranted.collectLatest { isGranted -> - findPreference(KEY_GRANT_SHIZUKU)?.apply { - if (isGranted) { - setTitle(R.string.title_pref_grant_shizuku_granted) - setIcon(R.drawable.ic_outline_check_circle_outline_24) - } else { - setTitle(R.string.title_pref_grant_shizuku_not_granted) - setIcon(R.drawable.ic_baseline_error_outline_24) - } - } - } - } - } - - private fun populatePreferenceScreen() = preferenceScreen.apply { - // dark theme - DropDownPreference(requireContext()).apply { - key = Keys.darkTheme.name - setDefaultValue(PreferenceDefaults.DARK_THEME) - isSingleLineTitle = false - - setTitle(R.string.title_pref_dark_theme) - setSummary(R.string.summary_pref_dark_theme) - entries = strArray(R.array.pref_dark_theme_entries) - entryValues = ThemeUtils.THEMES.map { it.toString() }.toTypedArray() - - addPreference(this) - } - - // automatic backup location - Preference(requireContext()).apply { - key = Keys.automaticBackupLocation.name - setDefaultValue("") - - setTitle(R.string.title_pref_automatic_backup_location) - isSingleLineTitle = false - - setOnPreferenceClickListener { - val backupLocation = viewModel.automaticBackupLocation.firstBlocking() - - if (backupLocation.isBlank()) { - try { - chooseAutomaticBackupLocationLauncher.launch(BackupUtils.DEFAULT_AUTOMATIC_BACKUP_NAME) - } catch (e: ActivityNotFoundException) { - viewModel.onCreateBackupFileActivityNotFound() - } - } else { - requireContext().alertDialog { - messageResource = R.string.dialog_message_change_location_or_disable - - positiveButton(R.string.pos_change_location) { - chooseAutomaticBackupLocationLauncher.launch(BackupUtils.DEFAULT_AUTOMATIC_BACKUP_NAME) - } - - negativeButton(R.string.neg_turn_off) { - viewModel.disableAutomaticBackup() - } - - show() - } - } - - true - } - addPreference(this) - } - - // hide home screen alerts - SwitchPreferenceCompat(requireContext()).apply { - key = Keys.hideHomeScreenAlerts.name - setDefaultValue(false) - - setTitle(R.string.title_pref_hide_home_screen_alerts) - isSingleLineTitle = false - setSummary(R.string.summary_pref_hide_home_screen_alerts) - - addPreference(this) - } - - // force vibrate - SwitchPreferenceCompat(requireContext()).apply { - key = Keys.forceVibrate.name - setDefaultValue(false) - - setTitle(R.string.title_pref_force_vibrate) - isSingleLineTitle = false - setSummary(R.string.summary_pref_force_vibrate) - - addPreference(this) - } - - // show device descriptors - SwitchPreferenceCompat(requireContext()).apply { - key = Keys.showDeviceDescriptors.name - setDefaultValue(false) - isSingleLineTitle = false - - setTitle(R.string.title_pref_show_device_descriptors) - setSummary(R.string.summary_pref_show_device_descriptors) - - addPreference(this) - } - - // toggle key maps notification - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // show a preference linking to the notification management screen - Preference(requireContext()).apply { - key = Keys.showToggleKeyMapsNotification.name - - setTitle(R.string.title_pref_show_toggle_keymaps_notification) - isSingleLineTitle = false - setSummary(R.string.summary_pref_show_toggle_keymaps_notification) - - setOnPreferenceClickListener { - onToggleKeyMapsNotificationClick() - - true - } - - addPreference(this) - } - } else { - SwitchPreferenceCompat(requireContext()).apply { - key = Keys.showToggleKeyMapsNotification.name - setDefaultValue(true) - - setTitle(R.string.title_pref_show_toggle_keymaps_notification) - isSingleLineTitle = false - setSummary(R.string.summary_pref_show_toggle_keymaps_notification) - - addPreference(this) - } - } - - // default options - Preference(requireContext()).apply { - setTitle(R.string.title_pref_default_options) - setSummary(R.string.summary_pref_default_options) - isSingleLineTitle = false - - setOnPreferenceClickListener { - val direction = MainSettingsFragmentDirections.toDefaultOptionsSettingsFragment() - findNavController().navigate(direction) - - true - } - - addPreference(this) - } - - // apps can't show the keyboard picker when in the background from Android 8.1+ - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) { - Preference(requireContext()).apply { - setTitle(R.string.title_pref_category_ime_picker) - setSummary(R.string.summary_pref_category_ime_picker) - isSingleLineTitle = false - - setOnPreferenceClickListener { - val direction = MainSettingsFragmentDirections.toImePickerSettingsFragment() - findNavController().navigate(direction) - - true - } - - addPreference(this) - } - } - - // delete sound files - Preference(requireContext()).apply { - setTitle(R.string.title_pref_delete_sound_files) - setSummary(R.string.summary_pref_delete_sound_files) - isSingleLineTitle = false - - setOnPreferenceClickListener { - viewModel.onDeleteSoundFilesClick() - - true - } - - addPreference(this) - } - - // link to settings to automatically change the ime - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - addPreference(automaticallyChangeImeSettingsLink()) - } - - // android 11 device id reset work around - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { - Preference(requireContext()).apply { - setTitle(R.string.title_pref_reroute_keyevents_link) - setSummary(R.string.summary_pref_reroute_keyevents_link) - isSingleLineTitle = false - - setOnPreferenceClickListener { - val direction = - MainSettingsFragmentDirections.toAndroid11BugWorkaroundSettingsFragment() - findNavController().navigate(direction) - - true - } - - addPreference(this) - } - } - - // Shizuku - // shizuku is only supported on Marhsmallow+ - if (ShizukuUtils.isSupportedForSdkVersion()) { - Preference(requireContext()).apply { - setTitle(R.string.title_pref_category_shizuku) - setSummary(R.string.summary_pref_category_shizuku) - isSingleLineTitle = false - - setOnPreferenceClickListener { - val direction = - MainSettingsFragmentDirections.toShizukuSettingsFragment() - findNavController().navigate(direction) - - true - } - - addPreference(this) - } - } - - Preference(requireContext()).apply { - setTitle(R.string.title_pref_reset_settings) - setSummary(R.string.summary_pref_reset_settings) - - setOnPreferenceClickListener { - viewModel.onResetAllSettingsClick() - true - } - - addPreference(this) - } - - // write secure settings - PreferenceCategory(requireContext()).apply { - key = CATEGORY_KEY_GRANT_WRITE_SECURE_SETTINGS - setTitle(R.string.title_pref_category_write_secure_settings) - - preferenceScreen.addPreference(this) - - Preference(requireContext()).apply { - isSelectable = false - setSummary(R.string.summary_pref_category_write_secure_settings) - - addPreference(this) - } - - Preference(requireContext()).apply { - key = KEY_GRANT_WRITE_SECURE_SETTINGS - - if (viewModel.isWriteSecureSettingsPermissionGranted.firstBlocking()) { - setTitle(R.string.title_pref_grant_write_secure_settings_granted) - setIcon(R.drawable.ic_outline_check_circle_outline_24) - } else { - setTitle(R.string.title_pref_grant_write_secure_settings_not_granted) - setIcon(R.drawable.ic_baseline_error_outline_24) - } - - setOnPreferenceClickListener { - viewModel.requestWriteSecureSettingsPermission() - true - } - - addPreference(this) - } - - // accessibility services can change the ime on Android 11+ - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { - addPreference(automaticallyChangeImeSettingsLink()) - } - } - - // root - createRootCategory() - - // log - createLogCategory() - } - - @RequiresApi(Build.VERSION_CODES.O) - private fun onToggleKeyMapsNotificationClick() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && - !viewModel.isNotificationPermissionGranted() - ) { - viewModel.requestNotificationsPermission() - return - } - - NotificationUtils.openChannelSettings( - requireContext(), - NotificationController.CHANNEL_TOGGLE_KEYMAPS, - ) - } - - private fun automaticallyChangeImeSettingsLink() = Preference(requireContext()).apply { - key = KEY_AUTOMATICALLY_CHANGE_IME_LINK - - setTitle(R.string.title_pref_automatically_change_ime) - setSummary(R.string.summary_pref_automatically_change_ime) - isSingleLineTitle = false - - setOnPreferenceClickListener { - val direction = MainSettingsFragmentDirections.toAutomaticallyChangeImeSettings() - findNavController().navigate(direction) - - true - } - } - - private fun createLogCategory() = PreferenceCategory(requireContext()).apply { - setTitle(R.string.title_pref_category_log) - preferenceScreen.addPreference(this) - - Preference(requireContext()).apply { - isSelectable = false - setSummary(R.string.summary_pref_category_log) - - addPreference(this) - } - - // enable logging - SwitchPreferenceCompat(requireContext()).apply { - key = Keys.log.name - setDefaultValue(false) - - isSingleLineTitle = false - setTitle(R.string.title_pref_toggle_logging) - - addPreference(this) - } - - // open log fragment - Preference(requireContext()).apply { - isSingleLineTitle = false - setTitle(R.string.title_pref_view_and_share_log) - - setOnPreferenceClickListener { - findNavController().navigate(MainSettingsFragmentDirections.toLogFragment()) - - true - } - - addPreference(this) - } - - // report issue to developer -// Preference(requireContext()).apply { -// isSingleLineTitle = false -// setTitle(R.string.title_pref_report_issue) -// -// setOnPreferenceClickListener { -// -// true -// } -// -// addPreference(this) -// } - } - - @SuppressLint("NewApi") - private fun createRootCategory() = PreferenceCategory(requireContext()).apply { - setTitle(R.string.title_pref_category_root) - preferenceScreen.addPreference(this) - - Preference(requireContext()).apply { - isSelectable = false - setSummary(R.string.summary_pref_category_root) - - addPreference(this) - } - - // root permission switch - SwitchPreferenceCompat(requireContext()).apply { - key = Keys.hasRootPermission.name - setDefaultValue(false) - - isSingleLineTitle = false - setTitle(R.string.title_pref_root_permission) - setSummary(R.string.summary_pref_root_permission) - - addPreference(this) - } - - // only show the options to show the keyboard picker when rooted in these versions - if (Build.VERSION.SDK_INT in Build.VERSION_CODES.O_MR1..Build.VERSION_CODES.P) { - // show a preference linking to the notification management screen - Preference(requireContext()).apply { - key = Keys.showImePickerNotification.name - - setTitle(R.string.title_pref_show_ime_picker_notification) - isSingleLineTitle = false - setSummary(R.string.summary_pref_show_ime_picker_notification) - - setOnPreferenceClickListener { - NotificationUtils.openChannelSettings( - requireContext(), - NotificationController.CHANNEL_IME_PICKER, - ) - - true - } - - addPreference(this) - } - - SwitchPreferenceCompat(requireContext()).apply { - key = Keys.showImePickerOnDeviceConnect.name - setDefaultValue(false) - - setTitle(R.string.title_pref_auto_show_ime_picker) - isSingleLineTitle = false - setSummary(R.string.summary_pref_auto_show_ime_picker) - - addPreference(this) - } - - addPreference( - SettingsUtils.createChooseDevicesPreference( - requireContext(), - viewModel, - Keys.devicesThatShowImePicker, - ), - ) - } - } -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt new file mode 100644 index 0000000000..d657cd8b48 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsScreen.kt @@ -0,0 +1,435 @@ +package io.github.sds100.keymapper.base.settings + +import android.content.ActivityNotFoundException +import android.content.Intent +import android.os.Build +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts.CreateDocument +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.displayCutoutPadding +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material.icons.outlined.BugReport +import androidx.compose.material.icons.outlined.FindInPage +import androidx.compose.material.icons.outlined.Gamepad +import androidx.compose.material.icons.rounded.Code +import androidx.compose.material.icons.rounded.Construction +import androidx.compose.material.icons.rounded.Devices +import androidx.compose.material.icons.rounded.Keyboard +import androidx.compose.material.icons.rounded.PlayCircleOutline +import androidx.compose.material.icons.rounded.Tune +import androidx.compose.material.icons.rounded.Vibration +import androidx.compose.material.icons.rounded.VisibilityOff +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.backup.BackupUtils +import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperSegmentedButtonRow +import io.github.sds100.keymapper.base.utils.ui.compose.OptionPageButton +import io.github.sds100.keymapper.base.utils.ui.compose.OptionsHeaderRow +import io.github.sds100.keymapper.base.utils.ui.compose.SwitchPreferenceCompose +import io.github.sds100.keymapper.base.utils.ui.compose.icons.FolderManaged +import io.github.sds100.keymapper.base.utils.ui.compose.icons.KeyMapperIcons +import io.github.sds100.keymapper.base.utils.ui.compose.icons.ProModeIcon +import io.github.sds100.keymapper.base.utils.ui.compose.icons.WandStars +import io.github.sds100.keymapper.common.utils.BuildUtils +import io.github.sds100.keymapper.system.files.FileUtils +import kotlinx.coroutines.launch + +private val isProModeSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + +@Composable +fun SettingsScreen(modifier: Modifier = Modifier, viewModel: SettingsViewModel) { + val state by viewModel.mainScreenState.collectAsStateWithLifecycle() + val snackbarHostState = SnackbarHostState() + var showAutomaticBackupDialog by remember { mutableStateOf(false) } + val context = LocalContext.current + val scope = rememberCoroutineScope() + + val automaticBackupLocationChooser = + rememberLauncherForActivityResult(CreateDocument(FileUtils.MIME_TYPE_ZIP)) { uri -> + uri ?: return@rememberLauncherForActivityResult + viewModel.setAutomaticBackupLocation(uri.toString()) + + val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION + + context.contentResolver.takePersistableUriPermission(uri, takeFlags) + } + + if (showAutomaticBackupDialog) { + val activityNotFoundText = + stringResource(R.string.dialog_message_no_app_found_to_create_file) + + AlertDialog( + onDismissRequest = { showAutomaticBackupDialog = false }, + title = { Text(stringResource(R.string.dialog_title_change_location_or_disable)) }, + text = { Text(stringResource(R.string.dialog_message_change_location_or_disable)) }, + confirmButton = { + TextButton( + onClick = { + showAutomaticBackupDialog = false + + try { + automaticBackupLocationChooser.launch(BackupUtils.DEFAULT_AUTOMATIC_BACKUP_NAME) + } catch (e: ActivityNotFoundException) { + scope.launch { + snackbarHostState.showSnackbar(activityNotFoundText) + } + } + }, + ) { + Text(stringResource(R.string.pos_change_location)) + } + }, + dismissButton = { + TextButton( + onClick = { + showAutomaticBackupDialog = false + viewModel.disableAutomaticBackup() + }, + ) { + Text(stringResource(R.string.neg_turn_off)) + } + }, + ) + } + + SettingsScreen( + modifier, + onBackClick = viewModel::onBackClick, + viewModel::onResetAllSettingsClick, + snackbarHostState = snackbarHostState, + ) { + Content( + state = state, + onThemeSelected = viewModel::onThemeSelected, + onPauseResumeNotificationClick = viewModel::onPauseResumeNotificationClick, + onDefaultOptionsClick = viewModel::onDefaultOptionsClick, + onProModeClick = { + if (isProModeSupported) { + viewModel.onProModeClick() + } else { + scope.launch { + snackbarHostState.showSnackbar( + context.getString( + R.string.error_sdk_version_too_low, + BuildUtils.getSdkVersionName(Build.VERSION_CODES.Q), + ), + ) + } + } + }, + onAutomaticChangeImeClick = viewModel::onAutomaticChangeImeClick, + onForceVibrateToggled = viewModel::onForceVibrateToggled, + onLoggingToggled = viewModel::onLoggingToggled, + onViewLogClick = viewModel::onViewLogClick, + onHideHomeScreenAlertsToggled = viewModel::onHideHomeScreenAlertsToggled, + onShowDeviceDescriptorsToggled = viewModel::onShowDeviceDescriptorsToggled, + onAutomaticBackupClick = { + if (state.autoBackupLocation.isNullOrBlank()) { + automaticBackupLocationChooser.launch(BackupUtils.DEFAULT_AUTOMATIC_BACKUP_NAME) + } else { + showAutomaticBackupDialog = true + } + }, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SettingsScreen( + modifier: Modifier = Modifier, + onBackClick: () -> Unit = {}, + onResetClick: () -> Unit = {}, + snackbarHostState: SnackbarHostState = SnackbarHostState(), + content: @Composable () -> Unit, +) { + Scaffold( + modifier = modifier.displayCutoutPadding(), + snackbarHost = { SnackbarHost(snackbarHostState) }, + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.action_settings)) }, + actions = { + OutlinedButton( + modifier = Modifier.padding(horizontal = 16.dp), + onClick = onResetClick, + ) { + Text(stringResource(R.string.settings_reset_app_bar_button)) + } + }, + ) + }, + bottomBar = { + BottomAppBar { + IconButton(onClick = onBackClick) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = stringResource(R.string.action_go_back), + ) + } + } + }, + ) { innerPadding -> + val layoutDirection = LocalLayoutDirection.current + val startPadding = innerPadding.calculateStartPadding(layoutDirection) + val endPadding = innerPadding.calculateEndPadding(layoutDirection) + + Surface( + modifier = Modifier + .fillMaxSize() + .padding( + top = innerPadding.calculateTopPadding(), + bottom = innerPadding.calculateBottomPadding(), + start = startPadding, + end = endPadding, + ), + ) { + content() + } + } +} + +@Composable +private fun Content( + modifier: Modifier = Modifier, + state: MainSettingsState, + onThemeSelected: (Theme) -> Unit = { }, + onPauseResumeNotificationClick: () -> Unit = { }, + onDefaultOptionsClick: () -> Unit = { }, + onAutomaticBackupClick: () -> Unit = { }, + onProModeClick: () -> Unit = { }, + onAutomaticChangeImeClick: () -> Unit = { }, + onForceVibrateToggled: (Boolean) -> Unit = { }, + onLoggingToggled: (Boolean) -> Unit = { }, + onViewLogClick: () -> Unit = { }, + onHideHomeScreenAlertsToggled: (Boolean) -> Unit = { }, + onShowDeviceDescriptorsToggled: (Boolean) -> Unit = { }, +) { + Column( + modifier + .verticalScroll(rememberScrollState()) + .padding(horizontal = 16.dp), + ) { + Spacer(modifier = Modifier.height(8.dp)) + + OptionsHeaderRow( + modifier = Modifier.fillMaxWidth(), + icon = KeyMapperIcons.WandStars, + text = stringResource(R.string.settings_section_customize_experience_title), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = stringResource(R.string.title_pref_dark_theme), + style = MaterialTheme.typography.bodyLarge, + ) + + val buttonStates: List> = listOf( + Theme.AUTO to stringResource(R.string.theme_system), + Theme.LIGHT to stringResource(R.string.theme_light), + Theme.DARK to stringResource(R.string.theme_dark), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + KeyMapperSegmentedButtonRow( + modifier = Modifier.fillMaxWidth(), + buttonStates, + state.theme, + onStateSelected = onThemeSelected, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OptionPageButton( + title = stringResource(R.string.title_pref_show_toggle_keymaps_notification), + text = stringResource(R.string.summary_pref_show_toggle_keymaps_notification), + icon = Icons.Rounded.PlayCircleOutline, + onClick = onPauseResumeNotificationClick, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + SwitchPreferenceCompose( + title = stringResource(R.string.title_pref_hide_home_screen_alerts), + text = stringResource(R.string.summary_pref_hide_home_screen_alerts), + icon = Icons.Rounded.VisibilityOff, + isChecked = state.hideHomeScreenAlerts, + onCheckedChange = onHideHomeScreenAlertsToggled, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OptionsHeaderRow( + modifier = Modifier.fillMaxWidth(), + icon = Icons.Outlined.Gamepad, + text = stringResource(R.string.settings_section_key_maps_title), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OptionPageButton( + title = stringResource(R.string.title_pref_default_options), + text = stringResource(R.string.summary_pref_default_options), + icon = Icons.Rounded.Tune, + onClick = onDefaultOptionsClick, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + SwitchPreferenceCompose( + title = stringResource(R.string.title_pref_force_vibrate), + text = stringResource(R.string.summary_pref_force_vibrate), + icon = Icons.Rounded.Vibration, + isChecked = state.forceVibrate, + onCheckedChange = onForceVibrateToggled, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + SwitchPreferenceCompose( + title = stringResource(R.string.title_pref_show_device_descriptors), + text = stringResource(R.string.summary_pref_show_device_descriptors), + icon = Icons.Rounded.Devices, + isChecked = state.showDeviceDescriptors, + onCheckedChange = onShowDeviceDescriptorsToggled, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OptionsHeaderRow( + modifier = Modifier.fillMaxWidth(), + icon = KeyMapperIcons.FolderManaged, + text = stringResource(R.string.settings_section_data_management_title), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OptionPageButton( + title = if (state.autoBackupLocation == null) { + stringResource(R.string.title_pref_automatic_backup_location_disabled) + } else { + stringResource(R.string.title_pref_automatic_backup_location_enabled) + }, + text = state.autoBackupLocation + ?: stringResource(R.string.summary_pref_automatic_backup_location_disabled), + icon = Icons.Rounded.Tune, + onClick = onAutomaticBackupClick, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OptionsHeaderRow( + modifier = Modifier.fillMaxWidth(), + icon = Icons.Rounded.Construction, + text = stringResource(R.string.settings_section_power_user_title), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OptionPageButton( + title = if (isProModeSupported) { + stringResource(R.string.title_pref_pro_mode) + } else { + stringResource(R.string.title_pref_pro_mode) + }, + text = if (isProModeSupported) { + stringResource(R.string.summary_pref_pro_mode) + } else { + stringResource( + R.string.error_sdk_version_too_low, + BuildUtils.getSdkVersionName(Build.VERSION_CODES.Q), + ) + }, + icon = KeyMapperIcons.ProModeIcon, + onClick = onProModeClick, + ) + + OptionPageButton( + title = stringResource(R.string.title_pref_automatically_change_ime), + text = stringResource(R.string.summary_pref_automatically_change_ime), + icon = Icons.Rounded.Keyboard, + onClick = onAutomaticChangeImeClick, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OptionsHeaderRow( + modifier = Modifier.fillMaxWidth(), + icon = Icons.Rounded.Code, + text = stringResource(R.string.settings_section_debugging_title), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + SwitchPreferenceCompose( + title = stringResource(R.string.title_pref_toggle_logging), + text = stringResource(R.string.summary_pref_toggle_logging), + icon = Icons.Outlined.BugReport, + isChecked = state.loggingEnabled, + onCheckedChange = onLoggingToggled, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OptionPageButton( + title = stringResource(R.string.title_pref_view_and_share_log), + text = stringResource(R.string.summary_pref_view_and_share_log), + icon = Icons.Outlined.FindInPage, + onClick = onViewLogClick, + ) + + Spacer(modifier = Modifier.height(8.dp)) + } +} + +@Preview +@Composable +private fun Preview() { + KeyMapperTheme { + SettingsScreen(modifier = Modifier.fillMaxSize(), onBackClick = {}) { + Content( + state = MainSettingsState(), + ) + } + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsUtils.kt deleted file mode 100644 index d1728bdb56..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsUtils.kt +++ /dev/null @@ -1,28 +0,0 @@ -package io.github.sds100.keymapper.base.settings - -import android.content.Context -import androidx.annotation.StringRes -import androidx.datastore.preferences.core.Preferences -import androidx.preference.Preference -import io.github.sds100.keymapper.base.R - -object SettingsUtils { - - fun createChooseDevicesPreference( - ctx: Context, - settingsViewModel: SettingsViewModel, - key: Preferences.Key>, - @StringRes title: Int = R.string.title_pref_choose_devices, - ): Preference = Preference(ctx).apply { - this.key = key.name - - setTitle(title) - isSingleLineTitle = false - - setOnPreferenceClickListener { - settingsViewModel.chooseDevicesForPreference(key) - - true - } - } -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt index 34d20d304a..7f88595d6e 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/SettingsViewModel.kt @@ -1,11 +1,16 @@ package io.github.sds100.keymapper.base.settings +import android.os.Build import androidx.datastore.preferences.core.Preferences import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.system.notifications.NotificationController import io.github.sds100.keymapper.base.utils.getFullMessage +import io.github.sds100.keymapper.base.utils.navigation.NavDestination +import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider +import io.github.sds100.keymapper.base.utils.navigation.navigate import io.github.sds100.keymapper.base.utils.ui.DialogModel import io.github.sds100.keymapper.base.utils.ui.DialogProvider import io.github.sds100.keymapper.base.utils.ui.DialogResponse @@ -16,10 +21,13 @@ import io.github.sds100.keymapper.common.utils.State import io.github.sds100.keymapper.common.utils.onFailure import io.github.sds100.keymapper.common.utils.onSuccess import io.github.sds100.keymapper.common.utils.otherwise +import io.github.sds100.keymapper.data.Keys +import io.github.sds100.keymapper.data.PreferenceDefaults import io.github.sds100.keymapper.data.utils.SharedPrefsDataStoreWrapper import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -29,38 +37,14 @@ import javax.inject.Inject class SettingsViewModel @Inject constructor( private val useCase: ConfigSettingsUseCase, private val resourceProvider: ResourceProvider, - dialogProvider: DialogProvider, val sharedPrefsDataStoreWrapper: SharedPrefsDataStoreWrapper, + dialogProvider: DialogProvider, + navigationProvider: NavigationProvider, ) : ViewModel(), DialogProvider by dialogProvider, - ResourceProvider by resourceProvider { - - val automaticBackupLocation = useCase.automaticBackupLocation - - val isWriteSecureSettingsPermissionGranted: StateFlow = - useCase.isWriteSecureSettingsGranted - .stateIn(viewModelScope, SharingStarted.Eagerly, true) - - val isShizukuInstalled: StateFlow = - useCase.isShizukuInstalled - .stateIn(viewModelScope, SharingStarted.Eagerly, true) - - val isShizukuStarted: StateFlow = - useCase.isShizukuStarted - .stateIn(viewModelScope, SharingStarted.Eagerly, true) - - val isShizukuPermissionGranted: StateFlow = - useCase.isShizukuPermissionGranted - .stateIn(viewModelScope, SharingStarted.Eagerly, true) - - val rerouteKeyEvents: StateFlow = useCase.rerouteKeyEvents - .stateIn(viewModelScope, SharingStarted.Lazily, false) - - val isCompatibleImeChosen: StateFlow = useCase.isCompatibleImeChosen - .stateIn(viewModelScope, SharingStarted.Lazily, false) - - val isCompatibleImeEnabled: StateFlow = useCase.isCompatibleImeEnabled - .stateIn(viewModelScope, SharingStarted.Lazily, false) + ResourceProvider by resourceProvider, + NavigationProvider by navigationProvider, + DefaultOptionsSettingsCallback { val defaultLongPressDelay: Flow = useCase.defaultLongPressDelay val defaultDoublePressDelay: Flow = useCase.defaultDoublePressDelay @@ -69,7 +53,65 @@ class SettingsViewModel @Inject constructor( val defaultVibrateDuration: Flow = useCase.defaultVibrateDuration val defaultRepeatRate: Flow = useCase.defaultRepeatRate + val mainScreenState: StateFlow = combine( + useCase.theme, + useCase.getPreference(Keys.log), + useCase.automaticBackupLocation, + useCase.getPreference(Keys.forceVibrate), + useCase.getPreference(Keys.hideHomeScreenAlerts), + useCase.getPreference(Keys.showDeviceDescriptors), + ) { values -> + MainSettingsState( + theme = values[0] as Theme, + loggingEnabled = values[1] as Boolean? ?: false, + autoBackupLocation = values[2] as String?, + forceVibrate = values[3] as Boolean? ?: false, + hideHomeScreenAlerts = values[4] as Boolean? ?: false, + showDeviceDescriptors = values[5] as Boolean? ?: false, + ) + }.stateIn(viewModelScope, SharingStarted.Lazily, MainSettingsState()) + + val defaultSettingsScreenState: StateFlow = combine( + useCase.getPreference(Keys.defaultLongPressDelay), + useCase.getPreference(Keys.defaultDoublePressDelay), + useCase.getPreference(Keys.defaultVibrateDuration), + useCase.getPreference(Keys.defaultRepeatDelay), + useCase.getPreference(Keys.defaultRepeatRate), + useCase.getPreference(Keys.defaultSequenceTriggerTimeout), + ) { values -> + DefaultSettingsState( + longPressDelay = values[0] ?: PreferenceDefaults.LONG_PRESS_DELAY, + defaultLongPressDelay = PreferenceDefaults.LONG_PRESS_DELAY, + doublePressDelay = values[1] ?: PreferenceDefaults.DOUBLE_PRESS_DELAY, + defaultDoublePressDelay = PreferenceDefaults.DOUBLE_PRESS_DELAY, + vibrateDuration = values[2] ?: PreferenceDefaults.VIBRATION_DURATION, + defaultVibrateDuration = PreferenceDefaults.VIBRATION_DURATION, + repeatDelay = values[3] ?: PreferenceDefaults.REPEAT_DELAY, + defaultRepeatDelay = PreferenceDefaults.REPEAT_DELAY, + repeatRate = values[4] ?: PreferenceDefaults.REPEAT_RATE, + defaultRepeatRate = PreferenceDefaults.REPEAT_RATE, + sequenceTriggerTimeout = values[5] ?: PreferenceDefaults.SEQUENCE_TRIGGER_TIMEOUT, + defaultSequenceTriggerTimeout = PreferenceDefaults.SEQUENCE_TRIGGER_TIMEOUT, + ) + }.stateIn(viewModelScope, SharingStarted.Lazily, DefaultSettingsState()) + + val automaticChangeImeSettingsState: StateFlow = combine( + useCase.getPreference(Keys.showToastWhenAutoChangingIme), + useCase.getPreference(Keys.changeImeOnInputFocus), + useCase.getPreference(Keys.changeImeOnDeviceConnect), + useCase.getPreference(Keys.toggleKeyboardOnToggleKeymaps), + ) { values -> + AutomaticChangeImeSettingsState( + showToastWhenAutoChangingIme = values[0] + ?: PreferenceDefaults.SHOW_TOAST_WHEN_AUTO_CHANGE_IME, + changeImeOnInputFocus = values[1] ?: PreferenceDefaults.CHANGE_IME_ON_INPUT_FOCUS, + changeImeOnDeviceConnect = values[2] ?: false, + toggleKeyboardOnToggleKeymaps = values[3] ?: false, + ) + }.stateIn(viewModelScope, SharingStarted.Lazily, AutomaticChangeImeSettingsState()) + fun setAutomaticBackupLocation(uri: String) = useCase.setAutomaticBackupLocation(uri) + fun disableAutomaticBackup() = useCase.disableAutomaticBackup() fun onChooseCompatibleImeClick() { @@ -102,7 +144,10 @@ class SettingsViewModel @Inject constructor( val soundFiles = useCase.getSoundFiles() if (soundFiles.isEmpty()) { - showDialog("no sound files", DialogModel.Toast(getString(R.string.toast_no_sound_files))) + showDialog( + "no sound files", + DialogModel.Toast(getString(R.string.toast_no_sound_files)), + ) return@launch } @@ -182,17 +227,6 @@ class SettingsViewModel @Inject constructor( } } - fun onCreateBackupFileActivityNotFound() { - val dialog = DialogModel.Alert( - message = getString(R.string.dialog_message_no_app_found_to_create_file), - positiveButtonText = getString(R.string.pos_ok), - ) - - viewModelScope.launch { - showDialog("create_document_activity_not_found", dialog) - } - } - fun onResetAllSettingsClick() { val dialog = DialogModel.Alert( title = getString(R.string.dialog_title_reset_settings), @@ -209,4 +243,193 @@ class SettingsViewModel @Inject constructor( } } } + + fun onRequestRootClick() { + useCase.requestRootPermission() + } + + fun onProModeClick() { + viewModelScope.launch { + navigate("pro_mode_settings", NavDestination.ProMode) + } + } + + fun onAutomaticChangeImeClick() { + viewModelScope.launch { + navigate("automatic_change_ime", NavDestination.AutomaticChangeImeSettings) + } + } + + fun onBackClick() { + viewModelScope.launch { + popBackStack() + } + } + + fun onThemeSelected(theme: Theme) { + viewModelScope.launch { + useCase.setPreference(Keys.darkTheme, theme.value.toString()) + } + } + + fun onPauseResumeNotificationClick() { + onNotificationSettingsClick(NotificationController.CHANNEL_TOGGLE_KEY_MAPS) + } + + fun onDefaultOptionsClick() { + viewModelScope.launch { + navigate("default_options", NavDestination.DefaultOptionsSettings) + } + } + + override fun onLongPressDelayChanged(delay: Int) { + viewModelScope.launch { + useCase.setPreference(Keys.defaultLongPressDelay, delay) + } + } + + override fun onDoublePressDelayChanged(delay: Int) { + viewModelScope.launch { + useCase.setPreference(Keys.defaultDoublePressDelay, delay) + } + } + + override fun onVibrateDurationChanged(duration: Int) { + viewModelScope.launch { + useCase.setPreference(Keys.defaultVibrateDuration, duration) + } + } + + override fun onRepeatDelayChanged(delay: Int) { + viewModelScope.launch { + useCase.setPreference(Keys.defaultRepeatDelay, delay) + } + } + + override fun onRepeatRateChanged(rate: Int) { + viewModelScope.launch { + useCase.setPreference(Keys.defaultRepeatRate, rate) + } + } + + override fun onSequenceTriggerTimeoutChanged(timeout: Int) { + viewModelScope.launch { + useCase.setPreference(Keys.defaultSequenceTriggerTimeout, timeout) + } + } + + fun onAutomaticChangeImeSettingsClick() { + viewModelScope.launch { + navigate("automatic_change_ime", NavDestination.AutomaticChangeImeSettings) + } + } + + fun onShowToastWhenAutoChangingImeToggled(enabled: Boolean) { + viewModelScope.launch { + useCase.setPreference(Keys.showToastWhenAutoChangingIme, enabled) + } + } + + fun onChangeImeOnInputFocusToggled(enabled: Boolean) { + viewModelScope.launch { + useCase.setPreference(Keys.changeImeOnInputFocus, enabled) + } + } + + fun onChangeImeOnDeviceConnectToggled(enabled: Boolean) { + viewModelScope.launch { + useCase.setPreference(Keys.changeImeOnDeviceConnect, enabled) + } + } + + fun onDevicesThatChangeImeClick() { + chooseDevicesForPreference(Keys.devicesThatChangeIme) + } + + fun onToggleKeyboardOnToggleKeymapsToggled(enabled: Boolean) { + viewModelScope.launch { + useCase.setPreference(Keys.toggleKeyboardOnToggleKeymaps, enabled) + } + } + + fun onShowToggleKeyboardNotificationClick() { + onNotificationSettingsClick(NotificationController.CHANNEL_TOGGLE_KEYBOARD) + } + + fun onForceVibrateToggled(enabled: Boolean) { + viewModelScope.launch { + useCase.setPreference(Keys.forceVibrate, enabled) + } + } + + fun onLoggingToggled(enabled: Boolean) { + viewModelScope.launch { + useCase.setPreference(Keys.log, enabled) + } + } + + fun onViewLogClick() { + viewModelScope.launch { + navigate("log", NavDestination.Log) + } + } + + fun onHideHomeScreenAlertsToggled(enabled: Boolean) { + viewModelScope.launch { + useCase.setPreference(Keys.hideHomeScreenAlerts, enabled) + } + } + + fun onShowDeviceDescriptorsToggled(enabled: Boolean) { + viewModelScope.launch { + useCase.setPreference(Keys.showDeviceDescriptors, enabled) + } + } + + private fun onNotificationSettingsClick(channel: String) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + !useCase.isNotificationsPermissionGranted() + ) { + useCase.requestNotificationsPermission() + return + } + + useCase.openNotificationChannelSettings(channel) + } } + +data class MainSettingsState( + val theme: Theme = Theme.AUTO, + val autoBackupLocation: String? = null, + val forceVibrate: Boolean = false, + val loggingEnabled: Boolean = false, + val hideHomeScreenAlerts: Boolean = false, + val showDeviceDescriptors: Boolean = false, +) + +data class DefaultSettingsState( + val longPressDelay: Int = PreferenceDefaults.LONG_PRESS_DELAY, + val defaultLongPressDelay: Int = PreferenceDefaults.LONG_PRESS_DELAY, + + val doublePressDelay: Int = PreferenceDefaults.DOUBLE_PRESS_DELAY, + val defaultDoublePressDelay: Int = PreferenceDefaults.DOUBLE_PRESS_DELAY, + + val vibrateDuration: Int = PreferenceDefaults.VIBRATION_DURATION, + val defaultVibrateDuration: Int = PreferenceDefaults.VIBRATION_DURATION, + + val repeatDelay: Int = PreferenceDefaults.REPEAT_DELAY, + val defaultRepeatDelay: Int = PreferenceDefaults.REPEAT_DELAY, + + val repeatRate: Int = PreferenceDefaults.REPEAT_RATE, + val defaultRepeatRate: Int = PreferenceDefaults.REPEAT_RATE, + + val sequenceTriggerTimeout: Int = PreferenceDefaults.SEQUENCE_TRIGGER_TIMEOUT, + val defaultSequenceTriggerTimeout: Int = PreferenceDefaults.SEQUENCE_TRIGGER_TIMEOUT, +) + +data class AutomaticChangeImeSettingsState( + val showToastWhenAutoChangingIme: Boolean = PreferenceDefaults.SHOW_TOAST_WHEN_AUTO_CHANGE_IME, + val changeImeOnInputFocus: Boolean = PreferenceDefaults.CHANGE_IME_ON_INPUT_FOCUS, + val changeImeOnDeviceConnect: Boolean = false, + val toggleKeyboardOnToggleKeymaps: Boolean = false, +) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/ShizukuSettingsFragment.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/ShizukuSettingsFragment.kt deleted file mode 100644 index d55063e4f9..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/ShizukuSettingsFragment.kt +++ /dev/null @@ -1,137 +0,0 @@ -package io.github.sds100.keymapper.base.settings - -import android.os.Bundle -import android.view.View -import androidx.lifecycle.Lifecycle -import androidx.preference.Preference -import androidx.preference.isEmpty -import io.github.sds100.keymapper.base.R -import io.github.sds100.keymapper.base.utils.ui.drawable -import io.github.sds100.keymapper.base.utils.ui.launchRepeatOnLifecycle -import io.github.sds100.keymapper.base.utils.ui.str -import io.github.sds100.keymapper.base.utils.ui.viewLifecycleScope -import io.github.sds100.keymapper.system.url.UrlUtils -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.launchIn - -class ShizukuSettingsFragment : BaseSettingsFragment() { - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - preferenceManager.preferenceDataStore = viewModel.sharedPrefsDataStoreWrapper - setPreferencesFromResource(R.xml.preferences_empty, rootKey) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - viewLifecycleScope.launchWhenResumed { - if (preferenceScreen.isEmpty()) { - populatePreferenceScreen() - } - } - } - - private fun populatePreferenceScreen() = preferenceScreen.apply { - // summary - Preference(requireContext()).apply { - setSummary(R.string.summary_pref_category_shizuku_follow_steps) - addPreference(this) - } - - // install shizuku - Preference(requireContext()).apply { - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.isShizukuInstalled.collectLatest { isInstalled -> - if (isInstalled) { - icon = drawable(R.drawable.ic_outline_check_circle_outline_24) - setTitle(R.string.title_pref_grant_shizuku_install_app_installed) - isEnabled = false - } else { - icon = drawable(R.drawable.ic_baseline_error_outline_24) - setTitle(R.string.title_pref_grant_shizuku_install_app_not_installed) - isEnabled = true - } - } - } - - isSingleLineTitle = false - - setOnPreferenceClickListener { - if (!viewModel.isShizukuInstalled.value) { - viewModel.downloadShizuku() - } - - true - } - - addPreference(this) - } - - // start shizuku - Preference(requireContext()).apply { - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - combine( - viewModel.isShizukuInstalled, - viewModel.isShizukuStarted, - ) { isInstalled, isStarted -> - isEnabled = isInstalled - - if (isStarted) { - icon = drawable(R.drawable.ic_outline_check_circle_outline_24) - setTitle(R.string.title_pref_grant_shizuku_started) - } else { - icon = drawable(R.drawable.ic_baseline_error_outline_24) - setTitle(R.string.title_pref_grant_shizuku_not_started) - } - }.launchIn(this) - } - - isSingleLineTitle = false - - setOnPreferenceClickListener { - if (!viewModel.isShizukuStarted.value) { - viewModel.openShizukuApp() - } - - true - } - - addPreference(this) - } - - // grant shizuku permission - Preference(requireContext()).apply { - viewLifecycleOwner.launchRepeatOnLifecycle(Lifecycle.State.RESUMED) { - combine( - viewModel.isShizukuStarted, - viewModel.isShizukuPermissionGranted, - ) { isStarted, isGranted -> - isEnabled = isStarted - - if (isGranted) { - icon = drawable(R.drawable.ic_outline_check_circle_outline_24) - setTitle(R.string.title_pref_grant_shizuku_granted) - } else { - icon = drawable(R.drawable.ic_baseline_error_outline_24) - setTitle(R.string.title_pref_grant_shizuku_not_granted) - } - }.launchIn(this) - } - - isSingleLineTitle = false - - setOnPreferenceClickListener { - if (viewModel.isShizukuPermissionGranted.value) { - UrlUtils.openUrl(requireContext(), str(R.string.url_shizuku_setting_benefits)) - } else { - viewModel.requestShizukuPermission() - } - - true - } - - addPreference(this) - } - } -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/Theme.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/Theme.kt new file mode 100644 index 0000000000..0ef9163046 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/settings/Theme.kt @@ -0,0 +1,7 @@ +package io.github.sds100.keymapper.base.settings + +enum class Theme(val value: Int) { + DARK(0), + LIGHT(1), + AUTO(2), +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/settings/ThemeUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/settings/ThemeUtils.kt deleted file mode 100644 index 63d6110cf6..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/settings/ThemeUtils.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.sds100.keymapper.base.settings - -object ThemeUtils { - const val DARK = 0 - const val LIGHT = 1 - const val AUTO = 2 - - val THEMES = arrayOf(LIGHT, DARK, AUTO) -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/CreateKeyMapShortcutActivity.kt b/base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutActivity.kt similarity index 98% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/CreateKeyMapShortcutActivity.kt rename to base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutActivity.kt index 66b9d37eed..7448cc5099 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/CreateKeyMapShortcutActivity.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutActivity.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.base.keymaps +package io.github.sds100.keymapper.base.shortcuts import android.os.Bundle import androidx.activity.SystemBarStyle @@ -16,7 +16,7 @@ import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase import io.github.sds100.keymapper.base.system.accessibility.AccessibilityServiceAdapterImpl import io.github.sds100.keymapper.base.system.permissions.RequestPermissionDelegate -import io.github.sds100.keymapper.base.trigger.RecordTriggerController +import io.github.sds100.keymapper.base.trigger.RecordTriggerControllerImpl import io.github.sds100.keymapper.base.utils.ui.ResourceProviderImpl import io.github.sds100.keymapper.base.utils.ui.launchRepeatOnLifecycle import io.github.sds100.keymapper.common.BuildConfigProvider @@ -42,7 +42,7 @@ class CreateKeyMapShortcutActivity : AppCompatActivity() { lateinit var onboardingUseCase: OnboardingUseCase @Inject - lateinit var recordTriggerController: RecordTriggerController + lateinit var recordTriggerController: RecordTriggerControllerImpl @Inject lateinit var notificationReceiverAdapter: NotificationReceiverAdapterImpl diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/CreateKeyMapShortcutScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutScreen.kt similarity index 98% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/CreateKeyMapShortcutScreen.kt rename to base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutScreen.kt index 366566c45a..134cf79287 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/CreateKeyMapShortcutScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutScreen.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.base.keymaps +package io.github.sds100.keymapper.base.shortcuts import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedContent @@ -42,6 +42,9 @@ import io.github.sds100.keymapper.base.constraints.ConstraintMode import io.github.sds100.keymapper.base.groups.GroupBreadcrumbRow import io.github.sds100.keymapper.base.groups.GroupListItemModel import io.github.sds100.keymapper.base.groups.GroupRow +import io.github.sds100.keymapper.base.home.KeyMapAppBarState +import io.github.sds100.keymapper.base.home.KeyMapList +import io.github.sds100.keymapper.base.home.KeyMapListState import io.github.sds100.keymapper.base.trigger.KeyMapListItemModel import io.github.sds100.keymapper.base.trigger.TriggerError import io.github.sds100.keymapper.base.utils.ui.UnsavedChangesDialog diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/CreateKeyMapShortcutUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutUseCase.kt similarity index 94% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/CreateKeyMapShortcutUseCase.kt rename to base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutUseCase.kt index 563b7bff63..5eaf510603 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/CreateKeyMapShortcutUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutUseCase.kt @@ -1,17 +1,17 @@ -package io.github.sds100.keymapper.base.keymaps +package io.github.sds100.keymapper.base.shortcuts import android.content.Intent import android.graphics.drawable.Drawable import androidx.core.os.bundleOf +import dagger.hilt.android.scopes.ViewModelScoped import io.github.sds100.keymapper.base.R -import io.github.sds100.keymapper.base.utils.ui.ResourceProvider import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.system.apps.AppShortcutAdapter import javax.inject.Inject +@ViewModelScoped class CreateKeyMapShortcutUseCaseImpl @Inject constructor( private val appShortcutAdapter: AppShortcutAdapter, - private val resourceProvider: ResourceProvider, ) : CreateKeyMapShortcutUseCase { companion object { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/CreateKeyMapShortcutViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutViewModel.kt similarity index 91% rename from base/src/main/java/io/github/sds100/keymapper/base/keymaps/CreateKeyMapShortcutViewModel.kt rename to base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutViewModel.kt index 086e330619..bb05d142ee 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/keymaps/CreateKeyMapShortcutViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/shortcuts/CreateKeyMapShortcutViewModel.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.base.keymaps +package io.github.sds100.keymapper.base.shortcuts import android.content.Intent import android.graphics.Color @@ -15,6 +15,13 @@ import io.github.sds100.keymapper.base.constraints.ConstraintErrorSnapshot import io.github.sds100.keymapper.base.constraints.ConstraintMode import io.github.sds100.keymapper.base.constraints.ConstraintUiHelper import io.github.sds100.keymapper.base.groups.GroupListItemModel +import io.github.sds100.keymapper.base.home.KeyMapAppBarState +import io.github.sds100.keymapper.base.home.KeyMapGroup +import io.github.sds100.keymapper.base.home.KeyMapListItemCreator +import io.github.sds100.keymapper.base.home.KeyMapListState +import io.github.sds100.keymapper.base.home.ListKeyMapsUseCase +import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapState +import io.github.sds100.keymapper.base.trigger.ConfigTriggerUseCase import io.github.sds100.keymapper.base.trigger.KeyMapListItemModel import io.github.sds100.keymapper.base.trigger.TriggerErrorSnapshot import io.github.sds100.keymapper.base.utils.ui.ResourceProvider @@ -35,7 +42,8 @@ import javax.inject.Inject @HiltViewModel class CreateKeyMapShortcutViewModel @Inject constructor( - private val config: ConfigKeyMapUseCase, + private val configKeyMapState: ConfigKeyMapState, + private val configTrigger: ConfigTriggerUseCase, private val listKeyMaps: ListKeyMapsUseCase, private val createKeyMapShortcut: CreateKeyMapShortcutUseCase, private val resourceProvider: ResourceProvider, @@ -156,10 +164,10 @@ class CreateKeyMapShortcutViewModel @Inject constructor( if (state.keyMaps !is State.Data) return@launch - config.loadKeyMap(uid) - config.setTriggerFromOtherAppsEnabled(true) + configKeyMapState.loadKeyMap(uid) + configTrigger.setTriggerFromOtherAppsEnabled(true) - val keyMapState = config.keyMap.first() + val keyMapState = configKeyMapState.keyMap.first() if (keyMapState !is State.Data) return@launch @@ -212,7 +220,7 @@ class CreateKeyMapShortcutViewModel @Inject constructor( icon = icon, ) - config.save() + configKeyMapState.save() _returnIntentResult.emit(intent) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/sorting/comparators/KeyMapOptionsComparator.kt b/base/src/main/java/io/github/sds100/keymapper/base/sorting/comparators/KeyMapOptionsComparator.kt index 448e860b59..b91819ea80 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/sorting/comparators/KeyMapOptionsComparator.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/sorting/comparators/KeyMapOptionsComparator.kt @@ -21,7 +21,6 @@ class KeyMapOptionsComparator( keyMap, otherKeyMap, { it.vibrate }, - { it.trigger.screenOffTrigger }, { it.trigger.triggerFromOtherApps }, { it.showToast }, ) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/AccessibilityServiceAdapterImpl.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/AccessibilityServiceAdapterImpl.kt index bb08d91614..941a9a9f3f 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/AccessibilityServiceAdapterImpl.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/AccessibilityServiceAdapterImpl.kt @@ -2,7 +2,6 @@ package io.github.sds100.keymapper.base.system.accessibility import android.content.ActivityNotFoundException import android.content.Context -import android.content.Intent import android.database.ContentObserver import android.net.Uri import android.os.Build @@ -14,11 +13,11 @@ import io.github.sds100.keymapper.common.BuildConfigProvider import io.github.sds100.keymapper.common.KeyMapperClassProvider import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult +import io.github.sds100.keymapper.common.utils.SettingsUtils import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.common.utils.onFailure import io.github.sds100.keymapper.common.utils.onSuccess import io.github.sds100.keymapper.system.JobSchedulerHelper -import io.github.sds100.keymapper.system.SettingsUtils import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceAdapter import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceEvent import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceState @@ -178,17 +177,7 @@ class AccessibilityServiceAdapterImpl @Inject constructor( private fun launchAccessibilitySettings(): Boolean { try { - val settingsIntent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS) - - settingsIntent.addFlags( - Intent.FLAG_ACTIVITY_NEW_TASK - or Intent.FLAG_ACTIVITY_CLEAR_TASK - or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS - // Add this flag so user only has to press back once. - or Intent.FLAG_ACTIVITY_NO_HISTORY, - ) - - ctx.startActivity(settingsIntent) + SettingsUtils.launchSettingsScreen(ctx, Settings.ACTION_ACCESSIBILITY_SETTINGS) return true } catch (e: ActivityNotFoundException) { @@ -197,15 +186,12 @@ class AccessibilityServiceAdapterImpl @Inject constructor( } private suspend fun disableServiceSuspend() { - // disableSelf method only exists in 7.0.0+ - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - send(AccessibilityServiceEvent.DisableService).onSuccess { - Timber.i("Disabling service by calling disableSelf()") + send(AccessibilityServiceEvent.DisableService).onSuccess { + Timber.i("Disabling service by calling disableSelf()") - return - }.onFailure { - Timber.i("Failed to disable service by calling disableSelf()") - } + return + }.onFailure { + Timber.i("Failed to disable service by calling disableSelf()") } if (permissionAdapter.isGranted(Permission.WRITE_SECURE_SETTINGS)) { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt index a39025a574..6fab500a93 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityService.kt @@ -11,7 +11,6 @@ import android.graphics.Path import android.graphics.Point import android.os.Build import android.view.KeyEvent -import android.view.MotionEvent import android.view.accessibility.AccessibilityEvent import androidx.core.content.getSystemService import androidx.core.os.bundleOf @@ -22,21 +21,16 @@ import androidx.savedstate.SavedStateRegistry import androidx.savedstate.SavedStateRegistryController import androidx.savedstate.SavedStateRegistryOwner import dagger.hilt.android.AndroidEntryPoint -import io.github.sds100.keymapper.api.IKeyEventRelayServiceCallback import io.github.sds100.keymapper.base.R -import io.github.sds100.keymapper.base.system.inputmethod.ImeInputEventInjectorImpl -import io.github.sds100.keymapper.base.trigger.KeyEventDetectionSource -import io.github.sds100.keymapper.common.utils.InputEventType +import io.github.sds100.keymapper.base.input.InputEventDetectionSource +import io.github.sds100.keymapper.common.utils.InputEventAction import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.MathUtils import io.github.sds100.keymapper.common.utils.PinchScreenType import io.github.sds100.keymapper.common.utils.Success -import io.github.sds100.keymapper.system.devices.InputDeviceUtils -import io.github.sds100.keymapper.system.inputevents.MyKeyEvent -import io.github.sds100.keymapper.system.inputevents.MyMotionEvent +import io.github.sds100.keymapper.system.inputevents.KMKeyEvent import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter -import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapperImpl import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update @@ -50,11 +44,6 @@ abstract class BaseAccessibilityService : IAccessibilityService, SavedStateRegistryOwner { - companion object { - - private const val CALLBACK_ID_ACCESSIBILITY_SERVICE = "accessibility_service" - } - @Inject lateinit var accessibilityServiceAdapter: AccessibilityServiceAdapterImpl @@ -142,53 +131,6 @@ abstract class BaseAccessibilityService : } } - private val relayServiceCallback: IKeyEventRelayServiceCallback = - object : IKeyEventRelayServiceCallback.Stub() { - override fun onKeyEvent(event: KeyEvent?): Boolean { - event ?: return false - - val device = event.device?.let { InputDeviceUtils.createInputDeviceInfo(it) } - - return getController() - ?.onKeyEventFromIme( - MyKeyEvent( - keyCode = event.keyCode, - action = event.action, - metaState = event.metaState, - scanCode = event.scanCode, - device = device, - repeatCount = event.repeatCount, - source = event.source, - ), - ) ?: false - } - - override fun onMotionEvent(event: MotionEvent?): Boolean { - event ?: return false - - return getController() - ?.onMotionEventFromIme(MyMotionEvent.fromMotionEvent(event)) - ?: return false - } - } - - val keyEventRelayServiceWrapper: KeyEventRelayServiceWrapperImpl by lazy { - KeyEventRelayServiceWrapperImpl( - ctx = this, - id = CALLBACK_ID_ACCESSIBILITY_SERVICE, - servicePackageName = packageName, - callback = relayServiceCallback, - ) - } - - val imeInputEventInjector by lazy { - ImeInputEventInjectorImpl( - this, - keyEventRelayService = keyEventRelayServiceWrapper, - inputMethodAdapter = inputMethodAdapter, - ) - } - override val lifecycle: Lifecycle get() = lifecycleRegistry @@ -211,8 +153,6 @@ abstract class BaseAccessibilityService : } } } - - keyEventRelayServiceWrapper.onCreate() } override fun onServiceConnected() { @@ -271,8 +211,6 @@ abstract class BaseAccessibilityService : .unregisterFingerprintGestureCallback(fingerprintGestureCallback) } - keyEventRelayServiceWrapper.onDestroy() - Timber.i("Accessibility service: onDestroy") super.onDestroy() @@ -306,23 +244,11 @@ abstract class BaseAccessibilityService : override fun onKeyEvent(event: KeyEvent?): Boolean { event ?: return super.onKeyEvent(event) - val device = if (event.device == null) { - null - } else { - InputDeviceUtils.createInputDeviceInfo(event.device) - } + val kmKeyEvent = KMKeyEvent.fromAndroidKeyEvent(event) ?: return false return getController()?.onKeyEvent( - MyKeyEvent( - keyCode = event.keyCode, - action = event.action, - metaState = event.metaState, - scanCode = event.scanCode, - device = device, - repeatCount = event.repeatCount, - source = event.source, - ), - KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + kmKeyEvent, + InputEventDetectionSource.ACCESSIBILITY_SERVICE, ) ?: false } @@ -358,7 +284,7 @@ abstract class BaseAccessibilityService : } } - override fun tapScreen(x: Int, y: Int, inputEventType: InputEventType): KMResult<*> { + override fun tapScreen(x: Int, y: Int, inputEventAction: InputEventAction): KMResult<*> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { val duration = 1L // ms @@ -368,7 +294,7 @@ abstract class BaseAccessibilityService : val strokeDescription = when { - inputEventType == InputEventType.DOWN && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> + inputEventAction == InputEventAction.DOWN && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> StrokeDescription( path, 0, @@ -376,7 +302,7 @@ abstract class BaseAccessibilityService : true, ) - inputEventType == InputEventType.UP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> + inputEventAction == InputEventAction.UP && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> StrokeDescription( path, 59999, @@ -412,7 +338,7 @@ abstract class BaseAccessibilityService : yEnd: Int, fingerCount: Int, duration: Int, - inputEventType: InputEventType, + inputEventAction: InputEventAction, ): KMResult<*> { // virtual distance between fingers on multitouch gestures val fingerGestureDistance = 10L @@ -514,7 +440,7 @@ abstract class BaseAccessibilityService : pinchType: PinchScreenType, fingerCount: Int, duration: Int, - inputEventType: InputEventType, + inputEventAction: InputEventAction, ): KMResult<*> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (fingerCount >= GestureDescription.getMaxStrokeCount()) { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt index 121afbadaa..c3ef0be0f0 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/BaseAccessibilityServiceController.kt @@ -4,24 +4,25 @@ import android.accessibilityservice.AccessibilityServiceInfo import android.content.res.Configuration import android.os.Build import android.view.KeyEvent +import android.view.MotionEvent import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo import androidx.lifecycle.lifecycleScope +import io.github.sds100.keymapper.api.IKeyEventRelayServiceCallback import io.github.sds100.keymapper.base.actions.ActionData import io.github.sds100.keymapper.base.actions.PerformActionsUseCaseImpl import io.github.sds100.keymapper.base.actions.TestActionEvent import io.github.sds100.keymapper.base.constraints.DetectConstraintsUseCaseImpl +import io.github.sds100.keymapper.base.detection.DetectKeyMapsUseCaseImpl +import io.github.sds100.keymapper.base.detection.KeyMapDetectionController +import io.github.sds100.keymapper.base.detection.TriggerKeyMapFromOtherAppsController +import io.github.sds100.keymapper.base.input.InputEventDetectionSource +import io.github.sds100.keymapper.base.input.InputEventHub import io.github.sds100.keymapper.base.keymaps.FingerprintGesturesSupportedUseCase import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase import io.github.sds100.keymapper.base.keymaps.TriggerKeyMapEvent -import io.github.sds100.keymapper.base.keymaps.detection.DetectKeyMapsUseCaseImpl -import io.github.sds100.keymapper.base.keymaps.detection.DetectScreenOffKeyEventsController -import io.github.sds100.keymapper.base.keymaps.detection.DpadMotionEventTracker -import io.github.sds100.keymapper.base.keymaps.detection.KeyMapController -import io.github.sds100.keymapper.base.keymaps.detection.TriggerKeyMapFromOtherAppsController -import io.github.sds100.keymapper.base.reroutekeyevents.RerouteKeyEventsController -import io.github.sds100.keymapper.base.trigger.KeyEventDetectionSource -import io.github.sds100.keymapper.base.trigger.RecordTriggerEvent +import io.github.sds100.keymapper.base.promode.SystemBridgeSetupAssistantController +import io.github.sds100.keymapper.base.trigger.RecordTriggerController import io.github.sds100.keymapper.common.utils.firstBlocking import io.github.sds100.keymapper.common.utils.hasFlag import io.github.sds100.keymapper.common.utils.minusFlag @@ -30,14 +31,9 @@ import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.PreferenceDefaults import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceEvent -import io.github.sds100.keymapper.system.devices.DevicesAdapter -import io.github.sds100.keymapper.system.inputevents.InputEventUtils -import io.github.sds100.keymapper.system.inputevents.MyKeyEvent -import io.github.sds100.keymapper.system.inputevents.MyMotionEvent -import io.github.sds100.keymapper.system.root.SuAdapter -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay +import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent +import io.github.sds100.keymapper.system.inputevents.KMKeyEvent +import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapper import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow @@ -53,51 +49,47 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update -import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import timber.log.Timber abstract class BaseAccessibilityServiceController( private val service: BaseAccessibilityService, - private val rerouteKeyEventsControllerFactory: RerouteKeyEventsController.Factory, private val accessibilityNodeRecorderFactory: AccessibilityNodeRecorder.Factory, private val performActionsUseCaseFactory: PerformActionsUseCaseImpl.Factory, private val detectKeyMapsUseCaseFactory: DetectKeyMapsUseCaseImpl.Factory, private val detectConstraintsUseCaseFactory: DetectConstraintsUseCaseImpl.Factory, private val fingerprintGesturesSupported: FingerprintGesturesSupportedUseCase, private val pauseKeyMapsUseCase: PauseKeyMapsUseCase, - private val devicesAdapter: DevicesAdapter, - private val suAdapter: SuAdapter, private val settingsRepository: PreferenceRepository, + private val keyEventRelayServiceWrapper: KeyEventRelayServiceWrapper, + private val inputEventHub: InputEventHub, + private val recordTriggerController: RecordTriggerController, + private val setupAssistantControllerFactory: SystemBridgeSetupAssistantController.Factory, ) { companion object { - - /** - * How long should the accessibility service record a trigger in seconds. - */ - private const val RECORD_TRIGGER_TIMER_LENGTH = 5 private const val DEFAULT_NOTIFICATION_TIMEOUT = 200L + private const val CALLBACK_ID_ACCESSIBILITY_SERVICE = "accessibility_service" } private val performActionsUseCase = performActionsUseCaseFactory.create( accessibilityService = service, - imeInputEventInjector = service.imeInputEventInjector, ) private val detectKeyMapsUseCase = detectKeyMapsUseCaseFactory.create( accessibilityService = service, coroutineScope = service.lifecycleScope, - imeInputEventInjector = service.imeInputEventInjector, ) val detectConstraintsUseCase = detectConstraintsUseCaseFactory.create(service) - val keyMapController = KeyMapController( + val keyMapDetectionController = KeyMapDetectionController( service.lifecycleScope, detectKeyMapsUseCase, performActionsUseCase, detectConstraintsUseCase, + inputEventHub, + pauseKeyMapsUseCase, + recordTriggerController, ) val triggerKeyMapFromOtherAppsController = TriggerKeyMapFromOtherAppsController( @@ -107,40 +99,19 @@ abstract class BaseAccessibilityServiceController( detectConstraintsUseCase, ) - val rerouteKeyEventsController = rerouteKeyEventsControllerFactory.create( - service.lifecycleScope, - service.imeInputEventInjector, - ) - val accessibilityNodeRecorder = accessibilityNodeRecorderFactory.create(service) - private val detectScreenOffKeyEventsController = DetectScreenOffKeyEventsController( - suAdapter, - devicesAdapter, - ) { event -> - if (!isPaused.value) { - withContext(Dispatchers.Main.immediate) { - keyMapController.onKeyEvent(event) - } + private val setupAssistantController: SystemBridgeSetupAssistantController? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + setupAssistantControllerFactory.create(service.lifecycleScope, service) + } else { + null } - } - - private var recordingTriggerJob: Job? = null - - private val recordingTrigger: Boolean - get() = recordingTriggerJob != null && recordingTriggerJob?.isActive == true - - private val recordDpadMotionEventTracker: DpadMotionEventTracker = - DpadMotionEventTracker() val isPaused: StateFlow = pauseKeyMapsUseCase.isPaused .stateIn(service.lifecycleScope, SharingStarted.Eagerly, false) - private val screenOffTriggersEnabled: StateFlow = - detectKeyMapsUseCase.detectScreenOffTriggers - .stateIn(service.lifecycleScope, SharingStarted.Eagerly, false) - private val changeImeOnInputFocusFlow: StateFlow = settingsRepository .get(Keys.changeImeOnInputFocus) @@ -190,9 +161,28 @@ abstract class BaseAccessibilityServiceController( private val inputEvents: SharedFlow = service.accessibilityServiceAdapter.eventsToService + private val outputEvents: MutableSharedFlow = service.accessibilityServiceAdapter.eventReceiver + private val relayServiceCallback: IKeyEventRelayServiceCallback = + object : IKeyEventRelayServiceCallback.Stub() { + override fun onKeyEvent(event: KeyEvent?): Boolean { + event ?: return false + + val kmKeyEvent = KMKeyEvent.fromAndroidKeyEvent(event) ?: return false + return onKeyEventFromIme(kmKeyEvent) + } + + override fun onMotionEvent(event: MotionEvent?): Boolean { + event ?: return false + + val gamePadEvent = KMGamePadEvent.fromMotionEvent(event) + ?: return false + return onMotionEventFromIme(gamePadEvent) + } + } + init { serviceFlags.onEach { flags -> // check that it isn't null because this can only be called once the service is bound @@ -236,20 +226,9 @@ abstract class BaseAccessibilityServiceController( } pauseKeyMapsUseCase.isPaused.distinctUntilChanged().onEach { - keyMapController.reset() triggerKeyMapFromOtherAppsController.reset() }.launchIn(service.lifecycleScope) - detectKeyMapsUseCase.isScreenOn.onEach { isScreenOn -> - if (!isScreenOn) { - if (screenOffTriggersEnabled.value) { - detectScreenOffKeyEventsController.startListening(service.lifecycleScope) - } - } else { - detectScreenOffKeyEventsController.stopListening() - } - }.launchIn(service.lifecycleScope) - inputEvents.onEach { onEventFromUi(it) }.launchIn(service.lifecycleScope) @@ -358,95 +337,38 @@ abstract class BaseAccessibilityServiceController( denyFingerprintGestureDetection() } } - } - open fun onDestroy() { - accessibilityNodeRecorder.teardown() - } + keyEventRelayServiceWrapper.registerClient( + CALLBACK_ID_ACCESSIBILITY_SERVICE, + relayServiceCallback, + ) - open fun onConfigurationChanged(newConfig: Configuration) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + setupAssistantController?.onServiceConnected() + } } - /** - * Returns an MyKeyEvent which is either the same or more unique - */ - private fun getUniqueEvent(event: MyKeyEvent): MyKeyEvent { - // Guard to ignore processing when not applicable - if (event.keyCode != KeyEvent.KEYCODE_UNKNOWN) return event + open fun onDestroy() { + keyMapDetectionController.teardown() + keyEventRelayServiceWrapper.unregisterClient(CALLBACK_ID_ACCESSIBILITY_SERVICE) + accessibilityNodeRecorder.teardown() - // Don't offset negative values - val scanCodeOffset: Int = if (event.scanCode >= 0) { - InputEventUtils.KEYCODE_TO_SCANCODE_OFFSET - } else { - 0 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + setupAssistantController?.teardown() } + } - val eventProxy = event.copy( - // Fallback to scanCode when keyCode is unknown as it's typically more unique - // Add offset to go past possible keyCode values - keyCode = event.scanCode + scanCodeOffset, - ) - - return eventProxy + open fun onConfigurationChanged(newConfig: Configuration) { } fun onKeyEvent( - event: MyKeyEvent, - detectionSource: KeyEventDetectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, + event: KMKeyEvent, + detectionSource: InputEventDetectionSource = InputEventDetectionSource.ACCESSIBILITY_SERVICE, ): Boolean { - val detailedLogInfo = event.toString() - - if (recordingTrigger) { - if (event.action == KeyEvent.ACTION_DOWN) { - Timber.d("Recorded key ${KeyEvent.keyCodeToString(event.keyCode)}, $detailedLogInfo") - - val uniqueEvent: MyKeyEvent = getUniqueEvent(event) - - service.lifecycleScope.launch { - outputEvents.emit( - RecordTriggerEvent.RecordedTriggerKey( - uniqueEvent.keyCode, - uniqueEvent.device, - detectionSource, - ), - ) - } - } - - return true - } - - if (isPaused.value) { - when (event.action) { - KeyEvent.ACTION_DOWN -> Timber.d("Down ${KeyEvent.keyCodeToString(event.keyCode)} - not filtering because paused, $detailedLogInfo") - KeyEvent.ACTION_UP -> Timber.d("Up ${KeyEvent.keyCodeToString(event.keyCode)} - not filtering because paused, $detailedLogInfo") - } - } else { - try { - var consume: Boolean - val uniqueEvent: MyKeyEvent = getUniqueEvent(event) - - consume = keyMapController.onKeyEvent(uniqueEvent) - - if (!consume) { - consume = rerouteKeyEventsController.onKeyEvent(uniqueEvent) - } - - when (uniqueEvent.action) { - KeyEvent.ACTION_DOWN -> Timber.d("Down ${KeyEvent.keyCodeToString(uniqueEvent.keyCode)} - consumed: $consume, $detailedLogInfo") - KeyEvent.ACTION_UP -> Timber.d("Up ${KeyEvent.keyCodeToString(uniqueEvent.keyCode)} - consumed: $consume, $detailedLogInfo") - } - - return consume - } catch (e: Exception) { - Timber.e(e) - } - } - - return false + return inputEventHub.onInputEvent(event, detectionSource) } - fun onKeyEventFromIme(event: MyKeyEvent): Boolean { + fun onKeyEventFromIme(event: KMKeyEvent): Boolean { /* Issue #850 If a volume key is sent while the phone is ringing or in a call @@ -455,65 +377,29 @@ abstract class BaseAccessibilityServiceController( before returning the UP key event. */ if (event.action == KeyEvent.ACTION_UP && (event.keyCode == KeyEvent.KEYCODE_VOLUME_UP || event.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN)) { - onKeyEvent( + inputEventHub.onInputEvent( event.copy(action = KeyEvent.ACTION_DOWN), - detectionSource = KeyEventDetectionSource.INPUT_METHOD, + detectionSource = InputEventDetectionSource.INPUT_METHOD, ) } - return onKeyEvent( - event, - detectionSource = KeyEventDetectionSource.INPUT_METHOD, - ) + return inputEventHub.onInputEvent(event, InputEventDetectionSource.INPUT_METHOD) } - fun onMotionEventFromIme(event: MyMotionEvent): Boolean { - if (isPaused.value) { - return false - } - - if (recordingTrigger) { - val dpadKeyEvents = recordDpadMotionEventTracker.convertMotionEvent(event) - - var consume = false - - for (keyEvent in dpadKeyEvents) { - if (keyEvent.action == KeyEvent.ACTION_DOWN) { - Timber.d("Recorded motion event ${KeyEvent.keyCodeToString(keyEvent.keyCode)}") - - service.lifecycleScope.launch { - outputEvents.emit( - RecordTriggerEvent.RecordedTriggerKey( - keyEvent.keyCode, - keyEvent.device, - KeyEventDetectionSource.INPUT_METHOD, - ), - ) - } - } - - // Consume the key event if it is an DOWN or UP. - consume = true - } - - if (consume) { - return true - } - } - - try { - val consume = keyMapController.onMotionEvent(event) - - return consume - } catch (e: Exception) { - Timber.e(e) - return false - } + fun onMotionEventFromIme(event: KMGamePadEvent): Boolean { + return inputEventHub.onInputEvent( + event, + detectionSource = InputEventDetectionSource.INPUT_METHOD, + ) } open fun onAccessibilityEvent(event: AccessibilityEvent) { accessibilityNodeRecorder.onAccessibilityEvent(event) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + setupAssistantController?.onAccessibilityEvent(event) + } + if (changeImeOnInputFocusFlow.value) { val focussedNode = service.findFocussedNode(AccessibilityNodeInfo.FOCUS_INPUT) @@ -533,7 +419,7 @@ abstract class BaseAccessibilityServiceController( } fun onFingerprintGesture(type: FingerprintGestureType) { - keyMapController.onFingerprintGesture(type) + keyMapDetectionController.onFingerprintGesture(type) } private fun triggerKeyMapFromIntent(uid: String) { @@ -542,27 +428,8 @@ abstract class BaseAccessibilityServiceController( open fun onEventFromUi(event: AccessibilityServiceEvent) { Timber.d("Service received event from UI: $event") - when (event) { - is RecordTriggerEvent.StartRecordingTrigger -> - if (!recordingTrigger) { - recordDpadMotionEventTracker.reset() - recordingTriggerJob = recordTriggerJob() - } - - is RecordTriggerEvent.StopRecordingTrigger -> { - val wasRecordingTrigger = recordingTrigger - - recordingTriggerJob?.cancel() - recordingTriggerJob = null - recordDpadMotionEventTracker.reset() - - if (wasRecordingTrigger) { - service.lifecycleScope.launch { - outputEvents.emit(RecordTriggerEvent.OnStoppedRecordingTrigger) - } - } - } + when (event) { is TestActionEvent -> service.lifecycleScope.launch { performActionsUseCase.perform( event.action, @@ -598,19 +465,6 @@ abstract class BaseAccessibilityServiceController( } } - private fun recordTriggerJob() = service.lifecycleScope.launch { - repeat(RECORD_TRIGGER_TIMER_LENGTH) { iteration -> - if (isActive) { - val timeLeft = RECORD_TRIGGER_TIMER_LENGTH - iteration - outputEvents.emit(RecordTriggerEvent.OnIncrementRecordTriggerTimer(timeLeft)) - - delay(1000) - } - } - - outputEvents.emit(RecordTriggerEvent.OnStoppedRecordingTrigger) - } - private fun requestFingerprintGestureDetection() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { Timber.d("Accessibility service: request fingerprint gesture detection") diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/IAccessibilityService.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/IAccessibilityService.kt index c62a1c8b05..3021d7f6a0 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/IAccessibilityService.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/accessibility/IAccessibilityService.kt @@ -2,7 +2,7 @@ package io.github.sds100.keymapper.base.system.accessibility import android.os.Build import androidx.annotation.RequiresApi -import io.github.sds100.keymapper.common.utils.InputEventType +import io.github.sds100.keymapper.common.utils.InputEventAction import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.PinchScreenType import kotlinx.coroutines.flow.Flow @@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.Flow interface IAccessibilityService { fun doGlobalAction(action: Int): KMResult<*> - fun tapScreen(x: Int, y: Int, inputEventType: InputEventType): KMResult<*> + fun tapScreen(x: Int, y: Int, inputEventAction: InputEventAction): KMResult<*> fun swipeScreen( xStart: Int, @@ -19,7 +19,7 @@ interface IAccessibilityService { yEnd: Int, fingerCount: Int, duration: Int, - inputEventType: InputEventType, + inputEventAction: InputEventAction, ): KMResult<*> fun pinchScreen( @@ -29,7 +29,7 @@ interface IAccessibilityService { pinchType: PinchScreenType, fingerCount: Int, duration: Int, - inputEventType: InputEventType, + inputEventAction: InputEventAction, ): KMResult<*> val isFingerprintGestureDetectionAvailable: Boolean diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/inputmethod/ImeInputEventInjector.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/inputmethod/ImeInputEventInjector.kt index db7abdfcde..97d4506bb9 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/inputmethod/ImeInputEventInjector.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/inputmethod/ImeInputEventInjector.kt @@ -1,49 +1,30 @@ package io.github.sds100.keymapper.base.system.inputmethod -import android.content.Context -import android.content.Intent -import android.os.Build import android.os.SystemClock import android.view.KeyCharacterMap import android.view.KeyEvent -import io.github.sds100.keymapper.common.utils.InputEventType -import io.github.sds100.keymapper.system.inputevents.InputEventInjector -import io.github.sds100.keymapper.system.inputmethod.InputKeyModel +import io.github.sds100.keymapper.base.input.InjectKeyEventModel import io.github.sds100.keymapper.system.inputmethod.InputMethodAdapter import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapper import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton /** * This class handles communicating with the Key Mapper input method services * so key events and text can be inputted. */ -class ImeInputEventInjectorImpl( - private val ctx: Context, +@Singleton +class ImeInputEventInjectorImpl @Inject constructor( private val keyEventRelayService: KeyEventRelayServiceWrapper, private val inputMethodAdapter: InputMethodAdapter, ) : ImeInputEventInjector { - companion object { - // DON'T CHANGE THESE!!! - private const val KEY_MAPPER_INPUT_METHOD_ACTION_INPUT_DOWN_UP = - "io.github.sds100.keymapper.inputmethod.ACTION_INPUT_DOWN_UP" - private const val KEY_MAPPER_INPUT_METHOD_ACTION_INPUT_DOWN = - "io.github.sds100.keymapper.inputmethod.ACTION_INPUT_DOWN" - private const val KEY_MAPPER_INPUT_METHOD_ACTION_INPUT_UP = - "io.github.sds100.keymapper.inputmethod.ACTION_INPUT_UP" - private const val KEY_MAPPER_INPUT_METHOD_ACTION_TEXT = - "io.github.sds100.keymapper.inputmethod.ACTION_INPUT_TEXT" - - private const val KEY_MAPPER_INPUT_METHOD_EXTRA_KEY_EVENT = - "io.github.sds100.keymapper.inputmethod.EXTRA_KEY_EVENT" - private const val KEY_MAPPER_INPUT_METHOD_EXTRA_TEXT = - "io.github.sds100.keymapper.inputmethod.EXTRA_TEXT" - private const val CALLBACK_ID_INPUT_METHOD = "input_method" } - override suspend fun inputKeyEvent(model: InputKeyModel) { - Timber.d("Inject key event with input method ${KeyEvent.keyCodeToString(model.keyCode)}, $model") + override fun inputKeyEvent(event: InjectKeyEventModel) { + Timber.d("Inject key event with input method $event") val imePackageName = inputMethodAdapter.chosenIme.value?.packageName @@ -52,79 +33,11 @@ class ImeInputEventInjectorImpl( return } - // Only use the new key event relay service on Android 14+ because - // it introduced a 1 second delay for broadcasts to context-registered - // receivers. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - inputKeyEventRelayService(model, imePackageName) - } else { - inputKeyEventBroadcast(model, imePackageName) - } - } - - private fun inputKeyEventBroadcast(model: InputKeyModel, imePackageName: String) { - val intentAction = when (model.inputType) { - InputEventType.DOWN -> KEY_MAPPER_INPUT_METHOD_ACTION_INPUT_DOWN - InputEventType.DOWN_UP -> KEY_MAPPER_INPUT_METHOD_ACTION_INPUT_DOWN_UP - InputEventType.UP -> KEY_MAPPER_INPUT_METHOD_ACTION_INPUT_UP - } - - Intent(intentAction).apply { - setPackage(imePackageName) - - val action = when (model.inputType) { - InputEventType.DOWN, InputEventType.DOWN_UP -> KeyEvent.ACTION_DOWN - InputEventType.UP -> KeyEvent.ACTION_UP - } - - val eventTime = SystemClock.uptimeMillis() - - val keyEvent = createInjectedKeyEvent(eventTime, action, model) - - putExtra(KEY_MAPPER_INPUT_METHOD_EXTRA_KEY_EVENT, keyEvent) - - ctx.sendBroadcast(this) - } - } - - private fun inputKeyEventRelayService(model: InputKeyModel, imePackageName: String) { - val eventTime = SystemClock.uptimeMillis() - - when (model.inputType) { - InputEventType.DOWN_UP -> { - val downKeyEvent = createInjectedKeyEvent(eventTime, KeyEvent.ACTION_DOWN, model) - keyEventRelayService.sendKeyEvent( - downKeyEvent, - imePackageName, - CALLBACK_ID_INPUT_METHOD, - ) - - val upKeyEvent = createInjectedKeyEvent(eventTime, KeyEvent.ACTION_UP, model) - keyEventRelayService.sendKeyEvent( - upKeyEvent, - imePackageName, - CALLBACK_ID_INPUT_METHOD, - ) - } - - InputEventType.DOWN -> { - val downKeyEvent = createInjectedKeyEvent(eventTime, KeyEvent.ACTION_DOWN, model) - keyEventRelayService.sendKeyEvent( - downKeyEvent, - imePackageName, - CALLBACK_ID_INPUT_METHOD, - ) - } - - InputEventType.UP -> { - val upKeyEvent = createInjectedKeyEvent(eventTime, KeyEvent.ACTION_UP, model) - keyEventRelayService.sendKeyEvent( - upKeyEvent, - imePackageName, - CALLBACK_ID_INPUT_METHOD, - ) - } - } + keyEventRelayService.sendKeyEvent( + event.toAndroidKeyEvent(), + imePackageName, + CALLBACK_ID_INPUT_METHOD, + ) } override fun inputText(text: String) { @@ -137,25 +50,11 @@ class ImeInputEventInjectorImpl( return } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - inputTextRelayService(text, imePackageName) - } else { - inputTextBroadcast(text, imePackageName) - } - } - - private fun inputTextBroadcast(text: String, imePackageName: String) { - Intent(KEY_MAPPER_INPUT_METHOD_ACTION_TEXT).apply { - setPackage(imePackageName) - - putExtra(KEY_MAPPER_INPUT_METHOD_EXTRA_TEXT, text) - ctx.sendBroadcast(this) - } + inputTextRelayService(text, imePackageName) } private fun inputTextRelayService(text: String, imePackageName: String) { // taken from android.view.inputmethod.BaseInputConnection.sendCurrentText() - if (text.isEmpty()) { return } @@ -201,6 +100,7 @@ class ImeInputEventInjectorImpl( } } -interface ImeInputEventInjector : InputEventInjector { +interface ImeInputEventInjector { fun inputText(text: String) + fun inputKeyEvent(event: InjectKeyEventModel) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/navigation/OpenMenuHelper.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/navigation/OpenMenuHelper.kt index 78120f505d..58e4f2663d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/navigation/OpenMenuHelper.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/navigation/OpenMenuHelper.kt @@ -1,27 +1,19 @@ package io.github.sds100.keymapper.base.system.navigation +import android.view.InputDevice import android.view.KeyEvent import androidx.core.view.accessibility.AccessibilityNodeInfoCompat +import io.github.sds100.keymapper.base.input.InjectKeyEventModel +import io.github.sds100.keymapper.base.input.InputEventHub import io.github.sds100.keymapper.base.system.accessibility.AccessibilityNodeAction import io.github.sds100.keymapper.base.system.accessibility.IAccessibilityService -import io.github.sds100.keymapper.common.utils.InputEventType import io.github.sds100.keymapper.common.utils.KMResult -import io.github.sds100.keymapper.common.utils.firstBlocking import io.github.sds100.keymapper.common.utils.success -import io.github.sds100.keymapper.system.inputevents.InputEventInjector -import io.github.sds100.keymapper.system.inputmethod.InputKeyModel -import io.github.sds100.keymapper.system.permissions.Permission -import io.github.sds100.keymapper.system.permissions.PermissionAdapter -import io.github.sds100.keymapper.system.root.SuAdapter -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch +import io.github.sds100.keymapper.common.utils.then class OpenMenuHelper( - private val suAdapter: SuAdapter, private val accessibilityService: IAccessibilityService, - private val shizukuInputEventInjector: InputEventInjector, - private val permissionAdapter: PermissionAdapter, - private val coroutineScope: CoroutineScope, + private val inputEventHub: InputEventHub, ) { companion object { @@ -30,22 +22,24 @@ class OpenMenuHelper( fun openMenu(): KMResult<*> { when { - permissionAdapter.isGranted(Permission.SHIZUKU) -> { - val inputKeyModel = InputKeyModel( + inputEventHub.isSystemBridgeConnected() -> { + val downEvent = InjectKeyEventModel( keyCode = KeyEvent.KEYCODE_MENU, - inputType = InputEventType.DOWN_UP, + action = KeyEvent.ACTION_DOWN, + metaState = 0, + scanCode = 0, + deviceId = -1, + repeatCount = 0, + source = InputDevice.SOURCE_UNKNOWN, ) - coroutineScope.launch { - shizukuInputEventInjector.inputKeyEvent(inputKeyModel) - } + val upEvent = downEvent.copy(action = KeyEvent.ACTION_UP) - return success() + return inputEventHub.injectKeyEventAsync(downEvent).then { + inputEventHub.injectKeyEventAsync(upEvent) + } } - suAdapter.isGranted.firstBlocking() -> - return suAdapter.execute("input keyevent ${KeyEvent.KEYCODE_MENU}\n") - else -> { accessibilityService.performActionOnNode({ it.contentDescription == OVERFLOW_MENU_CONTENT_DESCRIPTION }) { AccessibilityNodeAction( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/AndroidNotificationAdapter.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/AndroidNotificationAdapter.kt index dbae0c9ee3..ec320f82e5 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/AndroidNotificationAdapter.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/AndroidNotificationAdapter.kt @@ -1,40 +1,86 @@ package io.github.sds100.keymapper.base.system.notifications +import android.Manifest import android.app.NotificationChannel import android.app.PendingIntent +import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageManager import android.os.Build +import android.provider.Settings +import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat +import androidx.core.app.RemoteInput +import androidx.core.content.ContextCompat import com.google.android.material.color.DynamicColors import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.utils.ui.color import io.github.sds100.keymapper.common.KeyMapperClassProvider +import io.github.sds100.keymapper.common.notifications.KMNotificationAction import io.github.sds100.keymapper.system.notifications.NotificationAdapter import io.github.sds100.keymapper.system.notifications.NotificationChannelModel -import io.github.sds100.keymapper.system.notifications.NotificationIntentType import io.github.sds100.keymapper.system.notifications.NotificationModel +import io.github.sds100.keymapper.system.notifications.NotificationRemoteInput import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @Singleton class AndroidNotificationAdapter @Inject constructor( - @ApplicationContext private val context: Context, + @ApplicationContext private val ctx: Context, private val coroutineScope: CoroutineScope, private val classProvider: KeyMapperClassProvider, ) : NotificationAdapter { - private val ctx = context.applicationContext private val manager: NotificationManagerCompat = NotificationManagerCompat.from(ctx) - override val onNotificationActionClick = MutableSharedFlow() + override val onNotificationActionClick = MutableSharedFlow() + override val onNotificationRemoteInput = MutableSharedFlow() + + private val broadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + context ?: return + intent ?: return + + onReceiveNotificationActionIntent(intent) + + // dismiss the notification drawer after tapping on the notification. This is deprecated on S+ + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS).apply { + context.sendBroadcast(this) + } + } + } + } + + init { + val intentFilter = IntentFilter().apply { + for (entry in KMNotificationAction.IntentAction.entries) { + addAction(entry.name) + } + } + ContextCompat.registerReceiver( + ctx, + broadcastReceiver, + intentFilter, + ContextCompat.RECEIVER_EXPORTED, + ) + } override fun showNotification(notification: NotificationModel) { + if (ActivityCompat.checkSelfPermission(ctx, Manifest.permission.POST_NOTIFICATIONS) + != PackageManager.PERMISSION_GRANTED + ) { + return + } + val builder = NotificationCompat.Builder(ctx, notification.channel).apply { if (!DynamicColors.isDynamicColorAvailable()) { color = ctx.color(R.color.md_theme_secondary) @@ -59,21 +105,41 @@ class AndroidNotificationAdapter @Inject constructor( setStyle(NotificationCompat.BigTextStyle()) } + if (notification.timeout != null) { + this.setTimeoutAfter(notification.timeout!!) + } + + if (notification.showIndeterminateProgress) { + setProgress(1, 1, true) + } + setSmallIcon(notification.icon) if (!notification.showOnLockscreen) { setVisibility(NotificationCompat.VISIBILITY_SECRET) } - for (action in notification.actions) { - addAction( - NotificationCompat.Action( - 0, - action.text, - createActionIntent(action.intentType), - ), + for ((action, label) in notification.actions) { + val pendingIntent = createActionIntent(action) + + val notificationAction = NotificationCompat.Action.Builder( + 0, + label, + pendingIntent, ) + + if (action is KMNotificationAction.RemoteInput) { + val remoteInput = RemoteInput.Builder(action.key) + .setLabel(label) + .build() + + notificationAction.addRemoteInput(remoteInput) + } + + addAction(notificationAction.build()) } + + setSilent(notification.silent) } manager.notify(notification.id, builder.build()) @@ -84,52 +150,123 @@ class AndroidNotificationAdapter @Inject constructor( } override fun createChannel(channel: NotificationChannelModel) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - manager.createNotificationChannel( - NotificationChannel( - channel.id, - channel.name, - channel.importance, - ), - ) - } + val androidChannel = NotificationChannel( + channel.id, + channel.name, + channel.importance, + ) + + manager.createNotificationChannel(androidChannel) } override fun deleteChannel(channelId: String) { manager.deleteNotificationChannel(channelId) } + override fun openChannelSettings(channelId: String) { + Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply { + putExtra(Settings.EXTRA_APP_PACKAGE, ctx.packageName) + putExtra(Settings.EXTRA_CHANNEL_ID, channelId) + + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + ctx.startActivity(this) + } + } + fun onReceiveNotificationActionIntent(intent: Intent) { - val actionId = intent.action ?: return + val intentAction = + KMNotificationAction.IntentAction.entries.single { it.name == intent.action } + + Timber.d("Received notification click: actionId=$intentAction") + + // Check if there's RemoteInput data + val remoteInputBundle = RemoteInput.getResultsFromIntent(intent) + if (remoteInputBundle != null) { + for (key in remoteInputBundle.keySet()) { + val text = remoteInputBundle.getCharSequence(key)?.toString() + + if (!text.isNullOrEmpty()) { + coroutineScope.launch { + onNotificationRemoteInput.emit(NotificationRemoteInput(intentAction, text)) + } + + return + } + } + } + + // No text input, treat as regular action click coroutineScope.launch { - onNotificationActionClick.emit(actionId) + onNotificationActionClick.emit(intentAction) } } - private fun createActionIntent(intentType: NotificationIntentType): PendingIntent { - when (intentType) { - is NotificationIntentType.Broadcast -> { - val intent = Intent(ctx, NotificationClickReceiver::class.java).apply { - action = intentType.action - } + private fun createActionIntent( + notificationAction: KMNotificationAction, + ): PendingIntent { + return when (notificationAction) { + KMNotificationAction.Activity.AccessibilitySettings -> createActivityPendingIntent( + Settings.ACTION_ACCESSIBILITY_SETTINGS, + ) - return PendingIntent.getBroadcast(ctx, 0, intent, PendingIntent.FLAG_IMMUTABLE) - } + is KMNotificationAction.Activity.MainActivity -> createMainActivityPendingIntent( + notificationAction.action, + ) - is NotificationIntentType.MainActivity -> { - val intent = Intent(ctx, classProvider.getMainActivity()).apply { - action = intentType.customIntentAction ?: Intent.ACTION_MAIN - } + is KMNotificationAction.Broadcast -> createBroadcastPendingIntent(notificationAction.intentAction.name) + is KMNotificationAction.RemoteInput -> createRemoteInputPendingIntent(notificationAction.intentAction.name) + } + } - return PendingIntent.getActivity(ctx, 0, intent, PendingIntent.FLAG_IMMUTABLE) - } + private fun createRemoteInputPendingIntent(action: String): PendingIntent { + val intent = Intent(action).apply { + setPackage(ctx.packageName) + } - is NotificationIntentType.Activity -> { - val intent = Intent(intentType.action) + return PendingIntent.getBroadcast( + ctx, + 0, + intent, + PendingIntent.FLAG_MUTABLE, + ) + } - return PendingIntent.getActivity(ctx, 0, intent, PendingIntent.FLAG_IMMUTABLE) - } + private fun createBroadcastPendingIntent(action: String): PendingIntent { + val intent = Intent(action).apply { + setPackage(ctx.packageName) + } + + return PendingIntent.getBroadcast( + ctx, + 0, + intent, + PendingIntent.FLAG_IMMUTABLE, + ) + } + + private fun createActivityPendingIntent(action: String): PendingIntent { + val intent = Intent(action) + + return PendingIntent.getActivity( + ctx, + 0, + intent, + PendingIntent.FLAG_IMMUTABLE, + ) + } + + private fun createMainActivityPendingIntent(action: String?): PendingIntent { + val intent = Intent(ctx, classProvider.getMainActivity()).apply { + this.action = action ?: Intent.ACTION_MAIN } + + return PendingIntent.getActivity( + ctx, + 0, + intent, + PendingIntent.FLAG_IMMUTABLE, + ) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/ManageNotificationsUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/ManageNotificationsUseCase.kt index 3d699686ef..9874927eed 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/ManageNotificationsUseCase.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/ManageNotificationsUseCase.kt @@ -1,74 +1,24 @@ package io.github.sds100.keymapper.base.system.notifications -import android.os.Build -import io.github.sds100.keymapper.data.Keys -import io.github.sds100.keymapper.data.repositories.PreferenceRepository +import io.github.sds100.keymapper.common.notifications.KMNotificationAction import io.github.sds100.keymapper.system.notifications.NotificationAdapter import io.github.sds100.keymapper.system.notifications.NotificationChannelModel import io.github.sds100.keymapper.system.notifications.NotificationModel +import io.github.sds100.keymapper.system.notifications.NotificationRemoteInput import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.system.permissions.PermissionAdapter -import io.github.sds100.keymapper.system.root.SuAdapter import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map import javax.inject.Inject class ManageNotificationsUseCaseImpl @Inject constructor( - private val preferences: PreferenceRepository, private val notificationAdapter: NotificationAdapter, - private val suAdapter: SuAdapter, private val permissionAdapter: PermissionAdapter, ) : ManageNotificationsUseCase { - override val showImePickerNotification: Flow = - combine( - suAdapter.isGranted, - preferences.get(Keys.showImePickerNotification), - ) { hasRootPermission, show -> - when { - Build.VERSION.SDK_INT < Build.VERSION_CODES.O -> show ?: false - - /* - always show the notification on Oreo+ because the system/user controls - whether notifications are shown. - */ - Build.VERSION.SDK_INT == Build.VERSION_CODES.O -> true - - /* - This needs root permission on API 27 and 28 - */ - ( - Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1 || - Build.VERSION.SDK_INT == Build.VERSION_CODES.P - ) && - hasRootPermission -> true - - else -> false - } - } - - override val showToggleKeyboardNotification: Flow = - preferences.get(Keys.showToggleKeyboardNotification).map { - // always show the notification on Oreo+ because the system/user controls whether notifications are shown - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - true - } else { - it ?: true - } - } - - override val showToggleMappingsNotification: Flow = - preferences.get(Keys.showToggleKeyMapsNotification).map { - // always show the notification on Oreo+ because the system/user controls whether notifications are shown - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - true - } else { - it ?: true - } - } - - override val onActionClick: Flow = notificationAdapter.onNotificationActionClick + override val onNotificationTextInput: Flow = + notificationAdapter.onNotificationRemoteInput + override val onActionClick: Flow = + notificationAdapter.onNotificationActionClick override fun show(notification: NotificationModel) { notificationAdapter.showNotification(notification) @@ -92,14 +42,13 @@ class ManageNotificationsUseCaseImpl @Inject constructor( } interface ManageNotificationsUseCase { - val showImePickerNotification: Flow - val showToggleKeyboardNotification: Flow - val showToggleMappingsNotification: Flow /** - * The string is the ID of the action. + * Emits text input from notification actions that support RemoteInput. */ - val onActionClick: Flow + val onNotificationTextInput: Flow + + val onActionClick: Flow fun isPermissionGranted(): Boolean fun show(notification: NotificationModel) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationClickReceiver.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationClickReceiver.kt deleted file mode 100644 index 0558a8ad5f..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationClickReceiver.kt +++ /dev/null @@ -1,28 +0,0 @@ -package io.github.sds100.keymapper.base.system.notifications - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.os.Build -import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject - -@AndroidEntryPoint -class NotificationClickReceiver : BroadcastReceiver() { - - @Inject - lateinit var notificationAdapter: AndroidNotificationAdapter - - override fun onReceive(context: Context, intent: Intent?) { - intent ?: return - - notificationAdapter.onReceiveNotificationActionIntent(intent) - - // dismiss the notification drawer after tapping on the notification. This is deprecated on S+ - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { - Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS).apply { - context.sendBroadcast(this) - } - } - } -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt index 4a25132dae..3a730951cc 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/notifications/NotificationController.kt @@ -1,6 +1,6 @@ package io.github.sds100.keymapper.base.system.notifications -import android.provider.Settings +import android.os.Build import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import io.github.sds100.keymapper.base.BaseMainActivity @@ -9,18 +9,18 @@ import io.github.sds100.keymapper.base.keymaps.PauseKeyMapsUseCase import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase import io.github.sds100.keymapper.base.system.accessibility.ControlAccessibilityServiceUseCase import io.github.sds100.keymapper.base.system.inputmethod.ShowHideInputMethodUseCase -import io.github.sds100.keymapper.base.system.inputmethod.ShowInputMethodPickerUseCase import io.github.sds100.keymapper.base.system.inputmethod.ToggleCompatibleImeUseCase import io.github.sds100.keymapper.base.utils.getFullMessage import io.github.sds100.keymapper.base.utils.ui.ResourceProvider -import io.github.sds100.keymapper.common.BuildConfigProvider +import io.github.sds100.keymapper.common.notifications.KMNotificationAction import io.github.sds100.keymapper.common.utils.DefaultDispatcherProvider import io.github.sds100.keymapper.common.utils.DispatcherProvider import io.github.sds100.keymapper.common.utils.onFailure import io.github.sds100.keymapper.common.utils.onSuccess +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceState import io.github.sds100.keymapper.system.notifications.NotificationChannelModel -import io.github.sds100.keymapper.system.notifications.NotificationIntentType import io.github.sds100.keymapper.system.notifications.NotificationModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow @@ -40,30 +40,35 @@ class NotificationController @Inject constructor( private val coroutineScope: CoroutineScope, private val manageNotifications: ManageNotificationsUseCase, private val pauseMappings: PauseKeyMapsUseCase, - private val showImePicker: ShowInputMethodPickerUseCase, private val controlAccessibilityService: ControlAccessibilityServiceUseCase, private val toggleCompatibleIme: ToggleCompatibleImeUseCase, private val hideInputMethod: ShowHideInputMethodUseCase, private val onboardingUseCase: OnboardingUseCase, private val resourceProvider: ResourceProvider, + private val systemBridgeConnectionManager: SystemBridgeConnectionManager, private val dispatchers: DispatcherProvider = DefaultDispatcherProvider(), - private val buildConfigProvider: BuildConfigProvider, ) : ResourceProvider by resourceProvider { companion object { - private const val ID_IME_PICKER = 123 + // private const val ID_IME_PICKER = 123 private const val ID_KEYBOARD_HIDDEN = 747 private const val ID_TOGGLE_MAPPINGS = 231 private const val ID_TOGGLE_KEYBOARD = 143 + const val ID_SETUP_ASSISTANT = 144 + const val ID_SYSTEM_BRIDGE_STATUS = 145 // private const val ID_FEATURE_ASSISTANT_TRIGGER = 900 private const val ID_FEATURE_FLOATING_BUTTONS = 901 - const val CHANNEL_TOGGLE_KEYMAPS = "channel_toggle_remaps" + const val CHANNEL_TOGGLE_KEY_MAPS = "channel_toggle_remaps" + + @Deprecated("Removed in 4.0.0") const val CHANNEL_IME_PICKER = "channel_ime_picker" + const val CHANNEL_KEYBOARD_HIDDEN = "channel_warning_keyboard_hidden" const val CHANNEL_TOGGLE_KEYBOARD = "channel_toggle_keymapper_keyboard" const val CHANNEL_NEW_FEATURES = "channel_new_features" + const val CHANNEL_SETUP_ASSISTANT = "channel_setup_assistant" @Deprecated("Removed in 2.0. This channel shouldn't exist") private const val CHANNEL_ID_WARNINGS = "channel_warnings" @@ -72,31 +77,6 @@ class NotificationController @Inject constructor( private const val CHANNEL_ID_PERSISTENT = "channel_persistent" } - private val actionResumeMappings = - "${buildConfigProvider.packageName}.ACTION_RESUME_MAPPINGS" - - private val actionPauseMappings = "${buildConfigProvider.packageName}.ACTION_PAUSE_MAPPINGS" - - private val actionStartService = - "${buildConfigProvider.packageName}.ACTION_START_ACCESSIBILITY_SERVICE" - - private val actionRestartService = - "${buildConfigProvider.packageName}.ACTION_RESTART_ACCESSIBILITY_SERVICE" - - private val actionStopService = - "${buildConfigProvider.packageName}.ACTION_STOP_ACCESSIBILITY_SERVICE" - - private val actionDismissToggleMappings = - "${buildConfigProvider.packageName}.ACTION_DISMISS_TOGGLE_MAPPINGS" - - private val actionShowImePicker = - "${buildConfigProvider.packageName}.ACTION_SHOW_IME_PICKER" - - private val actionShowKeyboard = "${buildConfigProvider.packageName}.ACTION_SHOW_KEYBOARD" - - private val actionToggleKeyboard = - "${buildConfigProvider.packageName}.ACTION_TOGGLE_KEYBOARD" - /** * Open the app and use the String as the Intent action. */ @@ -109,6 +89,7 @@ class NotificationController @Inject constructor( fun init() { manageNotifications.deleteChannel(CHANNEL_ID_WARNINGS) manageNotifications.deleteChannel(CHANNEL_ID_PERSISTENT) + manageNotifications.deleteChannel(CHANNEL_IME_PICKER) manageNotifications.createChannel( NotificationChannelModel( @@ -119,28 +100,10 @@ class NotificationController @Inject constructor( ) combine( - manageNotifications.showToggleMappingsNotification, controlAccessibilityService.serviceState, pauseMappings.isPaused, - ) { show, serviceState, areMappingsPaused -> - invalidateToggleMappingsNotification(show, serviceState, areMappingsPaused) - }.flowOn(dispatchers.default()).launchIn(coroutineScope) - - manageNotifications.showImePickerNotification.onEach { show -> - if (show) { - manageNotifications.createChannel( - NotificationChannelModel( - id = CHANNEL_IME_PICKER, - name = getString(R.string.notification_channel_ime_picker), - NotificationManagerCompat.IMPORTANCE_MIN, - ), - ) - - manageNotifications.show(imePickerNotification()) - } else { - // don't delete the channel because then the user's notification config is lost - manageNotifications.dismiss(ID_IME_PICKER) - } + ) { serviceState, areMappingsPaused -> + invalidateToggleMappingsNotification(serviceState, areMappingsPaused) }.flowOn(dispatchers.default()).launchIn(coroutineScope) toggleCompatibleIme.sufficientPermissions.onEach { canToggleIme -> @@ -188,26 +151,41 @@ class NotificationController @Inject constructor( } else { manageNotifications.dismiss(ID_KEYBOARD_HIDDEN) } - }.flowOn(dispatchers.default()).launchIn(coroutineScope) + }.launchIn(coroutineScope) manageNotifications.onActionClick.onEach { actionId -> when (actionId) { - actionResumeMappings -> pauseMappings.resume() - actionPauseMappings -> pauseMappings.pause() - actionStartService -> attemptStartAccessibilityService() - actionRestartService -> attemptRestartAccessibilityService() - actionStopService -> controlAccessibilityService.stopService() - - actionDismissToggleMappings -> manageNotifications.dismiss(ID_TOGGLE_MAPPINGS) - actionShowImePicker -> showImePicker.show(fromForeground = false) - actionShowKeyboard -> hideInputMethod.show() - actionToggleKeyboard -> toggleCompatibleIme.toggle().onSuccess { - _showToast.emit(getString(R.string.toast_chose_keyboard, it.label)) - }.onFailure { - _showToast.emit(it.getFullMessage(this)) - } + KMNotificationAction.IntentAction.RESUME_KEY_MAPS -> pauseMappings.resume() + KMNotificationAction.IntentAction.PAUSE_KEY_MAPS -> pauseMappings.pause() + KMNotificationAction.IntentAction.DISMISS_TOGGLE_KEY_MAPS_NOTIFICATION -> manageNotifications.dismiss( + ID_TOGGLE_MAPPINGS, + ) + + KMNotificationAction.IntentAction.STOP_ACCESSIBILITY_SERVICE -> controlAccessibilityService.stopService() + KMNotificationAction.IntentAction.START_ACCESSIBILITY_SERVICE -> attemptStartAccessibilityService() + KMNotificationAction.IntentAction.RESTART_ACCESSIBILITY_SERVICE -> attemptRestartAccessibilityService() + KMNotificationAction.IntentAction.TOGGLE_KEY_MAPPER_IME -> toggleCompatibleIme.toggle() + .onSuccess { + _showToast.emit(getString(R.string.toast_chose_keyboard, it.label)) + }.onFailure { + _showToast.emit(it.getFullMessage(this)) + } + + KMNotificationAction.IntentAction.SHOW_KEYBOARD -> hideInputMethod.show() + else -> Unit // Ignore other notification actions } - }.flowOn(dispatchers.default()).launchIn(coroutineScope) + }.launchIn(coroutineScope) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + coroutineScope.launch { + systemBridgeConnectionManager.connectionState + .collect { connectionState -> + if (connectionState is SystemBridgeConnectionState.Connected) { + showSystemBridgeStartedNotification() + } + } + } + } } fun onOpenApp() { @@ -215,7 +193,6 @@ class NotificationController @Inject constructor( coroutineScope.launch { invalidateToggleMappingsNotification( - show = manageNotifications.showToggleMappingsNotification.first(), serviceState = controlAccessibilityService.serviceState.first(), areMappingsPaused = pauseMappings.isPaused.first(), ) @@ -239,23 +216,17 @@ class NotificationController @Inject constructor( } private fun invalidateToggleMappingsNotification( - show: Boolean, serviceState: AccessibilityServiceState, areMappingsPaused: Boolean, ) { manageNotifications.createChannel( NotificationChannelModel( - id = CHANNEL_TOGGLE_KEYMAPS, + id = CHANNEL_TOGGLE_KEY_MAPS, name = getString(R.string.notification_channel_toggle_mappings), NotificationManagerCompat.IMPORTANCE_MIN, ), ) - if (!show) { - manageNotifications.dismiss(ID_TOGGLE_MAPPINGS) - return - } - when (serviceState) { AccessibilityServiceState.ENABLED -> { if (areMappingsPaused) { @@ -274,35 +245,29 @@ class NotificationController @Inject constructor( } private fun mappingsPausedNotification(): NotificationModel { + // Since Notification trampolines are no longer allowed, the notification + // must directly launch the accessibility settings instead of relaying the request + // through a broadcast receiver that eventually calls the ServiceAdapter. val stopServiceAction = if (controlAccessibilityService.isUserInteractionRequired()) { - NotificationIntentType.Activity(Settings.ACTION_ACCESSIBILITY_SETTINGS) + KMNotificationAction.Activity.AccessibilitySettings } else { - NotificationIntentType.Broadcast(actionStopService) + KMNotificationAction.Broadcast.StopAccessibilityService } return NotificationModel( id = ID_TOGGLE_MAPPINGS, - channel = CHANNEL_TOGGLE_KEYMAPS, + channel = CHANNEL_TOGGLE_KEY_MAPS, title = getString(R.string.notification_keymaps_paused_title), text = getString(R.string.notification_keymaps_paused_text), icon = R.drawable.ic_notification_play, - onClickAction = NotificationIntentType.MainActivity(), + onClickAction = KMNotificationAction.Activity.MainActivity(), showOnLockscreen = true, onGoing = true, priority = NotificationCompat.PRIORITY_MIN, actions = listOf( - NotificationModel.Action( - getString(R.string.notification_action_resume), - NotificationIntentType.Broadcast(actionResumeMappings), - ), - NotificationModel.Action( - getString(R.string.notification_action_dismiss), - NotificationIntentType.Broadcast(actionDismissToggleMappings), - ), - NotificationModel.Action( - getString(R.string.notification_action_stop_acc_service), - stopServiceAction, - ), + KMNotificationAction.Broadcast.ResumeKeyMaps to getString(R.string.notification_action_resume), + KMNotificationAction.Broadcast.DismissToggleKeyMapsNotification to getString(R.string.notification_action_dismiss), + stopServiceAction to getString(R.string.notification_action_stop_acc_service), ), ) } @@ -312,34 +277,25 @@ class NotificationController @Inject constructor( // must directly launch the accessibility settings instead of relaying the request // through a broadcast receiver that eventually calls the ServiceAdapter. val stopServiceAction = if (controlAccessibilityService.isUserInteractionRequired()) { - NotificationIntentType.Activity(Settings.ACTION_ACCESSIBILITY_SETTINGS) + KMNotificationAction.Activity.AccessibilitySettings } else { - NotificationIntentType.Broadcast(actionStopService) + KMNotificationAction.Broadcast.StopAccessibilityService } return NotificationModel( id = ID_TOGGLE_MAPPINGS, - channel = CHANNEL_TOGGLE_KEYMAPS, + channel = CHANNEL_TOGGLE_KEY_MAPS, title = getString(R.string.notification_keymaps_resumed_title), text = getString(R.string.notification_keymaps_resumed_text), icon = R.drawable.ic_notification_pause, - onClickAction = NotificationIntentType.MainActivity(), + onClickAction = KMNotificationAction.Activity.MainActivity(), showOnLockscreen = true, onGoing = true, priority = NotificationCompat.PRIORITY_MIN, actions = listOf( - NotificationModel.Action( - getString(R.string.notification_action_pause), - NotificationIntentType.Broadcast(actionPauseMappings), - ), - NotificationModel.Action( - getString(R.string.notification_action_dismiss), - NotificationIntentType.Broadcast(actionDismissToggleMappings), - ), - NotificationModel.Action( - getString(R.string.notification_action_stop_acc_service), - stopServiceAction, - ), + KMNotificationAction.Broadcast.PauseKeyMaps to getString(R.string.notification_action_pause), + KMNotificationAction.Broadcast.DismissToggleKeyMapsNotification to getString(R.string.notification_action_dismiss), + stopServiceAction to getString(R.string.notification_action_stop_acc_service), ), ) } @@ -348,27 +304,25 @@ class NotificationController @Inject constructor( // Since Notification trampolines are no longer allowed, the notification // must directly launch the accessibility settings instead of relaying the request // through a broadcast receiver that eventually calls the ServiceAdapter. - val onClickAction = if (controlAccessibilityService.isUserInteractionRequired()) { - NotificationIntentType.Activity(Settings.ACTION_ACCESSIBILITY_SETTINGS) + val startServiceAction = if (controlAccessibilityService.isUserInteractionRequired()) { + KMNotificationAction.Activity.AccessibilitySettings } else { - NotificationIntentType.Broadcast(actionStartService) + KMNotificationAction.Broadcast.StartAccessibilityService } return NotificationModel( id = ID_TOGGLE_MAPPINGS, - channel = CHANNEL_TOGGLE_KEYMAPS, + channel = CHANNEL_TOGGLE_KEY_MAPS, title = getString(R.string.notification_accessibility_service_disabled_title), text = getString(R.string.notification_accessibility_service_disabled_text), icon = R.drawable.ic_notification_pause, - onClickAction = onClickAction, + onClickAction = startServiceAction, showOnLockscreen = true, onGoing = true, priority = NotificationCompat.PRIORITY_MIN, actions = listOf( - NotificationModel.Action( - getString(R.string.notification_action_dismiss), - NotificationIntentType.Broadcast(actionDismissToggleMappings), - ), + KMNotificationAction.Broadcast.DismissToggleKeyMapsNotification to getString(R.string.notification_action_dismiss), + ), ) } @@ -377,44 +331,29 @@ class NotificationController @Inject constructor( // Since Notification trampolines are no longer allowed, the notification // must directly launch the accessibility settings instead of relaying the request // through a broadcast receiver that eventually calls the ServiceAdapter. - val onClickAction = if (controlAccessibilityService.isUserInteractionRequired()) { - NotificationIntentType.Activity(Settings.ACTION_ACCESSIBILITY_SETTINGS) + val restartServiceAction = if (controlAccessibilityService.isUserInteractionRequired()) { + KMNotificationAction.Activity.AccessibilitySettings } else { - NotificationIntentType.Broadcast(actionRestartService) + KMNotificationAction.Broadcast.RestartAccessibilityService } return NotificationModel( id = ID_TOGGLE_MAPPINGS, - channel = CHANNEL_TOGGLE_KEYMAPS, + channel = CHANNEL_TOGGLE_KEY_MAPS, title = getString(R.string.notification_accessibility_service_crashed_title), text = getString(R.string.notification_accessibility_service_crashed_text), icon = R.drawable.ic_notification_pause, - onClickAction = onClickAction, + onClickAction = restartServiceAction, showOnLockscreen = true, onGoing = true, priority = NotificationCompat.PRIORITY_MIN, bigTextStyle = true, actions = listOf( - NotificationModel.Action( - getString(R.string.notification_action_restart_accessibility_service), - onClickAction, - ), + restartServiceAction to getString(R.string.notification_action_restart_accessibility_service), ), ) } - private fun imePickerNotification(): NotificationModel = NotificationModel( - id = ID_IME_PICKER, - channel = CHANNEL_IME_PICKER, - title = getString(R.string.notification_ime_persistent_title), - text = getString(R.string.notification_ime_persistent_text), - icon = R.drawable.ic_notification_keyboard, - onClickAction = NotificationIntentType.Broadcast(actionShowImePicker), - showOnLockscreen = false, - onGoing = true, - priority = NotificationCompat.PRIORITY_MIN, - ) - private fun toggleImeNotification(): NotificationModel = NotificationModel( id = ID_TOGGLE_KEYBOARD, channel = CHANNEL_TOGGLE_KEYBOARD, @@ -425,10 +364,7 @@ class NotificationController @Inject constructor( onGoing = true, priority = NotificationCompat.PRIORITY_MIN, actions = listOf( - NotificationModel.Action( - getString(R.string.notification_toggle_keyboard_action), - intentType = NotificationIntentType.Broadcast(actionToggleKeyboard), - ), + KMNotificationAction.Broadcast.TogglerKeyMapperIme to getString(R.string.notification_toggle_keyboard_action), ), ) @@ -438,7 +374,7 @@ class NotificationController @Inject constructor( title = getString(R.string.notification_keyboard_hidden_title), text = getString(R.string.notification_keyboard_hidden_text), icon = R.drawable.ic_notification_keyboard_hide, - onClickAction = NotificationIntentType.Broadcast(actionShowKeyboard), + onClickAction = KMNotificationAction.Broadcast.ShowKeyboard, showOnLockscreen = false, onGoing = true, priority = NotificationCompat.PRIORITY_LOW, @@ -450,11 +386,27 @@ class NotificationController @Inject constructor( title = getString(R.string.notification_floating_buttons_feature_title), text = getString(R.string.notification_floating_buttons_feature_text), icon = R.drawable.outline_bubble_chart_24, - onClickAction = NotificationIntentType.MainActivity(BaseMainActivity.ACTION_USE_FLOATING_BUTTONS), + onClickAction = KMNotificationAction.Activity.MainActivity(BaseMainActivity.ACTION_USE_FLOATING_BUTTONS), priority = NotificationCompat.PRIORITY_LOW, autoCancel = true, onGoing = false, showOnLockscreen = false, bigTextStyle = true, ) + + private fun showSystemBridgeStartedNotification() { + val model = NotificationModel( + id = ID_SYSTEM_BRIDGE_STATUS, + title = getString(R.string.pro_mode_setup_notification_system_bridge_started_title), + text = getString(R.string.pro_mode_setup_notification_system_bridge_started_text), + channel = CHANNEL_SETUP_ASSISTANT, + icon = R.drawable.pro_mode, + onGoing = false, + showOnLockscreen = false, + autoCancel = true, + timeout = 5000, + ) + + manageNotifications.show(model) + } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/permissions/AutoGrantPermissionController.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/permissions/AutoGrantPermissionController.kt index e00c42515d..566e81521d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/permissions/AutoGrantPermissionController.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/permissions/AutoGrantPermissionController.kt @@ -1,43 +1,44 @@ package io.github.sds100.keymapper.base.system.permissions import android.Manifest -import io.github.sds100.keymapper.base.R -import io.github.sds100.keymapper.base.utils.ui.ResourceProvider -import io.github.sds100.keymapper.common.utils.onSuccess import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.system.permissions.PermissionAdapter -import io.github.sds100.keymapper.system.popup.ToastAdapter +import io.github.sds100.keymapper.system.shizuku.ShizukuAdapter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.launchIn +import timber.log.Timber import javax.inject.Inject +import javax.inject.Singleton +@Singleton class AutoGrantPermissionController @Inject constructor( private val coroutineScope: CoroutineScope, private val permissionAdapter: PermissionAdapter, - private val popupAdapter: ToastAdapter, - private val resourceProvider: ResourceProvider, + private val shizukuAdapter: ShizukuAdapter, ) { fun start() { // automatically grant WRITE_SECURE_SETTINGS if Key Mapper has root or shizuku permission - combine( - permissionAdapter.isGrantedFlow(Permission.ROOT), - permissionAdapter.isGrantedFlow(Permission.SHIZUKU), - permissionAdapter.isGrantedFlow(Permission.WRITE_SECURE_SETTINGS), - ) { isRootGranted, isShizukuGranted, isWriteSecureSettingsGranted -> + permissionAdapter.isGrantedFlow(Permission.WRITE_SECURE_SETTINGS) + .flatMapLatest { isGranted -> + if (isGranted) { + emptyFlow() + } else { + combine( + permissionAdapter.isGrantedFlow(Permission.ROOT), + permissionAdapter.isGrantedFlow(Permission.SHIZUKU), + shizukuAdapter.isStarted, + ) { isRootGranted, isShizukuGranted, isShizukuStarted -> - if (!isWriteSecureSettingsGranted && (isRootGranted || isShizukuGranted)) { - permissionAdapter.grant(Manifest.permission.WRITE_SECURE_SETTINGS).onSuccess { - val stringRes = if (isRootGranted) { - R.string.toast_granted_itself_write_secure_settings_with_root - } else { - R.string.toast_granted_itself_write_secure_settings_with_shizuku + if (isRootGranted || (isShizukuGranted && isShizukuStarted)) { + Timber.i("Auto-granting WRITE_SECURE_SETTINGS permission") + permissionAdapter.grant(Manifest.permission.WRITE_SECURE_SETTINGS) + } } - - popupAdapter.show(resourceProvider.getString(stringRes)) } - } - }.launchIn(coroutineScope) + }.launchIn(coroutineScope) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/system/permissions/RequestPermissionDelegate.kt b/base/src/main/java/io/github/sds100/keymapper/base/system/permissions/RequestPermissionDelegate.kt index 4b3c3d8dbd..e4777e2af8 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/system/permissions/RequestPermissionDelegate.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/system/permissions/RequestPermissionDelegate.kt @@ -14,7 +14,6 @@ import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.navigation.NavController -import io.github.sds100.keymapper.base.NavBaseAppDirections import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.utils.ui.str import io.github.sds100.keymapper.common.BuildConfigProvider @@ -23,7 +22,6 @@ import io.github.sds100.keymapper.system.notifications.NotificationReceiverAdapt import io.github.sds100.keymapper.system.permissions.AndroidPermissionAdapter import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.system.shizuku.ShizukuAdapter -import io.github.sds100.keymapper.system.shizuku.ShizukuUtils import io.github.sds100.keymapper.system.url.UrlUtils import splitties.alertdialog.appcompat.messageResource import splitties.alertdialog.appcompat.negativeButton @@ -72,20 +70,12 @@ class RequestPermissionDelegate( Permission.CALL_PHONE -> requestPermissionLauncher.launch(Manifest.permission.CALL_PHONE) Permission.ANSWER_PHONE_CALL -> requestPermissionLauncher.launch(Manifest.permission.ANSWER_PHONE_CALLS) Permission.FIND_NEARBY_DEVICES -> requestPermissionLauncher.launch(Manifest.permission.BLUETOOTH_CONNECT) - Permission.ROOT -> { - require(navController != null) { "nav controller can't be null!" } - requestRootPermission(navController) - } + Permission.ROOT -> requestRootPermission() Permission.IGNORE_BATTERY_OPTIMISATION -> - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - requestIgnoreBatteryOptimisations() - } + requestIgnoreBatteryOptimisations() - Permission.SHIZUKU -> - if (ShizukuUtils.isSupportedForSdkVersion()) { - shizukuAdapter.requestPermission() - } + Permission.SHIZUKU -> shizukuAdapter.requestPermission() Permission.ACCESS_FINE_LOCATION -> requestPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION) @@ -113,10 +103,32 @@ class RequestPermissionDelegate( } private fun requestAccessNotificationPolicy() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val intent = Intent(Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS) + val intent = Intent(Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS) + + intent.addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK + or Intent.FLAG_ACTIVITY_CLEAR_TASK + or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS + // Add this flag so user only has to press back once. + or Intent.FLAG_ACTIVITY_NO_HISTORY, + ) + + try { + startActivityForResultLauncher.launch(intent) + } catch (e: Exception) { + Toast.makeText( + activity, + R.string.error_cant_find_dnd_access_settings, + Toast.LENGTH_SHORT, + ).show() + } + } + + private fun requestWriteSettings() { + Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS).apply { + data = Uri.parse("package:${buildConfigProvider.packageName}") - intent.addFlags( + addFlags( Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS @@ -125,43 +137,17 @@ class RequestPermissionDelegate( ) try { - startActivityForResultLauncher.launch(intent) + activity.startActivity(this) } catch (e: Exception) { Toast.makeText( activity, - R.string.error_cant_find_dnd_access_settings, + R.string.error_cant_find_write_settings_page, Toast.LENGTH_SHORT, ).show() } } } - private fun requestWriteSettings() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS).apply { - data = Uri.parse("package:${buildConfigProvider.packageName}") - - addFlags( - Intent.FLAG_ACTIVITY_NEW_TASK - or Intent.FLAG_ACTIVITY_CLEAR_TASK - or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS - // Add this flag so user only has to press back once. - or Intent.FLAG_ACTIVITY_NO_HISTORY, - ) - - try { - activity.startActivity(this) - } catch (e: Exception) { - Toast.makeText( - activity, - R.string.error_cant_find_write_settings_page, - Toast.LENGTH_SHORT, - ).show() - } - } - } - } - private fun requestWriteSecureSettings() { if (permissionAdapter.isGranted(Permission.SHIZUKU) || permissionAdapter.isGranted(Permission.ROOT) @@ -190,23 +176,18 @@ class RequestPermissionDelegate( } } - private fun requestRootPermission(navController: NavController) { + private fun requestRootPermission() { if (showDialogs) { activity.materialAlertDialog { titleResource = R.string.dialog_title_root_prompt messageResource = R.string.dialog_message_root_prompt setIcon(R.drawable.ic_baseline_warning_24) - okButton { - navController.navigate(NavBaseAppDirections.toSettingsFragment()) - } - + okButton() negativeButton(R.string.neg_cancel) { it.cancel() } show() } - } else { - navController.navigate(NavBaseAppDirections.toSettingsFragment()) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/AssistantTriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/AssistantTriggerKey.kt index 0acb6cde87..a3eba1b2a7 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/AssistantTriggerKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/AssistantTriggerKey.kt @@ -18,10 +18,6 @@ data class AssistantTriggerKey( override val clickType: ClickType, ) : TriggerKey() { - // This is always true for an assistant key event because Key Mapper can't forward the - // assistant event to another app (or can it??). - override val consumeEvent: Boolean = true - override val allowedLongPress: Boolean = false override val allowedDoublePress: Boolean = false diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt index 0244f5266b..cabd960617 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseConfigTriggerViewModel.kt @@ -8,11 +8,12 @@ import androidx.compose.material.icons.rounded.Fingerprint import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.input.InputEventDetectionSource import io.github.sds100.keymapper.base.keymaps.ClickType import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapOptionsViewModel -import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapUseCase -import io.github.sds100.keymapper.base.keymaps.CreateKeyMapShortcutUseCase import io.github.sds100.keymapper.base.keymaps.DisplayKeyMapUseCase import io.github.sds100.keymapper.base.keymaps.FingerprintGesturesSupportedUseCase import io.github.sds100.keymapper.base.keymaps.KeyMap @@ -21,8 +22,8 @@ import io.github.sds100.keymapper.base.onboarding.OnboardingTapTarget import io.github.sds100.keymapper.base.onboarding.OnboardingUseCase import io.github.sds100.keymapper.base.purchasing.ProductId import io.github.sds100.keymapper.base.purchasing.PurchasingManager +import io.github.sds100.keymapper.base.shortcuts.CreateKeyMapShortcutUseCase import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType -import io.github.sds100.keymapper.base.utils.InputEventStrings import io.github.sds100.keymapper.base.utils.navigation.NavigationProvider import io.github.sds100.keymapper.base.utils.ui.CheckBoxListItem import io.github.sds100.keymapper.base.utils.ui.DialogModel @@ -33,6 +34,8 @@ import io.github.sds100.keymapper.base.utils.ui.ResourceProvider import io.github.sds100.keymapper.base.utils.ui.ViewModelHelper import io.github.sds100.keymapper.base.utils.ui.compose.ComposeIconInfo import io.github.sds100.keymapper.base.utils.ui.showDialog +import io.github.sds100.keymapper.common.models.EvdevDeviceInfo +import io.github.sds100.keymapper.common.utils.InputDeviceUtils import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.State @@ -40,9 +43,6 @@ import io.github.sds100.keymapper.common.utils.dataOrNull import io.github.sds100.keymapper.common.utils.ifIsData import io.github.sds100.keymapper.common.utils.mapData import io.github.sds100.keymapper.common.utils.onSuccess -import io.github.sds100.keymapper.system.devices.InputDeviceUtils -import io.github.sds100.keymapper.system.inputevents.InputEventUtils -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -62,10 +62,9 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch abstract class BaseConfigTriggerViewModel( - private val coroutineScope: CoroutineScope, private val onboarding: OnboardingUseCase, - private val config: ConfigKeyMapUseCase, - private val recordTrigger: RecordTriggerUseCase, + private val config: ConfigTriggerUseCase, + private val recordTrigger: RecordTriggerController, private val createKeyMapShortcut: CreateKeyMapShortcutUseCase, private val displayKeyMap: DisplayKeyMapUseCase, private val purchasingManager: PurchasingManager, @@ -74,7 +73,8 @@ abstract class BaseConfigTriggerViewModel( resourceProvider: ResourceProvider, navigationProvider: NavigationProvider, dialogProvider: DialogProvider, -) : ResourceProvider by resourceProvider, +) : ViewModel(), + ResourceProvider by resourceProvider, DialogProvider by dialogProvider, NavigationProvider by navigationProvider { @@ -84,7 +84,7 @@ abstract class BaseConfigTriggerViewModel( } val optionsViewModel = ConfigKeyMapOptionsViewModel( - coroutineScope, + viewModelScope, config, displayKeyMap, createKeyMapShortcut, @@ -140,7 +140,7 @@ abstract class BaseConfigTriggerViewModel( val state: StateFlow> = _state.asStateFlow() val recordTriggerState: StateFlow = recordTrigger.state.stateIn( - coroutineScope, + viewModelScope, SharingStarted.Lazily, RecordTriggerState.Idle, ) @@ -160,7 +160,7 @@ abstract class BaseConfigTriggerViewModel( isChosen, ) }.stateIn( - coroutineScope, + viewModelScope, SharingStarted.Lazily, SetupGuiKeyboardState.DEFAULT, ) @@ -168,7 +168,7 @@ abstract class BaseConfigTriggerViewModel( val triggerKeyOptionsUid = MutableStateFlow(null) val triggerKeyOptionsState: StateFlow = combine(config.keyMap, triggerKeyOptionsUid, transform = ::buildKeyOptionsUiState) - .stateIn(coroutineScope, SharingStarted.Lazily, null) + .stateIn(viewModelScope, SharingStarted.Lazily, null) /** * Check whether the user stopped the trigger recording countdown. This @@ -176,6 +176,7 @@ abstract class BaseConfigTriggerViewModel( * when no buttons are recorded is not shown. */ private var isRecordingCompletionUserInitiated: Boolean = false + private val midDot = getString(R.string.middot) init { val showTapTargetsPairFlow: Flow> = combine( @@ -204,15 +205,18 @@ abstract class BaseConfigTriggerViewModel( showTapTargetsPair.second, ) } - }.launchIn(coroutineScope) + }.launchIn(viewModelScope) - coroutineScope.launch { - recordTrigger.onRecordKey.collectLatest { - onRecordTriggerKey(it) + viewModelScope.launch { + recordTrigger.onRecordKey.collect { key -> + when (key) { + is RecordedKey.EvdevEvent -> onRecordEvdevEvent(key) + is RecordedKey.KeyEvent -> onRecordKeyEvent(key) + } } } - coroutineScope.launch { + viewModelScope.launch { config.keyMap .mapNotNull { it.dataOrNull()?.trigger?.mode } .distinctUntilChanged() @@ -236,12 +240,12 @@ abstract class BaseConfigTriggerViewModel( // reset this field when recording has completed isRecordingCompletionUserInitiated = false - }.launchIn(coroutineScope) + }.launchIn(viewModelScope) } open fun onClickTriggerKeyShortcut(shortcut: TriggerKeyShortcut) { if (shortcut == TriggerKeyShortcut.FINGERPRINT_GESTURE) { - coroutineScope.launch { + viewModelScope.launch { val listItems = listOf( FingerprintGestureType.SWIPE_DOWN to getString(R.string.fingerprint_gesture_down), FingerprintGestureType.SWIPE_UP to getString(R.string.fingerprint_gesture_up), @@ -249,8 +253,9 @@ abstract class BaseConfigTriggerViewModel( FingerprintGestureType.SWIPE_RIGHT to getString(R.string.fingerprint_gesture_right), ) - val selectedType = showDialog("pick_assistant_type", DialogModel.SingleChoice(listItems)) - ?: return@launch + val selectedType = + showDialog("pick_assistant_type", DialogModel.SingleChoice(listItems)) + ?: return@launch config.addFingerprintGesture(type = selectedType) } @@ -285,7 +290,6 @@ abstract class BaseConfigTriggerViewModel( createListItems( keyMap, showDeviceDescriptors, - triggerKeyShortcuts.size, triggerErrorSnapshot, ) val isReorderingEnabled = trigger.keys.size > 1 @@ -353,11 +357,11 @@ abstract class BaseConfigTriggerViewModel( val showClickTypes = trigger.mode is TriggerMode.Sequence when (key) { - is KeyCodeTriggerKey -> { + is KeyEventTriggerKey -> { val showDeviceDescriptors = displayKeyMap.showDeviceDescriptors.first() val deviceListItems: List = config.getAvailableTriggerKeyDevices() - .map { device: TriggerKeyDevice -> + .map { device: KeyEventTriggerDevice -> buildDeviceListItem( device = device, showDeviceDescriptors = showDeviceDescriptors, @@ -365,11 +369,15 @@ abstract class BaseConfigTriggerViewModel( ) } - return TriggerKeyOptionsState.KeyCode( + return TriggerKeyOptionsState.KeyEvent( doNotRemapChecked = !key.consumeEvent, clickType = key.clickType, showClickTypes = showClickTypes, devices = deviceListItems, + keyCode = key.keyCode, + scanCode = key.scanCode, + isScanCodeDetectionSelected = key.detectWithScancode(), + isScanCodeSettingEnabled = key.isScanCodeDetectionUserConfigurable(), ) } @@ -394,30 +402,42 @@ abstract class BaseConfigTriggerViewModel( clickType = key.clickType, ) } + + is EvdevTriggerKey -> { + return TriggerKeyOptionsState.EvdevEvent( + doNotRemapChecked = !key.consumeEvent, + clickType = key.clickType, + showClickTypes = showClickTypes, + keyCode = key.keyCode, + scanCode = key.scanCode, + isScanCodeDetectionSelected = key.detectWithScancode(), + isScanCodeSettingEnabled = key.isScanCodeDetectionUserConfigurable(), + ) + } } } } } private fun buildDeviceListItem( - device: TriggerKeyDevice, + device: KeyEventTriggerDevice, isChecked: Boolean, showDeviceDescriptors: Boolean, ): CheckBoxListItem { return when (device) { - TriggerKeyDevice.Any -> CheckBoxListItem( + KeyEventTriggerDevice.Any -> CheckBoxListItem( id = DEVICE_ID_ANY, isChecked = isChecked, label = getString(R.string.any_device), ) - TriggerKeyDevice.Internal -> CheckBoxListItem( + KeyEventTriggerDevice.Internal -> CheckBoxListItem( id = DEVICE_ID_INTERNAL, isChecked = isChecked, label = getString(R.string.this_device), ) - is TriggerKeyDevice.External -> { + is KeyEventTriggerDevice.External -> { val name = if (showDeviceDescriptors) { InputDeviceUtils.appendDeviceDescriptorToName( device.descriptor, @@ -467,29 +487,20 @@ abstract class BaseConfigTriggerViewModel( } } - private suspend fun onRecordTriggerKey(key: RecordedKey) { - // Add the trigger key before showing the dialog so it doesn't - // need to be dismissed before it is added. - config.addKeyCodeTriggerKey(key.keyCode, key.device, key.detectionSource) - - if (key.keyCode >= InputEventUtils.KEYCODE_TO_SCANCODE_OFFSET || key.keyCode < 0) { - if (onboarding.shownKeyCodeToScanCodeTriggerExplanation) { - return - } - - val dialog = DialogModel.Alert( - title = getString(R.string.dialog_title_keycode_to_scancode_trigger_explanation), - message = getString(R.string.dialog_message_keycode_to_scancode_trigger_explanation), - positiveButtonText = getString(R.string.pos_understood), - ) - - val response = showDialog("keycode_to_scancode_message", dialog) - - if (response == DialogResponse.POSITIVE) { - onboarding.shownKeyCodeToScanCodeTriggerExplanation = true - } + private suspend fun onRecordKeyEvent(key: RecordedKey.KeyEvent) { + val triggerDevice = if (key.isExternalDevice) { + KeyEventTriggerDevice.External(key.deviceDescriptor, key.deviceName) + } else { + KeyEventTriggerDevice.Internal } + config.addKeyEventTriggerKey( + key.keyCode, + key.scanCode, + triggerDevice, + key.detectionSource != InputEventDetectionSource.ACCESSIBILITY_SERVICE, + ) + if (key.keyCode == KeyEvent.KEYCODE_CAPS_LOCK) { val dialog = DialogModel.Ok( message = getString(R.string.dialog_message_enable_physical_keyboard_caps_lock_a_keyboard_layout), @@ -509,7 +520,7 @@ abstract class BaseConfigTriggerViewModel( // Issue #491. Some key codes can only be detected through an input method. This will // be shown to the user by showing a keyboard icon next to the trigger key name so // explain this to the user. - if (key.detectionSource == KeyEventDetectionSource.INPUT_METHOD && displayKeyMap.showTriggerKeyboardIconExplanation.first()) { + if (key.detectionSource == InputEventDetectionSource.INPUT_METHOD && displayKeyMap.showTriggerKeyboardIconExplanation.first()) { val dialog = DialogModel.Alert( title = getString(R.string.dialog_title_keyboard_icon_means_ime_detection), message = getString(R.string.dialog_message_keyboard_icon_means_ime_detection), @@ -525,6 +536,19 @@ abstract class BaseConfigTriggerViewModel( } } + private suspend fun onRecordEvdevEvent(key: RecordedKey.EvdevEvent) { + config.addEvdevTriggerKey( + key.keyCode, + key.scanCode, + EvdevDeviceInfo( + name = key.device.name, + bus = key.device.bus, + vendor = key.device.vendor, + product = key.device.product, + ), + ) + } + fun onParallelRadioButtonChecked() { config.setParallelTriggerMode() } @@ -563,15 +587,15 @@ abstract class BaseConfigTriggerViewModel( fun onSelectTriggerKeyDevice(descriptor: String) { triggerKeyOptionsUid.value?.let { triggerKeyUid -> val device = when (descriptor) { - DEVICE_ID_ANY -> TriggerKeyDevice.Any - DEVICE_ID_INTERNAL -> TriggerKeyDevice.Internal + DEVICE_ID_ANY -> KeyEventTriggerDevice.Any + DEVICE_ID_INTERNAL -> KeyEventTriggerDevice.Internal else -> { val device = config.getAvailableTriggerKeyDevices() - .filterIsInstance() + .filterIsInstance() .firstOrNull { it.descriptor == descriptor } ?: return - TriggerKeyDevice.External( + KeyEventTriggerDevice.External( device.descriptor, device.name, ) @@ -597,8 +621,14 @@ abstract class BaseConfigTriggerViewModel( } } + fun onSelectScanCodeDetection(isSelected: Boolean) { + triggerKeyOptionsUid.value?.let { triggerKeyUid -> + config.setScanCodeDetectionEnabled(triggerKeyUid, isSelected) + } + } + fun onRecordTriggerButtonClick() { - coroutineScope.launch { + viewModelScope.launch { val recordTriggerState = recordTrigger.state.firstOrNull() ?: return@launch val result = when (recordTriggerState) { @@ -636,7 +666,7 @@ abstract class BaseConfigTriggerViewModel( } open fun onTriggerErrorClick(error: TriggerError) { - coroutineScope.launch { + viewModelScope.launch { when (error) { TriggerError.DND_ACCESS_DENIED -> ViewModelHelper.showDialogExplainingDndAccessBeingUnavailable( @@ -658,7 +688,6 @@ abstract class BaseConfigTriggerViewModel( private fun createListItems( keyMap: KeyMap, showDeviceDescriptors: Boolean, - shortcutCount: Int, triggerErrorSnapshot: TriggerErrorSnapshot, ): List { val trigger = keyMap.trigger @@ -695,11 +724,11 @@ abstract class BaseConfigTriggerViewModel( error = error, ) - is KeyCodeTriggerKey -> TriggerKeyListItemModel.KeyCode( + is KeyEventTriggerKey -> TriggerKeyListItemModel.KeyEvent( id = key.uid, keyName = getTriggerKeyName(key), clickType = clickType, - extraInfo = getTriggerKeyExtraInfo( + extraInfo = getKeyEventTriggerKeyExtraInfo( key, showDeviceDescriptors, ).takeIf { it.isNotBlank() }, @@ -725,17 +754,35 @@ abstract class BaseConfigTriggerViewModel( ) } } + + is EvdevTriggerKey -> TriggerKeyListItemModel.EvdevEvent( + id = key.uid, + keyName = key.getCodeLabel(this), + clickType = clickType, + extraInfo = getEvdevTriggerKeyExtraInfo(key), + linkType = linkType, + error = error, + ) + } + } + } + + private fun getEvdevTriggerKeyExtraInfo(key: EvdevTriggerKey): String { + return buildString { + append(key.device.name) + + if (!key.consumeEvent) { + append(" $midDot ${getString(R.string.flag_dont_override_default_action)}") } } } - private fun getTriggerKeyExtraInfo( - key: KeyCodeTriggerKey, + private fun getKeyEventTriggerKeyExtraInfo( + key: KeyEventTriggerKey, showDeviceDescriptors: Boolean, ): String { return buildString { append(getTriggerKeyDeviceName(key.device, showDeviceDescriptors)) - val midDot = getString(R.string.middot) if (!key.consumeEvent) { append(" $midDot ${getString(R.string.flag_dont_override_default_action)}") @@ -743,24 +790,23 @@ abstract class BaseConfigTriggerViewModel( } } - private fun getTriggerKeyName(key: KeyCodeTriggerKey): String { + private fun getTriggerKeyName(key: KeyEventTriggerKey): String { return buildString { - append(InputEventStrings.keyCodeToString(key.keyCode)) + append(key.getCodeLabel(this@BaseConfigTriggerViewModel)) - if (key.detectionSource == KeyEventDetectionSource.INPUT_METHOD) { - val midDot = getString(R.string.middot) + if (key.requiresIme) { append(" $midDot ${getString(R.string.flag_detect_from_input_method)}") } } } private fun getTriggerKeyDeviceName( - device: TriggerKeyDevice, + device: KeyEventTriggerDevice, showDeviceDescriptors: Boolean, ): String = when (device) { - is TriggerKeyDevice.Internal -> getString(R.string.this_device) - is TriggerKeyDevice.Any -> getString(R.string.any_device) - is TriggerKeyDevice.External -> { + is KeyEventTriggerDevice.Internal -> getString(R.string.this_device) + is KeyEventTriggerDevice.Any -> getString(R.string.any_device) + is KeyEventTriggerDevice.External -> { if (showDeviceDescriptors) { InputDeviceUtils.appendDeviceDescriptorToName( device.descriptor, @@ -773,7 +819,7 @@ abstract class BaseConfigTriggerViewModel( } fun onEnableGuiKeyboardClick() { - coroutineScope.launch { + viewModelScope.launch { setupGuiKeyboard.enableInputMethod() } } @@ -835,7 +881,16 @@ sealed class TriggerKeyListItemModel { abstract val error: TriggerError? abstract val clickType: ClickType - data class KeyCode( + data class KeyEvent( + override val id: String, + override val linkType: LinkType, + val keyName: String, + override val clickType: ClickType, + val extraInfo: String?, + override val error: TriggerError?, + ) : TriggerKeyListItemModel() + + data class EvdevEvent( override val id: String, override val linkType: LinkType, val keyName: String, @@ -884,11 +939,31 @@ sealed class TriggerKeyOptionsState { abstract val showClickTypes: Boolean abstract val showLongPressClickType: Boolean - data class KeyCode( + data class KeyEvent( val doNotRemapChecked: Boolean = false, override val clickType: ClickType, override val showClickTypes: Boolean, val devices: List, + val keyCode: Int, + val scanCode: Int?, + // Whether scan code is checked. + val isScanCodeDetectionSelected: Boolean, + // Whether the setting should be enabled and allow user interaction. + val isScanCodeSettingEnabled: Boolean, + ) : TriggerKeyOptionsState() { + override val showLongPressClickType: Boolean = true + } + + data class EvdevEvent( + val doNotRemapChecked: Boolean = false, + override val clickType: ClickType, + override val showClickTypes: Boolean, + val keyCode: Int, + val scanCode: Int, + // Whether scan code is checked. + val isScanCodeDetectionSelected: Boolean, + // Whether the setting should be enabled and allow user interaction. + val isScanCodeSettingEnabled: Boolean, ) : TriggerKeyOptionsState() { override val showLongPressClickType: Boolean = true } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt index 48b54e7ae8..8e2231106d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/BaseTriggerScreen.kt @@ -46,7 +46,7 @@ import io.github.sds100.keymapper.base.keymaps.ShortcutRow import io.github.sds100.keymapper.base.utils.ui.LinkType import io.github.sds100.keymapper.base.utils.ui.compose.ComposeIconInfo import io.github.sds100.keymapper.base.utils.ui.compose.DraggableItem -import io.github.sds100.keymapper.base.utils.ui.compose.RadioButtonText +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperSegmentedButtonRow import io.github.sds100.keymapper.base.utils.ui.compose.rememberDragDropState import io.github.sds100.keymapper.common.utils.State @@ -97,6 +97,7 @@ fun BaseTriggerScreen(modifier: Modifier = Modifier, viewModel: BaseConfigTrigge onEditFloatingButtonClick = viewModel::onEditFloatingButtonClick, onEditFloatingLayoutClick = viewModel::onEditFloatingLayoutClick, onSelectFingerprintGestureType = viewModel::onSelectFingerprintGestureType, + onScanCodeDetectionChanged = viewModel::onSelectScanCodeDetection, ) } @@ -190,6 +191,8 @@ private fun TriggerScreenVertical( ) { Surface(modifier = modifier) { Column { + val isCompact = isVerticalCompactLayout() + when (configState) { is ConfigTriggerState.Empty -> { Column( @@ -226,7 +229,6 @@ private fun TriggerScreenVertical( } is ConfigTriggerState.Loaded -> { - val isCompact = isVerticalCompactLayout() Spacer(Modifier.height(8.dp)) TriggerList( @@ -242,36 +244,40 @@ private fun TriggerScreenVertical( ) if (configState.clickTypeButtons.isNotEmpty()) { - ClickTypeRadioGroup( - modifier = Modifier.padding(horizontal = 8.dp), + ClickTypeSegmentedButtons( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), clickTypes = configState.clickTypeButtons, checkedClickType = configState.checkedClickType, onSelectClickType = onSelectClickType, - maxLines = if (isCompact) 1 else 2, + isCompact = isCompact, ) - } - if (configState.triggerModeButtonsVisible) { if (!isCompact) { - Text( - modifier = Modifier.padding(horizontal = 8.dp), - text = stringResource(R.string.press_dot_dot_dot), - style = MaterialTheme.typography.labelLarge, - ) + Spacer(Modifier.height(8.dp)) } + } - TriggerModeRadioGroup( - modifier = Modifier.padding(horizontal = 8.dp), + if (configState.triggerModeButtonsVisible) { + TriggerModeSegmentedButtons( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), mode = configState.checkedTriggerMode, isEnabled = configState.triggerModeButtonsEnabled, onSelectParallelMode = onSelectParallelMode, onSelectSequenceMode = onSelectSequenceMode, - maxLines = if (isCompact) 1 else 2, + isCompact = isCompact, ) } } } + if (!isCompact) { + Spacer(Modifier.height(8.dp)) + } + RecordTriggerButtonRow( modifier = Modifier .fillMaxWidth() @@ -386,30 +392,35 @@ private fun TriggerScreenHorizontal( .weight(1f) .verticalScroll(rememberScrollState()), ) { + Spacer(modifier = Modifier.height(16.dp)) if (configState.clickTypeButtons.isNotEmpty()) { - ClickTypeRadioGroup( - modifier = Modifier.padding(horizontal = 8.dp), + ClickTypeSegmentedButtons( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), clickTypes = configState.clickTypeButtons, checkedClickType = configState.checkedClickType, onSelectClickType = onSelectClickType, + isCompact = false, ) } - Text( - modifier = Modifier.padding(horizontal = 8.dp), - text = stringResource(R.string.press_dot_dot_dot), - style = MaterialTheme.typography.labelLarge, - ) + Spacer(modifier = Modifier.height(8.dp)) if (configState.triggerModeButtonsVisible) { - TriggerModeRadioGroup( - modifier = Modifier.padding(horizontal = 8.dp), + TriggerModeSegmentedButtons( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), mode = configState.checkedTriggerMode, isEnabled = configState.triggerModeButtonsEnabled, onSelectParallelMode = onSelectParallelMode, onSelectSequenceMode = onSelectSequenceMode, + isCompact = false, ) } + + Spacer(modifier = Modifier.height(16.dp)) } RecordTriggerButtonRow( @@ -508,79 +519,87 @@ private fun TriggerList( } @Composable -private fun ClickTypeRadioGroup( +private fun ClickTypeSegmentedButtons( modifier: Modifier = Modifier, clickTypes: Set, checkedClickType: ClickType?, onSelectClickType: (ClickType) -> Unit, - maxLines: Int = 2, + isCompact: Boolean, ) { - Column(modifier = modifier) { - Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - if (clickTypes.contains(ClickType.SHORT_PRESS)) { - RadioButtonText( - modifier = Modifier.weight(1f), - isSelected = checkedClickType == ClickType.SHORT_PRESS, - text = stringResource(R.string.radio_button_short_press), - onSelected = { onSelectClickType(ClickType.SHORT_PRESS) }, - maxLines = maxLines, - ) + // Always put the buttons in the same order + val clickTypeButtonContent: List> = buildList { + if (clickTypes.contains(ClickType.SHORT_PRESS)) { + val text = if (isCompact) { + stringResource(R.string.radio_button_short) + } else { + stringResource(R.string.radio_button_short_press) } - if (clickTypes.contains(ClickType.LONG_PRESS)) { - RadioButtonText( - modifier = Modifier.weight(1f), - isSelected = checkedClickType == ClickType.LONG_PRESS, - text = stringResource(R.string.radio_button_long_press), - onSelected = { onSelectClickType(ClickType.LONG_PRESS) }, - maxLines = maxLines, - ) + add(ClickType.SHORT_PRESS to text) + } + + if (clickTypes.contains(ClickType.LONG_PRESS)) { + val text = if (isCompact) { + stringResource(R.string.radio_button_long) + } else { + stringResource(R.string.radio_button_long_press) } - if (clickTypes.contains(ClickType.DOUBLE_PRESS)) { - RadioButtonText( - modifier = Modifier.weight(1f), - isSelected = checkedClickType == ClickType.DOUBLE_PRESS, - text = stringResource(R.string.radio_button_double_press), - onSelected = { onSelectClickType(ClickType.DOUBLE_PRESS) }, - maxLines = maxLines, - ) + add(ClickType.LONG_PRESS to text) + } + + if (clickTypes.contains(ClickType.DOUBLE_PRESS)) { + val text = if (isCompact) { + stringResource(R.string.radio_button_double) + } else { + stringResource(R.string.radio_button_double_press) } + add(ClickType.DOUBLE_PRESS to text) } } + + KeyMapperSegmentedButtonRow( + modifier = modifier, + buttonStates = clickTypeButtonContent, + selectedState = checkedClickType, + onStateSelected = onSelectClickType, + isCompact = isCompact, + ) } @Composable -private fun TriggerModeRadioGroup( +private fun TriggerModeSegmentedButtons( modifier: Modifier = Modifier, mode: TriggerMode, isEnabled: Boolean, onSelectParallelMode: () -> Unit, onSelectSequenceMode: () -> Unit, - maxLines: Int = 2, + isCompact: Boolean, ) { - Column(modifier = modifier) { - Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - RadioButtonText( - modifier = Modifier.weight(1f), - isSelected = mode is TriggerMode.Parallel, - isEnabled = isEnabled, - text = stringResource(R.string.radio_button_parallel), - onSelected = onSelectParallelMode, - maxLines = maxLines, - ) - RadioButtonText( - modifier = Modifier.weight(1f), - isSelected = mode == TriggerMode.Sequence, - isEnabled = isEnabled, - text = stringResource(R.string.radio_button_sequence), - onSelected = onSelectSequenceMode, - maxLines = maxLines, - ) - } - } + val triggerModeButtonContent = listOf( + "parallel" to stringResource(R.string.radio_button_parallel), + "sequence" to stringResource(R.string.radio_button_sequence), + ) + + KeyMapperSegmentedButtonRow( + modifier = modifier, + buttonStates = triggerModeButtonContent, + selectedState = when (mode) { + is TriggerMode.Parallel -> "parallel" + TriggerMode.Sequence -> "sequence" + TriggerMode.Undefined -> null + }, + onStateSelected = { selectedMode -> + when (selectedMode) { + "parallel" -> onSelectParallelMode() + "sequence" -> onSelectSequenceMode() + } + }, + isCompact = isCompact, + isEnabled = isEnabled, + ) } private val sampleList = listOf( - TriggerKeyListItemModel.KeyCode( + TriggerKeyListItemModel.KeyEvent( id = "id1", keyName = "Volume Up", clickType = ClickType.SHORT_PRESS, @@ -638,7 +657,7 @@ private fun VerticalPreview() { } } -@Preview(heightDp = 400, widthDp = 300) +@Preview(heightDp = 300, widthDp = 300) @Composable private fun VerticalPreviewTiny() { KeyMapperTheme { diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ChooseTriggerKeyDeviceModel.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ChooseTriggerKeyDeviceModel.kt index efc216cd3f..be95ca0e54 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ChooseTriggerKeyDeviceModel.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ChooseTriggerKeyDeviceModel.kt @@ -2,5 +2,5 @@ package io.github.sds100.keymapper.base.trigger data class ChooseTriggerKeyDeviceModel( val triggerKeyUid: String, - val devices: List, + val devices: List, ) diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt new file mode 100644 index 0000000000..e2a72f7d22 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegate.kt @@ -0,0 +1,513 @@ +package io.github.sds100.keymapper.base.trigger + +import android.view.KeyEvent +import io.github.sds100.keymapper.base.floating.FloatingButtonData +import io.github.sds100.keymapper.base.keymaps.ClickType +import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType +import io.github.sds100.keymapper.common.models.EvdevDeviceInfo +import io.github.sds100.keymapper.system.inputevents.KeyEventUtils +import io.github.sds100.keymapper.system.inputevents.Scancode + +/** + * This extracts the core logic when configuring a trigger which makes it easier to write tests. + */ +class ConfigTriggerDelegate { + + fun addFloatingButtonTriggerKey( + trigger: Trigger, + buttonUid: String, + button: FloatingButtonData?, + ): Trigger { + val clickType = when (trigger.mode) { + is TriggerMode.Parallel -> trigger.mode.clickType + TriggerMode.Sequence -> ClickType.SHORT_PRESS + TriggerMode.Undefined -> ClickType.SHORT_PRESS + } + + val triggerKey = FloatingButtonKey( + buttonUid = buttonUid, + button = button, + clickType = clickType, + ) + + return addTriggerKey(trigger, triggerKey) + } + + fun addAssistantTriggerKey(trigger: Trigger, type: AssistantTriggerType): Trigger { + val clickType = when (trigger.mode) { + is TriggerMode.Parallel -> trigger.mode.clickType + TriggerMode.Sequence -> ClickType.SHORT_PRESS + TriggerMode.Undefined -> ClickType.SHORT_PRESS + } + + val triggerKey = AssistantTriggerKey(type = type, clickType = clickType) + + return addTriggerKey(trigger, triggerKey) + } + + fun addFingerprintGesture(trigger: Trigger, type: FingerprintGestureType): Trigger { + val clickType = when (trigger.mode) { + is TriggerMode.Parallel -> trigger.mode.clickType + TriggerMode.Sequence -> ClickType.SHORT_PRESS + TriggerMode.Undefined -> ClickType.SHORT_PRESS + } + + val triggerKey = FingerprintTriggerKey(type = type, clickType = clickType) + + return addTriggerKey(trigger, triggerKey) + } + + private fun isPowerButtonKey(keyCode: Int, scanCode: Int): Boolean { + return keyCode == KeyEvent.KEYCODE_POWER || + keyCode == KeyEvent.KEYCODE_TV_POWER || + scanCode == Scancode.KEY_POWER || + scanCode == Scancode.KEY_POWER2 + } + + /** + * @param otherTriggerKeys This needs to check the other triggers in the app so that it can + * enable scancode detection by default in some situations. + */ + fun addKeyEventTriggerKey( + trigger: Trigger, + keyCode: Int, + scanCode: Int, + device: KeyEventTriggerDevice, + requiresIme: Boolean, + otherTriggerKeys: List = emptyList(), + ): Trigger { + val isPowerKey = isPowerButtonKey(keyCode, scanCode) + + val clickType = if (isPowerKey) { + ClickType.LONG_PRESS + } else { + when (trigger.mode) { + is TriggerMode.Parallel -> trigger.mode.clickType + TriggerMode.Sequence -> ClickType.SHORT_PRESS + TriggerMode.Undefined -> ClickType.SHORT_PRESS + } + } + + var consumeKeyEvent = true + + // Issue #753 + if (KeyEventUtils.isModifierKey(keyCode)) { + consumeKeyEvent = false + } + + // Scan code detection should be turned on by default if there are other + // keys from the same device that report the same key code but have a different scan code. + val logicallyEqualKeys = otherTriggerKeys.plus(trigger.keys) + .filterIsInstance() + .filter { + it.keyCode == keyCode && + it.scanCode != scanCode && + it.device == device + } + + val triggerKey = KeyEventTriggerKey( + keyCode = keyCode, + device = device, + clickType = clickType, + scanCode = scanCode, + consumeEvent = consumeKeyEvent, + requiresIme = requiresIme, + detectWithScanCodeUserSetting = logicallyEqualKeys.isNotEmpty(), + ) + + var newKeys = trigger.keys.filter { it !is EvdevTriggerKey } + + if (isPowerKey && trigger.mode is TriggerMode.Parallel) { + newKeys = newKeys.map { it.setClickType(ClickType.LONG_PRESS) } + } + + val newMode = if (isPowerKey && trigger.mode is TriggerMode.Parallel) { + TriggerMode.Parallel(ClickType.LONG_PRESS) + } else { + trigger.mode + } + + return addTriggerKey(trigger.copy(mode = newMode, keys = newKeys), triggerKey) + } + + fun addEvdevTriggerKey( + trigger: Trigger, + keyCode: Int, + scanCode: Int, + device: EvdevDeviceInfo, + otherTriggerKeys: List = emptyList(), + ): Trigger { + val isPowerKey = isPowerButtonKey(keyCode, scanCode) + + val clickType = if (isPowerKey) { + ClickType.LONG_PRESS + } else { + when (trigger.mode) { + is TriggerMode.Parallel -> trigger.mode.clickType + TriggerMode.Sequence -> ClickType.SHORT_PRESS + TriggerMode.Undefined -> ClickType.SHORT_PRESS + } + } + + // Scan code detection should be turned on by default if there are other + // keys from the same device that report the same key code but have a different scan code. + val conflictingKeys = otherTriggerKeys.plus(trigger.keys) + .filterIsInstance() + .filter { + it.keyCode == keyCode && + it.scanCode != scanCode && + it.device == device + } + + val triggerKey = EvdevTriggerKey( + keyCode = keyCode, + scanCode = scanCode, + device = device, + clickType = clickType, + consumeEvent = true, + detectWithScanCodeUserSetting = conflictingKeys.isNotEmpty(), + ) + + var newKeys = trigger.keys.filter { it !is KeyEventTriggerKey } + + if (isPowerKey && trigger.mode is TriggerMode.Parallel) { + newKeys = newKeys.map { it.setClickType(ClickType.LONG_PRESS) } + } + + val newMode = if (isPowerKey && trigger.mode is TriggerMode.Parallel) { + TriggerMode.Parallel(ClickType.LONG_PRESS) + } else { + trigger.mode + } + + return addTriggerKey(trigger.copy(mode = newMode, keys = newKeys), triggerKey) + } + + private fun addTriggerKey( + trigger: Trigger, + key: TriggerKey, + ): Trigger { + // Check whether the trigger already contains the key because if so + // then it must be converted to a sequence trigger. + val containsKey = trigger.keys.any { otherKey -> key.isLogicallyEqual(otherKey) } + + var newKeys: List = trigger.keys.plus(key) + + val newMode = when { + trigger.mode != TriggerMode.Sequence && containsKey -> TriggerMode.Sequence + newKeys.size <= 1 -> TriggerMode.Undefined + + /* Automatically make it a parallel trigger when the user makes a trigger with more than one key + because this is what most users are expecting when they make a trigger with multiple keys */ + newKeys.size == 2 && !containsKey -> { + newKeys = newKeys.map { it.setClickType(key.clickType) } + TriggerMode.Parallel(key.clickType) + } + + else -> trigger.mode + } + + return trigger.copy(keys = newKeys, mode = newMode).validate() + } + + fun removeTriggerKey(trigger: Trigger, uid: String): Trigger { + val newKeys = trigger.keys.toMutableList().apply { + removeAll { it.uid == uid } + } + + val newMode = when { + newKeys.size <= 1 -> TriggerMode.Undefined + else -> trigger.mode + } + + return trigger.copy(keys = newKeys, mode = newMode).validate() + } + + fun moveTriggerKey(trigger: Trigger, fromIndex: Int, toIndex: Int): Trigger { + // Don't need to validate. This should be low latency so moving is responsive. + return trigger.copy( + keys = trigger.keys.toMutableList().apply { + add(toIndex, removeAt(fromIndex)) + }, + ) + } + + fun setParallelTriggerMode(trigger: Trigger): Trigger { + if (trigger.mode is TriggerMode.Parallel) { + return trigger + } + + // undefined mode only allowed if one or no keys + if (trigger.keys.size <= 1) { + return trigger.copy(mode = TriggerMode.Undefined) + } + + val oldKeys = trigger.keys + val newKeys: MutableList = mutableListOf() + + // In a parallel trigger keys must be triggered by different key events + outerLoop@ for (key in oldKeys) { + for (other in newKeys) { + if (key.isLogicallyEqual(other)) { + continue@outerLoop + } + } + + // set all the keys to a short press if coming from a non-parallel trigger + // because they must all be the same click type and can't all be double pressed + newKeys.add(key.setClickType(ClickType.SHORT_PRESS)) + } + + return trigger + .copy(keys = newKeys, mode = TriggerMode.Parallel(ClickType.SHORT_PRESS)) + .validate() + } + + fun setSequenceTriggerMode(trigger: Trigger): Trigger { + if (trigger.mode == TriggerMode.Sequence) return trigger + // undefined mode only allowed if one or no keys + if (trigger.keys.size <= 1) { + return trigger.copy(mode = TriggerMode.Undefined) + } + + return trigger.copy(mode = TriggerMode.Sequence).validate() + } + + fun setUndefinedTriggerMode(trigger: Trigger): Trigger { + if (trigger.mode == TriggerMode.Undefined) return trigger + + // undefined mode only allowed if one or no keys + if (trigger.keys.size > 1) { + return trigger + } + + return trigger.copy(mode = TriggerMode.Undefined).validate() + } + + fun setTriggerShortPress(trigger: Trigger): Trigger { + if (trigger.mode == TriggerMode.Sequence) { + return trigger + } + + val newKeys = trigger.keys.map { it.setClickType(clickType = ClickType.SHORT_PRESS) } + val newMode = if (newKeys.size <= 1) { + TriggerMode.Undefined + } else { + TriggerMode.Parallel(ClickType.SHORT_PRESS) + } + return trigger.copy(keys = newKeys, mode = newMode).validate() + } + + fun setTriggerLongPress(trigger: Trigger): Trigger { + if (trigger.mode == TriggerMode.Sequence) { + return trigger + } + + // You can't set the trigger to a long press if it contains a key + // that isn't detected with key codes. This is because there aren't + // separate key events for the up and down press that can be timed. + if (trigger.keys.any { !it.allowedLongPress }) { + return trigger + } + + val newKeys = trigger.keys.map { it.setClickType(clickType = ClickType.LONG_PRESS) } + val newMode = if (newKeys.size <= 1) { + TriggerMode.Undefined + } else { + TriggerMode.Parallel(ClickType.LONG_PRESS) + } + + return trigger.copy(keys = newKeys, mode = newMode).validate() + } + + fun setTriggerDoublePress(trigger: Trigger): Trigger { + if (trigger.mode != TriggerMode.Undefined) { + return trigger + } + + if (trigger.keys.any { !it.allowedDoublePress }) { + return trigger + } + + val newKeys = trigger.keys.map { it.setClickType(clickType = ClickType.DOUBLE_PRESS) } + val newMode = TriggerMode.Undefined + + return trigger.copy(keys = newKeys, mode = newMode).validate() + } + + fun setTriggerKeyClickType(trigger: Trigger, keyUid: String, clickType: ClickType): Trigger { + val newKeys = trigger.keys.map { + if (it.uid == keyUid) { + it.setClickType(clickType = clickType) + } else { + it + } + } + + return trigger.copy(keys = newKeys).validate() + } + + fun setTriggerKeyDevice( + trigger: Trigger, + keyUid: String, + device: KeyEventTriggerDevice, + ): Trigger { + val newKeys = trigger.keys.map { key -> + if (key.uid == keyUid) { + if (key !is KeyEventTriggerKey) { + throw IllegalArgumentException("You can not set the device for non KeyEventTriggerKeys.") + } + + key.copy(device = device) + } else { + key + } + } + + return trigger.copy(keys = newKeys).validate() + } + + fun setTriggerKeyConsumeKeyEvent( + trigger: Trigger, + keyUid: String, + consumeKeyEvent: Boolean, + ): Trigger { + val newKeys = trigger.keys.map { key -> + if (key.uid == keyUid) { + when (key) { + is KeyEventTriggerKey -> { + key.copy(consumeEvent = consumeKeyEvent) + } + + is EvdevTriggerKey -> { + key.copy(consumeEvent = consumeKeyEvent) + } + + else -> { + key + } + } + } else { + key + } + } + + return trigger.copy(keys = newKeys).validate() + } + + fun setAssistantTriggerKeyType( + trigger: Trigger, + keyUid: String, + type: AssistantTriggerType, + ): Trigger { + val newKeys = trigger.keys.map { key -> + if (key.uid == keyUid) { + if (key is AssistantTriggerKey) { + key.copy(type = type) + } else { + key + } + } else { + key + } + } + + return trigger.copy(keys = newKeys).validate() + } + + fun setFingerprintGestureType( + trigger: Trigger, + keyUid: String, + type: FingerprintGestureType, + ): Trigger { + val newKeys = trigger.keys.map { key -> + if (key.uid == keyUid) { + if (key is FingerprintTriggerKey) { + key.copy(type = type) + } else { + key + } + } else { + key + } + } + + return trigger.copy(keys = newKeys).validate() + } + + fun setVibrateEnabled(trigger: Trigger, enabled: Boolean): Trigger { + return trigger.copy(vibrate = enabled).validate() + } + + fun setVibrationDuration( + trigger: Trigger, + duration: Int, + defaultVibrateDuration: Int, + ): Trigger { + return if (duration == defaultVibrateDuration) { + trigger.copy(vibrateDuration = null).validate() + } else { + trigger.copy(vibrateDuration = duration).validate() + } + } + + fun setLongPressDelay(trigger: Trigger, delay: Int, defaultLongPressDelay: Int): Trigger { + return if (delay == defaultLongPressDelay) { + trigger.copy(longPressDelay = null).validate() + } else { + trigger.copy(longPressDelay = delay).validate() + } + } + + fun setDoublePressDelay(trigger: Trigger, delay: Int, defaultDoublePressDelay: Int): Trigger { + return if (delay == defaultDoublePressDelay) { + trigger.copy(doublePressDelay = null).validate() + } else { + trigger.copy(doublePressDelay = delay).validate() + } + } + + fun setSequenceTriggerTimeout( + trigger: Trigger, + delay: Int, + defaultSequenceTriggerTimeout: Int, + ): Trigger { + return if (delay == defaultSequenceTriggerTimeout) { + trigger.copy(sequenceTriggerTimeout = null).validate() + } else { + trigger.copy(sequenceTriggerTimeout = delay).validate() + } + } + + fun setLongPressDoubleVibrationEnabled(trigger: Trigger, enabled: Boolean): Trigger { + return trigger.copy(longPressDoubleVibration = enabled).validate() + } + + fun setTriggerFromOtherAppsEnabled(trigger: Trigger, enabled: Boolean): Trigger { + return trigger.copy(triggerFromOtherApps = enabled).validate() + } + + fun setShowToastEnabled(trigger: Trigger, enabled: Boolean): Trigger { + return trigger.copy(showToast = enabled).validate() + } + + fun setScanCodeDetectionEnabled(trigger: Trigger, keyUid: String, enabled: Boolean): Trigger { + val newKeys = trigger.keys.map { otherKey -> + if (otherKey.uid == keyUid && otherKey is KeyCodeTriggerKey && otherKey.isScanCodeDetectionUserConfigurable()) { + when (otherKey) { + is KeyEventTriggerKey -> { + otherKey.copy(detectWithScanCodeUserSetting = enabled) + } + + is EvdevTriggerKey -> { + otherKey.copy(detectWithScanCodeUserSetting = enabled) + } + } + } else { + otherKey + } + } + + return trigger.copy(keys = newKeys).validate() + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt new file mode 100644 index 0000000000..65d633f0ab --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerUseCase.kt @@ -0,0 +1,348 @@ +package io.github.sds100.keymapper.base.trigger + +import dagger.hilt.android.scopes.ViewModelScoped +import io.github.sds100.keymapper.base.floating.FloatingButtonEntityMapper +import io.github.sds100.keymapper.base.keymaps.ClickType +import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapState +import io.github.sds100.keymapper.base.keymaps.GetDefaultKeyMapOptionsUseCase +import io.github.sds100.keymapper.base.keymaps.KeyMap +import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType +import io.github.sds100.keymapper.common.models.EvdevDeviceInfo +import io.github.sds100.keymapper.common.utils.InputDeviceUtils +import io.github.sds100.keymapper.common.utils.State +import io.github.sds100.keymapper.common.utils.dataOrNull +import io.github.sds100.keymapper.common.utils.firstBlocking +import io.github.sds100.keymapper.data.Keys +import io.github.sds100.keymapper.data.entities.AssistantTriggerKeyEntity +import io.github.sds100.keymapper.data.entities.EvdevTriggerKeyEntity +import io.github.sds100.keymapper.data.entities.FingerprintTriggerKeyEntity +import io.github.sds100.keymapper.data.entities.FloatingButtonKeyEntity +import io.github.sds100.keymapper.data.entities.KeyEventTriggerKeyEntity +import io.github.sds100.keymapper.data.entities.KeyMapEntity +import io.github.sds100.keymapper.data.repositories.FloatingButtonRepository +import io.github.sds100.keymapper.data.repositories.FloatingLayoutRepository +import io.github.sds100.keymapper.data.repositories.KeyMapRepository +import io.github.sds100.keymapper.data.repositories.PreferenceRepository +import io.github.sds100.keymapper.system.devices.DevicesAdapter +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@ViewModelScoped +class ConfigTriggerUseCaseImpl @Inject constructor( + private val state: ConfigKeyMapState, + private val preferenceRepository: PreferenceRepository, + private val floatingButtonRepository: FloatingButtonRepository, + private val devicesAdapter: DevicesAdapter, + private val floatingLayoutRepository: FloatingLayoutRepository, + private val getDefaultKeyMapOptionsUseCase: GetDefaultKeyMapOptionsUseCase, + private val keyMapRepository: KeyMapRepository, +) : ConfigTriggerUseCase, GetDefaultKeyMapOptionsUseCase by getDefaultKeyMapOptionsUseCase { + override val keyMap: StateFlow> = state.keyMap + + override val floatingButtonToUse: MutableStateFlow = state.floatingButtonToUse + + private val showDeviceDescriptors: Flow = + preferenceRepository.get(Keys.showDeviceDescriptors).map { it == true } + + private val delegate: ConfigTriggerDelegate = ConfigTriggerDelegate() + + // This class is viewmodel scoped so this will be recomputed each time + // the user starts configuring a key map + private val otherTriggerKeys: List by lazy { + keyMapRepository.keyMapList + .filterIsInstance>>() + .map { state -> state.data.flatMap { it.trigger.keys } } + .map { keys -> + keys + .mapNotNull { key -> + when (key) { + is EvdevTriggerKeyEntity -> EvdevTriggerKey.fromEntity(key) + is KeyEventTriggerKeyEntity -> KeyEventTriggerKey.fromEntity(key) + is AssistantTriggerKeyEntity, is FingerprintTriggerKeyEntity, is FloatingButtonKeyEntity -> null + } + }.filterIsInstance() + }.firstBlocking() + } + + override fun setEnabled(enabled: Boolean) { + state.update { it.copy(isEnabled = enabled) } + } + + override suspend fun getFloatingLayoutCount(): Int { + return floatingLayoutRepository.count() + } + + override suspend fun addFloatingButtonTriggerKey(buttonUid: String) { + floatingButtonToUse.update { null } + + val button = floatingButtonRepository.get(buttonUid) + ?.let { entity -> + FloatingButtonEntityMapper.fromEntity( + entity.button, + entity.layout.name, + ) + } + + updateTrigger { trigger -> + delegate.addFloatingButtonTriggerKey(trigger, buttonUid, button) + } + } + + override fun addAssistantTriggerKey(type: AssistantTriggerType) = updateTrigger { trigger -> + delegate.addAssistantTriggerKey(trigger, type) + } + + override fun addFingerprintGesture(type: FingerprintGestureType) = updateTrigger { trigger -> + delegate.addFingerprintGesture(trigger, type) + } + + override suspend fun addKeyEventTriggerKey( + keyCode: Int, + scanCode: Int, + device: KeyEventTriggerDevice, + requiresIme: Boolean, + ) = updateTrigger { trigger -> + delegate.addKeyEventTriggerKey( + trigger, + keyCode, + scanCode, + device, + requiresIme, + otherTriggerKeys = otherTriggerKeys, + ) + } + + override suspend fun addEvdevTriggerKey( + keyCode: Int, + scanCode: Int, + device: EvdevDeviceInfo, + ) = updateTrigger { trigger -> + delegate.addEvdevTriggerKey( + trigger, + keyCode, + scanCode, + device, + otherTriggerKeys = otherTriggerKeys, + ) + } + + override fun removeTriggerKey(uid: String) = updateTrigger { trigger -> + delegate.removeTriggerKey(trigger, uid) + } + + override fun moveTriggerKey(fromIndex: Int, toIndex: Int) = updateTrigger { trigger -> + delegate.moveTriggerKey(trigger, fromIndex, toIndex) + } + + override fun getTriggerKey(uid: String): TriggerKey? { + return state.keyMap.value.dataOrNull()?.trigger?.keys?.find { it.uid == uid } + } + + override fun setParallelTriggerMode() = updateTrigger { trigger -> + delegate.setParallelTriggerMode(trigger) + } + + override fun setSequenceTriggerMode() = updateTrigger { trigger -> + delegate.setSequenceTriggerMode(trigger) + } + + override fun setUndefinedTriggerMode() = updateTrigger { trigger -> + delegate.setUndefinedTriggerMode(trigger) + } + + override fun setTriggerShortPress() { + updateTrigger { trigger -> + delegate.setTriggerShortPress(trigger) + } + } + + override fun setTriggerLongPress() { + updateTrigger { trigger -> + delegate.setTriggerLongPress(trigger) + } + } + + override fun setTriggerDoublePress() { + updateTrigger { trigger -> + delegate.setTriggerDoublePress(trigger) + } + } + + override fun setTriggerKeyClickType(keyUid: String, clickType: ClickType) { + updateTrigger { trigger -> + delegate.setTriggerKeyClickType(trigger, keyUid, clickType) + } + } + + override fun setTriggerKeyDevice(keyUid: String, device: KeyEventTriggerDevice) { + updateTrigger { trigger -> + delegate.setTriggerKeyDevice(trigger, keyUid, device) + } + } + + override fun setTriggerKeyConsumeKeyEvent(keyUid: String, consumeKeyEvent: Boolean) { + updateTrigger { trigger -> + delegate.setTriggerKeyConsumeKeyEvent(trigger, keyUid, consumeKeyEvent) + } + } + + override fun setAssistantTriggerKeyType(keyUid: String, type: AssistantTriggerType) { + updateTrigger { trigger -> + delegate.setAssistantTriggerKeyType(trigger, keyUid, type) + } + } + + override fun setFingerprintGestureType(keyUid: String, type: FingerprintGestureType) { + updateTrigger { trigger -> + delegate.setFingerprintGestureType(trigger, keyUid, type) + } + } + + override fun setVibrateEnabled(enabled: Boolean) = updateTrigger { trigger -> + delegate.setVibrateEnabled(trigger, enabled) + } + + override fun setVibrationDuration(duration: Int) = updateTrigger { trigger -> + delegate.setVibrationDuration(trigger, duration, defaultVibrateDuration.value) + } + + override fun setLongPressDelay(delay: Int) = updateTrigger { trigger -> + delegate.setLongPressDelay(trigger, delay, defaultLongPressDelay.value) + } + + override fun setDoublePressDelay(delay: Int) { + updateTrigger { trigger -> + delegate.setDoublePressDelay(trigger, delay, defaultDoublePressDelay.value) + } + } + + override fun setSequenceTriggerTimeout(delay: Int) { + updateTrigger { trigger -> + delegate.setSequenceTriggerTimeout(trigger, delay, defaultSequenceTriggerTimeout.value) + } + } + + override fun setLongPressDoubleVibrationEnabled(enabled: Boolean) { + updateTrigger { trigger -> + delegate.setLongPressDoubleVibrationEnabled(trigger, enabled) + } + } + + override fun setTriggerFromOtherAppsEnabled(enabled: Boolean) { + updateTrigger { trigger -> + delegate.setTriggerFromOtherAppsEnabled(trigger, enabled) + } + } + + override fun setShowToastEnabled(enabled: Boolean) { + updateTrigger { trigger -> + delegate.setShowToastEnabled(trigger, enabled) + } + } + + override fun setScanCodeDetectionEnabled(keyUid: String, enabled: Boolean) { + updateTrigger { trigger -> + delegate.setScanCodeDetectionEnabled(trigger, keyUid, enabled) + } + } + + override fun getAvailableTriggerKeyDevices(): List { + val externalKeyEventTriggerDevices = sequence { + val inputDevices = + devicesAdapter.connectedInputDevices.value.dataOrNull() ?: emptyList() + + val showDeviceDescriptors = showDeviceDescriptors.firstBlocking() + + for (device in inputDevices) { + if (device.isExternal) { + val name = if (showDeviceDescriptors) { + InputDeviceUtils.appendDeviceDescriptorToName( + device.descriptor, + device.name, + ) + } else { + device.name + } + + yield(KeyEventTriggerDevice.External(device.descriptor, name)) + } + } + } + + return sequence { + yield(KeyEventTriggerDevice.Internal) + yield(KeyEventTriggerDevice.Any) + yieldAll(externalKeyEventTriggerDevices) + }.toList() + } + + private fun updateTrigger(block: (trigger: Trigger) -> Trigger) { + state.update { keyMap -> + val newTrigger = block(keyMap.trigger) + + keyMap.copy(trigger = newTrigger) + } + } +} + +interface ConfigTriggerUseCase : GetDefaultKeyMapOptionsUseCase { + + val keyMap: StateFlow> + + fun setEnabled(enabled: Boolean) + + // trigger + suspend fun addKeyEventTriggerKey( + keyCode: Int, + scanCode: Int, + device: KeyEventTriggerDevice, + requiresIme: Boolean, + ) + + suspend fun addFloatingButtonTriggerKey(buttonUid: String) + fun addAssistantTriggerKey(type: AssistantTriggerType) + fun addFingerprintGesture(type: FingerprintGestureType) + suspend fun addEvdevTriggerKey( + keyCode: Int, + scanCode: Int, + device: EvdevDeviceInfo, + ) + + fun removeTriggerKey(uid: String) + fun getTriggerKey(uid: String): TriggerKey? + fun moveTriggerKey(fromIndex: Int, toIndex: Int) + + fun setParallelTriggerMode() + fun setSequenceTriggerMode() + fun setUndefinedTriggerMode() + + fun setTriggerShortPress() + fun setTriggerLongPress() + fun setTriggerDoublePress() + + fun setTriggerKeyClickType(keyUid: String, clickType: ClickType) + fun setTriggerKeyDevice(keyUid: String, device: KeyEventTriggerDevice) + fun setTriggerKeyConsumeKeyEvent(keyUid: String, consumeKeyEvent: Boolean) + fun setAssistantTriggerKeyType(keyUid: String, type: AssistantTriggerType) + fun setFingerprintGestureType(keyUid: String, type: FingerprintGestureType) + + fun setVibrateEnabled(enabled: Boolean) + fun setVibrationDuration(duration: Int) + fun setLongPressDelay(delay: Int) + fun setDoublePressDelay(delay: Int) + fun setSequenceTriggerTimeout(delay: Int) + fun setLongPressDoubleVibrationEnabled(enabled: Boolean) + fun setTriggerFromOtherAppsEnabled(enabled: Boolean) + fun setShowToastEnabled(enabled: Boolean) + fun setScanCodeDetectionEnabled(keyUid: String, enabled: Boolean) + + fun getAvailableTriggerKeyDevices(): List + + val floatingButtonToUse: MutableStateFlow + suspend fun getFloatingLayoutCount(): Int +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/EvdevTriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/EvdevTriggerKey.kt new file mode 100644 index 0000000000..7962e19da8 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/EvdevTriggerKey.kt @@ -0,0 +1,96 @@ +package io.github.sds100.keymapper.base.trigger + +import io.github.sds100.keymapper.base.keymaps.ClickType +import io.github.sds100.keymapper.common.models.EvdevDeviceInfo +import io.github.sds100.keymapper.common.utils.hasFlag +import io.github.sds100.keymapper.common.utils.withFlag +import io.github.sds100.keymapper.data.entities.EvdevTriggerKeyEntity +import io.github.sds100.keymapper.data.entities.TriggerKeyEntity +import java.util.UUID + +/** + * This must be a different class to KeyEventTriggerKey because trigger keys from evdev events + * must come from one device, even if it is internal, and can not come from any device. The input + * devices must be grabbed so that Key Mapper can remap them. + */ +data class EvdevTriggerKey( + override val uid: String = UUID.randomUUID().toString(), + override val keyCode: Int, + override val scanCode: Int, + val device: EvdevDeviceInfo, + override val clickType: ClickType = ClickType.SHORT_PRESS, + override val consumeEvent: Boolean = true, + override val detectWithScanCodeUserSetting: Boolean = false, +) : TriggerKey(), KeyCodeTriggerKey { + override val allowedDoublePress: Boolean = true + override val allowedLongPress: Boolean = true + + override fun isSameDevice(otherKey: KeyCodeTriggerKey): Boolean { + if (otherKey !is EvdevTriggerKey) { + return false + } + return device == otherKey.device + } + + companion object { + fun fromEntity(entity: EvdevTriggerKeyEntity): TriggerKey { + val clickType = when (entity.clickType) { + TriggerKeyEntity.SHORT_PRESS -> ClickType.SHORT_PRESS + TriggerKeyEntity.LONG_PRESS -> ClickType.LONG_PRESS + TriggerKeyEntity.DOUBLE_PRESS -> ClickType.DOUBLE_PRESS + else -> ClickType.SHORT_PRESS + } + + val consumeEvent = + !entity.flags.hasFlag(EvdevTriggerKeyEntity.FLAG_DO_NOT_CONSUME_KEY_EVENT) + + val detectWithScancode = + entity.flags.hasFlag(EvdevTriggerKeyEntity.FLAG_DETECT_WITH_SCAN_CODE) + + return EvdevTriggerKey( + uid = entity.uid, + keyCode = entity.keyCode, + scanCode = entity.scanCode, + device = EvdevDeviceInfo( + name = entity.deviceName, + bus = entity.deviceBus, + vendor = entity.deviceVendor, + product = entity.deviceProduct, + ), + clickType = clickType, + consumeEvent = consumeEvent, + detectWithScanCodeUserSetting = detectWithScancode, + ) + } + + fun toEntity(key: EvdevTriggerKey): EvdevTriggerKeyEntity { + val clickType = when (key.clickType) { + ClickType.SHORT_PRESS -> TriggerKeyEntity.SHORT_PRESS + ClickType.LONG_PRESS -> TriggerKeyEntity.LONG_PRESS + ClickType.DOUBLE_PRESS -> TriggerKeyEntity.DOUBLE_PRESS + } + + var flags = 0 + + if (!key.consumeEvent) { + flags = flags.withFlag(EvdevTriggerKeyEntity.FLAG_DO_NOT_CONSUME_KEY_EVENT) + } + + if (key.detectWithScanCodeUserSetting) { + flags = flags.withFlag(EvdevTriggerKeyEntity.FLAG_DETECT_WITH_SCAN_CODE) + } + + return EvdevTriggerKeyEntity( + keyCode = key.keyCode, + scanCode = key.scanCode, + deviceName = key.device.name, + deviceBus = key.device.bus, + deviceVendor = key.device.vendor, + deviceProduct = key.device.product, + clickType = clickType, + flags = flags, + uid = key.uid, + ) + } + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/FingerprintTriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/FingerprintTriggerKey.kt index 820d11df43..cb233a0b2a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/FingerprintTriggerKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/FingerprintTriggerKey.kt @@ -17,7 +17,6 @@ data class FingerprintTriggerKey( override val clickType: ClickType, ) : TriggerKey() { - override val consumeEvent: Boolean = true override val allowedLongPress: Boolean = false override val allowedDoublePress: Boolean = false diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/FloatingButtonKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/FloatingButtonKey.kt index 65b51d395e..348d9fa489 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/FloatingButtonKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/FloatingButtonKey.kt @@ -17,7 +17,6 @@ data class FloatingButtonKey( override val clickType: ClickType, ) : TriggerKey() { - override val consumeEvent: Boolean = true override val allowedLongPress: Boolean = true override val allowedDoublePress: Boolean = true diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt index cbb4aa976c..b059d8a4b6 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyCodeTriggerKey.kt @@ -1,125 +1,60 @@ package io.github.sds100.keymapper.base.trigger +import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.keymaps.ClickType -import io.github.sds100.keymapper.common.utils.hasFlag -import io.github.sds100.keymapper.common.utils.withFlag -import io.github.sds100.keymapper.data.entities.KeyCodeTriggerKeyEntity -import io.github.sds100.keymapper.data.entities.TriggerKeyEntity -import kotlinx.serialization.Serializable -import java.util.UUID - -@Serializable -data class KeyCodeTriggerKey( - override val uid: String = UUID.randomUUID().toString(), - val keyCode: Int, - val device: TriggerKeyDevice, - override val clickType: ClickType, - override val consumeEvent: Boolean = true, - val detectionSource: KeyEventDetectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, -) : TriggerKey() { - - override val allowedLongPress: Boolean = true - override val allowedDoublePress: Boolean = true - - override fun toString(): String { - val deviceString = when (device) { - TriggerKeyDevice.Any -> "any" - is TriggerKeyDevice.External -> "external" - TriggerKeyDevice.Internal -> "internal" - } - return "KeyCodeTriggerKey(uid=${uid.substring(0..5)}, keyCode=$keyCode, device=$deviceString, clickType=$clickType, consume=$consumeEvent) " - } - - // key code -> click type -> device -> consume key event - override fun compareTo(other: TriggerKey) = when (other) { - is KeyCodeTriggerKey -> compareValuesBy( - this, - other, - { it.keyCode }, - { it.clickType }, - { it.device }, - { it.consumeEvent }, - ) - - else -> super.compareTo(other) - } - - companion object { - fun fromEntity(entity: KeyCodeTriggerKeyEntity): TriggerKey { - val device = when (entity.deviceId) { - KeyCodeTriggerKeyEntity.DEVICE_ID_THIS_DEVICE -> TriggerKeyDevice.Internal - KeyCodeTriggerKeyEntity.DEVICE_ID_ANY_DEVICE -> TriggerKeyDevice.Any - else -> TriggerKeyDevice.External( - entity.deviceId, - entity.deviceName ?: "", - ) - } - - val clickType = when (entity.clickType) { - TriggerKeyEntity.SHORT_PRESS -> ClickType.SHORT_PRESS - TriggerKeyEntity.LONG_PRESS -> ClickType.LONG_PRESS - TriggerKeyEntity.DOUBLE_PRESS -> ClickType.DOUBLE_PRESS - else -> ClickType.SHORT_PRESS - } - - val consumeEvent = - !entity.flags.hasFlag(KeyCodeTriggerKeyEntity.FLAG_DO_NOT_CONSUME_KEY_EVENT) - - val detectionSource = - if (entity.flags.hasFlag(KeyCodeTriggerKeyEntity.FLAG_DETECTION_SOURCE_INPUT_METHOD)) { - KeyEventDetectionSource.INPUT_METHOD - } else { - KeyEventDetectionSource.ACCESSIBILITY_SERVICE - } - - return KeyCodeTriggerKey( - uid = entity.uid, - keyCode = entity.keyCode, - device = device, - clickType = clickType, - consumeEvent = consumeEvent, - detectionSource = detectionSource, - ) - } - - fun toEntity(key: KeyCodeTriggerKey): KeyCodeTriggerKeyEntity { - val deviceId = when (key.device) { - TriggerKeyDevice.Any -> KeyCodeTriggerKeyEntity.DEVICE_ID_ANY_DEVICE - is TriggerKeyDevice.External -> key.device.descriptor - TriggerKeyDevice.Internal -> KeyCodeTriggerKeyEntity.DEVICE_ID_THIS_DEVICE - } - - val deviceName = - if (key.device is TriggerKeyDevice.External) { - key.device.name - } else { - null - } - - val clickType = when (key.clickType) { - ClickType.SHORT_PRESS -> TriggerKeyEntity.SHORT_PRESS - ClickType.LONG_PRESS -> TriggerKeyEntity.LONG_PRESS - ClickType.DOUBLE_PRESS -> TriggerKeyEntity.DOUBLE_PRESS - } +import io.github.sds100.keymapper.base.utils.KeyCodeStrings +import io.github.sds100.keymapper.base.utils.ScancodeStrings +import io.github.sds100.keymapper.base.utils.ui.ResourceProvider +import io.github.sds100.keymapper.system.inputevents.KeyEventUtils + +sealed interface KeyCodeTriggerKey { + val keyCode: Int + + /** + * Scancodes were only saved to KeyEvent trigger keys in version 4.0.0 so this is null + * to be backwards compatible. + */ + val scanCode: Int? + val clickType: ClickType + + /** + * The user can specify they want to detect with the scancode instead of the key code. + */ + val detectWithScanCodeUserSetting: Boolean + + /** + * Whether the event that triggers this key will be consumed and not passed + * onto subsequent apps. E.g consuming the volume down key event will mean the volume + * doesn't change. + */ + val consumeEvent: Boolean + + fun isSameDevice(otherKey: KeyCodeTriggerKey): Boolean +} - var flags = 0 +fun KeyCodeTriggerKey.detectWithScancode(): Boolean { + return scanCode != null && (detectWithScanCodeUserSetting || isKeyCodeUnknown()) +} - if (!key.consumeEvent) { - flags = flags.withFlag(KeyCodeTriggerKeyEntity.FLAG_DO_NOT_CONSUME_KEY_EVENT) - } +fun KeyCodeTriggerKey.isKeyCodeUnknown(): Boolean { + return KeyEventUtils.isKeyCodeUnknown(keyCode) +} - if (key.detectionSource == KeyEventDetectionSource.INPUT_METHOD) { - flags = flags.withFlag(KeyCodeTriggerKeyEntity.FLAG_DETECTION_SOURCE_INPUT_METHOD) - } +fun KeyCodeTriggerKey.isScanCodeDetectionUserConfigurable(): Boolean { + return scanCode != null && !isKeyCodeUnknown() +} - return KeyCodeTriggerKeyEntity( - keyCode = key.keyCode, - deviceId = deviceId, - deviceName = deviceName, - clickType = clickType, - flags = flags, - uid = key.uid, - ) - } +/** + * Get the label for the key code or scan code, depending on whether to detect it with a scan code. + */ +fun KeyCodeTriggerKey.getCodeLabel(resourceProvider: ResourceProvider): String { + if (detectWithScancode() && scanCode != null) { + val codeLabel = ScancodeStrings.getScancodeLabel(scanCode!!) + ?: resourceProvider.getString(R.string.trigger_key_unknown_scan_code, scanCode!!) + + return "$codeLabel (${resourceProvider.getString(R.string.trigger_key_scan_code_detection_flag)})" + } else { + return KeyCodeStrings.keyCodeToString(keyCode) + ?: resourceProvider.getString(R.string.trigger_key_unknown_key_code, keyCode) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventDetectionSource.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventDetectionSource.kt deleted file mode 100644 index c231beff8a..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventDetectionSource.kt +++ /dev/null @@ -1,6 +0,0 @@ -package io.github.sds100.keymapper.base.trigger - -enum class KeyEventDetectionSource { - ACCESSIBILITY_SERVICE, - INPUT_METHOD, -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerDevice.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerDevice.kt new file mode 100644 index 0000000000..bff15d411b --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerDevice.kt @@ -0,0 +1,43 @@ +package io.github.sds100.keymapper.base.trigger + +import kotlinx.serialization.Serializable + +@Serializable +sealed class KeyEventTriggerDevice() : Comparable { + override fun compareTo(other: KeyEventTriggerDevice) = + this.javaClass.name.compareTo(other.javaClass.name) + + @Serializable + data object Internal : KeyEventTriggerDevice() + + @Serializable + data object Any : KeyEventTriggerDevice() + + @Serializable + data class External(val descriptor: String, val name: String) : KeyEventTriggerDevice() { + override fun compareTo(other: KeyEventTriggerDevice): Int { + if (other !is External) { + return super.compareTo(other) + } + + return compareValuesBy( + this, + other, + { it.name }, + { it.descriptor }, + ) + } + } + + fun isSameDevice(other: KeyEventTriggerDevice): Boolean { + if (this is Any || other is Any) { + return true + } + + if (this is External && other is External) { + return this.descriptor == other.descriptor + } else { + return this is Internal && other is Internal + } + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt new file mode 100644 index 0000000000..5cefbd600f --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/KeyEventTriggerKey.kt @@ -0,0 +1,135 @@ +package io.github.sds100.keymapper.base.trigger + +import io.github.sds100.keymapper.base.keymaps.ClickType +import io.github.sds100.keymapper.common.utils.hasFlag +import io.github.sds100.keymapper.common.utils.withFlag +import io.github.sds100.keymapper.data.entities.KeyEventTriggerKeyEntity +import io.github.sds100.keymapper.data.entities.TriggerKeyEntity +import kotlinx.serialization.Serializable +import java.util.UUID + +@Serializable +data class KeyEventTriggerKey( + override val uid: String = UUID.randomUUID().toString(), + override val keyCode: Int, + val device: KeyEventTriggerDevice, + override val clickType: ClickType, + override val consumeEvent: Boolean = true, + /** + * Whether this key can only be detected by an input method. Some keys, such as DPAD buttons, + * do not send key events to the accessibility service. + */ + val requiresIme: Boolean = false, + override val scanCode: Int? = null, + override val detectWithScanCodeUserSetting: Boolean = false, +) : TriggerKey(), KeyCodeTriggerKey { + + override val allowedLongPress: Boolean = true + override val allowedDoublePress: Boolean = true + + override fun isSameDevice(otherKey: KeyCodeTriggerKey): Boolean { + if (otherKey !is KeyEventTriggerKey) { + return false + } + return device.isSameDevice(otherKey.device) + } + + // key code -> click type -> device -> consume key event + override fun compareTo(other: TriggerKey) = when (other) { + is KeyEventTriggerKey -> compareValuesBy( + this, + other, + { it.keyCode }, + { it.clickType }, + { it.device }, + { it.consumeEvent }, + ) + + else -> super.compareTo(other) + } + + companion object { + fun fromEntity(entity: KeyEventTriggerKeyEntity): TriggerKey { + val device = when (entity.deviceId) { + KeyEventTriggerKeyEntity.DEVICE_ID_THIS_DEVICE -> KeyEventTriggerDevice.Internal + KeyEventTriggerKeyEntity.DEVICE_ID_ANY_DEVICE -> KeyEventTriggerDevice.Any + else -> KeyEventTriggerDevice.External( + entity.deviceId, + entity.deviceName ?: "", + ) + } + + val clickType = when (entity.clickType) { + TriggerKeyEntity.SHORT_PRESS -> ClickType.SHORT_PRESS + TriggerKeyEntity.LONG_PRESS -> ClickType.LONG_PRESS + TriggerKeyEntity.DOUBLE_PRESS -> ClickType.DOUBLE_PRESS + else -> ClickType.SHORT_PRESS + } + + val consumeEvent = + !entity.flags.hasFlag(KeyEventTriggerKeyEntity.FLAG_DO_NOT_CONSUME_KEY_EVENT) + + val requiresIme = + entity.flags.hasFlag(KeyEventTriggerKeyEntity.FLAG_DETECTION_SOURCE_INPUT_METHOD) + + val detectWithScancode = + entity.flags.hasFlag(KeyEventTriggerKeyEntity.FLAG_DETECT_WITH_SCAN_CODE) + + return KeyEventTriggerKey( + uid = entity.uid, + keyCode = entity.keyCode, + device = device, + clickType = clickType, + consumeEvent = consumeEvent, + requiresIme = requiresIme, + scanCode = entity.scanCode, + detectWithScanCodeUserSetting = detectWithScancode, + ) + } + + fun toEntity(key: KeyEventTriggerKey): KeyEventTriggerKeyEntity { + val deviceId = when (key.device) { + KeyEventTriggerDevice.Any -> KeyEventTriggerKeyEntity.DEVICE_ID_ANY_DEVICE + is KeyEventTriggerDevice.External -> key.device.descriptor + KeyEventTriggerDevice.Internal -> KeyEventTriggerKeyEntity.DEVICE_ID_THIS_DEVICE + } + + val deviceName = + if (key.device is KeyEventTriggerDevice.External) { + key.device.name + } else { + null + } + + val clickType = when (key.clickType) { + ClickType.SHORT_PRESS -> TriggerKeyEntity.SHORT_PRESS + ClickType.LONG_PRESS -> TriggerKeyEntity.LONG_PRESS + ClickType.DOUBLE_PRESS -> TriggerKeyEntity.DOUBLE_PRESS + } + + var flags = 0 + + if (!key.consumeEvent) { + flags = flags.withFlag(KeyEventTriggerKeyEntity.FLAG_DO_NOT_CONSUME_KEY_EVENT) + } + + if (key.requiresIme) { + flags = flags.withFlag(KeyEventTriggerKeyEntity.FLAG_DETECTION_SOURCE_INPUT_METHOD) + } + + if (key.detectWithScanCodeUserSetting) { + flags = flags.withFlag(KeyEventTriggerKeyEntity.FLAG_DETECT_WITH_SCAN_CODE) + } + + return KeyEventTriggerKeyEntity( + keyCode = key.keyCode, + deviceId = deviceId, + deviceName = deviceName, + clickType = clickType, + flags = flags, + uid = key.uid, + scanCode = key.scanCode, + ) + } + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerButtonRow.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerButtonRow.kt index 09aabd1396..d8db9346d2 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerButtonRow.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerButtonRow.kt @@ -1,19 +1,33 @@ package io.github.sds100.keymapper.base.trigger +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.TextAutoSize import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Surface import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -39,45 +53,47 @@ fun RecordTriggerButtonRow( showAdvancedTriggerTapTarget: Boolean = false, onAdvancedTriggerTapTargetCompleted: () -> Unit = {}, ) { - Row(modifier, verticalAlignment = Alignment.CenterVertically) { - IntroShowcase( - showIntroShowCase = showRecordTriggerTapTarget, - onShowCaseCompleted = onRecordTriggerTapTargetCompleted, - dismissOnClickOutside = true, - ) { - RecordTriggerButton( - modifier = Modifier - .weight(1f) - .introShowCaseTarget(0, style = keyMapperShowcaseStyle()) { - KeyMapperTapTarget( - OnboardingTapTarget.RECORD_TRIGGER, - onSkipClick = onSkipTapTarget, - ) - }, - recordTriggerState, - onClick = onRecordTriggerClick, - ) - } + Column { + Row(modifier, verticalAlignment = Alignment.CenterVertically) { + IntroShowcase( + showIntroShowCase = showRecordTriggerTapTarget, + onShowCaseCompleted = onRecordTriggerTapTargetCompleted, + dismissOnClickOutside = true, + ) { + RecordTriggerButton( + modifier = Modifier + .weight(1f) + .introShowCaseTarget(0, style = keyMapperShowcaseStyle()) { + KeyMapperTapTarget( + OnboardingTapTarget.RECORD_TRIGGER, + onSkipClick = onSkipTapTarget, + ) + }, + recordTriggerState, + onClick = onRecordTriggerClick, + ) + } - Spacer(modifier = Modifier.width(8.dp)) + Spacer(modifier = Modifier.width(8.dp)) - IntroShowcase( - showIntroShowCase = showAdvancedTriggerTapTarget, - onShowCaseCompleted = onAdvancedTriggerTapTargetCompleted, - dismissOnClickOutside = true, - ) { - AdvancedTriggersButton( - modifier = Modifier - .weight(1f) - .introShowCaseTarget(0, style = keyMapperShowcaseStyle()) { - KeyMapperTapTarget( - OnboardingTapTarget.ADVANCED_TRIGGERS, - showSkipButton = false, - ) - }, - isEnabled = recordTriggerState !is RecordTriggerState.CountingDown, - onClick = onAdvancedTriggersClick, - ) + IntroShowcase( + showIntroShowCase = showAdvancedTriggerTapTarget, + onShowCaseCompleted = onAdvancedTriggerTapTargetCompleted, + dismissOnClickOutside = true, + ) { + AdvancedTriggersButton( + modifier = Modifier + .weight(1f) + .introShowCaseTarget(0, style = keyMapperShowcaseStyle()) { + KeyMapperTapTarget( + OnboardingTapTarget.ADVANCED_TRIGGERS, + showSkipButton = false, + ) + }, + isEnabled = recordTriggerState !is RecordTriggerState.CountingDown, + onClick = onAdvancedTriggersClick, + ) + } } } } @@ -101,22 +117,52 @@ private fun RecordTriggerButton( stringResource(R.string.button_record_trigger) } + // Create pulsing animation for the recording dot + val infiniteTransition = rememberInfiniteTransition(label = "recording_dot_pulse") + val alpha by infiniteTransition.animateFloat( + initialValue = 0.3f, + targetValue = 1.0f, + animationSpec = infiniteRepeatable( + animation = tween(1000), + repeatMode = RepeatMode.Reverse, + ), + label = "recording_dot_alpha", + ) + FilledTonalButton( modifier = modifier, onClick = onClick, colors = colors, ) { - BasicText( - text = text, - maxLines = 1, - autoSize = TextAutoSize.StepBased( - minFontSize = 5.sp, - maxFontSize = MaterialTheme.typography.labelLarge.fontSize, - ), - style = MaterialTheme.typography.labelLarge, - color = { colors.contentColor }, - overflow = TextOverflow.Ellipsis, - ) + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + // White recording dot + if (state is RecordTriggerState.CountingDown) { + Box( + modifier = Modifier + .size(8.dp) + .alpha(alpha) + .background( + color = Color.White, + shape = CircleShape, + ), + ) + Spacer(modifier = Modifier.width(8.dp)) + } + + BasicText( + text = text, + maxLines = 1, + autoSize = TextAutoSize.StepBased( + minFontSize = 5.sp, + maxFontSize = MaterialTheme.typography.labelLarge.fontSize, + ), + style = MaterialTheme.typography.labelLarge, + color = { colors.contentColor }, + overflow = TextOverflow.Ellipsis, + ) + } } } @@ -131,7 +177,7 @@ private fun AdvancedTriggersButton( enabled = isEnabled, onClick = onClick, ) { - val color = ButtonDefaults.textButtonColors().contentColor + val color = LocalContentColor.current BasicText( text = stringResource(R.string.button_advanced_triggers), maxLines = 1, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt new file mode 100644 index 0000000000..d369b0710c --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerController.kt @@ -0,0 +1,267 @@ +package io.github.sds100.keymapper.base.trigger + +import android.view.KeyEvent +import io.github.sds100.keymapper.base.detection.DpadMotionEventTracker +import io.github.sds100.keymapper.base.input.InputEventDetectionSource +import io.github.sds100.keymapper.base.input.InputEventHub +import io.github.sds100.keymapper.base.input.InputEventHubCallback +import io.github.sds100.keymapper.common.utils.KMResult +import io.github.sds100.keymapper.common.utils.Success +import io.github.sds100.keymapper.common.utils.isError +import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceAdapter +import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceEvent +import io.github.sds100.keymapper.system.inputevents.KMEvdevEvent +import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent +import io.github.sds100.keymapper.system.inputevents.KMInputEvent +import io.github.sds100.keymapper.system.inputevents.KMKeyEvent +import io.github.sds100.keymapper.system.inputevents.Scancode +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class RecordTriggerControllerImpl @Inject constructor( + private val coroutineScope: CoroutineScope, + private val inputEventHub: InputEventHub, + private val accessibilityServiceAdapter: AccessibilityServiceAdapter, +) : RecordTriggerController, InputEventHubCallback { + companion object { + /** + * How long should the accessibility service record a trigger in seconds. + */ + private const val RECORD_TRIGGER_TIMER_LENGTH = 5 + private const val INPUT_EVENT_HUB_ID = "record_trigger" + + private val SCAN_CODES_BLACKLIST = setOf( + Scancode.BTN_TOUCH, + Scancode.BTN_TOOL_FINGER, + ) + } + + override val state = MutableStateFlow(RecordTriggerState.Idle) + + private var recordingTriggerJob: Job? = null + + private val recordingTrigger: Boolean + get() = recordingTriggerJob != null && recordingTriggerJob?.isActive == true + + private val recordedKeys: MutableList = mutableListOf() + override val onRecordKey: MutableSharedFlow = MutableSharedFlow() + + /** + * The keys that are currently being held down. They are removed from the set when the up event + * is received and it is cleared when recording starts. + */ + private val downKeyEvents: MutableSet = mutableSetOf() + private val downEvdevEvents: MutableSet = mutableSetOf() + private val dpadMotionEventTracker: DpadMotionEventTracker = DpadMotionEventTracker() + + private var isEvdevRecordingEnabled: Boolean = true + + override fun setEvdevRecordingEnabled(enabled: Boolean) { + if (state.value is RecordTriggerState.CountingDown) { + return + } + + isEvdevRecordingEnabled = enabled + } + + override fun onInputEvent( + event: KMInputEvent, + detectionSource: InputEventDetectionSource, + ): Boolean { + if (!recordingTrigger) { + return false + } + + when (event) { + is KMEvdevEvent -> { + if (!isEvdevRecordingEnabled) { + return false + } + + // Do not record evdev events that are not key events. + if (event.type != KMEvdevEvent.TYPE_KEY_EVENT) { + return false + } + + if (SCAN_CODES_BLACKLIST.contains(event.code)) { + return false + } + + // Must also remove old down events if a new down even is received. + val matchingDownEvent: KMEvdevEvent? = downEvdevEvents.find { + it == event.copy(value = KMEvdevEvent.VALUE_DOWN) + } + + if (matchingDownEvent != null) { + downEvdevEvents.remove(matchingDownEvent) + } + + if (event.isDownEvent) { + downEvdevEvents.add(event) + } else if (event.isUpEvent) { + onRecordKey(createEvdevRecordedKey(event)) + Timber.d("Recorded evdev event ${event.code} ${KeyEvent.keyCodeToString(event.androidCode)}") + } + + return true + } + + is KMGamePadEvent -> { + val dpadKeyEvents = dpadMotionEventTracker.convertMotionEvent(event) + + for (keyEvent in dpadKeyEvents) { + if (keyEvent.action == KeyEvent.ACTION_DOWN) { + val recordedKey = createKeyEventRecordedKey( + keyEvent, + detectionSource, + ) + onRecordKey(recordedKey) + Timber.d("Recorded motion event ${KeyEvent.keyCodeToString(keyEvent.keyCode)}") + } + } + return true + } + + is KMKeyEvent -> { + val matchingDownEvent: KMKeyEvent? = downKeyEvents.find { + it.keyCode == event.keyCode && + it.scanCode == event.scanCode && + it.deviceId == event.deviceId && + it.source == event.source + } + + // Must also remove old down events if a new down even is received. + if (matchingDownEvent != null) { + downKeyEvents.remove(matchingDownEvent) + } + + if (event.action == KeyEvent.ACTION_DOWN) { + downKeyEvents.add(event) + } else if (event.action == KeyEvent.ACTION_UP) { + // Only record the key if there is a matching down event. + // Do not do this when recording motion events from the input method + // or Activity because they intentionally only input a down event. + if (matchingDownEvent != null) { + val recordedKey = createKeyEventRecordedKey(event, detectionSource) + onRecordKey(recordedKey) + Timber.d("Recorded key event ${KeyEvent.keyCodeToString(event.keyCode)}") + } + } + return true + } + } + } + + override suspend fun startRecording(): KMResult<*> { + val serviceResult = + accessibilityServiceAdapter.send(AccessibilityServiceEvent.Ping("record_trigger")) + if (serviceResult.isError) { + return serviceResult + } + + if (recordingTrigger) { + return Success(Unit) + } + + recordingTriggerJob = recordTriggerJob() + + return Success(Unit) + } + + override suspend fun stopRecording(): KMResult<*> { + recordingTriggerJob?.cancel() + recordingTriggerJob = null + + dpadMotionEventTracker.reset() + inputEventHub.unregisterClient(INPUT_EVENT_HUB_ID) + state.update { RecordTriggerState.Completed(recordedKeys) } + + return Success(Unit) + } + + private fun onRecordKey(recordedKey: RecordedKey) { + recordedKeys.add(recordedKey) + runBlocking { onRecordKey.emit(recordedKey) } + } + + private fun createKeyEventRecordedKey( + keyEvent: KMKeyEvent, + detectionSource: InputEventDetectionSource, + ): RecordedKey.KeyEvent { + return RecordedKey.KeyEvent( + keyCode = keyEvent.keyCode, + scanCode = keyEvent.scanCode, + deviceDescriptor = keyEvent.device.descriptor, + deviceName = keyEvent.device.name, + isExternalDevice = keyEvent.device.isExternal, + detectionSource = detectionSource, + ) + } + + private fun createEvdevRecordedKey(evdevEvent: KMEvdevEvent): RecordedKey.EvdevEvent { + return RecordedKey.EvdevEvent( + keyCode = evdevEvent.androidCode, + scanCode = evdevEvent.code, + device = evdevEvent.device, + ) + } + + // Run on a different thread in case the main thread is locked up while recording and + // the evdev devices aren't ungrabbed. + private fun recordTriggerJob(): Job = coroutineScope.launch(Dispatchers.Default) { + recordedKeys.clear() + dpadMotionEventTracker.reset() + downKeyEvents.clear() + + // TODO +// if (isEvdevRecordingEnabled && isEvdevRecordingPermitted.value) { + inputEventHub.registerClient( + INPUT_EVENT_HUB_ID, + this@RecordTriggerControllerImpl, + listOf(KMEvdevEvent.TYPE_KEY_EVENT), + ) + + // Grab all evdev devices + inputEventHub.grabAllEvdevDevices(INPUT_EVENT_HUB_ID) +// } + + repeat(RECORD_TRIGGER_TIMER_LENGTH) { iteration -> + val timeLeft = RECORD_TRIGGER_TIMER_LENGTH - iteration + + state.update { RecordTriggerState.CountingDown(timeLeft) } + + delay(1000) + } + + downKeyEvents.clear() + dpadMotionEventTracker.reset() + inputEventHub.unregisterClient(INPUT_EVENT_HUB_ID) + state.update { RecordTriggerState.Completed(recordedKeys) } + } +} + +interface RecordTriggerController { + val state: StateFlow + val onRecordKey: Flow + + fun setEvdevRecordingEnabled(enabled: Boolean) + + /** + * @return Success if started and an Error if failed to start. + */ + suspend fun startRecording(): KMResult<*> + suspend fun stopRecording(): KMResult<*> +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerEvent.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerEvent.kt deleted file mode 100644 index 3c52caf495..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerEvent.kt +++ /dev/null @@ -1,30 +0,0 @@ -package io.github.sds100.keymapper.base.trigger - -import android.os.Parcelable -import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceEvent -import io.github.sds100.keymapper.system.devices.InputDeviceInfo -import kotlinx.parcelize.Parcelize -import kotlinx.serialization.Serializable - -sealed class RecordTriggerEvent : AccessibilityServiceEvent() { - @Parcelize - @Serializable - data class RecordedTriggerKey( - val keyCode: Int, - val device: InputDeviceInfo?, - val detectionSource: KeyEventDetectionSource, - ) : RecordTriggerEvent(), - Parcelable - - @Serializable - data object StartRecordingTrigger : RecordTriggerEvent() - - @Serializable - data object StopRecordingTrigger : RecordTriggerEvent() - - @Serializable - data class OnIncrementRecordTriggerTimer(val timeLeft: Int) : RecordTriggerEvent() - - @Serializable - data object OnStoppedRecordingTrigger : RecordTriggerEvent() -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerUseCase.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerUseCase.kt deleted file mode 100644 index 0564e382c5..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordTriggerUseCase.kt +++ /dev/null @@ -1,132 +0,0 @@ -package io.github.sds100.keymapper.base.trigger - -import android.view.KeyEvent -import io.github.sds100.keymapper.base.keymaps.detection.DpadMotionEventTracker -import io.github.sds100.keymapper.common.utils.KMResult -import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceAdapter -import io.github.sds100.keymapper.system.devices.InputDeviceInfo -import io.github.sds100.keymapper.system.inputevents.InputEventUtils -import io.github.sds100.keymapper.system.inputevents.MyMotionEvent -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class RecordTriggerController @Inject constructor( - private val coroutineScope: CoroutineScope, - private val serviceAdapter: AccessibilityServiceAdapter, -) : RecordTriggerUseCase { - override val state = MutableStateFlow(RecordTriggerState.Idle) - - private val recordedKeys: MutableList = mutableListOf() - override val onRecordKey: MutableSharedFlow = MutableSharedFlow() - private val dpadMotionEventTracker: DpadMotionEventTracker = DpadMotionEventTracker() - - init { - serviceAdapter.eventReceiver.onEach { event -> - when (event) { - is RecordTriggerEvent.OnStoppedRecordingTrigger -> - state.value = - RecordTriggerState.Completed(recordedKeys) - - is RecordTriggerEvent.OnIncrementRecordTriggerTimer -> - state.value = - RecordTriggerState.CountingDown(event.timeLeft) - - else -> Unit - } - }.launchIn(coroutineScope) - - serviceAdapter.eventReceiver - .mapNotNull { - if (it is RecordTriggerEvent.RecordedTriggerKey) { - it - } else { - null - } - } - .map { createRecordedKeyEvent(it.keyCode, it.device, it.detectionSource) } - .onEach { key -> - recordedKeys.add(key) - onRecordKey.emit(key) - } - .launchIn(coroutineScope) - } - - override suspend fun startRecording(): KMResult<*> { - recordedKeys.clear() - dpadMotionEventTracker.reset() - return serviceAdapter.send(RecordTriggerEvent.StartRecordingTrigger) - } - - override suspend fun stopRecording(): KMResult<*> { - return serviceAdapter.send(RecordTriggerEvent.StopRecordingTrigger) - } - - /** - * Process motion events from the activity so that DPAD buttons can be recorded - * even when the Key Mapper IME is not being used. DO NOT record the key events because - * these are sent from the joy sticks. - * @return Whether the motion event is consumed. - */ - fun onActivityMotionEvent(event: MyMotionEvent): Boolean { - if (state.value !is RecordTriggerState.CountingDown) { - return false - } - - val keyEvent = - dpadMotionEventTracker.convertMotionEvent(event).firstOrNull() ?: return false - - if (!InputEventUtils.isDpadKeyCode(keyEvent.keyCode)) { - return false - } - - if (keyEvent.action == KeyEvent.ACTION_UP) { - val recordedKey = createRecordedKeyEvent( - keyEvent.keyCode, - keyEvent.device, - KeyEventDetectionSource.INPUT_METHOD, - ) - - recordedKeys.add(recordedKey) - coroutineScope.launch { - onRecordKey.emit(recordedKey) - } - } - - return true - } - - private fun createRecordedKeyEvent( - keyCode: Int, - device: InputDeviceInfo?, - detectionSource: KeyEventDetectionSource, - ): RecordedKey { - val triggerKeyDevice = if (device != null && device.isExternal) { - TriggerKeyDevice.External(device.descriptor, device.name) - } else { - TriggerKeyDevice.Internal - } - - return RecordedKey(keyCode, triggerKeyDevice, detectionSource) - } -} - -interface RecordTriggerUseCase { - val state: Flow - val onRecordKey: Flow - - /** - * @return Success if started and an Error if failed to start. - */ - suspend fun startRecording(): KMResult<*> - suspend fun stopRecording(): KMResult<*> -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordedKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordedKey.kt index f4db2f265b..cad2f10f2d 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordedKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/RecordedKey.kt @@ -1,7 +1,21 @@ package io.github.sds100.keymapper.base.trigger -data class RecordedKey( - val keyCode: Int, - val device: TriggerKeyDevice, - val detectionSource: KeyEventDetectionSource, -) +import io.github.sds100.keymapper.base.input.InputEventDetectionSource +import io.github.sds100.keymapper.common.models.EvdevDeviceHandle + +sealed class RecordedKey { + data class KeyEvent( + val keyCode: Int, + val scanCode: Int, + val deviceDescriptor: String, + val deviceName: String, + val isExternalDevice: Boolean, + val detectionSource: InputEventDetectionSource, + ) : RecordedKey() + + data class EvdevEvent( + val keyCode: Int, + val scanCode: Int, + val device: EvdevDeviceHandle, + ) : RecordedKey() +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/Trigger.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/Trigger.kt index 2bba80a7ae..73b1dbba0a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/Trigger.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/Trigger.kt @@ -7,13 +7,13 @@ import io.github.sds100.keymapper.common.utils.valueOrNull import io.github.sds100.keymapper.common.utils.withFlag import io.github.sds100.keymapper.data.entities.AssistantTriggerKeyEntity import io.github.sds100.keymapper.data.entities.EntityExtra +import io.github.sds100.keymapper.data.entities.EvdevTriggerKeyEntity import io.github.sds100.keymapper.data.entities.FingerprintTriggerKeyEntity import io.github.sds100.keymapper.data.entities.FloatingButtonEntityWithLayout import io.github.sds100.keymapper.data.entities.FloatingButtonKeyEntity -import io.github.sds100.keymapper.data.entities.KeyCodeTriggerKeyEntity +import io.github.sds100.keymapper.data.entities.KeyEventTriggerKeyEntity import io.github.sds100.keymapper.data.entities.TriggerEntity import io.github.sds100.keymapper.data.entities.getData -import io.github.sds100.keymapper.system.inputevents.InputEventUtils import kotlinx.serialization.Serializable @Serializable @@ -22,7 +22,6 @@ data class Trigger( val mode: TriggerMode = TriggerMode.Undefined, val vibrate: Boolean = false, val longPressDoubleVibration: Boolean = false, - val screenOffTrigger: Boolean = false, val longPressDelay: Int? = null, val doublePressDelay: Int? = null, val vibrateDuration: Int? = null, @@ -41,21 +40,6 @@ data class Trigger( fun isLongPressDoubleVibrationAllowed(): Boolean = (keys.size == 1 || (mode is TriggerMode.Parallel)) && keys.getOrNull(0)?.clickType == ClickType.LONG_PRESS - /** - * Must check that it is not empty otherwise it would be true from the "all" check. - * It is not allowed if the key is an assistant button because it is assumed to be true - * anyway. - */ - fun isDetectingWhenScreenOffAllowed(): Boolean { - return keys.isNotEmpty() && - keys.all { - it is KeyCodeTriggerKey && - InputEventUtils.canDetectKeyWhenScreenOff( - it.keyCode, - ) - } - } - fun isChangingSequenceTriggerTimeoutAllowed(): Boolean = keys.isNotEmpty() && keys.size > 1 && mode is TriggerMode.Sequence fun updateFloatingButtonData(buttons: List): Trigger { @@ -89,7 +73,7 @@ object TriggerEntityMapper { val keys = entity.keys.map { key -> when (key) { is AssistantTriggerKeyEntity -> AssistantTriggerKey.fromEntity(key) - is KeyCodeTriggerKeyEntity -> KeyCodeTriggerKey.fromEntity( + is KeyEventTriggerKeyEntity -> KeyEventTriggerKey.fromEntity( key, ) is FloatingButtonKeyEntity -> { @@ -98,6 +82,7 @@ object TriggerEntityMapper { } is FingerprintTriggerKeyEntity -> FingerprintTriggerKey.fromEntity(key) + is EvdevTriggerKeyEntity -> EvdevTriggerKey.fromEntity(key) } } @@ -130,7 +115,6 @@ object TriggerEntityMapper { triggerFromOtherApps = entity.flags.hasFlag(TriggerEntity.TRIGGER_FLAG_FROM_OTHER_APPS), showToast = entity.flags.hasFlag(TriggerEntity.TRIGGER_FLAG_SHOW_TOAST), - screenOffTrigger = entity.flags.hasFlag(TriggerEntity.TRIGGER_FLAG_SCREEN_OFF_TRIGGERS), ) } @@ -189,10 +173,6 @@ object TriggerEntityMapper { flags = flags.withFlag(TriggerEntity.TRIGGER_FLAG_LONG_PRESS_DOUBLE_VIBRATION) } - if (trigger.isDetectingWhenScreenOffAllowed() && trigger.screenOffTrigger) { - flags = flags.withFlag(TriggerEntity.TRIGGER_FLAG_SCREEN_OFF_TRIGGERS) - } - if (trigger.triggerFromOtherApps) { flags = flags.withFlag(TriggerEntity.TRIGGER_FLAG_FROM_OTHER_APPS) } @@ -204,11 +184,12 @@ object TriggerEntityMapper { val keys = trigger.keys.map { key -> when (key) { is AssistantTriggerKey -> AssistantTriggerKey.toEntity(key) - is KeyCodeTriggerKey -> KeyCodeTriggerKey.toEntity( + is KeyEventTriggerKey -> KeyEventTriggerKey.toEntity( key, ) is FloatingButtonKey -> FloatingButtonKey.toEntity(key) is FingerprintTriggerKey -> FingerprintTriggerKey.toEntity(key) + is EvdevTriggerKey -> EvdevTriggerKey.toEntity(key) } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerError.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerError.kt index b50cc06efe..dd3727110a 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerError.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerError.kt @@ -2,7 +2,6 @@ package io.github.sds100.keymapper.base.trigger enum class TriggerError(val isFixable: Boolean) { DND_ACCESS_DENIED(isFixable = true), - SCREEN_OFF_ROOT_DENIED(isFixable = true), CANT_DETECT_IN_PHONE_CALL(isFixable = true), // This error appears when a key map has an assistant trigger but the user hasn't purchased diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerErrorSnapshot.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerErrorSnapshot.kt index 70184806ee..d16971dfa0 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerErrorSnapshot.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerErrorSnapshot.kt @@ -1,6 +1,5 @@ package io.github.sds100.keymapper.base.trigger -import android.os.Build import android.view.KeyEvent import io.github.sds100.keymapper.base.keymaps.KeyMap import io.github.sds100.keymapper.base.keymaps.requiresImeKeyEventForwardingInPhoneCall @@ -9,7 +8,7 @@ import io.github.sds100.keymapper.base.purchasing.PurchasingError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.onFailure import io.github.sds100.keymapper.common.utils.onSuccess -import io.github.sds100.keymapper.system.inputevents.InputEventUtils +import io.github.sds100.keymapper.system.inputevents.KeyEventUtils /** * Store the data required for determining trigger errors to reduce the number of calls with @@ -54,27 +53,17 @@ data class TriggerErrorSnapshot( } val requiresDndAccess = - key is KeyCodeTriggerKey && key.keyCode in keysThatRequireDndAccess + key is KeyEventTriggerKey && key.keyCode in keysThatRequireDndAccess if (requiresDndAccess) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !isDndAccessGranted) { + if (!isDndAccessGranted) { return TriggerError.DND_ACCESS_DENIED } } - if (keyMap.trigger.screenOffTrigger && - !isRootGranted && - keyMap.trigger.isDetectingWhenScreenOffAllowed() - ) { - return TriggerError.SCREEN_OFF_ROOT_DENIED - } - val containsDpadKey = - key is KeyCodeTriggerKey && - InputEventUtils.isDpadKeyCode( - key.keyCode, - ) && - key.detectionSource == KeyEventDetectionSource.INPUT_METHOD + key is KeyEventTriggerKey && + KeyEventUtils.isDpadKeyCode(key.keyCode) && key.requiresIme if (showDpadImeSetupError && !isKeyMapperImeChosen && containsDpadKey) { return TriggerError.DPAD_IME_NOT_SELECTED diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKey.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKey.kt index 1239059181..5845cb546c 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKey.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKey.kt @@ -7,22 +7,43 @@ import kotlinx.serialization.Serializable sealed class TriggerKey : Comparable { abstract val clickType: ClickType - /** - * Whether the event that triggers this key will be consumed and not passed - * onto subsequent apps. E.g consuming the volume down key event will mean the volume - * doesn't change. - */ - abstract val consumeEvent: Boolean abstract val uid: String abstract val allowedLongPress: Boolean abstract val allowedDoublePress: Boolean fun setClickType(clickType: ClickType): TriggerKey = when (this) { is AssistantTriggerKey -> copy(clickType = clickType) - is KeyCodeTriggerKey -> copy(clickType = clickType) + is KeyEventTriggerKey -> copy(clickType = clickType) is FloatingButtonKey -> copy(clickType = clickType) is FingerprintTriggerKey -> copy(clickType = clickType) + is EvdevTriggerKey -> copy(clickType = clickType) } override fun compareTo(other: TriggerKey) = this.javaClass.name.compareTo(other.javaClass.name) + + fun isLogicallyEqual(other: TriggerKey): Boolean { + when { + this is KeyCodeTriggerKey && other is KeyCodeTriggerKey -> { + if (this.detectWithScancode()) { + return this.scanCode == other.scanCode && this.isSameDevice(other) + } else { + return this.keyCode == other.keyCode && this.isSameDevice(other) + } + } + + this is AssistantTriggerKey && other is AssistantTriggerKey -> { + return this.type == other.type + } + + this is FingerprintTriggerKey && other is FingerprintTriggerKey -> { + return this.type == other.type + } + + this is FloatingButtonKey && other is FloatingButtonKey -> { + return this.buttonUid == other.buttonUid + } + } + + return false + } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyDevice.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyDevice.kt deleted file mode 100644 index 08d7942721..0000000000 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyDevice.kt +++ /dev/null @@ -1,47 +0,0 @@ -package io.github.sds100.keymapper.base.trigger - -import io.github.sds100.keymapper.system.devices.InputDeviceInfo -import kotlinx.serialization.Serializable - -@Serializable -sealed class TriggerKeyDevice : Comparable { - override fun compareTo(other: TriggerKeyDevice) = this.javaClass.name.compareTo(other.javaClass.name) - - @Serializable - data object Internal : TriggerKeyDevice() - - @Serializable - data object Any : TriggerKeyDevice() - - @Serializable - data class External(val descriptor: String, val name: String) : TriggerKeyDevice() { - override fun compareTo(other: TriggerKeyDevice): Int { - if (other !is External) { - return super.compareTo(other) - } - - return compareValuesBy( - this, - other, - { it.name }, - { it.descriptor }, - ) - } - } - - fun isSameDevice(other: TriggerKeyDevice): Boolean { - if (other is External && this is External) { - return other.descriptor == this.descriptor - } else { - return true - } - } - - fun isSameDevice(device: InputDeviceInfo): Boolean { - if (this is External && device.isExternal) { - return device.descriptor == this.descriptor - } else { - return true - } - } -} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt index 4e38181428..7198f65e61 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyListItem.kt @@ -46,6 +46,8 @@ import io.github.sds100.keymapper.base.keymaps.ClickType import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType import io.github.sds100.keymapper.base.utils.ui.LinkType import io.github.sds100.keymapper.base.utils.ui.compose.DragDropState +import io.github.sds100.keymapper.base.utils.ui.compose.icons.KeyMapperIcons +import io.github.sds100.keymapper.base.utils.ui.compose.icons.ProModeIcon @Composable fun TriggerKeyListItem( @@ -112,6 +114,7 @@ fun TriggerKeyListItem( is TriggerKeyListItemModel.Assistant -> Icons.Outlined.Assistant is TriggerKeyListItemModel.FloatingButton -> Icons.Outlined.BubbleChart is TriggerKeyListItemModel.FingerprintGesture -> Icons.Outlined.Fingerprint + is TriggerKeyListItemModel.EvdevEvent -> KeyMapperIcons.ProModeIcon else -> null } @@ -137,7 +140,8 @@ fun TriggerKeyListItem( model.buttonName, ) - is TriggerKeyListItemModel.KeyCode -> model.keyName + is TriggerKeyListItemModel.KeyEvent -> model.keyName + is TriggerKeyListItemModel.EvdevEvent -> model.keyName is TriggerKeyListItemModel.FloatingButtonDeleted -> stringResource(R.string.trigger_error_floating_button_deleted_title) @@ -159,7 +163,8 @@ fun TriggerKeyListItem( } val tertiaryText = when (model) { - is TriggerKeyListItemModel.KeyCode -> model.extraInfo + is TriggerKeyListItemModel.KeyEvent -> model.extraInfo + is TriggerKeyListItemModel.EvdevEvent -> model.extraInfo is TriggerKeyListItemModel.FloatingButton -> model.layoutName else -> null @@ -253,7 +258,6 @@ fun TriggerKeyListItem( private fun getErrorMessage(error: TriggerError): String { return when (error) { TriggerError.DND_ACCESS_DENIED -> stringResource(R.string.trigger_error_dnd_access_denied) - TriggerError.SCREEN_OFF_ROOT_DENIED -> stringResource(R.string.trigger_error_screen_off_root_permission_denied) TriggerError.CANT_DETECT_IN_PHONE_CALL -> stringResource(R.string.trigger_error_cant_detect_in_phone_call) TriggerError.ASSISTANT_TRIGGER_NOT_PURCHASED -> stringResource(R.string.trigger_error_assistant_not_purchased) TriggerError.DPAD_IME_NOT_SELECTED -> stringResource(R.string.trigger_error_dpad_ime_not_selected) @@ -322,9 +326,9 @@ private fun ErrorTextColumn( @Preview @Composable -private fun KeyCodePreview() { +private fun KeyEventPreview() { TriggerKeyListItem( - model = TriggerKeyListItemModel.KeyCode( + model = TriggerKeyListItemModel.KeyEvent( id = "id", keyName = "Volume Up", clickType = ClickType.SHORT_PRESS, @@ -338,11 +342,29 @@ private fun KeyCodePreview() { ) } +@Preview +@Composable +private fun EvdevEventPreview() { + TriggerKeyListItem( + model = TriggerKeyListItemModel.EvdevEvent( + id = "id", + keyName = "Volume Up", + clickType = ClickType.SHORT_PRESS, + extraInfo = "Gpio-keys", + linkType = LinkType.ARROW, + error = null, + ), + isDragging = false, + isReorderingEnabled = true, + index = 0, + ) +} + @Preview @Composable private fun NoDragPreview() { TriggerKeyListItem( - model = TriggerKeyListItemModel.KeyCode( + model = TriggerKeyListItemModel.KeyEvent( id = "id", keyName = "Volume Up", clickType = ClickType.LONG_PRESS, diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt index e104ecf8c9..6650c95883 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerKeyOptionsBottomSheet.kt @@ -1,8 +1,8 @@ package io.github.sds100.keymapper.base.trigger +import android.view.KeyEvent import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -23,6 +23,7 @@ import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.SheetState import androidx.compose.material3.SheetValue.Expanded import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment @@ -32,16 +33,21 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.window.core.layout.WindowHeightSizeClass +import androidx.window.core.layout.WindowWidthSizeClass import io.github.sds100.keymapper.base.R import io.github.sds100.keymapper.base.compose.KeyMapperTheme import io.github.sds100.keymapper.base.keymaps.ClickType import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType import io.github.sds100.keymapper.base.utils.ui.CheckBoxListItem import io.github.sds100.keymapper.base.utils.ui.compose.CheckBoxText +import io.github.sds100.keymapper.base.utils.ui.compose.KeyMapperSegmentedButtonRow import io.github.sds100.keymapper.base.utils.ui.compose.RadioButtonText import io.github.sds100.keymapper.base.utils.ui.compose.openUriSafe +import io.github.sds100.keymapper.system.inputevents.Scancode import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @@ -58,7 +64,10 @@ fun TriggerKeyOptionsBottomSheet( onSelectFingerprintGestureType: (FingerprintGestureType) -> Unit = {}, onEditFloatingButtonClick: () -> Unit = {}, onEditFloatingLayoutClick: () -> Unit = {}, + onScanCodeDetectionChanged: (Boolean) -> Unit = {}, ) { + val isCompact = isVerticalCompactLayout() + ModalBottomSheet( modifier = modifier, onDismissRequest = onDismissRequest, @@ -66,37 +75,42 @@ fun TriggerKeyOptionsBottomSheet( // Hide drag handle because other bottom sheets don't have it dragHandle = {}, ) { - val uriHandler = LocalUriHandler.current - val ctx = LocalContext.current - val helpUrl = stringResource(R.string.url_trigger_key_options_guide) val scope = rememberCoroutineScope() Column(modifier = Modifier.verticalScroll(rememberScrollState())) { Spacer(modifier = Modifier.height(12.dp)) Box(modifier = Modifier.fillMaxWidth()) { Text( - modifier = Modifier.align(Alignment.Center), + modifier = Modifier + .align(Alignment.Center) + .fillMaxWidth() + .padding(horizontal = 48.dp), textAlign = TextAlign.Center, text = stringResource(R.string.trigger_key_options_title), style = MaterialTheme.typography.headlineMedium, + overflow = TextOverflow.Ellipsis, ) - IconButton( + HelpIconButton( modifier = Modifier .align(Alignment.TopEnd) .padding(horizontal = 8.dp), - onClick = { uriHandler.openUriSafe(ctx, helpUrl) }, - ) { - Icon( - imageVector = Icons.AutoMirrored.Rounded.HelpOutline, - contentDescription = null, - ) - } + ) } Spacer(modifier = Modifier.height(8.dp)) - if (state is TriggerKeyOptionsState.KeyCode) { + if (state is TriggerKeyOptionsState.KeyEvent) { + ScanCodeDetectionButtonRow( + modifier = Modifier.fillMaxWidth(), + isEnabled = state.isScanCodeSettingEnabled, + isScanCodeSelected = state.isScanCodeDetectionSelected, + keyCode = state.keyCode, + scanCode = state.scanCode, + onSelectedChange = onScanCodeDetectionChanged, + isCompact = isCompact, + ) + CheckBoxText( modifier = Modifier.padding(8.dp), text = stringResource(R.string.flag_dont_override_default_action), @@ -105,54 +119,47 @@ fun TriggerKeyOptionsBottomSheet( ) } - if (state.showClickTypes) { - Text( - modifier = Modifier.padding(horizontal = 16.dp), - text = stringResource(R.string.trigger_key_click_types_header), - style = MaterialTheme.typography.titleSmall, + if (state is TriggerKeyOptionsState.EvdevEvent) { + ScanCodeDetectionButtonRow( + modifier = Modifier.fillMaxWidth(), + isEnabled = state.isScanCodeSettingEnabled, + isScanCodeSelected = state.isScanCodeDetectionSelected, + keyCode = state.keyCode, + scanCode = state.scanCode, + onSelectedChange = onScanCodeDetectionChanged, + isCompact = isCompact, + ) + + CheckBoxText( + modifier = Modifier.padding(8.dp), + text = stringResource(R.string.flag_dont_override_default_action), + isChecked = state.doNotRemapChecked, + onCheckedChange = onCheckDoNotRemap, ) + } - FlowRow( + if (state.showClickTypes) { + ClickTypeSection( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 8.dp), - ) { - Spacer(Modifier.width(8.dp)) - - RadioButtonText( - modifier = Modifier.weight(1f), - isSelected = state.clickType == ClickType.SHORT_PRESS, - text = stringResource(R.string.radio_button_short_press), - onSelected = { onSelectClickType(ClickType.SHORT_PRESS) }, - ) - - if (state.showLongPressClickType) { - RadioButtonText( - modifier = Modifier.weight(1f), - isSelected = state.clickType == ClickType.LONG_PRESS, - text = stringResource(R.string.radio_button_long_press), - onSelected = { onSelectClickType(ClickType.LONG_PRESS) }, - ) - } - - RadioButtonText( - modifier = Modifier.weight(1f), - isSelected = state.clickType == ClickType.DOUBLE_PRESS, - text = stringResource(R.string.radio_button_double_press), - onSelected = { onSelectClickType(ClickType.DOUBLE_PRESS) }, - ) + .padding(horizontal = 16.dp), + state, + onSelectClickType, + isCompact = isCompact, + ) - Spacer(Modifier.width(8.dp)) - } + Spacer(Modifier.height(8.dp)) } - if (state is TriggerKeyOptionsState.KeyCode) { + if (state is TriggerKeyOptionsState.KeyEvent) { Text( modifier = Modifier.padding(horizontal = 16.dp), text = stringResource(R.string.trigger_key_device_header), style = MaterialTheme.typography.titleSmall, ) + Spacer(Modifier.height(8.dp)) + for (device in state.devices) { RadioButtonText( modifier = Modifier.padding(horizontal = 8.dp), @@ -283,10 +290,151 @@ fun TriggerKeyOptionsBottomSheet( } } +@Composable +private fun HelpIconButton( + modifier: Modifier, +) { + val uriHandler = LocalUriHandler.current + val helpUrl = stringResource(R.string.url_trigger_key_options_guide) + val ctx = LocalContext.current + + IconButton( + modifier = modifier, + onClick = { uriHandler.openUriSafe(ctx, helpUrl) }, + ) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.HelpOutline, + contentDescription = null, + ) + } +} + +@Composable +private fun ClickTypeSection( + modifier: Modifier, + state: TriggerKeyOptionsState, + onSelectClickType: (ClickType) -> Unit, + isCompact: Boolean, +) { + Column(modifier) { + Text( + text = stringResource(R.string.trigger_key_click_types_header), + style = MaterialTheme.typography.titleSmall, + ) + + Spacer(Modifier.height(8.dp)) + + val clickTypeButtonContent: List> = buildList { + add(ClickType.SHORT_PRESS to stringResource(R.string.radio_button_short_press)) + if (state.showLongPressClickType) { + add(ClickType.LONG_PRESS to stringResource(R.string.radio_button_long_press)) + } + add(ClickType.DOUBLE_PRESS to stringResource(R.string.radio_button_double_press)) + } + + KeyMapperSegmentedButtonRow( + modifier = Modifier.fillMaxWidth(), + buttonStates = clickTypeButtonContent, + selectedState = state.clickType, + onStateSelected = onSelectClickType, + isCompact = isCompact, + ) + } +} + +@Composable +private fun ScanCodeDetectionButtonRow( + modifier: Modifier = Modifier, + keyCode: Int, + scanCode: Int?, + isEnabled: Boolean, + isScanCodeSelected: Boolean, + onSelectedChange: (Boolean) -> Unit, + isCompact: Boolean, +) { + Column(modifier) { + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = stringResource(R.string.trigger_scan_code_detection_explanation), + style = MaterialTheme.typography.bodyMedium, + ) + Spacer(Modifier.height(8.dp)) + + val buttonStates = listOf( + false to if (keyCode == KeyEvent.KEYCODE_UNKNOWN) { + stringResource(R.string.trigger_use_key_code_button_disabled) + } else { + stringResource(R.string.trigger_use_key_code_button_enabled, keyCode) + }, + true to if (scanCode == null) { + stringResource(R.string.trigger_use_scan_code_button_disabled) + } else { + stringResource(R.string.trigger_use_scan_code_button_enabled, scanCode) + }, + ) + + KeyMapperSegmentedButtonRow( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + buttonStates = buttonStates, + selectedState = isScanCodeSelected, + onStateSelected = onSelectedChange, + isCompact = isCompact, + isEnabled = isEnabled, + ) + } +} + +@Composable +private fun isVerticalCompactLayout(): Boolean { + val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass + + return windowSizeClass.windowHeightSizeClass == WindowHeightSizeClass.COMPACT && windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT +} + @OptIn(ExperimentalMaterial3Api::class) @Preview @Composable -private fun Preview() { +private fun PreviewKeyEvent() { + KeyMapperTheme { + val sheetState = SheetState( + skipPartiallyExpanded = true, + density = LocalDensity.current, + initialValue = Expanded, + ) + + TriggerKeyOptionsBottomSheet( + sheetState = sheetState, + state = TriggerKeyOptionsState.KeyEvent( + doNotRemapChecked = true, + clickType = ClickType.DOUBLE_PRESS, + showClickTypes = true, + devices = listOf( + CheckBoxListItem( + id = "id1", + label = "Device 1", + isChecked = true, + ), + CheckBoxListItem( + id = "id2", + label = "Device 2", + isChecked = false, + ), + ), + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + isScanCodeDetectionSelected = true, + isScanCodeSettingEnabled = true, + ), + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview(heightDp = 400, widthDp = 300) +@Composable +private fun PreviewKeyEventTiny() { KeyMapperTheme { val sheetState = SheetState( skipPartiallyExpanded = true, @@ -296,7 +444,7 @@ private fun Preview() { TriggerKeyOptionsBottomSheet( sheetState = sheetState, - state = TriggerKeyOptionsState.KeyCode( + state = TriggerKeyOptionsState.KeyEvent( doNotRemapChecked = true, clickType = ClickType.DOUBLE_PRESS, showClickTypes = true, @@ -312,6 +460,36 @@ private fun Preview() { isChecked = false, ), ), + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + isScanCodeDetectionSelected = true, + isScanCodeSettingEnabled = true, + ), + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Composable +private fun PreviewEvdev() { + KeyMapperTheme { + val sheetState = SheetState( + skipPartiallyExpanded = true, + density = LocalDensity.current, + initialValue = Expanded, + ) + + TriggerKeyOptionsBottomSheet( + sheetState = sheetState, + state = TriggerKeyOptionsState.EvdevEvent( + doNotRemapChecked = true, + clickType = ClickType.DOUBLE_PRESS, + showClickTypes = true, + keyCode = KeyEvent.KEYCODE_UNKNOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + isScanCodeDetectionSelected = true, + isScanCodeSettingEnabled = false, ), ) } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerValidator.kt b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerValidator.kt new file mode 100644 index 0000000000..26bdd87986 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/trigger/TriggerValidator.kt @@ -0,0 +1,100 @@ +package io.github.sds100.keymapper.base.trigger + +import io.github.sds100.keymapper.base.keymaps.ClickType + +/** + * Checks that the trigger is still valid. If it is a parallel trigger it removes + * conflicting keys that can't both be pressed down at the same time. And if it is a single + * key trigger then it will check it has one key. If not then it will convert it into a sequence + * trigger and not a parallel trigger so no keys are removed. + */ +fun Trigger.validate(): Trigger { + when (this.mode) { + is TriggerMode.Parallel -> { + return validateParallelTrigger(this) + } + + TriggerMode.Undefined -> { + return validateSingleKeyTrigger(this) + } + + TriggerMode.Sequence -> { + // No validation needed for sequence triggers. Any keys can be pressed in any order + if (keys.size <= 1) { + return copy(mode = TriggerMode.Undefined) + } + + return this + } + } +} + +private fun validateSingleKeyTrigger(trigger: Trigger): Trigger { + if (trigger.keys.size > 1) { + // If there are multiple keys then we convert it to a sequence trigger + return Trigger(mode = TriggerMode.Sequence, keys = trigger.keys.take(1)) + } else { + return trigger + } +} + +private fun validateParallelTrigger(trigger: Trigger): Trigger { + if (trigger.keys.size <= 1) { + return trigger.copy(mode = TriggerMode.Undefined) + } + + var newMode = trigger.mode + + outerLoop@ for (key in trigger.keys) { + // If there are conflicting keys then set the mode to sequence trigger + for (otherKey in trigger.keys) { + if (key != otherKey && key.isLogicallyEqual(otherKey)) { + newMode = TriggerMode.Sequence + break@outerLoop + } + } + + // Set the trigger mode to a short press if any keys are not compatible with the selected + // trigger mode + if (newMode is TriggerMode.Parallel) { + if ( + (newMode.clickType == ClickType.LONG_PRESS && !key.allowedLongPress) || + (newMode.clickType == ClickType.DOUBLE_PRESS && !key.allowedDoublePress) + ) { + newMode = TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS) + } + } + } + + var newKeys = trigger.keys + + // If the trigger is still a parallel trigger then check that all the keys can be + // pressed at the same time. + if (newMode is TriggerMode.Parallel) { + newKeys = trigger.keys.distinctBy { key -> + when (key) { + // You can't mix assistant trigger types in a parallel trigger because there is no notion of a "down" key event, which means they can't be pressed at the same time + is AssistantTriggerKey, is FingerprintTriggerKey -> 0 + is FloatingButtonKey -> key.buttonUid + + is KeyEventTriggerKey -> { + if (key.detectWithScancode()) { + Pair(key.scanCode, key.device) + } else { + Pair(key.keyCode, key.device) + } + } + + is EvdevTriggerKey -> { + if (key.detectWithScancode()) { + Pair(key.scanCode, key.device) + } else { + Pair(key.keyCode, key.device) + } + } + } + }.toMutableList() + } + + return trigger.copy(mode = newMode, keys = newKeys) +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ErrorUtils.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ErrorUtils.kt index d419568ebf..e0fb856f14 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ErrorUtils.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ErrorUtils.kt @@ -8,6 +8,7 @@ import io.github.sds100.keymapper.base.utils.ui.ResourceProvider import io.github.sds100.keymapper.common.utils.BuildUtils import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.data.DataError +import io.github.sds100.keymapper.sysbridge.utils.SystemBridgeError import io.github.sds100.keymapper.system.SystemError import io.github.sds100.keymapper.system.permissions.Permission @@ -55,7 +56,10 @@ fun KMError.getFullMessage(resourceProvider: ResourceProvider): String { PackageManager.FEATURE_BLUETOOTH -> resourceProvider.getString(R.string.error_system_feature_bluetooth_unsupported) PackageManager.FEATURE_DEVICE_ADMIN -> resourceProvider.getString(R.string.error_system_feature_device_admin_unsupported) PackageManager.FEATURE_CAMERA_FLASH -> resourceProvider.getString(R.string.error_system_feature_camera_flash_unsupported) - PackageManager.FEATURE_TELEPHONY -> resourceProvider.getString(R.string.error_system_feature_telephony_unsupported) + PackageManager.FEATURE_TELEPHONY, PackageManager.FEATURE_TELEPHONY_DATA -> resourceProvider.getString( + R.string.error_system_feature_telephony_unsupported, + ) + else -> throw Exception("Don't know how to get error message for this system feature ${this.feature}") } @@ -85,7 +89,11 @@ fun KMError.getFullMessage(resourceProvider: ResourceProvider): String { is KMError.Exception -> exception.toString() is KMError.EmptyJson -> resourceProvider.getString(R.string.error_empty_json) is KMError.InvalidNumber -> resourceProvider.getString(R.string.error_invalid_number) - is KMError.NumberTooSmall -> resourceProvider.getString(R.string.error_number_too_small, min) + is KMError.NumberTooSmall -> resourceProvider.getString( + R.string.error_number_too_small, + min, + ) + is KMError.NumberTooBig -> resourceProvider.getString(R.string.error_number_too_big, max) is KMError.EmptyText -> resourceProvider.getString(R.string.error_cant_be_empty) KMError.BackupVersionTooNew -> resourceProvider.getString(R.string.error_backup_version_too_new) @@ -175,6 +183,7 @@ fun KMError.getFullMessage(resourceProvider: ResourceProvider): String { KMError.InvalidBackup -> resourceProvider.getString(R.string.error_invalid_backup) KMError.MalformedUrl -> resourceProvider.getString(R.string.error_malformed_url) KMError.UiElementNotFound -> resourceProvider.getString(R.string.error_ui_element_not_found) + is SystemBridgeError.Disconnected -> resourceProvider.getString(R.string.error_system_bridge_disconnected) PurchasingError.PurchasingProcessError.Cancelled -> resourceProvider.getString( R.string.purchasing_error_cancelled, @@ -208,7 +217,7 @@ fun KMError.getFullMessage(resourceProvider: ResourceProvider): String { } PurchasingError.PurchasingNotImplemented -> resourceProvider.getString(R.string.purchasing_error_not_implemented) - else -> throw IllegalArgumentException("Unknown error $this") + else -> this.toString() } } @@ -224,7 +233,7 @@ val KMError.isFixable: Boolean is SystemError.PermissionDenied, is KMError.ShizukuNotStarted, is KMError.CantDetectKeyEventsInPhoneCall, - + is SystemBridgeError.Disconnected, -> true else -> false diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/InputEventStrings.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/KeyCodeStrings.kt similarity index 97% rename from base/src/main/java/io/github/sds100/keymapper/base/utils/InputEventStrings.kt rename to base/src/main/java/io/github/sds100/keymapper/base/utils/KeyCodeStrings.kt index ea2bf1c8f0..b74936b4e9 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/InputEventStrings.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/KeyCodeStrings.kt @@ -2,9 +2,8 @@ package io.github.sds100.keymapper.base.utils import android.view.KeyEvent import io.github.sds100.keymapper.base.R -import io.github.sds100.keymapper.system.inputevents.InputEventUtils.KEYCODE_TO_SCANCODE_OFFSET -object InputEventStrings { +object KeyCodeStrings { val MODIFIER_LABELS = mapOf( KeyEvent.META_CTRL_ON to R.string.meta_state_ctrl, @@ -34,8 +33,8 @@ object InputEventStrings { * Maps keys which aren't single characters like the Control keys to a string representation */ private val NON_CHARACTER_KEY_LABELS: Map = mapOf( - KeyEvent.KEYCODE_VOLUME_DOWN to "Volume down", - KeyEvent.KEYCODE_VOLUME_UP to "Volume up", + KeyEvent.KEYCODE_VOLUME_DOWN to "Volume Down", + KeyEvent.KEYCODE_VOLUME_UP to "Volume Up", KeyEvent.KEYCODE_CTRL_LEFT to "Ctrl Left", KeyEvent.KEYCODE_CTRL_RIGHT to "Ctrl Right", @@ -369,11 +368,7 @@ object InputEventStrings { * Create a text representation of a key event. E.g if the control key was pressed, * "Ctrl" will be returned */ - fun keyCodeToString(keyCode: Int): String = NON_CHARACTER_KEY_LABELS[keyCode].let { - if (keyCode >= KEYCODE_TO_SCANCODE_OFFSET || keyCode < 0) { - "scancode $keyCode" - } else { - it ?: "unknown keycode $keyCode" - } + fun keyCodeToString(keyCode: Int): String? { + return NON_CHARACTER_KEY_LABELS[keyCode] } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ScancodeStrings.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ScancodeStrings.kt new file mode 100644 index 0000000000..b1b9c512fc --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ScancodeStrings.kt @@ -0,0 +1,579 @@ +package io.github.sds100.keymapper.base.utils + +import io.github.sds100.keymapper.system.inputevents.Scancode + +object ScancodeStrings { + private val SCANCODE_LABELS = mapOf( + // Number keys + Scancode.KEY_1 to "1", + Scancode.KEY_2 to "2", + Scancode.KEY_3 to "3", + Scancode.KEY_4 to "4", + Scancode.KEY_5 to "5", + Scancode.KEY_6 to "6", + Scancode.KEY_7 to "7", + Scancode.KEY_8 to "8", + Scancode.KEY_9 to "9", + Scancode.KEY_0 to "0", + + // Special characters + Scancode.KEY_MINUS to "-", + Scancode.KEY_EQUAL to "=", + Scancode.KEY_LEFTBRACE to "[", + Scancode.KEY_RIGHTBRACE to "]", + Scancode.KEY_SEMICOLON to ";", + Scancode.KEY_APOSTROPHE to "'", + Scancode.KEY_GRAVE to "`", + Scancode.KEY_BACKSLASH to "\\", + Scancode.KEY_COMMA to ",", + Scancode.KEY_DOT to ".", + Scancode.KEY_SLASH to "/", + + // Letter keys + Scancode.KEY_Q to "Q", + Scancode.KEY_W to "W", + Scancode.KEY_E to "E", + Scancode.KEY_R to "R", + Scancode.KEY_T to "T", + Scancode.KEY_Y to "Y", + Scancode.KEY_U to "U", + Scancode.KEY_I to "I", + Scancode.KEY_O to "O", + Scancode.KEY_P to "P", + Scancode.KEY_A to "A", + Scancode.KEY_S to "S", + Scancode.KEY_D to "D", + Scancode.KEY_F to "F", + Scancode.KEY_G to "G", + Scancode.KEY_H to "H", + Scancode.KEY_J to "J", + Scancode.KEY_K to "K", + Scancode.KEY_L to "L", + Scancode.KEY_Z to "Z", + Scancode.KEY_X to "X", + Scancode.KEY_C to "C", + Scancode.KEY_V to "V", + Scancode.KEY_B to "B", + Scancode.KEY_N to "N", + Scancode.KEY_M to "M", + + // Function keys + Scancode.KEY_F1 to "F1", + Scancode.KEY_F2 to "F2", + Scancode.KEY_F3 to "F3", + Scancode.KEY_F4 to "F4", + Scancode.KEY_F5 to "F5", + Scancode.KEY_F6 to "F6", + Scancode.KEY_F7 to "F7", + Scancode.KEY_F8 to "F8", + Scancode.KEY_F9 to "F9", + Scancode.KEY_F10 to "F10", + Scancode.KEY_F11 to "F11", + Scancode.KEY_F12 to "F12", + Scancode.KEY_F13 to "F13", + Scancode.KEY_F14 to "F14", + Scancode.KEY_F15 to "F15", + Scancode.KEY_F16 to "F16", + Scancode.KEY_F17 to "F17", + Scancode.KEY_F18 to "F18", + Scancode.KEY_F19 to "F19", + Scancode.KEY_F20 to "F20", + Scancode.KEY_F21 to "F21", + Scancode.KEY_F22 to "F22", + Scancode.KEY_F23 to "F23", + Scancode.KEY_F24 to "F24", + + // Control keys + Scancode.KEY_BACKSPACE to "Backspace", + Scancode.KEY_TAB to "Tab", + Scancode.KEY_ENTER to "Enter", + Scancode.KEY_LEFTCTRL to "Left Ctrl", + Scancode.KEY_RIGHTCTRL to "Right Ctrl", + Scancode.KEY_LEFTSHIFT to "Left Shift", + Scancode.KEY_RIGHTSHIFT to "Right Shift", + Scancode.KEY_LEFTALT to "Left Alt", + Scancode.KEY_RIGHTALT to "Right Alt", + Scancode.KEY_SPACE to "Space", + Scancode.KEY_CAPSLOCK to "Caps Lock", + Scancode.KEY_NUMLOCK to "Num Lock", + Scancode.KEY_SCROLLLOCK to "Scroll Lock", + Scancode.KEY_LEFTMETA to "Left Meta", + Scancode.KEY_RIGHTMETA to "Right Meta", + Scancode.KEY_COMPOSE to "Compose", + + // Navigation keys + Scancode.KEY_HOME to "Home", + Scancode.KEY_END to "End", + Scancode.KEY_UP to "Up", + Scancode.KEY_DOWN to "Down", + Scancode.KEY_LEFT to "Left", + Scancode.KEY_RIGHT to "Right", + Scancode.KEY_PAGEUP to "Page Up", + Scancode.KEY_PAGEDOWN to "Page Down", + Scancode.KEY_INSERT to "Insert", + Scancode.KEY_DELETE to "Delete", + + // Keypad keys + Scancode.KEY_KP0 to "Keypad 0", + Scancode.KEY_KP1 to "Keypad 1", + Scancode.KEY_KP2 to "Keypad 2", + Scancode.KEY_KP3 to "Keypad 3", + Scancode.KEY_KP4 to "Keypad 4", + Scancode.KEY_KP5 to "Keypad 5", + Scancode.KEY_KP6 to "Keypad 6", + Scancode.KEY_KP7 to "Keypad 7", + Scancode.KEY_KP8 to "Keypad 8", + Scancode.KEY_KP9 to "Keypad 9", + Scancode.KEY_KPDOT to "Keypad .", + Scancode.KEY_KPPLUS to "Keypad +", + Scancode.KEY_KPMINUS to "Keypad -", + Scancode.KEY_KPASTERISK to "Keypad *", + Scancode.KEY_KPSLASH to "Keypad /", + Scancode.KEY_KPENTER to "Keypad Enter", + Scancode.KEY_KPEQUAL to "Keypad =", + Scancode.KEY_KPPLUSMINUS to "Keypad +/-", + Scancode.KEY_KPCOMMA to "Keypad ,", + Scancode.KEY_KPLEFTPAREN to "Keypad (", + Scancode.KEY_KPRIGHTPAREN to "Keypad )", + Scancode.KEY_KPJPCOMMA to "Keypad JP Comma", + + // Media keys + Scancode.KEY_MUTE to "Mute", + Scancode.KEY_VOLUMEDOWN to "Volume Down", + Scancode.KEY_VOLUMEUP to "Volume Up", + Scancode.KEY_NEXTSONG to "Next Song", + Scancode.KEY_PREVIOUSSONG to "Previous Song", + Scancode.KEY_PLAYPAUSE to "Play/Pause", + Scancode.KEY_STOPCD to "Stop", + Scancode.KEY_RECORD to "Record", + Scancode.KEY_REWIND to "Rewind", + Scancode.KEY_FASTFORWARD to "Fast Forward", + Scancode.KEY_EJECTCD to "Eject", + Scancode.KEY_CLOSECD to "Close CD", + Scancode.KEY_EJECTCLOSECD to "Eject/Close CD", + Scancode.KEY_PLAYCD to "Play CD", + Scancode.KEY_PAUSECD to "Pause CD", + Scancode.KEY_PLAY to "Play", + + // System keys + Scancode.KEY_POWER to "Power", + Scancode.KEY_SLEEP to "Sleep", + Scancode.KEY_WAKEUP to "Wake Up", + Scancode.KEY_SUSPEND to "Suspend", + Scancode.KEY_PAUSE to "Pause", + Scancode.KEY_SYSRQ to "SysRq", + Scancode.KEY_LINEFEED to "Line Feed", + Scancode.KEY_MACRO to "Macro", + Scancode.KEY_SCALE to "Scale", + + // Brightness keys + Scancode.KEY_BRIGHTNESSDOWN to "Brightness Down", + Scancode.KEY_BRIGHTNESSUP to "Brightness Up", + Scancode.KEY_BRIGHTNESS_CYCLE to "Brightness Cycle", + Scancode.KEY_BRIGHTNESS_AUTO to "Brightness Auto", + Scancode.KEY_BRIGHTNESS_MIN to "Brightness Min", + Scancode.KEY_BRIGHTNESS_MAX to "Brightness Max", + Scancode.KEY_DISPLAY_OFF to "Display Off", + Scancode.KEY_SWITCHVIDEOMODE to "Switch Video Mode", + Scancode.KEY_KBDILLUMTOGGLE to "Keyboard Illumination Toggle", + Scancode.KEY_KBDILLUMDOWN to "Keyboard Illumination Down", + Scancode.KEY_KBDILLUMUP to "Keyboard Illumination Up", + + // International keys + Scancode.KEY_ZENKAKUHANKAKU to "Zenkaku/Hankaku", + Scancode.KEY_102ND to "102nd Key", + Scancode.KEY_RO to "Ro", + Scancode.KEY_KATAKANA to "Katakana", + Scancode.KEY_HIRAGANA to "Hiragana", + Scancode.KEY_HENKAN to "Henkan", + Scancode.KEY_KATAKANAHIRAGANA to "Katakana/Hiragana", + Scancode.KEY_MUHENKAN to "Muhenkan", + Scancode.KEY_HANGEUL to "Hangeul", + Scancode.KEY_HANJA to "Hanja", + Scancode.KEY_YEN to "Yen", + + // Application keys + Scancode.KEY_STOP to "Stop", + Scancode.KEY_AGAIN to "Again", + Scancode.KEY_PROPS to "Properties", + Scancode.KEY_UNDO to "Undo", + Scancode.KEY_FRONT to "Front", + Scancode.KEY_COPY to "Copy", + Scancode.KEY_OPEN to "Open", + Scancode.KEY_PASTE to "Paste", + Scancode.KEY_FIND to "Find", + Scancode.KEY_CUT to "Cut", + Scancode.KEY_HELP to "Help", + Scancode.KEY_MENU to "Menu", + Scancode.KEY_CALC to "Calculator", + Scancode.KEY_SETUP to "Setup", + Scancode.KEY_FILE to "File", + Scancode.KEY_SENDFILE to "Send File", + Scancode.KEY_DELETEFILE to "Delete File", + Scancode.KEY_XFER to "Transfer", + Scancode.KEY_PROG1 to "Program 1", + Scancode.KEY_PROG2 to "Program 2", + Scancode.KEY_PROG3 to "Program 3", + Scancode.KEY_PROG4 to "Program 4", + + // Web/Internet keys + Scancode.KEY_WWW to "WWW", + Scancode.KEY_MSDOS to "MS-DOS", + Scancode.KEY_COFFEE to "Coffee", + Scancode.KEY_ROTATE_DISPLAY to "Rotate Display", + Scancode.KEY_CYCLEWINDOWS to "Cycle Windows", + Scancode.KEY_MAIL to "Mail", + Scancode.KEY_BOOKMARKS to "Bookmarks", + Scancode.KEY_COMPUTER to "Computer", + Scancode.KEY_BACK to "Back", + Scancode.KEY_FORWARD to "Forward", + Scancode.KEY_HOMEPAGE to "Homepage", + Scancode.KEY_REFRESH to "Refresh", + Scancode.KEY_EXIT to "Exit", + Scancode.KEY_MOVE to "Move", + Scancode.KEY_EDIT to "Edit", + Scancode.KEY_SCROLLUP to "Scroll Up", + Scancode.KEY_SCROLLDOWN to "Scroll Down", + Scancode.KEY_NEW to "New", + Scancode.KEY_REDO to "Redo", + + // Multimedia keys + Scancode.KEY_BASSBOOST to "Bass Boost", + Scancode.KEY_PRINT to "Print", + Scancode.KEY_HP to "HP", + Scancode.KEY_CAMERA to "Camera", + Scancode.KEY_SOUND to "Sound", + Scancode.KEY_QUESTION to "Question", + Scancode.KEY_EMAIL to "Email", + Scancode.KEY_CHAT to "Chat", + Scancode.KEY_SEARCH to "Search", + Scancode.KEY_CONNECT to "Connect", + Scancode.KEY_FINANCE to "Finance", + Scancode.KEY_SPORT to "Sport", + Scancode.KEY_SHOP to "Shop", + Scancode.KEY_ALTERASE to "Alt Erase", + Scancode.KEY_CANCEL to "Cancel", + Scancode.KEY_MEDIA to "Media", + + // Wireless keys + Scancode.KEY_BLUETOOTH to "Bluetooth", + Scancode.KEY_WLAN to "WLAN", + Scancode.KEY_UWB to "UWB", + Scancode.KEY_UNKNOWN to "Unknown", + Scancode.KEY_WWAN to "WWAN", + Scancode.KEY_RFKILL to "RF Kill", + Scancode.KEY_MICMUTE to "Mic Mute", + + // Video keys + Scancode.KEY_VIDEO_NEXT to "Video Next", + Scancode.KEY_VIDEO_PREV to "Video Previous", + + // Battery and document keys + Scancode.KEY_BATTERY to "Battery", + Scancode.KEY_DOCUMENTS to "Documents", + Scancode.KEY_SEND to "Send", + Scancode.KEY_REPLY to "Reply", + Scancode.KEY_FORWARDMAIL to "Forward Mail", + Scancode.KEY_SAVE to "Save", + + // Mouse buttons + Scancode.BTN_LEFT to "Left Mouse Button", + Scancode.BTN_RIGHT to "Right Mouse Button", + Scancode.BTN_MIDDLE to "Middle Mouse Button", + Scancode.BTN_SIDE to "Side Mouse Button", + Scancode.BTN_EXTRA to "Extra Mouse Button", + Scancode.BTN_FORWARD to "Forward Mouse Button", + Scancode.BTN_BACK to "Back Mouse Button", + Scancode.BTN_TASK to "Task Mouse Button", + + // Generic buttons + Scancode.BTN_0 to "Button 0", + Scancode.BTN_1 to "Button 1", + Scancode.BTN_2 to "Button 2", + Scancode.BTN_3 to "Button 3", + Scancode.BTN_4 to "Button 4", + Scancode.BTN_5 to "Button 5", + Scancode.BTN_6 to "Button 6", + Scancode.BTN_7 to "Button 7", + Scancode.BTN_8 to "Button 8", + Scancode.BTN_9 to "Button 9", + + // Joystick buttons + Scancode.BTN_TRIGGER to "Trigger", + Scancode.BTN_THUMB to "Thumb", + Scancode.BTN_THUMB2 to "Thumb 2", + Scancode.BTN_TOP to "Top", + Scancode.BTN_TOP2 to "Top 2", + Scancode.BTN_PINKIE to "Pinkie", + Scancode.BTN_BASE to "Base", + Scancode.BTN_BASE2 to "Base 2", + Scancode.BTN_BASE3 to "Base 3", + Scancode.BTN_BASE4 to "Base 4", + Scancode.BTN_BASE5 to "Base 5", + Scancode.BTN_BASE6 to "Base 6", + Scancode.BTN_DEAD to "Dead", + + // Gamepad buttons + Scancode.BTN_SOUTH to "South Button", + Scancode.BTN_EAST to "East Button", + Scancode.BTN_C to "C Button", + Scancode.BTN_NORTH to "North Button", + Scancode.BTN_WEST to "West Button", + Scancode.BTN_Z to "Z Button", + Scancode.BTN_TL to "Top Left", + Scancode.BTN_TR to "Top Right", + Scancode.BTN_TL2 to "Top Left 2", + Scancode.BTN_TR2 to "Top Right 2", + Scancode.BTN_SELECT to "Select", + Scancode.BTN_START to "Start", + Scancode.BTN_MODE to "Mode", + Scancode.BTN_THUMBL to "Left Thumb", + Scancode.BTN_THUMBR to "Right Thumb", + + // Digital pen buttons + Scancode.BTN_TOOL_PEN to "Pen Tool", + Scancode.BTN_TOOL_RUBBER to "Rubber Tool", + Scancode.BTN_TOOL_BRUSH to "Brush Tool", + Scancode.BTN_TOOL_PENCIL to "Pencil Tool", + Scancode.BTN_TOOL_AIRBRUSH to "Airbrush Tool", + Scancode.BTN_TOOL_FINGER to "Finger Tool", + Scancode.BTN_TOOL_MOUSE to "Mouse Tool", + Scancode.BTN_TOOL_LENS to "Lens Tool", + Scancode.BTN_TOOL_QUINTTAP to "Quint Tap Tool", + Scancode.BTN_STYLUS3 to "Stylus 3", + Scancode.BTN_TOUCH to "Touch", + Scancode.BTN_STYLUS to "Stylus", + Scancode.BTN_STYLUS2 to "Stylus 2", + Scancode.BTN_TOOL_DOUBLETAP to "Double Tap Tool", + Scancode.BTN_TOOL_TRIPLETAP to "Triple Tap Tool", + Scancode.BTN_TOOL_QUADTAP to "Quad Tap Tool", + + // Wheel buttons + Scancode.BTN_GEAR_DOWN to "Gear Down", + Scancode.BTN_GEAR_UP to "Gear Up", + + // Remote control keys + Scancode.KEY_OK to "OK", + Scancode.KEY_SELECT to "Select", + Scancode.KEY_GOTO to "Goto", + Scancode.KEY_CLEAR to "Clear", + Scancode.KEY_POWER2 to "Power 2", + Scancode.KEY_OPTION to "Option", + Scancode.KEY_INFO to "Info", + Scancode.KEY_TIME to "Time", + Scancode.KEY_VENDOR to "Vendor", + Scancode.KEY_ARCHIVE to "Archive", + Scancode.KEY_PROGRAM to "Program", + Scancode.KEY_CHANNEL to "Channel", + Scancode.KEY_FAVORITES to "Favorites", + Scancode.KEY_EPG to "EPG", + Scancode.KEY_PVR to "PVR", + Scancode.KEY_MHP to "MHP", + Scancode.KEY_LANGUAGE to "Language", + Scancode.KEY_TITLE to "Title", + Scancode.KEY_SUBTITLE to "Subtitle", + Scancode.KEY_ANGLE to "Angle", + Scancode.KEY_ZOOM to "Zoom", + Scancode.KEY_MODE to "Mode", + Scancode.KEY_KEYBOARD to "Keyboard", + Scancode.KEY_SCREEN to "Screen", + Scancode.KEY_PC to "PC", + Scancode.KEY_TV to "TV", + Scancode.KEY_TV2 to "TV 2", + Scancode.KEY_VCR to "VCR", + Scancode.KEY_VCR2 to "VCR 2", + Scancode.KEY_SAT to "Satellite", + Scancode.KEY_SAT2 to "Satellite 2", + Scancode.KEY_CD to "CD", + Scancode.KEY_TAPE to "Tape", + Scancode.KEY_RADIO to "Radio", + Scancode.KEY_TUNER to "Tuner", + Scancode.KEY_PLAYER to "Player", + Scancode.KEY_TEXT to "Text", + Scancode.KEY_DVD to "DVD", + Scancode.KEY_AUX to "Aux", + Scancode.KEY_MP3 to "MP3", + Scancode.KEY_AUDIO to "Audio", + Scancode.KEY_VIDEO to "Video", + Scancode.KEY_DIRECTORY to "Directory", + Scancode.KEY_LIST to "List", + Scancode.KEY_MEMO to "Memo", + Scancode.KEY_CALENDAR to "Calendar", + Scancode.KEY_RED to "Red", + Scancode.KEY_GREEN to "Green", + Scancode.KEY_YELLOW to "Yellow", + Scancode.KEY_BLUE to "Blue", + Scancode.KEY_CHANNELUP to "Channel Up", + Scancode.KEY_CHANNELDOWN to "Channel Down", + Scancode.KEY_FIRST to "First", + Scancode.KEY_LAST to "Last", + Scancode.KEY_AB to "A-B", + Scancode.KEY_NEXT to "Next", + Scancode.KEY_RESTART to "Restart", + Scancode.KEY_SLOW to "Slow", + Scancode.KEY_SHUFFLE to "Shuffle", + Scancode.KEY_BREAK to "Break", + Scancode.KEY_PREVIOUS to "Previous", + Scancode.KEY_DIGITS to "Digits", + Scancode.KEY_TEEN to "Teen", + Scancode.KEY_TWEN to "Twen", + + // Phone keys + Scancode.KEY_PHONE to "Phone", + Scancode.KEY_VIDEOPHONE to "Video Phone", + Scancode.KEY_PICKUP_PHONE to "Pick Up Phone", + Scancode.KEY_HANGUP_PHONE to "Hang Up Phone", + + // Application keys + Scancode.KEY_GAMES to "Games", + Scancode.KEY_ZOOMIN to "Zoom In", + Scancode.KEY_ZOOMOUT to "Zoom Out", + Scancode.KEY_ZOOMRESET to "Zoom Reset", + Scancode.KEY_WORDPROCESSOR to "Word Processor", + Scancode.KEY_EDITOR to "Editor", + Scancode.KEY_SPREADSHEET to "Spreadsheet", + Scancode.KEY_GRAPHICSEDITOR to "Graphics Editor", + Scancode.KEY_PRESENTATION to "Presentation", + Scancode.KEY_DATABASE to "Database", + Scancode.KEY_NEWS to "News", + Scancode.KEY_VOICEMAIL to "Voicemail", + Scancode.KEY_ADDRESSBOOK to "Address Book", + Scancode.KEY_MESSENGER to "Messenger", + Scancode.KEY_DISPLAYTOGGLE to "Display Toggle", + Scancode.KEY_SPELLCHECK to "Spell Check", + Scancode.KEY_LOGOFF to "Log Off", + + // Currency keys + Scancode.KEY_DOLLAR to "Dollar", + Scancode.KEY_EURO to "Euro", + + // Media control keys + Scancode.KEY_FRAMEBACK to "Frame Back", + Scancode.KEY_FRAMEFORWARD to "Frame Forward", + Scancode.KEY_CONTEXT_MENU to "Context Menu", + Scancode.KEY_MEDIA_REPEAT to "Media Repeat", + Scancode.KEY_10CHANNELSUP to "10 Channels Up", + Scancode.KEY_10CHANNELSDOWN to "10 Channels Down", + Scancode.KEY_IMAGES to "Images", + Scancode.KEY_NOTIFICATION_CENTER to "Notification Center", + + // Numeric keypad + Scancode.KEY_NUMERIC_0 to "Numeric 0", + Scancode.KEY_NUMERIC_1 to "Numeric 1", + Scancode.KEY_NUMERIC_2 to "Numeric 2", + Scancode.KEY_NUMERIC_3 to "Numeric 3", + Scancode.KEY_NUMERIC_4 to "Numeric 4", + Scancode.KEY_NUMERIC_5 to "Numeric 5", + Scancode.KEY_NUMERIC_6 to "Numeric 6", + Scancode.KEY_NUMERIC_7 to "Numeric 7", + Scancode.KEY_NUMERIC_8 to "Numeric 8", + Scancode.KEY_NUMERIC_9 to "Numeric 9", + Scancode.KEY_NUMERIC_STAR to "Numeric *", + Scancode.KEY_NUMERIC_POUND to "Numeric #", + Scancode.KEY_NUMERIC_A to "Numeric A", + Scancode.KEY_NUMERIC_B to "Numeric B", + Scancode.KEY_NUMERIC_C to "Numeric C", + Scancode.KEY_NUMERIC_D to "Numeric D", + Scancode.KEY_NUMERIC_11 to "Numeric 11", + Scancode.KEY_NUMERIC_12 to "Numeric 12", + + // System control keys + Scancode.KEY_BUTTONCONFIG to "Button Config", + Scancode.KEY_TASKMANAGER to "Task Manager", + Scancode.KEY_JOURNAL to "Journal", + Scancode.KEY_CONTROLPANEL to "Control Panel", + Scancode.KEY_APPSELECT to "App Select", + Scancode.KEY_SCREENSAVER to "Screen Saver", + Scancode.KEY_VOICECOMMAND to "Voice Command", + Scancode.KEY_ASSISTANT to "Assistant", + Scancode.KEY_KBD_LAYOUT_NEXT to "Keyboard Layout Next", + Scancode.KEY_EMOJI_PICKER to "Emoji Picker", + Scancode.KEY_DICTATE to "Dictate", + + // D-pad buttons + Scancode.BTN_DPAD_UP to "D-pad Up", + Scancode.BTN_DPAD_DOWN to "D-pad Down", + Scancode.BTN_DPAD_LEFT to "D-pad Left", + Scancode.BTN_DPAD_RIGHT to "D-pad Right", + + // Text editing keys + Scancode.KEY_DEL_EOL to "Delete End of Line", + Scancode.KEY_DEL_EOS to "Delete End of Screen", + Scancode.KEY_INS_LINE to "Insert Line", + Scancode.KEY_DEL_LINE to "Delete Line", + + // Function modifier keys + Scancode.KEY_FN to "Fn", + Scancode.KEY_FN_ESC to "Fn+Esc", + Scancode.KEY_FN_F1 to "Fn+F1", + Scancode.KEY_FN_F2 to "Fn+F2", + Scancode.KEY_FN_F3 to "Fn+F3", + Scancode.KEY_FN_F4 to "Fn+F4", + Scancode.KEY_FN_F5 to "Fn+F5", + Scancode.KEY_FN_F6 to "Fn+F6", + Scancode.KEY_FN_F7 to "Fn+F7", + Scancode.KEY_FN_F8 to "Fn+F8", + Scancode.KEY_FN_F9 to "Fn+F9", + Scancode.KEY_FN_F10 to "Fn+F10", + Scancode.KEY_FN_F11 to "Fn+F11", + Scancode.KEY_FN_F12 to "Fn+F12", + Scancode.KEY_FN_1 to "Fn+1", + Scancode.KEY_FN_2 to "Fn+2", + Scancode.KEY_FN_D to "Fn+D", + Scancode.KEY_FN_E to "Fn+E", + Scancode.KEY_FN_F to "Fn+F", + Scancode.KEY_FN_S to "Fn+S", + Scancode.KEY_FN_B to "Fn+B", + Scancode.KEY_FN_RIGHT_SHIFT to "Fn+Right Shift", + + // Braille keys + Scancode.KEY_BRL_DOT1 to "Braille Dot 1", + Scancode.KEY_BRL_DOT2 to "Braille Dot 2", + Scancode.KEY_BRL_DOT3 to "Braille Dot 3", + Scancode.KEY_BRL_DOT4 to "Braille Dot 4", + Scancode.KEY_BRL_DOT5 to "Braille Dot 5", + Scancode.KEY_BRL_DOT6 to "Braille Dot 6", + Scancode.KEY_BRL_DOT7 to "Braille Dot 7", + Scancode.KEY_BRL_DOT8 to "Braille Dot 8", + Scancode.KEY_BRL_DOT9 to "Braille Dot 9", + Scancode.KEY_BRL_DOT10 to "Braille Dot 10", + + // Camera keys + Scancode.KEY_CAMERA_FOCUS to "Camera Focus", + Scancode.KEY_CAMERA_ZOOMIN to "Camera Zoom In", + Scancode.KEY_CAMERA_ZOOMOUT to "Camera Zoom Out", + Scancode.KEY_CAMERA_UP to "Camera Up", + Scancode.KEY_CAMERA_DOWN to "Camera Down", + Scancode.KEY_CAMERA_LEFT to "Camera Left", + Scancode.KEY_CAMERA_RIGHT to "Camera Right", + Scancode.KEY_CAMERA_ACCESS_ENABLE to "Camera Access Enable", + Scancode.KEY_CAMERA_ACCESS_DISABLE to "Camera Access Disable", + Scancode.KEY_CAMERA_ACCESS_TOGGLE to "Camera Access Toggle", + + // Wireless and touchpad keys + Scancode.KEY_WPS_BUTTON to "WPS Button", + Scancode.KEY_TOUCHPAD_TOGGLE to "Touchpad Toggle", + Scancode.KEY_TOUCHPAD_ON to "Touchpad On", + Scancode.KEY_TOUCHPAD_OFF to "Touchpad Off", + + // Sensor keys + Scancode.KEY_ALS_TOGGLE to "ALS Toggle", + Scancode.KEY_ROTATE_LOCK_TOGGLE to "Rotate Lock Toggle", + + // Macro keys (first 10) + Scancode.KEY_MACRO1 to "Macro 1", + Scancode.KEY_MACRO2 to "Macro 2", + Scancode.KEY_MACRO3 to "Macro 3", + Scancode.KEY_MACRO4 to "Macro 4", + Scancode.KEY_MACRO5 to "Macro 5", + Scancode.KEY_MACRO6 to "Macro 6", + Scancode.KEY_MACRO7 to "Macro 7", + Scancode.KEY_MACRO8 to "Macro 8", + Scancode.KEY_MACRO9 to "Macro 9", + Scancode.KEY_MACRO10 to "Macro 10", + ) + + fun getScancodeLabel(scancode: Int): String? { + return SCANCODE_LABELS[scancode] + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavDestination.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavDestination.kt index 8f6c870c49..6f3b6b5587 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavDestination.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavDestination.kt @@ -31,10 +31,13 @@ abstract class NavDestination(val isCompose: Boolean = false) { const val ID_CHOOSE_CONSTRAINT = "choose_constraint" const val ID_CHOOSE_BLUETOOTH_DEVICE = "choose_bluetooth_device" const val ID_SETTINGS = "settings" + const val ID_DEFAULT_OPTIONS_SETTINGS = "default_options_settings" + const val ID_AUTOMATIC_CHANGE_IME_SETTINGS = "automatic_change_ime_settings" const val ID_ABOUT = "about" const val ID_CONFIG_KEY_MAP = "config_key_map" - const val ID_SHIZUKU_SETTINGS = "shizuku_settings" const val ID_INTERACT_UI_ELEMENT_ACTION = "interact_ui_element_action" + const val ID_PRO_MODE = "pro_mode" + const val ID_LOG = "log" } @Serializable @@ -69,7 +72,8 @@ abstract class NavDestination(val isCompose: Boolean = false) { } @Serializable - data class PickCoordinate(val result: PickCoordinateResult? = null) : NavDestination() { + data class PickCoordinate(val result: PickCoordinateResult? = null) : + NavDestination() { override val id: String = ID_PICK_COORDINATE } @@ -86,7 +90,8 @@ abstract class NavDestination(val isCompose: Boolean = false) { } @Serializable - data class ConfigIntent(val result: ConfigIntentResult? = null) : NavDestination() { + data class ConfigIntent(val result: ConfigIntentResult? = null) : + NavDestination() { override val id: String = ID_CONFIG_INTENT } @@ -116,10 +121,20 @@ abstract class NavDestination(val isCompose: Boolean = false) { } @Serializable - data object Settings : NavDestination() { + data object Settings : NavDestination(isCompose = true) { override val id: String = ID_SETTINGS } + @Serializable + data object DefaultOptionsSettings : NavDestination(isCompose = true) { + override val id: String = ID_DEFAULT_OPTIONS_SETTINGS + } + + @Serializable + data object AutomaticChangeImeSettings : NavDestination(isCompose = true) { + override val id: String = ID_AUTOMATIC_CHANGE_IME_SETTINGS + } + @Serializable data object About : NavDestination() { override val id: String = ID_ABOUT @@ -140,14 +155,25 @@ abstract class NavDestination(val isCompose: Boolean = false) { override val id: String = ID_CONFIG_KEY_MAP } - @Serializable - data object ShizukuSettings : NavDestination() { - override val id: String = ID_SHIZUKU_SETTINGS - } - @Serializable data class InteractUiElement(val actionJson: String?) : NavDestination(isCompose = true) { override val id: String = ID_INTERACT_UI_ELEMENT_ACTION } + + @Serializable + data object ProMode : NavDestination(isCompose = true) { + override val id: String = ID_PRO_MODE + } + + @Serializable + data object ProModeSetup : NavDestination(isCompose = true) { + const val ID_PRO_MODE_SETUP = "pro_mode_setup_wizard" + override val id: String = ID_PRO_MODE_SETUP + } + + @Serializable + data object Log : NavDestination(isCompose = true) { + override val id: String = ID_LOG + } } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavigationProvider.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavigationProvider.kt index 0cea5a666a..00f811d795 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavigationProvider.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/navigation/NavigationProvider.kt @@ -1,5 +1,6 @@ package io.github.sds100.keymapper.base.utils.navigation +import android.annotation.SuppressLint import android.os.Bundle import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -42,6 +43,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json +import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -106,6 +108,7 @@ class NavigationProviderImpl @Inject constructor() : NavigationProvider { // wait for the view to collect so navigating can happen _onNavigate.subscriptionCount.first { it > 0 } + Timber.d("Navigation: Navigating to ${event.destination} with key ${event.key}") _onNavigate.emit(event) } @@ -114,6 +117,7 @@ class NavigationProviderImpl @Inject constructor() : NavigationProvider { } override suspend fun popBackStack() { + Timber.d("Navigation: Popping back stack") _popBackStack.value = Unit } @@ -122,6 +126,8 @@ class NavigationProviderImpl @Inject constructor() : NavigationProvider { */ override suspend fun popBackStackWithResult(data: String) { _onReturnResult.subscriptionCount.first { it > 0 } + + Timber.d("Navigation: Popping back stack with result") _onReturnResult.emit(data) } } @@ -167,8 +173,9 @@ fun SetupNavigation( navigationProvider: NavigationProviderImpl, navController: NavHostController, ) { + @SuppressLint("StateFlowValueCalledInComposition") val navEvent: NavigateEvent? by navigationProvider.onNavigate - .collectAsStateWithLifecycle(null) + .collectAsStateWithLifecycle(navigationProvider.onNavigate.value) val returnResult: String? by navigationProvider.onReturnResult .collectAsStateWithLifecycle(null) @@ -351,9 +358,6 @@ private fun getDirection(destination: NavDestination<*>, requestKey: String): Na ) NavDestination.About -> NavBaseAppDirections.actionGlobalAboutFragment() - NavDestination.Settings -> NavBaseAppDirections.toSettingsFragment() - - NavDestination.ShizukuSettings -> NavBaseAppDirections.toShizukuSettingsFragment() else -> throw IllegalArgumentException("Can not find a direction for this destination: $destination") } diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/CompactChip.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/CompactChip.kt index 226b93841f..c84d864907 100644 --- a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/CompactChip.kt +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/CompactChip.kt @@ -21,7 +21,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import io.github.sds100.keymapper.base.keymaps.chipHeight +import io.github.sds100.keymapper.base.home.chipHeight @Composable fun CompactChip( diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/KeyMapperSegmentedButtonRow.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/KeyMapperSegmentedButtonRow.kt new file mode 100644 index 0000000000..75d7a65ef7 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/KeyMapperSegmentedButtonRow.kt @@ -0,0 +1,105 @@ +package io.github.sds100.keymapper.base.utils.ui.compose + +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.text.BasicText +import androidx.compose.foundation.text.TextAutoSize +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +/** + * A reusable segmented button row that follows KeyMapper's design patterns. + * + * @param modifier The modifier to apply to the segmented button row + * @param buttonStates List of pairs containing the data and display text for each button + * @param selectedState The currently selected state + * @param onStateSelected Callback when a button is selected + * @param isCompact Whether to use compact styling (smaller shapes, auto-sizing text) + * @param isEnabled Whether the buttons are enabled + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun KeyMapperSegmentedButtonRow( + modifier: Modifier = Modifier, + buttonStates: List>, + selectedState: T?, + onStateSelected: (T) -> Unit, + isCompact: Boolean = false, + isEnabled: Boolean = true, +) { + val colors = if (isEnabled) { + SegmentedButtonDefaults.colors() + } else { + // The disabled border color of the inactive button is by default not greyed out enough + SegmentedButtonDefaults.colors( + disabledInactiveBorderColor = + SegmentedButtonDefaults.colors().inactiveBorderColor.copy(alpha = 0.5f), + ) + } + + SingleChoiceSegmentedButtonRow( + modifier = modifier, + ) { + for (content in buttonStates) { + val (state, label) = content + val isSelected = state == selectedState + val isDisabled = !isEnabled + val isUnselectedDisabled = isDisabled && !isSelected + + if (isCompact) { + SegmentedButton( + modifier = Modifier.height(36.dp), + selected = isSelected, + onClick = { onStateSelected(state) }, + enabled = isEnabled, + icon = { }, + shape = SegmentedButtonDefaults.itemShape( + index = buttonStates.indexOf(content), + count = buttonStates.size, + baseShape = MaterialTheme.shapes.extraSmall, + ), + colors = colors, + ) { + BasicText( + modifier = if (isUnselectedDisabled) Modifier.alpha(0.5f) else Modifier, + text = label, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + autoSize = TextAutoSize.StepBased( + maxFontSize = LocalTextStyle.current.fontSize, + minFontSize = 10.sp, + ), + ) + } + } else { + SegmentedButton( + selected = isSelected, + onClick = { onStateSelected(state) }, + enabled = isEnabled, + shape = SegmentedButtonDefaults.itemShape( + index = buttonStates.indexOf(content), + count = buttonStates.size, + ), + colors = colors, + ) { + Text( + modifier = if (isUnselectedDisabled) Modifier.alpha(0.5f) else Modifier, + text = label, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + } + } + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/OptionButton.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/OptionButton.kt new file mode 100644 index 0000000000..4b1f1eeeae --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/OptionButton.kt @@ -0,0 +1,54 @@ +package io.github.sds100.keymapper.base.utils.ui.compose + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.compose.KeyMapperTheme + +@Composable +fun OptionButton( + modifier: Modifier = Modifier, + title: String, + text: String, + onClick: () -> Unit, +) { + Surface(modifier = modifier, onClick = onClick, shape = MaterialTheme.shapes.medium) { + Row( + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + ) { + Column(modifier = Modifier.weight(1f)) { + Text(text = title, style = MaterialTheme.typography.bodyLarge) + Text( + text = text, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } +} + +@Preview +@Composable +private fun Preview() { + KeyMapperTheme { + OptionButton( + modifier = Modifier.fillMaxWidth(), + title = stringResource(R.string.title_pref_pro_mode), + text = stringResource(R.string.summary_pref_pro_mode), + onClick = {}, + ) + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/OptionPageButton.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/OptionPageButton.kt new file mode 100644 index 0000000000..0e92b46627 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/OptionPageButton.kt @@ -0,0 +1,85 @@ +package io.github.sds100.keymapper.base.utils.ui.compose + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ChevronRight +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.github.sds100.keymapper.base.R +import io.github.sds100.keymapper.base.compose.KeyMapperTheme +import io.github.sds100.keymapper.base.utils.ui.compose.icons.KeyMapperIcons +import io.github.sds100.keymapper.base.utils.ui.compose.icons.ProModeIcon + +@Composable +fun OptionPageButton( + modifier: Modifier = Modifier, + title: String, + text: String, + icon: ImageVector? = null, + onClick: () -> Unit, +) { + Surface(modifier = modifier, onClick = onClick, shape = MaterialTheme.shapes.medium) { + Row( + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + ) { + if (icon != null) { + Icon( + modifier = Modifier + .align(Alignment.CenterVertically) + .size(24.dp), + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + ) + + Spacer(modifier = Modifier.width(16.dp)) + } + + Column(modifier = Modifier.weight(1f)) { + Text(text = title, style = MaterialTheme.typography.bodyLarge) + Text( + text = text, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + Icon( + modifier = Modifier.align(Alignment.CenterVertically), + imageVector = Icons.Rounded.ChevronRight, + contentDescription = null, + ) + } + } +} + +@Preview +@Composable +private fun Preview() { + KeyMapperTheme { + OptionPageButton( + modifier = Modifier.fillMaxWidth(), + title = stringResource(R.string.title_pref_pro_mode), + text = stringResource(R.string.summary_pref_pro_mode), + icon = KeyMapperIcons.ProModeIcon, + onClick = {}, + ) + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SwitchPreferenceCompose.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SwitchPreferenceCompose.kt new file mode 100644 index 0000000000..b7a5b3eecc --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/SwitchPreferenceCompose.kt @@ -0,0 +1,63 @@ +package io.github.sds100.keymapper.base.utils.ui.compose + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp + +@Composable +fun SwitchPreferenceCompose( + modifier: Modifier = Modifier, + title: String, + text: String?, + icon: ImageVector, + isChecked: Boolean, + onCheckedChange: (Boolean) -> Unit, +) { + Surface( + modifier = modifier, + shape = MaterialTheme.shapes.medium, + onClick = { + onCheckedChange(!isChecked) + }, + ) { + Row( + modifier = Modifier.Companion + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + ) + + Column(modifier = Modifier.Companion.weight(1f)) { + Text(text = title, style = MaterialTheme.typography.bodyLarge) + if (text != null) { + Text( + text = text, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + + Switch( + checked = isChecked, + onCheckedChange = onCheckedChange, + ) + } + } +} diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/FakeShizuku.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/FakeShizuku.kt new file mode 100644 index 0000000000..3fbf8d0150 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/FakeShizuku.kt @@ -0,0 +1,45 @@ +package io.github.sds100.keymapper.base.utils.ui.compose.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val KeyMapperIcons.FakeShizuku: ImageVector + get() { + if (_FakeShizuku != null) { + return _FakeShizuku!! + } + _FakeShizuku = ImageVector.Builder( + name = "FakeShizuku", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f, + ).apply { + path(fill = SolidColor(Color(0xFF4053B9))) { + moveTo(12f, 24f) + curveTo(18.627f, 24f, 24f, 18.627f, 24f, 12f) + curveTo(24f, 5.373f, 18.627f, 0f, 12f, 0f) + curveTo(5.373f, 0f, 0f, 5.373f, 0f, 12f) + curveTo(0f, 18.627f, 5.373f, 24f, 12f, 24f) + close() + } + path(fill = SolidColor(Color(0xFF717DC0))) { + moveTo(15.384f, 17.771f) + lineTo(8.74f, 17.752f) + lineTo(5.434f, 11.989f) + lineTo(8.772f, 6.244f) + lineTo(15.416f, 6.263f) + lineTo(18.722f, 12.026f) + lineTo(15.384f, 17.771f) + close() + } + }.build() + + return _FakeShizuku!! + } + +@Suppress("ObjectPropertyName") +private var _FakeShizuku: ImageVector? = null diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/FolderManaged.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/FolderManaged.kt new file mode 100644 index 0000000000..84e2a94dea --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/FolderManaged.kt @@ -0,0 +1,131 @@ +package io.github.sds100.keymapper.base.utils.ui.compose.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val KeyMapperIcons.FolderManaged: ImageVector + get() { + if (_FolderManaged != null) { + return _FolderManaged!! + } + _FolderManaged = ImageVector.Builder( + name = "FolderManaged", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 960f, + viewportHeight = 960f, + ).apply { + path(fill = SolidColor(Color.Black)) { + moveTo(720f, 760f) + quadToRelative(33f, 0f, 56.5f, -23.5f) + reflectiveQuadTo(800f, 680f) + quadToRelative(0f, -33f, -23.5f, -56.5f) + reflectiveQuadTo(720f, 600f) + quadToRelative(-33f, 0f, -56.5f, 23.5f) + reflectiveQuadTo(640f, 680f) + quadToRelative(0f, 33f, 23.5f, 56.5f) + reflectiveQuadTo(720f, 760f) + close() + moveTo(712f, 880f) + quadToRelative(-14f, 0f, -24.5f, -9f) + reflectiveQuadTo(674f, 848f) + lineToRelative(-6f, -28f) + quadToRelative(-12f, -5f, -22.5f, -10.5f) + reflectiveQuadTo(624f, 796f) + lineToRelative(-29f, 9f) + quadToRelative(-13f, 4f, -25.5f, -1f) + reflectiveQuadTo(550f, 788f) + lineToRelative(-8f, -14f) + quadToRelative(-7f, -12f, -5f, -26f) + reflectiveQuadToRelative(13f, -23f) + lineToRelative(22f, -19f) + quadToRelative(-2f, -12f, -2f, -26f) + reflectiveQuadToRelative(2f, -26f) + lineToRelative(-22f, -19f) + quadToRelative(-11f, -9f, -13f, -22.5f) + reflectiveQuadToRelative(5f, -25.5f) + lineToRelative(9f, -15f) + quadToRelative(7f, -11f, 19f, -16f) + reflectiveQuadToRelative(25f, -1f) + lineToRelative(29f, 9f) + quadToRelative(11f, -8f, 21.5f, -13.5f) + reflectiveQuadTo(668f, 540f) + lineToRelative(6f, -29f) + quadToRelative(3f, -14f, 13.5f, -22.5f) + reflectiveQuadTo(712f, 480f) + horizontalLineToRelative(16f) + quadToRelative(14f, 0f, 24.5f, 9f) + reflectiveQuadToRelative(13.5f, 23f) + lineToRelative(6f, 28f) + quadToRelative(12f, 5f, 22.5f, 10.5f) + reflectiveQuadTo(816f, 564f) + lineToRelative(29f, -9f) + quadToRelative(13f, -4f, 25.5f, 1f) + reflectiveQuadToRelative(19.5f, 16f) + lineToRelative(8f, 14f) + quadToRelative(7f, 12f, 5f, 26f) + reflectiveQuadToRelative(-13f, 23f) + lineToRelative(-22f, 19f) + quadToRelative(2f, 12f, 2f, 26f) + reflectiveQuadToRelative(-2f, 26f) + lineToRelative(22f, 19f) + quadToRelative(11f, 9f, 13f, 22.5f) + reflectiveQuadToRelative(-5f, 25.5f) + lineToRelative(-9f, 15f) + quadToRelative(-7f, 11f, -19f, 16f) + reflectiveQuadToRelative(-25f, 1f) + lineToRelative(-29f, -9f) + quadToRelative(-11f, 8f, -21.5f, 13.5f) + reflectiveQuadTo(772f, 820f) + lineToRelative(-6f, 29f) + quadToRelative(-3f, 14f, -13.5f, 22.5f) + reflectiveQuadTo(728f, 880f) + horizontalLineToRelative(-16f) + close() + moveTo(160f, 720f) + verticalLineToRelative(-480f) + verticalLineToRelative(172f) + verticalLineToRelative(-12f) + verticalLineToRelative(320f) + close() + moveTo(160f, 800f) + quadToRelative(-33f, 0f, -56.5f, -23.5f) + reflectiveQuadTo(80f, 720f) + verticalLineToRelative(-480f) + quadToRelative(0f, -33f, 23.5f, -56.5f) + reflectiveQuadTo(160f, 160f) + horizontalLineToRelative(207f) + quadToRelative(16f, 0f, 30.5f, 6f) + reflectiveQuadToRelative(25.5f, 17f) + lineToRelative(57f, 57f) + horizontalLineToRelative(320f) + quadToRelative(33f, 0f, 56.5f, 23.5f) + reflectiveQuadTo(880f, 320f) + verticalLineToRelative(80f) + quadToRelative(0f, 17f, -11.5f, 28.5f) + reflectiveQuadTo(840f, 440f) + quadToRelative(-17f, 0f, -28.5f, -11.5f) + reflectiveQuadTo(800f, 400f) + verticalLineToRelative(-80f) + lineTo(447f, 320f) + lineToRelative(-80f, -80f) + lineTo(160f, 240f) + verticalLineToRelative(480f) + horizontalLineToRelative(280f) + quadToRelative(17f, 0f, 28.5f, 11.5f) + reflectiveQuadTo(480f, 760f) + quadToRelative(0f, 17f, -11.5f, 28.5f) + reflectiveQuadTo(440f, 800f) + lineTo(160f, 800f) + close() + } + }.build() + + return _FolderManaged!! + } + +@Suppress("ObjectPropertyName") +private var _FolderManaged: ImageVector? = null diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/KeyMapperIcon.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/KeyMapperIcon.kt new file mode 100644 index 0000000000..9a918c25f7 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/KeyMapperIcon.kt @@ -0,0 +1,313 @@ +package io.github.sds100.keymapper.base.utils.ui.compose.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.PathData +import androidx.compose.ui.graphics.vector.group +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val KeyMapperIcons.KeyMapperIcon: ImageVector + get() { + if (_KeyMapperIcon != null) { + return _KeyMapperIcon!! + } + _KeyMapperIcon = ImageVector.Builder( + name = "KeyMapperIcon", + defaultWidth = 61.637.dp, + defaultHeight = 61.637.dp, + viewportWidth = 61.637f, + viewportHeight = 61.637f, + ).apply { + group( + clipPathData = PathData { + moveTo(-42f, -40.363f) + lineTo(102f, -40.363f) + lineTo(102f, 103.637f) + lineTo(-42f, 103.637f) + close() + }, + ) { + } + group( + clipPathData = PathData { + moveTo(-42f, -40.363f) + lineTo(102f, -40.363f) + lineTo(102f, 103.637f) + lineTo(-42f, 103.637f) + close() + }, + ) { + } + group( + clipPathData = PathData { + moveTo(-42.094f, -40.457f) + lineTo(102.095f, -40.457f) + lineTo(102.095f, 103.731f) + lineTo(-42.094f, 103.731f) + close() + }, + ) { + } + group( + clipPathData = PathData { + moveTo(-42.047f, -40.409f) + lineTo(102.047f, -40.409f) + lineTo(102.047f, 103.684f) + lineTo(-42.047f, 103.684f) + close() + }, + ) { + } + group( + clipPathData = PathData { + moveTo(-42.047f, -40.409f) + lineTo(102.047f, -40.409f) + lineTo(102.047f, 103.684f) + lineTo(-42.047f, 103.684f) + close() + }, + ) { + } + group( + clipPathData = PathData { + moveTo(-4.667f, -3.029f) + lineTo(64.667f, -3.029f) + lineTo(64.667f, 66.304f) + lineTo(-4.667f, 66.304f) + close() + }, + ) { + } + group( + clipPathData = PathData { + moveTo(6f, 7.637f) + lineTo(54f, 7.637f) + lineTo(54f, 55.637f) + lineTo(6f, 55.637f) + close() + }, + ) { + } + group( + clipPathData = PathData { + moveTo(-4.8f, 7.504f) + lineTo(64.8f, 7.504f) + lineTo(64.8f, 55.771f) + lineTo(-4.8f, 55.771f) + close() + }, + ) { + } + group( + clipPathData = PathData { + moveTo(5.867f, -3.163f) + lineTo(54.133f, -3.163f) + lineTo(54.133f, 66.437f) + lineTo(5.867f, 66.437f) + close() + }, + ) { + } + group( + clipPathData = PathData { + moveTo(0.533f, 2.171f) + lineTo(59.467f, 2.171f) + lineTo(59.467f, 61.104f) + lineTo(0.533f, 61.104f) + close() + }, + ) { + } + group( + clipPathData = PathData { + moveTo(-18f, -16.363f) + lineTo(78f, -16.363f) + lineTo(78f, 79.637f) + lineTo(-18f, 79.637f) + close() + }, + ) { + } + path( + fill = SolidColor(Color(0xFFD32F2F)), + strokeLineWidth = 1.27586f, + ) { + moveTo(4f, 24.637f) + lineTo(33f, 24.637f) + arcTo(4f, 4f, 0f, isMoreThanHalf = false, isPositiveArc = true, 37f, 28.637f) + lineTo(37f, 57.637f) + arcTo(4f, 4f, 0f, isMoreThanHalf = false, isPositiveArc = true, 33f, 61.637f) + lineTo(4f, 61.637f) + arcTo(4f, 4f, 0f, isMoreThanHalf = false, isPositiveArc = true, 0f, 57.637f) + lineTo(0f, 28.637f) + arcTo(4f, 4f, 0f, isMoreThanHalf = false, isPositiveArc = true, 4f, 24.637f) + close() + } + path( + fill = SolidColor(Color.White), + strokeLineWidth = 1.3f, + ) { + moveTo(21.9f, 37.514f) + lineTo(21.9f, 31.937f) + curveTo(21.9f, 31.222f, 21.315f, 30.637f, 20.6f, 30.637f) + horizontalLineToRelative(-5.2f) + curveToRelative(-0.715f, 0f, -1.3f, 0.585f, -1.3f, 1.3f) + verticalLineToRelative(5.577f) + curveToRelative(0f, 0.169f, 0.065f, 0.338f, 0.195f, 0.455f) + lineToRelative(3.25f, 3.25f) + curveToRelative(0.26f, 0.26f, 0.663f, 0.26f, 0.923f, 0f) + lineToRelative(3.25f, -3.25f) + curveToRelative(0.117f, -0.117f, 0.182f, -0.273f, 0.182f, -0.455f) + close() + moveTo(11.877f, 39.737f) + lineTo(6.3f, 39.737f) + curveTo(5.585f, 39.737f, 5f, 40.322f, 5f, 41.037f) + verticalLineToRelative(5.2f) + curveToRelative(0f, 0.715f, 0.585f, 1.3f, 1.3f, 1.3f) + lineTo(11.877f, 47.537f) + curveToRelative(0.169f, 0f, 0.338f, -0.065f, 0.455f, -0.195f) + lineToRelative(3.25f, -3.25f) + curveToRelative(0.26f, -0.26f, 0.26f, -0.663f, 0f, -0.923f) + lineToRelative(-3.25f, -3.25f) + curveToRelative(-0.117f, -0.117f, -0.273f, -0.182f, -0.455f, -0.182f) + close() + moveTo(14.1f, 49.76f) + verticalLineToRelative(5.577f) + curveTo(14.1f, 56.052f, 14.685f, 56.637f, 15.4f, 56.637f) + horizontalLineToRelative(5.2f) + curveToRelative(0.715f, 0f, 1.3f, -0.585f, 1.3f, -1.3f) + lineTo(21.9f, 49.76f) + curveToRelative(0f, -0.169f, -0.065f, -0.338f, -0.195f, -0.455f) + lineToRelative(-3.25f, -3.25f) + curveToRelative(-0.26f, -0.26f, -0.663f, -0.26f, -0.923f, 0f) + lineToRelative(-3.25f, 3.25f) + curveToRelative(-0.117f, 0.117f, -0.182f, 0.273f, -0.182f, 0.455f) + close() + moveTo(23.655f, 39.932f) + lineTo(20.405f, 43.182f) + curveToRelative(-0.26f, 0.26f, -0.26f, 0.663f, 0f, 0.923f) + lineToRelative(3.25f, 3.25f) + curveToRelative(0.117f, 0.117f, 0.286f, 0.195f, 0.455f, 0.195f) + horizontalLineToRelative(5.59f) + curveTo(30.415f, 47.55f, 31f, 46.965f, 31f, 46.25f) + lineTo(31f, 41.05f) + curveTo(31f, 40.335f, 30.415f, 39.75f, 29.7f, 39.75f) + lineTo(24.123f, 39.75f) + curveToRelative(-0.182f, -0.013f, -0.338f, 0.052f, -0.468f, 0.182f) + close() + } + path( + fill = SolidColor(Color(0xFF212121)), + fillAlpha = 0f, + strokeLineWidth = 1.27586f, + ) { + moveTo(22.717f, 4.354f) + curveTo(21.679f, 5.075f, 21f, 6.272f, 21f, 7.637f) + verticalLineToRelative(29f) + curveToRelative(0f, 2.216f, 1.784f, 4f, 4f, 4f) + horizontalLineToRelative(29f) + curveToRelative(1.365f, 0f, 2.562f, -0.679f, 3.283f, -1.717f) + curveTo(56.636f, 39.37f, 55.851f, 39.637f, 55f, 39.637f) + lineTo(26f, 39.637f) + curveToRelative(-2.216f, 0f, -4f, -1.784f, -4f, -4f) + lineTo(22f, 6.637f) + curveToRelative(0f, -0.851f, 0.267f, -1.636f, 0.717f, -2.283f) + close() + } + path( + fill = SolidColor(Color(0xFF1565C0)), + strokeLineWidth = 1.27586f, + ) { + moveTo(55f, 2.862f) + lineTo(26f, 2.862f) + arcTo(4f, 4f, 0f, isMoreThanHalf = false, isPositiveArc = false, 22f, 6.862f) + lineTo(22f, 35.862f) + arcTo(4f, 4f, 0f, isMoreThanHalf = false, isPositiveArc = false, 26f, 39.862f) + lineTo(55f, 39.862f) + arcTo(4f, 4f, 0f, isMoreThanHalf = false, isPositiveArc = false, 59f, 35.862f) + lineTo(59f, 6.862f) + arcTo(4f, 4f, 0f, isMoreThanHalf = false, isPositiveArc = false, 55f, 2.862f) + close() + } + path( + fill = SolidColor(Color.White), + strokeLineWidth = 1.3f, + ) { + moveTo(51.4f, 11.437f) + lineTo(30.6f, 11.437f) + curveToRelative(-1.43f, 0f, -2.587f, 1.17f, -2.587f, 2.6f) + lineTo(28f, 27.037f) + curveToRelative(0f, 1.43f, 1.17f, 2.6f, 2.6f, 2.6f) + lineTo(51.4f, 29.637f) + curveTo(52.83f, 29.637f, 54f, 28.467f, 54f, 27.037f) + lineTo(54f, 14.037f) + curveToRelative(0f, -1.43f, -1.17f, -2.6f, -2.6f, -2.6f) + close() + moveTo(39.7f, 15.337f) + horizontalLineToRelative(2.6f) + verticalLineToRelative(2.6f) + lineTo(39.7f, 17.937f) + close() + moveTo(39.7f, 19.237f) + horizontalLineToRelative(2.6f) + lineTo(42.3f, 21.837f) + lineTo(39.7f, 21.837f) + close() + moveTo(35.8f, 15.337f) + horizontalLineToRelative(2.6f) + verticalLineToRelative(2.6f) + lineTo(35.8f, 17.937f) + close() + moveTo(35.8f, 19.237f) + horizontalLineToRelative(2.6f) + lineTo(38.4f, 21.837f) + lineTo(35.8f, 21.837f) + close() + moveTo(34.5f, 21.837f) + horizontalLineToRelative(-2.6f) + verticalLineToRelative(-2.6f) + horizontalLineToRelative(2.6f) + close() + moveTo(34.5f, 17.937f) + horizontalLineToRelative(-2.6f) + verticalLineToRelative(-2.6f) + horizontalLineToRelative(2.6f) + close() + moveTo(44.9f, 27.037f) + horizontalLineToRelative(-7.8f) + curveTo(36.385f, 27.037f, 35.8f, 26.452f, 35.8f, 25.737f) + curveTo(35.8f, 25.022f, 36.385f, 24.437f, 37.1f, 24.437f) + horizontalLineToRelative(7.8f) + curveToRelative(0.715f, 0f, 1.3f, 0.585f, 1.3f, 1.3f) + curveToRelative(0f, 0.715f, -0.585f, 1.3f, -1.3f, 1.3f) + close() + moveTo(46.2f, 21.837f) + horizontalLineToRelative(-2.6f) + verticalLineToRelative(-2.6f) + horizontalLineToRelative(2.6f) + close() + moveTo(46.2f, 17.937f) + horizontalLineToRelative(-2.6f) + verticalLineToRelative(-2.6f) + horizontalLineToRelative(2.6f) + close() + moveTo(50.1f, 21.837f) + horizontalLineToRelative(-2.6f) + verticalLineToRelative(-2.6f) + horizontalLineToRelative(2.6f) + close() + moveTo(50.1f, 17.937f) + horizontalLineToRelative(-2.6f) + verticalLineToRelative(-2.6f) + horizontalLineToRelative(2.6f) + close() + } + }.build() + + return _KeyMapperIcon!! + } + +@Suppress("ObjectPropertyName") +private var _KeyMapperIcon: ImageVector? = null diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/ProModeDisabled.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/ProModeDisabled.kt new file mode 100644 index 0000000000..7e6435500b --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/ProModeDisabled.kt @@ -0,0 +1,162 @@ +package io.github.sds100.keymapper.base.utils.ui.compose.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.PathData +import androidx.compose.ui.graphics.vector.group +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val KeyMapperIcons.ProModeIconDisabled: ImageVector + get() { + if (_ProModeDisabled != null) { + return _ProModeDisabled!! + } + _ProModeDisabled = ImageVector.Builder( + name = "ProModeDisabled", + defaultWidth = 32.dp, + defaultHeight = 32.dp, + viewportWidth = 32f, + viewportHeight = 32f, + ).apply { + group( + clipPathData = PathData { + moveTo(0f, 0f) + lineTo(32f, 0f) + lineTo(32f, 32f) + lineTo(0f, 32f) + close() + }, + ) { + } + group( + clipPathData = PathData { + moveTo(0f, 0f) + lineTo(32f, 0f) + lineTo(32f, 32f) + lineTo(0f, 32f) + close() + }, + ) { + } + group( + clipPathData = PathData { + moveTo(-0f, -0f) + lineTo(32f, -0f) + lineTo(32f, 32f) + lineTo(-0f, 32f) + close() + }, + ) { + } + group( + clipPathData = PathData { + moveTo(-0f, 32f) + lineTo(32f, 32f) + lineTo(32f, -0f) + lineTo(-0f, -0f) + close() + }, + ) { + } + group( + clipPathData = PathData { + moveTo(0f, 0f) + lineTo(32f, 0f) + lineTo(32f, 32f) + lineTo(0f, 32f) + close() + }, + ) { + } + group( + clipPathData = PathData { + moveTo(-0f, 32f) + lineTo(32f, 32f) + lineTo(32f, -0f) + lineTo(-0f, -0f) + close() + }, + ) { + } + group( + clipPathData = PathData { + moveTo(-0f, 32f) + lineTo(32f, 32f) + lineTo(32f, -0f) + lineTo(-0f, -0f) + close() + }, + ) { + } + path(fill = SolidColor(Color.Black)) { + moveToRelative(4f, 11f) + verticalLineToRelative(10f) + horizontalLineToRelative(2f) + verticalLineToRelative(-4f) + horizontalLineToRelative(2f) + arcToRelative(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = false, 2f, -2f) + verticalLineTo(13f) + arcTo(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = false, 8f, 11f) + horizontalLineTo(4f) + moveToRelative(2f, 2f) + horizontalLineToRelative(2f) + verticalLineToRelative(2f) + horizontalLineTo(6f) + close() + } + path(fill = SolidColor(Color.Black)) { + moveToRelative(13f, 11f) + verticalLineToRelative(10f) + horizontalLineToRelative(2f) + verticalLineToRelative(-4f) + horizontalLineToRelative(0.8f) + lineToRelative(1.2f, 4f) + horizontalLineToRelative(2f) + lineTo(17.76f, 16.85f) + curveTo(18.5f, 16.55f, 19f, 15.84f, 19f, 15f) + verticalLineToRelative(-2f) + arcToRelative(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = false, -2f, -2f) + horizontalLineToRelative(-4f) + moveToRelative(2f, 2f) + horizontalLineToRelative(2f) + verticalLineToRelative(2f) + horizontalLineToRelative(-2f) + close() + } + path(fill = SolidColor(Color.Black)) { + moveToRelative(24f, 11f) + arcToRelative(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = false, -2f, 2f) + verticalLineToRelative(6f) + arcToRelative(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = false, 2f, 2f) + horizontalLineToRelative(2f) + arcToRelative(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = false, 2f, -2f) + verticalLineToRelative(-6f) + arcToRelative(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = false, -2f, -2f) + horizontalLineToRelative(-2f) + moveToRelative(0f, 2f) + horizontalLineToRelative(2f) + verticalLineToRelative(6f) + horizontalLineToRelative(-2f) + close() + } + path( + fill = SolidColor(Color(0xFF808080)), + stroke = SolidColor(Color.Black), + strokeLineWidth = 2f, + strokeLineCap = StrokeCap.Round, + pathFillType = PathFillType.EvenOdd, + ) { + moveTo(26.664f, 5.353f) + lineTo(5.354f, 26.753f) + } + }.build() + + return _ProModeDisabled!! + } + +@Suppress("ObjectPropertyName") +private var _ProModeDisabled: ImageVector? = null diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/ProModeIcon.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/ProModeIcon.kt new file mode 100644 index 0000000000..ec3368f4da --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/ProModeIcon.kt @@ -0,0 +1,150 @@ +package io.github.sds100.keymapper.base.utils.ui.compose.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.PathData +import androidx.compose.ui.graphics.vector.group +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val KeyMapperIcons.ProModeIcon: ImageVector + get() { + if (_ProMode != null) { + return _ProMode!! + } + _ProMode = ImageVector.Builder( + name = "ProMode", + defaultWidth = 32.dp, + defaultHeight = 32.dp, + viewportWidth = 32f, + viewportHeight = 32f, + ).apply { + group( + clipPathData = PathData { + moveTo(0f, 0f) + lineTo(32f, 0f) + lineTo(32f, 32f) + lineTo(0f, 32f) + close() + }, + ) { + } + group( + clipPathData = PathData { + moveTo(0f, 0f) + lineTo(32f, 0f) + lineTo(32f, 32f) + lineTo(0f, 32f) + close() + }, + ) { + } + group( + clipPathData = PathData { + moveTo(-0f, -0f) + lineTo(32f, -0f) + lineTo(32f, 32f) + lineTo(-0f, 32f) + close() + }, + ) { + } + group( + clipPathData = PathData { + moveTo(-0f, 32f) + lineTo(32f, 32f) + lineTo(32f, -0f) + lineTo(-0f, -0f) + close() + }, + ) { + } + group( + clipPathData = PathData { + moveTo(0f, 0f) + lineTo(32f, 0f) + lineTo(32f, 32f) + lineTo(0f, 32f) + close() + }, + ) { + } + group( + clipPathData = PathData { + moveTo(-0f, 32f) + lineTo(32f, 32f) + lineTo(32f, -0f) + lineTo(-0f, -0f) + close() + }, + ) { + } + group( + clipPathData = PathData { + moveTo(-0f, 32f) + lineTo(32f, 32f) + lineTo(32f, -0f) + lineTo(-0f, -0f) + close() + }, + ) { + } + path(fill = SolidColor(Color.Black)) { + moveToRelative(4f, 11f) + verticalLineToRelative(10f) + horizontalLineToRelative(2f) + verticalLineToRelative(-4f) + horizontalLineToRelative(2f) + arcToRelative(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = false, 2f, -2f) + verticalLineTo(13f) + arcTo(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = false, 8f, 11f) + horizontalLineTo(4f) + moveToRelative(2f, 2f) + horizontalLineToRelative(2f) + verticalLineToRelative(2f) + horizontalLineTo(6f) + close() + } + path(fill = SolidColor(Color.Black)) { + moveToRelative(13f, 11f) + verticalLineToRelative(10f) + horizontalLineToRelative(2f) + verticalLineToRelative(-4f) + horizontalLineToRelative(0.8f) + lineToRelative(1.2f, 4f) + horizontalLineToRelative(2f) + lineTo(17.76f, 16.85f) + curveTo(18.5f, 16.55f, 19f, 15.84f, 19f, 15f) + verticalLineToRelative(-2f) + arcToRelative(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = false, -2f, -2f) + horizontalLineToRelative(-4f) + moveToRelative(2f, 2f) + horizontalLineToRelative(2f) + verticalLineToRelative(2f) + horizontalLineToRelative(-2f) + close() + } + path(fill = SolidColor(Color.Black)) { + moveToRelative(24f, 11f) + arcToRelative(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = false, -2f, 2f) + verticalLineToRelative(6f) + arcToRelative(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = false, 2f, 2f) + horizontalLineToRelative(2f) + arcToRelative(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = false, 2f, -2f) + verticalLineToRelative(-6f) + arcToRelative(2f, 2f, 0f, isMoreThanHalf = false, isPositiveArc = false, -2f, -2f) + horizontalLineToRelative(-2f) + moveToRelative(0f, 2f) + horizontalLineToRelative(2f) + verticalLineToRelative(6f) + horizontalLineToRelative(-2f) + close() + } + }.build() + + return _ProMode!! + } + +@Suppress("ObjectPropertyName") +private var _ProMode: ImageVector? = null diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/SignalWifiNotConnected.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/SignalWifiNotConnected.kt new file mode 100644 index 0000000000..00e8a9f9db --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/SignalWifiNotConnected.kt @@ -0,0 +1,91 @@ +package io.github.sds100.keymapper.base.utils.ui.compose.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val KeyMapperIcons.SignalWifiNotConnected: ImageVector + get() { + if (_SignalWifiNotConnected != null) { + return _SignalWifiNotConnected!! + } + _SignalWifiNotConnected = ImageVector.Builder( + name = "SignalWifiNotConnected", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 960f, + viewportHeight = 960f, + ).apply { + path(fill = SolidColor(Color.Black)) { + moveTo(423f, 783f) + lineTo(61f, 421f) + quadToRelative(-13f, -13f, -18.5f, -28f) + reflectiveQuadTo(38f, 361f) + quadToRelative(1f, -17f, 7f, -32f) + reflectiveQuadToRelative(20f, -26f) + quadToRelative(81f, -71f, 194.5f, -107f) + reflectiveQuadTo(480f, 160f) + quadToRelative(125f, 0f, 234f, 41f) + reflectiveQuadToRelative(203f, 122f) + quadToRelative(9f, 8f, 13.5f, 17.5f) + reflectiveQuadTo(935f, 361f) + quadToRelative(0f, 11f, -3.5f, 21f) + reflectiveQuadTo(920f, 400f) + quadToRelative(-28f, -36f, -69.5f, -58f) + reflectiveQuadTo(760f, 320f) + quadToRelative(-83f, 0f, -141.5f, 58.5f) + reflectiveQuadTo(560f, 520f) + quadToRelative(0f, 49f, 22f, 90.5f) + reflectiveQuadToRelative(58f, 69.5f) + lineTo(537f, 783f) + quadToRelative(-12f, 12f, -26.5f, 18f) + reflectiveQuadToRelative(-30.5f, 6f) + quadToRelative(-16f, 0f, -30.5f, -6f) + reflectiveQuadTo(423f, 783f) + close() + moveTo(760f, 800f) + quadToRelative(-17f, 0f, -29.5f, -12.5f) + reflectiveQuadTo(718f, 758f) + quadToRelative(0f, -17f, 12.5f, -29.5f) + reflectiveQuadTo(760f, 716f) + quadToRelative(17f, 0f, 29.5f, 12.5f) + reflectiveQuadTo(802f, 758f) + quadToRelative(0f, 17f, -12.5f, 29.5f) + reflectiveQuadTo(760f, 800f) + close() + moveTo(876f, 503f) + quadToRelative(0f, 23f, -10f, 41f) + reflectiveQuadToRelative(-38f, 46f) + quadToRelative(-17f, 17f, -24.5f, 28f) + reflectiveQuadToRelative(-9.5f, 25f) + quadToRelative(-2f, 12f, -11.5f, 20.5f) + reflectiveQuadTo(761f, 672f) + quadToRelative(-13f, 0f, -22f, -9f) + reflectiveQuadToRelative(-7f, -21f) + quadToRelative(3f, -23f, 14f, -40f) + reflectiveQuadToRelative(37f, -43f) + quadToRelative(21f, -21f, 27f, -31.5f) + reflectiveQuadToRelative(6f, -26.5f) + quadToRelative(0f, -18f, -14f, -31.5f) + reflectiveQuadTo(765f, 456f) + quadToRelative(-15f, 0f, -29f, 7f) + reflectiveQuadToRelative(-24f, 20f) + quadToRelative(-7f, 9f, -17.5f, 13f) + reflectiveQuadToRelative(-21.5f, -1f) + quadToRelative(-11f, -5f, -16.5f, -15f) + reflectiveQuadToRelative(0.5f, -20f) + quadToRelative(16f, -28f, 44.5f, -44f) + reflectiveQuadToRelative(63.5f, -16f) + quadToRelative(49f, 0f, 80f, 29f) + reflectiveQuadToRelative(31f, 74f) + close() + } + }.build() + + return _SignalWifiNotConnected!! + } + +@Suppress("ObjectPropertyName") +private var _SignalWifiNotConnected: ImageVector? = null diff --git a/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/WandStars.kt b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/WandStars.kt new file mode 100644 index 0000000000..0215618831 --- /dev/null +++ b/base/src/main/java/io/github/sds100/keymapper/base/utils/ui/compose/icons/WandStars.kt @@ -0,0 +1,99 @@ +package io.github.sds100.keymapper.base.utils.ui.compose.icons + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +val KeyMapperIcons.WandStars: ImageVector + get() { + if (_WandStars != null) { + return _WandStars!! + } + _WandStars = ImageVector.Builder( + name = "WandStars24Dp000000FILL0Wght400GRAD0Opsz24", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 960f, + viewportHeight = 960f, + ).apply { + path(fill = SolidColor(Color.Black)) { + moveToRelative(646f, 522f) + lineToRelative(-86f, 138f) + quadToRelative(-11f, 17f, -30.5f, 14f) + reflectiveQuadTo(505f, 651f) + lineToRelative(-28f, -112f) + lineToRelative(-273f, 273f) + quadToRelative(-11f, 11f, -27.5f, 11.5f) + reflectiveQuadTo(148f, 812f) + quadToRelative(-11f, -11f, -11f, -28f) + reflectiveQuadToRelative(11f, -28f) + lineToRelative(273f, -274f) + lineToRelative(-112f, -28f) + quadToRelative(-20f, -5f, -23f, -24.5f) + reflectiveQuadToRelative(14f, -30.5f) + lineToRelative(138f, -85f) + lineToRelative(-12f, -163f) + quadToRelative(-2f, -20f, 16f, -29f) + reflectiveQuadToRelative(33f, 4f) + lineToRelative(125f, 105f) + lineToRelative(151f, -61f) + quadToRelative(19f, -8f, 33f, 6f) + reflectiveQuadToRelative(6f, 33f) + lineToRelative(-61f, 151f) + lineToRelative(105f, 124f) + quadToRelative(13f, 15f, 4f, 33f) + reflectiveQuadToRelative(-29f, 16f) + lineToRelative(-163f, -11f) + close() + moveTo(134f, 254f) + quadToRelative(-6f, -6f, -6f, -14f) + reflectiveQuadToRelative(6f, -14f) + lineToRelative(52f, -52f) + quadToRelative(6f, -6f, 14f, -6f) + reflectiveQuadToRelative(14f, 6f) + lineToRelative(52f, 52f) + quadToRelative(6f, 6f, 6f, 14f) + reflectiveQuadToRelative(-6f, 14f) + lineToRelative(-52f, 52f) + quadToRelative(-6f, 6f, -14f, 6f) + reflectiveQuadToRelative(-14f, -6f) + lineToRelative(-52f, -52f) + close() + moveTo(555f, 517f) + lineTo(603f, 438f) + lineTo(696f, 445f) + lineTo(636f, 374f) + lineTo(671f, 288f) + lineTo(585f, 323f) + lineTo(514f, 264f) + lineTo(521f, 356f) + lineTo(442f, 405f) + lineTo(532f, 427f) + lineTo(555f, 517f) + close() + moveTo(706f, 826f) + lineTo(654f, 774f) + quadToRelative(-6f, -6f, -6f, -14f) + reflectiveQuadToRelative(6f, -14f) + lineToRelative(52f, -52f) + quadToRelative(6f, -6f, 14f, -6f) + reflectiveQuadToRelative(14f, 6f) + lineToRelative(52f, 52f) + quadToRelative(6f, 6f, 6f, 14f) + reflectiveQuadToRelative(-6f, 14f) + lineToRelative(-52f, 52f) + quadToRelative(-6f, 6f, -14f, 6f) + reflectiveQuadToRelative(-14f, -6f) + close() + moveTo(569f, 390f) + close() + } + }.build() + + return _WandStars!! + } + +@Suppress("ObjectPropertyName") +private var _WandStars: ImageVector? = null diff --git a/base/src/main/res/drawable/pro_mode.xml b/base/src/main/res/drawable/pro_mode.xml new file mode 100644 index 0000000000..a0062fc0ed --- /dev/null +++ b/base/src/main/res/drawable/pro_mode.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/base/src/main/res/drawable/profile_pic_bydario.png b/base/src/main/res/drawable/profile_pic_bydario.png deleted file mode 100644 index cbd6e85dad..0000000000 Binary files a/base/src/main/res/drawable/profile_pic_bydario.png and /dev/null differ diff --git a/base/src/main/res/drawable/profile_pic_kekero.png b/base/src/main/res/drawable/profile_pic_kekero.png deleted file mode 100644 index 1f2c7aee26..0000000000 Binary files a/base/src/main/res/drawable/profile_pic_kekero.png and /dev/null differ diff --git a/base/src/main/res/layout/fragment_about.xml b/base/src/main/res/layout/fragment_about.xml index 2ca991c4b6..1ae3fae328 100644 --- a/base/src/main/res/layout/fragment_about.xml +++ b/base/src/main/res/layout/fragment_about.xml @@ -259,32 +259,6 @@ bind:title="@{@string/about_team_jambl3r_name}" bind:url="@{@string/about_team_jambl3r_url}" /> - - - - - diff --git a/base/src/main/res/layout/fragment_config_key_event.xml b/base/src/main/res/layout/fragment_config_key_event.xml index 70b32c74a4..54ceababbe 100644 --- a/base/src/main/res/layout/fragment_config_key_event.xml +++ b/base/src/main/res/layout/fragment_config_key_event.xml @@ -88,22 +88,6 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/textViewKeycodeLabel" /> - - + app:layout_constraintTop_toBottomOf="@id/buttonChooseKeycode"> - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/base/src/main/res/navigation/nav_base_app.xml b/base/src/main/res/navigation/nav_base_app.xml index e3f43da0f9..54b478e60c 100644 --- a/base/src/main/res/navigation/nav_base_app.xml +++ b/base/src/main/res/navigation/nav_base_app.xml @@ -5,24 +5,6 @@ xmlns:tools="http://schemas.android.com/tools" android:id="@+id/nav_base_app"> - - - - - - - - \ No newline at end of file diff --git a/base/src/main/res/navigation/nav_settings.xml b/base/src/main/res/navigation/nav_settings.xml deleted file mode 100644 index fbca7f6d25..0000000000 --- a/base/src/main/res/navigation/nav_settings.xml +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/base/src/main/res/values/strings.xml b/base/src/main/res/values/strings.xml index 032e8d07ce..1e5875f4e7 100644 --- a/base/src/main/res/values/strings.xml +++ b/base/src/main/res/values/strings.xml @@ -10,7 +10,6 @@ ¯\\_(ツ)_/¯\n\nNothing here! The first step is to add some buttons that will trigger the key map.\n\nFirst tap ‘Record trigger’ and then press the buttons that you want to remap. They will appear here.\n\nAlternatively, you can trigger a key map using an ‘advanced trigger’.\n\nYou can mix and match any keys! Requires root - Press… No actions No trigger @@ -72,10 +71,8 @@ Send broadcast: %s Key map id - Use shell (ROOT only) Permission required to work properly in Do Not Disturb mode! - The option to trigger when the screen is off needs root permission to work! This trigger won\'t work while ringing or in a phone call! Android doesn\'t let accessibility services detect volume button presses while your phone is ringing or it is in a phone call but it does let input method services detect them. Therefore, you must use one of the Key Mapper keyboards if you want this trigger to work. Too many fingers to perform gesture due to android limitations. @@ -97,7 +94,6 @@ Open %s Type \'%s\' Input %s%s - Input %s through shell Input %s%s from %s Open %s Tap screen (%d, %d) @@ -156,13 +152,16 @@ - At the same time - In sequence + Press together + Press in sequence AND OR Short press Long press Double press + Short + Long + Double True False Activity @@ -398,7 +397,7 @@ NEW! Done Fix - Recording (%d…) + Press your keys Add constraint Choose Key code Add extra @@ -421,8 +420,7 @@ Can\'t find the accessibility settings page Unsaved changes You have unsaved changes. If you discard them, your edits will be lost. - If you know your phone isn\'t rooted or you don\'t know what root is, you can\'t use features which only work on rooted devices. When you tap \'OK\', you will be taken to the settings. - In the settings, scroll to the bottom and tap \'Key Mapper has root permission\' so you can use root features/actions. + Please grant Key Mapper root permission in your root management app, such as Magisk. Grant WRITE_SECURE_SETTINGS permission A PC/Mac is required to grant this permission. Read the online guide. @@ -431,8 +429,8 @@ The keys need to be listed from top to bottom in the order that they will be held down. A \"sequence\" trigger has a timeout unlike parallel triggers. This means after you press the first key, you will have a set amount of time to input the rest of the keys in the trigger. All the keys that you have added to the trigger won\'t do their usual action until the timeout has been reached. You can change this timeout in the "Options" tab. Android doesn\'t allow apps to get a list of connected (not paired) Bluetooth devices. Apps can only detect when they are connected and disconnected. So if your Bluetooth device is already connected to your device when the accessibility service starts, you will have to reconnect it for the app to know it is connected. + Automatic backup Change location or turn off automatic back up? - Screen on/off constraints will only work if you have turned on the \"detect trigger when screen is off\" key map option. This option will only show for some keys (e.g volume buttons) and if you are rooted. See a list of supported keys on the Help page. If you have any other screen lock chosen, such as PIN or Pattern then you don\'t have to worry. But if you have a Password screen lock you will *NOT* be able to unlock your phone if you use the Key Mapper Basic Input Method because it doesn\'t have a GUI. You can grant Key Mapper WRITE_SECURE_SETTINGS permission so it can show a notification to switch to and from the keyboard. There is a guide on how to do this if you tap the question mark at the bottom of the screen. Select the input method for actions that require one. You can change this later by tapping \"Select keyboard for actions\" in the bottom menu of the home screen. You need to choose the \"Caps Lock to camera\" keyboard layout for your keyboard otherwise the Caps Lock key will still lock caps. You can find this setting in your device settings -> Languages and Input -> Physical Keyboard -> Tap on your keyboard -> Set Up Keyboard Layouts. This will remap the Caps Lock key to KEYCODE_CAMERA so Key Mapper can remap it properly.\n\nAfter you\'ve done this you must remove the Caps Lock trigger key and record the Caps Lock key again. It should say \"Camera\" instead of \"Caps Lock\" if you did the steps correctly. @@ -507,9 +505,6 @@ Drag handle for %1$s Show example - Unrecognized key code - The pressed button was not recognized by the input system. In the past Key Mapper detected such buttons as one and the same. Currently the app tries to distinguish the button based on the scan code, which should be more unique. However, this is a makeshift incomplete solution, which doesn\'t guarantee uniqueness. - Turn on notifications Some actions and options need this permission to work. You can also get notified when there is important news about the app. @@ -549,15 +544,11 @@ - Keyboard picker Pause/Resume key maps Keyboard is hidden warning Toggle Key Mapper keyboard New features - Tap to change your keyboard. - Keyboard picker - Running Tap to open Key Mapper. Pause @@ -589,42 +580,35 @@ - Default long press delay (ms) + Default long press delay How long a button should be pressed for it to be detected as a long press. Default is 500ms. Can be overridden in a key map\'s options. - Default double press duration (ms) + Default double press duration How fast does a button have to be double pressed for it to be detected as a double press. Default is 300ms. Can be overridden in a key map\'s options. How long to vibrate if vibrating is enabled for a key map. Default is 200ms. Can be overridden in a key map\'s options. - Default vibrate duration (ms) + Default vibrate duration How long the trigger needs to be held down for the action to start repeating. Default is 400ms. Can be overridden in a key map\'s options. - Default delay until repeat (ms) + Default delay until repeat The delay between every time an action is repeated. Default is 50ms. Can be overridden in a key map\'s options. - Default delay between repeats (ms) + Default delay between repeats The time allowed to complete a sequence trigger. Default is 1000ms. Can be overridden in a key map\'s options. - Default sequence trigger timeout (ms) + Default sequence trigger timeout Reset - Force all key maps to vibrate. - Force vibrate - - Keyboard picker notification - Show a persistent notification to allow you to pick a keyboard. - - Pause/resume key maps notification - Show a persistent notification which starts/pauses your key maps. - - Automatically back up key maps to a specified location - No location chosen. + Make all key maps vibrate + Every time a key map is triggered - Choose devices + Show pause/resume notification + Toggle your key maps on and off - Automatically show keyboard picker - When a device that you have chosen connects or disconnects the keyboard picker will show automatically. Choose the devices below. + Change automatic backup location + Turn on automatic backup + Periodically back up your key maps Automatically change the on-screen keyboard when a device (e.g a keyboard) connects/disconnects The last used Key Mapper keyboard will be automatically selected when a chosen device is connected. Your normal keyboard will be automatically selected when the device disconnects. @@ -632,11 +616,11 @@ Automatically change the on-screen keyboard when you start inputting text The last used non Key Mapper keyboard will be automatically selected when you try to open the keyboard. Your Key Mapper keyboard will be automatically selected once you stop using the keyboard. - Show an on-screen message when automatically changing the keyboard + On-screen message + Show when automatically changing the keyboard - Key Mapper has root permission - Enable this if you want to use features/actions which only work on rooted devices. Key Mapper must have root permission from your root-access-management app (e.g Magisk, SuperSU) for these features to work. - Only turn this on if you know your device is rooted and you have given Key Mapper root permission. + Request root permission + If your device is rooted then this will show the root permission pop up from Magisk or your root app. Choose theme Light and dark themes available @@ -648,30 +632,16 @@ Automatically select the Key Mapper keyboard when you resume your key maps and select your default keyboard when pausing them. Hide home screen alerts - Hide the alerts at the top of the home screen. - - Show the first 5 characters of the device id for device specific triggers - This is useful to differentiate between devices that have the same name. - - Fix keyboards that are set to US English - This fixes keyboards that don\'t have the correct the keyboard layout when an accessibility service is enabled. Tap to read more and configure. - - Fix keyboards that are set to US English - There is a bug in Android 11 that turning on an accessibility service makes Android think all external devices are the same internal virtual device. Because it can\'t identify these devices correctly, it doesn\'t know which keyboard layout to use with them so it defaults to US English even if it is a German keyboard for example. You can use Key Mapper to work around this problem by following the steps below. - - 4. Choose devices - - 1. Install the Key Mapper GUI Keyboard (optional) - 1. Install the Key Mapper Leanback Keyboard (optional) + Hide the alerts at the top of the home screen - 2. Enable the Key Mapper GUI Keyboard or the Key Mapper Basic Input Method - 2. Enable the Key Mapper Leanback Keyboard or the Key Mapper Basic Input Method - - 3. Use the keyboard that you just enabled - (Recommended) Read user guide for this setting. + Show device IDs + Differentiate devices with the same name Enable extra logging - View and share log + Record more detailed logs + View log + Share this with the developer if you\'re having issues + Report issue Delete sound files @@ -689,22 +659,25 @@ 3. Key Mapper does not have permission to use Shizuku. Tap to grant this permission. 3. Key Mapper will automatically use Shizuku. Tap to read which Key Mapper features use Shizuku. - Default mapping options - Change the default options for your key maps. + Change default options + For triggers and actions - Reset all settings - DANGER! Reset all settings in the app to the default. Your key maps will NOT be reset. + Reset all DANGER! - Are you sure you want to reset all settings in the app to the default? Your key maps will NOT be reset. The introduction screen and all warning pop ups will show again. + Are you sure you want to reset all settings in the app to the default? Your existing key maps will not be affected. The introductions and warning pop ups will show again. Yes, reset + Reset all + Customize your experience + Key maps + Data management + Power user options + Debugging + Notifications - Automatically show the keyboard picker - Tap to see the settings that allow you to automatically show the keyboard picker. - Root settings These options will only work on root devices! If you don\'t know what root is or whether your device is rooted, please don\'t leave a poor review if they don\'t work. :) @@ -713,30 +686,30 @@ Shizuku support Shizuku is an app that allows Key Mapper to do things that only system apps can do. You don\'t need to use the Key Mapper keyboard for example. Tap to learn how to set this up. - Follow these steps to set up Shizuku. + Follow these steps to set up Shizuku - Automatically change the keyboard - These are really useful settings and you are recommended to check them out! + Automatically switch keyboard + Switch when needed then switch back + + Choose devices + Choose which devices trigger automatic keyboard switching Logging This may add latency to your key maps so only turn this on if you are trying to debug the app or have been asked to by the developer. - - - + Use PRO mode + Advanced detection of key events and more - - Light - Dark - Follow system - + Light + Dark + System + Show volume dialog Vibrate Show an on-screen message Vibrate again on long press - Detect trigger when the screen is off Repeat %dx @@ -919,6 +892,7 @@ Must be %d or less! UI element not found! + PRO Mode needs starting @@ -1061,7 +1035,6 @@ You will only be able to log back in with your PIN. The fingerprint scanner and face unlock will be disabled. This is the only reliable way I have found to lock non-rooted devices before Android Pie 9.0. Sleep/wake device - You must turn on the option to detect the trigger when the screen is off! Do nothing @@ -1516,6 +1489,15 @@ Swipe left fingerprint reader Swipe right fingerprint reader Advanced triggers + Key code %d + Scan code %d + Scan code + Keys can be identified by either a \'key code\' or a \'scan code\'. A scan code is more unique than a key code but your trigger may not work with other devices. We recommend using key codes. + Unknown key code + Use key code %d + Use scan code %d + No scan code saved + Use PRO mode Remove @@ -1574,4 +1556,110 @@ +%d inherited constraint +%d inherited constraints + + + + PRO mode + Important! + Remapping buttons with PRO mode is dangerous and can cause them to stop working if you remap them incorrectly.\n\nIf you make a mistake, you may need to force restart your device by holding down the power and volume buttons for 30 seconds — consult your device\'s manual or the internet for how to do this. + %d… + I understand + Understood + Set up + Root detected + You can skip the set up process by giving Key Mapper root permission. This will let Key Mapper auto start PRO mode on boot as well. + Start PRO mode + Shizuku detected + You can skip the set up process by giving Key Mapper Shizuku permission. + Start Shizuku + Request permission + Start PRO mode + Set up with Key Mapper + Continue + Continue (Android 11+) + Options + Enable PRO mode for all key maps + Key Mapper will use the ADB Shell for remapping + These settings are unavailable until you acknowledge the warning. + PRO mode service is running + Stop + + Automatically start at boot + PRO Mode will start itself whenever you turn on or restart your device + + Emergency tip + If your buttons stop working, hold down the power button for 10 seconds and release to disable PRO Mode. + + Setup wizard + Step %d of %d + Use interactive setup assistant + Automatically interact with settings + Enable accessibility service first + Watch tutorial + Start service + Go to settings + Start service + + Enable accessibility service + Key Mapper uses a service to help you set up PRO mode. It\'s also useful for ordinary key maps. + + Enable developer options + Key Mapper needs to use Android Debug Bridge to start PRO mode, and you need to enable developer options for that. + + Connect to a WiFi network + Key Mapper needs a WiFi network to enable ADB. You do not need an internet connection.\n\nNo WiFi network? Use a hotspot from someone else\'s phone. + + Enable wireless debugging + Key Mapper uses wireless debugging to launch its remapping and input service. + + Pair wireless debugging + Key Mapper needs to pair with wireless debugging before it can launch its remapping and input service. + + Start service + Key Mapper needs to connect to the Android Debug Bridge to start the PRO mode service. + + Allow notifications + Key Mapper needs permission to notify you if there are any issues with the set up process. + Give permission + + Setup assistant + + PRO mode is running + You can now remap buttons when the screen is off and use more actions. + + Finish + + Enable developer options + Tap build number repeatedly + + Pairing automatically + Searching for pairing code and port… + + Unable to find pairing port and code + Tap on the button to pair with pairing code and type the code in here + + Starting PRO Mode failed + Tap to set up again. Try ADB pairing and rebooting your phone if it repeatedly fails. + + Auto starting PRO mode + Using root + Using shizuku + Using ADB over WiFi + + PRO mode started + Have fun remapping! ❤️ + + Pairing failed + Keep the pairing code popup on-screen when submitting the pairing code + Input pairing code + + What can I do with PRO mode? + 📲 You can remap more buttons, such as your power button.\n⌨️ Use any keyboard with key code actions.\n⭐️ The following actions are unlocked: WiFi, Bluetooth, mobile data, NFC, and airplane mode, collapse status bar, and sleep/wake device. + Show PRO mode info + Dismiss + + PRO mode stopped unexpectedly + Automatically restarting… + Not auto restarting. If you\'re not killing the service report the issue to the developer. + diff --git a/base/src/test/java/io/github/sds100/keymapper/base/BackupManagerTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/BackupManagerTest.kt index da13c340f5..425d6343a4 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/BackupManagerTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/BackupManagerTest.kt @@ -131,170 +131,211 @@ class BackupManagerTest { } /** - * Issue #1655. If the list of groups in the backup has a child before the parent then the - * parent must be restored first. Otherwise the SqliteConstraintException will be thrown. + * If the backup contains a group who's parent does not exist in the backup then + * set the parent to null. */ @Test - fun `restore groups breadth first so parents exist before children are restored with child first in the backup`() = runTest(testDispatcher) { - val parentGroup1 = GroupEntity( - uid = "parent_group_1_uid", - name = "parent_group_1_name", - parentUid = null, + fun `restore group with missing parent to root group`() = runTest(testDispatcher) { + val group = GroupEntity( + uid = "child_group", + name = "Child", + parentUid = "parent_group", lastOpenedDate = 0L, ) - GroupEntity( - uid = "parent_group_2_uid", - name = "parent_group_2_name", - parentUid = null, - lastOpenedDate = 0L, + val backupContent = BackupContent( + appVersion = BuildConfig.VERSION_CODE, + dbVersion = AppDatabase.DATABASE_VERSION, + groups = listOf(group), ) - val childGroup = GroupEntity( - uid = "child_group_uid", - name = "child_group_name", - parentUid = parentGroup1.uid, - lastOpenedDate = 0L, + backupManager.restore( + RestoreType.REPLACE, + backupContent, + emptyList(), + currentTime = 0L, ) - val grandChildGroup = GroupEntity( - uid = "grand_child_group_uid", - name = "grand_child_group_name", - parentUid = childGroup.uid, + val expectedGroup = GroupEntity( + uid = "child_group", + name = "Child", + // The parent is null because it did not exist. + parentUid = null, lastOpenedDate = 0L, ) - val backupContent = BackupContent( - appVersion = BuildConfig.VERSION_CODE, - dbVersion = AppDatabase.DATABASE_VERSION, - groups = listOf(childGroup, grandChildGroup, parentGroup1), - ) + verify(mockGroupRepository).insert(expectedGroup) + } - inOrder(mockGroupRepository) { - backupManager.restore( - RestoreType.REPLACE, - backupContent, - emptyList(), - currentTime = 0L, + /** + * Issue #1655. If the list of groups in the backup has a child before the parent then the + * parent must be restored first. Otherwise the SqliteConstraintException will be thrown. + */ + @Test + fun `restore groups breadth first so parents exist before children are restored with child first in the backup`() = + runTest(testDispatcher) { + val parentGroup1 = GroupEntity( + uid = "parent_group_1_uid", + name = "parent_group_1_name", + parentUid = null, + lastOpenedDate = 0L, + ) + + GroupEntity( + uid = "parent_group_2_uid", + name = "parent_group_2_name", + parentUid = null, + lastOpenedDate = 0L, + ) + + val childGroup = GroupEntity( + uid = "child_group_uid", + name = "child_group_name", + parentUid = parentGroup1.uid, + lastOpenedDate = 0L, + ) + + val grandChildGroup = GroupEntity( + uid = "grand_child_group_uid", + name = "grand_child_group_name", + parentUid = childGroup.uid, + lastOpenedDate = 0L, ) - verify(mockGroupRepository).insert(parentGroup1) - verify(mockGroupRepository).insert(childGroup) - verify(mockGroupRepository).insert(grandChildGroup) - verify(mockGroupRepository, never()).update(any()) + val backupContent = BackupContent( + appVersion = BuildConfig.VERSION_CODE, + dbVersion = AppDatabase.DATABASE_VERSION, + groups = listOf(childGroup, grandChildGroup, parentGroup1), + ) + + inOrder(mockGroupRepository) { + backupManager.restore( + RestoreType.REPLACE, + backupContent, + emptyList(), + currentTime = 0L, + ) + + verify(mockGroupRepository).insert(parentGroup1) + verify(mockGroupRepository).insert(childGroup) + verify(mockGroupRepository).insert(grandChildGroup) + verify(mockGroupRepository, never()).update(any()) + } } - } /** * Issue #1655. If the list of groups in the backup has a child before the parent then the * parent must be restored first. Otherwise the SqliteConstraintException will be thrown. */ @Test - fun `restore groups breadth first so parents exist before children are restored`() = runTest(testDispatcher) { - val parentGroup1 = GroupEntity( - uid = "parent_group_1_uid", - name = "parent_group_1_name", - parentUid = null, - lastOpenedDate = 0L, - ) - - val parentGroup2 = GroupEntity( - uid = "parent_group_2_uid", - name = "parent_group_2_name", - parentUid = null, - lastOpenedDate = 0L, - ) + fun `restore groups breadth first so parents exist before children are restored`() = + runTest(testDispatcher) { + val parentGroup1 = GroupEntity( + uid = "parent_group_1_uid", + name = "parent_group_1_name", + parentUid = null, + lastOpenedDate = 0L, + ) - val childGroup = GroupEntity( - uid = "child_group_uid", - name = "child_group_name", - parentUid = parentGroup1.uid, - lastOpenedDate = 0L, - ) + val parentGroup2 = GroupEntity( + uid = "parent_group_2_uid", + name = "parent_group_2_name", + parentUid = null, + lastOpenedDate = 0L, + ) - val grandChildGroup = GroupEntity( - uid = "grand_child_group_uid", - name = "grand_child_group_name", - parentUid = childGroup.uid, - lastOpenedDate = 0L, - ) + val childGroup = GroupEntity( + uid = "child_group_uid", + name = "child_group_name", + parentUid = parentGroup1.uid, + lastOpenedDate = 0L, + ) - val backupContent = BackupContent( - appVersion = BuildConfig.VERSION_CODE, - dbVersion = AppDatabase.DATABASE_VERSION, - groups = listOf(parentGroup2, grandChildGroup, childGroup, parentGroup1), - ) + val grandChildGroup = GroupEntity( + uid = "grand_child_group_uid", + name = "grand_child_group_name", + parentUid = childGroup.uid, + lastOpenedDate = 0L, + ) - inOrder(mockGroupRepository) { - backupManager.restore( - RestoreType.REPLACE, - backupContent, - emptyList(), - currentTime = 0L, + val backupContent = BackupContent( + appVersion = BuildConfig.VERSION_CODE, + dbVersion = AppDatabase.DATABASE_VERSION, + groups = listOf(parentGroup2, grandChildGroup, childGroup, parentGroup1), ) - verify(mockGroupRepository).insert(parentGroup2) - verify(mockGroupRepository).insert(parentGroup1) - verify(mockGroupRepository).insert(childGroup) - verify(mockGroupRepository).insert(grandChildGroup) - verify(mockGroupRepository, never()).update(any()) + inOrder(mockGroupRepository) { + backupManager.restore( + RestoreType.REPLACE, + backupContent, + emptyList(), + currentTime = 0L, + ) + + verify(mockGroupRepository).insert(parentGroup2) + verify(mockGroupRepository).insert(parentGroup1) + verify(mockGroupRepository).insert(childGroup) + verify(mockGroupRepository).insert(grandChildGroup) + verify(mockGroupRepository, never()).update(any()) + } } - } @Test - fun `when backing up everything include layouts that are not in the list of key maps`() = runTest(testDispatcher) { - val layoutWithButtons = FloatingLayoutEntityWithButtons( - layout = FloatingLayoutEntity( - uid = "layout_uid", - name = "layout_name", - ), - buttons = listOf( - FloatingButtonEntity( - uid = "button_uid", - layoutUid = "layout_uid", - text = "Button", - buttonSize = 10, - x = 0, - y = 0, - orientation = "orientation", - displayWidth = 100, - displayHeight = 100, - borderOpacity = null, - backgroundOpacity = null, + fun `when backing up everything include layouts that are not in the list of key maps`() = + runTest(testDispatcher) { + val layoutWithButtons = FloatingLayoutEntityWithButtons( + layout = FloatingLayoutEntity( + uid = "layout_uid", + name = "layout_name", ), - ), - ) + buttons = listOf( + FloatingButtonEntity( + uid = "button_uid", + layoutUid = "layout_uid", + text = "Button", + buttonSize = 10, + x = 0, + y = 0, + orientation = "orientation", + displayWidth = 100, + displayHeight = 100, + borderOpacity = null, + backgroundOpacity = null, + ), + ), + ) - val content = backupManager.createBackupContent( - keyMapList = emptyList(), - extraGroups = emptyList(), - extraLayouts = listOf(layoutWithButtons), - ) + val content = backupManager.createBackupContent( + keyMapList = emptyList(), + extraGroups = emptyList(), + extraLayouts = listOf(layoutWithButtons), + ) - assertThat(content.floatingLayouts, Matchers.contains(layoutWithButtons.layout)) - assertThat( - content.floatingButtons, - Matchers.contains(*layoutWithButtons.buttons.toTypedArray()), - ) - } + assertThat(content.floatingLayouts, Matchers.contains(layoutWithButtons.layout)) + assertThat( + content.floatingButtons, + Matchers.contains(*layoutWithButtons.buttons.toTypedArray()), + ) + } @Test - fun `when backing up everything include groups that are not in the list of key maps`() = runTest(testDispatcher) { - val group = GroupEntity( - uid = "group_uid", - name = "group_name", - parentUid = null, - lastOpenedDate = 0L, - ) + fun `when backing up everything include groups that are not in the list of key maps`() = + runTest(testDispatcher) { + val group = GroupEntity( + uid = "group_uid", + name = "group_name", + parentUid = null, + lastOpenedDate = 0L, + ) - val content = backupManager.createBackupContent( - keyMapList = emptyList(), - extraGroups = listOf(group), - extraLayouts = emptyList(), - ) + val content = backupManager.createBackupContent( + keyMapList = emptyList(), + extraGroups = listOf(group), + extraLayouts = emptyList(), + ) - assertThat(content.groups, Matchers.contains(group)) - } + assertThat(content.groups, Matchers.contains(group)) + } /** * #745 @@ -352,24 +393,25 @@ class BackupManagerTest { } @Test - fun `successfully restore zip folder with data json and sound files`() = runTest(testDispatcher) { - // GIVEN - val dataJsonFile = "restore-all.zip/data.json" - val soundFile = "restore-all.zip/sounds/sound.ogg" - val zipFile = fakeFileAdapter.getPrivateFile("backup.zip") + fun `successfully restore zip folder with data json and sound files`() = + runTest(testDispatcher) { + // GIVEN + val dataJsonFile = "restore-all.zip/data.json" + val soundFile = "restore-all.zip/sounds/sound.ogg" + val zipFile = fakeFileAdapter.getPrivateFile("backup.zip") - copyFileToPrivateFolder(dataJsonFile, destination = "backup.zip/data.json") - copyFileToPrivateFolder(soundFile, destination = "backup.zip/sounds/sound.ogg") + copyFileToPrivateFolder(dataJsonFile, destination = "backup.zip/data.json") + copyFileToPrivateFolder(soundFile, destination = "backup.zip/sounds/sound.ogg") - // WHEN - val result = backupManager.restore(zipFile, RestoreType.REPLACE) + // WHEN + val result = backupManager.restore(zipFile, RestoreType.REPLACE) - // THEN - assertThat(result, `is`(Success(Unit))) + // THEN + assertThat(result, `is`(Success(Unit))) - verify(mockKeyMapRepository, times(1)).insert(any(), any()) - verify(mockSoundsManager, times(1)).restoreSound(any()) - } + verify(mockKeyMapRepository, times(1)).insert(any(), any()) + verify(mockSoundsManager, times(1)).restoreSound(any()) + } @Test fun `backup sound file if there is a key map with a sound action`() = runTest(testDispatcher) { @@ -514,67 +556,73 @@ class BackupManagerTest { } @Test - fun `restore keymaps with no db version, assume version is 9 and don't show error message`() = runTest(testDispatcher) { - val fileName = "restore-keymaps-no-db-version.json" + fun `restore keymaps with no db version, assume version is 9 and don't show error message`() = + runTest(testDispatcher) { + val fileName = "restore-keymaps-no-db-version.json" - val result = - backupManager.restore(copyFileToPrivateFolder(fileName), RestoreType.REPLACE) + val result = + backupManager.restore(copyFileToPrivateFolder(fileName), RestoreType.REPLACE) - assertThat(result, `is`(Success(Unit))) - verify(mockKeyMapRepository, times(1)).insert(any(), any()) - } + assertThat(result, `is`(Success(Unit))) + verify(mockKeyMapRepository, times(1)).insert(any(), any()) + } @Test - fun `restore a single legacy fingerprint map, only restore a single fingerprint map and a success message`() = runTest(testDispatcher) { - val fileName = "restore-legacy-single-fingerprint-map.json" + fun `restore a single legacy fingerprint map, only restore a single fingerprint map and a success message`() = + runTest(testDispatcher) { + val fileName = "restore-legacy-single-fingerprint-map.json" - val result = - backupManager.restore(copyFileToPrivateFolder(fileName), RestoreType.REPLACE) + val result = + backupManager.restore(copyFileToPrivateFolder(fileName), RestoreType.REPLACE) - assertThat(result, `is`(Success(Unit))) - } + assertThat(result, `is`(Success(Unit))) + } @Test - fun `restore all legacy fingerprint maps, all fingerprint maps should be restored and a success message`() = runTest(testDispatcher) { - val fileName = "restore-all-legacy-fingerprint-maps.json" + fun `restore all legacy fingerprint maps, all fingerprint maps should be restored and a success message`() = + runTest(testDispatcher) { + val fileName = "restore-all-legacy-fingerprint-maps.json" - val result = - backupManager.restore(copyFileToPrivateFolder(fileName), RestoreType.REPLACE) + val result = + backupManager.restore(copyFileToPrivateFolder(fileName), RestoreType.REPLACE) - assertThat(result, `is`(Success(Unit))) - } + assertThat(result, `is`(Success(Unit))) + } @Test - fun `restore many key maps and device info, all key maps and device info should be restored and a success message`() = runTest(testDispatcher) { - val fileName = "restore-many-keymaps.json" + fun `restore many key maps and device info, all key maps and device info should be restored and a success message`() = + runTest(testDispatcher) { + val fileName = "restore-many-keymaps.json" - val result = - backupManager.restore(copyFileToPrivateFolder(fileName), RestoreType.REPLACE) + val result = + backupManager.restore(copyFileToPrivateFolder(fileName), RestoreType.REPLACE) - assertThat(result, `is`(Success(Unit))) - verify(mockKeyMapRepository, times(1)).insert(any(), any(), any(), any()) - } + assertThat(result, `is`(Success(Unit))) + verify(mockKeyMapRepository, times(1)).insert(any(), any(), any(), any()) + } @Test - fun `restore with key map db version greater than allowed version, send incompatible backup event`() = runTest(testDispatcher) { - val fileName = "restore-keymap-db-version-too-big.json" + fun `restore with key map db version greater than allowed version, send incompatible backup event`() = + runTest(testDispatcher) { + val fileName = "restore-keymap-db-version-too-big.json" - val result = - backupManager.restore(copyFileToPrivateFolder(fileName), RestoreType.REPLACE) + val result = + backupManager.restore(copyFileToPrivateFolder(fileName), RestoreType.REPLACE) - assertThat(result, `is`(KMError.BackupVersionTooNew)) - verify(mockKeyMapRepository, never()).insert(anyVararg()) - } + assertThat(result, `is`(KMError.BackupVersionTooNew)) + verify(mockKeyMapRepository, never()).insert(anyVararg()) + } @Test - fun `restore with legacy fingerprint gesture map db version greater than allowed version, send incompatible backup event`() = runTest(testDispatcher) { - val fileName = "restore-legacy-fingerprint-map-version-too-big.json" + fun `restore with legacy fingerprint gesture map db version greater than allowed version, send incompatible backup event`() = + runTest(testDispatcher) { + val fileName = "restore-legacy-fingerprint-map-version-too-big.json" - val result = - backupManager.restore(copyFileToPrivateFolder(fileName), RestoreType.REPLACE) + val result = + backupManager.restore(copyFileToPrivateFolder(fileName), RestoreType.REPLACE) - assertThat(result, `is`(KMError.BackupVersionTooNew)) - } + assertThat(result, `is`(KMError.BackupVersionTooNew)) + } @Test fun `restore empty file, show empty json error message`() = runTest(testDispatcher) { @@ -595,46 +643,47 @@ class BackupManagerTest { } @Test - fun `backup key maps, return list of default key maps, keymap db version should be current database version`() = runTest(testDispatcher) { - // GIVEN - val backupDirUuid = "backup_uuid" + fun `backup key maps, return list of default key maps, keymap db version should be current database version`() = + runTest(testDispatcher) { + // GIVEN + val backupDirUuid = "backup_uuid" - whenever(mockUuidGenerator.random()).then { - backupDirUuid - } + whenever(mockUuidGenerator.random()).then { + backupDirUuid + } - val keyMapList = listOf(KeyMapEntity(0), KeyMapEntity(1)) + val keyMapList = listOf(KeyMapEntity(0), KeyMapEntity(1)) - whenever(mockKeyMapRepository.keyMapList).then { MutableStateFlow(State.Data(keyMapList)) } + whenever(mockKeyMapRepository.keyMapList).then { MutableStateFlow(State.Data(keyMapList)) } - val backupZip = File(temporaryFolder.root, "backup.zip") - backupZip.mkdirs() + val backupZip = File(temporaryFolder.root, "backup.zip") + backupZip.mkdirs() - // WHEN - val result = backupManager.backupKeyMaps(JavaFile(backupZip), keyMapList.map { it.uid }) + // WHEN + val result = backupManager.backupKeyMaps(JavaFile(backupZip), keyMapList.map { it.uid }) - // THEN - assertThat(result, `is`(Success(Unit))) + // THEN + assertThat(result, `is`(Success(Unit))) - // only 1 file has been backed up - assertThat(backupZip.listFiles()?.size, `is`(1)) + // only 1 file has been backed up + assertThat(backupZip.listFiles()?.size, `is`(1)) - val dataJson = File(backupZip, "data.json") - val json = dataJson.inputStream().bufferedReader().use { it.readText() } - val rootElement = parser.parse(json) + val dataJson = File(backupZip, "data.json") + val json = dataJson.inputStream().bufferedReader().use { it.readText() } + val rootElement = parser.parse(json) - // the key maps have been backed up - assertThat( - gson.toJson(rootElement["keymap_list"]), - `is`(gson.toJson(keyMapList)), - ) + // the key maps have been backed up + assertThat( + gson.toJson(rootElement["keymap_list"]), + `is`(gson.toJson(keyMapList)), + ) - // the database version has been backed up - assertThat( - rootElement["keymap_db_version"].asInt, - `is`(AppDatabase.DATABASE_VERSION), - ) - } + // the database version has been backed up + assertThat( + rootElement["keymap_db_version"].asInt, + `is`(AppDatabase.DATABASE_VERSION), + ) + } /** * @return a path to the copied file diff --git a/base/src/test/java/io/github/sds100/keymapper/base/ConfigKeyMapUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/ConfigKeyMapUseCaseTest.kt deleted file mode 100644 index 3d57377e3d..0000000000 --- a/base/src/test/java/io/github/sds100/keymapper/base/ConfigKeyMapUseCaseTest.kt +++ /dev/null @@ -1,480 +0,0 @@ -package io.github.sds100.keymapper.base - -import android.view.KeyEvent -import io.github.sds100.keymapper.base.actions.Action -import io.github.sds100.keymapper.base.actions.ActionData -import io.github.sds100.keymapper.base.constraints.Constraint -import io.github.sds100.keymapper.base.keymaps.ClickType -import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapUseCaseController -import io.github.sds100.keymapper.base.keymaps.KeyMap -import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType -import io.github.sds100.keymapper.base.trigger.AssistantTriggerKey -import io.github.sds100.keymapper.base.trigger.AssistantTriggerType -import io.github.sds100.keymapper.base.trigger.KeyEventDetectionSource -import io.github.sds100.keymapper.base.trigger.Trigger -import io.github.sds100.keymapper.base.trigger.TriggerKeyDevice -import io.github.sds100.keymapper.base.trigger.TriggerMode -import io.github.sds100.keymapper.base.utils.singleKeyTrigger -import io.github.sds100.keymapper.base.utils.triggerKey -import io.github.sds100.keymapper.common.utils.State -import io.github.sds100.keymapper.common.utils.dataOrNull -import io.github.sds100.keymapper.system.inputevents.InputEventUtils -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.runTest -import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers.contains -import org.hamcrest.Matchers.hasSize -import org.hamcrest.Matchers.instanceOf -import org.hamcrest.Matchers.`is` -import org.junit.Before -import org.junit.Test -import org.mockito.kotlin.mock - -@ExperimentalCoroutinesApi -class ConfigKeyMapUseCaseTest { - - private val testDispatcher = UnconfinedTestDispatcher() - private val testScope = TestScope(testDispatcher) - - private lateinit var useCase: ConfigKeyMapUseCaseController - - @Before - fun init() { - useCase = ConfigKeyMapUseCaseController( - coroutineScope = testScope, - devicesAdapter = mock(), - keyMapRepository = mock(), - preferenceRepository = mock(), - floatingLayoutRepository = mock(), - floatingButtonRepository = mock(), - serviceAdapter = mock(), - ) - } - - @Test - fun `Do not allow setting double press for parallel trigger with side key`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) - - useCase.addKeyCodeTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, - detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, - ) - useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) - - useCase.setTriggerDoublePress() - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) - assertThat(trigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) - } - - @Test - fun `Do not allow setting long press for parallel trigger with side key`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) - - useCase.addKeyCodeTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, - detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, - ) - useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) - - useCase.setTriggerLongPress() - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) - assertThat(trigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) - } - - @Test - fun `Do not allow setting double press for side key`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) - - useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) - - useCase.setTriggerDoublePress() - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Undefined)) - assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) - } - - @Test - fun `Do not allow setting long press for side key`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) - - useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) - - useCase.setTriggerLongPress() - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Undefined)) - assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) - } - - @Test - fun `Set click type to short press if side key added to double press volume button`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) - - useCase.addKeyCodeTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, - detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, - ) - - useCase.setTriggerDoublePress() - - useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) - assertThat(trigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) - } - - @Test - fun `Set click type to short press if fingerprint gestures added to double press volume button`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) - - useCase.addKeyCodeTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, - detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, - ) - - useCase.setTriggerDoublePress() - - useCase.addFingerprintGesture(FingerprintGestureType.SWIPE_UP) - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) - assertThat(trigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) - } - - @Test - fun `Set click type to short press if side key added to long press volume button`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) - - useCase.addKeyCodeTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, - detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, - ) - - useCase.setTriggerLongPress() - - useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) - assertThat(trigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) - } - - @Test - fun `Set click type to short press if fingerprint gestures added to long press volume button`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) - - useCase.addKeyCodeTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, - detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, - ) - - useCase.setTriggerLongPress() - - useCase.addFingerprintGesture(FingerprintGestureType.SWIPE_UP) - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - assertThat(trigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) - assertThat(trigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) - } - - @Test - fun `Enable hold down option for key event actions when the trigger is a DPAD button`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) - useCase.addKeyCodeTriggerKey( - KeyEvent.KEYCODE_DPAD_LEFT, - TriggerKeyDevice.Any, - KeyEventDetectionSource.INPUT_METHOD, - ) - - useCase.addAction(ActionData.InputKeyEvent(keyCode = KeyEvent.KEYCODE_W)) - - val actionList = useCase.keyMap.value.dataOrNull()!!.actionList - assertThat(actionList[0].holdDown, `is`(true)) - assertThat(actionList[0].repeat, `is`(false)) - } - - /** - * This ensures that it isn't possible to have two or more assistant triggers when the mode is parallel. - */ - @Test - fun `Remove device assistant trigger if setting mode to parallel and voice assistant already exists`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) - - useCase.addKeyCodeTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, - detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, - ) - useCase.addAssistantTriggerKey(AssistantTriggerType.VOICE) - useCase.addAssistantTriggerKey(AssistantTriggerType.DEVICE) - useCase.setParallelTriggerMode() - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.keys, hasSize(2)) - assertThat( - trigger.keys[0], - instanceOf(io.github.sds100.keymapper.base.trigger.KeyCodeTriggerKey::class.java), - ) - assertThat(trigger.keys[1], instanceOf(AssistantTriggerKey::class.java)) - } - - @Test - fun `Remove voice assistant trigger if setting mode to parallel and device assistant already exists`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) - - useCase.addKeyCodeTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, - detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, - ) - useCase.addAssistantTriggerKey(AssistantTriggerType.DEVICE) - useCase.addAssistantTriggerKey(AssistantTriggerType.VOICE) - useCase.setParallelTriggerMode() - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.keys, hasSize(2)) - assertThat( - trigger.keys[0], - instanceOf(io.github.sds100.keymapper.base.trigger.KeyCodeTriggerKey::class.java), - ) - assertThat(trigger.keys[1], instanceOf(AssistantTriggerKey::class.java)) - } - - @Test - fun `Set click type to short press when adding assistant key to multiple long press trigger keys`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) - - useCase.addKeyCodeTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, - detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, - ) - useCase.addKeyCodeTriggerKey( - KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Any, - detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, - ) - useCase.setTriggerLongPress() - - useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - } - - @Test - fun `Set click type to short press when adding assistant key to double press trigger key`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) - - useCase.addKeyCodeTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, - detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, - ) - useCase.setTriggerDoublePress() - useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - } - - @Test - fun `Set click type to short press when adding assistant key to long press trigger key`() = runTest(testDispatcher) { - useCase.keyMap.value = State.Data(KeyMap()) - - useCase.addKeyCodeTriggerKey( - KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, - detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, - ) - useCase.setTriggerLongPress() - useCase.addAssistantTriggerKey(AssistantTriggerType.ANY) - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - } - - @Test - fun `Do not allow long press for parallel trigger with assistant key`() = runTest(testDispatcher) { - val keyMap = KeyMap( - trigger = Trigger( - mode = TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS), - keys = listOf( - triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN), - AssistantTriggerKey( - type = AssistantTriggerType.ANY, - clickType = ClickType.SHORT_PRESS, - ), - ), - ), - ) - - useCase.keyMap.value = State.Data(keyMap) - useCase.setTriggerLongPress() - - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - assertThat(trigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) - } - - /** - * Issue #753. If a modifier key is used as a trigger then it the - * option to not override the default action must be chosen so that the modifier - * key can still be used normally. - */ - @Test - fun `when add modifier key trigger, enable do not remap option`() = runTest(testDispatcher) { - val modifierKeys = setOf( - KeyEvent.KEYCODE_SHIFT_LEFT, - KeyEvent.KEYCODE_SHIFT_RIGHT, - KeyEvent.KEYCODE_ALT_LEFT, - KeyEvent.KEYCODE_ALT_RIGHT, - KeyEvent.KEYCODE_CTRL_LEFT, - KeyEvent.KEYCODE_CTRL_RIGHT, - KeyEvent.KEYCODE_META_LEFT, - KeyEvent.KEYCODE_META_RIGHT, - KeyEvent.KEYCODE_SYM, - KeyEvent.KEYCODE_NUM, - KeyEvent.KEYCODE_FUNCTION, - ) - - for (modifierKeyCode in modifierKeys) { - // GIVEN - useCase.keyMap.value = State.Data(KeyMap()) - - // WHEN - useCase.addKeyCodeTriggerKey( - modifierKeyCode, - TriggerKeyDevice.Internal, - detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, - ) - - // THEN - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - - assertThat(trigger.keys[0].consumeEvent, `is`(false)) - } - } - - /** - * Issue #753. - */ - @Test - fun `when add non-modifier key trigger, do ont enable do not remap option`() = runTest(testDispatcher) { - // GIVEN - useCase.keyMap.value = State.Data(KeyMap()) - - // WHEN - useCase.addKeyCodeTriggerKey( - KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, - detectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, - ) - - // THEN - val trigger = useCase.keyMap.value.dataOrNull()!!.trigger - - assertThat(trigger.keys[0].consumeEvent, `is`(true)) - } - - /** - * Issue #852. Add a phone ringing constraint when you add an action - * to answer a phone call. - */ - @Test - fun `when add answer phone call action, then add phone ringing constraint`() = runTest(testDispatcher) { - // GIVEN - useCase.keyMap.value = State.Data(KeyMap()) - val action = ActionData.AnswerCall - - // WHEN - useCase.addAction(action) - - // THEN - val keyMap = useCase.keyMap.value.dataOrNull()!! - assertThat( - keyMap.constraintState.constraints, - contains(instanceOf(Constraint.PhoneRinging::class.java)), - ) - } - - /** - * Issue #852. Add a in phone call constraint when you add an action - * to end a phone call. - */ - @Test - fun `when add end phone call action, then add in phone call constraint`() = runTest(testDispatcher) { - // GIVEN - useCase.keyMap.value = State.Data(KeyMap()) - val action = ActionData.EndCall - - // WHEN - useCase.addAction(action) - - // THEN - val keyMap = useCase.keyMap.value.dataOrNull()!! - assertThat( - keyMap.constraintState.constraints, - contains(instanceOf(Constraint.InPhoneCall::class.java)), - ) - } - - /** - * issue #593 - */ - @Test - fun `key map with hold down action, load key map, hold down flag shouldn't disappear`() = runTest(testDispatcher) { - // given - val action = Action( - data = ActionData.TapScreen(100, 100, null), - holdDown = true, - ) - - val keyMap = KeyMap( - 0, - trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_0)), - actionList = listOf(action), - ) - - // when - useCase.keyMap.value = State.Data(keyMap) - - // then - assertThat(useCase.keyMap.value.dataOrNull()!!.actionList, `is`(listOf(action))) - } - - @Test - fun `add modifier key event action, enable hold down option and disable repeat option`() = runTest(testDispatcher) { - InputEventUtils.MODIFIER_KEYCODES.forEach { keyCode -> - useCase.keyMap.value = State.Data(KeyMap()) - - useCase.addAction(ActionData.InputKeyEvent(keyCode)) - - useCase.keyMap.value.dataOrNull()!!.actionList - .single() - .let { - assertThat(it.holdDown, `is`(true)) - assertThat(it.repeat, `is`(false)) - } - } - } -} diff --git a/base/src/test/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCaseTest.kt new file mode 100644 index 0000000000..156a03ca19 --- /dev/null +++ b/base/src/test/java/io/github/sds100/keymapper/base/actions/ConfigActionsUseCaseTest.kt @@ -0,0 +1,153 @@ +package io.github.sds100.keymapper.base.actions + +import android.view.KeyEvent +import io.github.sds100.keymapper.base.constraints.ConfigConstraintsUseCase +import io.github.sds100.keymapper.base.constraints.Constraint +import io.github.sds100.keymapper.base.keymaps.ClickType +import io.github.sds100.keymapper.base.keymaps.ConfigKeyMapStateImpl +import io.github.sds100.keymapper.base.keymaps.KeyMap +import io.github.sds100.keymapper.base.trigger.KeyEventTriggerDevice +import io.github.sds100.keymapper.base.trigger.KeyEventTriggerKey +import io.github.sds100.keymapper.base.utils.singleKeyTrigger +import io.github.sds100.keymapper.base.utils.triggerKey +import io.github.sds100.keymapper.common.utils.dataOrNull +import io.github.sds100.keymapper.system.inputevents.KeyEventUtils +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.`is` +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +class ConfigActionsUseCaseTest { + + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + + private lateinit var useCase: ConfigActionsUseCaseImpl + private lateinit var configKeyMapState: ConfigKeyMapStateImpl + private lateinit var mockConfigConstraintsUseCase: ConfigConstraintsUseCase + + @Before + fun before() { + configKeyMapState = ConfigKeyMapStateImpl( + testScope, + keyMapRepository = mock(), + floatingButtonRepository = mock(), + ) + + mockConfigConstraintsUseCase = mock() + + useCase = ConfigActionsUseCaseImpl( + state = configKeyMapState, + preferenceRepository = mock(), + configConstraints = mockConfigConstraintsUseCase, + defaultKeyMapOptionsUseCase = mock(), + ) + } + + @Test + fun `Enable hold down option for key event actions when the trigger is a DPAD button`() = + runTest(testDispatcher) { + configKeyMapState.setKeyMap( + KeyMap( + trigger = singleKeyTrigger( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_DPAD_LEFT, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + requiresIme = true, + ), + ), + ), + ) + + useCase.addAction(ActionData.InputKeyEvent(keyCode = KeyEvent.KEYCODE_W)) + + val actionList = useCase.keyMap.value.dataOrNull()!!.actionList + assertThat(actionList[0].holdDown, `is`(true)) + assertThat(actionList[0].repeat, `is`(false)) + } + + /** + * Issue #852. Add a phone ringing constraint when you add an action + * to answer a phone call. + */ + @Test + fun `when add answer phone call action, then add phone ringing constraint`() = + runTest(testDispatcher) { + // GIVEN + configKeyMapState.setKeyMap(KeyMap()) + val action = ActionData.AnswerCall + + // WHEN + useCase.addAction(action) + + // THEN + verify(mockConfigConstraintsUseCase).addConstraint(any()) + } + + /** + * Issue #852. Add a in phone call constraint when you add an action + * to end a phone call. + */ + @Test + fun `when add end phone call action, then add in phone call constraint`() = + runTest(testDispatcher) { + // GIVEN + configKeyMapState.setKeyMap(KeyMap()) + val action = ActionData.EndCall + + // WHEN + useCase.addAction(action) + + // THEN + verify(mockConfigConstraintsUseCase).addConstraint(any()) + } + + /** + * issue #593 + */ + @Test + fun `key map with hold down action, load key map, hold down flag shouldn't disappear`() = + runTest(testDispatcher) { + // given + val action = Action( + data = ActionData.TapScreen(100, 100, null), + holdDown = true, + ) + + val keyMap = KeyMap( + 0, + trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_0)), + actionList = listOf(action), + ) + + // when + configKeyMapState.setKeyMap(keyMap) + + // then + assertThat(useCase.keyMap.value.dataOrNull()!!.actionList, `is`(listOf(action))) + } + + @Test + fun `add modifier key event action, enable hold down option and disable repeat option`() = + runTest(testDispatcher) { + KeyEventUtils.MODIFIER_KEYCODES.forEach { keyCode -> + configKeyMapState.setKeyMap(KeyMap()) + + useCase.addAction(ActionData.InputKeyEvent(keyCode)) + + useCase.keyMap.value.dataOrNull()!!.actionList + .single() + .let { + assertThat(it.holdDown, `is`(true)) + assertThat(it.repeat, `is`(false)) + } + } + } +} diff --git a/base/src/test/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCaseTest.kt index b01a5f3724..139b645fd4 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCaseTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/actions/GetActionErrorUseCaseTest.kt @@ -7,11 +7,8 @@ import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.system.inputmethod.ImeInfo import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.system.permissions.PermissionAdapter -import io.github.sds100.keymapper.system.shizuku.ShizukuAdapter import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.update import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest @@ -52,13 +49,11 @@ class GetActionErrorUseCaseTest { private lateinit var useCase: GetActionErrorUseCaseImpl - private lateinit var mockShizukuAdapter: ShizukuAdapter private lateinit var fakeInputMethodAdapter: FakeInputMethodAdapter private lateinit var mockPermissionAdapter: PermissionAdapter @Before fun init() { - mockShizukuAdapter = mock() fakeInputMethodAdapter = FakeInputMethodAdapter() mockPermissionAdapter = mock() @@ -69,14 +64,14 @@ class GetActionErrorUseCaseTest { systemFeatureAdapter = mock(), cameraAdapter = mock(), soundsManager = mock(), - shizukuAdapter = mockShizukuAdapter, ringtoneAdapter = mock(), buildConfigProvider = TestBuildConfigProvider(), + systemBridgeConnectionManager = mock(), + preferenceRepository = mock(), ) } private fun setupKeyEventActionTest(chosenIme: ImeInfo) { - whenever(mockShizukuAdapter.isInstalled).then { MutableStateFlow(false) } whenever(mockPermissionAdapter.isGranted(Permission.WRITE_SECURE_SETTINGS)).then { true } fakeInputMethodAdapter.chosenIme.value = chosenIme fakeInputMethodAdapter.inputMethods.value = listOf(GBOARD_IME_INFO, GUI_KEYBOARD_IME_INFO) @@ -220,42 +215,4 @@ class GetActionErrorUseCaseTest { assertThat(errors[1], nullValue()) assertThat(errors[2], `is`(KMError.NoCompatibleImeChosen)) } - - /** - * #776 - */ - @Test - fun `don't show Shizuku errors if a compatible ime is selected`() = testScope.runTest { - // GIVEN - whenever(mockShizukuAdapter.isInstalled).then { MutableStateFlow(true) } - fakeInputMethodAdapter.chosenIme.update { GUI_KEYBOARD_IME_INFO } - - val action = ActionData.InputKeyEvent(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN) - - // WHEN - val errorMap = useCase.actionErrorSnapshot.first().getErrors(listOf(action)) - - // THEN - assertThat(errorMap[action], nullValue()) - } - - /** - * #776 - */ - @Test - fun `show Shizuku errors if a compatible ime is not selected and Shizuku is installed`() = - testScope.runTest { - // GIVEN - whenever(mockShizukuAdapter.isInstalled).then { MutableStateFlow(true) } - whenever(mockShizukuAdapter.isStarted).then { MutableStateFlow(false) } - fakeInputMethodAdapter.chosenIme.update { GBOARD_IME_INFO } - - val action = ActionData.InputKeyEvent(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN) - - // WHEN - val errorMap = useCase.actionErrorSnapshot.first().getErrors(listOf(action)) - - // THEN - assertThat(errorMap[action], `is`(KMError.ShizukuNotStarted)) - } } diff --git a/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt index 5dee3c53b7..8c67f904d1 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/actions/PerformActionsUseCaseTest.kt @@ -2,18 +2,18 @@ package io.github.sds100.keymapper.base.actions import android.view.InputDevice import android.view.KeyEvent +import io.github.sds100.keymapper.base.input.InjectKeyEventModel +import io.github.sds100.keymapper.base.input.InputEventHub import io.github.sds100.keymapper.base.system.accessibility.IAccessibilityService import io.github.sds100.keymapper.base.system.devices.FakeDevicesAdapter -import io.github.sds100.keymapper.base.system.inputmethod.ImeInputEventInjector -import io.github.sds100.keymapper.common.utils.InputEventType +import io.github.sds100.keymapper.common.utils.InputDeviceInfo +import io.github.sds100.keymapper.common.utils.InputEventAction import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.State -import io.github.sds100.keymapper.system.devices.InputDeviceInfo -import io.github.sds100.keymapper.system.inputmethod.InputKeyModel +import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.system.popup.ToastAdapter import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Before @@ -34,33 +34,31 @@ import org.mockito.kotlin.whenever class PerformActionsUseCaseTest { private val testDispatcher = UnconfinedTestDispatcher() - private val testScope = TestScope(testDispatcher) private lateinit var useCase: PerformActionsUseCaseImpl - private lateinit var mockImeInputEventInjector: ImeInputEventInjector private lateinit var fakeDevicesAdapter: FakeDevicesAdapter private lateinit var mockAccessibilityService: IAccessibilityService private lateinit var mockToastAdapter: ToastAdapter + private lateinit var mockInputEventHub: InputEventHub @Before fun init() { - mockImeInputEventInjector = mock() fakeDevicesAdapter = FakeDevicesAdapter() mockAccessibilityService = mock() mockToastAdapter = mock() + mockInputEventHub = mock { + on { runBlocking { injectKeyEvent(any()) } }.then { Success(Unit) } + } useCase = PerformActionsUseCaseImpl( - testScope, service = mockAccessibilityService, inputMethodAdapter = mock(), fileAdapter = mock(), - suAdapter = mock { - on { isGranted }.then { MutableStateFlow(false) } - }, + suAdapter = mock {}, shell = mock(), intentAdapter = mock(), getActionErrorUseCase = mock(), - keyMapperImeMessenger = mockImeInputEventInjector, + keyMapperImeMessenger = mock(), packageManagerAdapter = mock(), appShortcutAdapter = mock(), toastAdapter = mockToastAdapter, @@ -79,9 +77,10 @@ class PerformActionsUseCaseTest { resourceProvider = mock(), settingsRepository = mock(), soundsManager = mock(), - permissionAdapter = mock(), notificationReceiverAdapter = mock(), ringtoneAdapter = mock(), + inputEventHub = mockInputEventHub, + systemBridgeConnectionManager = mock(), ) } @@ -89,238 +88,267 @@ class PerformActionsUseCaseTest { * issue #771 */ @Test - fun `dont show accessibility service not found error for open menu action`() = runTest(testDispatcher) { - // GIVEN - val action = ActionData.OpenMenu - - whenever( - mockAccessibilityService.performActionOnNode( - any(), - any(), - ), - ).doReturn(KMError.FailedToFindAccessibilityNode) - - // WHEN - useCase.perform(action) - - // THEN - verify(mockToastAdapter, never()).show(anyOrNull()) - } + fun `dont show accessibility service not found error for open menu action`() = + runTest(testDispatcher) { + // GIVEN + val action = ActionData.OpenMenu + + whenever( + mockAccessibilityService.performActionOnNode( + any(), + any(), + ), + ).doReturn(KMError.FailedToFindAccessibilityNode) + + // WHEN + useCase.perform(action) + + // THEN + verify(mockToastAdapter, never()).show(anyOrNull()) + } /** * issue #772 */ @Test - fun `set the device id of key event actions to a connected game controller if is a game pad key code`() = runTest(testDispatcher) { - // GIVEN - val fakeGamePad = InputDeviceInfo( - descriptor = "game_pad", - name = "Game pad", - id = 1, - isExternal = true, - isGameController = true, - ) - - fakeDevicesAdapter.connectedInputDevices.value = State.Data(listOf(fakeGamePad)) - - val action = ActionData.InputKeyEvent( - keyCode = KeyEvent.KEYCODE_BUTTON_A, - device = null, - ) + fun `set the device id of key event actions to a connected game controller if is a game pad key code`() = + runTest(testDispatcher) { + // GIVEN + val fakeGamePad = InputDeviceInfo( + descriptor = "game_pad", + name = "Game pad", + id = 1, + isExternal = true, + isGameController = true, + sources = InputDevice.SOURCE_GAMEPAD, + ) + + fakeDevicesAdapter.connectedInputDevices.value = State.Data(listOf(fakeGamePad)) + + val action = ActionData.InputKeyEvent( + keyCode = KeyEvent.KEYCODE_BUTTON_A, + device = null, + ) + + // WHEN + useCase.perform(action) + + // THEN + val expectedDownEvent = InjectKeyEventModel( + keyCode = KeyEvent.KEYCODE_BUTTON_A, + action = KeyEvent.ACTION_DOWN, + metaState = 0, + deviceId = fakeGamePad.id, + scanCode = 0, + repeatCount = 0, + source = InputDevice.SOURCE_GAMEPAD, + ) - // WHEN - useCase.perform(action) - - // THEN - val expectedInputKeyModel = InputKeyModel( - keyCode = KeyEvent.KEYCODE_BUTTON_A, - inputType = InputEventType.DOWN_UP, - metaState = 0, - deviceId = fakeGamePad.id, - scanCode = 0, - repeat = 0, - source = InputDevice.SOURCE_GAMEPAD, - ) + val expectedUpEvent = expectedDownEvent.copy(action = KeyEvent.ACTION_UP) - verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedInputKeyModel) - } + verify(mockInputEventHub, times(1)).injectKeyEvent(expectedDownEvent) + verify(mockInputEventHub, times(1)).injectKeyEvent(expectedUpEvent) + } /** * issue #772 */ @Test - fun `don't set the device id of key event actions to a connected game controller if there are no connected game controllers`() = runTest(testDispatcher) { - // GIVEN - fakeDevicesAdapter.connectedInputDevices.value = State.Data(emptyList()) + fun `don't set the device id of key event actions to a connected game controller if there are no connected game controllers`() = + runTest(testDispatcher) { + // GIVEN + fakeDevicesAdapter.connectedInputDevices.value = State.Data(emptyList()) - val action = ActionData.InputKeyEvent( - keyCode = KeyEvent.KEYCODE_BUTTON_A, - device = null, - ) + val action = ActionData.InputKeyEvent( + keyCode = KeyEvent.KEYCODE_BUTTON_A, + device = null, + ) - // WHEN - useCase.perform(action) - - // THEN - val expectedInputKeyModel = InputKeyModel( - keyCode = KeyEvent.KEYCODE_BUTTON_A, - inputType = InputEventType.DOWN_UP, - metaState = 0, - deviceId = 0, - scanCode = 0, - repeat = 0, - source = InputDevice.SOURCE_GAMEPAD, - ) + // WHEN + useCase.perform(action) - verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedInputKeyModel) - } + // THEN + val expectedDownEvent = InjectKeyEventModel( + + keyCode = KeyEvent.KEYCODE_BUTTON_A, + action = KeyEvent.ACTION_DOWN, + metaState = 0, + deviceId = 0, + scanCode = 0, + repeatCount = 0, + source = InputDevice.SOURCE_GAMEPAD, + ) + + val expectedUpEvent = expectedDownEvent.copy(action = KeyEvent.ACTION_UP) + + verify(mockInputEventHub, times(1)).injectKeyEvent(expectedDownEvent) + verify(mockInputEventHub, times(1)).injectKeyEvent(expectedUpEvent) + } /** * issue #772 */ @Test - fun `don't set the device id of key event actions to a connected game controller if the action has a custom device set`() = runTest(testDispatcher) { - // GIVEN - val fakeGamePad = InputDeviceInfo( - descriptor = "game_pad", - name = "Game pad", - id = 1, - isExternal = true, - isGameController = true, - ) + fun `don't set the device id of key event actions to a connected game controller if the action has a custom device set`() = + runTest(testDispatcher) { + // GIVEN + val fakeGamePad = InputDeviceInfo( + descriptor = "game_pad", + name = "Game pad", + id = 1, + isExternal = true, + isGameController = true, + sources = InputDevice.SOURCE_GAMEPAD, + ) + + val fakeKeyboard = InputDeviceInfo( + descriptor = "keyboard", + name = "Keyboard", + id = 2, + isExternal = true, + isGameController = false, + sources = InputDevice.SOURCE_GAMEPAD, + ) + + fakeDevicesAdapter.connectedInputDevices.value = + State.Data(listOf(fakeGamePad, fakeKeyboard)) + + val action = ActionData.InputKeyEvent( + keyCode = KeyEvent.KEYCODE_BUTTON_A, + device = ActionData.InputKeyEvent.Device( + descriptor = "keyboard", + name = "Keyboard", + ), + ) - val fakeKeyboard = InputDeviceInfo( - descriptor = "keyboard", - name = "Keyboard", - id = 2, - isExternal = true, - isGameController = false, - ) + // WHEN + useCase.perform(action) - fakeDevicesAdapter.connectedInputDevices.value = - State.Data(listOf(fakeGamePad, fakeKeyboard)) + // THEN + val expectedDownEvent = InjectKeyEventModel( - val action = ActionData.InputKeyEvent( - keyCode = KeyEvent.KEYCODE_BUTTON_A, - device = ActionData.InputKeyEvent.Device( - descriptor = "keyboard", - name = "Keyboard", - ), - ) + keyCode = KeyEvent.KEYCODE_BUTTON_A, + action = KeyEvent.ACTION_DOWN, + metaState = 0, + deviceId = fakeKeyboard.id, + scanCode = 0, + repeatCount = 0, + source = InputDevice.SOURCE_GAMEPAD, + ) - // WHEN - useCase.perform(action) - - // THEN - val expectedInputKeyModel = InputKeyModel( - keyCode = KeyEvent.KEYCODE_BUTTON_A, - inputType = InputEventType.DOWN_UP, - metaState = 0, - deviceId = fakeKeyboard.id, - scanCode = 0, - repeat = 0, - source = InputDevice.SOURCE_GAMEPAD, - ) + val expectedUpEvent = expectedDownEvent.copy(action = KeyEvent.ACTION_UP) - verify(mockImeInputEventInjector, times(1)).inputKeyEvent(expectedInputKeyModel) - } + verify(mockInputEventHub, times(1)).injectKeyEvent(expectedDownEvent) + verify(mockInputEventHub, times(1)).injectKeyEvent(expectedUpEvent) + } /** * issue #637 */ @Test - fun `perform key event action with device name and multiple devices connected with same descriptor and none support the key code, ensure action is still performed`() = runTest(testDispatcher) { - // GIVEN - val descriptor = "fake_device_descriptor" - - val action = ActionData.InputKeyEvent( - keyCode = 1, - metaState = 0, - useShell = false, - device = ActionData.InputKeyEvent.Device( - descriptor = descriptor, - name = "fake_name_2", - ), - ) - - fakeDevicesAdapter.connectedInputDevices.value = State.Data( - listOf( - InputDeviceInfo( - descriptor = descriptor, - name = "fake_name_1", - id = 10, - isExternal = true, - isGameController = false, - ), + fun `perform key event action with device name and multiple devices connected with same descriptor and none support the key code, ensure action is still performed`() = + runTest(testDispatcher) { + // GIVEN + val descriptor = "fake_device_descriptor" - InputDeviceInfo( + val action = ActionData.InputKeyEvent( + keyCode = 1, + metaState = 0, + device = ActionData.InputKeyEvent.Device( descriptor = descriptor, name = "fake_name_2", - id = 11, - isExternal = true, - isGameController = false, ), - ), - ) + ) + + fakeDevicesAdapter.connectedInputDevices.value = State.Data( + listOf( + InputDeviceInfo( + descriptor = descriptor, + name = "fake_name_1", + id = 10, + isExternal = true, + isGameController = false, + sources = InputDevice.SOURCE_GAMEPAD, + ), + + InputDeviceInfo( + descriptor = descriptor, + name = "fake_name_2", + id = 11, + isExternal = true, + isGameController = false, + sources = InputDevice.SOURCE_GAMEPAD, + ), + ), + ) + + // none of the devices support the key code + fakeDevicesAdapter.deviceHasKey = { id, keyCode -> false } - // none of the devices support the key code - fakeDevicesAdapter.deviceHasKey = { id, keyCode -> false } + // WHEN + useCase.perform(action, inputEventAction = InputEventAction.DOWN_UP, keyMetaState = 0) - // WHEN - useCase.perform(action, inputEventType = InputEventType.DOWN_UP, keyMetaState = 0) + // THEN + val expectedDownEvent = InjectKeyEventModel( - // THEN - verify(mockImeInputEventInjector, times(1)).inputKeyEvent( - InputKeyModel( keyCode = 1, - inputType = InputEventType.DOWN_UP, + action = KeyEvent.ACTION_DOWN, metaState = 0, deviceId = 11, scanCode = 0, - repeat = 0, + repeatCount = 0, source = InputDevice.SOURCE_KEYBOARD, - ), - ) - } + ) + + val expectedUpEvent = expectedDownEvent.copy(action = KeyEvent.ACTION_UP) + + verify(mockInputEventHub, times(1)).injectKeyEvent(expectedDownEvent) + verify(mockInputEventHub, times(1)).injectKeyEvent(expectedUpEvent) + } @Test - fun `perform key event action with no device name, ensure action is still performed with correct device id`() = runTest(testDispatcher) { - // GIVEN - val descriptor = "fake_device_descriptor" - - val action = ActionData.InputKeyEvent( - keyCode = 1, - metaState = 0, - useShell = false, - device = ActionData.InputKeyEvent.Device(descriptor = descriptor, name = ""), - ) + fun `perform key event action with no device name, ensure action is still performed with correct device id`() = + runTest(testDispatcher) { + // GIVEN + val descriptor = "fake_device_descriptor" - fakeDevicesAdapter.connectedInputDevices.value = State.Data( - listOf( - InputDeviceInfo( - descriptor = descriptor, - name = "fake_name", - id = 10, - isExternal = true, - isGameController = false, + val action = ActionData.InputKeyEvent( + keyCode = 1, + metaState = 0, + device = ActionData.InputKeyEvent.Device(descriptor = descriptor, name = ""), + ) + + fakeDevicesAdapter.connectedInputDevices.value = State.Data( + listOf( + InputDeviceInfo( + descriptor = descriptor, + name = "fake_name", + id = 10, + isExternal = true, + isGameController = false, + sources = InputDevice.SOURCE_GAMEPAD, + ), ), - ), - ) + ) + + // WHEN + useCase.perform(action, inputEventAction = InputEventAction.DOWN_UP, keyMetaState = 0) - // WHEN - useCase.perform(action, inputEventType = InputEventType.DOWN_UP, keyMetaState = 0) + // THEN + val expectedDownEvent = InjectKeyEventModel( - // THEN - verify(mockImeInputEventInjector, times(1)).inputKeyEvent( - InputKeyModel( keyCode = 1, - inputType = InputEventType.DOWN_UP, + action = KeyEvent.ACTION_DOWN, metaState = 0, deviceId = 10, scanCode = 0, - repeat = 0, + repeatCount = 0, source = InputDevice.SOURCE_KEYBOARD, - ), - ) - } + ) + + val expectedUpEvent = expectedDownEvent.copy(action = KeyEvent.ACTION_UP) + + verify(mockInputEventHub, times(1)).injectKeyEvent(expectedDownEvent) + verify(mockInputEventHub, times(1)).injectKeyEvent(expectedUpEvent) + } } diff --git a/base/src/test/java/io/github/sds100/keymapper/base/actions/keyevents/ConfigKeyServiceEventActionViewModelTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/actions/keyevents/ConfigKeyServiceEventActionViewModelTest.kt index 03b1da73f4..06c15a8e01 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/actions/keyevents/ConfigKeyServiceEventActionViewModelTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/actions/keyevents/ConfigKeyServiceEventActionViewModelTest.kt @@ -1,9 +1,10 @@ package io.github.sds100.keymapper.base.actions.keyevents +import android.view.InputDevice import androidx.arch.core.executor.testing.InstantTaskExecutorRule import io.github.sds100.keymapper.base.actions.keyevent.ConfigKeyEventActionViewModel import io.github.sds100.keymapper.base.actions.keyevent.ConfigKeyEventUseCase -import io.github.sds100.keymapper.system.devices.InputDeviceInfo +import io.github.sds100.keymapper.common.utils.InputDeviceInfo import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow @@ -71,6 +72,7 @@ class ConfigKeyServiceEventActionViewModelTest { id = 0, isExternal = false, isGameController = false, + sources = InputDevice.SOURCE_KEYBOARD, ) val fakeDevice2 = InputDeviceInfo( @@ -79,6 +81,7 @@ class ConfigKeyServiceEventActionViewModelTest { id = 1, isExternal = false, isGameController = false, + sources = InputDevice.SOURCE_KEYBOARD, ) // WHEN diff --git a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DpadMotionEventTrackerTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DpadMotionEventTrackerTest.kt index 23ce84f14b..0cf0ab5c52 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DpadMotionEventTrackerTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/DpadMotionEventTrackerTest.kt @@ -2,10 +2,10 @@ package io.github.sds100.keymapper.base.keymaps import android.view.InputDevice import android.view.KeyEvent -import io.github.sds100.keymapper.base.keymaps.detection.DpadMotionEventTracker -import io.github.sds100.keymapper.system.devices.InputDeviceInfo -import io.github.sds100.keymapper.system.inputevents.MyKeyEvent -import io.github.sds100.keymapper.system.inputevents.MyMotionEvent +import io.github.sds100.keymapper.base.detection.DpadMotionEventTracker +import io.github.sds100.keymapper.common.utils.InputDeviceInfo +import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent +import io.github.sds100.keymapper.system.inputevents.KMKeyEvent import junitparams.JUnitParamsRunner import kotlinx.coroutines.ExperimentalCoroutinesApi import org.hamcrest.MatcherAssert.assertThat @@ -27,6 +27,7 @@ class DpadMotionEventTrackerTest { name = "Controller 1", isExternal = true, isGameController = true, + sources = InputDevice.SOURCE_GAMEPAD, ) private val CONTROLLER_2_DEVICE = InputDeviceInfo( @@ -35,6 +36,7 @@ class DpadMotionEventTrackerTest { name = "Controller 2", isExternal = true, isGameController = true, + sources = InputDevice.SOURCE_GAMEPAD, ) } @@ -60,7 +62,7 @@ class DpadMotionEventTrackerTest { assertThat( keyEvents, hasItem( - MyKeyEvent( + KMKeyEvent( KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.ACTION_UP, metaState = 0, @@ -68,13 +70,14 @@ class DpadMotionEventTrackerTest { device = CONTROLLER_1_DEVICE, repeatCount = 0, source = InputDevice.SOURCE_DPAD, + eventTime = motionEvent.eventTime, ), ), ) assertThat( keyEvents, hasItem( - MyKeyEvent( + KMKeyEvent( KeyEvent.KEYCODE_DPAD_UP, KeyEvent.ACTION_UP, metaState = 0, @@ -82,6 +85,7 @@ class DpadMotionEventTrackerTest { device = CONTROLLER_1_DEVICE, repeatCount = 0, source = InputDevice.SOURCE_DPAD, + eventTime = motionEvent.eventTime, ), ), ) @@ -257,19 +261,18 @@ class DpadMotionEventTrackerTest { axisHatX: Float = 0.0f, axisHatY: Float = 0.0f, device: InputDeviceInfo = CONTROLLER_1_DEVICE, - isDpad: Boolean = true, - ): MyMotionEvent { - return MyMotionEvent( + ): KMGamePadEvent { + return KMGamePadEvent( metaState = 0, device = device, axisHatX = axisHatX, axisHatY = axisHatY, - isDpad = isDpad, + eventTime = System.currentTimeMillis(), ) } - private fun createDownKeyEvent(keyCode: Int, device: InputDeviceInfo): MyKeyEvent { - return MyKeyEvent( + private fun createDownKeyEvent(keyCode: Int, device: InputDeviceInfo): KMKeyEvent { + return KMKeyEvent( keyCode = keyCode, action = KeyEvent.ACTION_DOWN, metaState = 0, @@ -277,11 +280,12 @@ class DpadMotionEventTrackerTest { device = device, repeatCount = 0, source = 0, + eventTime = System.currentTimeMillis(), ) } - private fun createUpKeyEvent(keyCode: Int, device: InputDeviceInfo): MyKeyEvent { - return MyKeyEvent( + private fun createUpKeyEvent(keyCode: Int, device: InputDeviceInfo): KMKeyEvent { + return KMKeyEvent( keyCode = keyCode, action = KeyEvent.ACTION_UP, metaState = 0, @@ -289,6 +293,8 @@ class DpadMotionEventTrackerTest { device = device, repeatCount = 0, source = 0, + eventTime = System.currentTimeMillis(), + ) } } diff --git a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapControllerTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt similarity index 79% rename from base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapControllerTest.kt rename to base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt index 4224d0c656..d34f57c237 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapControllerTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/KeyMapAlgorithmTest.kt @@ -1,5 +1,6 @@ package io.github.sds100.keymapper.base.keymaps +import android.view.InputDevice import android.view.KeyEvent import androidx.arch.core.executor.testing.InstantTaskExecutorRule import io.github.sds100.keymapper.base.actions.Action @@ -12,37 +13,39 @@ import io.github.sds100.keymapper.base.constraints.ConstraintMode import io.github.sds100.keymapper.base.constraints.ConstraintSnapshot import io.github.sds100.keymapper.base.constraints.ConstraintState import io.github.sds100.keymapper.base.constraints.DetectConstraintsUseCase -import io.github.sds100.keymapper.base.keymaps.detection.DetectKeyMapModel -import io.github.sds100.keymapper.base.keymaps.detection.DetectKeyMapsUseCase -import io.github.sds100.keymapper.base.keymaps.detection.KeyMapController +import io.github.sds100.keymapper.base.detection.DetectKeyMapModel +import io.github.sds100.keymapper.base.detection.DetectKeyMapsUseCase +import io.github.sds100.keymapper.base.detection.KeyMapAlgorithm import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType +import io.github.sds100.keymapper.base.trigger.EvdevTriggerKey import io.github.sds100.keymapper.base.trigger.FingerprintTriggerKey -import io.github.sds100.keymapper.base.trigger.KeyCodeTriggerKey -import io.github.sds100.keymapper.base.trigger.KeyEventDetectionSource +import io.github.sds100.keymapper.base.trigger.KeyEventTriggerDevice +import io.github.sds100.keymapper.base.trigger.KeyEventTriggerKey import io.github.sds100.keymapper.base.trigger.Trigger import io.github.sds100.keymapper.base.trigger.TriggerKey -import io.github.sds100.keymapper.base.trigger.TriggerKeyDevice import io.github.sds100.keymapper.base.trigger.TriggerMode import io.github.sds100.keymapper.base.utils.TestConstraintSnapshot import io.github.sds100.keymapper.base.utils.parallelTrigger import io.github.sds100.keymapper.base.utils.sequenceTrigger import io.github.sds100.keymapper.base.utils.singleKeyTrigger import io.github.sds100.keymapper.base.utils.triggerKey -import io.github.sds100.keymapper.common.utils.InputEventType +import io.github.sds100.keymapper.common.models.EvdevDeviceHandle +import io.github.sds100.keymapper.common.models.EvdevDeviceInfo +import io.github.sds100.keymapper.common.utils.InputDeviceInfo +import io.github.sds100.keymapper.common.utils.InputEventAction import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.withFlag import io.github.sds100.keymapper.system.camera.CameraLens -import io.github.sds100.keymapper.system.devices.InputDeviceInfo -import io.github.sds100.keymapper.system.inputevents.MyKeyEvent -import io.github.sds100.keymapper.system.inputevents.MyMotionEvent -import io.github.sds100.keymapper.system.inputmethod.ImeInfo +import io.github.sds100.keymapper.system.inputevents.KMEvdevEvent +import io.github.sds100.keymapper.system.inputevents.KMGamePadEvent +import io.github.sds100.keymapper.system.inputevents.KMKeyEvent +import io.github.sds100.keymapper.system.inputevents.Scancode import junitparams.JUnitParamsRunner import junitparams.Parameters import junitparams.naming.TestCaseName import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.advanceTimeBy @@ -51,11 +54,14 @@ import kotlinx.coroutines.test.currentTime import kotlinx.coroutines.test.runTest import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.`is` +import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.eq +import org.mockito.MockedStatic +import org.mockito.Mockito.mockStatic import org.mockito.kotlin.any import org.mockito.kotlin.atLeast import org.mockito.kotlin.doAnswer @@ -69,33 +75,56 @@ import org.mockito.kotlin.whenever @ExperimentalCoroutinesApi @RunWith(JUnitParamsRunner::class) -class KeyMapControllerTest { +class KeyMapAlgorithmTest { companion object { private const val FAKE_KEYBOARD_DEVICE_ID = 123 private const val FAKE_KEYBOARD_DESCRIPTOR = "fake_keyboard" - private val FAKE_KEYBOARD_TRIGGER_KEY_DEVICE = TriggerKeyDevice.External( + private val FAKE_KEYBOARD_TRIGGER_KEY_DEVICE = KeyEventTriggerDevice.External( descriptor = FAKE_KEYBOARD_DESCRIPTOR, name = "Fake Keyboard", ) + private val FAKE_INTERNAL_DEVICE = InputDeviceInfo( + descriptor = "volume_keys", + name = "Volume keys", + id = 0, + isExternal = false, + isGameController = false, + sources = InputDevice.SOURCE_UNKNOWN, + ) private const val FAKE_HEADPHONE_DESCRIPTOR = "fake_headphone" - private val FAKE_HEADPHONE_TRIGGER_KEY_DEVICE = TriggerKeyDevice.External( + private val FAKE_HEADPHONE_TRIGGER_KEY_DEVICE = KeyEventTriggerDevice.External( descriptor = FAKE_HEADPHONE_DESCRIPTOR, name = "Fake Headphones", ) private const val FAKE_CONTROLLER_DESCRIPTOR = "fake_controller" - private val FAKE_CONTROLLER_TRIGGER_KEY_DEVICE = TriggerKeyDevice.External( + private val FAKE_CONTROLLER_TRIGGER_KEY_DEVICE = KeyEventTriggerDevice.External( descriptor = FAKE_CONTROLLER_DESCRIPTOR, name = "Fake Controller", ) private val FAKE_CONTROLLER_INPUT_DEVICE = InputDeviceInfo( descriptor = FAKE_CONTROLLER_DESCRIPTOR, name = "Fake Controller", - id = 0, + id = 1, isExternal = true, isGameController = true, + sources = InputDevice.SOURCE_GAMEPAD, + ) + + private val FAKE_CONTROLLER_EVDEV_DEVICE = EvdevDeviceInfo( + name = "Fake Controller", + bus = 1, + vendor = 2, + product = 1, + ) + + private val FAKE_VOLUME_EVDEV_DEVICE = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2, ) private const val FAKE_PACKAGE_NAME = "test_package" @@ -116,30 +145,13 @@ class KeyMapControllerTest { private val TEST_ACTION_2: Action = Action( data = ActionData.App(FAKE_PACKAGE_NAME), ) - - private val GUI_KEYBOARD_IME_INFO = ImeInfo( - id = "ime_id", - packageName = "io.github.sds100.keymapper.inputmethod.latin", - label = "Key Mapper GUI Keyboard", - isEnabled = true, - isChosen = true, - ) - - private val GBOARD_IME_INFO = ImeInfo( - id = "gboard_id", - packageName = "com.google.android.inputmethod.latin", - label = "Gboard", - isEnabled = true, - isChosen = false, - ) } - private lateinit var controller: KeyMapController + private lateinit var controller: KeyMapAlgorithm private lateinit var detectKeyMapsUseCase: DetectKeyMapsUseCase private lateinit var performActionsUseCase: PerformActionsUseCase private lateinit var detectConstraintsUseCase: DetectConstraintsUseCase - private lateinit var keyMapListFlow: MutableStateFlow> - private lateinit var detectKeyMapListFlow: MutableStateFlow> + private lateinit var mockedKeyEvent: MockedStatic @get:Rule var instantExecutorRule = InstantTaskExecutorRule() @@ -147,19 +159,17 @@ class KeyMapControllerTest { private val testDispatcher = UnconfinedTestDispatcher() private val testScope = TestScope(testDispatcher) + private fun loadKeyMaps(vararg keyMap: KeyMap) { + controller.loadKeyMaps(keyMap.map { DetectKeyMapModel(it) }) + } + + private fun loadKeyMaps(vararg keyMap: DetectKeyMapModel) { + controller.loadKeyMaps(keyMap.toList()) + } + @Before fun init() { - keyMapListFlow = MutableStateFlow(emptyList()) - detectKeyMapListFlow = MutableStateFlow(emptyList()) - detectKeyMapsUseCase = mock { - on { allKeyMapList } doReturn combine( - keyMapListFlow, - detectKeyMapListFlow, - ) { keyMapList, detectKeyMapList -> - keyMapList.map { DetectKeyMapModel(keyMap = it) }.plus(detectKeyMapList) - } - MutableStateFlow(VIBRATION_DURATION).apply { on { defaultVibrateDuration } doReturn this } @@ -208,7 +218,10 @@ class KeyMapControllerTest { on { getSnapshot() } doReturn TestConstraintSnapshot() } - controller = KeyMapController( + mockedKeyEvent = mockStatic(KeyEvent::class.java) + mockedKeyEvent.`when` { KeyEvent.getMaxKeyCode() }.thenReturn(1000) + + controller = KeyMapAlgorithm( testScope, detectKeyMapsUseCase, performActionsUseCase, @@ -216,10 +229,413 @@ class KeyMapControllerTest { ) } + @After + fun tearDown() { + mockedKeyEvent.close() + } + + @Test + fun `Detect mouse button which only has scan code`() = runTest(testDispatcher) { + val trigger = singleKeyTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_UNKNOWN, + scanCode = Scancode.BTN_LEFT, + device = FAKE_CONTROLLER_EVDEV_DEVICE, + ), + ) + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + inputDownEvdevEvent( + KeyEvent.KEYCODE_UNKNOWN, + Scancode.BTN_LEFT, + FAKE_CONTROLLER_EVDEV_DEVICE, + ) + inputUpEvdevEvent(KeyEvent.KEYCODE_UNKNOWN, Scancode.BTN_LEFT, FAKE_CONTROLLER_EVDEV_DEVICE) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + + @Test + fun `Detect evdev trigger with scan code if key has unknown key code`() = + runTest(testDispatcher) { + val trigger = singleKeyTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_UNKNOWN, + scanCode = Scancode.KEY_B, + device = FAKE_CONTROLLER_EVDEV_DEVICE, + ), + ) + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + inputDownEvdevEvent(KeyEvent.KEYCODE_B, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + inputUpEvdevEvent(KeyEvent.KEYCODE_B, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + + @Test + fun `Detect evdev trigger with scan code if user setting enabled`() = + runTest(testDispatcher) { + val trigger = singleKeyTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_A, + scanCode = Scancode.KEY_B, + device = FAKE_CONTROLLER_EVDEV_DEVICE, + detectWithScanCodeUserSetting = true, + ), + ) + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + inputDownEvdevEvent(KeyEvent.KEYCODE_B, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + inputUpEvdevEvent(KeyEvent.KEYCODE_B, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + + @Test + fun `Detect evdev trigger with scan code when scan code matches but key code differs`() = + runTest(testDispatcher) { + val trigger = singleKeyTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_A, + scanCode = Scancode.KEY_B, + device = FAKE_CONTROLLER_EVDEV_DEVICE, + detectWithScanCodeUserSetting = true, + ), + ) + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + // Input with different key code but matching scan code + inputDownEvdevEvent(KeyEvent.KEYCODE_C, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + inputUpEvdevEvent(KeyEvent.KEYCODE_C, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + + @Test + fun `Do not detect evdev trigger when scan code differs`() = + runTest(testDispatcher) { + val trigger = singleKeyTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_A, + scanCode = Scancode.KEY_B, + device = FAKE_CONTROLLER_EVDEV_DEVICE, + detectWithScanCodeUserSetting = true, + ), + ) + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + // Input with matching key code but different scan code + inputDownEvdevEvent(KeyEvent.KEYCODE_A, Scancode.KEY_C, FAKE_CONTROLLER_EVDEV_DEVICE) + inputUpEvdevEvent(KeyEvent.KEYCODE_A, Scancode.KEY_C, FAKE_CONTROLLER_EVDEV_DEVICE) + + verify(performActionsUseCase, never()).perform(TEST_ACTION.data) + } + + @Test + fun `Sequence trigger with multiple evdev keys and scan code detection is triggered`() = + runTest(testDispatcher) { + val trigger = sequenceTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_A, + // Different scan code + scanCode = Scancode.KEY_B, + device = FAKE_CONTROLLER_EVDEV_DEVICE, + detectWithScanCodeUserSetting = true, + ), + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_C, + // Different scan code + scanCode = Scancode.KEY_D, + device = FAKE_CONTROLLER_EVDEV_DEVICE, + detectWithScanCodeUserSetting = true, + ), + ) + + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + // Input with scan codes that match the trigger + inputDownEvdevEvent(KeyEvent.KEYCODE_X, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + inputUpEvdevEvent(KeyEvent.KEYCODE_X, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + + inputDownEvdevEvent(KeyEvent.KEYCODE_Y, Scancode.KEY_D, FAKE_CONTROLLER_EVDEV_DEVICE) + inputUpEvdevEvent(KeyEvent.KEYCODE_Y, Scancode.KEY_D, FAKE_CONTROLLER_EVDEV_DEVICE) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + + @Test + fun `Parallel trigger with multiple evdev keys and scan code detection is triggered`() = + runTest(testDispatcher) { + val trigger = parallelTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_A, + // Different scan code + scanCode = Scancode.KEY_B, + device = FAKE_CONTROLLER_EVDEV_DEVICE, + detectWithScanCodeUserSetting = true, + ), + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_C, + // Different scan code + scanCode = Scancode.KEY_D, + device = FAKE_CONTROLLER_EVDEV_DEVICE, + detectWithScanCodeUserSetting = true, + ), + ) + + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + // Input both keys simultaneously with scan codes that match the trigger + inputDownEvdevEvent(KeyEvent.KEYCODE_X, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + inputDownEvdevEvent(KeyEvent.KEYCODE_Y, Scancode.KEY_D, FAKE_CONTROLLER_EVDEV_DEVICE) + + inputUpEvdevEvent(KeyEvent.KEYCODE_X, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + inputUpEvdevEvent(KeyEvent.KEYCODE_Y, Scancode.KEY_D, FAKE_CONTROLLER_EVDEV_DEVICE) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + + @Test + fun `Scan code detection works with long press evdev trigger`() = + runTest(testDispatcher) { + val trigger = singleKeyTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_A, + scanCode = Scancode.KEY_B, + device = FAKE_CONTROLLER_EVDEV_DEVICE, + clickType = ClickType.LONG_PRESS, + detectWithScanCodeUserSetting = true, + ), + ) + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + inputDownEvdevEvent(KeyEvent.KEYCODE_X, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + // Wait for long press duration + delay(LONG_PRESS_DELAY + 100L) + inputUpEvdevEvent(KeyEvent.KEYCODE_X, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + + @Test + fun `Scan code detection works with double press evdev trigger`() = + runTest(testDispatcher) { + val trigger = singleKeyTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_A, + scanCode = Scancode.KEY_B, + device = FAKE_CONTROLLER_EVDEV_DEVICE, + clickType = ClickType.DOUBLE_PRESS, + detectWithScanCodeUserSetting = true, + ), + ) + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + // First press + inputDownEvdevEvent(KeyEvent.KEYCODE_X, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + delay(50L) + inputUpEvdevEvent(KeyEvent.KEYCODE_X, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + delay(50L) + + // Second press + inputDownEvdevEvent(KeyEvent.KEYCODE_X, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + delay(50L) + inputUpEvdevEvent(KeyEvent.KEYCODE_X, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + + @Test + fun `Scan code detection fails when device differs for evdev trigger`() = + runTest(testDispatcher) { + val trigger = singleKeyTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_A, + scanCode = Scancode.KEY_B, + device = FAKE_CONTROLLER_EVDEV_DEVICE, + ), + ) + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + // Input from different device + inputDownEvdevEvent(KeyEvent.KEYCODE_X, Scancode.KEY_B, FAKE_VOLUME_EVDEV_DEVICE) + inputUpEvdevEvent(KeyEvent.KEYCODE_X, Scancode.KEY_B, FAKE_VOLUME_EVDEV_DEVICE) + + verify(performActionsUseCase, never()).perform(TEST_ACTION.data) + } + + @Test + fun `Detect key event trigger with scan code if key has unknown key code`() = + runTest(testDispatcher) { + val trigger = singleKeyTrigger( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_UNKNOWN, + scanCode = Scancode.KEY_B, + device = FAKE_CONTROLLER_TRIGGER_KEY_DEVICE, + clickType = ClickType.SHORT_PRESS, + // It will be automatically enabled even if the user hasn't explicitly turned it on + detectWithScanCodeUserSetting = false, + ), + ) + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + inputKeyEvent( + keyCode = KeyEvent.KEYCODE_B, + action = KeyEvent.ACTION_DOWN, + device = FAKE_CONTROLLER_INPUT_DEVICE, + scanCode = Scancode.KEY_B, + ) + inputKeyEvent( + keyCode = KeyEvent.KEYCODE_B, + action = KeyEvent.ACTION_UP, + device = FAKE_CONTROLLER_INPUT_DEVICE, + scanCode = Scancode.KEY_B, + ) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + + @Test + fun `Detect key event trigger with scan code if user setting enabled`() = + runTest(testDispatcher) { + val trigger = singleKeyTrigger( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_A, + scanCode = Scancode.KEY_B, + device = FAKE_CONTROLLER_TRIGGER_KEY_DEVICE, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = true, + ), + ) + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + inputKeyEvent( + keyCode = KeyEvent.KEYCODE_B, + action = KeyEvent.ACTION_DOWN, + device = FAKE_CONTROLLER_INPUT_DEVICE, + scanCode = Scancode.KEY_B, + ) + inputKeyEvent( + keyCode = KeyEvent.KEYCODE_B, + action = KeyEvent.ACTION_UP, + device = FAKE_CONTROLLER_INPUT_DEVICE, + scanCode = Scancode.KEY_B, + ) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + + @Test + fun `Sequence trigger with multiple evdev keys is triggered`() = + runTest(testDispatcher) { + val trigger = sequenceTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_A, + scanCode = Scancode.KEY_A, + device = FAKE_CONTROLLER_EVDEV_DEVICE, + ), + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_B, + scanCode = Scancode.KEY_B, + device = FAKE_CONTROLLER_EVDEV_DEVICE, + ), + ) + + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + inputDownEvdevEvent(KeyEvent.KEYCODE_A, Scancode.KEY_A, FAKE_CONTROLLER_EVDEV_DEVICE) + inputUpEvdevEvent(KeyEvent.KEYCODE_A, Scancode.KEY_A, FAKE_CONTROLLER_EVDEV_DEVICE) + + inputDownEvdevEvent(KeyEvent.KEYCODE_B, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + inputUpEvdevEvent(KeyEvent.KEYCODE_B, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + + @Test + fun `Parallel trigger with multiple evdev keys is triggered`() = + runTest(testDispatcher) { + val trigger = parallelTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_A, + scanCode = Scancode.KEY_A, + device = FAKE_CONTROLLER_EVDEV_DEVICE, + ), + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_B, + scanCode = Scancode.KEY_B, + device = FAKE_CONTROLLER_EVDEV_DEVICE, + ), + ) + + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + inputDownEvdevEvent(KeyEvent.KEYCODE_A, Scancode.KEY_A, FAKE_CONTROLLER_EVDEV_DEVICE) + inputDownEvdevEvent(KeyEvent.KEYCODE_B, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + + inputUpEvdevEvent(KeyEvent.KEYCODE_A, Scancode.KEY_A, FAKE_CONTROLLER_EVDEV_DEVICE) + inputUpEvdevEvent(KeyEvent.KEYCODE_B, Scancode.KEY_B, FAKE_CONTROLLER_EVDEV_DEVICE) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + + @Test + fun `Evdev trigger is not triggered from events from other devices`() = + runTest(testDispatcher) { + val trigger = singleKeyTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_POWER, + scanCode = Scancode.KEY_POWER, + device = FAKE_VOLUME_EVDEV_DEVICE, + ), + ) + + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + mockEvdevKeyInput(trigger.keys[0], FAKE_CONTROLLER_EVDEV_DEVICE) + + verify(performActionsUseCase, never()).perform(TEST_ACTION.data) + } + + @Test + fun `Short press trigger evdev trigger from external device`() = runTest(testDispatcher) { + val trigger = singleKeyTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_A, + scanCode = Scancode.KEY_POWER, + device = FAKE_CONTROLLER_EVDEV_DEVICE, + ), + ) + + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + mockEvdevKeyInput(trigger.keys[0], FAKE_CONTROLLER_EVDEV_DEVICE) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + + @Test + fun `Short press trigger evdev trigger from internal device`() = runTest(testDispatcher) { + val trigger = singleKeyTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_POWER, + scanCode = Scancode.KEY_POWER, + device = FAKE_VOLUME_EVDEV_DEVICE, + ), + ) + + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION))) + + mockEvdevKeyInput(trigger.keys[0], FAKE_VOLUME_EVDEV_DEVICE) + + verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) + } + @Test fun `Do not perform if one group constraint set is not satisfied`() = runTest(testDispatcher) { val trigger = singleKeyTrigger(triggerKey(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN)) - detectKeyMapListFlow.value = listOf( + loadKeyMaps( DetectKeyMapModel( keyMap = KeyMap( trigger = trigger, @@ -267,7 +683,7 @@ class KeyMapControllerTest { fun `Perform if all group constraints and key map constraints are satisfied`() = runTest(testDispatcher) { val trigger = singleKeyTrigger(triggerKey(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN)) - detectKeyMapListFlow.value = listOf( + loadKeyMaps( DetectKeyMapModel( keyMap = KeyMap( trigger = trigger, @@ -348,7 +764,7 @@ class KeyMapControllerTest { vibrate = true, ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = shortPressTrigger, actionList = listOf(TEST_ACTION)), KeyMap(1, trigger = doublePressTrigger, actionList = listOf(TEST_ACTION_2)), KeyMap(2, trigger = longPressTrigger, actionList = listOf(TEST_ACTION_2)), @@ -389,7 +805,7 @@ class KeyMapControllerTest { vibrate = true, ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = shortPressTrigger, actionList = listOf(TEST_ACTION)), KeyMap(1, trigger = doublePressTrigger, actionList = listOf(TEST_ACTION_2)), ) @@ -428,7 +844,7 @@ class KeyMapControllerTest { vibrate = true, ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = shortPressTrigger, actionList = listOf(TEST_ACTION)), KeyMap(1, trigger = doublePressTrigger, actionList = listOf(TEST_ACTION_2)), ) @@ -469,7 +885,7 @@ class KeyMapControllerTest { vibrate = true, ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = shortPressTrigger, actionList = listOf(TEST_ACTION)), KeyMap(1, trigger = longPressTrigger, actionList = listOf(TEST_ACTION_2)), ) @@ -510,7 +926,7 @@ class KeyMapControllerTest { vibrate = true, ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = shortPressTrigger, actionList = listOf(TEST_ACTION)), KeyMap(1, trigger = longPressTrigger, actionList = listOf(TEST_ACTION_2)), ) @@ -551,7 +967,7 @@ class KeyMapControllerTest { vibrate = true, ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = shortPressTrigger, actionList = listOf(TEST_ACTION)), KeyMap(1, trigger = longPressTrigger, actionList = listOf(TEST_ACTION_2)), ) @@ -590,7 +1006,7 @@ class KeyMapControllerTest { vibrate = true, ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = shortPressTrigger, actionList = listOf(TEST_ACTION)), KeyMap(1, trigger = longPressTrigger, actionList = listOf(TEST_ACTION_2)), ) @@ -606,7 +1022,7 @@ class KeyMapControllerTest { @Test fun `Sequence trigger with fingerprint gesture and key code`() = runTest(testDispatcher) { // GIVEN - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap( trigger = sequenceTrigger( triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN), @@ -631,7 +1047,7 @@ class KeyMapControllerTest { @Test fun `Input fingerprint gesture`() = runTest(testDispatcher) { // GIVEN - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap( trigger = singleKeyTrigger( FingerprintTriggerKey( @@ -664,7 +1080,7 @@ class KeyMapControllerTest { sequenceTrigger(triggerKey(KeyEvent.KEYCODE_J), triggerKey(KeyEvent.KEYCODE_K)) val enterAction = Action(data = ActionData.InputKeyEvent(KeyEvent.KEYCODE_ENTER)) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = copyTrigger, actionList = listOf(copyAction)), KeyMap(1, trigger = sequenceTrigger, actionList = listOf(enterAction)), ) @@ -715,7 +1131,7 @@ class KeyMapControllerTest { val sequenceTriggerAction2 = Action(data = ActionData.InputKeyEvent(KeyEvent.KEYCODE_ENTER)) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = copyTrigger, actionList = listOf(copyAction)), KeyMap(1, trigger = sequenceTrigger1, actionList = listOf(sequenceTriggerAction1)), KeyMap(2, trigger = sequenceTrigger2, actionList = listOf(sequenceTriggerAction2)), @@ -758,7 +1174,7 @@ class KeyMapControllerTest { sequenceTrigger(triggerKey(KeyEvent.KEYCODE_J), triggerKey(KeyEvent.KEYCODE_K)) val enterAction = Action(data = ActionData.InputKeyEvent(KeyEvent.KEYCODE_ENTER)) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = copyTrigger, actionList = listOf(copyAction)), KeyMap(1, trigger = pasteTrigger, actionList = listOf(pasteAction)), KeyMap(2, trigger = sequenceTrigger, actionList = listOf(enterAction)), @@ -791,7 +1207,7 @@ class KeyMapControllerTest { sequenceTrigger(triggerKey(KeyEvent.KEYCODE_J), triggerKey(KeyEvent.KEYCODE_K)) val enterAction = Action(data = ActionData.InputKeyEvent(KeyEvent.KEYCODE_ENTER)) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = copyTrigger, actionList = listOf(copyAction)), KeyMap(1, trigger = pasteTrigger, actionList = listOf(pasteAction)), KeyMap(2, trigger = sequenceTrigger, actionList = listOf(enterAction)), @@ -823,7 +1239,7 @@ class KeyMapControllerTest { sequenceTrigger(triggerKey(KeyEvent.KEYCODE_J), triggerKey(KeyEvent.KEYCODE_K)) val enterAction = Action(data = ActionData.InputKeyEvent(KeyEvent.KEYCODE_ENTER)) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = copyTrigger, actionList = listOf(copyAction)), KeyMap(1, trigger = pasteTrigger, actionList = listOf(pasteAction)), KeyMap(2, trigger = sequenceTrigger, actionList = listOf(enterAction)), @@ -846,7 +1262,7 @@ class KeyMapControllerTest { triggerKey( KeyEvent.KEYCODE_DPAD_LEFT, clickType = ClickType.SHORT_PRESS, - detectionSource = KeyEventDetectionSource.INPUT_METHOD, + requiresIme = true, device = FAKE_CONTROLLER_TRIGGER_KEY_DEVICE, ), ) @@ -856,17 +1272,17 @@ class KeyMapControllerTest { holdDown = true, ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = trigger, actionList = listOf(action)), ) inOrder(performActionsUseCase) { inputMotionEvent(axisHatX = -1.0f) - verify(performActionsUseCase, times(1)).perform(action.data, InputEventType.DOWN) + verify(performActionsUseCase, times(1)).perform(action.data, InputEventAction.DOWN) delay(1000) // Hold down the DPAD button for 1 second. inputMotionEvent(axisHatX = 0.0f) - verify(performActionsUseCase, times(1)).perform(action.data, InputEventType.UP) + verify(performActionsUseCase, times(1)).perform(action.data, InputEventAction.UP) } } @@ -877,12 +1293,12 @@ class KeyMapControllerTest { triggerKey( KeyEvent.KEYCODE_DPAD_LEFT, clickType = ClickType.SHORT_PRESS, - detectionSource = KeyEventDetectionSource.INPUT_METHOD, + requiresIme = true, device = FAKE_CONTROLLER_TRIGGER_KEY_DEVICE, ), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = trigger, actionList = listOf(TEST_ACTION)), ) @@ -912,12 +1328,12 @@ class KeyMapControllerTest { triggerKey( KeyEvent.KEYCODE_DPAD_LEFT, clickType = ClickType.SHORT_PRESS, - detectionSource = KeyEventDetectionSource.INPUT_METHOD, + requiresIme = true, device = FAKE_CONTROLLER_TRIGGER_KEY_DEVICE, ), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = trigger, actionList = listOf(TEST_ACTION)), ) @@ -941,12 +1357,12 @@ class KeyMapControllerTest { triggerKey( KeyEvent.KEYCODE_DPAD_LEFT, clickType = ClickType.LONG_PRESS, - detectionSource = KeyEventDetectionSource.INPUT_METHOD, + requiresIme = true, device = FAKE_CONTROLLER_TRIGGER_KEY_DEVICE, ), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = trigger, actionList = listOf(TEST_ACTION)), ) @@ -968,12 +1384,12 @@ class KeyMapControllerTest { triggerKey( KeyEvent.KEYCODE_DPAD_LEFT, clickType = ClickType.SHORT_PRESS, - detectionSource = KeyEventDetectionSource.INPUT_METHOD, + requiresIme = true, device = FAKE_CONTROLLER_TRIGGER_KEY_DEVICE, ), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = trigger, actionList = listOf(TEST_ACTION)), ) @@ -998,11 +1414,11 @@ class KeyMapControllerTest { triggerKey( KeyEvent.KEYCODE_DPAD_LEFT, clickType = ClickType.SHORT_PRESS, - detectionSource = KeyEventDetectionSource.INPUT_METHOD, + requiresIme = true, ), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap( 0, trigger = trigger, @@ -1047,11 +1463,11 @@ class KeyMapControllerTest { triggerKey( KeyEvent.KEYCODE_DPAD_LEFT, clickType = ClickType.LONG_PRESS, - detectionSource = KeyEventDetectionSource.INPUT_METHOD, + requiresIme = true, ), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap( 0, trigger = longPressTrigger, @@ -1092,7 +1508,7 @@ class KeyMapControllerTest { ) val doublePressConstraints = ConstraintState(constraints = setOf(Constraint.WifiOff())) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap( 0, trigger = shortPressTrigger, @@ -1133,7 +1549,7 @@ class KeyMapControllerTest { ) val doublePressConstraints = ConstraintState(constraints = setOf(Constraint.WifiOff())) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap( 0, trigger = shortPressTrigger, @@ -1181,7 +1597,7 @@ class KeyMapControllerTest { ) .copy(longPressDelay = 500) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = longerTrigger, actionList = listOf(TEST_ACTION)), KeyMap(1, trigger = shorterTrigger, actionList = listOf(TEST_ACTION_2)), ) @@ -1193,7 +1609,7 @@ class KeyMapControllerTest { verify(performActionsUseCase, times(1)).perform(TEST_ACTION_2.data) verify(performActionsUseCase, never()).perform(TEST_ACTION_2.data) - verify(detectKeyMapsUseCase, never()).imitateButtonPress( + verify(detectKeyMapsUseCase, never()).imitateKeyEvent( any(), any(), any(), @@ -1208,7 +1624,7 @@ class KeyMapControllerTest { verify(performActionsUseCase, times(1)).perform(TEST_ACTION_2.data) verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) - verify(detectKeyMapsUseCase, never()).imitateButtonPress( + verify(detectKeyMapsUseCase, never()).imitateKeyEvent( any(), any(), any(), @@ -1218,12 +1634,11 @@ class KeyMapControllerTest { ) // If no triggers are detected - mockTriggerKeyInput(shorterTrigger.keys[0], 100L) verify(performActionsUseCase, never()).perform(TEST_ACTION_2.data) verify(performActionsUseCase, never()).perform(TEST_ACTION.data) - verify(detectKeyMapsUseCase, times(1)).imitateButtonPress( + verify(detectKeyMapsUseCase, times(2)).imitateKeyEvent( any(), any(), any(), @@ -1256,7 +1671,7 @@ class KeyMapControllerTest { ), ) - keyMapListFlow.value = listOf(keyMap) + loadKeyMaps(keyMap) var isFlashlightEnabled = false @@ -1310,7 +1725,7 @@ class KeyMapControllerTest { actionList = listOf(TEST_ACTION_2), ) - keyMapListFlow.value = listOf(keyMap1, keyMap2) + loadKeyMaps(keyMap1, keyMap2) // WHEN inOrder(performActionsUseCase) { @@ -1356,7 +1771,7 @@ class KeyMapControllerTest { val trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) val actionList = listOf(Action(data = ActionData.InputKeyEvent(2))) - keyMapListFlow.value = listOf(KeyMap(trigger = trigger, actionList = actionList)) + loadKeyMaps(KeyMap(trigger = trigger, actionList = actionList)) // WHEN whenever(performActionsUseCase.getErrorSnapshot()).thenReturn(object : @@ -1394,7 +1809,7 @@ class KeyMapControllerTest { Action(data = ActionData.InputKeyEvent(2)), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(trigger = trigger, actionList = actionList), ) @@ -1424,7 +1839,7 @@ class KeyMapControllerTest { repeatLimit = 2, ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(trigger = trigger, actionList = listOf(action)), ) @@ -1461,15 +1876,13 @@ class KeyMapControllerTest { data = ActionData.InputKeyEvent(keyCode = 3), ) - val keyMaps = listOf( + loadKeyMaps( KeyMap( trigger = trigger, actionList = listOf(action1, action2, action3), ), ) - keyMapListFlow.value = keyMaps - // WHEN // ensure consumed @@ -1488,7 +1901,7 @@ class KeyMapControllerTest { // GIVEN val trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) - val keyMaps = listOf( + loadKeyMaps( KeyMap( trigger = trigger, actionList = listOf(TEST_ACTION), @@ -1499,8 +1912,6 @@ class KeyMapControllerTest { ), ) - keyMapListFlow.value = keyMaps - // WHEN // ensure consumed @@ -1536,7 +1947,7 @@ class KeyMapControllerTest { actionList = listOf(action), ) - keyMapListFlow.value = listOf(keyMap) + loadKeyMaps(keyMap) // WHEN mockTriggerKeyInput(keyMap.trigger.keys[0]) @@ -1567,7 +1978,7 @@ class KeyMapControllerTest { actionList = listOf(action), ) - keyMapListFlow.value = listOf(keyMap) + loadKeyMaps(keyMap) // WHEN mockTriggerKeyInput(keyMap.trigger.keys[0]) @@ -1600,7 +2011,7 @@ class KeyMapControllerTest { actionList = listOf(action), ) - keyMapListFlow.value = listOf(keyMap) + loadKeyMaps(keyMap) // WHEN mockTriggerKeyInput(keyMap.trigger.keys[0]) @@ -1636,7 +2047,7 @@ class KeyMapControllerTest { actionList = listOf(action), ) - keyMapListFlow.value = listOf(keyMap) + loadKeyMaps(keyMap) // WHEN mockTriggerKeyInput(keyMap.trigger.keys[0], delay = 300) @@ -1664,7 +2075,7 @@ class KeyMapControllerTest { actionList = listOf(action), ) - keyMapListFlow.value = listOf(keyMap) + loadKeyMaps(keyMap) // WHEN @@ -1681,7 +2092,7 @@ class KeyMapControllerTest { @Test fun `overlapping triggers 3`() = runTest(testDispatcher) { // GIVEN - val keyMaps = listOf( + val keyMaps = arrayOf( KeyMap( trigger = parallelTrigger( triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN), @@ -1697,7 +2108,7 @@ class KeyMapControllerTest { ), ) - keyMapListFlow.value = keyMaps + loadKeyMaps(*keyMaps) inOrder(performActionsUseCase) { // WHEN @@ -1749,7 +2160,7 @@ class KeyMapControllerTest { @Test fun `overlapping triggers 2`() = runTest(testDispatcher) { // GIVEN - val keyMaps = listOf( + val keyMaps = arrayOf( KeyMap( trigger = parallelTrigger( triggerKey(KeyEvent.KEYCODE_P), @@ -1765,7 +2176,7 @@ class KeyMapControllerTest { ), ) - keyMapListFlow.value = keyMaps + loadKeyMaps(*keyMaps) inOrder(performActionsUseCase) { // WHEN @@ -1810,7 +2221,7 @@ class KeyMapControllerTest { @Test fun `overlapping triggers 1`() = runTest(testDispatcher) { // GIVEN - val keyMaps = listOf( + val keyMaps = arrayOf( KeyMap( trigger = parallelTrigger( triggerKey(KeyEvent.KEYCODE_CTRL_LEFT), @@ -1828,7 +2239,7 @@ class KeyMapControllerTest { ), ) - keyMapListFlow.value = keyMaps + loadKeyMaps(*keyMaps) inOrder(performActionsUseCase) { // WHEN @@ -1912,7 +2323,7 @@ class KeyMapControllerTest { triggerKey(keyCode = 2), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap( trigger = trigger, actionList = listOf(TEST_ACTION), @@ -1925,7 +2336,14 @@ class KeyMapControllerTest { inputKeyEvent(keyCode = 1, action = KeyEvent.ACTION_UP) // THEN - verify(detectKeyMapsUseCase, times(1)).imitateButtonPress(keyCode = 1) + verify(detectKeyMapsUseCase, times(1)).imitateKeyEvent( + keyCode = 1, + action = KeyEvent.ACTION_DOWN, + ) + verify(detectKeyMapsUseCase, times(1)).imitateKeyEvent( + keyCode = 1, + action = KeyEvent.ACTION_UP, + ) verifyNoMoreInteractions() // verify nothing happens and no key events are consumed when the 2nd key in the trigger is pressed @@ -1934,8 +2352,22 @@ class KeyMapControllerTest { assertThat(inputKeyEvent(keyCode = 2, action = KeyEvent.ACTION_UP), `is`(false)) // THEN - verify(detectKeyMapsUseCase, never()).imitateButtonPress(keyCode = 1) - verify(detectKeyMapsUseCase, never()).imitateButtonPress(keyCode = 2) + verify(detectKeyMapsUseCase, never()).imitateKeyEvent( + keyCode = 1, + action = KeyEvent.ACTION_DOWN, + ) + verify(detectKeyMapsUseCase, never()).imitateKeyEvent( + keyCode = 1, + action = KeyEvent.ACTION_UP, + ) + verify(detectKeyMapsUseCase, never()).imitateKeyEvent( + keyCode = 2, + action = KeyEvent.ACTION_DOWN, + ) + verify(detectKeyMapsUseCase, never()).imitateKeyEvent( + keyCode = 2, + action = KeyEvent.ACTION_UP, + ) verify(performActionsUseCase, never()).perform(action = TEST_ACTION.data) // verify the action is performed and no keys are imitated when triggering the key map @@ -1972,7 +2404,7 @@ class KeyMapControllerTest { triggerKey(keyCode = 2), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap( trigger = trigger, actionList = listOf(TEST_ACTION), @@ -1986,8 +2418,22 @@ class KeyMapControllerTest { inputKeyEvent(keyCode = 2, action = KeyEvent.ACTION_UP) // THEN - verify(detectKeyMapsUseCase, never()).imitateButtonPress(keyCode = 1) - verify(detectKeyMapsUseCase, never()).imitateButtonPress(keyCode = 2) + verify(detectKeyMapsUseCase, never()).imitateKeyEvent( + keyCode = 1, + action = KeyEvent.ACTION_DOWN, + ) + verify(detectKeyMapsUseCase, never()).imitateKeyEvent( + keyCode = 1, + action = KeyEvent.ACTION_UP, + ) + verify(detectKeyMapsUseCase, never()).imitateKeyEvent( + keyCode = 2, + action = KeyEvent.ACTION_DOWN, + ) + verify(detectKeyMapsUseCase, never()).imitateKeyEvent( + keyCode = 2, + action = KeyEvent.ACTION_UP, + ) } /** @@ -2010,7 +2456,7 @@ class KeyMapControllerTest { actionList = listOf(action), ) - keyMapListFlow.value = listOf(keyMap) + loadKeyMaps(keyMap) // WHEN mockTriggerKeyInput(triggerKey(keyCode = 2), delay = 1) @@ -2054,7 +2500,7 @@ class KeyMapControllerTest { repeat = true, ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = trigger1, actionList = listOf(action1)), KeyMap(1, trigger = trigger2, actionList = listOf(action2)), ) @@ -2101,7 +2547,7 @@ class KeyMapControllerTest { sequenceTrigger(triggerKey(clickType = ClickType.DOUBLE_PRESS, keyCode = 1)) val action2 = Action(data = ActionData.InputKeyEvent(keyCode = 3)) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = trigger1, actionList = listOf(action1)), KeyMap(1, trigger = trigger2, actionList = listOf(action2)), ) @@ -2157,7 +2603,7 @@ class KeyMapControllerTest { sequenceTrigger(triggerKey(clickType = ClickType.DOUBLE_PRESS, keyCode = 1)) val action3 = Action(data = ActionData.InputKeyEvent(keyCode = 4)) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = trigger1, actionList = listOf(action1)), KeyMap(1, trigger = trigger2, actionList = listOf(action2)), KeyMap(2, trigger = trigger3, actionList = listOf(action3)), @@ -2212,7 +2658,7 @@ class KeyMapControllerTest { parallelTrigger(triggerKey(clickType = ClickType.LONG_PRESS, keyCode = 1)) val action2 = Action(data = ActionData.InputKeyEvent(keyCode = 3), repeat = true) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = trigger1, actionList = listOf(action1)), KeyMap(1, trigger = trigger2, actionList = listOf(action2)), ) @@ -2262,7 +2708,7 @@ class KeyMapControllerTest { sequenceTrigger(triggerKey(clickType = ClickType.DOUBLE_PRESS, keyCode = 1)) val action2 = Action(data = ActionData.InputKeyEvent(keyCode = 3)) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = trigger1, actionList = listOf(action1)), KeyMap(1, trigger = trigger2, actionList = listOf(action2)), ) @@ -2324,7 +2770,7 @@ class KeyMapControllerTest { sequenceTrigger(triggerKey(clickType = ClickType.DOUBLE_PRESS, keyCode = 1)) val action3 = Action(data = ActionData.InputKeyEvent(keyCode = 4)) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = trigger1, actionList = listOf(action1)), KeyMap(1, trigger = trigger2, actionList = listOf(action2)), KeyMap(2, trigger = trigger3, actionList = listOf(action3)), @@ -2371,7 +2817,7 @@ class KeyMapControllerTest { triggerKey(KeyEvent.KEYCODE_A, clickType = ClickType.DOUBLE_PRESS), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION)), ) @@ -2402,7 +2848,7 @@ class KeyMapControllerTest { holdDown = true, ) - keyMapListFlow.value = listOf(KeyMap(trigger = trigger, actionList = listOf(action))) + loadKeyMaps(KeyMap(trigger = trigger, actionList = listOf(action))) val metaState = KeyEvent.META_META_ON.withFlag(KeyEvent.META_META_LEFT_ON) @@ -2449,29 +2895,29 @@ class KeyMapControllerTest { verify(performActionsUseCase, times(1)).perform( action.data, - InputEventType.DOWN, + InputEventAction.DOWN, metaState, ) - verify(detectKeyMapsUseCase, times(1)).imitateButtonPress( + verify(detectKeyMapsUseCase, times(1)).imitateKeyEvent( KeyEvent.KEYCODE_E, metaState, FAKE_KEYBOARD_DEVICE_ID, - InputEventType.DOWN, + KeyEvent.ACTION_DOWN, scanCode = 33, ) verify(performActionsUseCase, times(1)).perform( action.data, - InputEventType.UP, + InputEventAction.UP, 0, ) - verify(detectKeyMapsUseCase, times(1)).imitateButtonPress( + verify(detectKeyMapsUseCase, times(1)).imitateKeyEvent( KeyEvent.KEYCODE_E, 0, FAKE_KEYBOARD_DEVICE_ID, - InputEventType.UP, + KeyEvent.ACTION_UP, scanCode = 33, ) @@ -2518,29 +2964,29 @@ class KeyMapControllerTest { verify(performActionsUseCase, times(1)).perform( action.data, - InputEventType.DOWN, + InputEventAction.DOWN, metaState, ) - verify(detectKeyMapsUseCase, times(1)).imitateButtonPress( + verify(detectKeyMapsUseCase, times(1)).imitateKeyEvent( KeyEvent.KEYCODE_E, metaState, FAKE_KEYBOARD_DEVICE_ID, - InputEventType.DOWN, + KeyEvent.ACTION_DOWN, scanCode = 33, ) - verify(detectKeyMapsUseCase, times(1)).imitateButtonPress( + verify(detectKeyMapsUseCase, times(1)).imitateKeyEvent( KeyEvent.KEYCODE_E, metaState, FAKE_KEYBOARD_DEVICE_ID, - InputEventType.UP, + KeyEvent.ACTION_UP, scanCode = 33, ) verify(performActionsUseCase, times(1)).perform( action.data, - InputEventType.UP, + InputEventAction.UP, 0, ) @@ -2561,7 +3007,7 @@ class KeyMapControllerTest { triggerKey(KeyEvent.KEYCODE_A), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = oneKeyTrigger, actionList = listOf(TEST_ACTION_2)), KeyMap(1, trigger = twoKeyTrigger, actionList = listOf(TEST_ACTION)), ) @@ -2602,11 +3048,11 @@ class KeyMapControllerTest { val triggerAnyDevice = singleKeyTrigger( triggerKey( KeyEvent.KEYCODE_A, - device = TriggerKeyDevice.Any, + device = KeyEventTriggerDevice.Any, ), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = triggerKeyboard, actionList = listOf(TEST_ACTION)), KeyMap(1, trigger = triggerAnyDevice, actionList = listOf(TEST_ACTION_2)), ) @@ -2626,7 +3072,7 @@ class KeyMapControllerTest { triggerKey(KeyEvent.KEYCODE_A, FAKE_HEADPHONE_TRIGGER_KEY_DEVICE), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = triggerHeadphone, actionList = listOf(TEST_ACTION)), ) @@ -2656,7 +3102,7 @@ class KeyMapControllerTest { actionList = listOf(action), ) - keyMapListFlow.value = listOf(keymap) + loadKeyMaps(keymap) // WHEN mockTriggerKeyInput(trigger.keys[0]) @@ -2664,7 +3110,7 @@ class KeyMapControllerTest { // THEN verify(performActionsUseCase, times(1)).perform( action.data, - InputEventType.DOWN, + InputEventAction.DOWN, ) // WHEN @@ -2672,7 +3118,7 @@ class KeyMapControllerTest { verify(performActionsUseCase, times(1)).perform( action.data, - InputEventType.UP, + InputEventAction.UP, ) } @@ -2684,7 +3130,7 @@ class KeyMapControllerTest { runTest(testDispatcher) { val trigger = singleKeyTrigger(triggerKey(KeyEvent.KEYCODE_CTRL_LEFT)) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap( 0, trigger = trigger, @@ -2724,7 +3170,7 @@ class KeyMapControllerTest { inputKeyEvent(KeyEvent.KEYCODE_C, KeyEvent.ACTION_UP) inOrder(detectKeyMapsUseCase) { - verify(detectKeyMapsUseCase, times(1)).imitateButtonPress( + verify(detectKeyMapsUseCase, times(1)).imitateKeyEvent( any(), metaState = eq(KeyEvent.META_ALT_LEFT_ON + KeyEvent.META_ALT_ON + KeyEvent.META_SHIFT_LEFT_ON + KeyEvent.META_SHIFT_ON), any(), @@ -2733,7 +3179,7 @@ class KeyMapControllerTest { any(), ) - verify(detectKeyMapsUseCase, times(1)).imitateButtonPress( + verify(detectKeyMapsUseCase, times(1)).imitateKeyEvent( any(), metaState = eq(0), any(), @@ -2750,7 +3196,7 @@ class KeyMapControllerTest { val firstTrigger = sequenceTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - device = TriggerKeyDevice.Any, + device = KeyEventTriggerDevice.Any, ), triggerKey(KeyEvent.KEYCODE_VOLUME_UP), ) @@ -2759,12 +3205,12 @@ class KeyMapControllerTest { triggerKey(KeyEvent.KEYCODE_HOME), triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - device = TriggerKeyDevice.Any, + device = KeyEventTriggerDevice.Any, ), triggerKey(KeyEvent.KEYCODE_VOLUME_UP), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = firstTrigger, actionList = listOf(TEST_ACTION)), KeyMap(1, trigger = secondTrigger, actionList = listOf(TEST_ACTION_2)), ) @@ -2773,7 +3219,7 @@ class KeyMapControllerTest { mockTriggerKeyInput( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - device = TriggerKeyDevice.Any, + device = KeyEventTriggerDevice.Any, ), ) mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_VOLUME_UP)) @@ -2793,17 +3239,18 @@ class KeyMapControllerTest { triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.LONG_PRESS), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = homeTrigger, actionList = listOf(TEST_ACTION)), ) - val consumedHomeDown = inputKeyEvent(KeyEvent.KEYCODE_HOME, KeyEvent.ACTION_DOWN, null) - inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_DOWN, null) + val consumedHomeDown = + inputKeyEvent(KeyEvent.KEYCODE_HOME, KeyEvent.ACTION_DOWN, FAKE_INTERNAL_DEVICE) + inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_DOWN, FAKE_INTERNAL_DEVICE) advanceUntilIdle() - inputKeyEvent(KeyEvent.KEYCODE_HOME, KeyEvent.ACTION_UP, null) - inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_UP, null) + inputKeyEvent(KeyEvent.KEYCODE_HOME, KeyEvent.ACTION_UP, FAKE_INTERNAL_DEVICE) + inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_UP, FAKE_INTERNAL_DEVICE) assertThat(consumedHomeDown, `is`(true)) @@ -2816,18 +3263,22 @@ class KeyMapControllerTest { triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.LONG_PRESS), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = recentsTrigger, actionList = listOf(TEST_ACTION)), ) val consumedRecentsDown = - inputKeyEvent(KeyEvent.KEYCODE_APP_SWITCH, KeyEvent.ACTION_DOWN, null) - inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_DOWN, null) + inputKeyEvent( + KeyEvent.KEYCODE_APP_SWITCH, + KeyEvent.ACTION_DOWN, + FAKE_INTERNAL_DEVICE, + ) + inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_DOWN, FAKE_INTERNAL_DEVICE) advanceUntilIdle() - inputKeyEvent(KeyEvent.KEYCODE_APP_SWITCH, KeyEvent.ACTION_UP, null) - inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_UP, null) + inputKeyEvent(KeyEvent.KEYCODE_APP_SWITCH, KeyEvent.ACTION_UP, FAKE_INTERNAL_DEVICE) + inputKeyEvent(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.ACTION_UP, FAKE_INTERNAL_DEVICE) assertThat(consumedRecentsDown, `is`(true)) } @@ -2841,7 +3292,7 @@ class KeyMapControllerTest { triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.DOUBLE_PRESS), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = shortPressTrigger, actionList = listOf(TEST_ACTION)), KeyMap(1, trigger = doublePressTrigger, actionList = listOf(TEST_ACTION_2)), ) @@ -2882,7 +3333,7 @@ class KeyMapControllerTest { ), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = shortPressTrigger, actionList = listOf(TEST_ACTION)), KeyMap(1, trigger = longPressTrigger, actionList = listOf(TEST_ACTION_2)), ) @@ -2915,7 +3366,7 @@ class KeyMapControllerTest { repeat = true, ) - keyMapListFlow.value = listOf(KeyMap(0, trigger = trigger, actionList = listOf(action))) + loadKeyMaps(KeyMap(0, trigger = trigger, actionList = listOf(action))) when (trigger.mode) { is TriggerMode.Parallel -> mockParallelTrigger(trigger, delay = 2000L) @@ -2962,11 +3413,10 @@ class KeyMapControllerTest { trigger: Trigger, ) = runTest(testDispatcher) { // given - keyMapListFlow.value = - listOf(KeyMap(0, trigger = trigger, actionList = listOf(TEST_ACTION))) + loadKeyMaps(KeyMap(0, trigger = trigger, actionList = listOf(TEST_ACTION))) // when - (trigger.keys[1] as KeyCodeTriggerKey).let { + (trigger.keys[1] as KeyEventTriggerKey).let { inputKeyEvent( it.keyCode, KeyEvent.ACTION_DOWN, @@ -2974,7 +3424,7 @@ class KeyMapControllerTest { ) } - (trigger.keys[1] as KeyCodeTriggerKey).let { + (trigger.keys[1] as KeyEventTriggerKey).let { val consumed = inputKeyEvent( it.keyCode, KeyEvent.ACTION_UP, @@ -3012,11 +3462,10 @@ class KeyMapControllerTest { triggerKey(KeyEvent.KEYCODE_VOLUME_UP), ) - keyMapListFlow.value = - listOf(KeyMap(0, trigger = trigger, actionList = listOf(TEST_ACTION))) + loadKeyMaps(KeyMap(0, trigger = trigger, actionList = listOf(TEST_ACTION))) // when - for (key in trigger.keys.mapNotNull { it as? KeyCodeTriggerKey }) { + for (key in trigger.keys.mapNotNull { it as? KeyEventTriggerKey }) { inputKeyEvent( key.keyCode, KeyEvent.ACTION_DOWN, @@ -3026,7 +3475,7 @@ class KeyMapControllerTest { var consumedUpCount = 0 - for (key in trigger.keys.mapNotNull { it as? KeyCodeTriggerKey }) { + for (key in trigger.keys.mapNotNull { it as? KeyEventTriggerKey }) { val consumed = inputKeyEvent( key.keyCode, @@ -3051,11 +3500,10 @@ class KeyMapControllerTest { triggerKey(KeyEvent.KEYCODE_VOLUME_UP, clickType = ClickType.LONG_PRESS), ) - keyMapListFlow.value = - listOf(KeyMap(0, trigger = trigger, actionList = listOf(TEST_ACTION))) + loadKeyMaps(KeyMap(0, trigger = trigger, actionList = listOf(TEST_ACTION))) // when - for (key in trigger.keys.mapNotNull { it as? KeyCodeTriggerKey }) { + for (key in trigger.keys.mapNotNull { it as? KeyEventTriggerKey }) { inputKeyEvent( key.keyCode, KeyEvent.ACTION_DOWN, @@ -3067,7 +3515,7 @@ class KeyMapControllerTest { var consumedUpCount = 0 - for (key in trigger.keys.mapNotNull { it as? KeyCodeTriggerKey }) { + for (key in trigger.keys.mapNotNull { it as? KeyEventTriggerKey }) { val consumed = inputKeyEvent( key.keyCode, @@ -3099,7 +3547,7 @@ class KeyMapControllerTest { ), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = longPressTrigger, actionList = listOf(TEST_ACTION)), KeyMap(1, trigger = doublePressTrigger, actionList = listOf(TEST_ACTION_2)), ) @@ -3109,10 +3557,16 @@ class KeyMapControllerTest { advanceUntilIdle() // then - verify( - detectKeyMapsUseCase, - times(1), - ).imitateButtonPress(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN) + verify(detectKeyMapsUseCase, times(1)) + .imitateKeyEvent( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + action = KeyEvent.ACTION_DOWN, + ) + verify(detectKeyMapsUseCase, times(1)) + .imitateKeyEvent( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + action = KeyEvent.ACTION_UP, + ) } @Test @@ -3125,7 +3579,7 @@ class KeyMapControllerTest { triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.LONG_PRESS), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = shortPressTrigger, actionList = listOf(TEST_ACTION)), KeyMap(1, trigger = longPressTrigger, actionList = listOf(TEST_ACTION_2)), ) @@ -3136,7 +3590,7 @@ class KeyMapControllerTest { // then verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) verify(performActionsUseCase, never()).perform(TEST_ACTION_2.data) - verify(detectKeyMapsUseCase, never()).imitateButtonPress( + verify(detectKeyMapsUseCase, never()).imitateKeyEvent( any(), any(), any(), @@ -3156,7 +3610,7 @@ class KeyMapControllerTest { triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN, clickType = ClickType.DOUBLE_PRESS), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = shortPressTrigger, actionList = listOf(TEST_ACTION)), KeyMap(1, trigger = doublePressTrigger, actionList = listOf(TEST_ACTION_2)), ) @@ -3169,7 +3623,7 @@ class KeyMapControllerTest { verify(performActionsUseCase, times(1)).perform(TEST_ACTION.data) // wait for the double press to try and imitate the key. - verify(detectKeyMapsUseCase, never()).imitateButtonPress( + verify(detectKeyMapsUseCase, never()).imitateKeyEvent( any(), any(), any(), @@ -3189,7 +3643,7 @@ class KeyMapControllerTest { triggerKey(KeyEvent.KEYCODE_VOLUME_UP), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(0, trigger = singleKeyTrigger, actionList = listOf(TEST_ACTION)), KeyMap(1, trigger = parallelTrigger, actionList = listOf(TEST_ACTION_2)), ) @@ -3198,7 +3652,7 @@ class KeyMapControllerTest { mockTriggerKeyInput(triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN)) // then - verify(detectKeyMapsUseCase, never()).imitateButtonPress( + verify(detectKeyMapsUseCase, never()).imitateKeyEvent( any(), any(), any(), @@ -3216,7 +3670,7 @@ class KeyMapControllerTest { triggerKey(KeyEvent.KEYCODE_VOLUME_UP), ) - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(trigger = trigger, actionList = listOf(TEST_ACTION)), ) @@ -3225,10 +3679,14 @@ class KeyMapControllerTest { delay = 100L, ) - verify( - detectKeyMapsUseCase, - times(1), - ).imitateButtonPress(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN) + verify(detectKeyMapsUseCase, times(1)) + .imitateKeyEvent( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + action = KeyEvent.ACTION_DOWN, + ) + + verify(detectKeyMapsUseCase, times(1)) + .imitateKeyEvent(keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, action = KeyEvent.ACTION_UP) } @Test @@ -3237,7 +3695,7 @@ class KeyMapControllerTest { runTest(testDispatcher) { val actionList = listOf(TEST_ACTION, TEST_ACTION_2) // GIVEN - keyMapListFlow.value = listOf( + loadKeyMaps( KeyMap(trigger = trigger, actionList = actionList), ) @@ -3262,7 +3720,7 @@ class KeyMapControllerTest { singleKeyTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, ), ), ), @@ -3271,7 +3729,7 @@ class KeyMapControllerTest { sequenceTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, ), ), ), @@ -3290,12 +3748,12 @@ class KeyMapControllerTest { fun invalidInput_downNotConsumed(description: String, keyMap: KeyMap) = runTest(testDispatcher) { // GIVEN - keyMapListFlow.value = listOf(keyMap) + loadKeyMaps(keyMap) // WHEN var consumedCount = 0 - for (key in keyMap.trigger.keys.mapNotNull { it as? KeyCodeTriggerKey }) { + for (key in keyMap.trigger.keys.mapNotNull { it as? KeyEventTriggerKey }) { val consumed = inputKeyEvent( 999, @@ -3317,11 +3775,11 @@ class KeyMapControllerTest { @TestCaseName("{0}") fun validInput_downConsumed(description: String, keyMap: KeyMap) = runTest(testDispatcher) { // GIVEN - keyMapListFlow.value = listOf(keyMap) + loadKeyMaps(keyMap) var consumedCount = 0 - for (key in keyMap.trigger.keys.mapNotNull { it as? KeyCodeTriggerKey }) { + for (key in keyMap.trigger.keys.mapNotNull { it as? KeyEventTriggerKey }) { val consumed = inputKeyEvent( key.keyCode, @@ -3342,11 +3800,11 @@ class KeyMapControllerTest { @TestCaseName("{0}") fun validInput_doNotConsumeFlag_doNotConsumeDown(description: String, keyMap: KeyMap) = runTest(testDispatcher) { - keyMapListFlow.value = listOf(keyMap) + loadKeyMaps(keyMap) var consumedCount = 0 - for (key in keyMap.trigger.keys.mapNotNull { it as? KeyCodeTriggerKey }) { + for (key in keyMap.trigger.keys.mapNotNull { it as? KeyEventTriggerKey }) { val consumed = inputKeyEvent( key.keyCode, @@ -3367,14 +3825,14 @@ class KeyMapControllerTest { "undefined single short-press this-device, do not consume" to singleKeyTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, consume = false, ), ), "undefined single long-press this-device, do not consume" to singleKeyTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, consume = false, ), @@ -3382,7 +3840,7 @@ class KeyMapControllerTest { "undefined single double-press this-device, do not consume" to singleKeyTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.DOUBLE_PRESS, consume = false, ), @@ -3391,14 +3849,14 @@ class KeyMapControllerTest { "undefined single short-press any-device, do not consume" to singleKeyTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, consume = false, ), ), "undefined single long-press any-device, do not consume" to singleKeyTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.LONG_PRESS, consume = false, ), @@ -3406,7 +3864,7 @@ class KeyMapControllerTest { "undefined single double-press any-device, do not consume" to singleKeyTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.DOUBLE_PRESS, consume = false, ), @@ -3415,19 +3873,19 @@ class KeyMapControllerTest { "sequence multiple short-press this-device, do not consume" to sequenceTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, consume = false, ), @@ -3436,19 +3894,19 @@ class KeyMapControllerTest { "sequence multiple long-press this-device, do not consume" to sequenceTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, consume = false, ), @@ -3457,19 +3915,19 @@ class KeyMapControllerTest { "sequence multiple double-press this-device, do not consume" to sequenceTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.DOUBLE_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.DOUBLE_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.DOUBLE_PRESS, consume = false, ), @@ -3478,19 +3936,19 @@ class KeyMapControllerTest { "sequence multiple mix this-device, do not consume" to sequenceTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.DOUBLE_PRESS, consume = false, ), @@ -3532,13 +3990,13 @@ class KeyMapControllerTest { ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.SHORT_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, consume = false, ), @@ -3553,13 +4011,13 @@ class KeyMapControllerTest { ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.LONG_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, consume = false, ), @@ -3574,13 +4032,13 @@ class KeyMapControllerTest { ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.DOUBLE_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.DOUBLE_PRESS, consume = false, ), @@ -3595,13 +4053,13 @@ class KeyMapControllerTest { ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.SHORT_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.DOUBLE_PRESS, consume = false, ), @@ -3610,7 +4068,7 @@ class KeyMapControllerTest { "sequence multiple mix mixed-device, do not consume" to sequenceTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.LONG_PRESS, consume = false, ), @@ -3622,7 +4080,7 @@ class KeyMapControllerTest { ), triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, consume = false, ), @@ -3637,19 +4095,19 @@ class KeyMapControllerTest { "parallel multiple short-press this-device, do not consume" to parallelTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, consume = false, ), @@ -3658,19 +4116,19 @@ class KeyMapControllerTest { "parallel multiple long-press this-device, do not consume" to parallelTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, consume = false, ), @@ -3721,13 +4179,13 @@ class KeyMapControllerTest { "parallel multiple short-press mix-device, do not consume" to parallelTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.SHORT_PRESS, consume = false, ), @@ -3742,13 +4200,13 @@ class KeyMapControllerTest { "parallel multiple long-press mix-device, do not consume" to parallelTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, consume = false, ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.LONG_PRESS, consume = false, ), @@ -3778,20 +4236,20 @@ class KeyMapControllerTest { "undefined single short-press this-device" to singleKeyTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, ), ), "undefined single long-press this-device" to singleKeyTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, ), ), "undefined single double-press this-device" to singleKeyTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.DOUBLE_PRESS, ), ), @@ -3799,20 +4257,20 @@ class KeyMapControllerTest { "undefined single short-press any-device" to singleKeyTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, ), ), "undefined single long-press any-device" to singleKeyTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.LONG_PRESS, ), ), "undefined single double-press any-device" to singleKeyTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.DOUBLE_PRESS, ), ), @@ -3820,68 +4278,68 @@ class KeyMapControllerTest { "sequence multiple short-press this-device" to sequenceTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, ), ), "sequence multiple long-press this-device" to sequenceTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, ), ), "sequence multiple double-press this-device" to sequenceTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.DOUBLE_PRESS, ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.DOUBLE_PRESS, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.DOUBLE_PRESS, ), ), "sequence multiple mix this-device" to sequenceTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.DOUBLE_PRESS, ), ), @@ -3916,12 +4374,12 @@ class KeyMapControllerTest { ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.SHORT_PRESS, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, ), ), @@ -3933,12 +4391,12 @@ class KeyMapControllerTest { ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.LONG_PRESS, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, ), ), @@ -3950,12 +4408,12 @@ class KeyMapControllerTest { ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.DOUBLE_PRESS, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.DOUBLE_PRESS, ), ), @@ -3967,19 +4425,19 @@ class KeyMapControllerTest { ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.SHORT_PRESS, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.DOUBLE_PRESS, ), ), "sequence multiple mix mixed-device" to sequenceTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.LONG_PRESS, ), triggerKey( @@ -3989,7 +4447,7 @@ class KeyMapControllerTest { ), triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, ), triggerKey( @@ -4002,34 +4460,34 @@ class KeyMapControllerTest { "parallel multiple short-press this-device" to parallelTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, ), ), "parallel multiple long-press this-device" to parallelTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, ), triggerKey( KeyEvent.KEYCODE_A, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, ), ), @@ -4070,12 +4528,12 @@ class KeyMapControllerTest { "parallel multiple short-press mix-device" to parallelTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.SHORT_PRESS, ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.SHORT_PRESS, ), triggerKey( @@ -4087,12 +4545,12 @@ class KeyMapControllerTest { "parallel multiple long-press mix-device" to parallelTrigger( triggerKey( KeyEvent.KEYCODE_VOLUME_DOWN, - TriggerKeyDevice.Internal, + KeyEventTriggerDevice.Internal, clickType = ClickType.LONG_PRESS, ), triggerKey( KeyEvent.KEYCODE_VOLUME_UP, - TriggerKeyDevice.Any, + KeyEventTriggerDevice.Any, clickType = ClickType.LONG_PRESS, ), triggerKey( @@ -4120,7 +4578,7 @@ class KeyMapControllerTest { @TestCaseName("{0}") fun validInput_actionPerformed(description: String, keyMap: KeyMap) = runTest(testDispatcher) { // GIVEN - keyMapListFlow.value = listOf(keyMap) + loadKeyMaps(keyMap) if (keyMap.trigger.mode is TriggerMode.Parallel) { // WHEN @@ -4140,37 +4598,76 @@ class KeyMapControllerTest { } private suspend fun mockTriggerKeyInput(key: TriggerKey, delay: Long? = null) { - if (key !is KeyCodeTriggerKey) { - return + if (key !is KeyEventTriggerKey) { + throw IllegalArgumentException("Key must be a KeyEventTriggerKey") } - val deviceDescriptor = triggerKeyDeviceToInputDevice(key.device) + val inputDevice = triggerKeyDeviceToInputDevice(key.device) val pressDuration: Long = delay ?: when (key.clickType) { ClickType.LONG_PRESS -> LONG_PRESS_DELAY + 100L else -> 50L } - inputKeyEvent(key.keyCode, KeyEvent.ACTION_DOWN, deviceDescriptor) + inputKeyEvent(key.keyCode, KeyEvent.ACTION_DOWN, inputDevice) when (key.clickType) { ClickType.SHORT_PRESS -> { delay(pressDuration) - inputKeyEvent(key.keyCode, KeyEvent.ACTION_UP, deviceDescriptor) + inputKeyEvent(key.keyCode, KeyEvent.ACTION_UP, inputDevice) } ClickType.LONG_PRESS -> { delay(pressDuration) - inputKeyEvent(key.keyCode, KeyEvent.ACTION_UP, deviceDescriptor) + inputKeyEvent(key.keyCode, KeyEvent.ACTION_UP, inputDevice) } ClickType.DOUBLE_PRESS -> { delay(pressDuration) - inputKeyEvent(key.keyCode, KeyEvent.ACTION_UP, deviceDescriptor) + inputKeyEvent(key.keyCode, KeyEvent.ACTION_UP, inputDevice) delay(pressDuration) - inputKeyEvent(key.keyCode, KeyEvent.ACTION_DOWN, deviceDescriptor) + inputKeyEvent(key.keyCode, KeyEvent.ACTION_DOWN, inputDevice) delay(pressDuration) - inputKeyEvent(key.keyCode, KeyEvent.ACTION_UP, deviceDescriptor) + inputKeyEvent(key.keyCode, KeyEvent.ACTION_UP, inputDevice) + } + } + } + + private suspend fun mockEvdevKeyInput( + key: TriggerKey, + evdevDevice: EvdevDeviceInfo, + delay: Long? = null, + ) { + if (key !is EvdevTriggerKey) { + throw IllegalArgumentException("Key must be an EvdevTriggerKey") + } + + val pressDuration: Long = delay ?: when (key.clickType) { + ClickType.LONG_PRESS -> LONG_PRESS_DELAY + 100L + else -> 50L + } + + inputDownEvdevEvent(key.keyCode, key.scanCode, evdevDevice) + + when (key.clickType) { + ClickType.SHORT_PRESS -> { + delay(pressDuration) + inputUpEvdevEvent(key.keyCode, key.scanCode, evdevDevice) + } + + ClickType.LONG_PRESS -> { + delay(pressDuration) + inputUpEvdevEvent(key.keyCode, key.scanCode, evdevDevice) + } + + ClickType.DOUBLE_PRESS -> { + delay(pressDuration) + inputUpEvdevEvent(key.keyCode, key.scanCode, evdevDevice) + delay(pressDuration) + + inputDownEvdevEvent(key.keyCode, key.scanCode, evdevDevice) + delay(pressDuration) + inputUpEvdevEvent(key.keyCode, key.scanCode, evdevDevice) } } } @@ -4179,14 +4676,13 @@ class KeyMapControllerTest { axisHatX: Float = 0.0f, axisHatY: Float = 0.0f, device: InputDeviceInfo = FAKE_CONTROLLER_INPUT_DEVICE, - isDpad: Boolean = true, - ): MyMotionEvent { - return MyMotionEvent( + ): KMGamePadEvent { + return KMGamePadEvent( metaState = 0, device = device, axisHatX = axisHatX, axisHatY = axisHatY, - isDpad = isDpad, + eventTime = System.currentTimeMillis(), ) } @@ -4195,24 +4691,24 @@ class KeyMapControllerTest { axisHatY: Float = 0.0f, device: InputDeviceInfo = FAKE_CONTROLLER_INPUT_DEVICE, ): Boolean = controller.onMotionEvent( - MyMotionEvent( + KMGamePadEvent( metaState = 0, device = device, axisHatX = axisHatX, axisHatY = axisHatY, - isDpad = true, + eventTime = System.currentTimeMillis(), ), ) private fun inputKeyEvent( keyCode: Int, action: Int, - device: InputDeviceInfo? = null, + device: InputDeviceInfo = FAKE_INTERNAL_DEVICE, metaState: Int? = null, scanCode: Int = 0, repeatCount: Int = 0, - ): Boolean = controller.onKeyEvent( - MyKeyEvent( + ): Boolean = controller.onInputEvent( + KMKeyEvent( keyCode = keyCode, action = action, metaState = metaState ?: 0, @@ -4220,6 +4716,51 @@ class KeyMapControllerTest { device = device, repeatCount = repeatCount, source = 0, + eventTime = System.currentTimeMillis(), + ), + ) + + private fun inputDownEvdevEvent( + keyCode: Int, + scanCode: Int, + device: EvdevDeviceInfo, + ): Boolean = controller.onInputEvent( + KMEvdevEvent( + type = KMEvdevEvent.TYPE_KEY_EVENT, + device = EvdevDeviceHandle( + path = "/dev/input${device.name}", + name = device.name, + bus = device.bus, + vendor = device.vendor, + product = device.product, + ), + code = scanCode, + androidCode = keyCode, + value = KMEvdevEvent.VALUE_DOWN, + timeSec = testScope.currentTime, + timeUsec = 0, + ), + ) + + private fun inputUpEvdevEvent( + keyCode: Int, + scanCode: Int, + device: EvdevDeviceInfo, + ): Boolean = controller.onInputEvent( + KMEvdevEvent( + type = KMEvdevEvent.TYPE_KEY_EVENT, + device = EvdevDeviceHandle( + path = "/dev/input${device.name}", + name = device.name, + bus = device.bus, + vendor = device.vendor, + product = device.product, + ), + code = scanCode, + androidCode = keyCode, + value = KMEvdevEvent.VALUE_UP, + timeSec = testScope.currentTime, + timeUsec = 0, ), ) @@ -4228,22 +4769,22 @@ class KeyMapControllerTest { delay: Long? = null, ) { require(trigger.mode is TriggerMode.Parallel) - require(trigger.keys.all { it is KeyCodeTriggerKey }) + require(trigger.keys.all { it is KeyEventTriggerKey }) for (key in trigger.keys) { - if (key !is KeyCodeTriggerKey) { + if (key !is KeyEventTriggerKey) { continue } - val deviceDescriptor = triggerKeyDeviceToInputDevice(key.device) + val inputDevice = triggerKeyDeviceToInputDevice(key.device) - inputKeyEvent(key.keyCode, KeyEvent.ACTION_DOWN, deviceDescriptor) + inputKeyEvent(key.keyCode, KeyEvent.ACTION_DOWN, inputDevice) } if (delay != null) { delay(delay) } else { - when ((trigger.mode as TriggerMode.Parallel).clickType) { + when (trigger.mode.clickType) { ClickType.SHORT_PRESS -> delay(50) ClickType.LONG_PRESS -> delay(LONG_PRESS_DELAY + 100L) ClickType.DOUBLE_PRESS -> {} @@ -4251,7 +4792,7 @@ class KeyMapControllerTest { } for (key in trigger.keys) { - if (key !is KeyCodeTriggerKey) { + if (key !is KeyEventTriggerKey) { continue } @@ -4262,32 +4803,35 @@ class KeyMapControllerTest { } private fun triggerKeyDeviceToInputDevice( - device: TriggerKeyDevice, + device: KeyEventTriggerDevice, deviceId: Int = 0, isGameController: Boolean = false, ): InputDeviceInfo = when (device) { - TriggerKeyDevice.Any -> InputDeviceInfo( + KeyEventTriggerDevice.Any -> InputDeviceInfo( descriptor = "any_device", name = "any_device_name", isExternal = false, id = deviceId, isGameController = isGameController, + sources = if (isGameController) InputDevice.SOURCE_GAMEPAD else InputDevice.SOURCE_KEYBOARD, ) - is TriggerKeyDevice.External -> InputDeviceInfo( + is KeyEventTriggerDevice.External -> InputDeviceInfo( descriptor = device.descriptor, name = "device_name", isExternal = true, id = deviceId, isGameController = isGameController, + sources = if (isGameController) InputDevice.SOURCE_GAMEPAD else InputDevice.SOURCE_KEYBOARD, ) - TriggerKeyDevice.Internal -> InputDeviceInfo( + KeyEventTriggerDevice.Internal -> InputDeviceInfo( descriptor = "internal_device", name = "internal_device_name", isExternal = false, id = deviceId, isGameController = isGameController, + sources = if (isGameController) InputDevice.SOURCE_GAMEPAD else InputDevice.SOURCE_KEYBOARD, ) } } diff --git a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/ProcessKeyMapGroupsForDetectionTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/ProcessKeyMapGroupsForDetectionTest.kt index abcd815259..97fb879f63 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/ProcessKeyMapGroupsForDetectionTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/ProcessKeyMapGroupsForDetectionTest.kt @@ -3,9 +3,9 @@ package io.github.sds100.keymapper.base.keymaps import io.github.sds100.keymapper.base.constraints.Constraint import io.github.sds100.keymapper.base.constraints.ConstraintMode import io.github.sds100.keymapper.base.constraints.ConstraintState +import io.github.sds100.keymapper.base.detection.DetectKeyMapModel +import io.github.sds100.keymapper.base.detection.DetectKeyMapsUseCaseImpl import io.github.sds100.keymapper.base.groups.Group -import io.github.sds100.keymapper.base.keymaps.detection.DetectKeyMapModel -import io.github.sds100.keymapper.base.keymaps.detection.DetectKeyMapsUseCaseImpl import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers import org.junit.Test diff --git a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/TriggerKeyMapFromOtherAppsControllerTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/TriggerKeyMapFromOtherAppsControllerTest.kt index 4b95335488..44cce647b7 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/keymaps/TriggerKeyMapFromOtherAppsControllerTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/keymaps/TriggerKeyMapFromOtherAppsControllerTest.kt @@ -6,8 +6,8 @@ import io.github.sds100.keymapper.base.actions.ActionErrorSnapshot import io.github.sds100.keymapper.base.actions.PerformActionsUseCase import io.github.sds100.keymapper.base.actions.RepeatMode import io.github.sds100.keymapper.base.constraints.DetectConstraintsUseCase -import io.github.sds100.keymapper.base.keymaps.detection.DetectKeyMapsUseCase -import io.github.sds100.keymapper.base.keymaps.detection.TriggerKeyMapFromOtherAppsController +import io.github.sds100.keymapper.base.detection.DetectKeyMapsUseCase +import io.github.sds100.keymapper.base.detection.TriggerKeyMapFromOtherAppsController import io.github.sds100.keymapper.base.trigger.Trigger import io.github.sds100.keymapper.base.utils.TestConstraintSnapshot import io.github.sds100.keymapper.common.utils.KMError diff --git a/base/src/test/java/io/github/sds100/keymapper/base/repositories/KeyMapRepositoryTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/repositories/KeyMapRepositoryTest.kt index fb53bae60e..9ec3715c80 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/repositories/KeyMapRepositoryTest.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/repositories/KeyMapRepositoryTest.kt @@ -6,7 +6,6 @@ import io.github.sds100.keymapper.data.db.dao.KeyMapDao import io.github.sds100.keymapper.data.entities.FingerprintMapEntity import io.github.sds100.keymapper.data.entities.KeyMapEntity import io.github.sds100.keymapper.data.repositories.RoomKeyMapRepository -import io.github.sds100.keymapper.system.devices.InputDeviceInfo import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -26,16 +25,6 @@ import org.mockito.kotlin.times @RunWith(MockitoJUnitRunner::class) class KeyMapRepositoryTest { - companion object { - private val FAKE_KEYBOARD = InputDeviceInfo( - descriptor = "fake_keyboard_descriptor", - name = "fake keyboard", - id = 1, - isExternal = true, - isGameController = false, - ) - } - private val testDispatcher = UnconfinedTestDispatcher() private val testScope = TestScope(testDispatcher) diff --git a/base/src/test/java/io/github/sds100/keymapper/base/system/devices/FakeDevicesAdapter.kt b/base/src/test/java/io/github/sds100/keymapper/base/system/devices/FakeDevicesAdapter.kt index ba2e23d056..298a040d79 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/system/devices/FakeDevicesAdapter.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/system/devices/FakeDevicesAdapter.kt @@ -1,10 +1,10 @@ package io.github.sds100.keymapper.base.system.devices +import io.github.sds100.keymapper.common.utils.InputDeviceInfo import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.State import io.github.sds100.keymapper.system.bluetooth.BluetoothDeviceInfo import io.github.sds100.keymapper.system.devices.DevicesAdapter -import io.github.sds100.keymapper.system.devices.InputDeviceInfo import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -31,4 +31,8 @@ class FakeDevicesAdapter : DevicesAdapter { override fun getInputDeviceName(descriptor: String): KMResult { throw Exception() } + + override fun getInputDevice(deviceId: Int): InputDeviceInfo? { + throw NotImplementedError() + } } diff --git a/base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegateTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegateTest.kt new file mode 100644 index 0000000000..312edb60aa --- /dev/null +++ b/base/src/test/java/io/github/sds100/keymapper/base/trigger/ConfigTriggerDelegateTest.kt @@ -0,0 +1,1390 @@ +package io.github.sds100.keymapper.base.trigger + +import android.view.KeyEvent +import io.github.sds100.keymapper.base.keymaps.ClickType +import io.github.sds100.keymapper.base.system.accessibility.FingerprintGestureType +import io.github.sds100.keymapper.base.utils.parallelTrigger +import io.github.sds100.keymapper.base.utils.sequenceTrigger +import io.github.sds100.keymapper.base.utils.singleKeyTrigger +import io.github.sds100.keymapper.base.utils.triggerKey +import io.github.sds100.keymapper.common.models.EvdevDeviceInfo +import io.github.sds100.keymapper.system.inputevents.Scancode +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.contains +import org.hamcrest.Matchers.hasSize +import org.hamcrest.Matchers.instanceOf +import org.hamcrest.Matchers.`is` +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.MockedStatic +import org.mockito.Mockito.mockStatic + +class ConfigTriggerDelegateTest { + + private lateinit var mockedKeyEvent: MockedStatic + private lateinit var delegate: ConfigTriggerDelegate + + @Before + fun before() { + mockedKeyEvent = mockStatic(KeyEvent::class.java) + mockedKeyEvent.`when` { KeyEvent.getMaxKeyCode() }.thenReturn(1000) + + delegate = ConfigTriggerDelegate() + } + + @After + fun tearDown() { + mockedKeyEvent.close() + } + + @Test + fun `set click type to long press when adding power button by key event to empty trigger`() { + val trigger = Trigger() + + val newTrigger = delegate.addKeyEventTriggerKey( + trigger, + keyCode = KeyEvent.KEYCODE_POWER, + scanCode = Scancode.KEY_POWER, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + assertThat(newTrigger.keys, hasSize(1)) + assertThat(newTrigger.keys[0].clickType, `is`(ClickType.LONG_PRESS)) + } + + @Test + fun `set click type to long press when adding KEY_POWER by evdev event to empty trigger`() { + val trigger = Trigger() + val device = EvdevDeviceInfo( + name = "Power Button", + bus = 0, + vendor = 1, + product = 2, + ) + + val newTrigger = delegate.addEvdevTriggerKey( + trigger, + keyCode = KeyEvent.KEYCODE_POWER, + scanCode = Scancode.KEY_POWER, + device = device, + ) + + assertThat(newTrigger.keys, hasSize(1)) + assertThat(newTrigger.keys[0].clickType, `is`(ClickType.LONG_PRESS)) + } + + @Test + fun `set click type to long press when adding KEY_POWER2 by evdev event to empty trigger`() { + val trigger = Trigger() + val device = EvdevDeviceInfo( + name = "Power Button", + bus = 0, + vendor = 1, + product = 2, + ) + + val newTrigger = delegate.addEvdevTriggerKey( + trigger, + keyCode = KeyEvent.KEYCODE_POWER, + scanCode = Scancode.KEY_POWER2, + device = device, + ) + + assertThat(newTrigger.keys, hasSize(1)) + assertThat(newTrigger.keys[0].clickType, `is`(ClickType.LONG_PRESS)) + } + + @Test + fun `set click type to long press when adding TV power button by evdev event to empty trigger`() { + val trigger = Trigger() + val device = EvdevDeviceInfo( + name = "TV Remote", + bus = 0, + vendor = 1, + product = 2, + ) + + val newTrigger = delegate.addEvdevTriggerKey( + trigger, + keyCode = KeyEvent.KEYCODE_TV_POWER, + scanCode = Scancode.KEY_POWER, + device = device, + ) + + assertThat(newTrigger.keys, hasSize(1)) + assertThat(newTrigger.keys[0].clickType, `is`(ClickType.LONG_PRESS)) + } + + @Test + fun `set click type to long press when adding power button to parallel trigger`() { + val trigger = parallelTrigger( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = true, + ), + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = Scancode.KEY_VOLUMEUP, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = true, + ), + ) + + val newTrigger = delegate.addKeyEventTriggerKey( + trigger, + keyCode = KeyEvent.KEYCODE_POWER, + scanCode = Scancode.KEY_POWER, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + assertThat(newTrigger.keys, hasSize(3)) + assertThat((newTrigger.mode as TriggerMode.Parallel).clickType, `is`(ClickType.LONG_PRESS)) + assertThat(newTrigger.keys[2].clickType, `is`(ClickType.LONG_PRESS)) + } + + @Test + fun `set click type to long press when adding power button to sequence trigger`() { + val trigger = sequenceTrigger( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = true, + ), + AssistantTriggerKey( + type = AssistantTriggerType.ANY, + clickType = ClickType.SHORT_PRESS, + ), + ) + + val newTrigger = delegate.addKeyEventTriggerKey( + trigger, + keyCode = KeyEvent.KEYCODE_POWER, + scanCode = Scancode.KEY_POWER, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + assertThat(newTrigger.keys, hasSize(3)) + assertThat(newTrigger.mode, `is`(TriggerMode.Sequence)) + assertThat(newTrigger.keys[2].clickType, `is`(ClickType.LONG_PRESS)) + } + + @Test + fun `Remove keys with the same scan code if scan code detection is enabled when switching to a parallel trigger`() { + val key = KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = true, + ) + + val trigger = sequenceTrigger( + key, + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = true, + ), + ) + + val newTrigger = delegate.setParallelTriggerMode(trigger) + + assertThat(newTrigger.mode, `is`(TriggerMode.Undefined)) + assertThat(newTrigger.keys, hasSize(1)) + assertThat(newTrigger.keys, contains(key)) + } + + @Test + fun `Convert to sequence trigger when enabling scan code detection when scan codes are the same`() { + val key = KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false, + ) + val trigger = parallelTrigger( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = true, + ), + key, + ) + + val newTrigger = delegate.setScanCodeDetectionEnabled(trigger, key.uid, true) + assertThat(newTrigger.mode, `is`(TriggerMode.Sequence)) + assertThat(newTrigger.keys, hasSize(2)) + assertThat(newTrigger.keys[1], `is`(key.copy(detectWithScanCodeUserSetting = true))) + } + + @Test + fun `Do not remove other keys with the same scan code when enabling scan code detection for sequence triggers`() { + val key = KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = true, + ) + val trigger = sequenceTrigger( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false, + ), + key, + ) + + val newTrigger = delegate.setScanCodeDetectionEnabled(trigger, key.uid, true) + assertThat(newTrigger.keys, hasSize(2)) + assertThat( + newTrigger.keys, + contains(trigger.keys[0], key.copy(detectWithScanCodeUserSetting = true)), + ) + } + + @Test + fun `Convert to sequence trigger when disabling scan code detection and other keys with same key code`() { + val key = KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEUP, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false, + ) + + val trigger = parallelTrigger( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false, + ), + key, + ) + + val newTrigger = delegate.setScanCodeDetectionEnabled(trigger, key.uid, false) + assertThat(newTrigger.mode, `is`(TriggerMode.Sequence)) + assertThat(newTrigger.keys, hasSize(2)) + assertThat(newTrigger.keys[1], `is`(key.copy(detectWithScanCodeUserSetting = false))) + } + + @Test + fun `Do not remove other keys from different devices when enabling scan code detection`() { + val key = KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.External(descriptor = "keyboard0", name = "Keyboard"), + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false, + ) + val trigger = parallelTrigger( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false, + ), + key, + ) + + val newTrigger = delegate.setScanCodeDetectionEnabled(trigger, key.uid, true) + assertThat(newTrigger.keys, hasSize(2)) + assertThat( + newTrigger.keys, + contains(trigger.keys[0], key.copy(detectWithScanCodeUserSetting = true)), + ) + } + + /** + * Issue #761 + */ + @Test + fun `Do not enable scan code detection if a key in another key map has the same key code, different scan code and is from a different device`() { + val device1 = KeyEventTriggerDevice.External( + descriptor = "keyboard0", + name = "Keyboard", + ) + + val device2 = KeyEventTriggerDevice.External( + descriptor = "keyboard1", + name = "Other Keyboard", + ) + + val otherTriggers = listOf( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = 123, + device = device1, + clickType = ClickType.SHORT_PRESS, + ), + ) + + val newTrigger = delegate.addKeyEventTriggerKey( + trigger = Trigger(), + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = 124, + // Different device + device = device2, + requiresIme = false, + otherTriggers, + ) + + assertThat( + (newTrigger.keys[0] as KeyEventTriggerKey).detectWithScanCodeUserSetting, + `is`(false), + ) + } + + /** + * Issue #761 + */ + @Test + fun `Do not enable scan code detection if a key in the trigger has the same key code, different scan code and is from a different device`() { + val device1 = KeyEventTriggerDevice.External( + descriptor = "keyboard0", + name = "Keyboard", + ) + + val device2 = KeyEventTriggerDevice.External( + descriptor = "keyboard1", + name = "Other Keyboard", + ) + + val trigger = singleKeyTrigger( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = 123, + device = device1, + clickType = ClickType.SHORT_PRESS, + ), + ) + + val newTrigger = delegate.addKeyEventTriggerKey( + trigger = trigger, + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = 124, + // Different device + device = device2, + requiresIme = false, + ) + + assertThat( + (newTrigger.keys[1] as KeyEventTriggerKey).detectWithScanCodeUserSetting, + `is`(false), + ) + } + + /** + * Issue #761 + */ + @Test + fun `Enable scan code detection for an evdev trigger if a key in another key map has the same key code but different scan code`() { + val device = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2, + ) + + val otherTriggers = listOf( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = 123, + device = device, + ), + ) + + val newTrigger = delegate.addEvdevTriggerKey( + trigger = Trigger(), + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = 124, + device = device, + otherTriggers, + ) + + assertThat( + (newTrigger.keys[0] as EvdevTriggerKey).detectWithScanCodeUserSetting, + `is`(true), + ) + } + + /** + * Issue #761 + */ + @Test + fun `Enable scan code detection for a key event trigger if a key in another key map has the same key code but different scan code`() { + val device = KeyEventTriggerDevice.External( + descriptor = "keyboard0", + name = "Keyboard", + ) + + val otherTriggers = listOf( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = 123, + device = device, + clickType = ClickType.SHORT_PRESS, + ), + ) + + val newTrigger = delegate.addKeyEventTriggerKey( + trigger = Trigger(), + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = 124, + device = device, + requiresIme = false, + otherTriggers, + ) + + assertThat( + (newTrigger.keys[0] as KeyEventTriggerKey).detectWithScanCodeUserSetting, + `is`(true), + ) + } + + /** + * Issue #761 + */ + @Test + fun `Enable scan code detection if another key event key exists in the trigger with the same key code but different scan code`() { + val device = KeyEventTriggerDevice.External( + descriptor = "keyboard0", + name = "Keyboard", + ) + + val trigger = singleKeyTrigger( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = 123, + device = device, + clickType = ClickType.SHORT_PRESS, + ), + ) + + val newTrigger = delegate.addKeyEventTriggerKey( + trigger = trigger, + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = 124, + device = device, + requiresIme = false, + ) + + assertThat( + (newTrigger.keys[1] as KeyEventTriggerKey).detectWithScanCodeUserSetting, + `is`(true), + ) + } + + /** + * Issue #761 + */ + @Test + fun `Enable scan code detection if another evdev key exists in the trigger with the same key code but different scan code`() { + val device = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2, + ) + + val trigger = singleKeyTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = 123, + device = device, + ), + ) + + val newTrigger = delegate.addEvdevTriggerKey( + trigger = trigger, + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = 124, + device = device, + ) + + assertThat( + (newTrigger.keys[1] as EvdevTriggerKey).detectWithScanCodeUserSetting, + `is`(true), + ) + } + + @Test + fun `Adding a non evdev key deletes all evdev keys in the trigger`() { + val trigger = parallelTrigger( + FloatingButtonKey( + buttonUid = "floating_button", + button = null, + clickType = ClickType.SHORT_PRESS, + ), + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = 123, + device = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2, + ), + ), + AssistantTriggerKey( + type = AssistantTriggerType.ANY, + clickType = ClickType.SHORT_PRESS, + ), + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 100, + device = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2, + ), + ), + ) + + val newTrigger = delegate.addKeyEventTriggerKey( + trigger, + KeyEvent.KEYCODE_VOLUME_DOWN, + 0, + KeyEventTriggerDevice.Internal, + false, + ) + + assertThat(newTrigger.keys, hasSize(3)) + assertThat(newTrigger.keys[0], instanceOf(FloatingButtonKey::class.java)) + assertThat(newTrigger.keys[1], instanceOf(AssistantTriggerKey::class.java)) + assertThat(newTrigger.keys[2], instanceOf(KeyEventTriggerKey::class.java)) + assertThat( + (newTrigger.keys[2] as KeyEventTriggerKey).requiresIme, + `is`(false), + ) + } + + @Test + fun `Adding an evdev key deletes all non evdev keys in the trigger`() { + val trigger = parallelTrigger( + FloatingButtonKey( + buttonUid = "floating_button", + button = null, + clickType = ClickType.SHORT_PRESS, + ), + triggerKey( + KeyEvent.KEYCODE_VOLUME_UP, + KeyEventTriggerDevice.Internal, + ), + AssistantTriggerKey( + type = AssistantTriggerType.ANY, + clickType = ClickType.SHORT_PRESS, + ), + triggerKey( + KeyEvent.KEYCODE_VOLUME_DOWN, + KeyEventTriggerDevice.Internal, + ), + ) + + val evdevDevice = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2, + ) + + val newTrigger = delegate.addEvdevTriggerKey( + trigger = trigger, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = evdevDevice, + ) + + assertThat(newTrigger.keys, hasSize(3)) + assertThat(newTrigger.keys[0], instanceOf(FloatingButtonKey::class.java)) + assertThat(newTrigger.keys[1], instanceOf(AssistantTriggerKey::class.java)) + assertThat(newTrigger.keys[2], instanceOf(EvdevTriggerKey::class.java)) + assertThat( + (newTrigger.keys[2] as EvdevTriggerKey).device, + `is`(evdevDevice), + ) + } + + @Test + fun `Converting a sequence trigger to parallel trigger removes duplicate evdev keys`() { + val trigger = sequenceTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2, + ), + ), + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2, + ), + ), + ) + + val newTrigger = delegate.setParallelTriggerMode(trigger) + + assertThat(newTrigger.keys, hasSize(1)) + assertThat(newTrigger.keys[0], instanceOf(EvdevTriggerKey::class.java)) + assertThat( + (newTrigger.keys[0] as EvdevTriggerKey).keyCode, + `is`(KeyEvent.KEYCODE_VOLUME_DOWN), + ) + } + + @Test + fun `Adding the same evdev trigger key from same device makes the trigger a sequence`() { + val emptyTrigger = Trigger() + val device = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2, + ) + + val triggerWithFirstKey = delegate.addEvdevTriggerKey( + trigger = emptyTrigger, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = device, + ) + + val triggerWithSecondKey = delegate.addEvdevTriggerKey( + trigger = triggerWithFirstKey, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = device, + ) + + assertThat(triggerWithSecondKey.mode, `is`(TriggerMode.Sequence)) + } + + @Test + fun `Adding a key which has the same scan code as another key with scan code detection enabled makes the trigger a sequence`() { + val device = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2, + ) + + val trigger = parallelTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + clickType = ClickType.SHORT_PRESS, + device = device, + detectWithScanCodeUserSetting = true, + ), + AssistantTriggerKey(type = AssistantTriggerType.ANY, clickType = ClickType.SHORT_PRESS), + ) + + val newTrigger = delegate.addEvdevTriggerKey( + trigger = trigger, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = device, + ) + + assertThat(newTrigger.mode, `is`(TriggerMode.Sequence)) + } + + @Test + fun `Adding a key which has the same scan code as the only other key with scan code detection enabled makes the trigger a sequence`() { + val device = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2, + ) + + val trigger = singleKeyTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + clickType = ClickType.SHORT_PRESS, + device = device, + detectWithScanCodeUserSetting = true, + ), + ) + + val newTrigger = delegate.addEvdevTriggerKey( + trigger = trigger, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = device, + ) + + assertThat(newTrigger.mode, `is`(TriggerMode.Sequence)) + } + + @Test + fun `Adding an evdev trigger key to a sequence trigger keeps it sequence`() { + val device = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2, + ) + + val trigger = sequenceTrigger( + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = device, + ), + EvdevTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = device, + ), + ) + + // Add a third key and it should still be a sequence trigger now + val newTrigger = delegate.addEvdevTriggerKey( + trigger = trigger, + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = 0, + device = device, + ) + + assertThat(newTrigger.mode, `is`(TriggerMode.Sequence)) + } + + @Test + fun `Adding the same evdev trigger key code from different devices keeps the trigger parallel`() { + val emptyTrigger = Trigger() + val device1 = EvdevDeviceInfo( + name = "Volume Keys", + bus = 0, + vendor = 1, + product = 2, + ) + val device2 = EvdevDeviceInfo( + name = "Fake Controller", + bus = 1, + vendor = 2, + product = 1, + ) + + val triggerWithFirstKey = delegate.addEvdevTriggerKey( + trigger = emptyTrigger, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = device1, + ) + + val triggerWithSecondKey = delegate.addEvdevTriggerKey( + trigger = triggerWithFirstKey, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = device2, + ) + + assertThat(triggerWithSecondKey.mode, `is`(TriggerMode.Parallel(ClickType.SHORT_PRESS))) + } + + @Test + fun `Do not allow setting double press for parallel trigger with side key`() { + val emptyTrigger = Trigger() + + val triggerWithKeyEvent = delegate.addKeyEventTriggerKey( + trigger = emptyTrigger, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + val triggerWithAssistant = delegate.addAssistantTriggerKey( + trigger = triggerWithKeyEvent, + type = AssistantTriggerType.ANY, + ) + + val finalTrigger = delegate.setTriggerDoublePress(triggerWithAssistant) + + assertThat(finalTrigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + assertThat(finalTrigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) + assertThat(finalTrigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) + } + + @Test + fun `Do not allow setting long press for parallel trigger with side key`() { + val emptyTrigger = Trigger() + + val triggerWithKeyEvent = delegate.addKeyEventTriggerKey( + trigger = emptyTrigger, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + val triggerWithAssistant = delegate.addAssistantTriggerKey( + trigger = triggerWithKeyEvent, + type = AssistantTriggerType.ANY, + ) + + val finalTrigger = delegate.setTriggerLongPress(triggerWithAssistant) + + assertThat(finalTrigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + assertThat(finalTrigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) + assertThat(finalTrigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) + } + + @Test + fun `Do not allow setting double press for side key`() { + val emptyTrigger = Trigger() + + val triggerWithAssistant = delegate.addAssistantTriggerKey( + trigger = emptyTrigger, + type = AssistantTriggerType.ANY, + ) + + val finalTrigger = delegate.setTriggerDoublePress(triggerWithAssistant) + + assertThat(finalTrigger.mode, `is`(TriggerMode.Undefined)) + assertThat(finalTrigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) + } + + @Test + fun `Do not allow setting long press for side key`() { + val emptyTrigger = Trigger() + + val triggerWithAssistant = delegate.addAssistantTriggerKey( + trigger = emptyTrigger, + type = AssistantTriggerType.ANY, + ) + + val finalTrigger = delegate.setTriggerLongPress(triggerWithAssistant) + + assertThat(finalTrigger.mode, `is`(TriggerMode.Undefined)) + assertThat(finalTrigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) + } + + @Test + fun `Set click type to short press if side key added to double press volume button`() { + val emptyTrigger = Trigger() + + val triggerWithKeyEvent = delegate.addKeyEventTriggerKey( + trigger = emptyTrigger, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + val triggerWithDoublePress = delegate.setTriggerDoublePress(triggerWithKeyEvent) + + val finalTrigger = delegate.addAssistantTriggerKey( + trigger = triggerWithDoublePress, + type = AssistantTriggerType.ANY, + ) + + assertThat(finalTrigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + assertThat(finalTrigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) + assertThat(finalTrigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) + } + + @Test + fun `Set click type to short press if fingerprint gestures added to double press volume button`() { + val emptyTrigger = Trigger() + + val triggerWithKeyEvent = delegate.addKeyEventTriggerKey( + trigger = emptyTrigger, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + val triggerWithDoublePress = delegate.setTriggerDoublePress(triggerWithKeyEvent) + + val finalTrigger = delegate.addFingerprintGesture( + trigger = triggerWithDoublePress, + type = FingerprintGestureType.SWIPE_UP, + ) + + assertThat(finalTrigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + assertThat(finalTrigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) + assertThat(finalTrigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) + } + + @Test + fun `Set click type to short press if side key added to long press volume button`() { + val emptyTrigger = Trigger() + + val triggerWithKeyEvent = delegate.addKeyEventTriggerKey( + trigger = emptyTrigger, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + val triggerWithLongPress = delegate.setTriggerLongPress(triggerWithKeyEvent) + + val finalTrigger = delegate.addAssistantTriggerKey( + trigger = triggerWithLongPress, + type = AssistantTriggerType.ANY, + ) + + assertThat(finalTrigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + assertThat(finalTrigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) + assertThat(finalTrigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) + } + + @Test + fun `Set click type to short press if fingerprint gestures added to long press volume button`() { + val emptyTrigger = Trigger() + + val triggerWithKeyEvent = delegate.addKeyEventTriggerKey( + trigger = emptyTrigger, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + val triggerWithLongPress = delegate.setTriggerLongPress(triggerWithKeyEvent) + + val finalTrigger = delegate.addFingerprintGesture( + trigger = triggerWithLongPress, + type = FingerprintGestureType.SWIPE_UP, + ) + + assertThat(finalTrigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + assertThat(finalTrigger.keys[0].clickType, `is`(ClickType.SHORT_PRESS)) + assertThat(finalTrigger.keys[1].clickType, `is`(ClickType.SHORT_PRESS)) + } + + /** + * This ensures that it isn't possible to have two or more assistant triggers when the mode is parallel. + */ + @Test + fun `Remove device assistant trigger if setting mode to parallel and voice assistant already exists`() { + val emptyTrigger = Trigger() + + val triggerWithKeyEvent = delegate.addKeyEventTriggerKey( + trigger = emptyTrigger, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + val triggerWithVoiceAssistant = delegate.addAssistantTriggerKey( + trigger = triggerWithKeyEvent, + type = AssistantTriggerType.VOICE, + ) + + val triggerWithDeviceAssistant = delegate.addAssistantTriggerKey( + trigger = triggerWithVoiceAssistant, + type = AssistantTriggerType.DEVICE, + ) + + val finalTrigger = delegate.setParallelTriggerMode(triggerWithDeviceAssistant) + + assertThat(finalTrigger.keys, hasSize(2)) + assertThat( + finalTrigger.keys[0], + instanceOf(KeyEventTriggerKey::class.java), + ) + assertThat(finalTrigger.keys[1], instanceOf(AssistantTriggerKey::class.java)) + } + + @Test + fun `Remove voice assistant trigger if setting mode to parallel and device assistant already exists`() { + val emptyTrigger = Trigger() + + val triggerWithKeyEvent = delegate.addKeyEventTriggerKey( + trigger = emptyTrigger, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + val triggerWithDeviceAssistant = delegate.addAssistantTriggerKey( + trigger = triggerWithKeyEvent, + type = AssistantTriggerType.DEVICE, + ) + + val triggerWithVoiceAssistant = delegate.addAssistantTriggerKey( + trigger = triggerWithDeviceAssistant, + type = AssistantTriggerType.VOICE, + ) + + val finalTrigger = delegate.setParallelTriggerMode(triggerWithVoiceAssistant) + + assertThat(finalTrigger.keys, hasSize(2)) + assertThat( + finalTrigger.keys[0], + instanceOf(KeyEventTriggerKey::class.java), + ) + assertThat(finalTrigger.keys[1], instanceOf(AssistantTriggerKey::class.java)) + } + + @Test + fun `Set click type to short press when adding assistant key to multiple long press trigger keys`() { + val emptyTrigger = Trigger() + + val triggerWithFirstKey = delegate.addKeyEventTriggerKey( + trigger = emptyTrigger, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + val triggerWithSecondKey = delegate.addKeyEventTriggerKey( + trigger = triggerWithFirstKey, + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = 0, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + val triggerWithLongPress = delegate.setTriggerLongPress(triggerWithSecondKey) + + val finalTrigger = delegate.addAssistantTriggerKey( + trigger = triggerWithLongPress, + type = AssistantTriggerType.ANY, + ) + + assertThat(finalTrigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + } + + @Test + fun `Set click type to short press when adding assistant key to double press trigger key`() { + val emptyTrigger = Trigger() + + val triggerWithKeyEvent = delegate.addKeyEventTriggerKey( + trigger = emptyTrigger, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + val triggerWithDoublePress = delegate.setTriggerDoublePress(triggerWithKeyEvent) + + val finalTrigger = delegate.addAssistantTriggerKey( + trigger = triggerWithDoublePress, + type = AssistantTriggerType.ANY, + ) + + assertThat(finalTrigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + } + + @Test + fun `Set click type to short press when adding assistant key to long press trigger key`() { + val emptyTrigger = Trigger() + + val triggerWithKeyEvent = delegate.addKeyEventTriggerKey( + trigger = emptyTrigger, + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = 0, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + val triggerWithLongPress = delegate.setTriggerLongPress(triggerWithKeyEvent) + + val finalTrigger = delegate.addAssistantTriggerKey( + trigger = triggerWithLongPress, + type = AssistantTriggerType.ANY, + ) + + assertThat(finalTrigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + } + + @Test + fun `Do not allow long press for parallel trigger with assistant key`() { + val trigger = Trigger( + mode = TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS), + keys = listOf( + triggerKey(KeyEvent.KEYCODE_VOLUME_DOWN), + AssistantTriggerKey( + type = AssistantTriggerType.ANY, + clickType = ClickType.SHORT_PRESS, + ), + ), + ) + + val finalTrigger = delegate.setTriggerLongPress(trigger) + + assertThat(finalTrigger.mode, `is`(TriggerMode.Parallel(clickType = ClickType.SHORT_PRESS))) + } + + /** + * Issue #753. If a modifier key is used as a trigger then it the + * option to not override the default action must be chosen so that the modifier + * key can still be used normally. + */ + @Test + fun `when add modifier key trigger, enable do not remap option`() { + val modifierKeys = setOf( + KeyEvent.KEYCODE_SHIFT_LEFT, + KeyEvent.KEYCODE_SHIFT_RIGHT, + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_ALT_RIGHT, + KeyEvent.KEYCODE_CTRL_LEFT, + KeyEvent.KEYCODE_CTRL_RIGHT, + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_META_RIGHT, + KeyEvent.KEYCODE_SYM, + KeyEvent.KEYCODE_NUM, + KeyEvent.KEYCODE_FUNCTION, + ) + + for (modifierKeyCode in modifierKeys) { + // GIVEN + val emptyTrigger = Trigger() + + // WHEN + val trigger = delegate.addKeyEventTriggerKey( + trigger = emptyTrigger, + keyCode = modifierKeyCode, + scanCode = 0, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + // THEN + assertThat((trigger.keys[0] as KeyEventTriggerKey).consumeEvent, `is`(false)) + } + } + + /** + * Issue #753. + */ + @Test + fun `when add non-modifier key trigger, do not enable do not remap option`() { + // GIVEN + val emptyTrigger = Trigger() + + // WHEN + val trigger = delegate.addKeyEventTriggerKey( + trigger = emptyTrigger, + keyCode = KeyEvent.KEYCODE_A, + scanCode = 0, + device = KeyEventTriggerDevice.Internal, + requiresIme = false, + ) + + // THEN + assertThat((trigger.keys[0] as KeyEventTriggerKey).consumeEvent, `is`(true)) + } + + @Test + fun `Remove keys with same key code from the same internal device when converting to a parallel trigger`() { + val key = KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false, + ) + + val trigger = sequenceTrigger( + key, + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false, + ), + ) + + val newTrigger = delegate.setParallelTriggerMode(trigger) + assertThat(newTrigger.keys, hasSize(1)) + assertThat(newTrigger.keys, contains(key)) + } + + @Test + fun `Do not remove keys with same key code from different devices when converting to a parallel trigger`() { + val trigger = sequenceTrigger( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false, + ), + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.External( + descriptor = "keyboard0", + name = "Keyboard", + ), + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false, + ), + ) + + val newTrigger = delegate.setParallelTriggerMode(trigger) + assertThat(newTrigger.keys, hasSize(2)) + assertThat(newTrigger.keys, `is`(trigger.keys)) + } + + @Test + fun `Do not remove keys with different key code from the same device when converting to a parallel trigger`() { + val trigger = sequenceTrigger( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_UP, + scanCode = Scancode.KEY_VOLUMEUP, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false, + ), + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false, + ), + ) + + val newTrigger = delegate.setParallelTriggerMode(trigger) + assertThat(newTrigger.keys, hasSize(2)) + assertThat(newTrigger.keys, `is`(trigger.keys)) + } + + @Test + fun `Remove keys from an internal device if it conflicts with any-device key when converting to a parallel trigger`() { + val internalKey = KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false, + ) + + val anyDeviceKey = KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Any, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false, + ) + + val trigger = sequenceTrigger(internalKey, anyDeviceKey) + + val newTrigger = delegate.setParallelTriggerMode(trigger) + assertThat(newTrigger.keys, hasSize(1)) + assertThat(newTrigger.keys, contains(internalKey)) + } + + @Test + fun `Remove keys with the same key code from the same external device when converting to a parallel trigger`() { + val key = KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.External( + descriptor = "keyboard0", + name = "Keyboard", + ), + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false, + ) + + val trigger = sequenceTrigger( + key, + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.External( + descriptor = "keyboard0", + name = "Keyboard", + ), + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false, + ), + ) + + val newTrigger = delegate.setParallelTriggerMode(trigger) + assertThat(newTrigger.keys, hasSize(1)) + assertThat(newTrigger.keys, contains(key)) + } + + @Test + fun `Remove conflicting keys that are all any-device or internal when converting to a parallel trigger`() { + val trigger = sequenceTrigger( + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Any, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false, + ), + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Any, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false, + ), + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false, + ), + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.External( + descriptor = "keyboard0", + name = "Keyboard", + ), + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false, + ), + KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Any, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false, + ), + ) + + val newTrigger = delegate.setParallelTriggerMode(trigger) + assertThat(newTrigger.keys, hasSize(1)) + assertThat(newTrigger.keys, contains(trigger.keys[0])) + } +} diff --git a/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyDeviceTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyDeviceTest.kt new file mode 100644 index 0000000000..2543222291 --- /dev/null +++ b/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyDeviceTest.kt @@ -0,0 +1,63 @@ +package io.github.sds100.keymapper.base.trigger + +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.`is` +import org.junit.Test + +class TriggerKeyDeviceTest { + @Test + fun `external device is same as another external device`() { + val device1 = KeyEventTriggerDevice.External("keyboard0", "Keyboard 0") + val device2 = KeyEventTriggerDevice.External("keyboard0", "Keyboard 0") + assertThat(device1.isSameDevice(device2), `is`(true)) + } + + @Test + fun `external device is not the same as a different external device`() { + val device1 = KeyEventTriggerDevice.External("keyboard0", "Keyboard 0") + val device2 = KeyEventTriggerDevice.External("keyboard1", "Keyboard 1") + assertThat(device1.isSameDevice(device2), `is`(false)) + } + + @Test + fun `external device is not the same as a different external device with the same name`() { + val device1 = KeyEventTriggerDevice.External("keyboard0", "Keyboard 0") + val device2 = KeyEventTriggerDevice.External("keyboard1", "Keyboard 0") + assertThat(device1.isSameDevice(device2), `is`(false)) + } + + @Test + fun `internal device is not the same as a an external`() { + val device1 = KeyEventTriggerDevice.Internal + val device2 = KeyEventTriggerDevice.External("keyboard1", "Keyboard 0") + assertThat(device1.isSameDevice(device2), `is`(false)) + } + + @Test + fun `internal device is the same as an internal device`() { + val device1 = KeyEventTriggerDevice.Internal + val device2 = KeyEventTriggerDevice.Internal + assertThat(device1.isSameDevice(device2), `is`(true)) + } + + @Test + fun `any device is the same as an internal device`() { + val device1 = KeyEventTriggerDevice.Any + val device2 = KeyEventTriggerDevice.Internal + assertThat(device1.isSameDevice(device2), `is`(true)) + } + + @Test + fun `any device is the same as any device`() { + val device1 = KeyEventTriggerDevice.Any + val device2 = KeyEventTriggerDevice.Any + assertThat(device1.isSameDevice(device2), `is`(true)) + } + + @Test + fun `any device is the same as an external device`() { + val device1 = KeyEventTriggerDevice.Any + val device2 = KeyEventTriggerDevice.External("keyboard1", "Keyboard 0") + assertThat(device1.isSameDevice(device2), `is`(true)) + } +} diff --git a/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyTest.kt b/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyTest.kt new file mode 100644 index 0000000000..060313fb27 --- /dev/null +++ b/base/src/test/java/io/github/sds100/keymapper/base/trigger/TriggerKeyTest.kt @@ -0,0 +1,124 @@ +package io.github.sds100.keymapper.base.trigger + +import android.view.KeyEvent +import io.github.sds100.keymapper.base.keymaps.ClickType +import io.github.sds100.keymapper.system.inputevents.Scancode +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.`is` +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.MockedStatic +import org.mockito.Mockito.mockStatic + +class TriggerKeyTest { + + private lateinit var mockedKeyEvent: MockedStatic + + @Before + fun setUp() { + mockedKeyEvent = mockStatic(KeyEvent::class.java) + mockedKeyEvent.`when` { KeyEvent.getMaxKeyCode() }.thenReturn(1000) + } + + @After + fun tearDown() { + mockedKeyEvent.close() + } + + @Test + fun `User can not change scan code detection if the scan code is null`() { + val triggerKey = KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = null, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = true, + ) + assertThat(triggerKey.isScanCodeDetectionUserConfigurable(), `is`(false)) + } + + @Test + fun `User can not change scan code detection if the key code is unknown and scan code is non null`() { + val triggerKey = KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_UNKNOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = true, + ) + assertThat(triggerKey.isScanCodeDetectionUserConfigurable(), `is`(false)) + } + + @Test + fun `User can change scan code detection if the key code is known and scan code is non null`() { + val triggerKey = KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = true, + ) + assertThat(triggerKey.isScanCodeDetectionUserConfigurable(), `is`(true)) + } + + @Test + fun `detect with scan code if key code is unknown and user setting enabled`() { + val triggerKey = KeyEventTriggerKey( + keyCode = 0, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = true, + ) + assertThat(triggerKey.detectWithScancode(), `is`(true)) + } + + @Test + fun `detect with scan code if key code is unknown and user setting disabled`() { + val triggerKey = KeyEventTriggerKey( + keyCode = 0, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false, + ) + assertThat(triggerKey.detectWithScancode(), `is`(true)) + } + + @Test + fun `detect with scan code if user setting enabled and scan code non null`() { + val triggerKey = KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = true, + ) + assertThat(triggerKey.detectWithScancode(), `is`(true)) + } + + @Test + fun `detect with key code if user setting enabled and scan code is null`() { + val triggerKey = KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = null, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = true, + ) + assertThat(triggerKey.detectWithScancode(), `is`(false)) + } + + @Test + fun `detect with key code if user setting false and key code is known`() { + val triggerKey = KeyEventTriggerKey( + keyCode = KeyEvent.KEYCODE_VOLUME_DOWN, + scanCode = Scancode.KEY_VOLUMEDOWN, + device = KeyEventTriggerDevice.Internal, + clickType = ClickType.SHORT_PRESS, + detectWithScanCodeUserSetting = false, + ) + assertThat(triggerKey.detectWithScancode(), `is`(false)) + } +} diff --git a/base/src/test/java/io/github/sds100/keymapper/base/utils/KeyMapUtils.kt b/base/src/test/java/io/github/sds100/keymapper/base/utils/KeyMapUtils.kt index cacadd68b9..a7aaf7c89e 100644 --- a/base/src/test/java/io/github/sds100/keymapper/base/utils/KeyMapUtils.kt +++ b/base/src/test/java/io/github/sds100/keymapper/base/utils/KeyMapUtils.kt @@ -1,11 +1,10 @@ package io.github.sds100.keymapper.base.utils import io.github.sds100.keymapper.base.keymaps.ClickType -import io.github.sds100.keymapper.base.trigger.KeyCodeTriggerKey -import io.github.sds100.keymapper.base.trigger.KeyEventDetectionSource +import io.github.sds100.keymapper.base.trigger.KeyEventTriggerDevice +import io.github.sds100.keymapper.base.trigger.KeyEventTriggerKey import io.github.sds100.keymapper.base.trigger.Trigger import io.github.sds100.keymapper.base.trigger.TriggerKey -import io.github.sds100.keymapper.base.trigger.TriggerKeyDevice import io.github.sds100.keymapper.base.trigger.TriggerMode fun singleKeyTrigger(key: TriggerKey): Trigger = Trigger( @@ -25,14 +24,14 @@ fun sequenceTrigger(vararg keys: TriggerKey): Trigger = Trigger( fun triggerKey( keyCode: Int, - device: TriggerKeyDevice = TriggerKeyDevice.Internal, + device: KeyEventTriggerDevice = KeyEventTriggerDevice.Internal, clickType: ClickType = ClickType.SHORT_PRESS, consume: Boolean = true, - detectionSource: KeyEventDetectionSource = KeyEventDetectionSource.ACCESSIBILITY_SERVICE, -): KeyCodeTriggerKey = KeyCodeTriggerKey( + requiresIme: Boolean = false, +): KeyEventTriggerKey = KeyEventTriggerKey( keyCode = keyCode, device = device, clickType = clickType, consumeEvent = consume, - detectionSource = detectionSource, + requiresIme = requiresIme, ) diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 4ca3da941b..820f93c8fc 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -5,6 +5,7 @@ plugins { alias(libs.plugins.kotlin.serialization) alias(libs.plugins.google.devtools.ksp) alias(libs.plugins.dagger.hilt.android) + alias(libs.plugins.kotlin.parcelize) } android { @@ -25,6 +26,11 @@ android { ) } } + + buildFeatures { + aidl = true + } + compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 @@ -43,6 +49,7 @@ dependencies { // kotlin stuff implementation(libs.kotlinx.coroutines.android) implementation(libs.kotlinx.serialization.json) + implementation(libs.jakewharton.timber) implementation(libs.androidx.core.ktx) implementation(libs.dagger.hilt.android) diff --git a/common/src/main/aidl/io/github/sds100/keymapper/common/models/EvdevDeviceHandle.aidl b/common/src/main/aidl/io/github/sds100/keymapper/common/models/EvdevDeviceHandle.aidl new file mode 100644 index 0000000000..8ea6d7f489 --- /dev/null +++ b/common/src/main/aidl/io/github/sds100/keymapper/common/models/EvdevDeviceHandle.aidl @@ -0,0 +1,3 @@ +package io.github.sds100.keymapper.common.models; + +parcelable EvdevDeviceHandle; \ No newline at end of file diff --git a/common/src/main/java/io/github/sds100/keymapper/common/models/EvdevDeviceHandle.kt b/common/src/main/java/io/github/sds100/keymapper/common/models/EvdevDeviceHandle.kt new file mode 100644 index 0000000000..1b14c03b86 --- /dev/null +++ b/common/src/main/java/io/github/sds100/keymapper/common/models/EvdevDeviceHandle.kt @@ -0,0 +1,16 @@ +package io.github.sds100.keymapper.common.models + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class EvdevDeviceHandle( + /** + * The path to the device. E.g /dev/input/event1 + */ + val path: String, + val name: String, + val bus: Int, + val vendor: Int, + val product: Int, +) : Parcelable diff --git a/common/src/main/java/io/github/sds100/keymapper/common/models/EvdevDeviceInfo.kt b/common/src/main/java/io/github/sds100/keymapper/common/models/EvdevDeviceInfo.kt new file mode 100644 index 0000000000..1150b3c45e --- /dev/null +++ b/common/src/main/java/io/github/sds100/keymapper/common/models/EvdevDeviceInfo.kt @@ -0,0 +1,12 @@ +package io.github.sds100.keymapper.common.models + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class EvdevDeviceInfo( + val name: String, + val bus: Int, + val vendor: Int, + val product: Int, +) : Parcelable diff --git a/common/src/main/java/io/github/sds100/keymapper/common/notifications/KMNotificationAction.kt b/common/src/main/java/io/github/sds100/keymapper/common/notifications/KMNotificationAction.kt new file mode 100644 index 0000000000..bf28ed29c7 --- /dev/null +++ b/common/src/main/java/io/github/sds100/keymapper/common/notifications/KMNotificationAction.kt @@ -0,0 +1,46 @@ +package io.github.sds100.keymapper.common.notifications + +sealed class KMNotificationAction { + enum class IntentAction { + RESUME_KEY_MAPS, + PAUSE_KEY_MAPS, + PAIRING_CODE_REPLY, + DISMISS_TOGGLE_KEY_MAPS_NOTIFICATION, + STOP_ACCESSIBILITY_SERVICE, + START_ACCESSIBILITY_SERVICE, + RESTART_ACCESSIBILITY_SERVICE, + TOGGLE_KEY_MAPPER_IME, + SHOW_KEYBOARD, + } + + sealed class Broadcast(val intentAction: IntentAction) : KMNotificationAction() { + data object ResumeKeyMaps : Broadcast(IntentAction.RESUME_KEY_MAPS) + data object PauseKeyMaps : Broadcast(IntentAction.PAUSE_KEY_MAPS) + data object DismissToggleKeyMapsNotification : + Broadcast(IntentAction.DISMISS_TOGGLE_KEY_MAPS_NOTIFICATION) + + data object StopAccessibilityService : Broadcast(IntentAction.STOP_ACCESSIBILITY_SERVICE) + data object StartAccessibilityService : Broadcast(IntentAction.START_ACCESSIBILITY_SERVICE) + data object RestartAccessibilityService : + Broadcast(IntentAction.RESTART_ACCESSIBILITY_SERVICE) + + data object TogglerKeyMapperIme : Broadcast(IntentAction.TOGGLE_KEY_MAPPER_IME) + data object ShowKeyboard : Broadcast(IntentAction.SHOW_KEYBOARD) + } + + sealed class RemoteInput( + val key: String, + val intentAction: IntentAction, + ) : KMNotificationAction() { + + data object PairingCode : RemoteInput( + key = "pairing_code", + intentAction = IntentAction.PAIRING_CODE_REPLY, + ) + } + + sealed class Activity() : KMNotificationAction() { + data object AccessibilitySettings : Activity() + data class MainActivity(val action: String? = null) : Activity() + } +} diff --git a/system/src/main/java/io/github/sds100/keymapper/system/devices/InputDeviceInfo.kt b/common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceInfo.kt similarity index 61% rename from system/src/main/java/io/github/sds100/keymapper/system/devices/InputDeviceInfo.kt rename to common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceInfo.kt index c66b126bcc..e5605a3f8e 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/devices/InputDeviceInfo.kt +++ b/common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceInfo.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.system.devices +package io.github.sds100.keymapper.common.utils import android.os.Parcelable import kotlinx.parcelize.Parcelize @@ -12,4 +12,9 @@ data class InputDeviceInfo( val id: Int, val isExternal: Boolean, val isGameController: Boolean, -) : Parcelable + val sources: Int, +) : Parcelable { + fun supportsSource(source: Int): Boolean { + return sources and source == source + } +} diff --git a/system/src/main/java/io/github/sds100/keymapper/system/devices/InputDeviceUtils.kt b/common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceUtils.kt similarity index 92% rename from system/src/main/java/io/github/sds100/keymapper/system/devices/InputDeviceUtils.kt rename to common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceUtils.kt index b81d75f2ea..7c38413e92 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/devices/InputDeviceUtils.kt +++ b/common/src/main/java/io/github/sds100/keymapper/common/utils/InputDeviceUtils.kt @@ -1,4 +1,4 @@ -package io.github.sds100.keymapper.system.devices +package io.github.sds100.keymapper.common.utils import android.os.Build import android.view.InputDevice @@ -15,6 +15,7 @@ object InputDeviceUtils { inputDevice.id, inputDevice.isExternalCompat, isGameController = inputDevice.controllerNumber != 0, + sources = inputDevice.sources, ) } diff --git a/common/src/main/java/io/github/sds100/keymapper/common/utils/InputEventType.kt b/common/src/main/java/io/github/sds100/keymapper/common/utils/InputEventAction.kt similarity index 73% rename from common/src/main/java/io/github/sds100/keymapper/common/utils/InputEventType.kt rename to common/src/main/java/io/github/sds100/keymapper/common/utils/InputEventAction.kt index e3805172d5..0eab15976d 100644 --- a/common/src/main/java/io/github/sds100/keymapper/common/utils/InputEventType.kt +++ b/common/src/main/java/io/github/sds100/keymapper/common/utils/InputEventAction.kt @@ -1,6 +1,6 @@ package io.github.sds100.keymapper.common.utils -enum class InputEventType { +enum class InputEventAction { DOWN_UP, DOWN, UP, diff --git a/system/src/main/java/io/github/sds100/keymapper/system/SettingsUtils.kt b/common/src/main/java/io/github/sds100/keymapper/common/utils/SettingsUtils.kt similarity index 73% rename from system/src/main/java/io/github/sds100/keymapper/system/SettingsUtils.kt rename to common/src/main/java/io/github/sds100/keymapper/common/utils/SettingsUtils.kt index 6fbe786ccd..c38e7e074f 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/SettingsUtils.kt +++ b/common/src/main/java/io/github/sds100/keymapper/common/utils/SettingsUtils.kt @@ -1,10 +1,20 @@ -package io.github.sds100.keymapper.system +package io.github.sds100.keymapper.common.utils import android.Manifest +import android.content.ActivityNotFoundException import android.content.Context +import android.content.Intent +import android.database.ContentObserver +import android.net.Uri +import android.os.Handler +import android.os.Looper import android.provider.Settings import androidx.annotation.RequiresPermission - +import androidx.core.os.bundleOf +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import timber.log.Timber object SettingsUtils { @@ -130,4 +140,44 @@ object SettingsUtils { } } } + + fun launchSettingsScreen(ctx: Context, action: String, fragmentArg: String? = null) { + val intent = Intent(action).apply { + if (fragmentArg != null) { + val fragmentArgKey = ":settings:fragment_args_key" + val showFragmentArgsKey = ":settings:show_fragment_args" + + putExtra(fragmentArgKey, fragmentArg) + + val bundle = bundleOf(fragmentArgKey to fragmentArg) + putExtra(showFragmentArgsKey, bundle) + } + + flags = Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_NO_HISTORY or + Intent.FLAG_ACTIVITY_CLEAR_TASK or + Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS + } + + try { + ctx.startActivity(intent) + } catch (e: ActivityNotFoundException) { + Timber.e("Failed to start Settings activity: $e") + } + } + + fun settingsCallbackFlow(ctx: Context, uri: Uri): Flow = callbackFlow { + val observer = + object : ContentObserver(Handler(Looper.getMainLooper())) { + override fun onChange(selfChange: Boolean, uri: Uri?) { + super.onChange(selfChange, uri) + + trySend(Unit) + } + } + + ctx.contentResolver.registerContentObserver(uri, false, observer) + + this.awaitClose { ctx.contentResolver.unregisterContentObserver(observer) } + } } diff --git a/common/src/main/java/io/github/sds100/keymapper/common/utils/TreeNode.kt b/common/src/main/java/io/github/sds100/keymapper/common/utils/TreeNode.kt index 1165dfb6fc..4315f43d5b 100644 --- a/common/src/main/java/io/github/sds100/keymapper/common/utils/TreeNode.kt +++ b/common/src/main/java/io/github/sds100/keymapper/common/utils/TreeNode.kt @@ -2,7 +2,7 @@ package io.github.sds100.keymapper.common.utils data class TreeNode(val value: T, val children: MutableList> = mutableListOf()) -inline fun TreeNode.breadFirstTraversal( +inline fun TreeNode.breadthFirstTraversal( action: (T) -> Unit, ) { val queue = ArrayDeque>() diff --git a/common/src/main/java/io/github/sds100/keymapper/common/utils/UserHandleUtils.kt b/common/src/main/java/io/github/sds100/keymapper/common/utils/UserHandleUtils.kt index 43880737a5..59032ad5fa 100644 --- a/common/src/main/java/io/github/sds100/keymapper/common/utils/UserHandleUtils.kt +++ b/common/src/main/java/io/github/sds100/keymapper/common/utils/UserHandleUtils.kt @@ -2,8 +2,12 @@ package io.github.sds100.keymapper.common.utils import android.os.UserHandle -fun UserHandle.getIdentifier(): Int { - val getIdentifierMethod = UserHandle::class.java.getMethod("getIdentifier") +object UserHandleUtils { + fun getCallingUserId(): Int { + return UserHandle::class.java.getMethod("getCallingUserId").invoke(null) as Int + } +} - return getIdentifierMethod.invoke(this) as Int +fun UserHandle.getIdentifier(): Int { + return UserHandle::class.java.getMethod("getIdentifier").invoke(this) as Int } diff --git a/data/build.gradle.kts b/data/build.gradle.kts index ca419f4ad6..422f713ae5 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -36,7 +36,6 @@ android { } room { - schemaDirectory("$projectDir/schemas") } } diff --git a/data/src/main/java/io/github/sds100/keymapper/data/DataHiltModule.kt b/data/src/main/java/io/github/sds100/keymapper/data/DataHiltModule.kt index 0dd446dbfe..4fd6319c42 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/DataHiltModule.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/DataHiltModule.kt @@ -11,13 +11,13 @@ import io.github.sds100.keymapper.data.repositories.GroupRepository import io.github.sds100.keymapper.data.repositories.KeyMapRepository import io.github.sds100.keymapper.data.repositories.LogRepository import io.github.sds100.keymapper.data.repositories.PreferenceRepository +import io.github.sds100.keymapper.data.repositories.PreferenceRepositoryImpl import io.github.sds100.keymapper.data.repositories.RoomAccessibilityNodeRepository import io.github.sds100.keymapper.data.repositories.RoomFloatingButtonRepository import io.github.sds100.keymapper.data.repositories.RoomFloatingLayoutRepository import io.github.sds100.keymapper.data.repositories.RoomGroupRepository import io.github.sds100.keymapper.data.repositories.RoomKeyMapRepository import io.github.sds100.keymapper.data.repositories.RoomLogRepository -import io.github.sds100.keymapper.data.repositories.SettingsPreferenceRepository import javax.inject.Singleton @Module @@ -25,7 +25,7 @@ import javax.inject.Singleton abstract class DataHiltModule { @Singleton @Binds - abstract fun providePreferenceRepository(impl: SettingsPreferenceRepository): PreferenceRepository + abstract fun providePreferenceRepository(impl: PreferenceRepositoryImpl): PreferenceRepository @Singleton @Binds diff --git a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt index 681c7eaba9..705a8176f0 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/Keys.kt @@ -7,12 +7,15 @@ import androidx.datastore.preferences.core.stringSetPreferencesKey object Keys { val darkTheme = stringPreferencesKey("pref_dark_theme_mode") + + @Deprecated("Now use the libsu library to detect whether the device is rooted.") val hasRootPermission = booleanPreferencesKey("pref_allow_root_features") + val shownAppIntro = booleanPreferencesKey("pref_first_time") - val showImePickerNotification = booleanPreferencesKey("pref_show_ime_notification") - val showToggleKeyMapsNotification = booleanPreferencesKey("pref_show_remappings_notification") - val showToggleKeyboardNotification = - booleanPreferencesKey("pref_toggle_key_mapper_keyboard_notification") + +// val showToggleKeyMapsNotification = booleanPreferencesKey("pref_show_remappings_notification") +// val showToggleKeyboardNotification = +// booleanPreferencesKey("pref_toggle_key_mapper_keyboard_notification") val devicesThatChangeIme = stringSetPreferencesKey("pref_devices_that_change_ime") val changeImeOnDeviceConnect = @@ -49,8 +52,6 @@ object Keys { booleanPreferencesKey("key_shown_parallel_trigger_order_warning") val shownSequenceTriggerExplanation = booleanPreferencesKey("key_shown_sequence_trigger_explanation_dialog") - val shownKeyCodeToScanCodeTriggerExplanation = - booleanPreferencesKey("key_shown_keycode_to_scancode_trigger_explanation_dialog") val lastInstalledVersionCodeHomeScreen = intPreferencesKey("last_installed_version_home_screen") val lastInstalledVersionCodeBackground = @@ -59,9 +60,8 @@ object Keys { val fingerprintGesturesAvailable = booleanPreferencesKey("fingerprint_gestures_available") - val rerouteKeyEvents = booleanPreferencesKey("key_reroute_key_events_from_specified_devices") - val devicesToRerouteKeyEvents = - stringSetPreferencesKey("key_devices_to_reroute_key_events") +// val rerouteKeyEvents = booleanPreferencesKey("key_reroute_key_events_from_specified_devices") +// val devicesToRerouteKeyEvents = stringSetPreferencesKey("key_devices_to_reroute_key_events") val log = booleanPreferencesKey("key_log") val shownShizukuPermissionPrompt = booleanPreferencesKey("key_shown_shizuku_permission_prompt") @@ -109,4 +109,24 @@ object Keys { val skipTapTargetTutorial = booleanPreferencesKey("key_skip_tap_target_tutorial") + + val isProModeWarningUnderstood = + booleanPreferencesKey("key_is_pro_mode_warning_understood") + + val isProModeInteractiveSetupAssistantEnabled = + booleanPreferencesKey("key_is_pro_mode_setup_assistant_enabled") + + val isProModeInfoDismissed = + booleanPreferencesKey("key_is_pro_mode_info_dismissed") + + val isProModeAutoStartBootEnabled = + booleanPreferencesKey("key_is_pro_mode_auto_start_boot_enabled") + + val isSystemBridgeEmergencyKilled = + booleanPreferencesKey("key_is_system_bridge_emergency_killed") + + /** + * Whether the user has started the system bridge before. + */ + val isSystemBridgeUsed = booleanPreferencesKey("key_is_system_bridge_used") } diff --git a/data/src/main/java/io/github/sds100/keymapper/data/PreferenceDefaults.kt b/data/src/main/java/io/github/sds100/keymapper/data/PreferenceDefaults.kt index 187f4ef34e..be770ab4b7 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/PreferenceDefaults.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/PreferenceDefaults.kt @@ -14,4 +14,10 @@ object PreferenceDefaults { const val REPEAT_RATE = 50 const val SEQUENCE_TRIGGER_TIMEOUT = 1000 const val HOLD_DOWN_DURATION = 1000 + + const val PRO_MODE_INTERACTIVE_SETUP_ASSISTANT = true + + // Enable this by default so that key maps will still work for root users + // who upgrade to version 4.0. + const val PRO_MODE_AUTOSTART_BOOT = true } diff --git a/data/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt b/data/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt index d358daeee6..9390eb2276 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt @@ -61,7 +61,8 @@ data class ActionEntity( const val EXTRA_KEY_EVENT_META_STATE = "extra_meta_state" const val EXTRA_KEY_EVENT_DEVICE_DESCRIPTOR = "extra_device_descriptor" const val EXTRA_KEY_EVENT_DEVICE_NAME = "extra_device_name" - const val EXTRA_KEY_EVENT_USE_SHELL = "extra_key_event_use_shell" + +// const val EXTRA_KEY_EVENT_USE_SHELL = "extra_key_event_use_shell" const val EXTRA_IME_ID = "extra_ime_id" const val EXTRA_IME_NAME = "extra_ime_name" diff --git a/data/src/main/java/io/github/sds100/keymapper/data/entities/EvdevTriggerKeyEntity.kt b/data/src/main/java/io/github/sds100/keymapper/data/entities/EvdevTriggerKeyEntity.kt new file mode 100644 index 0000000000..e8974e17ea --- /dev/null +++ b/data/src/main/java/io/github/sds100/keymapper/data/entities/EvdevTriggerKeyEntity.kt @@ -0,0 +1,52 @@ +package io.github.sds100.keymapper.data.entities + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.Parcelize +import java.util.UUID + +@Parcelize +data class EvdevTriggerKeyEntity( + @SerializedName(NAME_KEYCODE) + val keyCode: Int, + + @SerializedName(NAME_SCANCODE) + val scanCode: Int, + + @SerializedName(NAME_DEVICE_NAME) + val deviceName: String, + + @SerializedName(NAME_DEVICE_BUS) + val deviceBus: Int, + + @SerializedName(NAME_DEVICE_VENDOR) + val deviceVendor: Int, + + @SerializedName(NAME_DEVICE_PRODUCT) + val deviceProduct: Int, + + @SerializedName(NAME_CLICK_TYPE) + override val clickType: Int = SHORT_PRESS, + + @SerializedName(NAME_FLAGS) + val flags: Int = 0, + + @SerializedName(NAME_UID) + override val uid: String = UUID.randomUUID().toString(), +) : TriggerKeyEntity(), + Parcelable { + + companion object { + // DON'T CHANGE THESE. Used for JSON serialization and parsing. + const val NAME_KEYCODE = "keyCode" + const val NAME_SCANCODE = "scanCode" + const val NAME_DEVICE_NAME = "deviceName" + const val NAME_DEVICE_BUS = "deviceBus" + const val NAME_DEVICE_VENDOR = "deviceVendor" + const val NAME_DEVICE_PRODUCT = "deviceProduct" + const val NAME_FLAGS = "flags" + + const val FLAG_DO_NOT_CONSUME_KEY_EVENT = 1 + const val FLAG_DETECT_WITH_SCAN_CODE = 2 + } +} diff --git a/data/src/main/java/io/github/sds100/keymapper/data/entities/KeyCodeTriggerKeyEntity.kt b/data/src/main/java/io/github/sds100/keymapper/data/entities/KeyEventTriggerKeyEntity.kt similarity index 86% rename from data/src/main/java/io/github/sds100/keymapper/data/entities/KeyCodeTriggerKeyEntity.kt rename to data/src/main/java/io/github/sds100/keymapper/data/entities/KeyEventTriggerKeyEntity.kt index e26132c25a..210bb61ec6 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/entities/KeyCodeTriggerKeyEntity.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/entities/KeyEventTriggerKeyEntity.kt @@ -6,7 +6,7 @@ import kotlinx.parcelize.Parcelize import java.util.UUID @Parcelize -data class KeyCodeTriggerKeyEntity( +data class KeyEventTriggerKeyEntity( @SerializedName(NAME_KEYCODE) val keyCode: Int, @@ -24,12 +24,16 @@ data class KeyCodeTriggerKeyEntity( @SerializedName(NAME_UID) override val uid: String = UUID.randomUUID().toString(), + + @SerializedName(NAME_SCANCODE) + val scanCode: Int? = null, ) : TriggerKeyEntity(), Parcelable { companion object { // DON'T CHANGE THESE. Used for JSON serialization and parsing. const val NAME_KEYCODE = "keyCode" + const val NAME_SCANCODE = "scanCode" const val NAME_DEVICE_ID = "deviceId" const val NAME_DEVICE_NAME = "deviceName" const val NAME_FLAGS = "flags" @@ -40,5 +44,6 @@ data class KeyCodeTriggerKeyEntity( const val FLAG_DO_NOT_CONSUME_KEY_EVENT = 1 const val FLAG_DETECTION_SOURCE_INPUT_METHOD = 2 + const val FLAG_DETECT_WITH_SCAN_CODE = 4 } } diff --git a/data/src/main/java/io/github/sds100/keymapper/data/entities/LogEntryEntity.kt b/data/src/main/java/io/github/sds100/keymapper/data/entities/LogEntryEntity.kt index bd42e17c29..7e3d8901e0 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/entities/LogEntryEntity.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/entities/LogEntryEntity.kt @@ -20,5 +20,6 @@ data class LogEntryEntity( const val SEVERITY_ERROR = 0 const val SEVERITY_DEBUG = 1 const val SEVERITY_INFO = 2 + const val SEVERITY_WARNING = 3 } } diff --git a/data/src/main/java/io/github/sds100/keymapper/data/entities/TriggerEntity.kt b/data/src/main/java/io/github/sds100/keymapper/data/entities/TriggerEntity.kt index 1a2d114c70..ec2e77603d 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/entities/TriggerEntity.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/entities/TriggerEntity.kt @@ -39,7 +39,10 @@ data class TriggerEntity( // DON'T CHANGE THESE AND THEY MUST BE POWERS OF 2!! const val TRIGGER_FLAG_VIBRATE = 1 const val TRIGGER_FLAG_LONG_PRESS_DOUBLE_VIBRATION = 2 + + @Deprecated("This is now on by default for evdev trigger keys") const val TRIGGER_FLAG_SCREEN_OFF_TRIGGERS = 4 + const val TRIGGER_FLAG_FROM_OTHER_APPS = 8 const val TRIGGER_FLAG_SHOW_TOAST = 16 diff --git a/data/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt b/data/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt index ce98690420..e294f7c795 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/entities/TriggerKeyEntity.kt @@ -35,7 +35,8 @@ sealed class TriggerKeyEntity : Parcelable { val SERIALIZER: JsonSerializer = jsonSerializer { (key) -> when (key) { is AssistantTriggerKeyEntity -> Gson().toJsonTree(key) - is KeyCodeTriggerKeyEntity -> Gson().toJsonTree(key) + is KeyEventTriggerKeyEntity -> Gson().toJsonTree(key) + is EvdevTriggerKeyEntity -> Gson().toJsonTree(key) is FloatingButtonKeyEntity -> Gson().toJsonTree(key) is FingerprintTriggerKeyEntity -> Gson().toJsonTree(key) } @@ -60,6 +61,10 @@ sealed class TriggerKeyEntity : Parcelable { return@jsonDeserializer deserializeFingerprintTriggerKey(json, uid!!) } + json.obj.has(EvdevTriggerKeyEntity.NAME_DEVICE_PRODUCT) -> { + return@jsonDeserializer deserializeEvdevTriggerKey(json, uid!!) + } + else -> { return@jsonDeserializer deserializeKeyCodeTriggerKey(json, uid!!) } @@ -96,23 +101,51 @@ sealed class TriggerKeyEntity : Parcelable { return FingerprintTriggerKeyEntity(type, clickType, uid) } + private fun deserializeEvdevTriggerKey( + json: JsonElement, + uid: String, + ): EvdevTriggerKeyEntity { + val keyCode by json.byInt(EvdevTriggerKeyEntity.NAME_KEYCODE) + val scanCode by json.byInt(EvdevTriggerKeyEntity.NAME_SCANCODE) + val deviceName by json.byString(EvdevTriggerKeyEntity.NAME_DEVICE_NAME) + val deviceVendor by json.byInt(EvdevTriggerKeyEntity.NAME_DEVICE_VENDOR) + val deviceProduct by json.byInt(EvdevTriggerKeyEntity.NAME_DEVICE_PRODUCT) + val deviceBus by json.byInt(EvdevTriggerKeyEntity.NAME_DEVICE_BUS) + val clickType by json.byInt(NAME_CLICK_TYPE) + val flags by json.byNullableInt(EvdevTriggerKeyEntity.NAME_FLAGS) + + return EvdevTriggerKeyEntity( + keyCode, + scanCode, + deviceName, + deviceBus, + deviceVendor, + deviceProduct, + clickType, + flags ?: 0, + uid, + ) + } + private fun deserializeKeyCodeTriggerKey( json: JsonElement, uid: String, - ): KeyCodeTriggerKeyEntity { - val keyCode by json.byInt(KeyCodeTriggerKeyEntity.NAME_KEYCODE) - val deviceId by json.byString(KeyCodeTriggerKeyEntity.NAME_DEVICE_ID) - val deviceName by json.byNullableString(KeyCodeTriggerKeyEntity.NAME_DEVICE_NAME) + ): KeyEventTriggerKeyEntity { + val keyCode by json.byInt(KeyEventTriggerKeyEntity.NAME_KEYCODE) + val scanCode by json.byNullableInt(KeyEventTriggerKeyEntity.NAME_SCANCODE) + val deviceId by json.byString(KeyEventTriggerKeyEntity.NAME_DEVICE_ID) + val deviceName by json.byNullableString(KeyEventTriggerKeyEntity.NAME_DEVICE_NAME) val clickType by json.byInt(NAME_CLICK_TYPE) - val flags by json.byNullableInt(KeyCodeTriggerKeyEntity.NAME_FLAGS) + val flags by json.byNullableInt(KeyEventTriggerKeyEntity.NAME_FLAGS) - return KeyCodeTriggerKeyEntity( + return KeyEventTriggerKeyEntity( keyCode, deviceId, deviceName, clickType, flags ?: 0, uid, + scanCode, ) } } diff --git a/data/src/main/java/io/github/sds100/keymapper/data/migration/Migration1To2.kt b/data/src/main/java/io/github/sds100/keymapper/data/migration/Migration1To2.kt index 41037750a4..df4375a50d 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/migration/Migration1To2.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/migration/Migration1To2.kt @@ -14,7 +14,7 @@ import com.google.gson.JsonObject import com.google.gson.JsonParser import io.github.sds100.keymapper.common.utils.hasFlag import io.github.sds100.keymapper.data.entities.ActionEntity -import io.github.sds100.keymapper.data.entities.KeyCodeTriggerKeyEntity +import io.github.sds100.keymapper.data.entities.KeyEventTriggerKeyEntity import timber.log.Timber /** @@ -103,7 +103,7 @@ object Migration1To2 { createTriggerKey2( it.asInt, - KeyCodeTriggerKeyEntity.DEVICE_ID_ANY_DEVICE, + KeyEventTriggerKeyEntity.DEVICE_ID_ANY_DEVICE, clickType, ) } diff --git a/data/src/main/java/io/github/sds100/keymapper/data/migration/Migration6To7.kt b/data/src/main/java/io/github/sds100/keymapper/data/migration/Migration6To7.kt index a137888a53..e23a70d779 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/migration/Migration6To7.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/migration/Migration6To7.kt @@ -11,7 +11,7 @@ import com.google.gson.GsonBuilder import io.github.sds100.keymapper.common.utils.hasFlag import io.github.sds100.keymapper.common.utils.minusFlag import io.github.sds100.keymapper.common.utils.withFlag -import io.github.sds100.keymapper.data.entities.KeyCodeTriggerKeyEntity +import io.github.sds100.keymapper.data.entities.KeyEventTriggerKeyEntity import io.github.sds100.keymapper.data.entities.TriggerEntity import io.github.sds100.keymapper.data.entities.TriggerKeyEntity @@ -40,10 +40,10 @@ object Migration6To7 { val trigger = gson.fromJson(getString(triggerColumnIndex)) val newTriggerKeys = trigger.keys - .mapNotNull { it as? KeyCodeTriggerKeyEntity } + .mapNotNull { it as? KeyEventTriggerKeyEntity } .map { key -> if (trigger.flags.hasFlag(TRIGGER_FLAG_DONT_OVERRIDE_DEFAULT_ACTION)) { - key.copy(flags = key.flags.withFlag(KeyCodeTriggerKeyEntity.FLAG_DO_NOT_CONSUME_KEY_EVENT)) + key.copy(flags = key.flags.withFlag(KeyEventTriggerKeyEntity.FLAG_DO_NOT_CONSUME_KEY_EVENT)) } else { key } diff --git a/data/src/main/java/io/github/sds100/keymapper/data/repositories/LogRepository.kt b/data/src/main/java/io/github/sds100/keymapper/data/repositories/LogRepository.kt index 1df1d3479a..bdfc30164e 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/repositories/LogRepository.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/repositories/LogRepository.kt @@ -1,11 +1,10 @@ package io.github.sds100.keymapper.data.repositories -import io.github.sds100.keymapper.common.utils.State import io.github.sds100.keymapper.data.entities.LogEntryEntity import kotlinx.coroutines.flow.Flow interface LogRepository { - val log: Flow>> + val log: Flow> fun insert(entry: LogEntryEntity) suspend fun insertSuspend(entry: LogEntryEntity) fun deleteAll() diff --git a/data/src/main/java/io/github/sds100/keymapper/data/repositories/SettingsPreferenceRepository.kt b/data/src/main/java/io/github/sds100/keymapper/data/repositories/PreferenceRepositoryImpl.kt similarity index 97% rename from data/src/main/java/io/github/sds100/keymapper/data/repositories/SettingsPreferenceRepository.kt rename to data/src/main/java/io/github/sds100/keymapper/data/repositories/PreferenceRepositoryImpl.kt index 8d7f330d20..e07a4d6d77 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/repositories/SettingsPreferenceRepository.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/repositories/PreferenceRepositoryImpl.kt @@ -14,7 +14,7 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class SettingsPreferenceRepository @Inject constructor( +class PreferenceRepositoryImpl @Inject constructor( @ApplicationContext context: Context, private val coroutineScope: CoroutineScope, ) : PreferenceRepository { diff --git a/data/src/main/java/io/github/sds100/keymapper/data/repositories/RoomKeyMapRepository.kt b/data/src/main/java/io/github/sds100/keymapper/data/repositories/RoomKeyMapRepository.kt index dd05f6d82d..9a5500646a 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/repositories/RoomKeyMapRepository.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/repositories/RoomKeyMapRepository.kt @@ -12,6 +12,7 @@ import io.github.sds100.keymapper.data.migration.fingerprintmaps.FingerprintToKe import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map @@ -34,7 +35,7 @@ class RoomKeyMapRepository @Inject constructor( private const val MAX_KEY_MAP_BATCH_SIZE = 200 } - override val keyMapList = keyMapDao.getAll() + override val keyMapList: StateFlow>> = keyMapDao.getAll() .map { State.Data(it) } .flowOn(dispatchers.io()) .stateIn(coroutineScope, SharingStarted.Eagerly, State.Loading) diff --git a/data/src/main/java/io/github/sds100/keymapper/data/repositories/RoomLogRepository.kt b/data/src/main/java/io/github/sds100/keymapper/data/repositories/RoomLogRepository.kt index 083a8034b9..f6c4baf36f 100644 --- a/data/src/main/java/io/github/sds100/keymapper/data/repositories/RoomLogRepository.kt +++ b/data/src/main/java/io/github/sds100/keymapper/data/repositories/RoomLogRepository.kt @@ -1,18 +1,14 @@ package io.github.sds100.keymapper.data.repositories -import io.github.sds100.keymapper.common.utils.State import io.github.sds100.keymapper.data.db.dao.LogEntryDao import io.github.sds100.keymapper.data.entities.LogEntryEntity import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Singleton @@ -22,15 +18,8 @@ class RoomLogRepository @Inject constructor( private val coroutineScope: CoroutineScope, private val dao: LogEntryDao, ) : LogRepository { - override val log: Flow>> = dao.getAll() - .map { entityList -> State.Data(entityList) } - .flowOn(Dispatchers.Default) - .stateIn( - coroutineScope, - // save memory by only caching the log when necessary - SharingStarted.WhileSubscribed(replayExpirationMillis = 1000L), - State.Loading, - ) + override val log: Flow> = dao.getAll() + .flowOn(Dispatchers.IO) init { dao.getIds() @@ -38,18 +27,18 @@ class RoomLogRepository @Inject constructor( .onEach { log -> val middleId = log.getOrNull(500) ?: return@onEach dao.deleteRowsWithIdLessThan(middleId) - }.flowOn(Dispatchers.Default) + }.flowOn(Dispatchers.IO) .launchIn(coroutineScope) } override fun deleteAll() { - coroutineScope.launch(Dispatchers.Default) { + coroutineScope.launch(Dispatchers.IO) { dao.deleteAll() } } override fun insert(entry: LogEntryEntity) { - coroutineScope.launch(Dispatchers.Default) { + coroutineScope.launch(Dispatchers.IO) { dao.insert(entry) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9c4942c902..4992c424ad 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,12 +1,13 @@ [versions] compile-sdk = "36" hilt-navigation-compose = "1.2.0" -min-sdk = "21" +min-sdk = "26" build-tools = "36.0.0" target-sdk = "36" android-gradle-plugin = "8.9.1" androidx-activity = "1.10.1" +androidx-annotation = "1.9.1" androidx-appcompat = "1.7.0" androidx-arch-core-testing = "2.2.0" androidx-constraintlayout = "2.2.1" @@ -38,7 +39,13 @@ epoxy = "4.6.2" flexbox = "3.0.0" google-accompanist-drawablepainter = "0.35.0-alpha" hiddenapibypass = "4.3" +hiddenapibypass-lsposed = "6.1" introshowcaseview = "2.0.2" +conscrypt-android = "2.5.3" +boringssl-ndk = "20250114" +bouncycastle-bcpkix = "1.70" +appiconloader = "1.5.0" +rikkax-core = "1.4.1" junit = "4.13.2" junit-params = "1.1.1" @@ -69,6 +76,9 @@ room-testing-legacy = "1.1.1" espresso-core = "3.6.1" ui-tooling = "1.8.1" # android.arch.persistence.room:testing +libsu-core = "6.0.0" +rikka-hidden = "4.3.3" + [libraries] # Kotlin androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hilt-navigation-compose" } @@ -82,6 +92,7 @@ kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx- # AndroidX androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidx-activity" } androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "androidx-activity" } +androidx-annotation-jvm = { group = "androidx.annotation", name = "annotation-jvm", version.ref = "androidx-annotation" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" } androidx-arch-core-testing = { group = "androidx.arch.core", name = "core-testing", version.ref = "androidx-arch-core-testing" } androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidx-constraintlayout" } @@ -149,6 +160,8 @@ splitties-toast = { group = "com.louiscad.splitties", name = "splitties-toast", # Rikka Shizuku rikka-shizuku-api = { group = "dev.rikka.shizuku", name = "api", version.ref = "shizuku" } rikka-shizuku-provider = { group = "dev.rikka.shizuku", name = "provider", version.ref = "shizuku" } +rikka-hidden-compat = { group = "dev.rikka.hidden", name = "compat", version.ref = "rikka-hidden" } +rikka-hidden-stub = { group = "dev.rikka.hidden", name = "stub", version.ref = "rikka-hidden" } # Testing junit = { group = "junit", name = "junit", version.ref = "junit" } @@ -168,7 +181,15 @@ github-mflisar-dragselectrecyclerview = { group = "com.github.MFlisar", name = " jakewharton-timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" } kotson = { group = "com.github.salomonbrys.kotson", name = "kotson", version.ref = "kotson" } lsposed-hiddenapibypass = { group = "org.lsposed.hiddenapibypass", name = "hiddenapibypass", version.ref = "hiddenapibypass" } +lsposed-hiddenapibypass-updated = { group = "org.lsposed.hiddenapibypass", name = "hiddenapibypass", version.ref = "hiddenapibypass-lsposed" } net-lingala-zip4j = { group = "net.lingala.zip4j", name = "zip4j", version.ref = "lingala-zip4j" } +github-topjohnwu-libsu = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu-core" } +conscrypt-android = { group = "org.conscrypt", name = "conscrypt-android", version.ref = "conscrypt-android" } +vvb2060-ndk-boringssl = { group = "io.github.vvb2060.ndk", name = "boringssl", version.ref = "boringssl-ndk" } +bouncycastle-bcpkix = { group = "org.bouncycastle", name = "bcpkix-jdk15on", version.ref = "bouncycastle-bcpkix" } +zhanghai-appiconloader = { group = "me.zhanghai.android.appiconloader", name = "appiconloader", version.ref = "appiconloader" } +rikka-rikkax-core = { group = "dev.rikka.rikkax.core", name = "core-ktx", version.ref = "rikkax-core" } + # Gradle Plugins - Aliases for buildscript dependencies / plugins block # These are referenced in build.gradle.kts files' plugins blocks by their ID. diff --git a/settings.gradle.kts b/settings.gradle.kts index 266e977f2e..f035ff415c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -30,4 +30,4 @@ include(":api") include(":system") include(":common") include(":data") -include(":shizuku") +include(":sysbridge") diff --git a/shizuku/.gitignore b/shizuku/.gitignore deleted file mode 100644 index 42afabfd2a..0000000000 --- a/shizuku/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/shizuku/build.gradle.kts b/shizuku/build.gradle.kts deleted file mode 100644 index 3b8d3dfb26..0000000000 --- a/shizuku/build.gradle.kts +++ /dev/null @@ -1,42 +0,0 @@ -plugins { - alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.android) -} - -android { - namespace = "io.github.sds100.keymapper.shizuku" - compileSdk = 35 - - defaultConfig { - minSdk = 21 - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - release { - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - kotlinOptions { - jvmTarget = "11" - } -} - -dependencies { - - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.appcompat) - implementation(libs.google.android.material) - testImplementation(libs.junit) - androidTestImplementation(libs.androidx.test.ext.junit) - androidTestImplementation(libs.androidx.espresso.core) -} \ No newline at end of file diff --git a/shizuku/src/androidTest/java/io/github/sds100/keymapper/shizuku/ExampleInstrumentedTest.kt b/shizuku/src/androidTest/java/io/github/sds100/keymapper/shizuku/ExampleInstrumentedTest.kt deleted file mode 100644 index 44e338cfc3..0000000000 --- a/shizuku/src/androidTest/java/io/github/sds100/keymapper/shizuku/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package io.github.sds100.keymapper.shizuku - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("io.github.sds100.keymapper.shizuku.test", appContext.packageName) - } -} \ No newline at end of file diff --git a/shizuku/src/main/AndroidManifest.xml b/shizuku/src/main/AndroidManifest.xml deleted file mode 100644 index a5918e68ab..0000000000 --- a/shizuku/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/shizuku/src/test/java/io/github/sds100/keymapper/shizuku/ExampleUnitTest.kt b/shizuku/src/test/java/io/github/sds100/keymapper/shizuku/ExampleUnitTest.kt deleted file mode 100644 index c372bb7be5..0000000000 --- a/shizuku/src/test/java/io/github/sds100/keymapper/shizuku/ExampleUnitTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package io.github.sds100.keymapper.shizuku - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} \ No newline at end of file diff --git a/sysbridge/.gitignore b/sysbridge/.gitignore new file mode 100644 index 0000000000..131b793244 --- /dev/null +++ b/sysbridge/.gitignore @@ -0,0 +1,3 @@ +/build +.cxx +/src/main/cpp/libevdev/event-names.h diff --git a/sysbridge/build.gradle.kts b/sysbridge/build.gradle.kts new file mode 100644 index 0000000000..a8ccfada99 --- /dev/null +++ b/sysbridge/build.gradle.kts @@ -0,0 +1,204 @@ +import kotlin.io.path.absolutePathString + +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.google.devtools.ksp) + alias(libs.plugins.dagger.hilt.android) + alias(libs.plugins.kotlin.parcelize) +} + +android { + namespace = "io.github.sds100.keymapper.sysbridge" + compileSdk = libs.versions.compile.sdk.get().toInt() + + defaultConfig { + // Must be API 29 so that the binder-ndk library can be found. + minSdk = 29 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + + externalNativeBuild { + cmake { + val aidlSrcDir = project.file("src/main/cpp/aidl") + + // -DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON is required to get the app running on the Android 15. This is related to the new 16kB page size support. + // -DANDROID_WEAK_API_DEFS=ON is required so the libevdev_jni file can run code depending on the SDK. https://developer.android.com/ndk/guides/using-newer-apis + arguments( + "-DANDROID_STL=c++_static", + "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON", + "-DANDROID_WEAK_API_DEFS=ON", + "-Daidl_src_dir=${aidlSrcDir.absolutePath}" + ) + } + } + } + + buildTypes { + release { + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + externalNativeBuild { + cmake { + path("src/main/cpp/CMakeLists.txt") + version = "3.22.1" + } + } + + buildFeatures { + aidl = true + prefab = true + buildConfig = true + } + + packaging { + jniLibs { + // This replaces extractNativeLibs option in the manifest. This is needed so the + // libraries are extracted to a location on disk where the system bridge process + // can access them. Start in Android 6.0, they are no longer extracted by default. + useLegacyPackaging = true + + // This is required on Android 15. Otherwise a java.lang.UnsatisfiedLinkError: dlopen failed: empty/missing DT_HASH/DT_GNU_HASH error is thrown. + keepDebugSymbols.add("**/*.so") + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = "11" + } +} + +dependencies { + compileOnly(project(":systemstubs")) + implementation(project(":common")) + + implementation(libs.jakewharton.timber) + + implementation(libs.conscrypt.android) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.dagger.hilt.android) + ksp(libs.dagger.hilt.android.compiler) + implementation(libs.github.topjohnwu.libsu) + implementation(libs.rikka.shizuku.api) + implementation(libs.rikka.shizuku.provider) + implementation(libs.rikka.hidden.compat) + compileOnly(libs.rikka.hidden.stub) + + // From Shizuku :manager module build.gradle file. + implementation(libs.vvb2060.ndk.boringssl) + implementation(libs.lsposed.hiddenapibypass.updated) + implementation(libs.bouncycastle.bcpkix) + implementation(libs.zhanghai.appiconloader) + implementation(libs.rikka.rikkax.core) +} + +tasks.named("preBuild") { + dependsOn(generateLibEvDevEventNames) + dependsOn(compileAidlNdk) +} + +// The list of event names needs to be parsed from the input.h file in the NDK. +// input.h can be found in the Android/sdk/ndk/27.0.12077973/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/include/linux/input.h +// folder on macOS. +val generateLibEvDevEventNames by tasks.registering(Exec::class) { + dependsOn(compileAidlNdk) + + group = "build" + description = "Generates event names header from input.h" + + val prebuiltDir = File(android.ndkDirectory, "toolchains/llvm/prebuilt") + + // The "darwin-x86_64" part of the path is different on each operating system but it seems like + // the SDK Manager only downloads the NDK specific to the local operating system. So, just + // go into the only directory that the "prebuilt" directory contains. + val hostDirs = prebuiltDir.listFiles { file -> file.isDirectory } + ?: throw GradleException("No prebuilt toolchain directories found in $prebuiltDir") + + if (hostDirs.size != 1) { + throw GradleException("Expected exactly one prebuilt toolchain directory in $prebuiltDir, found ${hostDirs.size}") + } + val toolchainDir = hostDirs[0].absolutePath + + val inputHeader = "$toolchainDir/sysroot/usr/include/linux/input.h" + val inputEventCodesHeader = "$toolchainDir/sysroot/usr/include/linux/input-event-codes.h" + val outputHeader = "$projectDir/src/main/cpp/libevdev/event-names.h" + val pythonScript = "$projectDir/src/main/cpp/libevdev/make-event-names.py" + + commandLine("python3", pythonScript, inputHeader, inputEventCodesHeader) + + standardOutput = File(outputHeader).outputStream() + + inputs.file(pythonScript) + inputs.file(inputHeader) + inputs.file(inputEventCodesHeader) + outputs.file(outputHeader) +} + +// Task to compile AIDL files for NDK. +// Taken from https://github.com/lakinduboteju/AndroidNdkBinderExamples +val compileAidlNdk by tasks.registering(Exec::class) { + group = "build" + description = "Compiles AIDL files in src/main/aidl to NDK C++ headers and sources." + + val aidlSrcDir = project.file("src/main/aidl") + // Find all .aidl files. Using fileTree ensures it's dynamic. + val aidlFiles = project.fileTree(aidlSrcDir) { + include("**/IEvdevCallback.aidl") + include("**/InputDeviceIdentifier.aidl") + } + + inputs.files(aidlFiles) + .withPathSensitivity(PathSensitivity.RELATIVE) + .withPropertyName("aidlInputFiles") + + val cppOutDir = project.file("src/main/cpp/aidl") + val cppHeaderOutDir = project.file("src/main/cpp") + + outputs.dir(cppOutDir).withPropertyName("cppOutputDir") + outputs.dir(cppHeaderOutDir).withPropertyName("cppHeaderOutputDir") + + // Path to the aidl executable in the Android SDK + val aidlToolPath = + android.sdkDirectory.toPath() + .resolve("build-tools") + .resolve(android.buildToolsVersion) + .resolve("aidl") + .absolutePathString() + val importSearchPath = aidlSrcDir.absolutePath + + // Ensure output directories exist before trying to write to them + cppOutDir.mkdirs() + cppHeaderOutDir.mkdirs() + + if (aidlFiles.isEmpty) { + logger.info("No AIDL files found in $aidlSrcDir. Skipping compileAidlNdk task.") + return@registering // Exit doLast if no files to process + } + + for (aidlFile in aidlFiles) { + logger.lifecycle("Compiling AIDL file (NDK): ${aidlFile.path}") + + commandLine( + aidlToolPath, + "--lang=ndk", + "-o", cppOutDir.absolutePath, + "-h", cppHeaderOutDir.absolutePath, + "-I", importSearchPath, + aidlFile.absolutePath + ) + } + + logger.lifecycle("AIDL NDK compilation finished. Check outputs in $cppOutDir and $cppHeaderOutDir") +} \ No newline at end of file diff --git a/shizuku/consumer-rules.pro b/sysbridge/consumer-rules.pro similarity index 100% rename from shizuku/consumer-rules.pro rename to sysbridge/consumer-rules.pro diff --git a/shizuku/proguard-rules.pro b/sysbridge/proguard-rules.pro similarity index 100% rename from shizuku/proguard-rules.pro rename to sysbridge/proguard-rules.pro diff --git a/sysbridge/src/main/AndroidManifest.xml b/sysbridge/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..77951bd794 --- /dev/null +++ b/sysbridge/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.aidl b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.aidl new file mode 100644 index 0000000000..f6b4039a6d --- /dev/null +++ b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.aidl @@ -0,0 +1,7 @@ +package io.github.sds100.keymapper.sysbridge; + +interface IEvdevCallback { + oneway void onEvdevEventLoopStarted(); + boolean onEvdevEvent(String devicePath, long timeSec, long timeUsec, int type, int code, int value, int androidCode); + void onEmergencyKillSystemBridge(); +} \ No newline at end of file diff --git a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IShizukuStarterService.aidl b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IShizukuStarterService.aidl new file mode 100644 index 0000000000..14e94f98ce --- /dev/null +++ b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IShizukuStarterService.aidl @@ -0,0 +1,7 @@ +package io.github.sds100.keymapper.sysbridge; + +interface IShizukuStarterService { + void destroy() = 16777114; // Destroy method defined by Shizuku server + + String executeCommand(String command) = 1; +} \ No newline at end of file diff --git a/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl new file mode 100644 index 0000000000..d6a91ee54f --- /dev/null +++ b/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/ISystemBridge.aidl @@ -0,0 +1,38 @@ +package io.github.sds100.keymapper.sysbridge; + +import io.github.sds100.keymapper.sysbridge.IEvdevCallback; +import io.github.sds100.keymapper.common.models.EvdevDeviceHandle; +import android.view.InputEvent; + +interface ISystemBridge { + void destroy() = 16777114; + int getProcessUid() = 16777113; + int getVersionCode() = 16777112; + String executeCommand(String command) = 16777111; + + boolean grabEvdevDevice(String devicePath) = 1; + boolean grabEvdevDeviceArray(in String[] devicePath) = 2; + + boolean ungrabEvdevDevice(String devicePath) = 3; + boolean ungrabAllEvdevDevices() = 4; + + void registerEvdevCallback(IEvdevCallback callback) = 5; + void unregisterEvdevCallback() = 6; + + boolean writeEvdevEvent(String devicePath, int type, int code, int value) = 7; + boolean injectInputEvent(in InputEvent event, int mode) = 8; + + EvdevDeviceHandle[] getEvdevInputDevices() = 9; + + boolean setWifiEnabled(boolean enable) = 10; + + void grantPermission(String permission, int deviceId) = 11; + + void setDataEnabled(int subId, boolean enable) = 12; + + void setBluetoothEnabled(boolean enable) = 13; + + void setNfcEnabled(boolean enable) = 14; + + void setAirplaneMode(boolean enable) = 15; +} \ No newline at end of file diff --git a/sysbridge/src/main/cpp/CMakeLists.txt b/sysbridge/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000000..f87842b840 --- /dev/null +++ b/sysbridge/src/main/cpp/CMakeLists.txt @@ -0,0 +1,119 @@ +# For more information about using CMake with Android Studio, read the +# documentation: https://d.android.com/studio/projects/add-native-code.html. +# For more examples on how to use CMake, see https://github.com/android/ndk-samples. + +# Sets the minimum CMake version required for this project. +cmake_minimum_required(VERSION 3.22.1) + +# Declares the project name. The project name can be accessed via ${ PROJECT_NAME}, +# Since this is the top level CMakeLists.txt, the project name is also accessible +# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level +# build script scope). +project("sysbridge") + +set(CMAKE_CXX_STANDARD 20) + +set(C_FLAGS "-Werror=format -fdata-sections -ffunction-sections -fno-exceptions -fno-rtti -fno-threadsafe-statics") +set(LINKER_FLAGS "-Wl,--hash-style=both") + +if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug") + message("Building Release...") + + set(C_FLAGS "${C_FLAGS} -O2 -fvisibility=hidden -fvisibility-inlines-hidden") + set(LINKER_FLAGS "${LINKER_FLAGS} -Wl,-exclude-libs,ALL -Wl,--gc-sections") +else () + message("Building Debug...") + + add_definitions(-DDEBUG) +endif () + +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${C_FLAGS}") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${C_FLAGS}") + +set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${LINKER_FLAGS}") +set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} ${LINKER_FLAGS}") + +find_library(log-lib log) +find_package(boringssl REQUIRED CONFIG) + +add_executable(libsysbridge.so + starter.cpp + misc.cpp + selinux.cpp + cgroup.cpp + android.cpp + adb_pairing.cpp) + +target_link_libraries(libsysbridge.so ${log-lib} boringssl::crypto_static) + +if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug") + add_custom_command(TARGET libsysbridge.so POST_BUILD + COMMAND ${CMAKE_STRIP} --remove-section=.comment "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/libsysbridge.so") +endif () + +add_library(adb SHARED + adb_pairing.cpp misc.cpp) + +target_link_libraries(adb ${log-lib} boringssl::crypto_static) + +if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug") + add_custom_command(TARGET adb POST_BUILD + COMMAND ${CMAKE_STRIP} --remove-section=.comment "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/libadb.so") +endif () + +# Creates and names a library, sets it as either STATIC +# or SHARED, and provides the relative paths to its source code. +# You can define multiple libraries, and CMake builds them for you. +# Gradle automatically packages shared libraries with your APK. +# +# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define +# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME} +# is preferred for the same purpose. +# +# In order to load a library into your app from Java/Kotlin, you must call +# System.loadLibrary() and pass the name of the library defined here; +# for GameActivity/NativeActivity derived applications, the same library name must be +# used in the AndroidManifest.xml file. +add_library(evdev SHARED + # List C/C++ source files with relative paths to this CMakeLists.txt. + libevdev_jni.cpp + libevdev/libevdev.c + libevdev/libevdev-names.c + libevdev/libevdev-uinput.c + android/input/KeyLayoutMap.cpp + android/input/InputEventLabels.cpp + android/libbase/result.cpp + android/utils/Tokenizer.cpp + android/utils/String16.cpp + android/utils/String8.cpp + android/utils/SharedBuffer.cpp + android/utils/FileMap.cpp + android/utils/Unicode.cpp + android/input/InputDevice.cpp + android/input/Input.cpp + android/libbase/stringprintf.cpp + ${aidl_src_dir}/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp) + +find_library( + binder_ndk-lib + binder_ndk +) + +# Specifies libraries CMake should link to your target library. You +# can link libraries from various origins, such as libraries defined in this +# build script, prebuilt third-party libraries, or Android system libraries. +target_link_libraries(evdev + # List libraries link to the target library + android + log + ${binder_ndk-lib}) + +# Add include directories for header files +target_include_directories(evdev PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/android + ${CMAKE_CURRENT_SOURCE_DIR}/android/input + ${CMAKE_CURRENT_SOURCE_DIR}/android/libbase + ${CMAKE_CURRENT_SOURCE_DIR}/android/utils + ${CMAKE_CURRENT_SOURCE_DIR}/libevdev) + diff --git a/sysbridge/src/main/cpp/adb_pairing.cpp b/sysbridge/src/main/cpp/adb_pairing.cpp new file mode 100644 index 0000000000..a7651671c0 --- /dev/null +++ b/sysbridge/src/main/cpp/adb_pairing.cpp @@ -0,0 +1,231 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "adb_pairing.h" + +#define LOG_TAG "AdbPairClient" + +#include "logging.h" + +// --------------------------------------------------------- + +static constexpr spake2_role_t kClientRole = spake2_role_alice; +static constexpr spake2_role_t kServerRole = spake2_role_bob; + +static const uint8_t kClientName[] = "adb pair client"; +static const uint8_t kServerName[] = "adb pair server"; + +static constexpr size_t kHkdfKeyLength = 16; + +struct PairingContextNative { + SPAKE2_CTX *spake2_ctx; + uint8_t key[SPAKE2_MAX_MSG_SIZE]; + size_t key_size; + + EVP_AEAD_CTX *aes_ctx; + uint64_t dec_sequence; + uint64_t enc_sequence; +}; + +static jlong PairingContext_Constructor(JNIEnv *env, jclass clazz, jboolean isClient, jbyteArray jPassword) { + spake2_role_t spake_role; + const uint8_t *my_name; + const uint8_t *their_name; + size_t my_len; + size_t their_len; + + if (isClient) { + spake_role = kClientRole; + my_name = kClientName; + my_len = sizeof(kClientName); + their_name = kServerName; + their_len = sizeof(kServerName); + } else { + spake_role = kServerRole; + my_name = kServerName; + my_len = sizeof(kServerName); + their_name = kClientName; + their_len = sizeof(kClientName); + } + + auto spake2_ctx = SPAKE2_CTX_new(spake_role, my_name, my_len, their_name, their_len); + if (spake2_ctx == nullptr) { + LOGE("Unable to create a SPAKE2 context."); + return 0; + } + + auto pswd_size = env->GetArrayLength(jPassword); + auto pswd = env->GetByteArrayElements(jPassword, nullptr); + + size_t key_size = 0; + uint8_t key[SPAKE2_MAX_MSG_SIZE]; + int status = SPAKE2_generate_msg(spake2_ctx, key, &key_size, SPAKE2_MAX_MSG_SIZE, (uint8_t *) pswd, pswd_size); + if (status != 1 || key_size == 0) { + LOGE("Unable to generate the SPAKE2 public key."); + + env->ReleaseByteArrayElements(jPassword, pswd, 0); + SPAKE2_CTX_free(spake2_ctx); + return 0; + } + env->ReleaseByteArrayElements(jPassword, pswd, 0); + + auto ctx = (PairingContextNative *) malloc(sizeof(PairingContextNative)); + memset(ctx, 0, sizeof(PairingContextNative)); + ctx->spake2_ctx = spake2_ctx; + memcpy(ctx->key, key, SPAKE2_MAX_MSG_SIZE); + ctx->key_size = key_size; + return (jlong) ctx; +} + +static jbyteArray PairingContext_Msg(JNIEnv *env, jobject obj, jlong ptr) { + auto ctx = (PairingContextNative *) ptr; + jbyteArray our_msg = env->NewByteArray(ctx->key_size); + env->SetByteArrayRegion(our_msg, 0, ctx->key_size, (jbyte *) ctx->key); + return our_msg; +} + +static jboolean PairingContext_InitCipher(JNIEnv *env, jobject obj, jlong ptr, jbyteArray jTheirMsg) { + auto res = JNI_TRUE; + + auto ctx = (PairingContextNative *) ptr; + auto spake2_ctx = ctx->spake2_ctx; + auto their_msg_size = env->GetArrayLength(jTheirMsg); + + if (their_msg_size > SPAKE2_MAX_MSG_SIZE) { + LOGE("their_msg size [%d] greater then max size [%d].", their_msg_size, SPAKE2_MAX_MSG_SIZE); + return JNI_FALSE; + } + + auto their_msg = env->GetByteArrayElements(jTheirMsg, nullptr); + + size_t key_material_len = 0; + uint8_t key_material[SPAKE2_MAX_KEY_SIZE]; + int status = SPAKE2_process_msg(spake2_ctx, key_material, &key_material_len, + sizeof(key_material), (uint8_t *) their_msg, their_msg_size); + + env->ReleaseByteArrayElements(jTheirMsg, their_msg, 0); + + if (status != 1) { + LOGE("Unable to process their public key"); + return JNI_FALSE; + } + + // -------- + uint8_t key[kHkdfKeyLength]; + uint8_t info[] = "adb pairing_auth aes-128-gcm key"; + + status = HKDF(key, sizeof(key), EVP_sha256(), key_material, key_material_len, nullptr, 0, info, + sizeof(info) - 1); + if (status != 1) { + LOGE("HKDF"); + return JNI_FALSE; + } + + ctx->aes_ctx = EVP_AEAD_CTX_new(EVP_aead_aes_128_gcm(), key, sizeof(key), EVP_AEAD_DEFAULT_TAG_LENGTH); + + if (!ctx->aes_ctx) { + LOGE("EVP_AEAD_CTX_new"); + return JNI_FALSE; + } + + return res; +} + +static jbyteArray PairingContext_Encrypt(JNIEnv *env, jobject obj, jlong ptr, jbyteArray jIn) { + auto ctx = (PairingContextNative *) ptr; + auto aes_ctx = ctx->aes_ctx; + + auto in = env->GetByteArrayElements(jIn, nullptr); + auto in_size = env->GetArrayLength(jIn); + + auto out_size = (size_t) in_size + EVP_AEAD_max_overhead(EVP_AEAD_CTX_aead(ctx->aes_ctx)); + uint8_t out[out_size]; + + auto nonce_size = EVP_AEAD_nonce_length(EVP_AEAD_CTX_aead(aes_ctx)); + uint8_t nonce[nonce_size]; + memset(nonce, 0, nonce_size); + memcpy(nonce, &ctx->enc_sequence, sizeof(ctx->enc_sequence)); + + size_t written_sz; + int status = EVP_AEAD_CTX_seal(aes_ctx, out, &written_sz, out_size, nonce, nonce_size, (uint8_t *) in, in_size, nullptr, 0); + + env->ReleaseByteArrayElements(jIn, in, 0); + + if (!status) { + LOGE("Failed to encrypt (in_len=%d, out_len=%" PRIuPTR", out_len_needed=%d)", in_size, out_size, in_size); + return nullptr; + } + ++ctx->enc_sequence; + + jbyteArray jOut = env->NewByteArray(written_sz); + env->SetByteArrayRegion(jOut, 0, written_sz, (jbyte *) out); + return jOut; +} + +static jbyteArray PairingContext_Decrypt(JNIEnv *env, jobject obj, jlong ptr, jbyteArray jIn) { + auto ctx = (PairingContextNative *) ptr; + auto aes_ctx = ctx->aes_ctx; + + auto in = env->GetByteArrayElements(jIn, nullptr); + auto in_size = env->GetArrayLength(jIn); + + auto out_size = (size_t) in_size; + uint8_t out[out_size]; + + auto nonce_size = EVP_AEAD_nonce_length(EVP_AEAD_CTX_aead(aes_ctx)); + uint8_t nonce[nonce_size]; + memset(nonce, 0, nonce_size); + memcpy(nonce, &ctx->dec_sequence, sizeof(ctx->dec_sequence)); + + size_t written_sz; + int status = EVP_AEAD_CTX_open(aes_ctx, out, &written_sz, out_size, nonce, nonce_size, (uint8_t *) in, in_size, nullptr, 0); + + env->ReleaseByteArrayElements(jIn, in, 0); + + if (!status) { + LOGE("Failed to decrypt (in_len=%d, out_len=%" PRIuPTR", out_len_needed=%d)", in_size, out_size, in_size); + return nullptr; + } + ++ctx->dec_sequence; + + jbyteArray jOut = env->NewByteArray(written_sz); + env->SetByteArrayRegion(jOut, 0, written_sz, (jbyte *) out); + return jOut; +} + +static void PairingContext_Destroy(JNIEnv *env, jobject obj, jlong ptr) { + auto ctx = (PairingContextNative *) ptr; + SPAKE2_CTX_free(ctx->spake2_ctx); + if (ctx->aes_ctx) EVP_AEAD_CTX_free(ctx->aes_ctx); + free(ctx); +} + +// --------------------------------------------------------- + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { + JNIEnv *env = nullptr; + + if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) + return -1; + + JNINativeMethod methods_PairingContext[] = { + {"nativeConstructor", "(Z[B)J", (void *) PairingContext_Constructor}, + {"nativeMsg", "(J)[B", (void *) PairingContext_Msg}, + {"nativeInitCipher", "(J[B)Z", (void *) PairingContext_InitCipher}, + {"nativeEncrypt", "(J[B)[B", (void *) PairingContext_Encrypt}, + {"nativeDecrypt", "(J[B)[B", (void *) PairingContext_Decrypt}, + {"nativeDestroy", "(J)V", (void *) PairingContext_Destroy}, + }; + + env->RegisterNatives(env->FindClass("io/github/sds100/keymapper/sysbridge/adb/PairingContext"), + methods_PairingContext, + sizeof(methods_PairingContext) / sizeof(JNINativeMethod)); + + return JNI_VERSION_1_6; +} diff --git a/sysbridge/src/main/cpp/adb_pairing.h b/sysbridge/src/main/cpp/adb_pairing.h new file mode 100644 index 0000000000..3f9eb70a66 --- /dev/null +++ b/sysbridge/src/main/cpp/adb_pairing.h @@ -0,0 +1,4 @@ +#ifndef ADB_H +#define ADB_H + +#endif // ADB_H diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h new file mode 100644 index 0000000000..d07bb0aae2 --- /dev/null +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BnEvdevCallback.h @@ -0,0 +1,56 @@ +/* + * This file is auto-generated. DO NOT MODIFY. + * Using: /Users/sethd/Library/Android/sdk/build-tools/35.0.0/aidl --lang=ndk -o /Users/sethd/Projects/KeyMapper/foss/sysbridge/src/main/cpp/aidl -h /Users/sethd/Projects/KeyMapper/foss/sysbridge/src/main/cpp -I /Users/sethd/Projects/KeyMapper/foss/sysbridge/src/main/aidl /Users/sethd/Projects/KeyMapper/foss/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.aidl + */ +#pragma once + +#include "aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h" + +#include +#include + +#ifndef __BIONIC__ +#ifndef __assert2 +#define __assert2(a,b,c,d) ((void)0) +#endif +#endif + +namespace aidl { +namespace io { +namespace github { +namespace sds100 { +namespace keymapper { +namespace sysbridge { +class BnEvdevCallback : public ::ndk::BnCInterface { +public: + BnEvdevCallback(); + virtual ~BnEvdevCallback(); +protected: + ::ndk::SpAIBinder createBinder() override; +private: +}; +class IEvdevCallbackDelegator : public BnEvdevCallback { +public: + explicit IEvdevCallbackDelegator(const std::shared_ptr &impl) : _impl(impl) { + } + + ::ndk::ScopedAStatus onEvdevEventLoopStarted() override { + return _impl->onEvdevEventLoopStarted(); + } + ::ndk::ScopedAStatus onEvdevEvent(const std::string& in_devicePath, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, bool* _aidl_return) override { + return _impl->onEvdevEvent(in_devicePath, in_timeSec, in_timeUsec, in_type, in_code, in_value, in_androidCode, _aidl_return); + } + ::ndk::ScopedAStatus onEmergencyKillSystemBridge() override { + return _impl->onEmergencyKillSystemBridge(); + } +protected: +private: + std::shared_ptr _impl; +}; + +} // namespace sysbridge +} // namespace keymapper +} // namespace sds100 +} // namespace github +} // namespace io +} // namespace aidl diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h new file mode 100644 index 0000000000..33c5c4a1ff --- /dev/null +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/BpEvdevCallback.h @@ -0,0 +1,31 @@ +/* + * This file is auto-generated. DO NOT MODIFY. + * Using: /Users/sethd/Library/Android/sdk/build-tools/35.0.0/aidl --lang=ndk -o /Users/sethd/Projects/KeyMapper/foss/sysbridge/src/main/cpp/aidl -h /Users/sethd/Projects/KeyMapper/foss/sysbridge/src/main/cpp -I /Users/sethd/Projects/KeyMapper/foss/sysbridge/src/main/aidl /Users/sethd/Projects/KeyMapper/foss/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.aidl + */ +#pragma once + +#include "aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h" + +#include + +namespace aidl { +namespace io { +namespace github { +namespace sds100 { +namespace keymapper { +namespace sysbridge { +class BpEvdevCallback : public ::ndk::BpCInterface { +public: + explicit BpEvdevCallback(const ::ndk::SpAIBinder& binder); + virtual ~BpEvdevCallback(); + + ::ndk::ScopedAStatus onEvdevEventLoopStarted() override; + ::ndk::ScopedAStatus onEvdevEvent(const std::string& in_devicePath, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, bool* _aidl_return) override; + ::ndk::ScopedAStatus onEmergencyKillSystemBridge() override; +}; +} // namespace sysbridge +} // namespace keymapper +} // namespace sds100 +} // namespace github +} // namespace io +} // namespace aidl diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp new file mode 100644 index 0000000000..65943c8ecf --- /dev/null +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.cpp @@ -0,0 +1,297 @@ +/* + * This file is auto-generated. DO NOT MODIFY. + * Using: /Users/sethd/Library/Android/sdk/build-tools/35.0.0/aidl --lang=ndk -o /Users/sethd/Projects/KeyMapper/foss/sysbridge/src/main/cpp/aidl -h /Users/sethd/Projects/KeyMapper/foss/sysbridge/src/main/cpp -I /Users/sethd/Projects/KeyMapper/foss/sysbridge/src/main/aidl /Users/sethd/Projects/KeyMapper/foss/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.aidl + */ +#include "aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h" + +#include +#include +#include + +namespace aidl { +namespace io { +namespace github { +namespace sds100 { +namespace keymapper { +namespace sysbridge { +static binder_status_t _aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback_onTransact(AIBinder* _aidl_binder, transaction_code_t _aidl_code, const AParcel* _aidl_in, AParcel* _aidl_out) { + (void)_aidl_in; + (void)_aidl_out; + binder_status_t _aidl_ret_status = STATUS_UNKNOWN_TRANSACTION; + std::shared_ptr _aidl_impl = std::static_pointer_cast(::ndk::ICInterface::asInterface(_aidl_binder)); + switch (_aidl_code) { + case (FIRST_CALL_TRANSACTION + 0 /*onEvdevEventLoopStarted*/): { + + ::ndk::ScopedAStatus _aidl_status = _aidl_impl->onEvdevEventLoopStarted(); + _aidl_ret_status = STATUS_OK; + break; + } + case (FIRST_CALL_TRANSACTION + 1 /*onEvdevEvent*/): { + std::string in_devicePath; + int64_t in_timeSec; + int64_t in_timeUsec; + int32_t in_type; + int32_t in_code; + int32_t in_value; + int32_t in_androidCode; + bool _aidl_return; + + _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_devicePath); + if (_aidl_ret_status != STATUS_OK) break; + + _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_timeSec); + if (_aidl_ret_status != STATUS_OK) break; + + _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_timeUsec); + if (_aidl_ret_status != STATUS_OK) break; + + _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_type); + if (_aidl_ret_status != STATUS_OK) break; + + _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_code); + if (_aidl_ret_status != STATUS_OK) break; + + _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_value); + if (_aidl_ret_status != STATUS_OK) break; + + _aidl_ret_status = ::ndk::AParcel_readData(_aidl_in, &in_androidCode); + if (_aidl_ret_status != STATUS_OK) break; + + ::ndk::ScopedAStatus _aidl_status = _aidl_impl->onEvdevEvent(in_devicePath, in_timeSec, in_timeUsec, in_type, in_code, in_value, in_androidCode, &_aidl_return); + _aidl_ret_status = AParcel_writeStatusHeader(_aidl_out, _aidl_status.get()); + if (_aidl_ret_status != STATUS_OK) break; + + if (!AStatus_isOk(_aidl_status.get())) break; + + _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_out, _aidl_return); + if (_aidl_ret_status != STATUS_OK) break; + + break; + } + case (FIRST_CALL_TRANSACTION + 2 /*onEmergencyKillSystemBridge*/): { + + ::ndk::ScopedAStatus _aidl_status = _aidl_impl->onEmergencyKillSystemBridge(); + _aidl_ret_status = AParcel_writeStatusHeader(_aidl_out, _aidl_status.get()); + if (_aidl_ret_status != STATUS_OK) break; + + if (!AStatus_isOk(_aidl_status.get())) break; + + break; + } + } + return _aidl_ret_status; +} + +static AIBinder_Class* _g_aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback_clazz = ::ndk::ICInterface::defineClass(IEvdevCallback::descriptor, _aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback_onTransact); + +BpEvdevCallback::BpEvdevCallback(const ::ndk::SpAIBinder& binder) : BpCInterface(binder) {} +BpEvdevCallback::~BpEvdevCallback() {} + +::ndk::ScopedAStatus BpEvdevCallback::onEvdevEventLoopStarted() { + binder_status_t _aidl_ret_status = STATUS_OK; + ::ndk::ScopedAStatus _aidl_status; + ::ndk::ScopedAParcel _aidl_in; + ::ndk::ScopedAParcel _aidl_out; + + _aidl_ret_status = AIBinder_prepareTransaction(asBinder().get(), _aidl_in.getR()); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + _aidl_ret_status = AIBinder_transact( + asBinder().get(), + (FIRST_CALL_TRANSACTION + 0 /*onEvdevEventLoopStarted*/), + _aidl_in.getR(), + _aidl_out.getR(), + FLAG_ONEWAY + #ifdef BINDER_STABILITY_SUPPORT + | FLAG_PRIVATE_LOCAL + #endif // BINDER_STABILITY_SUPPORT + ); + if (_aidl_ret_status == STATUS_UNKNOWN_TRANSACTION && IEvdevCallback::getDefaultImpl()) { + _aidl_status = IEvdevCallback::getDefaultImpl()->onEvdevEventLoopStarted(); + goto _aidl_status_return; + } + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + _aidl_error: + _aidl_status.set(AStatus_fromStatus(_aidl_ret_status)); + _aidl_status_return: + return _aidl_status; +} +::ndk::ScopedAStatus BpEvdevCallback::onEvdevEvent(const std::string& in_devicePath, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, bool* _aidl_return) { + binder_status_t _aidl_ret_status = STATUS_OK; + ::ndk::ScopedAStatus _aidl_status; + ::ndk::ScopedAParcel _aidl_in; + ::ndk::ScopedAParcel _aidl_out; + + _aidl_ret_status = AIBinder_prepareTransaction(asBinder().get(), _aidl_in.getR()); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_devicePath); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_timeSec); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_timeUsec); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_type); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_code); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_value); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + _aidl_ret_status = ::ndk::AParcel_writeData(_aidl_in.get(), in_androidCode); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + _aidl_ret_status = AIBinder_transact( + asBinder().get(), + (FIRST_CALL_TRANSACTION + 1 /*onEvdevEvent*/), + _aidl_in.getR(), + _aidl_out.getR(), + 0 + #ifdef BINDER_STABILITY_SUPPORT + | FLAG_PRIVATE_LOCAL + #endif // BINDER_STABILITY_SUPPORT + ); + if (_aidl_ret_status == STATUS_UNKNOWN_TRANSACTION && IEvdevCallback::getDefaultImpl()) { + _aidl_status = IEvdevCallback::getDefaultImpl()->onEvdevEvent(in_devicePath, in_timeSec, in_timeUsec, in_type, in_code, in_value, in_androidCode, _aidl_return); + goto _aidl_status_return; + } + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + _aidl_ret_status = AParcel_readStatusHeader(_aidl_out.get(), _aidl_status.getR()); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + if (!AStatus_isOk(_aidl_status.get())) goto _aidl_status_return; + _aidl_ret_status = ::ndk::AParcel_readData(_aidl_out.get(), _aidl_return); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + _aidl_error: + _aidl_status.set(AStatus_fromStatus(_aidl_ret_status)); + _aidl_status_return: + return _aidl_status; +} +::ndk::ScopedAStatus BpEvdevCallback::onEmergencyKillSystemBridge() { + binder_status_t _aidl_ret_status = STATUS_OK; + ::ndk::ScopedAStatus _aidl_status; + ::ndk::ScopedAParcel _aidl_in; + ::ndk::ScopedAParcel _aidl_out; + + _aidl_ret_status = AIBinder_prepareTransaction(asBinder().get(), _aidl_in.getR()); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + _aidl_ret_status = AIBinder_transact( + asBinder().get(), + (FIRST_CALL_TRANSACTION + 2 /*onEmergencyKillSystemBridge*/), + _aidl_in.getR(), + _aidl_out.getR(), + 0 + #ifdef BINDER_STABILITY_SUPPORT + | FLAG_PRIVATE_LOCAL + #endif // BINDER_STABILITY_SUPPORT + ); + if (_aidl_ret_status == STATUS_UNKNOWN_TRANSACTION && IEvdevCallback::getDefaultImpl()) { + _aidl_status = IEvdevCallback::getDefaultImpl()->onEmergencyKillSystemBridge(); + goto _aidl_status_return; + } + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + _aidl_ret_status = AParcel_readStatusHeader(_aidl_out.get(), _aidl_status.getR()); + if (_aidl_ret_status != STATUS_OK) goto _aidl_error; + + if (!AStatus_isOk(_aidl_status.get())) goto _aidl_status_return; + _aidl_error: + _aidl_status.set(AStatus_fromStatus(_aidl_ret_status)); + _aidl_status_return: + return _aidl_status; +} +// Source for BnEvdevCallback +BnEvdevCallback::BnEvdevCallback() {} +BnEvdevCallback::~BnEvdevCallback() {} +::ndk::SpAIBinder BnEvdevCallback::createBinder() { + AIBinder* binder = AIBinder_new(_g_aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback_clazz, static_cast(this)); + #ifdef BINDER_STABILITY_SUPPORT + AIBinder_markCompilationUnitStability(binder); + #endif // BINDER_STABILITY_SUPPORT + return ::ndk::SpAIBinder(binder); +} +// Source for IEvdevCallback +const char* IEvdevCallback::descriptor = "io.github.sds100.keymapper.sysbridge.IEvdevCallback"; +IEvdevCallback::IEvdevCallback() {} +IEvdevCallback::~IEvdevCallback() {} + + +std::shared_ptr IEvdevCallback::fromBinder(const ::ndk::SpAIBinder& binder) { + if (!AIBinder_associateClass(binder.get(), _g_aidl_io_github_sds100_keymapper_sysbridge_IEvdevCallback_clazz)) { + #if __ANDROID_API__ >= 31 + const AIBinder_Class* originalClass = AIBinder_getClass(binder.get()); + if (originalClass == nullptr) return nullptr; + if (0 == strcmp(AIBinder_Class_getDescriptor(originalClass), descriptor)) { + return ::ndk::SharedRefBase::make(binder); + } + #endif + return nullptr; + } + std::shared_ptr<::ndk::ICInterface> interface = ::ndk::ICInterface::asInterface(binder.get()); + if (interface) { + return std::static_pointer_cast(interface); + } + return ::ndk::SharedRefBase::make(binder); +} + +binder_status_t IEvdevCallback::writeToParcel(AParcel* parcel, const std::shared_ptr& instance) { + return AParcel_writeStrongBinder(parcel, instance ? instance->asBinder().get() : nullptr); +} +binder_status_t IEvdevCallback::readFromParcel(const AParcel* parcel, std::shared_ptr* instance) { + ::ndk::SpAIBinder binder; + binder_status_t status = AParcel_readStrongBinder(parcel, binder.getR()); + if (status != STATUS_OK) return status; + *instance = IEvdevCallback::fromBinder(binder); + return STATUS_OK; +} +bool IEvdevCallback::setDefaultImpl(const std::shared_ptr& impl) { + // Only one user of this interface can use this function + // at a time. This is a heuristic to detect if two different + // users in the same process use this function. + assert(!IEvdevCallback::default_impl); + if (impl) { + IEvdevCallback::default_impl = impl; + return true; + } + return false; +} +const std::shared_ptr& IEvdevCallback::getDefaultImpl() { + return IEvdevCallback::default_impl; +} +std::shared_ptr IEvdevCallback::default_impl = nullptr; +::ndk::ScopedAStatus IEvdevCallbackDefault::onEvdevEventLoopStarted() { + ::ndk::ScopedAStatus _aidl_status; + _aidl_status.set(AStatus_fromStatus(STATUS_UNKNOWN_TRANSACTION)); + return _aidl_status; +} +::ndk::ScopedAStatus IEvdevCallbackDefault::onEvdevEvent(const std::string& /*in_devicePath*/, int64_t /*in_timeSec*/, int64_t /*in_timeUsec*/, int32_t /*in_type*/, int32_t /*in_code*/, int32_t /*in_value*/, int32_t /*in_androidCode*/, bool* /*_aidl_return*/) { + ::ndk::ScopedAStatus _aidl_status; + _aidl_status.set(AStatus_fromStatus(STATUS_UNKNOWN_TRANSACTION)); + return _aidl_status; +} +::ndk::ScopedAStatus IEvdevCallbackDefault::onEmergencyKillSystemBridge() { + ::ndk::ScopedAStatus _aidl_status; + _aidl_status.set(AStatus_fromStatus(STATUS_UNKNOWN_TRANSACTION)); + return _aidl_status; +} +::ndk::SpAIBinder IEvdevCallbackDefault::asBinder() { + return ::ndk::SpAIBinder(); +} +bool IEvdevCallbackDefault::isRemote() { + return false; +} +} // namespace sysbridge +} // namespace keymapper +} // namespace sds100 +} // namespace github +} // namespace io +} // namespace aidl diff --git a/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h new file mode 100644 index 0000000000..b81eafec61 --- /dev/null +++ b/sysbridge/src/main/cpp/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h @@ -0,0 +1,60 @@ +/* + * This file is auto-generated. DO NOT MODIFY. + * Using: /Users/sethd/Library/Android/sdk/build-tools/35.0.0/aidl --lang=ndk -o /Users/sethd/Projects/KeyMapper/foss/sysbridge/src/main/cpp/aidl -h /Users/sethd/Projects/KeyMapper/foss/sysbridge/src/main/cpp -I /Users/sethd/Projects/KeyMapper/foss/sysbridge/src/main/aidl /Users/sethd/Projects/KeyMapper/foss/sysbridge/src/main/aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.aidl + */ +#pragma once + +#include +#include +#include +#include +#include +#include +#ifdef BINDER_STABILITY_SUPPORT +#include +#endif // BINDER_STABILITY_SUPPORT + +namespace aidl { +namespace io { +namespace github { +namespace sds100 { +namespace keymapper { +namespace sysbridge { +class IEvdevCallbackDelegator; + +class IEvdevCallback : public ::ndk::ICInterface { +public: + typedef IEvdevCallbackDelegator DefaultDelegator; + static const char* descriptor; + IEvdevCallback(); + virtual ~IEvdevCallback(); + + static constexpr uint32_t TRANSACTION_onEvdevEventLoopStarted = FIRST_CALL_TRANSACTION + 0; + static constexpr uint32_t TRANSACTION_onEvdevEvent = FIRST_CALL_TRANSACTION + 1; + static constexpr uint32_t TRANSACTION_onEmergencyKillSystemBridge = FIRST_CALL_TRANSACTION + 2; + + static std::shared_ptr fromBinder(const ::ndk::SpAIBinder& binder); + static binder_status_t writeToParcel(AParcel* parcel, const std::shared_ptr& instance); + static binder_status_t readFromParcel(const AParcel* parcel, std::shared_ptr* instance); + static bool setDefaultImpl(const std::shared_ptr& impl); + static const std::shared_ptr& getDefaultImpl(); + virtual ::ndk::ScopedAStatus onEvdevEventLoopStarted() = 0; + virtual ::ndk::ScopedAStatus onEvdevEvent(const std::string& in_devicePath, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, bool* _aidl_return) = 0; + virtual ::ndk::ScopedAStatus onEmergencyKillSystemBridge() = 0; +private: + static std::shared_ptr default_impl; +}; +class IEvdevCallbackDefault : public IEvdevCallback { +public: + ::ndk::ScopedAStatus onEvdevEventLoopStarted() override; + ::ndk::ScopedAStatus onEvdevEvent(const std::string& in_devicePath, int64_t in_timeSec, int64_t in_timeUsec, int32_t in_type, int32_t in_code, int32_t in_value, int32_t in_androidCode, bool* _aidl_return) override; + ::ndk::ScopedAStatus onEmergencyKillSystemBridge() override; + ::ndk::SpAIBinder asBinder() override; + bool isRemote() override; +}; +} // namespace sysbridge +} // namespace keymapper +} // namespace sds100 +} // namespace github +} // namespace io +} // namespace aidl diff --git a/sysbridge/src/main/cpp/android.cpp b/sysbridge/src/main/cpp/android.cpp new file mode 100644 index 0000000000..a24006a45d --- /dev/null +++ b/sysbridge/src/main/cpp/android.cpp @@ -0,0 +1,31 @@ +#include +#include +#include +#include +#include +#include + +namespace android { + + int GetApiLevel() { + static int apiLevel = 0; + if (apiLevel > 0) return apiLevel; + + char buf[PROP_VALUE_MAX + 1]; + if (__system_property_get("ro.build.version.sdk", buf) > 0) + apiLevel = atoi(buf); + + return apiLevel; + } + + int GetPreviewApiLevel() { + static int previewApiLevel = 0; + if (previewApiLevel > 0) return previewApiLevel; + + char buf[PROP_VALUE_MAX + 1]; + if (__system_property_get("ro.build.version.preview_sdk", buf) > 0) + previewApiLevel = atoi(buf); + + return previewApiLevel; + } +} \ No newline at end of file diff --git a/sysbridge/src/main/cpp/android.h b/sysbridge/src/main/cpp/android.h new file mode 100644 index 0000000000..a4b78fba96 --- /dev/null +++ b/sysbridge/src/main/cpp/android.h @@ -0,0 +1,8 @@ +#pragma once + +namespace android { + + int GetApiLevel(); + + int GetPreviewApiLevel(); +} \ No newline at end of file diff --git a/sysbridge/src/main/cpp/android/ftl/algorithm.h b/sysbridge/src/main/cpp/android/ftl/algorithm.h new file mode 100644 index 0000000000..81993de177 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/algorithm.h @@ -0,0 +1,108 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include + +namespace android::ftl { + +// Determines if a container contains a value. This is a simplified version of the C++23 +// std::ranges::contains function. +// +// const ftl::StaticVector vector = {1, 2, 3}; +// assert(ftl::contains(vector, 1)); +// +// TODO: Remove in C++23. + template + auto contains(const Container &container, const Value &value) -> bool { + return std::find(container.begin(), container.end(), value) != container.end(); + } + +// Adapter for std::find_if that converts the return value from iterator to optional. +// +// const ftl::StaticVector vector = {"upside"sv, "down"sv, "cake"sv}; +// assert(ftl::find_if(vector, [](const auto& str) { return str.front() == 'c'; }) == "cake"sv); +// + template + constexpr auto find_if(const Container &container, Predicate &&predicate) + -> Optional> { + const auto it = std::find_if(std::cbegin(container), std::cend(container), + std::forward(predicate)); + if (it == std::cend(container)) return {}; + return std::cref(*it); + } + +// Transformers for ftl::find_if on a map-like `Container` that contains key-value pairs. +// +// const ftl::SmallMap map = ftl::init::map>( +// 12, "snow"sv, "cone"sv)(13, "tiramisu"sv)(14, "upside"sv, "down"sv, "cake"sv); +// +// using Map = decltype(map); +// +// assert(14 == ftl::find_if(map, [](const auto& pair) { +// return pair.second.size() == 3; +// }).transform(ftl::to_key)); +// +// const auto opt = ftl::find_if(map, [](const auto& pair) { +// return pair.second.size() == 1; +// }).transform(ftl::to_mapped_ref); +// +// assert(opt); +// assert(opt->get() == ftl::StaticVector("tiramisu"sv)); +// + template + constexpr auto to_key(const Pair &pair) -> Key { + return pair.first; + } + + template + constexpr auto to_mapped_ref(const Pair &pair) -> std::reference_wrapper { + return std::cref(pair.second); + } + +// Combinator for ftl::Optional::or_else when T is std::reference_wrapper. Given a +// lambda argument that returns a `constexpr` value, ftl::static_ref binds a reference to a +// static T initialized to that constant. +// +// const ftl::SmallMap map = ftl::init::map(13, "tiramisu"sv)(14, "upside-down cake"sv); +// assert("???"sv == +// map.get(20).or_else(ftl::static_ref([] { return "???"sv; }))->get()); +// +// using Map = decltype(map); +// +// assert("snow cone"sv == +// ftl::find_if(map, [](const auto& pair) { return pair.second.front() == 's'; }) +// .transform(ftl::to_mapped_ref) +// .or_else(ftl::static_ref([] { return "snow cone"sv; })) +// ->get()); +// + template + constexpr auto static_ref(F &&f) { + return [f = std::forward(f)] { + constexpr auto kInitializer = f(); + static const T kValue = kInitializer; + return Optional(std::cref(kValue)); + }; + } + +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/cast.h b/sysbridge/src/main/cpp/android/ftl/cast.h new file mode 100644 index 0000000000..64129a1a58 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/cast.h @@ -0,0 +1,86 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include + +namespace android::ftl { + + enum class CastSafety { + kSafe, kUnderflow, kOverflow + }; + +// Returns whether static_cast(v) is safe, or would result in underflow or overflow. +// +// static_assert(ftl::cast_safety(-1) == ftl::CastSafety::kUnderflow); +// static_assert(ftl::cast_safety(128u) == ftl::CastSafety::kOverflow); +// +// static_assert(ftl::cast_safety(-.1f) == ftl::CastSafety::kUnderflow); +// static_assert(ftl::cast_safety(static_cast(INT32_MAX)) == +// ftl::CastSafety::kOverflow); +// +// static_assert(ftl::cast_safety(-DBL_MAX) == ftl::CastSafety::kUnderflow); +// + template + constexpr CastSafety cast_safety(T v) { + static_assert(std::is_arithmetic_v); + static_assert(std::is_arithmetic_v); + + constexpr bool kFromSigned = std::is_signed_v; + constexpr bool kToSigned = std::is_signed_v; + + using details::max_exponent; + + // If the R range contains the T range, then casting is always safe. + if constexpr ((kFromSigned == kToSigned && max_exponent >= max_exponent) || + (!kFromSigned && kToSigned && max_exponent > max_exponent)) { + return CastSafety::kSafe; + } + + using C = std::common_type_t; + + if constexpr (kFromSigned) { + using L = details::safe_limits; + + if constexpr (kToSigned) { + // Signed to signed. + if (v < L::lowest()) return CastSafety::kUnderflow; + return v <= L::max() ? CastSafety::kSafe : CastSafety::kOverflow; + } else { + // Signed to unsigned. + if (v < 0) return CastSafety::kUnderflow; + return static_cast(v) <= static_cast(L::max()) ? CastSafety::kSafe + : CastSafety::kOverflow; + } + } else { + using L = std::numeric_limits; + + if constexpr (kToSigned) { + // Unsigned to signed. + return static_cast(v) <= static_cast(L::max()) ? CastSafety::kSafe + : CastSafety::kOverflow; + } else { + // Unsigned to unsigned. + return v <= L::max() ? CastSafety::kSafe : CastSafety::kOverflow; + } + } + } + +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/concat.h b/sysbridge/src/main/cpp/android/ftl/concat.h new file mode 100644 index 0000000000..b826a2e858 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/concat.h @@ -0,0 +1,90 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace android::ftl { + +// Lightweight (not allocating nor sprintf-based) concatenation. The variadic arguments can be +// values of integral type (including bool and char), string literals, or strings whose length +// is constrained: +// +// std::string_view name = "Volume"; +// ftl::Concat string(ftl::truncated<3>(name), ": ", -3, " dB"); +// +// assert(string.str() == "Vol: -3 dB"); +// assert(string.c_str()[string.size()] == '\0'); +// + template + struct Concat; + + template + struct Concat : Concat::N, Ts...> { + explicit constexpr Concat(T v, Ts... args) { append(v, args...); } + + protected: + constexpr Concat() = default; + + constexpr void append(T v, Ts... args) { + using Str = details::StaticString; + const Str str(v); + + // TODO: Replace with constexpr std::copy in C++20. + for (auto it = str.view.begin(); it != str.view.end();) { + *this->end_++ = *it++; + } + + using Base = Concat; + this->Base::append(args...); + } + }; + + template + struct Concat { + static constexpr std::size_t max_size() { return N; } + + constexpr std::size_t size() const { return static_cast(end_ - buffer_); } + + constexpr const char *c_str() const { return buffer_; } + + constexpr std::string_view str() const { + // TODO: Replace with {buffer_, end_} in C++20. + return {buffer_, size()}; + } + + protected: + constexpr Concat() : end_(buffer_) {} + + constexpr Concat(const Concat &) = delete; + + constexpr void append() { *end_ = '\0'; } + + char buffer_[N + 1]; + char *end_; + }; + +// Deduction guide. + template + Concat(Ts &&...) -> Concat<0, Ts...>; + + template + constexpr auto truncated(std::string_view v) { + return details::Truncated{v}; + } + +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/details/array_traits.h b/sysbridge/src/main/cpp/android/ftl/details/array_traits.h new file mode 100644 index 0000000000..f66742598c --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/details/array_traits.h @@ -0,0 +1,181 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +#define FTL_ARRAY_TRAIT(T, U) using U = typename details::ArrayTraits::U + +namespace android::ftl::details { + + template + struct ArrayTraits { + using value_type = T; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + + using pointer = value_type *; + using reference = value_type &; + using iterator = pointer; + using reverse_iterator = std::reverse_iterator; + + using const_pointer = const value_type *; + using const_reference = const value_type &; + using const_iterator = const_pointer; + using const_reverse_iterator = std::reverse_iterator; + + template + static constexpr pointer construct_at(const_iterator it, Args &&... args) { + void *const ptr = const_cast(static_cast(it)); + if constexpr (std::is_constructible_v) { + // TODO: Replace with std::construct_at in C++20. + return new(ptr) value_type(std::forward(args)...); + } else { + // Fall back to list initialization. + return new(ptr) value_type{std::forward(args)...}; + } + } + + // TODO: Make constexpr in C++20. + template + static reference replace_at(const_iterator it, Args &&... args) { + value_type element{std::forward(args)...}; + return replace_at(it, std::move(element)); + } + + // TODO: Make constexpr in C++20. + static reference replace_at(const_iterator it, value_type &&value) { + std::destroy_at(it); + // This is only safe because exceptions are disabled. + return *construct_at(it, std::move(value)); + } + + // TODO: Make constexpr in C++20. + static void in_place_swap(reference a, reference b) { + value_type c{std::move(a)}; + replace_at(&a, std::move(b)); + replace_at(&b, std::move(c)); + } + + // TODO: Make constexpr in C++20. + static void in_place_swap_ranges(iterator first1, iterator last1, iterator first2) { + while (first1 != last1) { + in_place_swap(*first1++, *first2++); + } + } + + // TODO: Replace with std::uninitialized_copy in C++20. + template + static void uninitialized_copy(Iterator first, Iterator last, const_iterator out) { + while (first != last) { + construct_at(out++, *first++); + } + } + }; + +// CRTP mixin to define iterator functions in terms of non-const Self::begin and Self::end. + template + class ArrayIterators { + FTL_ARRAY_TRAIT(T, size_type); + + FTL_ARRAY_TRAIT(T, reference); + FTL_ARRAY_TRAIT(T, iterator); + FTL_ARRAY_TRAIT(T, reverse_iterator); + + FTL_ARRAY_TRAIT(T, const_reference); + FTL_ARRAY_TRAIT(T, const_iterator); + FTL_ARRAY_TRAIT(T, const_reverse_iterator); + + Self &self() const { return *const_cast(static_cast(this)); } + + public: + const_iterator begin() const { return cbegin(); } + + const_iterator cbegin() const { return self().begin(); } + + const_iterator end() const { return cend(); } + + const_iterator cend() const { return self().end(); } + + reverse_iterator rbegin() { return std::make_reverse_iterator(self().end()); } + + const_reverse_iterator rbegin() const { return crbegin(); } + + const_reverse_iterator crbegin() const { return self().rbegin(); } + + reverse_iterator rend() { return std::make_reverse_iterator(self().begin()); } + + const_reverse_iterator rend() const { return crend(); } + + const_reverse_iterator crend() const { return self().rend(); } + + iterator last() { return self().end() - 1; } + + const_iterator last() const { return self().last(); } + + reference front() { return *self().begin(); } + + const_reference front() const { return self().front(); } + + reference back() { return *last(); } + + const_reference back() const { return self().back(); } + + reference operator[](size_type i) { return *(self().begin() + i); } + + const_reference operator[](size_type i) const { return self()[i]; } + }; + +// Mixin to define comparison operators for an array-like template. +// TODO: Replace with operator<=> in C++20. + template class Array> + struct ArrayComparators { + template + friend bool operator==(const Array &lhs, const Array &rhs) { + return lhs.size() == rhs.size() && std::equal(lhs.begin(), lhs.end(), rhs.begin()); + } + + template + friend bool operator<(const Array &lhs, const Array &rhs) { + return std::lexicographical_compare(lhs.begin(), lhs.end(), rhs.begin(), rhs.end()); + } + + template + friend bool operator>(const Array &lhs, const Array &rhs) { + return rhs < lhs; + } + + template + friend bool operator!=(const Array &lhs, const Array &rhs) { + return !(lhs == rhs); + } + + template + friend bool operator>=(const Array &lhs, const Array &rhs) { + return !(lhs < rhs); + } + + template + friend bool operator<=(const Array &lhs, const Array &rhs) { + return !(lhs > rhs); + } + }; + +} // namespace android::ftl::details diff --git a/sysbridge/src/main/cpp/android/ftl/details/cast.h b/sysbridge/src/main/cpp/android/ftl/details/cast.h new file mode 100644 index 0000000000..8196ba61a7 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/details/cast.h @@ -0,0 +1,57 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +namespace android::ftl::details { + +// Exponent whose power of 2 is the (exclusive) upper bound of T. + template> + constexpr int max_exponent = std::is_floating_point_v ? L::max_exponent : L::digits; + +// Extension of std::numeric_limits that reduces the maximum for integral types T such that it +// has an exact representation for floating-point types F. For example, the maximum int32_t value +// is 2'147'483'647, but casting it to float commonly rounds up to 2'147'483'650.f, which cannot +// be safely converted back lest the signed overflow invokes undefined behavior. This pitfall is +// avoided by clearing the lower (31 - 24 =) 7 bits of precision to 2'147'483'520. Note that the +// minimum is representable. + template + struct safe_limits : std::numeric_limits { + static constexpr T max() { + using Base = std::numeric_limits; + + if constexpr (std::is_integral_v && std::is_floating_point_v) { + // Assume the mantissa is 24 bits for float, or 53 bits for double. + using Float = std::numeric_limits; + static_assert(Float::is_iec559); + + // If the integer is wider than the mantissa, clear the excess bits of precision. + constexpr int kShift = Base::digits - Float::digits; + if constexpr (kShift > 0) { + using U = std::make_unsigned_t; + constexpr U kOne = static_cast(1); + return static_cast(Base::max()) & ~((kOne << kShift) - kOne); + } + } + + return Base::max(); + } + }; + +} // namespace android::ftl::details diff --git a/sysbridge/src/main/cpp/android/ftl/details/concat.h b/sysbridge/src/main/cpp/android/ftl/details/concat.h new file mode 100644 index 0000000000..c1462a658c --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/details/concat.h @@ -0,0 +1,91 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include +#include + +namespace android::ftl::details { + + template + struct StaticString; + +// Booleans. + template + struct StaticString>> { + static constexpr std::size_t N = 5; // Length of "false". + + explicit constexpr StaticString(bool b) : view(b ? "true" : "false") {} + + const std::string_view view; + }; + +// Characters. + template + struct StaticString>> { + static constexpr std::size_t N = 1; + + explicit constexpr StaticString(char c) : character(c) {} + + const char character; + const std::string_view view{&character, 1u}; + }; + +// Integers, including the integer value of other character types like char32_t. + template + struct StaticString< + T, std::enable_if_t< + std::is_integral_v> && !is_bool_v && !is_char_v>> { + using U = remove_cvref_t; + static constexpr std::size_t N = to_chars_length_v; + + // TODO: Mark this and to_chars as `constexpr` in C++23. + explicit StaticString(U v) : view(to_chars(buffer, v)) {} + + to_chars_buffer_t buffer; + const std::string_view view; + }; + +// Character arrays. + template + struct StaticString { + static constexpr std::size_t N = M - 1; + + explicit constexpr StaticString(const char (&str)[M]) : view(str, N) {} + + const std::string_view view; + }; + + template + struct Truncated { + std::string_view view; + }; + +// Strings with constrained length. + template + struct StaticString, void> { + static constexpr std::size_t N = M; + + explicit constexpr StaticString(Truncated str) : view(str.view.substr(0, N)) {} + + const std::string_view view; + }; + +} // namespace android::ftl::details diff --git a/sysbridge/src/main/cpp/android/ftl/details/function.h b/sysbridge/src/main/cpp/android/ftl/details/function.h new file mode 100644 index 0000000000..8d7fc9bd58 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/details/function.h @@ -0,0 +1,137 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace android::ftl::details { + +// The maximum allowed value for the template argument `N` in +// `ftl::Function`. + constexpr size_t kFunctionMaximumN = 14; + +// Converts a member function pointer type `Ret(Class::*)(Args...)` to an equivalent non-member +// function type `Ret(Args...)`. + + template + struct remove_member_function_pointer; + + template + struct remove_member_function_pointer { + using type = Ret(Args...); + }; + + template + struct remove_member_function_pointer { + using type = Ret(Args...); + }; + + template + using remove_member_function_pointer_t = + typename remove_member_function_pointer::type; + +// Helper functions for binding to the supported targets. + + template + auto bind_opaque_no_op() -> Ret (*)(void *, Args...) { + return [](void *, Args...) -> Ret { + if constexpr (!std::is_void_v) { + return Ret{}; + } + }; + } + + template + auto bind_opaque_function_object(const F &) -> Ret (*)(void *, Args...) { + return [](void *opaque, Args... args) -> Ret { + return std::invoke(*static_cast(opaque), std::forward(args)...); + }; + } + + template + auto bind_member_function(Class *instance, Ret (*)(Args...) = nullptr) { + return [instance](Args... args) -> Ret { + return std::invoke(MemberFunction, instance, std::forward(args)...); + }; + } + + template + auto bind_free_function(Ret (*)(Args...) = nullptr) { + return [](Args... args) -> Ret { + return std::invoke(FreeFunction, std::forward(args)...); + }; + } + +// Traits class for the opaque storage used by Function. + + template + struct function_opaque_storage { + // The actual type used for the opaque storage. An `N` of zero specifies the minimum useful size, + // which allows a lambda with zero or one capture args. + using type = std::array; + + template + static constexpr bool require_trivially_copyable = std::is_trivially_copyable_v; + + template + static constexpr bool require_trivially_destructible = std::is_trivially_destructible_v; + + template + static constexpr bool require_will_fit_in_opaque_storage = sizeof(S) <= sizeof(type); + + template + static constexpr bool require_alignment_compatible = + std::alignment_of_v <= std::alignment_of_v; + + // Copies `src` into the opaque storage, and returns that storage. + template + static type opaque_copy(const S &src) { + // TODO: Replace with C++20 concepts/constraints which can give more details. + static_assert(require_trivially_copyable, + "ftl::Function can only store lambdas that capture trivially copyable data."); + static_assert( + require_trivially_destructible, + "ftl::Function can only store lambdas that capture trivially destructible data."); + static_assert(require_will_fit_in_opaque_storage, + "ftl::Function has limited storage for lambda captured state. Maybe you need to " + "increase N?"); + static_assert(require_alignment_compatible); + + type opaque; + std::memcpy(opaque.data(), &src, sizeof(S)); + return opaque; + } + }; + +// Traits class to help determine the template parameters to use for a ftl::Function, given a +// function object. + + template + struct function_traits { + // The function type `F` with which to instantiate the `Function` template. + using type = remove_member_function_pointer_t<&F::operator()>; + + // The (minimum) size `N` with which to instantiate the `Function` template. + static constexpr std::size_t size = + (std::max(sizeof(std::intptr_t), sizeof(F)) - 1) / sizeof(std::intptr_t); + }; + +} // namespace android::ftl::details diff --git a/sysbridge/src/main/cpp/android/ftl/details/future.h b/sysbridge/src/main/cpp/android/ftl/details/future.h new file mode 100644 index 0000000000..ec3926ba8c --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/details/future.h @@ -0,0 +1,119 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +namespace android::ftl { + + template class> + class Future; + + namespace details { + + template + struct future_result { + using type = T; + }; + + template + struct future_result> { + using type = T; + }; + + template + struct future_result> { + using type = T; +}; + +template class FutureImpl> +struct future_result> { +using type = T; +}; + +template +using future_result_t = typename future_result::type; + +struct ValueTag { +}; + +template class> +class BaseFuture; + +template +class BaseFuture { + using Impl = std::future; + +public: + Future share() { + if (T *value = std::get_if(&self())) { + return {ValueTag{}, std::move(*value)}; + } + + return std::get(self()).share(); + } + +protected: + T get() { + if (T *value = std::get_if(&self())) { + return std::move(*value); + } + + return std::get(self()).get(); + } + + template + std::future_status wait_for(const std::chrono::duration &timeout_duration) const { + if (std::holds_alternative(self())) { + return std::future_status::ready; + } + + return std::get(self()).wait_for(timeout_duration); + } + +private: + auto &self() { return static_cast(*this).future_; } + + const auto &self() const { return static_cast(*this).future_; } +}; + +template +class BaseFuture { + using Impl = std::shared_future; + +protected: + const T &get() const { + if (const T *value = std::get_if(&self())) { + return *value; + } + + return std::get(self()).get(); + } + + template + std::future_status wait_for(const std::chrono::duration &timeout_duration) const { + if (std::holds_alternative(self())) { + return std::future_status::ready; + } + + return std::get(self()).wait_for(timeout_duration); + } + +private: + const auto &self() const { return static_cast(*this).future_; } +}; + +} // namespace details +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/details/hash.h b/sysbridge/src/main/cpp/android/ftl/details/hash.h new file mode 100644 index 0000000000..cb9939036a --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/details/hash.h @@ -0,0 +1,125 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +namespace android::ftl::details { + +// Based on CityHash64 v1.0.1 (http://code.google.com/p/cityhash/), but slightly +// modernized and trimmed for cases with bounded lengths. + + template + inline T read_unaligned(const void *ptr) { + T v; + std::memcpy(&v, ptr, sizeof(T)); + return v; + } + + template + constexpr std::uint64_t rotate(std::uint64_t v, std::uint8_t shift) { + if constexpr (!NonZeroShift) { + if (shift == 0) return v; + } + return (v >> shift) | (v << (64 - shift)); + } + + constexpr std::uint64_t shift_mix(std::uint64_t v) { + return v ^ (v >> 47); + } + + __attribute__((no_sanitize("unsigned-integer-overflow"))) + constexpr std::uint64_t hash_length_16(std::uint64_t u, std::uint64_t v) { + constexpr std::uint64_t kPrime = 0x9ddfea08eb382d69ull; + auto a = (u ^ v) * kPrime; + a ^= (a >> 47); + auto b = (v ^ a) * kPrime; + b ^= (b >> 47); + b *= kPrime; + return b; + } + + constexpr std::uint64_t kPrime0 = 0xc3a5c85c97cb3127ull; + constexpr std::uint64_t kPrime1 = 0xb492b66fbe98f273ull; + constexpr std::uint64_t kPrime2 = 0x9ae16a3b2f90404full; + constexpr std::uint64_t kPrime3 = 0xc949d7c7509e6557ull; + + __attribute__((no_sanitize("unsigned-integer-overflow"))) + inline std::uint64_t hash_length_0_to_16(const char *str, std::uint64_t length) { + if (length > 8) { + const auto a = read_unaligned(str); + const auto b = read_unaligned(str + length - 8); + return hash_length_16(a, rotate(b + length, static_cast(length))) ^ + b; + } + if (length >= 4) { + const auto a = read_unaligned(str); + const auto b = read_unaligned(str + length - 4); + return hash_length_16(length + (a << 3), b); + } + if (length > 0) { + const auto a = static_cast(str[0]); + const auto b = static_cast(str[length >> 1]); + const auto c = static_cast(str[length - 1]); + const auto y = static_cast(a) + (static_cast(b) << 8); + const auto z = + static_cast(length) + (static_cast(c) << 2); + return shift_mix(y * kPrime2 ^ z * kPrime3) * kPrime2; + } + return kPrime2; + } + + __attribute__((no_sanitize("unsigned-integer-overflow"))) + inline std::uint64_t hash_length_17_to_32(const char *str, std::uint64_t length) { + const auto a = read_unaligned(str) * kPrime1; + const auto b = read_unaligned(str + 8); + const auto c = read_unaligned(str + length - 8) * kPrime2; + const auto d = read_unaligned(str + length - 16) * kPrime0; + return hash_length_16(rotate(a - b, 43) + rotate(c, 30) + d, + a + rotate(b ^ kPrime3, 20) - c + length); + } + + __attribute__((no_sanitize("unsigned-integer-overflow"))) + inline std::uint64_t hash_length_33_to_64(const char *str, std::uint64_t length) { + auto z = read_unaligned(str + 24); + auto a = read_unaligned(str) + (length + read_unaligned(str + length - 16)) * kPrime0; + auto b = rotate(a + z, 52); + auto c = rotate(a, 37); + + a += read_unaligned(str + 8); + c += rotate(a, 7); + a += read_unaligned(str + 16); + + const auto vf = a + z; + const auto vs = b + rotate(a, 31) + c; + + a = read_unaligned(str + 16) + read_unaligned(str + length - 32); + z += read_unaligned(str + length - 8); + b = rotate(a + z, 52); + c = rotate(a, 37); + a += read_unaligned(str + length - 24); + c += rotate(a, 7); + a += read_unaligned(str + length - 16); + + const auto wf = a + z; + const auto ws = b + rotate(a, 31) + c; + const auto r = shift_mix((vf + ws) * kPrime2 + (wf + vs) * kPrime0); + return shift_mix(r * kPrime0 + vs) * kPrime2; + } + +} // namespace android::ftl::details diff --git a/sysbridge/src/main/cpp/android/ftl/details/match.h b/sysbridge/src/main/cpp/android/ftl/details/match.h new file mode 100644 index 0000000000..cee77c83ee --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/details/match.h @@ -0,0 +1,59 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +namespace android::ftl::details { + + template + struct Matcher : Ms ... { + using Ms::operator()...; + }; + +// Deduction guide. + template + Matcher(Ms...) -> Matcher; + + template + constexpr bool is_exhaustive_match_v = (std::is_invocable_v && ...); + + template + struct Match; + + template + struct Match { + template + static decltype(auto) match(Variant &variant, const Matcher &matcher) { + if (auto *const ptr = std::get_if(&variant)) { + return matcher(*ptr); + } else { + return Match::match(variant, matcher); + } + } + }; + + template + struct Match { + template + static decltype(auto) match(Variant &variant, const Matcher &matcher) { + return matcher(std::get(variant)); + } + }; + +} // namespace android::ftl::details diff --git a/sysbridge/src/main/cpp/android/ftl/details/mixins.h b/sysbridge/src/main/cpp/android/ftl/details/mixins.h new file mode 100644 index 0000000000..a52d74c504 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/details/mixins.h @@ -0,0 +1,31 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +namespace android::ftl::details { + + template class> + class Mixin { + protected: + constexpr Self &self() { return *static_cast(this); } + + constexpr const Self &self() const { return *static_cast(this); } + + constexpr auto &mut() { return self().value_; } + }; + +} // namespace android::ftl::details diff --git a/sysbridge/src/main/cpp/android/ftl/details/optional.h b/sysbridge/src/main/cpp/android/ftl/details/optional.h new file mode 100644 index 0000000000..bcbd8826c0 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/details/optional.h @@ -0,0 +1,72 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include + +namespace android::ftl { + + template + struct Optional; + + namespace details { + + template + struct is_optional : std::false_type { + }; + + template + struct is_optional> : std::true_type { + }; + + template + struct is_optional> : std::true_type { + }; + + template + struct transform_result { + using type = Optional>>; + }; + + template + using transform_result_t = typename transform_result::type; + + template + struct and_then_result { + using type = remove_cvref_t>; + static_assert(is_optional{}, "and_then function must return an optional"); + }; + + template + using and_then_result_t = typename and_then_result::type; + + template + struct or_else_result { + using type = remove_cvref_t>; + static_assert( + std::is_same_v> || std::is_same_v>, + "or_else function must return an optional T"); + }; + + template + using or_else_result_t = typename or_else_result::type; + + } // namespace details +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/details/type_traits.h b/sysbridge/src/main/cpp/android/ftl/details/type_traits.h new file mode 100644 index 0000000000..70a602c330 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/details/type_traits.h @@ -0,0 +1,33 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace android::ftl::details { + +// TODO: Replace with std::remove_cvref_t in C++20. + template + using remove_cvref_t = std::remove_cv_t>; + + template + constexpr bool is_bool_v = std::is_same_v, bool>; + + template + constexpr bool is_char_v = std::is_same_v, char>; + +} // namespace android::ftl::details diff --git a/sysbridge/src/main/cpp/android/ftl/enum.h b/sysbridge/src/main/cpp/android/ftl/enum.h new file mode 100644 index 0000000000..e31c0a1322 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/enum.h @@ -0,0 +1,369 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +// Returns the name of enumerator E::V and optionally the class (i.e. "E::V" or "V") as +// std::optional by parsing the compiler-generated string literal for the +// signature of this function. The function is defined in the global namespace with a short name +// and inferred return type to reduce bloat in the read-only data segment. +template +constexpr auto ftl_enum_builder() { + static_assert(std::is_enum_v); + + using R = std::optional; + using namespace std::literals; + + // The "pretty" signature has the following format: + // + // auto ftl_enum() [E = android::test::Enum, V = android::test::Enum::kValue] + // + std::string_view view = __PRETTY_FUNCTION__; + const auto template_begin = view.rfind('['); + const auto template_end = view.rfind(']'); + if (template_begin == view.npos || template_end == view.npos) return R{}; + + // Extract the template parameters without the enclosing brackets. Example (cont'd): + // + // E = android::test::Enum, V = android::test::Enum::kValue + // + view = view.substr(template_begin + 1, template_end - template_begin - 1); + const auto value_begin = view.rfind("V = "sv); + if (value_begin == view.npos) return R{}; + + // Example (cont'd): + // + // V = android::test::Enum::kValue + // + view = view.substr(value_begin); + const auto pos = S ? view.rfind("::"sv) - 2 : view.npos; + + const auto name_begin = view.rfind("::"sv, pos); + if (name_begin == view.npos) return R{}; + + // Chop off the leading "::". + const auto name = view.substr(name_begin + 2); + + // A value that is not enumerated has the format "Enum)42". + return name.find(')') == view.npos ? R{name} : R{}; +} + +// Returns the name of enumerator E::V (i.e. "V") as std::optional +template +constexpr auto ftl_enum() { + return ftl_enum_builder(); +} + +// Returns the name of enumerator and class E::V (i.e. "E::V") as std::optional +template +constexpr auto ftl_enum_full() { + return ftl_enum_builder(); +} + +namespace android::ftl { + +// Trait for determining whether a type is specifically a scoped enum or not. By definition, a +// scoped enum is one that is not implicitly convertible to its underlying type. +// +// TODO: Replace with std::is_scoped_enum in C++23. +// + template> + struct is_scoped_enum : std::false_type { + }; + + template + struct is_scoped_enum + : std::negation>> { + }; + + template + inline constexpr bool is_scoped_enum_v = is_scoped_enum::value; + +// Shorthand for casting an enumerator to its integral value. +// +// TODO: Replace with std::to_underlying in C++23. +// +// enum class E { A, B, C }; +// static_assert(ftl::to_underlying(E::B) == 1); +// + template>> + constexpr auto to_underlying(E v) { + return static_cast>(v); + } + +// Traits for retrieving an enum's range. An enum specifies its range by defining enumerators named +// ftl_first and ftl_last. If omitted, ftl_first defaults to 0, whereas ftl_last defaults to N - 1 +// where N is the bit width of the underlying type, but only if that type is unsigned, assuming the +// enumerators are flags. Also, note that unscoped enums must define both bounds, as casting out-of- +// range values results in undefined behavior if the underlying type is not fixed. +// +// enum class E { A, B, C, F = 5, ftl_last = F }; +// +// static_assert(ftl::enum_begin_v == E::A); +// static_assert(ftl::enum_last_v == E::F); +// static_assert(ftl::enum_size_v == 6); +// +// enum class F : std::uint16_t { X = 0b1, Y = 0b10, Z = 0b100 }; +// +// static_assert(ftl::enum_begin_v == F{0}); +// static_assert(ftl::enum_last_v == F{15}); +// static_assert(ftl::enum_size_v == 16); +// + template + struct enum_begin { + static_assert(is_scoped_enum_v, "Missing ftl_first enumerator"); + static constexpr E value{0}; + }; + + template + struct enum_begin> { + static constexpr E value = E::ftl_first; + }; + + template + inline constexpr E enum_begin_v = enum_begin::value; + + template + struct enum_end { + using U = std::underlying_type_t; + static_assert(is_scoped_enum_v && std::is_unsigned_v, "Missing ftl_last enumerator"); + + static constexpr E value{std::numeric_limits::digits}; + }; + + template + struct enum_end> { + static constexpr E value = E{to_underlying(E::ftl_last) + 1}; + }; + + template + inline constexpr E enum_end_v = enum_end::value; + + template + inline constexpr E enum_last_v = E{to_underlying(enum_end_v) - 1}; + + template + struct enum_size { + static constexpr auto kBegin = to_underlying(enum_begin_v); + static constexpr auto kEnd = to_underlying(enum_end_v); + static_assert(kBegin < kEnd, "Invalid range"); + + static constexpr std::size_t value = kEnd - kBegin; + static_assert(value <= 64, "Excessive range size"); + }; + + template + inline constexpr std::size_t enum_size_v = enum_size::value; + + namespace details { + + template + struct Identity { + static constexpr auto value = V; + }; + + template + using make_enum_sequence = std::make_integer_sequence, enum_size_v>; + + template class = Identity, typename = make_enum_sequence> + struct EnumRange; + + template class F, typename T, T... Vs> + struct EnumRange> { + static constexpr auto kBegin = to_underlying(enum_begin_v); + static constexpr auto kSize = enum_size_v; + + using R = decltype(F::value); + const R values[kSize] = {F(Vs + kBegin)>::value...}; + + constexpr const auto *begin() const { return values; } + + constexpr const auto *end() const { return values + kSize; } + }; + + template + struct EnumName { + static constexpr auto value = ftl_enum(); + }; + + template + struct EnumNameFull { + static constexpr auto value = ftl_enum_full(); + }; + + template + struct FlagName { + using E = decltype(I); + using U = std::underlying_type_t; + + static constexpr E V{U{1} << to_underlying(I)}; + static constexpr auto value = ftl_enum(); + }; + + } // namespace details + +// Returns an iterable over the range of an enum. +// +// enum class E { A, B, C, F = 5, ftl_last = F }; +// +// std::string string; +// for (E v : ftl::enum_range()) { +// string += ftl::enum_name(v).value_or("?"); +// } +// +// assert(string == "ABC??F"); +// + template + constexpr auto enum_range() { + return details::EnumRange{}; + } + +// Returns a stringified enumerator at compile time. +// +// enum class E { A, B, C }; +// static_assert(ftl::enum_name() == "B"); +// + template + constexpr std::string_view enum_name() { + constexpr auto kName = ftl_enum(); + static_assert(kName, "Unknown enumerator"); + return *kName; + } + +// Returns a stringified enumerator with class at compile time. +// +// enum class E { A, B, C }; +// static_assert(ftl::enum_name() == "E::B"); +// + template + constexpr std::string_view enum_name_full() { + constexpr auto kName = ftl_enum_full(); + static_assert(kName, "Unknown enumerator"); + return *kName; + } + +// Returns a stringified enumerator, possibly at compile time. +// +// enum class E { A, B, C, F = 5, ftl_last = F }; +// +// static_assert(ftl::enum_name(E::C).value_or("?") == "C"); +// static_assert(ftl::enum_name(E{3}).value_or("?") == "?"); +// + template + constexpr std::optional enum_name(E v) { + const auto value = to_underlying(v); + + constexpr auto kBegin = to_underlying(enum_begin_v); + constexpr auto kLast = to_underlying(enum_last_v); + if (value < kBegin || value > kLast) return {}; + + constexpr auto kRange = details::EnumRange{}; + return kRange.values[value - kBegin]; + } + +// Returns a stringified enumerator with class, possibly at compile time. +// +// enum class E { A, B, C, F = 5, ftl_last = F }; +// +// static_assert(ftl::enum_name(E::C).value_or("?") == "E::C"); +// static_assert(ftl::enum_name(E{3}).value_or("?") == "?"); +// + template + constexpr std::optional enum_name_full(E v) { + const auto value = to_underlying(v); + + constexpr auto kBegin = to_underlying(enum_begin_v); + constexpr auto kLast = to_underlying(enum_last_v); + if (value < kBegin || value > kLast) return {}; + + constexpr auto kRange = details::EnumRange{}; + return kRange.values[value - kBegin]; + } + +// Returns a stringified flag enumerator, possibly at compile time. +// +// enum class F : std::uint16_t { X = 0b1, Y = 0b10, Z = 0b100 }; +// +// static_assert(ftl::flag_name(F::Z).value_or("?") == "Z"); +// static_assert(ftl::flag_name(F{0b111}).value_or("?") == "?"); +// + template + constexpr std::optional flag_name(E v) { + const auto value = to_underlying(v); + + // TODO: Replace with std::popcount and std::countr_zero in C++20. + if (__builtin_popcountll(value) != 1) return {}; + + constexpr auto kRange = details::EnumRange{}; + return kRange.values[__builtin_ctzll(value)]; + } + +// Returns a stringified enumerator, or its integral value if not named. +// +// enum class E { A, B, C, F = 5, ftl_last = F }; +// +// assert(ftl::enum_string(E::C) == "C"); +// assert(ftl::enum_string(E{3}) == "3"); +// + template + inline std::string enum_string(E v) { + if (const auto name = enum_name(v)) { + return std::string(*name); + } + return to_string(to_underlying(v)); + } + +// Returns a stringified enumerator with class, or its integral value if not named. +// +// enum class E { A, B, C, F = 5, ftl_last = F }; +// +// assert(ftl::enum_string(E::C) == "E::C"); +// assert(ftl::enum_string(E{3}) == "3"); +// + template + inline std::string enum_string_full(E v) { + if (const auto name = enum_name_full(v)) { + return std::string(*name); + } + return to_string(to_underlying(v)); + } + +// Returns a stringified flag enumerator, or its integral value if not named. +// +// enum class F : std::uint16_t { X = 0b1, Y = 0b10, Z = 0b100 }; +// +// assert(ftl::flag_string(F::Z) == "Z"); +// assert(ftl::flag_string(F{7}) == "0b111"); +// + template + inline std::string flag_string(E v) { + if (const auto name = flag_name(v)) { + return std::string(*name); + } + constexpr auto radix = sizeof(E) == 1 ? Radix::kBin : Radix::kHex; + return to_string(to_underlying(v), radix); + } + +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/expected.h b/sysbridge/src/main/cpp/android/ftl/expected.h new file mode 100644 index 0000000000..5ed362e536 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/expected.h @@ -0,0 +1,144 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "../libbase/expected.h" +#include +#include + +#include + +// Given an expression `expr` that evaluates to an ftl::Expected result (R for short), FTL_TRY +// unwraps T out of R, or bails out of the enclosing function F if R has an error E. The return type +// of F must be R, since FTL_TRY propagates R in the error case. As a special case, ftl::Unit may be +// used as the error E to allow FTL_TRY expressions when F returns `void`. +// +// The non-standard syntax requires `-Wno-gnu-statement-expression-from-macro-expansion` to compile. +// The UnitToVoid conversion allows the macro to be used for early exit from a function that returns +// `void`. +// +// Example usage: +// +// using StringExp = ftl::Expected; +// +// StringExp repeat(StringExp exp) { +// const std::string str = FTL_TRY(exp); +// return StringExp(str + str); +// } +// +// assert(StringExp("haha"s) == repeat(StringExp("ha"s))); +// assert(repeat(ftl::Unexpected(std::errc::bad_message)).has_error([](std::errc e) { +// return e == std::errc::bad_message; +// })); +// +// +// FTL_TRY may be used in void-returning functions by using ftl::Unit as the error type: +// +// void uppercase(char& c, ftl::Optional opt) { +// c = std::toupper(FTL_TRY(std::move(opt).ok_or(ftl::Unit()))); +// } +// +// char c = '?'; +// uppercase(c, std::nullopt); +// assert(c == '?'); +// +// uppercase(c, 'a'); +// assert(c == 'A'); +// +#define FTL_TRY(expr) \ + ({ \ + auto exp_ = (expr); \ + if (!exp_.has_value()) { \ + using E = decltype(exp_)::error_type; \ + return android::ftl::details::UnitToVoid::from(std::move(exp_)); \ + } \ + exp_.value(); \ + }) + +// Given an expression `expr` that evaluates to an ftl::Expected result (R for short), +// FTL_EXPECT unwraps T out of R, or bails out of the enclosing function F if R has an error E. +// While FTL_TRY bails out with R, FTL_EXPECT bails out with E, which is useful when F does not +// need to propagate R because T is not relevant to the caller. +// +// Example usage: +// +// using StringExp = ftl::Expected; +// +// std::errc repeat(StringExp exp, std::string& out) { +// const std::string str = FTL_EXPECT(exp); +// out = str + str; +// return std::errc::operation_in_progress; +// } +// +// std::string str; +// assert(std::errc::operation_in_progress == repeat(StringExp("ha"s), str)); +// assert("haha"s == str); +// assert(std::errc::bad_message == repeat(ftl::Unexpected(std::errc::bad_message), str)); +// assert("haha"s == str); +// +#define FTL_EXPECT(expr) \ + ({ \ + auto exp_ = (expr); \ + if (!exp_.has_value()) { \ + return std::move(exp_.error()); \ + } \ + exp_.value(); \ + }) + +namespace android::ftl { + +// Superset of base::expected with monadic operations. +// +// TODO: Extend std::expected in C++23. +// + template + struct Expected final : base::expected { + using Base = base::expected; + using Base::expected; + + using Base::error; + using Base::has_value; + using Base::value; + + template + constexpr bool has_error(P predicate) const { + return !has_value() && predicate(error()); + } + + constexpr Optional value_opt() const &{ + return has_value() ? Optional(value()) : std::nullopt; + } + + constexpr Optional value_opt() &&{ + return has_value() ? Optional(std::move(value())) : std::nullopt; + } + + // Delete new for this class. Its base doesn't have a virtual destructor, and + // if it got deleted via base class pointer, it would cause undefined + // behavior. There's not a good reason to allocate this object on the heap + // anyway. + static void *operator new(size_t) = delete; + + static void *operator new[](size_t) = delete; + }; + + template + constexpr auto Unexpected(E &&error) { + return base::unexpected(std::forward(error)); + } + +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/fake_guard.h b/sysbridge/src/main/cpp/android/ftl/fake_guard.h new file mode 100644 index 0000000000..056564a1a1 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/fake_guard.h @@ -0,0 +1,87 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#define FTL_ATTRIBUTE(a) __attribute__((a)) + +namespace android::ftl { + +// Granular alternative to [[clang::no_thread_safety_analysis]]. Given a std::mutex-like object, +// FakeGuard suppresses enforcement of thread-safe access to guarded variables within its scope. +// While FakeGuard is scoped to a block, there are macro shorthands for a single expression, as +// well as function/lambda scope (though calls must be indirect, e.g. virtual or std::function): +// +// struct { +// std::mutex mutex; +// int x FTL_ATTRIBUTE(guarded_by(mutex)) = -1; +// +// int f() { +// { +// ftl::FakeGuard guard(mutex); +// x = 0; +// } +// +// return FTL_FAKE_GUARD(mutex, x + 1); +// } +// +// std::function g() const { +// return [this]() FTL_FAKE_GUARD(mutex) { return x; }; +// } +// } s; +// +// assert(s.f() == 1); +// assert(s.g()() == 0); +// +// An example of a situation where FakeGuard helps is a mutex that guards writes on Thread 1, and +// reads on Thread 2. Reads on Thread 1, which is the only writer, need not be under lock, so can +// use FakeGuard to appease the thread safety analyzer. Another example is enforcing and documenting +// exclusive access by a single thread. This is done by defining a global constant that represents a +// thread context, and annotating guarded variables as if it were a mutex (though without any effect +// at run time): +// +// constexpr class [[clang::capability("mutex")]] { +// } kMainThreadContext; +// + template + struct [[clang::scoped_lockable]] FakeGuard final { + explicit FakeGuard(const Mutex &mutex) FTL_ATTRIBUTE(acquire_capability(mutex)) {} + + [[clang::release_capability()]] ~FakeGuard() {} + + FakeGuard(const FakeGuard &) = delete; + + FakeGuard &operator=(const FakeGuard &) = delete; + }; + +} // namespace android::ftl + +// TODO: Enable in C++23 once standard attributes can be used on lambdas. +#if 0 +#define FTL_FAKE_GUARD1(mutex) [[using clang: acquire_capability(mutex), release_capability(mutex)]] +#else +#define FTL_FAKE_GUARD1(mutex) \ + FTL_ATTRIBUTE(acquire_capability(mutex)) \ + FTL_ATTRIBUTE(release_capability(mutex)) +#endif + +#define FTL_FAKE_GUARD2(mutex, expr) \ + (android::ftl::FakeGuard(mutex), expr) + +#define FTL_MAKE_FAKE_GUARD(arg1, arg2, guard, ...) guard + +#define FTL_FAKE_GUARD(...) \ + FTL_MAKE_FAKE_GUARD(__VA_ARGS__, FTL_FAKE_GUARD2, FTL_FAKE_GUARD1, )(__VA_ARGS__) diff --git a/sysbridge/src/main/cpp/android/ftl/finalizer.h b/sysbridge/src/main/cpp/android/ftl/finalizer.h new file mode 100644 index 0000000000..b3342e959c --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/finalizer.h @@ -0,0 +1,213 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include +#include + +#include + +namespace android::ftl { + +// An RAII wrapper that invokes a function object as a finalizer when destroyed. +// +// The function object must take no arguments, and must return void. If the function object needs +// any context for the call, it must store it itself, for example with a lambda capture. +// +// The stored function object will be called once (unless canceled via the `cancel()` member +// function) at the first of: +// +// - The Finalizer instance is destroyed. +// - `operator()` is used to invoke the contained function. +// - The Finalizer instance is move-assigned a new value. The function being replaced will be +// invoked, and the replacement will be stored to be called later. +// +// The intent with this class is to keep cleanup code next to the code that requires that +// cleanup be performed. +// +// bool read_file(std::string filename) { +// FILE* f = fopen(filename.c_str(), "rb"); +// if (f == nullptr) return false; +// const auto cleanup = ftl::Finalizer([f]() { fclose(f); }); +// // fread(...), etc +// return true; +// } +// +// The `FinalFunction` template argument to Finalizer allows a polymorphic function +// type for storing the finalization function, such as `std::function` or `ftl::Function`. +// +// For convenience, this header defines a few useful aliases for using those types. +// +// - `FinalizerStd`, an alias for `Finalizer>` +// - `FinalizerFtl`, an alias for `Finalizer>` +// - `FinalizerFtl1`, an alias for `Finalizer>` +// - `FinalizerFtl2`, an alias for `Finalizer>` +// - `FinalizerFtl3`, an alias for `Finalizer>` +// +// Clients of this header are free to define other aliases they need. +// +// A Finalizer that uses a polymorphic function type can be returned from a function call and/or +// stored as member data (to be destroyed along with the containing class). +// +// auto register(Observer* observer) -> ftl::FinalizerStd { +// const auto id = observers.add(observer); +// return ftl::Finalizer([id]() { observers.remove(id); }); +// } +// +// { +// const auto _ = register(observer); +// // do the things that required the registered observer. +// } +// // the observer is removed. +// +// Cautions: +// +// 1. When a Finalizer is stored as member data, you will almost certainly want that cleanup to +// happen first, before the rest of the other member data is destroyed. For safety you should +// assume that the finalization function will access that data directly or indirectly. +// +// This means that Finalizers should be defined last, after all other normal member data in a +// class. +// +// class MyClass { +// public: +// bool initialize() { +// ready_ = true; +// cleanup_ = ftl::Finalizer([this]() { ready_ = false; }); +// return true; +// } +// +// bool ready_ = false; +// +// // Finalizers should be last so other class members can be accessed before being +// // destroyed. +// ftl::FinalizerStd cleanup_; +// }; +// +// 2. Care must be taken to use `ftl::Finalizer()` when constructing locally from a lambda. If you +// forget to do so, you are just creating a lambda that won't be automatically invoked! +// +// const auto bad = [&counter](){ ++counter; }; // Just a lambda instance +// const auto good = ftl::Finalizer([&counter](){ ++counter; }); +// + template + class Finalizer final { + // requires(std::is_invocable_r_v) + static_assert(std::is_invocable_r_v); + + public: + // A default constructed Finalizer does nothing when destroyed. + // requires(std::is_default_constructible_v) + constexpr Finalizer() = default; + + // Constructs a Finalizer from a function object. + // requires(std::is_invocable_v) + template>> + [[nodiscard]] explicit constexpr Finalizer(F &&function) + : Finalizer(std::forward(function), false) {} + + constexpr ~Finalizer() { maybe_invoke(); } + + // Disallow copying. + Finalizer(const Finalizer &that) = delete; + + auto operator=(const Finalizer &that) = delete; + + // Move construction + // requires(std::is_move_constructible_v) + [[nodiscard]] constexpr Finalizer(Finalizer &&that) + : Finalizer(std::move(that.function_), std::exchange(that.canceled_, true)) {} + + // Implicit conversion move construction + // requires(!std::is_same_v>) + template>>> + // NOLINTNEXTLINE(google-explicit-constructor, cppcoreguidelines-rvalue-reference-param-not-moved) + [[nodiscard]] constexpr Finalizer(Finalizer &&that) + : Finalizer(std::move(that.function_), std::exchange(that.canceled_, true)) {} + + // Move assignment + // requires(std::is_move_assignable_v) + constexpr auto operator=(Finalizer &&that) -> Finalizer & { + maybe_invoke(); + + function_ = std::move(that.function_); + canceled_ = std::exchange(that.canceled_, true); + + return *this; + } + + // Implicit conversion move assignment + // requires(!std::is_same_v>) + template>>> + // NOLINTNEXTLINE(cppcoreguidelines-rvalue-reference-param-not-moved) + constexpr auto operator=(Finalizer &&that) -> Finalizer & { + *this = Finalizer(std::move(that.function_), std::exchange(that.canceled_, true)); + return *this; + } + + // Cancels the final function, preventing it from being invoked. + constexpr void cancel() { + canceled_ = true; + maybe_nullify_function(); + } + + // Invokes the final function now, if not already invoked. + constexpr void operator()() { maybe_invoke(); } + + private: + template + friend + class Finalizer; + + template>> + [[nodiscard]] explicit constexpr Finalizer(F &&function, bool canceled) + : function_(std::forward(function)), canceled_(canceled) {} + + constexpr void maybe_invoke() { + if (!std::exchange(canceled_, true)) { + std::invoke(function_); + maybe_nullify_function(); + } + } + + constexpr void maybe_nullify_function() { + // Sets function_ to nullptr if that is supported for the backing type. + if constexpr (std::is_assignable_v) { + function_ = nullptr; + } + } + + FinalFunction function_; + bool canceled_ = true; + }; + + template + Finalizer(F &&) -> Finalizer>; + +// A standard alias for using `std::function` as the polymorphic function type. + using FinalizerStd = Finalizer>; + +// Helpful aliases for using `ftl::Function` as the polymorphic function type. + using FinalizerFtl = Finalizer>; + using FinalizerFtl1 = Finalizer>; + using FinalizerFtl2 = Finalizer>; + using FinalizerFtl3 = Finalizer>; + +} // namespace android::ftl \ No newline at end of file diff --git a/sysbridge/src/main/cpp/android/ftl/flags.h b/sysbridge/src/main/cpp/android/ftl/flags.h new file mode 100644 index 0000000000..338acc240e --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/flags.h @@ -0,0 +1,246 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include + +// TODO(b/185536303): Align with FTL style. + +namespace android::ftl { + +/* A class for handling flags defined by an enum or enum class in a type-safe way. */ + template + class Flags { + // F must be an enum or its underlying type is undefined. Theoretically we could specialize this + // further to avoid this restriction but in general we want to encourage the use of enums + // anyways. + static_assert(std::is_enum_v, "Flags type must be an enum"); + using U = std::underlying_type_t; + + public: + constexpr Flags(F f) : mFlags(static_cast(f)) {} + + constexpr Flags(std::initializer_list fs) : mFlags(combine(fs)) {} + + constexpr Flags() : mFlags(0) {} + + constexpr Flags(const Flags &f) : mFlags(f.mFlags) {} + + // Provide a non-explicit construct for non-enum classes since they easily convert to their + // underlying types (e.g. when used with bitwise operators). For enum classes, however, we + // should force them to be explicitly constructed from their underlying types to make full use + // of the type checker. + template + constexpr Flags(T t, std::enable_if_t, T> * = nullptr) : mFlags(t) {} + + template + explicit constexpr Flags(T t, std::enable_if_t, T> * = nullptr) + : mFlags(t) {} + + class Iterator { + using Bits = std::uint64_t; + static_assert(sizeof(U) <= sizeof(Bits)); + + public: + constexpr Iterator() = default; + + Iterator(Flags flags) : mRemainingFlags(flags.mFlags) { (*this)++; } + + // Pre-fix ++ + Iterator &operator++() { + if (mRemainingFlags.none()) { + mCurrFlag = 0; + } else { + // TODO: Replace with std::countr_zero in C++20. + const Bits bit = static_cast(__builtin_ctzll( + mRemainingFlags.to_ullong())); + mRemainingFlags.reset(static_cast(bit)); + mCurrFlag = static_cast(static_cast(1) << bit); + } + return *this; + } + + // Post-fix ++ + Iterator operator++(int) { + Iterator iter = *this; + ++*this; + return iter; + } + + bool operator==(Iterator other) const { + return mCurrFlag == other.mCurrFlag && mRemainingFlags == other.mRemainingFlags; + } + + bool operator!=(Iterator other) const { return !(*this == other); } + + F operator*() const { return F{mCurrFlag}; } + + // iterator traits + + // In the future we could make this a bidirectional const iterator instead of a forward + // iterator but it doesn't seem worth the added complexity at this point. This could not, + // however, be made a non-const iterator as assigning one flag to another is a non-sensical + // operation. + using iterator_category = std::input_iterator_tag; + using value_type = F; + // Per the C++ spec, because input iterators are not assignable the iterator's reference + // type does not actually need to be a reference. In fact, making it a reference would imply + // that modifying it would change the underlying Flags object, which is obviously wrong for + // the same reason this can't be a non-const iterator. + using reference = F; + using difference_type = void; + using pointer = void; + + private: + std::bitset mRemainingFlags; + U mCurrFlag = 0; + }; + + /* + * Tests whether the given flag is set. + */ + bool test(F flag) const { + U f = static_cast(flag); + return (f & mFlags) == f; + } + + /* Tests whether any of the given flags are set */ + bool any(Flags f = ~Flags()) const { return (mFlags & f.mFlags) != 0; } + + /* Tests whether all of the given flags are set */ + bool all(Flags f) const { return (mFlags & f.mFlags) == f.mFlags; } + + constexpr Flags operator|(Flags rhs) const { + return static_cast(mFlags | rhs.mFlags); + } + + Flags &operator|=(Flags rhs) { + mFlags = mFlags | rhs.mFlags; + return *this; + } + + Flags operator&(Flags rhs) const { return static_cast(mFlags & rhs.mFlags); } + + Flags &operator&=(Flags rhs) { + mFlags = mFlags & rhs.mFlags; + return *this; + } + + Flags operator^(Flags rhs) const { return static_cast(mFlags ^ rhs.mFlags); } + + Flags &operator^=(Flags rhs) { + mFlags = mFlags ^ rhs.mFlags; + return *this; + } + + Flags operator~() { return static_cast(~mFlags); } + + bool operator==(Flags rhs) const { return mFlags == rhs.mFlags; } + + bool operator!=(Flags rhs) const { return !operator==(rhs); } + + Flags &operator=(const Flags &rhs) { + mFlags = rhs.mFlags; + return *this; + } + + inline Flags &clear(Flags f = static_cast(~static_cast(0))) { + return *this &= ~f; + } + + Iterator begin() const { return Iterator(*this); } + + Iterator end() const { return Iterator(); } + + /* + * Returns the stored set of flags. + * + * Note that this returns the underlying type rather than the base enum class. This is because + * the value is no longer necessarily a strict member of the enum since the returned value could + * be multiple enum variants OR'd together. + */ + U get() const { return mFlags; } + + std::string string() const { + std::string result; + bool first = true; + U unstringified = 0; + for (const F f: *this) { + if (const auto flagName = flag_name(f)) { + appendFlag(result, flagName.value(), first); + } else { + unstringified |= static_cast(f); + } + } + + if (unstringified != 0) { + constexpr auto radix = sizeof(U) == 1 ? Radix::kBin : Radix::kHex; + appendFlag(result, to_string(unstringified, radix), first); + } + + if (first) { + result += "0x0"; + } + + return result; + } + + private: + U mFlags; + + static constexpr U combine(std::initializer_list fs) { + U result = 0; + for (const F f: fs) { + result |= static_cast(f); + } + return result; + } + + static void appendFlag(std::string &str, const std::string_view &flag, bool &first) { + if (first) { + first = false; + } else { + str += " | "; + } + str += flag; + } + }; + +// This namespace provides operator overloads for enum classes to make it easier to work with them +// as flags. In order to use these, add them via a `using namespace` declaration. + namespace flag_operators { + + template>> + inline Flags operator~(F f) { + return static_cast(~to_underlying(f)); + } + + template>> + constexpr Flags operator|(F lhs, F rhs) { + return static_cast(to_underlying(lhs) | to_underlying(rhs)); + } + + } // namespace flag_operators +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/function.h b/sysbridge/src/main/cpp/android/ftl/function.h new file mode 100644 index 0000000000..80caf4e743 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/function.h @@ -0,0 +1,313 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +#include + +namespace android::ftl { + +// ftl::Function is a container for function object, and can mostly be used in place of +// std::function. +// +// Unlike std::function, a ftl::Function: +// +// * Uses a static amount of memory (controlled by N), and never any dynamic allocation. +// * Satisfies the std::is_trivially_copyable<> trait. +// * Satisfies the std::is_trivially_destructible<> trait. +// +// However those same limits are also required from the contained function object in turn. +// +// The size of a ftl::Function is guaranteed to be: +// +// sizeof(std::intptr_t) * (N + 2) +// +// A ftl::Function can always be implicitly converted to a larger size ftl::Function. +// Trying to convert the other way leads to a compilation error. +// +// A default-constructed ftl::Function is in an empty state. The operator bool() overload returns +// false in this state. It is undefined behavior to attempt to invoke the function in this state. +// +// The ftl::Function can also be constructed or assigned from ftl::no_op. This sets up the +// ftl::Function to be non-empty, with a function that when called does nothing except +// default-constructs a return value. +// +// The ftl::make_function() helpers construct a ftl::Function, including deducing the +// values of F and N from the arguments it is given. +// +// The static ftl::Function::make() helpers construct a ftl::Function without that +// deduction, and also allow for implicit argument conversion if the target being called needs them. +// +// The construction helpers allow any of the following types of functions to be stored: +// +// * Any SMALL function object (as defined by the C++ Standard), such as a lambda with a small +// capture, or other "functor". The requirements are: +// +// 1) The function object must be trivial to destroy (in fact, the destructor will never +// actually be called once copied to the internal storage). +// 2) The function object must be trivial to copy (the raw bytes will be copied as the +// ftl::Function is copied/moved). +// 3) The size of the function object cannot be larger than sizeof(std::intptr_t) * (N + 1), +// and it cannot require stricter alignment than alignof(std::intptr_t). +// +// With the default of N=0, a lambda can only capture a single pointer-sized argument. This is +// enough to capture `this`, which is why N=0 is the default. +// +// * A member function, with the address passed as the template value argument to the construction +// helper function, along with the instance pointer needed to invoke it passed as an ordinary +// argument. +// +// ftl::make_function<&Class::member_function>(this); +// +// Note that the indicated member function will be invoked non-virtually. If you need it to be +// invoked virtually, you should invoke it yourself with a small lambda like so: +// +// ftl::function([this] { virtual_member_function(); }); +// +// * An ordinary function ("free function"), with the address of the function passed as a template +// value argument. +// +// ftl::make_function<&std::atoi>(); +// +// As with the member function helper, as the function is known at compile time, it will be called +// directly. +// +// Example usage: +// +// class MyClass { +// public: +// void on_event() const {} +// int on_string(int*, std::string_view) { return 1; } +// +// auto get_function() { +// return ftl::function([this] { on_event(); }); +// } +// } cls; +// +// // A function container with no arguments, and returning no value. +// ftl::Function f; +// +// // Construct a ftl::Function containing a small lambda. +// f = cls.get_function(); +// +// // Construct a ftl::Function that calls `cls.on_event()`. +// f = ftl::function<&MyClass::on_event>(&cls); +// +// // Create a do-nothing function. +// f = ftl::no_op; +// +// // Invoke the contained function. +// f(); +// +// // Also invokes it. +// std::invoke(f); +// +// // Create a typedef to give a more meaningful name and bound the size. +// using MyFunction = ftl::Function; +// int* ptr = nullptr; +// auto f1 = MyFunction::make( +// [cls = &cls, ptr](std::string_view sv) { +// return cls->on_string(ptr, sv); +// }); +// int r = f1("abc"sv); +// +// // Returns a default-constructed int (0). +// f1 = ftl::no_op; +// r = f1("abc"sv); +// assert(r == 0); + + template + class Function; + +// Used to construct a Function that does nothing. + struct NoOpTag { + }; + + constexpr NoOpTag no_op; + +// Detects that a type is a `ftl::Function` regardless of what `F` and `N` are. + template + struct is_function : public std::false_type { + }; + + template + struct is_function> : public std::true_type { + }; + + template + constexpr bool is_function_v = is_function::value; + + template + class Function final { + // Enforce a valid size, with an arbitrary maximum allowed size for the container of + // sizeof(std::intptr_t) * 16, though that maximum can be relaxed. + static_assert(N <= details::kFunctionMaximumN); + + using OpaqueStorageTraits = details::function_opaque_storage; + + public: + // Defining result_type allows ftl::Function to be substituted for std::function. + using result_type = Ret; + + // Constructs an empty ftl::Function. + Function() = default; + + // Constructing or assigning from nullptr_t also creates an empty ftl::Function. + Function(std::nullptr_t) {} + + Function &operator=(std::nullptr_t) { return *this = Function(nullptr); } + + // Constructing from NoOpTag sets up a a special no-op function which is valid to call, and which + // returns a default constructed return value. + Function(NoOpTag) : function_(details::bind_opaque_no_op()) {} + + Function &operator=(NoOpTag) { return *this = Function(no_op); } + + // Constructing/assigning from a function object stores a copy of that function object, however: + // * It must be trivially copyable, as the implementation makes a copy with memcpy(). + // * It must be trivially destructible, as the implementation doesn't destroy the copy! + // * It must fit in the limited internal storage, which enforces size/alignment restrictions. + + template>> + Function(const F &f) + : opaque_(OpaqueStorageTraits::opaque_copy(f)), + function_(details::bind_opaque_function_object(f)) {} + + template>> + Function &operator=(const F &f) noexcept { + return *this = Function{OpaqueStorageTraits::opaque_copy(f), + details::bind_opaque_function_object(f)}; + } + + // Constructing/assigning from a smaller ftl::Function is allowed, but not anything else. + + template + Function(const Function &other) + : opaque_{OpaqueStorageTraits::opaque_copy(other.opaque_)}, + function_(other.function_) {} + + template + auto &operator=(const Function &other) { + return *this = Function{OpaqueStorageTraits::opaque_copy(other.opaque_), + other.function_}; + } + + // Returns true if a function is set. + explicit operator bool() const { return function_ != nullptr; } + + // Checks if the other function has the same contents as this one. + bool operator==(const Function &other) const { + return other.opaque_ == opaque_ && other.function_ == function_; + } + + bool operator!=(const Function &other) const { return !operator==(other); } + + // Alternative way of testing for a function being set. + bool operator==(std::nullptr_t) const { return function_ == nullptr; } + + bool operator!=(std::nullptr_t) const { return function_ != nullptr; } + + // Invokes the function. + Ret operator()(Args... args) const { + return std::invoke(function_, opaque_.data(), std::forward(args)...); + } + + // Creation helper for function objects, such as lambdas. + template + static auto make(const F &f) -> decltype(Function{f}) { + return Function{f}; + } + + // Creation helper for a class pointer and a compile-time chosen member function to call. + template + static auto make(Class *instance) -> decltype(Function{ + details::bind_member_function(instance, + static_cast(nullptr))}) { + return Function{details::bind_member_function( + instance, static_cast(nullptr))}; + } + + // Creation helper for a compile-time chosen free function to call. + template + static auto make() -> decltype(Function{ + details::bind_free_function( + static_cast(nullptr))}) { + return Function{ + details::bind_free_function( + static_cast(nullptr))}; + } + + private: + // Needed so a Function can be converted to a Function. + template + friend + class Function; + + // The function pointer type of function stored in `function_`. The first argument is always + // `&opaque_`. + using StoredFunction = Ret(void *, Args...); + + // The type of the opaque storage, used to hold an appropriate function object. + // The type stored here is ONLY known to the StoredFunction. + // We always use at least one std::intptr_t worth of storage, and always a multiple of that size. + using OpaqueStorage = typename OpaqueStorageTraits::type; + + // Internal constructor for creating from a raw opaque blob + function pointer. + Function(const OpaqueStorage &opaque, StoredFunction *function) + : opaque_(opaque), function_(function) {} + + // Note: `mutable` so that `operator() const` can use it. + mutable OpaqueStorage opaque_{}; + StoredFunction *function_{nullptr}; + }; + +// Makes a ftl::Function given a function object `F`. + template> + Function(const F &) -> Function; + + template + auto make_function(const F &f) -> decltype(Function{f}) { + return Function{f}; + } + +// Makes a ftl::Function given a `MemberFunction` and a instance pointer to the associated `Class`. + template + auto make_function(Class *instance) + -> decltype(Function{details::bind_member_function( + instance, + static_cast *>(nullptr))}) { + return Function{details::bind_member_function( + instance, + static_cast *>(nullptr))}; + } + +// Makes a ftl::Function given an ordinary free function. + template + auto make_function() -> decltype(Function{ + details::bind_free_function( + static_cast(nullptr))}) { + return Function{ + details::bind_free_function( + static_cast(nullptr))}; + } + +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/future.h b/sysbridge/src/main/cpp/android/ftl/future.h new file mode 100644 index 0000000000..4110674618 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/future.h @@ -0,0 +1,135 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +#include + +namespace android::ftl { + +// Thin wrapper around FutureImpl (concretely std::future or std::shared_future) with +// extensions for pure values (created via ftl::yield) and continuations. +// +// See also SharedFuture shorthand below. +// + template class FutureImpl = std::future> + class Future final : public details::BaseFuture, T, FutureImpl> { + using Base = details::BaseFuture; + + friend Base; // For BaseFuture<...>::self. + friend details::BaseFuture, T, std::future>; // For BaseFuture<...>::share. + + public: + // Constructs an invalid future. + Future() : future_(std::in_place_type>) {} + + // Constructs a future from its standard counterpart, implicitly. + Future(FutureImpl &&f) : future_(std::move(f)) {} + + bool valid() const { + return std::holds_alternative(future_) || std::get>(future_).valid(); + } + + // Forwarding functions. Base::share is only defined when FutureImpl is std::future, whereas the + // following are defined for either FutureImpl: + using Base::get; + using Base::wait_for; + + // Attaches a continuation to the future. The continuation is a function that maps T to either R + // or ftl::Future. In the former case, the chain wraps the result in a future as if by + // ftl::yield. + // + // auto future = ftl::yield(123); + // ftl::Future futures[] = {ftl::yield('a'), ftl::yield('b')}; + // + // auto chain = + // ftl::Future(std::move(future)) + // .then([](int x) { return static_cast(x % 2); }) + // .then([&futures](std::size_t i) { return std::move(futures[i]); }); + // + // assert(chain.get() == 'b'); + // + template> + auto then(F &&op) && -> Future> { + return defer( + [](auto &&f, F &&op) { + R r = op(f.get()); + if constexpr (std::is_same_v>) { + return r; + } else { + return r.get(); + } + }, + std::move(*this), std::forward(op)); + } + + private: + template + friend Future yield(V &&); + + template + friend Future yield(Args &&...); + + template + Future(details::ValueTag, Args &&... args) + : future_(std::in_place_type, std::forward(args)...) {} + + std::variant> future_; + }; + + template + using SharedFuture = Future; + +// Deduction guide for implicit conversion. + template class FutureImpl> + Future(FutureImpl &&) -> Future; + +// Creates a future that wraps a value. +// +// auto future = ftl::yield(42); +// assert(future.get() == 42); +// +// auto ptr = std::make_unique('!'); +// auto future = ftl::yield(std::move(ptr)); +// assert(*future.get() == '!'); +// + template + inline Future yield(V &&value) { + return {details::ValueTag{}, std::move(value)}; + } + + template + inline Future yield(Args &&... args) { + return {details::ValueTag{}, std::forward(args)...}; + } + +// Creates a future that defers a function call until its result is queried. +// +// auto future = ftl::defer([](int x) { return x + 1; }, 99); +// assert(future.get() == 100); +// + template + inline auto defer(F &&f, Args &&... args) { + return Future( + std::async(std::launch::deferred, std::forward(f), std::forward(args)...)); + } + +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/hash.h b/sysbridge/src/main/cpp/android/ftl/hash.h new file mode 100644 index 0000000000..6d13672e7e --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/hash.h @@ -0,0 +1,44 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include + +namespace android::ftl { + +// Non-cryptographic hash function (namely CityHash64) for strings with at most 64 characters. +// Unlike std::hash, which returns std::size_t and is only required to produce the same result +// for the same input within a single execution of a program, this hash is stable. + inline std::optional stable_hash(std::string_view view) { + const auto length = view.length(); + if (length <= 16) { + return details::hash_length_0_to_16(view.data(), length); + } + if (length <= 32) { + return details::hash_length_17_to_32(view.data(), length); + } + if (length <= 64) { + return details::hash_length_33_to_64(view.data(), length); + } + return {}; + } + +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/ignore.h b/sysbridge/src/main/cpp/android/ftl/ignore.h new file mode 100644 index 0000000000..bd33511747 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/ignore.h @@ -0,0 +1,43 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +namespace android::ftl { + +// An alternative to `std::ignore` that makes it easy to ignore multiple values. +// +// Examples: +// +// void ftl_ignore_multiple(int arg1, const char* arg2, std::string arg3) { +// // When invoked, all the arguments are ignored. +// ftl::ignore(arg1, arg2, arg3); +// } +// +// void ftl_ignore_single(int arg) { +// // It can be used like std::ignore to ignore a single value +// ftl::ignore = arg; +// } +// + inline constexpr struct { + // NOLINTNEXTLINE(misc-unconventional-assign-operator, readability-named-parameter) + constexpr auto operator=(auto &&) const -> decltype(*this) { return *this; } + + // NOLINTNEXTLINE(readability-named-parameter) + constexpr void operator()(auto &&...) const {} + } ignore; + +} // namespace android::ftl \ No newline at end of file diff --git a/sysbridge/src/main/cpp/android/ftl/initializer_list.h b/sysbridge/src/main/cpp/android/ftl/initializer_list.h new file mode 100644 index 0000000000..e6d15c3061 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/initializer_list.h @@ -0,0 +1,112 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +namespace android::ftl { + +// Compile-time counterpart of std::initializer_list that stores per-element constructor +// arguments with heterogeneous types. For a container with elements of type T, given Sizes +// (S0, S1, ..., SN), N elements are initialized: the first element is initialized with the +// first S0 arguments, the second element is initialized with the next S1 arguments, and so +// on. The list of Types (T0, ..., TM) is flattened, so M is equal to the sum of the Sizes. +// +// An InitializerList is created using ftl::init::list, and is consumed by constructors of +// containers. The function call operator is overloaded such that arguments are accumulated +// in a tuple with each successive call. For instance, the following calls initialize three +// strings using different constructors, i.e. string literal, default, and count/character: +// +// ... = ftl::init::list("abc")()(3u, '?'); +// +// The following syntax is a shorthand for key-value pairs, where the first argument is the +// key, and the rest construct the value. The types of the key and value are deduced if the +// first pair contains exactly two arguments: +// +// ... = ftl::init::map(-1, "abc")(-2)(-3, 3u, '?'); +// +// ... = ftl::init::map(0, 'a')(1, 'b')(2, 'c'); +// +// WARNING: The InitializerList returned by an ftl::init::list expression must be consumed +// immediately, since temporary arguments are destroyed after the full expression. Storing +// an InitializerList results in dangling references. +// + template, typename... Types> + struct InitializerList; + + template + struct InitializerList, Types...> { + // Creates a superset InitializerList by appending the number of arguments to Sizes, and + // expanding Types with forwarding references for each argument. + template + [[nodiscard]] constexpr auto operator()(Args &&... args) && -> InitializerList< + T, std::index_sequence, Types..., Args && ...> { + return {std::tuple_cat(std::move(tuple), + std::forward_as_tuple(std::forward(args)...))}; + } + + // The temporary InitializerList returned by operator() is bound to an rvalue reference in + // container constructors, which extends the lifetime of any temporary arguments that this + // tuple refers to until the completion of the full expression containing the construction. + std::tuple tuple; + }; + + template> + struct KeyValue { + }; + +// Shorthand for key-value pairs that assigns the first argument to the key, and the rest to the +// value. The specialization is on KeyValue rather than std::pair, so that ftl::init::list works +// with the latter. + template + struct InitializerList, std::index_sequence, Types...> { + // Accumulate the three arguments to std::pair's piecewise constructor. + template + [[nodiscard]] constexpr auto operator()(K &&k, Args &&... args) && -> InitializerList< + KeyValue, std::index_sequence, Types..., std::piecewise_construct_t, + std::tuple, std::tuple> { + return {std::tuple_cat( + std::move(tuple), + std::forward_as_tuple(std::piecewise_construct, + std::forward_as_tuple(std::forward(k)), + std::forward_as_tuple(std::forward(args)...)))}; + } + + std::tuple tuple; + }; + + namespace init { + + template + [[nodiscard]] constexpr auto list(Args &&... args) { + return InitializerList{}(std::forward(args)...); + } + + template, typename... Args> + [[nodiscard]] constexpr auto map(Args &&... args) { + return list>(std::forward(args)...); + } + + template + [[nodiscard]] constexpr auto map(K &&k, V &&v) { + return list>(std::forward(k), std::forward(v)); + } + + } // namespace init +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/match.h b/sysbridge/src/main/cpp/android/ftl/match.h new file mode 100644 index 0000000000..490a4e72eb --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/match.h @@ -0,0 +1,63 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include + +namespace android::ftl { + +// Concise alternative to std::visit that compiles to branches rather than a dispatch table. For +// std::variant where N is small, this is slightly faster since the branches can be +// inlined unlike the function pointers. +// +// using namespace std::chrono; +// std::variant duration = 119min; +// +// // Mutable match. +// ftl::match(duration, [](auto& d) { ++d; }); +// +// // Immutable match. Exhaustive due to minutes being convertible to seconds. +// assert("2 hours"s == +// ftl::match(duration, +// [](const seconds& s) { +// const auto h = duration_cast(s); +// return std::to_string(h.count()) + " hours"s; +// }, +// [](const hours& h) { return std::to_string(h.count() / 24) + " days"s; })); +// + template + decltype(auto) match(std::variant &variant, Ms &&... matchers) { + const auto matcher = details::Matcher{std::forward(matchers)...}; + static_assert(details::is_exhaustive_match_v, + "Non-exhaustive match"); + + return details::Match::match(variant, matcher); + } + + template + decltype(auto) match(const std::variant &variant, Ms &&... matchers) { + const auto matcher = details::Matcher{std::forward(matchers)...}; + static_assert(details::is_exhaustive_match_v, + "Non-exhaustive match"); + + return details::Match::match(variant, matcher); + } + +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/mixins.h b/sysbridge/src/main/cpp/android/ftl/mixins.h new file mode 100644 index 0000000000..0cfae3ce27 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/mixins.h @@ -0,0 +1,152 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace android::ftl { + +// CRTP mixins for defining type-safe wrappers that are distinct from their underlying type. Common +// uses are IDs, opaque handles, and physical quantities. The constructor is provided by (and must +// be inherited from) the `Constructible` mixin, whereas operators (equality, ordering, arithmetic, +// etc.) are enabled through inheritance: +// +// struct Id : ftl::Constructible, ftl::Equatable { +// using Constructible::Constructible; +// }; +// +// static_assert(!std::is_default_constructible_v); +// +// Unlike `Constructible`, `DefaultConstructible` allows default construction. The default value is +// zero-initialized unless specified: +// +// struct Color : ftl::DefaultConstructible, +// ftl::Equatable, +// ftl::Orderable { +// using DefaultConstructible::DefaultConstructible; +// }; +// +// static_assert(Color() == Color(0u)); +// static_assert(ftl::to_underlying(Color(-1)) == 255u); +// static_assert(Color(1u) < Color(2u)); +// +// struct Sequence : ftl::DefaultConstructible, +// ftl::Equatable, +// ftl::Orderable, +// ftl::Incrementable { +// using DefaultConstructible::DefaultConstructible; +// }; +// +// static_assert(Sequence() == Sequence(-1)); +// +// The underlying type need not be a fundamental type: +// +// struct Timeout : ftl::DefaultConstructible, +// ftl::Equatable, +// ftl::Addable { +// using DefaultConstructible::DefaultConstructible; +// }; +// +// using namespace std::chrono_literals; +// static_assert(Timeout() + Timeout(5s) == Timeout(15s)); +// + template + struct Constructible { + explicit constexpr Constructible(T value) : value_(value) {} + + explicit constexpr operator const T &() const { return value_; } + + private: + template class> + friend + class details::Mixin; + + T value_; + }; + + template + struct DefaultConstructible : Constructible { + using Constructible::Constructible; + + constexpr DefaultConstructible() : DefaultConstructible(T{kDefault}) {} + }; + +// Shorthand for casting a type-safe wrapper to its underlying value. + template + constexpr const T &to_underlying(const Constructible &c) { + return static_cast(c); + } + +// Comparison operators for equality. + template + struct Equatable : details::Mixin { + constexpr bool operator==(const Self &other) const { + return to_underlying(this->self()) == to_underlying(other); + } + + constexpr bool operator!=(const Self &other) const { return !(*this == other); } + }; + +// Comparison operators for ordering. + template + struct Orderable : details::Mixin { + constexpr bool operator<(const Self &other) const { + return to_underlying(this->self()) < to_underlying(other); + } + + constexpr bool operator>(const Self &other) const { return other < this->self(); } + + constexpr bool operator>=(const Self &other) const { return !(*this < other); } + + constexpr bool operator<=(const Self &other) const { return !(*this > other); } + }; + +// Pre-increment and post-increment operators. + template + struct Incrementable : details::Mixin { + constexpr Self &operator++() { + ++this->mut(); + return this->self(); + } + + constexpr Self operator++(int) { + const Self tmp = this->self(); + operator++(); + return tmp; + } + }; + +// Additive operators, including incrementing. + template + struct Addable : details::Mixin, Incrementable { + constexpr Self &operator+=(const Self &other) { + this->mut() += to_underlying(other); + return this->self(); + } + + constexpr Self operator+(const Self &other) const { + Self tmp = this->self(); + return tmp += other; + } + + private: + using Base = details::Mixin; + using Base::mut; + using Base::self; + }; + +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/non_null.h b/sysbridge/src/main/cpp/android/ftl/non_null.h new file mode 100644 index 0000000000..df3e49dfd9 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/non_null.h @@ -0,0 +1,228 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +namespace android::ftl { + +// Enforces and documents non-null pre/post-condition for (raw or smart) pointers. +// +// void get_length(const ftl::NonNull>& string_ptr, +// ftl::NonNull length_ptr) { +// // No need for `nullptr` checks. +// *length_ptr = string_ptr->length(); +// } +// +// const auto string_ptr = ftl::as_non_null(std::make_shared("android")); +// std::size_t size; +// get_length(string_ptr, ftl::as_non_null(&size)); +// assert(size == 7u); +// +// For compatibility with std::unique_ptr and performance with std::shared_ptr, move +// operations are allowed despite breaking the invariant: +// +// using Pair = std::pair>, std::shared_ptr>; +// +// Pair dupe_if(ftl::NonNull> non_null_ptr, bool condition) { +// // Move the underlying pointer out, so `non_null_ptr` must not be accessed after this point. +// auto unique_ptr = std::move(non_null_ptr).take(); +// +// auto non_null_shared_ptr = ftl::as_non_null(std::shared_ptr(std::move(unique_ptr))); +// auto nullable_shared_ptr = condition ? non_null_shared_ptr.get() : nullptr; +// +// return {std::move(non_null_shared_ptr), std::move(nullable_shared_ptr)}; +// } +// +// auto ptr = ftl::as_non_null(std::make_unique(42)); +// const auto [ptr1, ptr2] = dupe_if(std::move(ptr), true); +// assert(ptr1.get() == ptr2); +// + template + class NonNull final { + struct Passkey { + }; + + public: + // Disallow `nullptr` explicitly for clear compilation errors. + NonNull() = delete; + + NonNull(std::nullptr_t) = delete; + + // Copy operations. + + constexpr NonNull(const NonNull &) = default; + + constexpr NonNull &operator=(const NonNull &) = default; + + template>> + constexpr NonNull(const NonNull &other) : pointer_(other.get()) {} + + template>> + constexpr NonNull &operator=(const NonNull &other) { + pointer_ = other.get(); + return *this; + } + + [[nodiscard]] constexpr const Pointer &get() const { return pointer_; } + + [[nodiscard]] constexpr explicit operator const Pointer &() const { return get(); } + + // Move operations. These break the invariant, so care must be taken to avoid subsequent access. + + constexpr NonNull(NonNull &&) = default; + + constexpr NonNull &operator=(NonNull &&) = default; + + [[nodiscard]] constexpr Pointer take() &&{ return std::move(pointer_); } + + [[nodiscard]] constexpr explicit operator Pointer() && { return take(); } + + // Dereferencing. + [[nodiscard]] constexpr decltype(auto) operator*() const { return *get(); } + + [[nodiscard]] constexpr decltype(auto) operator->() const { return get(); } + + [[nodiscard]] constexpr explicit operator bool() const { return !(pointer_ == nullptr); } + + // Private constructor for ftl::as_non_null. Excluded from candidate constructors for conversions + // through the passkey idiom, for clear compilation errors. + template + constexpr NonNull(Passkey, P &&pointer) : pointer_(std::forward

(pointer)) { + if (pointer_ == nullptr) std::abort(); + } + + private: + template + friend constexpr auto as_non_null(P &&) -> NonNull>; + + Pointer pointer_; + }; + + template + [[nodiscard]] constexpr auto as_non_null(P &&pointer) -> NonNull> { + using Passkey = typename NonNull>::Passkey; + return {Passkey{}, std::forward

(pointer)}; + } + +// NonNull

<=> NonNull + + template + constexpr bool operator==(const NonNull

&lhs, const NonNull &rhs) { + return lhs.get() == rhs.get(); + } + + template + constexpr bool operator!=(const NonNull

&lhs, const NonNull &rhs) { + return !operator==(lhs, rhs); + } + + template + constexpr bool operator<(const NonNull

&lhs, const NonNull &rhs) { + return lhs.get() < rhs.get(); + } + + template + constexpr bool operator<=(const NonNull

&lhs, const NonNull &rhs) { + return lhs.get() <= rhs.get(); + } + + template + constexpr bool operator>=(const NonNull

&lhs, const NonNull &rhs) { + return lhs.get() >= rhs.get(); + } + + template + constexpr bool operator>(const NonNull

&lhs, const NonNull &rhs) { + return lhs.get() > rhs.get(); + } + +// NonNull

<=> Q + + template + constexpr bool operator==(const NonNull

&lhs, const Q &rhs) { + return lhs.get() == rhs; + } + + template + constexpr bool operator!=(const NonNull

&lhs, const Q &rhs) { + return lhs.get() != rhs; + } + + template + constexpr bool operator<(const NonNull

&lhs, const Q &rhs) { + return lhs.get() < rhs; + } + + template + constexpr bool operator<=(const NonNull

&lhs, const Q &rhs) { + return lhs.get() <= rhs; + } + + template + constexpr bool operator>=(const NonNull

&lhs, const Q &rhs) { + return lhs.get() >= rhs; + } + + template + constexpr bool operator>(const NonNull

&lhs, const Q &rhs) { + return lhs.get() > rhs; + } + +// P <=> NonNull + + template + constexpr bool operator==(const P &lhs, const NonNull &rhs) { + return lhs == rhs.get(); + } + + template + constexpr bool operator!=(const P &lhs, const NonNull &rhs) { + return lhs != rhs.get(); + } + + template + constexpr bool operator<(const P &lhs, const NonNull &rhs) { + return lhs < rhs.get(); + } + + template + constexpr bool operator<=(const P &lhs, const NonNull &rhs) { + return lhs <= rhs.get(); + } + + template + constexpr bool operator>=(const P &lhs, const NonNull &rhs) { + return lhs >= rhs.get(); + } + + template + constexpr bool operator>(const P &lhs, const NonNull &rhs) { + return lhs > rhs.get(); + } + +} // namespace android::ftl + +// Specialize std::hash for ftl::NonNull +template +struct std::hash> { + std::size_t operator()(const android::ftl::NonNull

&ptr) const { + return std::hash

()(ptr.get()); + } +}; diff --git a/sysbridge/src/main/cpp/android/ftl/optional.h b/sysbridge/src/main/cpp/android/ftl/optional.h new file mode 100644 index 0000000000..c7b5d26a67 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/optional.h @@ -0,0 +1,147 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include "../libbase/expected.h" + +#include + +namespace android::ftl { + +// Superset of std::optional with monadic operations, as proposed in https://wg21.link/P0798R8. +// +// TODO: Remove standard APIs in C++23. +// + template + struct Optional final : std::optional { + using std::optional::optional; + + // Implicit downcast. + Optional(std::optional other) : std::optional(std::move(other)) {} + + using std::optional::has_value; + using std::optional::value; + + // Returns Optional where F is a function that maps T to U. + template + constexpr auto transform(F &&f) const &{ + using R = details::transform_result_t; + if (has_value()) return R(std::invoke(std::forward(f), value())); + return R(); + } + + template + constexpr auto transform(F &&f) &{ + using R = details::transform_result_t; + if (has_value()) return R(std::invoke(std::forward(f), value())); + return R(); + } + + template + constexpr auto transform(F &&f) const &&{ + using R = details::transform_result_t; + if (has_value()) return R(std::invoke(std::forward(f), std::move(value()))); + return R(); + } + + template + constexpr auto transform(F &&f) &&{ + using R = details::transform_result_t; + if (has_value()) return R(std::invoke(std::forward(f), std::move(value()))); + return R(); + } + + // Returns Optional where F is a function that maps T to Optional. + template + constexpr auto and_then(F &&f) const &{ + using R = details::and_then_result_t; + if (has_value()) return std::invoke(std::forward(f), value()); + return R(); + } + + template + constexpr auto and_then(F &&f) &{ + using R = details::and_then_result_t; + if (has_value()) return std::invoke(std::forward(f), value()); + return R(); + } + + template + constexpr auto and_then(F &&f) const &&{ + using R = details::and_then_result_t; + if (has_value()) return std::invoke(std::forward(f), std::move(value())); + return R(); + } + + template + constexpr auto and_then(F &&f) &&{ + using R = details::and_then_result_t; + if (has_value()) return std::invoke(std::forward(f), std::move(value())); + return R(); + } + + // Returns this Optional if not nullopt, or else the Optional returned by the function F. + template + constexpr auto or_else(F &&f) const & -> details::or_else_result_t { + if (has_value()) return *this; + return std::forward(f)(); + } + + template + constexpr auto or_else(F &&f) && -> details::or_else_result_t { + if (has_value()) return std::move(*this); + return std::forward(f)(); + } + + // Maps this Optional to expected where nullopt becomes E. + template + constexpr auto ok_or(E &&e) && -> base::expected { + if (has_value()) return std::move(value()); + return base::unexpected(std::forward(e)); + } + + // Delete new for this class. Its base doesn't have a virtual destructor, and + // if it got deleted via base class pointer, it would cause undefined + // behavior. There's not a good reason to allocate this object on the heap + // anyway. + static void *operator new(size_t) = delete; + + static void *operator new[](size_t) = delete; + }; + + template + constexpr bool operator==(const Optional &lhs, const Optional &rhs) { + return static_cast>(lhs) == static_cast>(rhs); + } + + template + constexpr bool operator!=(const Optional &lhs, const Optional &rhs) { + return !(lhs == rhs); + } + +// Deduction guides. + template + Optional(T) -> Optional; + + template + Optional(std::optional) -> Optional; + +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/shared_mutex.h b/sysbridge/src/main/cpp/android/ftl/shared_mutex.h new file mode 100644 index 0000000000..3008313038 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/shared_mutex.h @@ -0,0 +1,49 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace android::ftl { + +// Wrapper around std::shared_mutex to provide capabilities for thread-safety +// annotations. +// TODO(b/257958323): This class is no longer needed once b/135688034 is fixed (currently blocked on +// b/175635923). + class [[clang::capability("shared_mutex")]] SharedMutex final { + public: + [[clang::acquire_capability()]] void lock() { + mutex_.lock(); + } + + [[clang::release_capability()]] void unlock() { + mutex_.unlock(); + } + + [[clang::acquire_shared_capability()]] void lock_shared() { + mutex_.lock_shared(); + } + + [[clang::release_shared_capability()]] void unlock_shared() { + mutex_.unlock_shared(); + } + + private: + std::shared_mutex mutex_; + }; + +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/small_map.h b/sysbridge/src/main/cpp/android/ftl/small_map.h new file mode 100644 index 0000000000..f564909cef --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/small_map.h @@ -0,0 +1,309 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include +#include +#include +#include + +namespace android::ftl { + +// Associative container with unique, unordered keys. Unlike std::unordered_map, key-value pairs are +// stored in contiguous storage for cache efficiency. The map is allocated statically until its size +// exceeds N, at which point mappings are relocated to dynamic memory. The try_emplace operation has +// a non-standard analogue try_replace that destructively emplaces. The API also defines an in-place +// counterpart to insert_or_assign: emplace_or_replace. Lookup is done not via a subscript operator, +// but immutable getters that can optionally transform the value. +// +// SmallMap unconditionally allocates on the heap. +// +// Example usage: +// +// ftl::SmallMap map; +// assert(map.empty()); +// assert(!map.dynamic()); +// +// map = ftl::init::map(123, "abc")(-1)(42, 3u, '?'); +// assert(map.size() == 3u); +// assert(!map.dynamic()); +// +// assert(map.contains(123)); +// assert(map.get(42).transform([](const std::string& s) { return s.size(); }) == 3u); +// +// const auto opt = map.get(-1); +// assert(opt); +// +// std::string& ref = *opt; +// assert(ref.empty()); +// ref = "xyz"; +// +// map.emplace_or_replace(0, "vanilla", 2u, 3u); +// assert(map.dynamic()); +// +// assert(map == SmallMap(ftl::init::map(-1, "xyz"sv)(0, "nil"sv)(42, "???"sv)(123, "abc"sv))); +// + template> + class SmallMap final { + using Map = SmallVector, N>; + + template + friend + class SmallMap; + + public: + using key_type = K; + using mapped_type = V; + + using value_type = typename Map::value_type; + using size_type = typename Map::size_type; + using difference_type = typename Map::difference_type; + + using reference = typename Map::reference; + using iterator = typename Map::iterator; + + using const_reference = typename Map::const_reference; + using const_iterator = typename Map::const_iterator; + + // Creates an empty map. + SmallMap() = default; + + // Constructs at most N key-value pairs in place by forwarding per-pair constructor arguments. + // The template arguments K, V, and N are inferred using the deduction guide defined below. + // The syntax for listing pairs is as follows: + // + // ftl::SmallMap map = ftl::init::map(123, "abc")(-1)(42, 3u, '?'); + // static_assert(std::is_same_v>); + // + // The types of the key and value are deduced if the first pair contains exactly two arguments: + // + // ftl::SmallMap map = ftl::init::map(0, 'a')(1, 'b')(2, 'c'); + // static_assert(std::is_same_v>); + // + template + SmallMap(InitializerList, Types...> &&list) + : map_(std::move(list)) { + deduplicate(); + } + + // Copies or moves key-value pairs from a convertible map. + template + SmallMap(SmallMap other) : map_(std::move(other.map_)) {} + + static constexpr size_type static_capacity() { return N; } + + size_type max_size() const { return map_.max_size(); } + + size_type size() const { return map_.size(); } + + bool empty() const { return map_.empty(); } + + // Returns whether the map is backed by static or dynamic storage. + bool dynamic() const { + if constexpr (static_capacity() > 0) { + return map_.dynamic(); + } else { + return true; + } + } + + iterator begin() { return map_.begin(); } + + const_iterator begin() const { return cbegin(); } + + const_iterator cbegin() const { return map_.cbegin(); } + + iterator end() { return map_.end(); } + + const_iterator end() const { return cend(); } + + const_iterator cend() const { return map_.cend(); } + + // Returns whether a mapping exists for the given key. + bool contains(const key_type &key) const { return get(key).has_value(); } + + // Returns a reference to the value for the given key, or std::nullopt if the key was not found. + // + // ftl::SmallMap map = ftl::init::map('a', 'A')('b', 'B')('c', 'C'); + // + // const auto opt = map.get('c'); + // assert(opt == 'C'); + // + // char d = 'd'; + // const auto ref = map.get('d').value_or(std::ref(d)); + // ref.get() = 'D'; + // assert(d == 'D'); + // + auto get(const key_type &key) const -> Optional> { + for (const auto &[k, v]: *this) { + if (KeyEqual{}(k, key)) { + return std::cref(v); + } + } + return {}; + } + + auto get(const key_type &key) -> Optional> { + for (auto &[k, v]: *this) { + if (KeyEqual{}(k, key)) { + return std::ref(v); + } + } + return {}; + } + + // Returns an iterator to an existing mapping for the given key, or the end() iterator otherwise. + const_iterator find(const key_type &key) const { + return const_cast(*this).find(key); + } + + iterator find(const key_type &key) { return find(key, begin()); } + + // Inserts a mapping unless it exists. Returns an iterator to the inserted or existing mapping, + // and whether the mapping was inserted. + // + // On emplace, if the map reaches its static or dynamic capacity, then all iterators are + // invalidated. Otherwise, only the end() iterator is invalidated. + // + template + std::pair try_emplace(const key_type &key, Args &&... args) { + if (const auto it = find(key); it != end()) { + return {it, false}; + } + + decltype(auto) ref_or_it = + map_.emplace_back(std::piecewise_construct, std::forward_as_tuple(key), + std::forward_as_tuple(std::forward(args)...)); + + if constexpr (static_capacity() > 0) { + return {&ref_or_it, true}; + } else { + return {ref_or_it, true}; + } + } + + // Replaces a mapping if it exists, and returns an iterator to it. Returns the end() iterator + // otherwise. + // + // The value is replaced via move constructor, so type V does not need to define copy/move + // assignment, e.g. its data members may be const. + // + // The arguments may directly or indirectly refer to the mapping being replaced. + // + // Iterators to the replaced mapping point to its replacement, and others remain valid. + // + template + iterator try_replace(const key_type &key, Args &&... args) { + const auto it = find(key); + if (it == end()) return it; + map_.replace(it, std::piecewise_construct, std::forward_as_tuple(key), + std::forward_as_tuple(std::forward(args)...)); + return it; + } + + // In-place counterpart of std::unordered_map's insert_or_assign. Returns true on emplace, or + // false on replace. + // + // The value is emplaced and replaced via move constructor, so type V does not need to define + // copy/move assignment, e.g. its data members may be const. + // + // On emplace, if the map reaches its static or dynamic capacity, then all iterators are + // invalidated. Otherwise, only the end() iterator is invalidated. On replace, iterators + // to the replaced mapping point to its replacement, and others remain valid. + // + template + std::pair emplace_or_replace(const key_type &key, Args &&... args) { + const auto [it, ok] = try_emplace(key, std::forward(args)...); + if (ok) return {it, ok}; + map_.replace(it, std::piecewise_construct, std::forward_as_tuple(key), + std::forward_as_tuple(std::forward(args)...)); + return {it, ok}; + } + + // Removes a mapping if it exists, and returns whether it did. + // + // The last() and end() iterators, as well as those to the erased mapping, are invalidated. + // + bool erase(const key_type &key) { return erase(key, begin()); } + + // Removes a mapping. + // + // The last() and end() iterators, as well as those to the erased mapping, are invalidated. + // + void erase(iterator it) { map_.unstable_erase(it); } + + // Removes all mappings. + // + // All iterators are invalidated. + // + void clear() { map_.clear(); } + + private: + iterator find(const key_type &key, iterator first) { + return std::find_if(first, end(), + [&key](const auto &pair) { return KeyEqual{}(pair.first, key); }); + } + + bool erase(const key_type &key, iterator first) { + const auto it = find(key, first); + if (it == end()) return false; + map_.unstable_erase(it); + return true; + } + + void deduplicate() { + for (auto it = begin(); it != end();) { + if (const auto key = it->first; ++it != end()) { + while (erase(key, it)); + } + } + } + + Map map_; + }; + +// Deduction guide for in-place constructor. + template + SmallMap(InitializerList, std::index_sequence, Types...> &&) + -> SmallMap; + +// Returns whether the key-value pairs of two maps are equal. + template + bool operator==(const SmallMap &lhs, const SmallMap &rhs) { + if (lhs.size() != rhs.size()) return false; + + for (const auto &[k, v]: lhs) { + const auto &lv = v; + if (!rhs.get(k).transform([&lv](const W &rv) { return lv == rv; }).value_or(false)) { + return false; + } + } + + return true; + } + +// TODO: Remove in C++20. + template + inline bool operator!=(const SmallMap &lhs, const SmallMap &rhs) { + return !(lhs == rhs); + } + +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/small_vector.h b/sysbridge/src/main/cpp/android/ftl/small_vector.h new file mode 100644 index 0000000000..ebbe63f16e --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/small_vector.h @@ -0,0 +1,469 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + +#include + +namespace android::ftl { + + template + struct is_small_vector; + +// ftl::StaticVector that promotes to std::vector when full. SmallVector is a drop-in replacement +// for std::vector with statically allocated storage for N elements, whose goal is to improve run +// time by avoiding heap allocation and increasing probability of cache hits. The standard API is +// augmented by an unstable_erase operation that does not preserve order, and a replace operation +// that destructively emplaces. +// +// Unlike std::vector, T does not require copy/move assignment, so may be an object with const data +// members, or be const itself. +// +// SmallVector is a specialization that thinly wraps std::vector. +// +// Example usage: +// +// ftl::SmallVector vector; +// assert(vector.empty()); +// assert(!vector.dynamic()); +// +// vector = {'a', 'b', 'c'}; +// assert(vector.size() == 3u); +// assert(!vector.dynamic()); +// +// vector.push_back('d'); +// assert(vector.dynamic()); +// +// vector.unstable_erase(vector.begin()); +// assert(vector == (ftl::SmallVector{'d', 'b', 'c'})); +// +// vector.pop_back(); +// assert(vector.back() == 'b'); +// assert(vector.dynamic()); +// +// const char array[] = "hi"; +// vector = ftl::SmallVector(array); +// assert(vector == (ftl::SmallVector{'h', 'i', '\0'})); +// assert(!vector.dynamic()); +// +// ftl::SmallVector strings = ftl::init::list("abc")("123456", 3u)(3u, '?'); +// assert(strings.size() == 3u); +// assert(!strings.dynamic()); +// +// assert(strings[0] == "abc"); +// assert(strings[1] == "123"); +// assert(strings[2] == "???"); +// + template + class SmallVector final : details::ArrayTraits, details::ArrayComparators { + using Static = StaticVector; + using Dynamic = SmallVector; + + public: + FTL_ARRAY_TRAIT(T, value_type); + FTL_ARRAY_TRAIT(T, size_type); + FTL_ARRAY_TRAIT(T, difference_type); + + FTL_ARRAY_TRAIT(T, pointer); + FTL_ARRAY_TRAIT(T, reference); + FTL_ARRAY_TRAIT(T, iterator); + FTL_ARRAY_TRAIT(T, reverse_iterator); + + FTL_ARRAY_TRAIT(T, const_pointer); + FTL_ARRAY_TRAIT(T, const_reference); + FTL_ARRAY_TRAIT(T, const_iterator); + FTL_ARRAY_TRAIT(T, const_reverse_iterator); + + // Creates an empty vector. + SmallVector() = default; + + // Constructs at most N elements. See StaticVector for underlying constructors. + template>{}>> + SmallVector(Arg &&arg, Args &&... args) + : vector_(std::in_place_type, std::forward(arg), + std::forward(args)...) {} + + // Copies or moves elements from a smaller convertible vector. + template 0)>> + SmallVector(SmallVector other) : vector_(convert(std::move(other))) {} + + void swap(SmallVector &other) { vector_.swap(other.vector_); } + + // Returns whether the vector is backed by static or dynamic storage. + bool dynamic() const { return std::holds_alternative(vector_); } + + // Avoid std::visit as it generates a dispatch table. +#define DISPATCH(T, F, ...) \ + T F() __VA_ARGS__ { \ + return dynamic() ? std::get(vector_).F() : std::get(vector_).F(); \ + } + + DISPATCH(size_type, max_size, const) + + DISPATCH(size_type, size, const) + + DISPATCH(bool, empty, const) + + DISPATCH(iterator, begin,) + + DISPATCH(const_iterator, begin, const) + + DISPATCH(const_iterator, cbegin, const) + + DISPATCH(iterator, end,) + + DISPATCH(const_iterator, end, const) + + DISPATCH(const_iterator, cend, const) + + DISPATCH(reverse_iterator, rbegin,) + + DISPATCH(const_reverse_iterator, rbegin, const) + + DISPATCH(const_reverse_iterator, crbegin, const) + + DISPATCH(reverse_iterator, rend,) + + DISPATCH(const_reverse_iterator, rend, const) + + DISPATCH(const_reverse_iterator, crend, const) + + DISPATCH(iterator, last,) + + DISPATCH(const_iterator, last, const) + + DISPATCH(reference, front,) + + DISPATCH(const_reference, front, const) + + DISPATCH(reference, back,) + + DISPATCH(const_reference, back, const) + + reference operator[](size_type i) { + return dynamic() ? std::get(vector_)[i] : std::get(vector_)[i]; + } + + const_reference + operator[](size_type i) const { return const_cast(*this)[i]; } + + // Replaces an element, and returns a reference to it. The iterator must be dereferenceable, so + // replacing at end() is erroneous. + // + // The element is emplaced via move constructor, so type T does not need to define copy/move + // assignment, e.g. its data members may be const. + // + // The arguments may directly or indirectly refer to the element being replaced. + // + // Iterators to the replaced element point to its replacement, and others remain valid. + // + template + reference replace(const_iterator it, Args &&... args) { + if (dynamic()) { + return std::get(vector_).replace(it, std::forward(args)...); + } else { + return std::get(vector_).replace(it, std::forward(args)...); + } + } + + // Appends an element, and returns a reference to it. + // + // If the vector reaches its static or dynamic capacity, then all iterators are invalidated. + // Otherwise, only the end() iterator is invalidated. + // + template + reference emplace_back(Args &&... args) { + constexpr auto kInsertStatic = &Static::template emplace_back; + constexpr auto kInsertDynamic = &Dynamic::template emplace_back; + return *insert(std::forward(args)...); + } + + // Appends an element. + // + // If the vector reaches its static or dynamic capacity, then all iterators are invalidated. + // Otherwise, only the end() iterator is invalidated. + // + void push_back(const value_type &v) { + constexpr auto kInsertStatic = + static_cast(&Static::push_back); + constexpr auto kInsertDynamic = + static_cast(&Dynamic::push_back); + insert(v); + } + + void push_back(value_type &&v) { + constexpr auto kInsertStatic = static_cast(&Static::push_back); + constexpr auto kInsertDynamic = + static_cast(&Dynamic::push_back); + insert(std::move(v)); + } + + // Removes the last element. The vector must not be empty, or the call is erroneous. + // + // The last() and end() iterators are invalidated. + // + DISPATCH(void, pop_back,) + + // Removes all elements. + // + // All iterators are invalidated. + // + DISPATCH(void, clear,) + +#undef DISPATCH + + // Erases an element, but does not preserve order. Rather than shifting subsequent elements, + // this moves the last element to the slot of the erased element. + // + // The last() and end() iterators, as well as those to the erased element, are invalidated. + // + void unstable_erase(iterator it) { + if (dynamic()) { + std::get(vector_).unstable_erase(it); + } else { + std::get(vector_).unstable_erase(it); + } + } + + // Extracts the elements as std::vector. + std::vector> promote() &&{ + if (dynamic()) { + return std::get(std::move(vector_)).promote(); + } else { + return {std::make_move_iterator(begin()), std::make_move_iterator(end())}; + } + } + + private: + template + friend + class SmallVector; + + template + static std::variant convert(SmallVector &&other) { + using Other = SmallVector; + + if (other.dynamic()) { + return std::get(std::move(other.vector_)); + } else { + return std::get(std::move(other.vector_)); + } + } + + template + auto insert(Args &&... args) { + if (Dynamic *const vector = std::get_if(&vector_)) { + return (vector->*InsertDynamic)(std::forward(args)...); + } + + auto &vector = std::get(vector_); + if (vector.full()) { + return (promote(vector).*InsertDynamic)(std::forward(args)...); + } else { + return (vector.*InsertStatic)(std::forward(args)...); + } + } + + Dynamic &promote(Static &static_vector) { + assert(static_vector.full()); + + // Allocate double capacity to reduce probability of reallocation. + Dynamic vector; + vector.reserve(Static::max_size() * 2); + std::move(static_vector.begin(), static_vector.end(), std::back_inserter(vector)); + + return vector_.template emplace(std::move(vector)); + } + + std::variant vector_; + }; + +// Partial specialization without static storage. + template + class SmallVector final : details::ArrayTraits, + details::ArrayComparators, + details::ArrayIterators, T>, + std::vector> { + using details::ArrayTraits::replace_at; + + using Iter = details::ArrayIterators; + using Impl = std::vector>; + + friend Iter; + + public: + FTL_ARRAY_TRAIT(T, value_type); + FTL_ARRAY_TRAIT(T, size_type); + FTL_ARRAY_TRAIT(T, difference_type); + + FTL_ARRAY_TRAIT(T, pointer); + FTL_ARRAY_TRAIT(T, reference); + FTL_ARRAY_TRAIT(T, iterator); + FTL_ARRAY_TRAIT(T, reverse_iterator); + + FTL_ARRAY_TRAIT(T, const_pointer); + FTL_ARRAY_TRAIT(T, const_reference); + FTL_ARRAY_TRAIT(T, const_iterator); + FTL_ARRAY_TRAIT(T, const_reverse_iterator); + + // See std::vector for underlying constructors. + using Impl::Impl; + + // Copies and moves a vector, respectively. + SmallVector(const SmallVector &) = default; + + SmallVector(SmallVector &&) = default; + + // Constructs elements in place. See StaticVector for underlying constructor. + template + SmallVector(InitializerList, Types...> &&list) + : SmallVector(SmallVector(std::move(list))) {} + + // Copies or moves elements from a convertible vector. + template + SmallVector(SmallVector other) : Impl(convert(std::move(other))) {} + + SmallVector &operator=(SmallVector other) { + // Define copy/move assignment in terms of copy/move construction. + swap(other); + return *this; + } + + void swap(SmallVector &other) { Impl::swap(other); } + + using Impl::empty; + using Impl::max_size; + using Impl::size; + + using Impl::reserve; + + // std::vector iterators are not necessarily raw pointers. + iterator begin() { return Impl::data(); } + + iterator end() { return Impl::data() + size(); } + + using Iter::begin; + using Iter::end; + + using Iter::cbegin; + using Iter::cend; + + using Iter::rbegin; + using Iter::rend; + + using Iter::crbegin; + using Iter::crend; + + using Iter::last; + + using Iter::back; + using Iter::front; + + using Iter::operator[]; + + template + reference replace(const_iterator it, Args &&... args) { + return replace_at(it, std::forward(args)...); + } + + template + iterator emplace_back(Args &&... args) { + return &Impl::emplace_back(std::forward(args)...); + } + + bool push_back(const value_type &v) { + Impl::push_back(v); + return true; + } + + bool push_back(value_type &&v) { + Impl::push_back(std::move(v)); + return true; + } + + using Impl::clear; + using Impl::pop_back; + + void unstable_erase(iterator it) { + if (it != last()) replace(it, std::move(back())); + pop_back(); + } + + std::vector> promote() &&{ return std::move(*this); } + + private: + template + static Impl convert(SmallVector &&other) { + if constexpr (std::is_constructible_v> &&>) { + return std::move(other).promote(); + } else { + SmallVector vector(other.size()); + + // Consistently with StaticVector, T only requires copy/move construction from U, rather than + // copy/move assignment. + auto it = vector.begin(); + for (auto &element: other) { + vector.replace(it++, std::move(element)); + } + + return vector; + } + } + }; + + template + struct is_small_vector : std::false_type { + }; + + template + struct is_small_vector> : std::true_type { + }; + +// Deduction guide for array constructor. + template + SmallVector(T (&)[N]) -> SmallVector, N>; + +// Deduction guide for variadic constructor. + template, + typename = std::enable_if_t<(std::is_constructible_v && ...)>> + SmallVector(T &&, Us &&...) -> SmallVector; + +// Deduction guide for in-place constructor. + template + SmallVector(InitializerList, Types...> &&) + -> SmallVector; + +// Deduction guide for StaticVector conversion. + template + SmallVector(StaticVector &&) -> SmallVector; + + template + inline void swap(SmallVector &lhs, SmallVector &rhs) { + lhs.swap(rhs); + } + +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/static_vector.h b/sysbridge/src/main/cpp/android/ftl/static_vector.h new file mode 100644 index 0000000000..d14c12b390 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/static_vector.h @@ -0,0 +1,431 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace android::ftl { + + constexpr struct IteratorRangeTag { + } kIteratorRange; + +// Fixed-capacity, statically allocated counterpart of std::vector. Like std::array, StaticVector +// allocates contiguous storage for N elements of type T at compile time, but stores at most (rather +// than exactly) N elements. Unlike std::array, its default constructor does not require T to have a +// default constructor, since elements are constructed in place as the vector grows. Operations that +// insert an element (emplace_back, push_back, etc.) fail when the vector is full. The API otherwise +// adheres to standard containers, except the unstable_erase operation that does not preserve order, +// and the replace operation that destructively emplaces. +// +// Unlike std::vector, T does not require copy/move assignment, so may be an object with const data +// members, or be const itself. +// +// StaticVector is analogous to an iterable std::optional. +// StaticVector is an error. +// +// Example usage: +// +// ftl::StaticVector vector; +// assert(vector.empty()); +// +// vector = {'a', 'b'}; +// assert(vector.size() == 2u); +// +// vector.push_back('c'); +// assert(vector.full()); +// +// assert(!vector.push_back('d')); +// assert(vector.size() == 3u); +// +// vector.unstable_erase(vector.begin()); +// assert(vector == (ftl::StaticVector{'c', 'b'})); +// +// vector.pop_back(); +// assert(vector.back() == 'c'); +// +// const char array[] = "hi"; +// vector = ftl::StaticVector(array); +// assert(vector == (ftl::StaticVector{'h', 'i', '\0'})); +// +// ftl::StaticVector strings = ftl::init::list("abc")("123456", 3u)(3u, '?'); +// assert(strings.size() == 3u); +// assert(strings[0] == "abc"); +// assert(strings[1] == "123"); +// assert(strings[2] == "???"); +// + template + class StaticVector final : details::ArrayTraits, + details::ArrayIterators, T>, + details::ArrayComparators { + static_assert(N > 0); + + // For constructor that moves from a smaller convertible vector. + template + friend + class StaticVector; + + using details::ArrayTraits::construct_at; + using details::ArrayTraits::replace_at; + using details::ArrayTraits::in_place_swap_ranges; + using details::ArrayTraits::uninitialized_copy; + + using Iter = details::ArrayIterators; + friend Iter; + + // There is ambiguity when constructing from two iterator-like elements like pointers: + // they could be an iterator range, or arguments for in-place construction. Assume the + // latter unless they are input iterators and cannot be used to construct elements. If + // the former is intended, the caller can pass an IteratorRangeTag to disambiguate. + template> + using is_input_iterator = + std::conjunction, + std::negation>>; + + public: + FTL_ARRAY_TRAIT(T, value_type); + FTL_ARRAY_TRAIT(T, size_type); + FTL_ARRAY_TRAIT(T, difference_type); + + FTL_ARRAY_TRAIT(T, pointer); + FTL_ARRAY_TRAIT(T, reference); + FTL_ARRAY_TRAIT(T, iterator); + FTL_ARRAY_TRAIT(T, reverse_iterator); + + FTL_ARRAY_TRAIT(T, const_pointer); + FTL_ARRAY_TRAIT(T, const_reference); + FTL_ARRAY_TRAIT(T, const_iterator); + FTL_ARRAY_TRAIT(T, const_reverse_iterator); + + // Creates an empty vector. + StaticVector() = default; + + // Copies and moves a vector, respectively. + StaticVector(const StaticVector &other) + : StaticVector(kIteratorRange, other.begin(), other.end()) {} + + StaticVector(StaticVector &&other) { swap(other); } + + // Copies at most N elements from a smaller convertible vector. + template + StaticVector(const StaticVector &other) + : StaticVector(kIteratorRange, other.begin(), other.end()) { + static_assert(N >= M, "Insufficient capacity"); + } + + // Copies at most N elements from a smaller convertible array. + template + explicit StaticVector(U (&array)[M]) + : StaticVector(kIteratorRange, std::begin(array), std::end(array)) { + static_assert(N >= M, "Insufficient capacity"); + } + + // Copies at most N elements from the range [first, last). + // + // IteratorRangeTag disambiguates with initialization from two iterator-like elements. + // + template{}>> + StaticVector(Iterator first, Iterator last) : StaticVector(kIteratorRange, first, last) { + using V = typename std::iterator_traits::value_type; + static_assert(std::is_constructible_v, "Incompatible iterator range"); + } + + template + StaticVector(IteratorRangeTag, Iterator first, Iterator last) + : size_(std::min(max_size(), static_cast(std::distance(first, last)))) { + uninitialized_copy(first, first + size_, begin()); + } + + // Moves at most N elements from a smaller convertible vector. + template + StaticVector(StaticVector &&other) { + static_assert(N >= M, "Insufficient capacity"); + + // Same logic as swap, though M need not be equal to N. + std::uninitialized_move(other.begin(), other.end(), begin()); + std::destroy(other.begin(), other.end()); + std::swap(size_, other.size_); + } + + // Constructs at most N elements. The template arguments T and N are inferred using the + // deduction guide defined below. Note that T is determined from the first element, and + // subsequent elements must have convertible types: + // + // ftl::StaticVector vector = {1, 2, 3}; + // static_assert(std::is_same_v>); + // + // const auto copy = "quince"s; + // auto move = "tart"s; + // ftl::StaticVector vector = {copy, std::move(move)}; + // + // static_assert(std::is_same_v>); + // + template>> + StaticVector(E &&element, Es &&... elements) + : StaticVector(std::index_sequence<0>{}, std::forward(element), + std::forward(elements)...) { + static_assert(sizeof...(elements) < N, "Too many elements"); + } + + // Constructs at most N elements in place by forwarding per-element constructor arguments. The + // template arguments T and N are inferred using the deduction guide defined below. The syntax + // for listing arguments is as follows: + // + // ftl::StaticVector vector = ftl::init::list("abc")()(3u, '?'); + // + // static_assert(std::is_same_v>); + // assert(vector.full()); + // assert(vector[0] == "abc"); + // assert(vector[1].empty()); + // assert(vector[2] == "???"); + // + template + StaticVector(InitializerList, Types...> &&list) + : StaticVector(std::index_sequence<0, 0, Size>{}, std::make_index_sequence{}, + std::index_sequence{}, list.tuple) { + static_assert(sizeof...(Sizes) < N, "Too many elements"); + } + + ~StaticVector() { std::destroy(begin(), end()); } + + StaticVector &operator=(const StaticVector &other) { + StaticVector copy(other); + swap(copy); + return *this; + } + + StaticVector &operator=(StaticVector &&other) { + clear(); + swap(other); + return *this; + } + + // IsEmpty enables a fast path when the vector is known to be empty at compile time. + template + void swap(StaticVector &); + + static constexpr size_type max_size() { return N; } + + size_type size() const { return size_; } + + bool empty() const { return size() == 0; } + + bool full() const { return size() == max_size(); } + + iterator begin() { return std::launder(reinterpret_cast(data_)); } + + iterator end() { return begin() + size(); } + + using Iter::begin; + using Iter::end; + + using Iter::cbegin; + using Iter::cend; + + using Iter::rbegin; + using Iter::rend; + + using Iter::crbegin; + using Iter::crend; + + using Iter::last; + + using Iter::back; + using Iter::front; + + using Iter::operator[]; + + // Replaces an element, and returns a reference to it. The iterator must be dereferenceable, so + // replacing at end() is erroneous. + // + // The element is emplaced via move constructor, so type T does not need to define copy/move + // assignment, e.g. its data members may be const. + // + // The arguments may directly or indirectly refer to the element being replaced. + // + // Iterators to the replaced element point to its replacement, and others remain valid. + // + template + reference replace(const_iterator it, Args &&... args) { + return replace_at(it, std::forward(args)...); + } + + // Appends an element, and returns an iterator to it. If the vector is full, the element is not + // inserted, and the end() iterator is returned. + // + // On success, the end() iterator is invalidated. + // + template + iterator emplace_back(Args &&... args) { + if (full()) return end(); + const iterator it = construct_at(end(), std::forward(args)...); + ++size_; + return it; + } + + // Appends an element unless the vector is full, and returns whether the element was inserted. + // + // On success, the end() iterator is invalidated. + // + bool push_back(const value_type &v) { + // Two statements for sequence point. + const iterator it = emplace_back(v); + return it != end(); + } + + bool push_back(value_type &&v) { + // Two statements for sequence point. + const iterator it = emplace_back(std::move(v)); + return it != end(); + } + + // Removes the last element. The vector must not be empty, or the call is erroneous. + // + // The last() and end() iterators are invalidated. + // + void pop_back() { unstable_erase(last()); } + + // Removes all elements. + // + // All iterators are invalidated. + // + void clear() { + std::destroy(begin(), end()); + size_ = 0; + } + + // Erases an element, but does not preserve order. Rather than shifting subsequent elements, + // this moves the last element to the slot of the erased element. + // + // The last() and end() iterators, as well as those to the erased element, are invalidated. + // + void unstable_erase(const_iterator it) { + std::destroy_at(it); + if (it != last()) { + // Move last element and destroy its source for destructor side effects. This is only + // safe because exceptions are disabled. + construct_at(it, std::move(back())); + std::destroy_at(last()); + } + --size_; + } + + private: + // Recursion for variadic constructor. + template + StaticVector(std::index_sequence, E &&element, Es &&... elements) + : StaticVector(std::index_sequence{}, std::forward(elements)...) { + construct_at(begin() + I, std::forward(element)); + } + + // Base case for variadic constructor. + template + explicit StaticVector(std::index_sequence) : size_(I) {} + + // Recursion for in-place constructor. + // + // Construct element I by extracting its arguments from the InitializerList tuple. ArgIndex + // is the position of its first argument in Args, and ArgCount is the number of arguments. + // The Indices sequence corresponds to [0, ArgCount). + // + // The Sizes sequence lists the argument counts for elements after I, so Size is the ArgCount + // for the next element. The recursion stops when Sizes is empty for the last element. + // + template + StaticVector(std::index_sequence, std::index_sequence, + std::index_sequence, std::tuple &tuple) + : StaticVector(std::index_sequence{}, + std::make_index_sequence{}, std::index_sequence{}, + tuple) { + construct_at(begin() + I, std::move(std::get(tuple))...); + } + + // Base case for in-place constructor. + template + StaticVector(std::index_sequence, std::index_sequence, + std::index_sequence<>, std::tuple &tuple) + : size_(I + 1) { + construct_at(begin() + I, std::move(std::get(tuple))...); + } + + size_type size_ = 0; + std::aligned_storage_t data_[N]; + }; + +// Deduction guide for array constructor. + template + StaticVector(T (&)[N]) -> StaticVector, N>; + +// Deduction guide for variadic constructor. + template, + typename = std::enable_if_t<(std::is_constructible_v && ...)>> + StaticVector(T &&, Us &&...) -> StaticVector; + +// Deduction guide for in-place constructor. + template + StaticVector(InitializerList, Types...> &&) + -> StaticVector; + + template + template + void StaticVector::swap(StaticVector &other) { + auto [to, from] = std::make_pair(this, &other); + if (from == this) return; + + // Assume this vector has fewer elements, so the excess of the other vector will be moved to it. + auto [min, max] = std::make_pair(size(), other.size()); + + // No elements to swap if moving into an empty vector. + if constexpr (IsEmpty) { + assert(min == 0); + } else { + if (min > max) { + std::swap(from, to); + std::swap(min, max); + } + + // Swap elements [0, min). + in_place_swap_ranges(begin(), begin() + min, other.begin()); + + // No elements to move if sizes are equal. + if (min == max) return; + } + + // Move elements [min, max) and destroy their source for destructor side effects. + const auto [first, last] = std::make_pair(from->begin() + min, from->begin() + max); + std::uninitialized_move(first, last, to->begin() + min); + std::destroy(first, last); + + std::swap(size_, other.size_); + } + + template + inline void swap(StaticVector &lhs, StaticVector &rhs) { + lhs.swap(rhs); + } + +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/string.h b/sysbridge/src/main/cpp/android/ftl/string.h new file mode 100644 index 0000000000..46e45e8169 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/string.h @@ -0,0 +1,105 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace android::ftl { + + enum class Radix { + kBin = 2, kDec = 10, kHex = 16 + }; + + template + struct to_chars_length { + static_assert(std::is_integral_v); + // Maximum binary digits, plus minus sign and radix prefix. + static constexpr std::size_t value = + std::numeric_limits>::digits + 3; + }; + + template + constexpr std::size_t to_chars_length_v = to_chars_length::value; + + template + using to_chars_buffer_t = char[to_chars_length_v]; + +// Lightweight (not allocating nor sprintf-based) alternative to std::to_string for integers, with +// optional radix. See also ftl::to_string below. +// +// ftl::to_chars_buffer_t<> buffer; +// +// assert(ftl::to_chars(buffer, 123u) == "123"); +// assert(ftl::to_chars(buffer, -42, ftl::Radix::kBin) == "-0b101010"); +// assert(ftl::to_chars(buffer, 0xcafe, ftl::Radix::kHex) == "0xcafe"); +// assert(ftl::to_chars(buffer, '*', ftl::Radix::kHex) == "0x2a"); +// + template + std::string_view to_chars(char (&buffer)[N], T v, Radix radix = Radix::kDec) { + static_assert(N >= to_chars_length_v); + + auto begin = buffer + 2; + const auto [end, err] = std::to_chars(begin, buffer + N, v, static_cast(radix)); + assert(err == std::errc()); + + if (radix == Radix::kDec) { + // TODO: Replace with {begin, end} in C++20. + return {begin, static_cast(end - begin)}; + } + + const auto prefix = radix == Radix::kBin ? 'b' : 'x'; + if constexpr (std::is_unsigned_v) { + buffer[0] = '0'; + buffer[1] = prefix; + } else { + if (*begin == '-') { + *buffer = '-'; + } else { + --begin; + } + + *begin-- = prefix; + *begin = '0'; + } + + // TODO: Replace with {buffer, end} in C++20. + return {buffer, static_cast(end - buffer)}; + } + +// Lightweight (not sprintf-based) alternative to std::to_string for integers, with optional radix. +// +// assert(ftl::to_string(123u) == "123"); +// assert(ftl::to_string(-42, ftl::Radix::kBin) == "-0b101010"); +// assert(ftl::to_string(0xcafe, ftl::Radix::kHex) == "0xcafe"); +// assert(ftl::to_string('*', ftl::Radix::kHex) == "0x2a"); +// + template + inline std::string to_string(T v, Radix radix = Radix::kDec) { + to_chars_buffer_t buffer; + return std::string(to_chars(buffer, v, radix)); + } + + std::string to_string(bool) = delete; + + std::string to_string(bool, Radix) = delete; + +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/ftl/unit.h b/sysbridge/src/main/cpp/android/ftl/unit.h new file mode 100644 index 0000000000..2dd71e43a1 --- /dev/null +++ b/sysbridge/src/main/cpp/android/ftl/unit.h @@ -0,0 +1,79 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +namespace android::ftl { + +// The unit type, and its only value. + constexpr struct Unit { + } unit; + + constexpr bool operator==(Unit, Unit) { + return true; + } + + constexpr bool operator!=(Unit, Unit) { + return false; + } + +// Adapts a function object F to return Unit. The return value of F is ignored. +// +// As a practical use, the function passed to ftl::Optional::transform is not allowed to return +// void (cf. https://wg21.link/P0798R8#mapping-functions-returning-void), but may return Unit if +// only its side effects are meaningful: +// +// ftl::Optional opt = "food"s; +// opt.transform(ftl::unit_fn([](std::string& str) { str.pop_back(); })); +// assert(opt == "foo"s); +// + template + struct UnitFn { + F f; + + template + Unit operator()(Args &&... args) { + return f(std::forward(args)...), unit; + } + }; + + template + constexpr auto unit_fn(F &&f) -> UnitFn> { + return {std::forward(f)}; + } + + namespace details { + +// Identity function for all T except Unit, which maps to void. + template + struct UnitToVoid { + template + static auto from(U &&value) { + return value; + } + }; + + template<> + struct UnitToVoid { + template + static void from(U &&) {} + }; + + } // namespace details +} // namespace android::ftl diff --git a/sysbridge/src/main/cpp/android/input/Input.cpp b/sysbridge/src/main/cpp/android/input/Input.cpp new file mode 100644 index 0000000000..7af317a488 --- /dev/null +++ b/sysbridge/src/main/cpp/android/input/Input.cpp @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#define LOG_TAG "Input" + +namespace android { + + bool isFromSource(uint32_t source, uint32_t test) { + return (source & test) == test; + } +} \ No newline at end of file diff --git a/sysbridge/src/main/cpp/android/input/Input.h b/sysbridge/src/main/cpp/android/input/Input.h new file mode 100644 index 0000000000..b07479be2a --- /dev/null +++ b/sysbridge/src/main/cpp/android/input/Input.h @@ -0,0 +1,71 @@ + +/* + * Flags that flow alongside events in the input dispatch system to help with certain + * policy decisions such as waking from device sleep. + * + * These flags are also defined in frameworks/base/core/java/android/view/WindowManagerPolicy.java. + */ +enum { + /* These flags originate in RawEvents and are generally set in the key map. + * NOTE: If you want a flag to be able to set in a keylayout file, then you must add it to + * InputEventLabels.h as well. */ + + // Indicates that the event should wake the device. + POLICY_FLAG_WAKE = 0x00000001, + + // Indicates that the key is virtual, such as a capacitive button, and should + // generate haptic feedback. Virtual keys may be suppressed for some time + // after a recent touch to prevent accidental activation of virtual keys adjacent + // to the touch screen during an edge swipe. + POLICY_FLAG_VIRTUAL = 0x00000002, + + // Indicates that the key is the special function modifier. + POLICY_FLAG_FUNCTION = 0x00000004, + + // Indicates that the key represents a special gesture that has been detected by + // the touch firmware or driver. Causes touch events from the same device to be canceled. + // This policy flag prevents key events from changing touch mode state. + POLICY_FLAG_GESTURE = 0x00000008, + + // Indicates that key usage mapping represents a fallback mapping. + // Fallback mappings cannot be used to definitively determine whether a device + // supports a key code. For example, a HID device can report a key press + // as a HID usage code if it is not mapped to any linux key code in the kernel. + // However, we cannot know which HID usage codes that device supports from + // userspace through the evdev. We can use fallback mappings to convert HID + // usage codes to Android key codes without needing to know if a device can + // actually report the usage code. + POLICY_FLAG_FALLBACK_USAGE_MAPPING = 0x00000010, + + POLICY_FLAG_RAW_MASK = 0x0000ffff, + + /* These flags are set by the input dispatcher. */ + + // Indicates that the input event was injected. + POLICY_FLAG_INJECTED = 0x01000000, + + // Indicates that the input event is from a trusted source such as a directly attached + // input device or an application with system-wide event injection permission. + POLICY_FLAG_TRUSTED = 0x02000000, + + // Indicates that the input event has passed through an input filter. + POLICY_FLAG_FILTERED = 0x04000000, + + // Disables automatic key repeating behavior. + POLICY_FLAG_DISABLE_KEY_REPEAT = 0x08000000, + + /* These flags are set by the input reader policy as it intercepts each event. */ + + // Indicates that the device was in an interactive state when the + // event was intercepted. + POLICY_FLAG_INTERACTIVE = 0x20000000, + + // Indicates that the event should be dispatched to applications. + // The input event should still be sent to the InputDispatcher so that it can see all + // input events received include those that it will not deliver. + POLICY_FLAG_PASS_TO_USER = 0x40000000, +}; + +namespace android { + bool isFromSource(uint32_t source, uint32_t test); +} \ No newline at end of file diff --git a/sysbridge/src/main/cpp/android/input/InputDevice.cpp b/sysbridge/src/main/cpp/android/input/InputDevice.cpp new file mode 100644 index 0000000000..66c72d20a7 --- /dev/null +++ b/sysbridge/src/main/cpp/android/input/InputDevice.cpp @@ -0,0 +1,358 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +#include +#include +#include + +#include "../logging.h" +#include "../libbase/stringprintf.h" +#include +#include "InputDevice.h" +#include "InputEventLabels.h" +#include "../ui/LogicalDisplayId.h" + +using android::base::StringPrintf; + +namespace android { + +// Set to true to log detailed debugging messages about IDC file probing. + static constexpr bool DEBUG_PROBE = false; + + static const char *CONFIGURATION_FILE_DIR[] = { + "idc/", + "keylayout/", + "keychars/", + }; + + static const char *CONFIGURATION_FILE_EXTENSION[] = { + ".idc", + ".kl", + ".kcm", + }; + + static bool isValidNameChar(char ch) { + return isascii(ch) && (isdigit(ch) || isalpha(ch) || ch == '-' || ch == '_'); + } + + static void appendInputDeviceConfigurationFileRelativePath(std::string &path, + const std::string &name, + InputDeviceConfigurationFileType type) { + path += CONFIGURATION_FILE_DIR[static_cast(type)]; + path += name; + path += CONFIGURATION_FILE_EXTENSION[static_cast(type)]; + } + + std::string getInputDeviceConfigurationFilePathByDeviceIdentifier( + const InputDeviceIdentifier &deviceIdentifier, InputDeviceConfigurationFileType type, + const char *suffix) { + if (deviceIdentifier.vendor != 0 && deviceIdentifier.product != 0) { + if (deviceIdentifier.version != 0) { + // Try vendor product version. + std::string versionPath = + getInputDeviceConfigurationFilePathByName( + StringPrintf("Vendor_%04x_Product_%" + "04x_Version_%04x%s", + deviceIdentifier.vendor, + deviceIdentifier.product, + deviceIdentifier.version, + suffix), + type); + if (!versionPath.empty()) { + LOGI("Found key layout map by version path %s", versionPath.c_str()); + return versionPath; + } + } + + // Try vendor product. + std::string productPath = + getInputDeviceConfigurationFilePathByName( + StringPrintf("Vendor_%04x_Product_%04x%s", + deviceIdentifier.vendor, + deviceIdentifier.product, + suffix), + type); + if (!productPath.empty()) { + LOGI("Found key layout map by product path %s", productPath.c_str()); + return productPath; + } + } + + // Try device name. + std::string namePath = getInputDeviceConfigurationFilePathByName( + deviceIdentifier.getCanonicalName() + suffix, + type); + + if (!namePath.empty()) { + LOGI("Found key layout map by name path %s", namePath.c_str()); + return namePath; + } + + // As a last resort, just use the Generic file. + return getInputDeviceConfigurationFilePathByName("Generic", type); + } + + std::string getInputDeviceConfigurationFilePathByName( + const std::string &name, InputDeviceConfigurationFileType type) { + // Search system repository. + std::string path; + + // Treblized input device config files will be located /product/usr, /system_ext/usr, + // /odm/usr or /vendor/usr. + std::vector pathPrefixes{ + "/product/usr/", + "/system_ext/usr/", + "/odm/usr/", + "/vendor/usr/", + "/system/usr", + }; + // These files may also be in the APEX pointed by input_device.config_file.apex sysprop. +// if (auto apex = GetProperty("input_device.config_file.apex", ""); !apex.empty()) { +// pathPrefixes.push_back("/apex/" + apex + "/etc/usr/"); +// } + // ANDROID_ROOT may not be set on host + if (auto android_root = getenv("ANDROID_ROOT"); android_root != nullptr) { + pathPrefixes.push_back(std::string(android_root) + "/usr/"); + } + for (const auto &prefix: pathPrefixes) { + path = prefix; + appendInputDeviceConfigurationFileRelativePath(path, name, type); + if (!access(path.c_str(), R_OK)) { + if (DEBUG_PROBE) { + LOGI("Found system-provided input device configuration file at %s", + path.c_str()); + } + return path; + } else if (errno != ENOENT) { + if (DEBUG_PROBE) { + LOGW("Couldn't find a system-provided input device configuration file at %s due to error %d (%s); there may be an IDC file there that cannot be loaded.", + path.c_str(), errno, strerror(errno)); + } + } else { + if (DEBUG_PROBE) { + LOGE("Didn't find system-provided input device configuration file at %s: %s", + path.c_str(), strerror(errno)); + } + } + } + + // Search user repository. + // TODO Should only look here if not in safe mode. + path = ""; + char *androidData = getenv("ANDROID_DATA"); + if (androidData != nullptr) { + path += androidData; + } + path += "/system/devices/"; + appendInputDeviceConfigurationFileRelativePath(path, name, type); + if (!access(path.c_str(), R_OK)) { + if (DEBUG_PROBE) { + LOGI("Found system user input device configuration file at %s", path.c_str()); + } + return path; + } else if (errno != ENOENT) { + LOGW("Couldn't find a system user input device configuration file at %s due to error %d (%s); there may be an IDC file there that cannot be loaded.", + path.c_str(), errno, strerror(errno)); + } else { + if (DEBUG_PROBE) { + LOGE("Didn't find system user input device configuration file at %s: %s", + path.c_str(), strerror(errno)); + } + } + + // Not found. + if (DEBUG_PROBE) { + LOGI("Probe failed to find input device configuration file with name '%s' and type %s", + name.c_str(), ftl::enum_string(type).c_str()); + } + + + return ""; + } + +// --- InputDeviceIdentifier + + std::string InputDeviceIdentifier::getCanonicalName() const { + std::string replacedName = name; + for (char &ch: replacedName) { + if (!isValidNameChar(ch)) { + ch = '_'; + } + } + return replacedName; + } + + +// --- InputDeviceInfo --- + + InputDeviceInfo::InputDeviceInfo() { + initialize(-1, 0, -1, InputDeviceIdentifier(), "", false, false, + ui::LogicalDisplayId::INVALID); + } + + InputDeviceInfo::InputDeviceInfo(const InputDeviceInfo &other) + : mId(other.mId), + mGeneration(other.mGeneration), + mControllerNumber(other.mControllerNumber), + mIdentifier(other.mIdentifier), + mAlias(other.mAlias), + mIsExternal(other.mIsExternal), + mHasMic(other.mHasMic), + mKeyboardLayoutInfo(other.mKeyboardLayoutInfo), + mSources(other.mSources), + mKeyboardType(other.mKeyboardType), + mUsiVersion(other.mUsiVersion), + mAssociatedDisplayId(other.mAssociatedDisplayId), + mEnabled(other.mEnabled), + mHasVibrator(other.mHasVibrator), + mHasBattery(other.mHasBattery), + mHasButtonUnderPad(other.mHasButtonUnderPad), + mHasSensor(other.mHasSensor), + mMotionRanges(other.mMotionRanges), + mSensors(other.mSensors), + mLights(other.mLights), + mViewBehavior(other.mViewBehavior) {} + + InputDeviceInfo &InputDeviceInfo::operator=(const InputDeviceInfo &other) { + mId = other.mId; + mGeneration = other.mGeneration; + mControllerNumber = other.mControllerNumber; + mIdentifier = other.mIdentifier; + mAlias = other.mAlias; + mIsExternal = other.mIsExternal; + mHasMic = other.mHasMic; + mKeyboardLayoutInfo = other.mKeyboardLayoutInfo; + mSources = other.mSources; + mKeyboardType = other.mKeyboardType; + mUsiVersion = other.mUsiVersion; + mAssociatedDisplayId = other.mAssociatedDisplayId; + mEnabled = other.mEnabled; + mHasVibrator = other.mHasVibrator; + mHasBattery = other.mHasBattery; + mHasButtonUnderPad = other.mHasButtonUnderPad; + mHasSensor = other.mHasSensor; + mMotionRanges = other.mMotionRanges; + mSensors = other.mSensors; + mLights = other.mLights; + mViewBehavior = other.mViewBehavior; + return *this; + } + + InputDeviceInfo::~InputDeviceInfo() { + } + + void InputDeviceInfo::initialize(int32_t id, int32_t generation, int32_t controllerNumber, + const InputDeviceIdentifier &identifier, + const std::string &alias, + bool isExternal, bool hasMic, + ui::LogicalDisplayId associatedDisplayId, + InputDeviceViewBehavior viewBehavior, bool enabled) { + mId = id; + mGeneration = generation; + mControllerNumber = controllerNumber; + mIdentifier = identifier; + mAlias = alias; + mIsExternal = isExternal; + mHasMic = hasMic; + mSources = 0; + mKeyboardType = AINPUT_KEYBOARD_TYPE_NONE; + mAssociatedDisplayId = associatedDisplayId; + mEnabled = enabled; + mHasVibrator = false; + mHasBattery = false; + mHasButtonUnderPad = false; + mHasSensor = false; + mViewBehavior = viewBehavior; + mUsiVersion.reset(); + mMotionRanges.clear(); + mSensors.clear(); + mLights.clear(); + } + + const InputDeviceInfo::MotionRange *InputDeviceInfo::getMotionRange( + int32_t axis, uint32_t source) const { + for (const MotionRange &range: mMotionRanges) { + if (range.axis == axis && isFromSource(range.source, source)) { + return ⦥ + } + } + return nullptr; + } + + void InputDeviceInfo::addSource(uint32_t source) { + mSources |= source; + } + + void InputDeviceInfo::addMotionRange(int32_t axis, uint32_t source, float min, float max, + float flat, float fuzz, float resolution) { + MotionRange range = {axis, source, min, max, flat, fuzz, resolution}; + mMotionRanges.push_back(range); + } + + void InputDeviceInfo::addMotionRange(const MotionRange &range) { + mMotionRanges.push_back(range); + } + + void InputDeviceInfo::addSensorInfo(const InputDeviceSensorInfo &info) { + if (mSensors.find(info.type) != mSensors.end()) { + LOGW("Sensor type %s already exists, will be replaced by new sensor added.", + ftl::enum_string(info.type).c_str()); + } + mSensors.insert_or_assign(info.type, info); + } + + void InputDeviceInfo::addBatteryInfo(const InputDeviceBatteryInfo &info) { + if (mBatteries.find(info.id) != mBatteries.end()) { + LOGW("Battery id %d already exists, will be replaced by new battery added.", info.id); + } + mBatteries.insert_or_assign(info.id, info); + } + + void InputDeviceInfo::addLightInfo(const InputDeviceLightInfo &info) { + if (mLights.find(info.id) != mLights.end()) { + LOGW("Light id %d already exists, will be replaced by new light added.", info.id); + } + mLights.insert_or_assign(info.id, info); + } + + void InputDeviceInfo::setKeyboardType(int32_t keyboardType) { + mKeyboardType = keyboardType; + } + + void InputDeviceInfo::setKeyboardLayoutInfo(KeyboardLayoutInfo layoutInfo) { + mKeyboardLayoutInfo = std::move(layoutInfo); + } + + std::vector InputDeviceInfo::getSensors() { + std::vector infos; + infos.reserve(mSensors.size()); + for (const auto &[type, info]: mSensors) { + infos.push_back(info); + } + return infos; + } + + std::vector InputDeviceInfo::getLights() { + std::vector infos; + infos.reserve(mLights.size()); + for (const auto &[id, info]: mLights) { + infos.push_back(info); + } + return infos; + } + +} // namespace android \ No newline at end of file diff --git a/sysbridge/src/main/cpp/android/input/InputDevice.h b/sysbridge/src/main/cpp/android/input/InputDevice.h new file mode 100644 index 0000000000..c7af07cc20 --- /dev/null +++ b/sysbridge/src/main/cpp/android/input/InputDevice.h @@ -0,0 +1,455 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include "../ftl/flags.h" +#include "../ftl/mixins.h" +#include "Input.h" +#include +#include +#include +#include "../ui/LogicalDisplayId.h" + +namespace android { + +/* + * Identifies a device. + */ + struct InputDeviceIdentifier { + inline InputDeviceIdentifier() : + bus(0), vendor(0), product(0), version(0) { + } + + // Information provided by the kernel. + std::string name; + std::string location; + std::string uniqueId; + int bus; + int vendor; + int product; + int version; + + // A composite input device descriptor string that uniquely identifies the device + // even across reboots or reconnections. The value of this field is used by + // upper layers of the input system to associate settings with individual devices. + // It is hashed from whatever kernel provided information is available. + // Ideally, the way this value is computed should not change between Android releases + // because that would invalidate persistent settings that rely on it. + std::string descriptor; + + // A value added to uniquely identify a device in the absence of a unique id. This + // is intended to be a minimum way to distinguish from other active devices and may + // reuse values that are not associated with an input anymore. + uint16_t nonce; + + // The bluetooth address of the device, if known. + std::optional bluetoothAddress; + + /** + * Return InputDeviceIdentifier.name that has been adjusted as follows: + * - all characters besides alphanumerics, dash, + * and underscore have been replaced with underscores. + * This helps in situations where a file that matches the device name is needed, + * while conforming to the filename limitations. + */ + std::string getCanonicalName() const; + + bool operator==(const InputDeviceIdentifier &) const = default; + + bool operator!=(const InputDeviceIdentifier &) const = default; + }; + +/** + * Holds View related behaviors for an InputDevice. + */ + struct InputDeviceViewBehavior { + /** + * The smooth scroll behavior that applies for all source/axis, if defined by the device. + * Empty optional if the device has not specified the default smooth scroll behavior. + */ + std::optional shouldSmoothScroll; + }; + +/* Types of input device sensors. Keep sync with core/java/android/hardware/Sensor.java */ + enum class InputDeviceSensorType : int32_t { + ACCELEROMETER = ASENSOR_TYPE_ACCELEROMETER, + MAGNETIC_FIELD = ASENSOR_TYPE_MAGNETIC_FIELD, + ORIENTATION = 3, + GYROSCOPE = ASENSOR_TYPE_GYROSCOPE, + LIGHT = ASENSOR_TYPE_LIGHT, + PRESSURE = ASENSOR_TYPE_PRESSURE, + TEMPERATURE = 7, + PROXIMITY = ASENSOR_TYPE_PROXIMITY, + GRAVITY = ASENSOR_TYPE_GRAVITY, + LINEAR_ACCELERATION = ASENSOR_TYPE_LINEAR_ACCELERATION, + ROTATION_VECTOR = ASENSOR_TYPE_ROTATION_VECTOR, + RELATIVE_HUMIDITY = ASENSOR_TYPE_RELATIVE_HUMIDITY, + AMBIENT_TEMPERATURE = ASENSOR_TYPE_AMBIENT_TEMPERATURE, + MAGNETIC_FIELD_UNCALIBRATED = ASENSOR_TYPE_MAGNETIC_FIELD_UNCALIBRATED, + GAME_ROTATION_VECTOR = ASENSOR_TYPE_GAME_ROTATION_VECTOR, + GYROSCOPE_UNCALIBRATED = ASENSOR_TYPE_GYROSCOPE_UNCALIBRATED, + SIGNIFICANT_MOTION = ASENSOR_TYPE_SIGNIFICANT_MOTION, + + ftl_first = ACCELEROMETER, + ftl_last = SIGNIFICANT_MOTION + }; + + enum class InputDeviceSensorAccuracy : int32_t { + NONE = 0, + LOW = 1, + MEDIUM = 2, + HIGH = 3, + + ftl_last = HIGH, + }; + + enum class InputDeviceSensorReportingMode : int32_t { + CONTINUOUS = 0, + ON_CHANGE = 1, + ONE_SHOT = 2, + SPECIAL_TRIGGER = 3, + }; + + enum class InputDeviceLightType : int32_t { + INPUT = 0, + PLAYER_ID = 1, + KEYBOARD_BACKLIGHT = 2, + KEYBOARD_MIC_MUTE = 3, + KEYBOARD_VOLUME_MUTE = 4, + + ftl_last = KEYBOARD_VOLUME_MUTE + }; + + enum class InputDeviceLightCapability : uint32_t { + /** Capability to change brightness of the light */ + BRIGHTNESS = 0x00000001, + /** Capability to change color of the light */ + RGB = 0x00000002, + }; + + struct InputDeviceSensorInfo { + explicit InputDeviceSensorInfo(std::string name, std::string vendor, int32_t version, + InputDeviceSensorType type, + InputDeviceSensorAccuracy accuracy, + float maxRange, float resolution, float power, + int32_t minDelay, + int32_t fifoReservedEventCount, int32_t fifoMaxEventCount, + std::string stringType, int32_t maxDelay, int32_t flags, + int32_t id) + : name(name), + vendor(vendor), + version(version), + type(type), + accuracy(accuracy), + maxRange(maxRange), + resolution(resolution), + power(power), + minDelay(minDelay), + fifoReservedEventCount(fifoReservedEventCount), + fifoMaxEventCount(fifoMaxEventCount), + stringType(stringType), + maxDelay(maxDelay), + flags(flags), + id(id) {} + + // Name string of the sensor. + std::string name; + // Vendor string of this sensor. + std::string vendor; + // Version of the sensor's module. + int32_t version; + // Generic type of this sensor. + InputDeviceSensorType type; + // The current accuracy of sensor event. + InputDeviceSensorAccuracy accuracy; + // Maximum range of the sensor in the sensor's unit. + float maxRange; + // Resolution of the sensor in the sensor's unit. + float resolution; + // The power in mA used by this sensor while in use. + float power; + // The minimum delay allowed between two events in microsecond or zero if this sensor only + // returns a value when the data it's measuring changes. + int32_t minDelay; + // Number of events reserved for this sensor in the batch mode FIFO. + int32_t fifoReservedEventCount; + // Maximum number of events of this sensor that could be batched. + int32_t fifoMaxEventCount; + // The type of this sensor as a string. + std::string stringType; + // The delay between two sensor events corresponding to the lowest frequency that this sensor + // supports. + int32_t maxDelay; + // Sensor flags + int32_t flags; + // Sensor id, same as the input device ID it belongs to. + int32_t id; + }; + + struct BrightnessLevel : ftl::DefaultConstructible, + ftl::Equatable, + ftl::Orderable, + ftl::Addable { + using DefaultConstructible::DefaultConstructible; + }; + + struct InputDeviceLightInfo { + explicit InputDeviceLightInfo(std::string name, int32_t id, InputDeviceLightType type, + ftl::Flags capabilityFlags, + int32_t ordinal, + std::set preferredBrightnessLevels) + : name(name), + id(id), + type(type), + capabilityFlags(capabilityFlags), + ordinal(ordinal), + preferredBrightnessLevels(std::move(preferredBrightnessLevels)) {} + + // Name string of the light. + std::string name; + // Light id + int32_t id; + // Type of the light. + InputDeviceLightType type; + // Light capabilities. + ftl::Flags capabilityFlags; + // Ordinal of the light + int32_t ordinal; + // Custom brightness levels for the light + std::set preferredBrightnessLevels; + }; + + struct InputDeviceBatteryInfo { + explicit InputDeviceBatteryInfo(std::string name, int32_t id) : name(name), id(id) {} + + // Name string of the battery. + std::string name; + // Battery id + int32_t id; + }; + + struct KeyboardLayoutInfo { + explicit KeyboardLayoutInfo(std::string languageTag, std::string layoutType) + : languageTag(languageTag), layoutType(layoutType) {} + + // A BCP 47 conformant language tag such as "en-US". + std::string languageTag; + // The layout type such as QWERTY or AZERTY. + std::string layoutType; + + inline bool operator==(const KeyboardLayoutInfo &other) const { + return languageTag == other.languageTag && layoutType == other.layoutType; + } + + inline bool operator!=(const KeyboardLayoutInfo &other) const { return !(*this == other); } + }; + +// The version of the Universal Stylus Initiative (USI) protocol supported by the input device. + struct InputDeviceUsiVersion { + int32_t majorVersion = -1; + int32_t minorVersion = -1; + }; + +/* + * Describes the characteristics and capabilities of an input device. + */ + class InputDeviceInfo { + public: + InputDeviceInfo(); + + InputDeviceInfo(const InputDeviceInfo &other); + + InputDeviceInfo &operator=(const InputDeviceInfo &other); + + ~InputDeviceInfo(); + + struct MotionRange { + int32_t axis; + uint32_t source; + float min; + float max; + float flat; + float fuzz; + float resolution; + }; + + void initialize(int32_t id, int32_t generation, int32_t controllerNumber, + const InputDeviceIdentifier &identifier, const std::string &alias, + bool isExternal, bool hasMic, ui::LogicalDisplayId associatedDisplayId, + InputDeviceViewBehavior viewBehavior = {{}}, bool enabled = true); + + inline int32_t getId() const { return mId; } + + inline int32_t getControllerNumber() const { return mControllerNumber; } + + inline int32_t getGeneration() const { return mGeneration; } + + inline const InputDeviceIdentifier &getIdentifier() const { return mIdentifier; } + + inline const std::string &getAlias() const { return mAlias; } + + inline const std::string &getDisplayName() const { + return mAlias.empty() ? mIdentifier.name : mAlias; + } + + inline bool isExternal() const { return mIsExternal; } + + inline bool hasMic() const { return mHasMic; } + + inline uint32_t getSources() const { return mSources; } + + const MotionRange *getMotionRange(int32_t axis, uint32_t source) const; + + void addSource(uint32_t source); + + void addMotionRange(int32_t axis, uint32_t source, + float min, float max, float flat, float fuzz, float resolution); + + void addMotionRange(const MotionRange &range); + + void addSensorInfo(const InputDeviceSensorInfo &info); + + void addBatteryInfo(const InputDeviceBatteryInfo &info); + + void addLightInfo(const InputDeviceLightInfo &info); + + void setKeyboardType(int32_t keyboardType); + + inline int32_t getKeyboardType() const { return mKeyboardType; } + + void setKeyboardLayoutInfo(KeyboardLayoutInfo keyboardLayoutInfo); + + inline const std::optional &getKeyboardLayoutInfo() const { + return mKeyboardLayoutInfo; + } + + inline const InputDeviceViewBehavior &getViewBehavior() const { return mViewBehavior; } + + inline void setVibrator(bool hasVibrator) { mHasVibrator = hasVibrator; } + + inline bool hasVibrator() const { return mHasVibrator; } + + inline void setHasBattery(bool hasBattery) { mHasBattery = hasBattery; } + + inline bool hasBattery() const { return mHasBattery; } + + inline void setButtonUnderPad(bool hasButton) { mHasButtonUnderPad = hasButton; } + + inline bool hasButtonUnderPad() const { return mHasButtonUnderPad; } + + inline void setHasSensor(bool hasSensor) { mHasSensor = hasSensor; } + + inline bool hasSensor() const { return mHasSensor; } + + inline const std::vector &getMotionRanges() const { + return mMotionRanges; + } + + std::vector getSensors(); + + std::vector getLights(); + + inline void setUsiVersion(std::optional usiVersion) { + mUsiVersion = std::move(usiVersion); + } + + inline std::optional getUsiVersion() const { return mUsiVersion; } + + inline ui::LogicalDisplayId getAssociatedDisplayId() const { return mAssociatedDisplayId; } + + inline void setEnabled(bool enabled) { mEnabled = enabled; } + + inline bool isEnabled() const { return mEnabled; } + + private: + int32_t mId; + int32_t mGeneration; + int32_t mControllerNumber; + InputDeviceIdentifier mIdentifier; + std::string mAlias; + bool mIsExternal; + bool mHasMic; + std::optional mKeyboardLayoutInfo; + uint32_t mSources; + int32_t mKeyboardType; + std::optional mUsiVersion; + ui::LogicalDisplayId mAssociatedDisplayId{ui::LogicalDisplayId::INVALID}; + bool mEnabled; + + bool mHasVibrator; + bool mHasBattery; + bool mHasButtonUnderPad; + bool mHasSensor; + + std::vector mMotionRanges; + std::unordered_map mSensors; + /* Map from light ID to light info */ + std::unordered_map mLights; + /* Map from battery ID to battery info */ + std::unordered_map mBatteries; + /** The View related behaviors for the device. */ + InputDeviceViewBehavior mViewBehavior; + }; + +/* Types of input device configuration files. */ + enum class InputDeviceConfigurationFileType : int32_t { + CONFIGURATION = 0, /* .idc file */ + KEY_LAYOUT = 1, /* .kl file */ + KEY_CHARACTER_MAP = 2, /* .kcm file */ + ftl_last = KEY_CHARACTER_MAP, + }; + +/* + * Gets the path of an input device configuration file, if one is available. + * Considers both system provided and user installed configuration files. + * The optional suffix is appended to the end of the file name (before the + * extension). + * + * The device identifier is used to construct several default configuration file + * names to try based on the device name, vendor, product, and version. + * + * Returns an empty string if not found. + */ + extern std::string getInputDeviceConfigurationFilePathByDeviceIdentifier( + const InputDeviceIdentifier &deviceIdentifier, InputDeviceConfigurationFileType type, + const char *suffix = ""); + +/* + * Gets the path of an input device configuration file, if one is available. + * Considers both system provided and user installed configuration files. + * + * The name is case-sensitive and is used to construct the filename to resolve. + * All characters except 'a'-'z', 'A'-'Z', '0'-'9', '-', and '_' are replaced by underscores. + * + * Returns an empty string if not found. + */ + extern std::string getInputDeviceConfigurationFilePathByName( + const std::string &name, InputDeviceConfigurationFileType type); + + enum ReservedInputDeviceId : int32_t { + // Device id representing an invalid device + INVALID_INPUT_DEVICE_ID = -2, + // Device id of a special "virtual" keyboard that is always present. + VIRTUAL_KEYBOARD_ID = -1, + // Device id of the "built-in" keyboard if there is one. + BUILT_IN_KEYBOARD_ID = 0, + // First device id available for dynamic devices + END_RESERVED_ID = 1, + }; + +} // namespace android diff --git a/sysbridge/src/main/cpp/android/input/InputEventLabels.cpp b/sysbridge/src/main/cpp/android/input/InputEventLabels.cpp new file mode 100644 index 0000000000..d8d3c5b0ca --- /dev/null +++ b/sysbridge/src/main/cpp/android/input/InputEventLabels.cpp @@ -0,0 +1,1422 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "InputEventLabels.h" + +#include +#include +#include +#include "Input.h" + +#define DEFINE_KEYCODE(key) { #key, AKEYCODE_##key } +#define DEFINE_AXIS(axis) { #axis, AMOTION_EVENT_AXIS_##axis } +#define DEFINE_FLAG(flag) { #flag, POLICY_FLAG_##flag } + +namespace android { + +// clang-format off + +// NOTE: If you add a new keycode here you must also add it to several other files. +// Refer to frameworks/base/core/java/android/view/KeyEvent.java for the full list. +#define KEYCODES_SEQUENCE \ + DEFINE_KEYCODE(UNKNOWN), \ + DEFINE_KEYCODE(SOFT_LEFT), \ + DEFINE_KEYCODE(SOFT_RIGHT), \ + DEFINE_KEYCODE(HOME), \ + DEFINE_KEYCODE(BACK), \ + DEFINE_KEYCODE(CALL), \ + DEFINE_KEYCODE(ENDCALL), \ + DEFINE_KEYCODE(0), \ + DEFINE_KEYCODE(1), \ + DEFINE_KEYCODE(2), \ + DEFINE_KEYCODE(3), \ + DEFINE_KEYCODE(4), \ + DEFINE_KEYCODE(5), \ + DEFINE_KEYCODE(6), \ + DEFINE_KEYCODE(7), \ + DEFINE_KEYCODE(8), \ + DEFINE_KEYCODE(9), \ + DEFINE_KEYCODE(STAR), \ + DEFINE_KEYCODE(POUND), \ + DEFINE_KEYCODE(DPAD_UP), \ + DEFINE_KEYCODE(DPAD_DOWN), \ + DEFINE_KEYCODE(DPAD_LEFT), \ + DEFINE_KEYCODE(DPAD_RIGHT), \ + DEFINE_KEYCODE(DPAD_CENTER), \ + DEFINE_KEYCODE(VOLUME_UP), \ + DEFINE_KEYCODE(VOLUME_DOWN), \ + DEFINE_KEYCODE(POWER), \ + DEFINE_KEYCODE(CAMERA), \ + DEFINE_KEYCODE(CLEAR), \ + DEFINE_KEYCODE(A), \ + DEFINE_KEYCODE(B), \ + DEFINE_KEYCODE(C), \ + DEFINE_KEYCODE(D), \ + DEFINE_KEYCODE(E), \ + DEFINE_KEYCODE(F), \ + DEFINE_KEYCODE(G), \ + DEFINE_KEYCODE(H), \ + DEFINE_KEYCODE(I), \ + DEFINE_KEYCODE(J), \ + DEFINE_KEYCODE(K), \ + DEFINE_KEYCODE(L), \ + DEFINE_KEYCODE(M), \ + DEFINE_KEYCODE(N), \ + DEFINE_KEYCODE(O), \ + DEFINE_KEYCODE(P), \ + DEFINE_KEYCODE(Q), \ + DEFINE_KEYCODE(R), \ + DEFINE_KEYCODE(S), \ + DEFINE_KEYCODE(T), \ + DEFINE_KEYCODE(U), \ + DEFINE_KEYCODE(V), \ + DEFINE_KEYCODE(W), \ + DEFINE_KEYCODE(X), \ + DEFINE_KEYCODE(Y), \ + DEFINE_KEYCODE(Z), \ + DEFINE_KEYCODE(COMMA), \ + DEFINE_KEYCODE(PERIOD), \ + DEFINE_KEYCODE(ALT_LEFT), \ + DEFINE_KEYCODE(ALT_RIGHT), \ + DEFINE_KEYCODE(SHIFT_LEFT), \ + DEFINE_KEYCODE(SHIFT_RIGHT), \ + DEFINE_KEYCODE(TAB), \ + DEFINE_KEYCODE(SPACE), \ + DEFINE_KEYCODE(SYM), \ + DEFINE_KEYCODE(EXPLORER), \ + DEFINE_KEYCODE(ENVELOPE), \ + DEFINE_KEYCODE(ENTER), \ + DEFINE_KEYCODE(DEL), \ + DEFINE_KEYCODE(GRAVE), \ + DEFINE_KEYCODE(MINUS), \ + DEFINE_KEYCODE(EQUALS), \ + DEFINE_KEYCODE(LEFT_BRACKET), \ + DEFINE_KEYCODE(RIGHT_BRACKET), \ + DEFINE_KEYCODE(BACKSLASH), \ + DEFINE_KEYCODE(SEMICOLON), \ + DEFINE_KEYCODE(APOSTROPHE), \ + DEFINE_KEYCODE(SLASH), \ + DEFINE_KEYCODE(AT), \ + DEFINE_KEYCODE(NUM), \ + DEFINE_KEYCODE(HEADSETHOOK), \ + DEFINE_KEYCODE(FOCUS), \ + DEFINE_KEYCODE(PLUS), \ + DEFINE_KEYCODE(MENU), \ + DEFINE_KEYCODE(NOTIFICATION), \ + DEFINE_KEYCODE(SEARCH), \ + DEFINE_KEYCODE(MEDIA_PLAY_PAUSE), \ + DEFINE_KEYCODE(MEDIA_STOP), \ + DEFINE_KEYCODE(MEDIA_NEXT), \ + DEFINE_KEYCODE(MEDIA_PREVIOUS), \ + DEFINE_KEYCODE(MEDIA_REWIND), \ + DEFINE_KEYCODE(MEDIA_FAST_FORWARD), \ + DEFINE_KEYCODE(MUTE), \ + DEFINE_KEYCODE(PAGE_UP), \ + DEFINE_KEYCODE(PAGE_DOWN), \ + DEFINE_KEYCODE(PICTSYMBOLS), \ + DEFINE_KEYCODE(SWITCH_CHARSET), \ + DEFINE_KEYCODE(BUTTON_A), \ + DEFINE_KEYCODE(BUTTON_B), \ + DEFINE_KEYCODE(BUTTON_C), \ + DEFINE_KEYCODE(BUTTON_X), \ + DEFINE_KEYCODE(BUTTON_Y), \ + DEFINE_KEYCODE(BUTTON_Z), \ + DEFINE_KEYCODE(BUTTON_L1), \ + DEFINE_KEYCODE(BUTTON_R1), \ + DEFINE_KEYCODE(BUTTON_L2), \ + DEFINE_KEYCODE(BUTTON_R2), \ + DEFINE_KEYCODE(BUTTON_THUMBL), \ + DEFINE_KEYCODE(BUTTON_THUMBR), \ + DEFINE_KEYCODE(BUTTON_START), \ + DEFINE_KEYCODE(BUTTON_SELECT), \ + DEFINE_KEYCODE(BUTTON_MODE), \ + DEFINE_KEYCODE(ESCAPE), \ + DEFINE_KEYCODE(FORWARD_DEL), \ + DEFINE_KEYCODE(CTRL_LEFT), \ + DEFINE_KEYCODE(CTRL_RIGHT), \ + DEFINE_KEYCODE(CAPS_LOCK), \ + DEFINE_KEYCODE(SCROLL_LOCK), \ + DEFINE_KEYCODE(META_LEFT), \ + DEFINE_KEYCODE(META_RIGHT), \ + DEFINE_KEYCODE(FUNCTION), \ + DEFINE_KEYCODE(SYSRQ), \ + DEFINE_KEYCODE(BREAK), \ + DEFINE_KEYCODE(MOVE_HOME), \ + DEFINE_KEYCODE(MOVE_END), \ + DEFINE_KEYCODE(INSERT), \ + DEFINE_KEYCODE(FORWARD), \ + DEFINE_KEYCODE(MEDIA_PLAY), \ + DEFINE_KEYCODE(MEDIA_PAUSE), \ + DEFINE_KEYCODE(MEDIA_CLOSE), \ + DEFINE_KEYCODE(MEDIA_EJECT), \ + DEFINE_KEYCODE(MEDIA_RECORD), \ + DEFINE_KEYCODE(F1), \ + DEFINE_KEYCODE(F2), \ + DEFINE_KEYCODE(F3), \ + DEFINE_KEYCODE(F4), \ + DEFINE_KEYCODE(F5), \ + DEFINE_KEYCODE(F6), \ + DEFINE_KEYCODE(F7), \ + DEFINE_KEYCODE(F8), \ + DEFINE_KEYCODE(F9), \ + DEFINE_KEYCODE(F10), \ + DEFINE_KEYCODE(F11), \ + DEFINE_KEYCODE(F12), \ + DEFINE_KEYCODE(NUM_LOCK), \ + DEFINE_KEYCODE(NUMPAD_0), \ + DEFINE_KEYCODE(NUMPAD_1), \ + DEFINE_KEYCODE(NUMPAD_2), \ + DEFINE_KEYCODE(NUMPAD_3), \ + DEFINE_KEYCODE(NUMPAD_4), \ + DEFINE_KEYCODE(NUMPAD_5), \ + DEFINE_KEYCODE(NUMPAD_6), \ + DEFINE_KEYCODE(NUMPAD_7), \ + DEFINE_KEYCODE(NUMPAD_8), \ + DEFINE_KEYCODE(NUMPAD_9), \ + DEFINE_KEYCODE(NUMPAD_DIVIDE), \ + DEFINE_KEYCODE(NUMPAD_MULTIPLY), \ + DEFINE_KEYCODE(NUMPAD_SUBTRACT), \ + DEFINE_KEYCODE(NUMPAD_ADD), \ + DEFINE_KEYCODE(NUMPAD_DOT), \ + DEFINE_KEYCODE(NUMPAD_COMMA), \ + DEFINE_KEYCODE(NUMPAD_ENTER), \ + DEFINE_KEYCODE(NUMPAD_EQUALS), \ + DEFINE_KEYCODE(NUMPAD_LEFT_PAREN), \ + DEFINE_KEYCODE(NUMPAD_RIGHT_PAREN), \ + DEFINE_KEYCODE(VOLUME_MUTE), \ + DEFINE_KEYCODE(INFO), \ + DEFINE_KEYCODE(CHANNEL_UP), \ + DEFINE_KEYCODE(CHANNEL_DOWN), \ + DEFINE_KEYCODE(ZOOM_IN), \ + DEFINE_KEYCODE(ZOOM_OUT), \ + DEFINE_KEYCODE(TV), \ + DEFINE_KEYCODE(WINDOW), \ + DEFINE_KEYCODE(GUIDE), \ + DEFINE_KEYCODE(DVR), \ + DEFINE_KEYCODE(BOOKMARK), \ + DEFINE_KEYCODE(CAPTIONS), \ + DEFINE_KEYCODE(SETTINGS), \ + DEFINE_KEYCODE(TV_POWER), \ + DEFINE_KEYCODE(TV_INPUT), \ + DEFINE_KEYCODE(STB_POWER), \ + DEFINE_KEYCODE(STB_INPUT), \ + DEFINE_KEYCODE(AVR_POWER), \ + DEFINE_KEYCODE(AVR_INPUT), \ + DEFINE_KEYCODE(PROG_RED), \ + DEFINE_KEYCODE(PROG_GREEN), \ + DEFINE_KEYCODE(PROG_YELLOW), \ + DEFINE_KEYCODE(PROG_BLUE), \ + DEFINE_KEYCODE(APP_SWITCH), \ + DEFINE_KEYCODE(BUTTON_1), \ + DEFINE_KEYCODE(BUTTON_2), \ + DEFINE_KEYCODE(BUTTON_3), \ + DEFINE_KEYCODE(BUTTON_4), \ + DEFINE_KEYCODE(BUTTON_5), \ + DEFINE_KEYCODE(BUTTON_6), \ + DEFINE_KEYCODE(BUTTON_7), \ + DEFINE_KEYCODE(BUTTON_8), \ + DEFINE_KEYCODE(BUTTON_9), \ + DEFINE_KEYCODE(BUTTON_10), \ + DEFINE_KEYCODE(BUTTON_11), \ + DEFINE_KEYCODE(BUTTON_12), \ + DEFINE_KEYCODE(BUTTON_13), \ + DEFINE_KEYCODE(BUTTON_14), \ + DEFINE_KEYCODE(BUTTON_15), \ + DEFINE_KEYCODE(BUTTON_16), \ + DEFINE_KEYCODE(LANGUAGE_SWITCH), \ + DEFINE_KEYCODE(MANNER_MODE), \ + DEFINE_KEYCODE(3D_MODE), \ + DEFINE_KEYCODE(CONTACTS), \ + DEFINE_KEYCODE(CALENDAR), \ + DEFINE_KEYCODE(MUSIC), \ + DEFINE_KEYCODE(CALCULATOR), \ + DEFINE_KEYCODE(ZENKAKU_HANKAKU), \ + DEFINE_KEYCODE(EISU), \ + DEFINE_KEYCODE(MUHENKAN), \ + DEFINE_KEYCODE(HENKAN), \ + DEFINE_KEYCODE(KATAKANA_HIRAGANA), \ + DEFINE_KEYCODE(YEN), \ + DEFINE_KEYCODE(RO), \ + DEFINE_KEYCODE(KANA), \ + DEFINE_KEYCODE(ASSIST), \ + DEFINE_KEYCODE(BRIGHTNESS_DOWN), \ + DEFINE_KEYCODE(BRIGHTNESS_UP), \ + DEFINE_KEYCODE(MEDIA_AUDIO_TRACK), \ + DEFINE_KEYCODE(SLEEP), \ + DEFINE_KEYCODE(WAKEUP), \ + DEFINE_KEYCODE(PAIRING), \ + DEFINE_KEYCODE(MEDIA_TOP_MENU), \ + DEFINE_KEYCODE(11), \ + DEFINE_KEYCODE(12), \ + DEFINE_KEYCODE(LAST_CHANNEL), \ + DEFINE_KEYCODE(TV_DATA_SERVICE), \ + DEFINE_KEYCODE(VOICE_ASSIST), \ + DEFINE_KEYCODE(TV_RADIO_SERVICE), \ + DEFINE_KEYCODE(TV_TELETEXT), \ + DEFINE_KEYCODE(TV_NUMBER_ENTRY), \ + DEFINE_KEYCODE(TV_TERRESTRIAL_ANALOG), \ + DEFINE_KEYCODE(TV_TERRESTRIAL_DIGITAL), \ + DEFINE_KEYCODE(TV_SATELLITE), \ + DEFINE_KEYCODE(TV_SATELLITE_BS), \ + DEFINE_KEYCODE(TV_SATELLITE_CS), \ + DEFINE_KEYCODE(TV_SATELLITE_SERVICE), \ + DEFINE_KEYCODE(TV_NETWORK), \ + DEFINE_KEYCODE(TV_ANTENNA_CABLE), \ + DEFINE_KEYCODE(TV_INPUT_HDMI_1), \ + DEFINE_KEYCODE(TV_INPUT_HDMI_2), \ + DEFINE_KEYCODE(TV_INPUT_HDMI_3), \ + DEFINE_KEYCODE(TV_INPUT_HDMI_4), \ + DEFINE_KEYCODE(TV_INPUT_COMPOSITE_1), \ + DEFINE_KEYCODE(TV_INPUT_COMPOSITE_2), \ + DEFINE_KEYCODE(TV_INPUT_COMPONENT_1), \ + DEFINE_KEYCODE(TV_INPUT_COMPONENT_2), \ + DEFINE_KEYCODE(TV_INPUT_VGA_1), \ + DEFINE_KEYCODE(TV_AUDIO_DESCRIPTION), \ + DEFINE_KEYCODE(TV_AUDIO_DESCRIPTION_MIX_UP), \ + DEFINE_KEYCODE(TV_AUDIO_DESCRIPTION_MIX_DOWN), \ + DEFINE_KEYCODE(TV_ZOOM_MODE), \ + DEFINE_KEYCODE(TV_CONTENTS_MENU), \ + DEFINE_KEYCODE(TV_MEDIA_CONTEXT_MENU), \ + DEFINE_KEYCODE(TV_TIMER_PROGRAMMING), \ + DEFINE_KEYCODE(HELP), \ + DEFINE_KEYCODE(NAVIGATE_PREVIOUS), \ + DEFINE_KEYCODE(NAVIGATE_NEXT), \ + DEFINE_KEYCODE(NAVIGATE_IN), \ + DEFINE_KEYCODE(NAVIGATE_OUT), \ + DEFINE_KEYCODE(STEM_PRIMARY), \ + DEFINE_KEYCODE(STEM_1), \ + DEFINE_KEYCODE(STEM_2), \ + DEFINE_KEYCODE(STEM_3), \ + DEFINE_KEYCODE(DPAD_UP_LEFT), \ + DEFINE_KEYCODE(DPAD_DOWN_LEFT), \ + DEFINE_KEYCODE(DPAD_UP_RIGHT), \ + DEFINE_KEYCODE(DPAD_DOWN_RIGHT), \ + DEFINE_KEYCODE(MEDIA_SKIP_FORWARD), \ + DEFINE_KEYCODE(MEDIA_SKIP_BACKWARD), \ + DEFINE_KEYCODE(MEDIA_STEP_FORWARD), \ + DEFINE_KEYCODE(MEDIA_STEP_BACKWARD), \ + DEFINE_KEYCODE(SOFT_SLEEP), \ + DEFINE_KEYCODE(CUT), \ + DEFINE_KEYCODE(COPY), \ + DEFINE_KEYCODE(PASTE), \ + DEFINE_KEYCODE(SYSTEM_NAVIGATION_UP), \ + DEFINE_KEYCODE(SYSTEM_NAVIGATION_DOWN), \ + DEFINE_KEYCODE(SYSTEM_NAVIGATION_LEFT), \ + DEFINE_KEYCODE(SYSTEM_NAVIGATION_RIGHT), \ + DEFINE_KEYCODE(ALL_APPS), \ + DEFINE_KEYCODE(REFRESH), \ + DEFINE_KEYCODE(THUMBS_UP), \ + DEFINE_KEYCODE(THUMBS_DOWN), \ + DEFINE_KEYCODE(PROFILE_SWITCH), \ + DEFINE_KEYCODE(VIDEO_APP_1), \ + DEFINE_KEYCODE(VIDEO_APP_2), \ + DEFINE_KEYCODE(VIDEO_APP_3), \ + DEFINE_KEYCODE(VIDEO_APP_4), \ + DEFINE_KEYCODE(VIDEO_APP_5), \ + DEFINE_KEYCODE(VIDEO_APP_6), \ + DEFINE_KEYCODE(VIDEO_APP_7), \ + DEFINE_KEYCODE(VIDEO_APP_8), \ + DEFINE_KEYCODE(FEATURED_APP_1), \ + DEFINE_KEYCODE(FEATURED_APP_2), \ + DEFINE_KEYCODE(FEATURED_APP_3), \ + DEFINE_KEYCODE(FEATURED_APP_4), \ + DEFINE_KEYCODE(DEMO_APP_1), \ + DEFINE_KEYCODE(DEMO_APP_2), \ + DEFINE_KEYCODE(DEMO_APP_3), \ + DEFINE_KEYCODE(DEMO_APP_4), \ + DEFINE_KEYCODE(KEYBOARD_BACKLIGHT_DOWN), \ + DEFINE_KEYCODE(KEYBOARD_BACKLIGHT_UP), \ + DEFINE_KEYCODE(KEYBOARD_BACKLIGHT_TOGGLE), \ + DEFINE_KEYCODE(STYLUS_BUTTON_PRIMARY), \ + DEFINE_KEYCODE(STYLUS_BUTTON_SECONDARY), \ + DEFINE_KEYCODE(STYLUS_BUTTON_TERTIARY), \ + DEFINE_KEYCODE(STYLUS_BUTTON_TAIL), \ + DEFINE_KEYCODE(RECENT_APPS), \ + DEFINE_KEYCODE(MACRO_1), \ + DEFINE_KEYCODE(MACRO_2), \ + DEFINE_KEYCODE(MACRO_3), \ + DEFINE_KEYCODE(MACRO_4) +// DEFINE_KEYCODE(EMOJI_PICKER), \ +// DEFINE_KEYCODE(SCREENSHOT), \ +// DEFINE_KEYCODE(DICTATE), \ +// DEFINE_KEYCODE(NEW), \ +// DEFINE_KEYCODE(CLOSE), \ +// DEFINE_KEYCODE(DO_NOT_DISTURB), \ +// DEFINE_KEYCODE(PRINT), \ +// DEFINE_KEYCODE(LOCK), \ +// DEFINE_KEYCODE(FULLSCREEN), \ +// DEFINE_KEYCODE(F13), \ +// DEFINE_KEYCODE(F14), \ +// DEFINE_KEYCODE(F15), \ +// DEFINE_KEYCODE(F16), \ +// DEFINE_KEYCODE(F17), \ +// DEFINE_KEYCODE(F18), \ +// DEFINE_KEYCODE(F19),\ +// DEFINE_KEYCODE(F20), \ +// DEFINE_KEYCODE(F21), \ +// DEFINE_KEYCODE(F22), \ +// DEFINE_KEYCODE(F23), \ +// DEFINE_KEYCODE(F24) + +// NOTE: If you add a new axis here you must also add it to several other files. +// Refer to frameworks/base/core/java/android/view/MotionEvent.java for the full list. +#define AXES_SEQUENCE \ + DEFINE_AXIS(X), \ + DEFINE_AXIS(Y), \ + DEFINE_AXIS(PRESSURE), \ + DEFINE_AXIS(SIZE), \ + DEFINE_AXIS(TOUCH_MAJOR), \ + DEFINE_AXIS(TOUCH_MINOR), \ + DEFINE_AXIS(TOOL_MAJOR), \ + DEFINE_AXIS(TOOL_MINOR), \ + DEFINE_AXIS(ORIENTATION), \ + DEFINE_AXIS(VSCROLL), \ + DEFINE_AXIS(HSCROLL), \ + DEFINE_AXIS(Z), \ + DEFINE_AXIS(RX), \ + DEFINE_AXIS(RY), \ + DEFINE_AXIS(RZ), \ + DEFINE_AXIS(HAT_X), \ + DEFINE_AXIS(HAT_Y), \ + DEFINE_AXIS(LTRIGGER), \ + DEFINE_AXIS(RTRIGGER), \ + DEFINE_AXIS(THROTTLE), \ + DEFINE_AXIS(RUDDER), \ + DEFINE_AXIS(WHEEL), \ + DEFINE_AXIS(GAS), \ + DEFINE_AXIS(BRAKE), \ + DEFINE_AXIS(DISTANCE), \ + DEFINE_AXIS(TILT), \ + DEFINE_AXIS(SCROLL), \ + DEFINE_AXIS(RELATIVE_X), \ + DEFINE_AXIS(RELATIVE_Y), \ + {"RESERVED_29", 29}, \ + {"RESERVED_30", 30}, \ + {"RESERVED_31", 31}, \ + DEFINE_AXIS(GENERIC_1), \ + DEFINE_AXIS(GENERIC_2), \ + DEFINE_AXIS(GENERIC_3), \ + DEFINE_AXIS(GENERIC_4), \ + DEFINE_AXIS(GENERIC_5), \ + DEFINE_AXIS(GENERIC_6), \ + DEFINE_AXIS(GENERIC_7), \ + DEFINE_AXIS(GENERIC_8), \ + DEFINE_AXIS(GENERIC_9), \ + DEFINE_AXIS(GENERIC_10), \ + DEFINE_AXIS(GENERIC_11), \ + DEFINE_AXIS(GENERIC_12), \ + DEFINE_AXIS(GENERIC_13), \ + DEFINE_AXIS(GENERIC_14), \ + DEFINE_AXIS(GENERIC_15), \ + DEFINE_AXIS(GENERIC_16), \ + DEFINE_AXIS(GESTURE_X_OFFSET), \ + DEFINE_AXIS(GESTURE_Y_OFFSET), \ + DEFINE_AXIS(GESTURE_SCROLL_X_DISTANCE), \ + DEFINE_AXIS(GESTURE_SCROLL_Y_DISTANCE), \ + DEFINE_AXIS(GESTURE_PINCH_SCALE_FACTOR), \ + DEFINE_AXIS(GESTURE_SWIPE_FINGER_COUNT) + +#define FLAGS_SEQUENCE \ + DEFINE_FLAG(VIRTUAL), \ + DEFINE_FLAG(FUNCTION), \ + DEFINE_FLAG(GESTURE), \ + DEFINE_FLAG(WAKE), \ + DEFINE_FLAG(FALLBACK_USAGE_MAPPING) + +// clang-format on + +// --- InputEventLookup --- + + InputEventLookup::InputEventLookup() + : KEYCODES({KEYCODES_SEQUENCE}), + KEY_NAMES({KEYCODES_SEQUENCE}), + AXES({AXES_SEQUENCE}), + AXES_NAMES({AXES_SEQUENCE}), + FLAGS({FLAGS_SEQUENCE}) {} + + std::optional InputEventLookup::lookupValueByLabel( + const std::unordered_map &map, const char *literal) { + std::string str(literal); + auto it = map.find(str); + return it != map.end() ? std::make_optional(it->second) : std::nullopt; + } + + const char *InputEventLookup::lookupLabelByValue(const std::vector &vec, + int value) { + if (static_cast(value) < vec.size()) { + return vec[value].literal; + } + return nullptr; + } + + std::optional InputEventLookup::getKeyCodeByLabel(const char *label) { + const auto &self = get(); + return self.lookupValueByLabel(self.KEYCODES, label); + } + + const char *InputEventLookup::getLabelByKeyCode(int32_t keyCode) { + const auto &self = get(); + if (keyCode >= 0 && static_cast(keyCode) < self.KEYCODES.size()) { + return get().lookupLabelByValue(self.KEY_NAMES, keyCode); + } + return nullptr; + } + + std::optional InputEventLookup::getKeyFlagByLabel(const char *label) { + const auto &self = get(); + return lookupValueByLabel(self.FLAGS, label); + } + + std::optional InputEventLookup::getAxisByLabel(const char *label) { + const auto &self = get(); + return lookupValueByLabel(self.AXES, label); + } + + const char *InputEventLookup::getAxisLabel(int32_t axisId) { + const auto &self = get(); + return lookupLabelByValue(self.AXES_NAMES, axisId); + } + + std::optional InputEventLookup::getLedByLabel(const char *label) { + const auto &self = get(); + return lookupValueByLabel(self.LEDS, label); + } + + namespace { + + struct label { + const char *name; + int value; + }; + +#define LABEL(constant) \ + { #constant, constant } +#define LABEL_END \ + { nullptr, -1 } + +// Inserted from the file: out/soong/.intermediates/system/core/toolbox/toolbox_input_labels/gen/input.h-labels.h + static struct label ev_key_value_labels[] = { + {"UP", 0}, + {"DOWN", 1}, + {"REPEAT", 2}, + LABEL_END, + }; + + + static struct label input_prop_labels[] = { + LABEL(INPUT_PROP_POINTER), + LABEL(INPUT_PROP_DIRECT), + LABEL(INPUT_PROP_BUTTONPAD), + LABEL(INPUT_PROP_SEMI_MT), + LABEL(INPUT_PROP_TOPBUTTONPAD), + LABEL(INPUT_PROP_POINTING_STICK), + LABEL(INPUT_PROP_ACCELEROMETER), + LABEL(INPUT_PROP_MAX), + LABEL_END, + }; + static struct label ev_labels[] = { + LABEL(EV_VERSION), + LABEL(EV_SYN), + LABEL(EV_KEY), + LABEL(EV_REL), + LABEL(EV_ABS), + LABEL(EV_MSC), + LABEL(EV_SW), + LABEL(EV_LED), + LABEL(EV_SND), + LABEL(EV_REP), + LABEL(EV_FF), + LABEL(EV_PWR), + LABEL(EV_FF_STATUS), + LABEL(EV_MAX), + LABEL_END, + }; + static struct label syn_labels[] = { + LABEL(SYN_REPORT), + LABEL(SYN_CONFIG), + LABEL(SYN_MT_REPORT), + LABEL(SYN_DROPPED), + LABEL(SYN_MAX), + LABEL_END, + }; + static struct label key_labels[] = { + LABEL(KEY_RESERVED), + LABEL(KEY_ESC), + LABEL(KEY_1), + LABEL(KEY_2), + LABEL(KEY_3), + LABEL(KEY_4), + LABEL(KEY_5), + LABEL(KEY_6), + LABEL(KEY_7), + LABEL(KEY_8), + LABEL(KEY_9), + LABEL(KEY_0), + LABEL(KEY_MINUS), + LABEL(KEY_EQUAL), + LABEL(KEY_BACKSPACE), + LABEL(KEY_TAB), + LABEL(KEY_Q), + LABEL(KEY_W), + LABEL(KEY_E), + LABEL(KEY_R), + LABEL(KEY_T), + LABEL(KEY_Y), + LABEL(KEY_U), + LABEL(KEY_I), + LABEL(KEY_O), + LABEL(KEY_P), + LABEL(KEY_LEFTBRACE), + LABEL(KEY_RIGHTBRACE), + LABEL(KEY_ENTER), + LABEL(KEY_LEFTCTRL), + LABEL(KEY_A), + LABEL(KEY_S), + LABEL(KEY_D), + LABEL(KEY_F), + LABEL(KEY_G), + LABEL(KEY_H), + LABEL(KEY_J), + LABEL(KEY_K), + LABEL(KEY_L), + LABEL(KEY_SEMICOLON), + LABEL(KEY_APOSTROPHE), + LABEL(KEY_GRAVE), + LABEL(KEY_LEFTSHIFT), + LABEL(KEY_BACKSLASH), + LABEL(KEY_Z), + LABEL(KEY_X), + LABEL(KEY_C), + LABEL(KEY_V), + LABEL(KEY_B), + LABEL(KEY_N), + LABEL(KEY_M), + LABEL(KEY_COMMA), + LABEL(KEY_DOT), + LABEL(KEY_SLASH), + LABEL(KEY_RIGHTSHIFT), + LABEL(KEY_KPASTERISK), + LABEL(KEY_LEFTALT), + LABEL(KEY_SPACE), + LABEL(KEY_CAPSLOCK), + LABEL(KEY_F1), + LABEL(KEY_F2), + LABEL(KEY_F3), + LABEL(KEY_F4), + LABEL(KEY_F5), + LABEL(KEY_F6), + LABEL(KEY_F7), + LABEL(KEY_F8), + LABEL(KEY_F9), + LABEL(KEY_F10), + LABEL(KEY_NUMLOCK), + LABEL(KEY_SCROLLLOCK), + LABEL(KEY_KP7), + LABEL(KEY_KP8), + LABEL(KEY_KP9), + LABEL(KEY_KPMINUS), + LABEL(KEY_KP4), + LABEL(KEY_KP5), + LABEL(KEY_KP6), + LABEL(KEY_KPPLUS), + LABEL(KEY_KP1), + LABEL(KEY_KP2), + LABEL(KEY_KP3), + LABEL(KEY_KP0), + LABEL(KEY_KPDOT), + LABEL(KEY_ZENKAKUHANKAKU), + LABEL(KEY_102ND), + LABEL(KEY_F11), + LABEL(KEY_F12), + LABEL(KEY_RO), + LABEL(KEY_KATAKANA), + LABEL(KEY_HIRAGANA), + LABEL(KEY_HENKAN), + LABEL(KEY_KATAKANAHIRAGANA), + LABEL(KEY_MUHENKAN), + LABEL(KEY_KPJPCOMMA), + LABEL(KEY_KPENTER), + LABEL(KEY_RIGHTCTRL), + LABEL(KEY_KPSLASH), + LABEL(KEY_SYSRQ), + LABEL(KEY_RIGHTALT), + LABEL(KEY_LINEFEED), + LABEL(KEY_HOME), + LABEL(KEY_UP), + LABEL(KEY_PAGEUP), + LABEL(KEY_LEFT), + LABEL(KEY_RIGHT), + LABEL(KEY_END), + LABEL(KEY_DOWN), + LABEL(KEY_PAGEDOWN), + LABEL(KEY_INSERT), + LABEL(KEY_DELETE), + LABEL(KEY_MACRO), + LABEL(KEY_MUTE), + LABEL(KEY_VOLUMEDOWN), + LABEL(KEY_VOLUMEUP), + LABEL(KEY_POWER), + LABEL(KEY_KPEQUAL), + LABEL(KEY_KPPLUSMINUS), + LABEL(KEY_PAUSE), + LABEL(KEY_SCALE), + LABEL(KEY_KPCOMMA), + LABEL(KEY_HANGEUL), + LABEL(KEY_HANJA), + LABEL(KEY_YEN), + LABEL(KEY_LEFTMETA), + LABEL(KEY_RIGHTMETA), + LABEL(KEY_COMPOSE), + LABEL(KEY_STOP), + LABEL(KEY_AGAIN), + LABEL(KEY_PROPS), + LABEL(KEY_UNDO), + LABEL(KEY_FRONT), + LABEL(KEY_COPY), + LABEL(KEY_OPEN), + LABEL(KEY_PASTE), + LABEL(KEY_FIND), + LABEL(KEY_CUT), + LABEL(KEY_HELP), + LABEL(KEY_MENU), + LABEL(KEY_CALC), + LABEL(KEY_SETUP), + LABEL(KEY_SLEEP), + LABEL(KEY_WAKEUP), + LABEL(KEY_FILE), + LABEL(KEY_SENDFILE), + LABEL(KEY_DELETEFILE), + LABEL(KEY_XFER), + LABEL(KEY_PROG1), + LABEL(KEY_PROG2), + LABEL(KEY_WWW), + LABEL(KEY_MSDOS), + LABEL(KEY_COFFEE), + LABEL(KEY_ROTATE_DISPLAY), + LABEL(KEY_CYCLEWINDOWS), + LABEL(KEY_MAIL), + LABEL(KEY_BOOKMARKS), + LABEL(KEY_COMPUTER), + LABEL(KEY_BACK), + LABEL(KEY_FORWARD), + LABEL(KEY_CLOSECD), + LABEL(KEY_EJECTCD), + LABEL(KEY_EJECTCLOSECD), + LABEL(KEY_NEXTSONG), + LABEL(KEY_PLAYPAUSE), + LABEL(KEY_PREVIOUSSONG), + LABEL(KEY_STOPCD), + LABEL(KEY_RECORD), + LABEL(KEY_REWIND), + LABEL(KEY_PHONE), + LABEL(KEY_ISO), + LABEL(KEY_CONFIG), + LABEL(KEY_HOMEPAGE), + LABEL(KEY_REFRESH), + LABEL(KEY_EXIT), + LABEL(KEY_MOVE), + LABEL(KEY_EDIT), + LABEL(KEY_SCROLLUP), + LABEL(KEY_SCROLLDOWN), + LABEL(KEY_KPLEFTPAREN), + LABEL(KEY_KPRIGHTPAREN), + LABEL(KEY_NEW), + LABEL(KEY_REDO), + LABEL(KEY_F13), + LABEL(KEY_F14), + LABEL(KEY_F15), + LABEL(KEY_F16), + LABEL(KEY_F17), + LABEL(KEY_F18), + LABEL(KEY_F19), + LABEL(KEY_F20), + LABEL(KEY_F21), + LABEL(KEY_F22), + LABEL(KEY_F23), + LABEL(KEY_F24), + LABEL(KEY_PLAYCD), + LABEL(KEY_PAUSECD), + LABEL(KEY_PROG3), + LABEL(KEY_PROG4), + LABEL(KEY_ALL_APPLICATIONS), + LABEL(KEY_SUSPEND), + LABEL(KEY_CLOSE), + LABEL(KEY_PLAY), + LABEL(KEY_FASTFORWARD), + LABEL(KEY_BASSBOOST), + LABEL(KEY_PRINT), + LABEL(KEY_HP), + LABEL(KEY_CAMERA), + LABEL(KEY_SOUND), + LABEL(KEY_QUESTION), + LABEL(KEY_EMAIL), + LABEL(KEY_CHAT), + LABEL(KEY_SEARCH), + LABEL(KEY_CONNECT), + LABEL(KEY_FINANCE), + LABEL(KEY_SPORT), + LABEL(KEY_SHOP), + LABEL(KEY_ALTERASE), + LABEL(KEY_CANCEL), + LABEL(KEY_BRIGHTNESSDOWN), + LABEL(KEY_BRIGHTNESSUP), + LABEL(KEY_MEDIA), + LABEL(KEY_SWITCHVIDEOMODE), + LABEL(KEY_KBDILLUMTOGGLE), + LABEL(KEY_KBDILLUMDOWN), + LABEL(KEY_KBDILLUMUP), + LABEL(KEY_SEND), + LABEL(KEY_REPLY), + LABEL(KEY_FORWARDMAIL), + LABEL(KEY_SAVE), + LABEL(KEY_DOCUMENTS), + LABEL(KEY_BATTERY), + LABEL(KEY_BLUETOOTH), + LABEL(KEY_WLAN), + LABEL(KEY_UWB), + LABEL(KEY_UNKNOWN), + LABEL(KEY_VIDEO_NEXT), + LABEL(KEY_VIDEO_PREV), + LABEL(KEY_BRIGHTNESS_CYCLE), + LABEL(KEY_BRIGHTNESS_AUTO), + LABEL(KEY_DISPLAY_OFF), + LABEL(KEY_WWAN), + LABEL(KEY_RFKILL), + LABEL(KEY_MICMUTE), + LABEL(BTN_MISC), + LABEL(BTN_0), + LABEL(BTN_1), + LABEL(BTN_2), + LABEL(BTN_3), + LABEL(BTN_4), + LABEL(BTN_5), + LABEL(BTN_6), + LABEL(BTN_7), + LABEL(BTN_8), + LABEL(BTN_9), + LABEL(BTN_MOUSE), + LABEL(BTN_LEFT), + LABEL(BTN_RIGHT), + LABEL(BTN_MIDDLE), + LABEL(BTN_SIDE), + LABEL(BTN_EXTRA), + LABEL(BTN_FORWARD), + LABEL(BTN_BACK), + LABEL(BTN_TASK), + LABEL(BTN_JOYSTICK), + LABEL(BTN_TRIGGER), + LABEL(BTN_THUMB), + LABEL(BTN_THUMB2), + LABEL(BTN_TOP), + LABEL(BTN_TOP2), + LABEL(BTN_PINKIE), + LABEL(BTN_BASE), + LABEL(BTN_BASE2), + LABEL(BTN_BASE3), + LABEL(BTN_BASE4), + LABEL(BTN_BASE5), + LABEL(BTN_BASE6), + LABEL(BTN_DEAD), + LABEL(BTN_GAMEPAD), + LABEL(BTN_SOUTH), + LABEL(BTN_EAST), + LABEL(BTN_C), + LABEL(BTN_NORTH), + LABEL(BTN_WEST), + LABEL(BTN_Z), + LABEL(BTN_TL), + LABEL(BTN_TR), + LABEL(BTN_TL2), + LABEL(BTN_TR2), + LABEL(BTN_SELECT), + LABEL(BTN_START), + LABEL(BTN_MODE), + LABEL(BTN_THUMBL), + LABEL(BTN_THUMBR), + LABEL(BTN_DIGI), + LABEL(BTN_TOOL_PEN), + LABEL(BTN_TOOL_RUBBER), + LABEL(BTN_TOOL_BRUSH), + LABEL(BTN_TOOL_PENCIL), + LABEL(BTN_TOOL_AIRBRUSH), + LABEL(BTN_TOOL_FINGER), + LABEL(BTN_TOOL_MOUSE), + LABEL(BTN_TOOL_LENS), + LABEL(BTN_TOOL_QUINTTAP), + LABEL(BTN_STYLUS3), + LABEL(BTN_TOUCH), + LABEL(BTN_STYLUS), + LABEL(BTN_STYLUS2), + LABEL(BTN_TOOL_DOUBLETAP), + LABEL(BTN_TOOL_TRIPLETAP), + LABEL(BTN_TOOL_QUADTAP), + LABEL(BTN_WHEEL), + LABEL(BTN_GEAR_DOWN), + LABEL(BTN_GEAR_UP), + LABEL(KEY_OK), + LABEL(KEY_SELECT), + LABEL(KEY_GOTO), + LABEL(KEY_CLEAR), + LABEL(KEY_POWER2), + LABEL(KEY_OPTION), + LABEL(KEY_INFO), + LABEL(KEY_TIME), + LABEL(KEY_VENDOR), + LABEL(KEY_ARCHIVE), + LABEL(KEY_PROGRAM), + LABEL(KEY_CHANNEL), + LABEL(KEY_FAVORITES), + LABEL(KEY_EPG), + LABEL(KEY_PVR), + LABEL(KEY_MHP), + LABEL(KEY_LANGUAGE), + LABEL(KEY_TITLE), + LABEL(KEY_SUBTITLE), + LABEL(KEY_ANGLE), + LABEL(KEY_FULL_SCREEN), + LABEL(KEY_MODE), + LABEL(KEY_KEYBOARD), + LABEL(KEY_ASPECT_RATIO), + LABEL(KEY_PC), + LABEL(KEY_TV), + LABEL(KEY_TV2), + LABEL(KEY_VCR), + LABEL(KEY_VCR2), + LABEL(KEY_SAT), + LABEL(KEY_SAT2), + LABEL(KEY_CD), + LABEL(KEY_TAPE), + LABEL(KEY_RADIO), + LABEL(KEY_TUNER), + LABEL(KEY_PLAYER), + LABEL(KEY_TEXT), + LABEL(KEY_DVD), + LABEL(KEY_AUX), + LABEL(KEY_MP3), + LABEL(KEY_AUDIO), + LABEL(KEY_VIDEO), + LABEL(KEY_DIRECTORY), + LABEL(KEY_LIST), + LABEL(KEY_MEMO), + LABEL(KEY_CALENDAR), + LABEL(KEY_RED), + LABEL(KEY_GREEN), + LABEL(KEY_YELLOW), + LABEL(KEY_BLUE), + LABEL(KEY_CHANNELUP), + LABEL(KEY_CHANNELDOWN), + LABEL(KEY_FIRST), + LABEL(KEY_LAST), + LABEL(KEY_AB), + LABEL(KEY_NEXT), + LABEL(KEY_RESTART), + LABEL(KEY_SLOW), + LABEL(KEY_SHUFFLE), + LABEL(KEY_BREAK), + LABEL(KEY_PREVIOUS), + LABEL(KEY_DIGITS), + LABEL(KEY_TEEN), + LABEL(KEY_TWEN), + LABEL(KEY_VIDEOPHONE), + LABEL(KEY_GAMES), + LABEL(KEY_ZOOMIN), + LABEL(KEY_ZOOMOUT), + LABEL(KEY_ZOOMRESET), + LABEL(KEY_WORDPROCESSOR), + LABEL(KEY_EDITOR), + LABEL(KEY_SPREADSHEET), + LABEL(KEY_GRAPHICSEDITOR), + LABEL(KEY_PRESENTATION), + LABEL(KEY_DATABASE), + LABEL(KEY_NEWS), + LABEL(KEY_VOICEMAIL), + LABEL(KEY_ADDRESSBOOK), + LABEL(KEY_MESSENGER), + LABEL(KEY_DISPLAYTOGGLE), + LABEL(KEY_SPELLCHECK), + LABEL(KEY_LOGOFF), + LABEL(KEY_DOLLAR), + LABEL(KEY_EURO), + LABEL(KEY_FRAMEBACK), + LABEL(KEY_FRAMEFORWARD), + LABEL(KEY_CONTEXT_MENU), + LABEL(KEY_MEDIA_REPEAT), + LABEL(KEY_10CHANNELSUP), + LABEL(KEY_10CHANNELSDOWN), + LABEL(KEY_IMAGES), + LABEL(KEY_NOTIFICATION_CENTER), + LABEL(KEY_PICKUP_PHONE), + LABEL(KEY_HANGUP_PHONE), + LABEL(KEY_DEL_EOL), + LABEL(KEY_DEL_EOS), + LABEL(KEY_INS_LINE), + LABEL(KEY_DEL_LINE), + LABEL(KEY_FN), + LABEL(KEY_FN_ESC), + LABEL(KEY_FN_F1), + LABEL(KEY_FN_F2), + LABEL(KEY_FN_F3), + LABEL(KEY_FN_F4), + LABEL(KEY_FN_F5), + LABEL(KEY_FN_F6), + LABEL(KEY_FN_F7), + LABEL(KEY_FN_F8), + LABEL(KEY_FN_F9), + LABEL(KEY_FN_F10), + LABEL(KEY_FN_F11), + LABEL(KEY_FN_F12), + LABEL(KEY_FN_1), + LABEL(KEY_FN_2), + LABEL(KEY_FN_D), + LABEL(KEY_FN_E), + LABEL(KEY_FN_F), + LABEL(KEY_FN_S), + LABEL(KEY_FN_B), + LABEL(KEY_FN_RIGHT_SHIFT), + LABEL(KEY_BRL_DOT1), + LABEL(KEY_BRL_DOT2), + LABEL(KEY_BRL_DOT3), + LABEL(KEY_BRL_DOT4), + LABEL(KEY_BRL_DOT5), + LABEL(KEY_BRL_DOT6), + LABEL(KEY_BRL_DOT7), + LABEL(KEY_BRL_DOT8), + LABEL(KEY_BRL_DOT9), + LABEL(KEY_BRL_DOT10), + LABEL(KEY_NUMERIC_0), + LABEL(KEY_NUMERIC_1), + LABEL(KEY_NUMERIC_2), + LABEL(KEY_NUMERIC_3), + LABEL(KEY_NUMERIC_4), + LABEL(KEY_NUMERIC_5), + LABEL(KEY_NUMERIC_6), + LABEL(KEY_NUMERIC_7), + LABEL(KEY_NUMERIC_8), + LABEL(KEY_NUMERIC_9), + LABEL(KEY_NUMERIC_STAR), + LABEL(KEY_NUMERIC_POUND), + LABEL(KEY_NUMERIC_A), + LABEL(KEY_NUMERIC_B), + LABEL(KEY_NUMERIC_C), + LABEL(KEY_NUMERIC_D), + LABEL(KEY_CAMERA_FOCUS), + LABEL(KEY_WPS_BUTTON), + LABEL(KEY_TOUCHPAD_TOGGLE), + LABEL(KEY_TOUCHPAD_ON), + LABEL(KEY_TOUCHPAD_OFF), + LABEL(KEY_CAMERA_ZOOMIN), + LABEL(KEY_CAMERA_ZOOMOUT), + LABEL(KEY_CAMERA_UP), + LABEL(KEY_CAMERA_DOWN), + LABEL(KEY_CAMERA_LEFT), + LABEL(KEY_CAMERA_RIGHT), + LABEL(KEY_ATTENDANT_ON), + LABEL(KEY_ATTENDANT_OFF), + LABEL(KEY_ATTENDANT_TOGGLE), + LABEL(KEY_LIGHTS_TOGGLE), + LABEL(BTN_DPAD_UP), + LABEL(BTN_DPAD_DOWN), + LABEL(BTN_DPAD_LEFT), + LABEL(BTN_DPAD_RIGHT), + LABEL(KEY_ALS_TOGGLE), + LABEL(KEY_ROTATE_LOCK_TOGGLE), +// LABEL(KEY_REFRESH_RATE_TOGGLE), + LABEL(KEY_BUTTONCONFIG), + LABEL(KEY_TASKMANAGER), + LABEL(KEY_JOURNAL), + LABEL(KEY_CONTROLPANEL), + LABEL(KEY_APPSELECT), + LABEL(KEY_SCREENSAVER), + LABEL(KEY_VOICECOMMAND), + LABEL(KEY_ASSISTANT), + LABEL(KEY_KBD_LAYOUT_NEXT), + LABEL(KEY_EMOJI_PICKER), + LABEL(KEY_DICTATE), + LABEL(KEY_CAMERA_ACCESS_ENABLE), + LABEL(KEY_CAMERA_ACCESS_DISABLE), + LABEL(KEY_CAMERA_ACCESS_TOGGLE), +// LABEL(KEY_ACCESSIBILITY), +// LABEL(KEY_DO_NOT_DISTURB), + LABEL(KEY_BRIGHTNESS_MIN), + LABEL(KEY_BRIGHTNESS_MAX), + LABEL(KEY_KBDINPUTASSIST_PREV), + LABEL(KEY_KBDINPUTASSIST_NEXT), + LABEL(KEY_KBDINPUTASSIST_PREVGROUP), + LABEL(KEY_KBDINPUTASSIST_NEXTGROUP), + LABEL(KEY_KBDINPUTASSIST_ACCEPT), + LABEL(KEY_KBDINPUTASSIST_CANCEL), + LABEL(KEY_RIGHT_UP), + LABEL(KEY_RIGHT_DOWN), + LABEL(KEY_LEFT_UP), + LABEL(KEY_LEFT_DOWN), + LABEL(KEY_ROOT_MENU), + LABEL(KEY_MEDIA_TOP_MENU), + LABEL(KEY_NUMERIC_11), + LABEL(KEY_NUMERIC_12), + LABEL(KEY_AUDIO_DESC), + LABEL(KEY_3D_MODE), + LABEL(KEY_NEXT_FAVORITE), + LABEL(KEY_STOP_RECORD), + LABEL(KEY_PAUSE_RECORD), + LABEL(KEY_VOD), + LABEL(KEY_UNMUTE), + LABEL(KEY_FASTREVERSE), + LABEL(KEY_SLOWREVERSE), + LABEL(KEY_DATA), + LABEL(KEY_ONSCREEN_KEYBOARD), + LABEL(KEY_PRIVACY_SCREEN_TOGGLE), + LABEL(KEY_SELECTIVE_SCREENSHOT), + LABEL(KEY_NEXT_ELEMENT), + LABEL(KEY_PREVIOUS_ELEMENT), + LABEL(KEY_AUTOPILOT_ENGAGE_TOGGLE), + LABEL(KEY_MARK_WAYPOINT), + LABEL(KEY_SOS), + LABEL(KEY_NAV_CHART), + LABEL(KEY_FISHING_CHART), + LABEL(KEY_SINGLE_RANGE_RADAR), + LABEL(KEY_DUAL_RANGE_RADAR), + LABEL(KEY_RADAR_OVERLAY), + LABEL(KEY_TRADITIONAL_SONAR), + LABEL(KEY_CLEARVU_SONAR), + LABEL(KEY_SIDEVU_SONAR), + LABEL(KEY_NAV_INFO), + LABEL(KEY_BRIGHTNESS_MENU), + LABEL(KEY_MACRO1), + LABEL(KEY_MACRO2), + LABEL(KEY_MACRO3), + LABEL(KEY_MACRO4), + LABEL(KEY_MACRO5), + LABEL(KEY_MACRO6), + LABEL(KEY_MACRO7), + LABEL(KEY_MACRO8), + LABEL(KEY_MACRO9), + LABEL(KEY_MACRO10), + LABEL(KEY_MACRO11), + LABEL(KEY_MACRO12), + LABEL(KEY_MACRO13), + LABEL(KEY_MACRO14), + LABEL(KEY_MACRO15), + LABEL(KEY_MACRO16), + LABEL(KEY_MACRO17), + LABEL(KEY_MACRO18), + LABEL(KEY_MACRO19), + LABEL(KEY_MACRO20), + LABEL(KEY_MACRO21), + LABEL(KEY_MACRO22), + LABEL(KEY_MACRO23), + LABEL(KEY_MACRO24), + LABEL(KEY_MACRO25), + LABEL(KEY_MACRO26), + LABEL(KEY_MACRO27), + LABEL(KEY_MACRO28), + LABEL(KEY_MACRO29), + LABEL(KEY_MACRO30), + LABEL(KEY_MACRO_RECORD_START), + LABEL(KEY_MACRO_RECORD_STOP), + LABEL(KEY_MACRO_PRESET_CYCLE), + LABEL(KEY_MACRO_PRESET1), + LABEL(KEY_MACRO_PRESET2), + LABEL(KEY_MACRO_PRESET3), + LABEL(KEY_KBD_LCD_MENU1), + LABEL(KEY_KBD_LCD_MENU2), + LABEL(KEY_KBD_LCD_MENU3), + LABEL(KEY_KBD_LCD_MENU4), + LABEL(KEY_KBD_LCD_MENU5), + LABEL(BTN_TRIGGER_HAPPY), + LABEL(BTN_TRIGGER_HAPPY1), + LABEL(BTN_TRIGGER_HAPPY2), + LABEL(BTN_TRIGGER_HAPPY3), + LABEL(BTN_TRIGGER_HAPPY4), + LABEL(BTN_TRIGGER_HAPPY5), + LABEL(BTN_TRIGGER_HAPPY6), + LABEL(BTN_TRIGGER_HAPPY7), + LABEL(BTN_TRIGGER_HAPPY8), + LABEL(BTN_TRIGGER_HAPPY9), + LABEL(BTN_TRIGGER_HAPPY10), + LABEL(BTN_TRIGGER_HAPPY11), + LABEL(BTN_TRIGGER_HAPPY12), + LABEL(BTN_TRIGGER_HAPPY13), + LABEL(BTN_TRIGGER_HAPPY14), + LABEL(BTN_TRIGGER_HAPPY15), + LABEL(BTN_TRIGGER_HAPPY16), + LABEL(BTN_TRIGGER_HAPPY17), + LABEL(BTN_TRIGGER_HAPPY18), + LABEL(BTN_TRIGGER_HAPPY19), + LABEL(BTN_TRIGGER_HAPPY20), + LABEL(BTN_TRIGGER_HAPPY21), + LABEL(BTN_TRIGGER_HAPPY22), + LABEL(BTN_TRIGGER_HAPPY23), + LABEL(BTN_TRIGGER_HAPPY24), + LABEL(BTN_TRIGGER_HAPPY25), + LABEL(BTN_TRIGGER_HAPPY26), + LABEL(BTN_TRIGGER_HAPPY27), + LABEL(BTN_TRIGGER_HAPPY28), + LABEL(BTN_TRIGGER_HAPPY29), + LABEL(BTN_TRIGGER_HAPPY30), + LABEL(BTN_TRIGGER_HAPPY31), + LABEL(BTN_TRIGGER_HAPPY32), + LABEL(BTN_TRIGGER_HAPPY33), + LABEL(BTN_TRIGGER_HAPPY34), + LABEL(BTN_TRIGGER_HAPPY35), + LABEL(BTN_TRIGGER_HAPPY36), + LABEL(BTN_TRIGGER_HAPPY37), + LABEL(BTN_TRIGGER_HAPPY38), + LABEL(BTN_TRIGGER_HAPPY39), + LABEL(BTN_TRIGGER_HAPPY40), + LABEL(KEY_MAX), + LABEL_END, + }; + static struct label rel_labels[] = { + LABEL(REL_X), + LABEL(REL_Y), + LABEL(REL_Z), + LABEL(REL_RX), + LABEL(REL_RY), + LABEL(REL_RZ), + LABEL(REL_HWHEEL), + LABEL(REL_DIAL), + LABEL(REL_WHEEL), + LABEL(REL_MISC), + LABEL(REL_RESERVED), + LABEL(REL_WHEEL_HI_RES), + LABEL(REL_HWHEEL_HI_RES), + LABEL(REL_MAX), + LABEL_END, + }; + static struct label abs_labels[] = { + LABEL(ABS_X), + LABEL(ABS_Y), + LABEL(ABS_Z), + LABEL(ABS_RX), + LABEL(ABS_RY), + LABEL(ABS_RZ), + LABEL(ABS_THROTTLE), + LABEL(ABS_RUDDER), + LABEL(ABS_WHEEL), + LABEL(ABS_GAS), + LABEL(ABS_BRAKE), + LABEL(ABS_HAT0X), + LABEL(ABS_HAT0Y), + LABEL(ABS_HAT1X), + LABEL(ABS_HAT1Y), + LABEL(ABS_HAT2X), + LABEL(ABS_HAT2Y), + LABEL(ABS_HAT3X), + LABEL(ABS_HAT3Y), + LABEL(ABS_PRESSURE), + LABEL(ABS_DISTANCE), + LABEL(ABS_TILT_X), + LABEL(ABS_TILT_Y), + LABEL(ABS_TOOL_WIDTH), + LABEL(ABS_VOLUME), + LABEL(ABS_PROFILE), + LABEL(ABS_MISC), + LABEL(ABS_RESERVED), + LABEL(ABS_MT_SLOT), + LABEL(ABS_MT_TOUCH_MAJOR), + LABEL(ABS_MT_TOUCH_MINOR), + LABEL(ABS_MT_WIDTH_MAJOR), + LABEL(ABS_MT_WIDTH_MINOR), + LABEL(ABS_MT_ORIENTATION), + LABEL(ABS_MT_POSITION_X), + LABEL(ABS_MT_POSITION_Y), + LABEL(ABS_MT_TOOL_TYPE), + LABEL(ABS_MT_BLOB_ID), + LABEL(ABS_MT_TRACKING_ID), + LABEL(ABS_MT_PRESSURE), + LABEL(ABS_MT_DISTANCE), + LABEL(ABS_MT_TOOL_X), + LABEL(ABS_MT_TOOL_Y), + LABEL(ABS_MAX), + LABEL_END, + }; + static struct label sw_labels[] = { + LABEL(SW_LID), + LABEL(SW_TABLET_MODE), + LABEL(SW_HEADPHONE_INSERT), + LABEL(SW_RFKILL_ALL), + LABEL(SW_MICROPHONE_INSERT), + LABEL(SW_DOCK), + LABEL(SW_LINEOUT_INSERT), + LABEL(SW_JACK_PHYSICAL_INSERT), + LABEL(SW_VIDEOOUT_INSERT), + LABEL(SW_CAMERA_LENS_COVER), + LABEL(SW_KEYPAD_SLIDE), + LABEL(SW_FRONT_PROXIMITY), + LABEL(SW_ROTATE_LOCK), + LABEL(SW_LINEIN_INSERT), + LABEL(SW_MUTE_DEVICE), + LABEL(SW_PEN_INSERTED), + LABEL(SW_MACHINE_COVER), + LABEL(SW_MAX), + LABEL_END, + }; + static struct label msc_labels[] = { + LABEL(MSC_SERIAL), + LABEL(MSC_PULSELED), + LABEL(MSC_GESTURE), + LABEL(MSC_RAW), + LABEL(MSC_SCAN), + LABEL(MSC_TIMESTAMP), + LABEL(MSC_MAX), + LABEL_END, + }; + static struct label led_labels[] = { + LABEL(LED_NUML), + LABEL(LED_CAPSL), + LABEL(LED_SCROLLL), + LABEL(LED_COMPOSE), + LABEL(LED_KANA), + LABEL(LED_SLEEP), + LABEL(LED_SUSPEND), + LABEL(LED_MUTE), + LABEL(LED_MISC), + LABEL(LED_MAIL), + LABEL(LED_CHARGING), + LABEL(LED_MAX), + LABEL_END, + }; + static struct label rep_labels[] = { + LABEL(REP_DELAY), + LABEL(REP_PERIOD), + LABEL(REP_MAX), + LABEL_END, + }; + static struct label snd_labels[] = { + LABEL(SND_CLICK), + LABEL(SND_BELL), + LABEL(SND_TONE), + LABEL(SND_MAX), + LABEL_END, + }; + static struct label mt_tool_labels[] = { + LABEL(MT_TOOL_FINGER), + LABEL(MT_TOOL_PEN), + LABEL(MT_TOOL_PALM), + LABEL(MT_TOOL_DIAL), + LABEL(MT_TOOL_MAX), + LABEL_END, + }; + static struct label ff_status_labels[] = { + LABEL(FF_STATUS_STOPPED), + LABEL(FF_STATUS_PLAYING), + LABEL(FF_STATUS_MAX), + LABEL_END, + }; + static struct label ff_labels[] = { + LABEL(FF_RUMBLE), + LABEL(FF_PERIODIC), + LABEL(FF_CONSTANT), + LABEL(FF_SPRING), + LABEL(FF_FRICTION), + LABEL(FF_DAMPER), + LABEL(FF_INERTIA), + LABEL(FF_RAMP), + LABEL(FF_SQUARE), + LABEL(FF_TRIANGLE), + LABEL(FF_SINE), + LABEL(FF_SAW_UP), + LABEL(FF_SAW_DOWN), + LABEL(FF_CUSTOM), + LABEL(FF_GAIN), + LABEL(FF_AUTOCENTER), + LABEL(FF_MAX), + LABEL_END, + }; + +#undef LABEL +#undef LABEL_END + + std::string getLabel(const label *labels, int value) { + if (labels == nullptr) return std::to_string(value); + while (labels->name != nullptr && value != labels->value) { + labels++; + } + return labels->name != nullptr ? labels->name : std::to_string(value); + } + + std::optional getValue(const label *labels, const char *searchLabel) { + if (labels == nullptr) return {}; + while (labels->name != nullptr && ::strcasecmp(labels->name, searchLabel) != 0) { + labels++; + } + return labels->name != nullptr ? std::make_optional(labels->value) : std::nullopt; + } + + const label *getCodeLabelsForType(int32_t type) { + switch (type) { + case EV_SYN: + return syn_labels; + case EV_KEY: + return key_labels; + case EV_REL: + return rel_labels; + case EV_ABS: + return abs_labels; + case EV_SW: + return sw_labels; + case EV_MSC: + return msc_labels; + case EV_LED: + return led_labels; + case EV_REP: + return rep_labels; + case EV_SND: + return snd_labels; + case EV_FF: + return ff_labels; + case EV_FF_STATUS: + return ff_status_labels; + default: + return nullptr; + } + } + + const label *getValueLabelsForTypeAndCode(int32_t type, int32_t code) { + if (type == EV_KEY) { + return ev_key_value_labels; + } + if (type == EV_ABS && code == ABS_MT_TOOL_TYPE) { + return mt_tool_labels; + } + return nullptr; + } + + } // namespace + + EvdevEventLabel + InputEventLookup::getLinuxEvdevLabel(int32_t type, int32_t code, int32_t value) { + return { + .type = getLabel(ev_labels, type), + .code = getLabel(getCodeLabelsForType(type), code), + .value = getLabel(getValueLabelsForTypeAndCode(type, code), value), + }; + } + + std::optional InputEventLookup::getLinuxEvdevEventTypeByLabel(const char *label) { + return getValue(ev_labels, label); + } + + std::optional InputEventLookup::getLinuxEvdevEventCodeByLabel(int32_t type, + const char *label) { + return getValue(getCodeLabelsForType(type), label); + } + + std::optional InputEventLookup::getLinuxEvdevInputPropByLabel(const char *label) { + return getValue(input_prop_labels, label); + } + +} // namespace android diff --git a/sysbridge/src/main/cpp/android/input/InputEventLabels.h b/sysbridge/src/main/cpp/android/input/InputEventLabels.h new file mode 100644 index 0000000000..066ff06ca7 --- /dev/null +++ b/sysbridge/src/main/cpp/android/input/InputEventLabels.h @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace android { + + template + size_t size(T (&)[N]) { return N; } + + struct InputEventLabel { + const char *literal; + int value; + }; + + struct EvdevEventLabel { + std::string type; + std::string code; + std::string value; + }; + +// NOTE: If you want a new key code, axis code, led code or flag code in keylayout file, +// then you must add it to InputEventLabels.cpp. + + class InputEventLookup { + /** + * This class is not purely static, but uses a singleton pattern in order to delay the + * initialization of the maps that it contains. If it were purely static, the maps could be + * created early, and would cause sanitizers to report memory leaks. + */ + public: + InputEventLookup(InputEventLookup& other) = delete; + + void operator=(const InputEventLookup&) = delete; + + static std::optional lookupValueByLabel(const std::unordered_map& map, + const char* literal); + + static const char* lookupLabelByValue(const std::vector& vec, int value); + + static std::optional getKeyCodeByLabel(const char* label); + + static const char* getLabelByKeyCode(int32_t keyCode); + + static std::optional getKeyFlagByLabel(const char* label); + + static std::optional getAxisByLabel(const char* label); + + static const char* getAxisLabel(int32_t axisId); + + static std::optional getLedByLabel(const char* label); + + static EvdevEventLabel getLinuxEvdevLabel(int32_t type, int32_t code, int32_t value); + + static std::optional getLinuxEvdevEventTypeByLabel(const char* label); + + static std::optional getLinuxEvdevEventCodeByLabel(int32_t type, const char* label); + + static std::optional getLinuxEvdevInputPropByLabel(const char* label); + + private: + InputEventLookup(); + + static const InputEventLookup& get() { + static InputEventLookup sLookup; + return sLookup; + } + + const std::unordered_map KEYCODES; + + const std::vector KEY_NAMES; + + const std::unordered_map AXES; + + const std::vector AXES_NAMES; + + const std::unordered_map LEDS; + + const std::unordered_map FLAGS; + }; + +} // namespace android diff --git a/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp b/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp new file mode 100644 index 0000000000..bbf5a5437b --- /dev/null +++ b/sysbridge/src/main/cpp/android/input/KeyLayoutMap.cpp @@ -0,0 +1,444 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, Tokenizer.cppeither express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "../../logging.h" +#include +#include "../utils/String8.h" +#include "KeyLayoutMap.h" +#include "../utils/Tokenizer.h" +#include "InputEventLabels.h" +#include + +#include +#include +#include +#include "../libbase/result.h" +#include "../liblog/log_main.h" +#include "Input.h" + +#define DEBUG_MAPPING false +#define DEBUG_PARSER false + +// Enables debug output for parser performance. +#define DEBUG_PARSER_PERFORMANCE 0 + +namespace android { + + namespace { + + std::optional parseInt(const char *str) { + char *end; + errno = 0; + const int value = strtol(str, &end, 0); + if (end == str) { + LOGE("Could not parse %s", str); + return {}; + } + if (errno == ERANGE) { + LOGE("Out of bounds: %s", str); + return {}; + } + return value; + } + + constexpr const char *WHITESPACE = " \t\r"; + + } // namespace + + KeyLayoutMap::KeyLayoutMap() = default; + + KeyLayoutMap::~KeyLayoutMap() = default; + + base::Result> + KeyLayoutMap::loadContents(const std::string &filename, + const char *contents) { + return load(filename, contents); + } + + base::Result> KeyLayoutMap::load(const std::string &filename, + const char *contents) { + Tokenizer *tokenizer; + status_t status; + if (contents == nullptr) { + status = Tokenizer::open(String8(filename.c_str()), &tokenizer); + } else { + status = Tokenizer::fromContents(String8(filename.c_str()), contents, &tokenizer); + } + + if (status) { + LOGE("Error %d opening key layout map file %s.", status, filename.c_str()); + return Errorf("Error {} opening key layout map file {}.", status, filename.c_str()); + } + std::unique_ptr t(tokenizer); + auto ret = load(t.get()); + if (!ret.ok()) { + return ret; + } + const std::shared_ptr &map = *ret; + LOG_ALWAYS_FATAL_IF(map == nullptr, "Returned map should not be null if there's no error"); + + map->mLoadFileName = filename; + return ret; + } + + base::Result> KeyLayoutMap::load(Tokenizer *tokenizer) { + std::shared_ptr map = std::shared_ptr(new KeyLayoutMap()); + status_t status = OK; + if (!map.get()) { + LOGE("Error allocating key layout map."); + return Errorf("Error allocating key layout map."); + } else { +#if DEBUG_PARSER_PERFORMANCE + nsecs_t startTime = systemTime(SYSTEM_TIME_MONOTONIC); +#endif + Parser parser(map.get(), tokenizer); + status = parser.parse(); +#if DEBUG_PARSER_PERFORMANCE + nsecs_t elapsedTime = systemTime(SYSTEM_TIME_MONOTONIC) - startTime; + LOGD("Parsed key layout map file '%s' %d lines in %0.3fms.", + tokenizer->getFilename().c_str(), tokenizer->getLineNumber(), + elapsedTime / 1000000.0); +#endif + if (!status) { + return std::move(map); + } + } + return Errorf("Load KeyLayoutMap failed {}.", status); + } + + status_t KeyLayoutMap::mapKey(int32_t scanCode, int32_t usageCode, + int32_t *outKeyCode, uint32_t *outFlags) const { + const Key *key = getKey(scanCode, usageCode); + if (!key) { + ALOGD_IF(DEBUG_MAPPING, "mapKey: scanCode=%d, usageCode=0x%08x ~ Failed.", scanCode, + usageCode); + *outKeyCode = AKEYCODE_UNKNOWN; + *outFlags = 0; + return NAME_NOT_FOUND; + } + + *outKeyCode = key->keyCode; + *outFlags = key->flags; + + ALOGD_IF(DEBUG_MAPPING, + "mapKey: scanCode=%d, usageCode=0x%08x ~ Result keyCode=%d, outFlags=0x%08x.", + scanCode, usageCode, *outKeyCode, *outFlags); + return NO_ERROR; + } + + const KeyLayoutMap::Key *KeyLayoutMap::getKey(int32_t scanCode, int32_t usageCode) const { + if (usageCode) { + auto it = mKeysByUsageCode.find(usageCode); + if (it != mKeysByUsageCode.end()) { + return &it->second; + } + } + if (scanCode) { + auto it = mKeysByScanCode.find(scanCode); + if (it != mKeysByScanCode.end()) { + return &it->second; + } + } + return nullptr; + } + + std::vector KeyLayoutMap::findScanCodesForKey(int32_t keyCode) const { + std::vector scanCodes; + // b/354333072: Only consider keys without FUNCTION flag + for (const auto &[scanCode, key]: mKeysByScanCode) { + if (keyCode == key.keyCode && !(key.flags & POLICY_FLAG_FUNCTION)) { + scanCodes.push_back(scanCode); + } + } + return scanCodes; + } + + std::vector KeyLayoutMap::findUsageCodesForKey(int32_t keyCode) const { + std::vector usageCodes; + for (const auto &[usageCode, key]: mKeysByUsageCode) { + if (keyCode == key.keyCode && !(key.flags & POLICY_FLAG_FALLBACK_USAGE_MAPPING)) { + usageCodes.push_back(usageCode); + } + } + return usageCodes; + } + + std::optional KeyLayoutMap::mapAxis(int32_t scanCode) const { + auto it = mAxes.find(scanCode); + if (it == mAxes.end()) { + ALOGD_IF(DEBUG_MAPPING, "mapAxis: scanCode=%d ~ Failed.", scanCode); + return std::nullopt; + } + + const AxisInfo &axisInfo = it->second; + ALOGD_IF(DEBUG_MAPPING, + "mapAxis: scanCode=%d ~ Result mode=%d, axis=%d, highAxis=%d, " + "splitValue=%d, flatOverride=%d.", + scanCode, axisInfo.mode, axisInfo.axis, axisInfo.highAxis, axisInfo.splitValue, + axisInfo.flatOverride); + return axisInfo; + } + +// --- KeyLayoutMap::Parser --- + + KeyLayoutMap::Parser::Parser(KeyLayoutMap *map, Tokenizer *tokenizer) : + mMap(map), mTokenizer(tokenizer) { + } + + KeyLayoutMap::Parser::~Parser() { + } + + status_t KeyLayoutMap::Parser::parse() { + while (!mTokenizer->isEof()) { + ALOGD_IF(DEBUG_PARSER, "Parsing %s: '%s'.", mTokenizer->getLocation().c_str(), + mTokenizer->peekRemainderOfLine().c_str()); + + mTokenizer->skipDelimiters(WHITESPACE); + + if (!mTokenizer->isEol() && mTokenizer->peekChar() != '#') { + String8 keywordToken = mTokenizer->nextToken(WHITESPACE); + if (keywordToken == "key") { + mTokenizer->skipDelimiters(WHITESPACE); + status_t status = parseKey(); + if (status) return status; + } else if (keywordToken == "axis") { + mTokenizer->skipDelimiters(WHITESPACE); + status_t status = parseAxis(); + if (status) return status; + } else if (keywordToken == "led") { + // Skip LEDs, we don't need them for Key Mapper + mTokenizer->nextLine(); + continue; + } else if (keywordToken == "sensor") { + // Skip Sensors, we don't need them for Key Mapper + mTokenizer->nextLine(); + continue; + } else if (keywordToken == "requires_kernel_config") { + mTokenizer->skipDelimiters(WHITESPACE); + status_t status = parseRequiredKernelConfig(); + if (status) return status; + } else { + LOGE("%s: Expected keyword, got '%s'.", mTokenizer->getLocation().c_str(), + keywordToken.c_str()); + return BAD_VALUE; + } + + mTokenizer->skipDelimiters(WHITESPACE); + + if (!mTokenizer->isEol() && mTokenizer->peekChar() != '#') { + LOGW("%s: Expected end of line or trailing comment, got '%s'.", + mTokenizer->getLocation().c_str(), + mTokenizer->peekRemainderOfLine().c_str()); + return BAD_VALUE; + } + } + + mTokenizer->nextLine(); + } + return NO_ERROR; + } + + status_t KeyLayoutMap::Parser::parseKey() { + String8 codeToken = mTokenizer->nextToken(WHITESPACE); + bool mapUsage = false; + if (codeToken == "usage") { + mapUsage = true; + mTokenizer->skipDelimiters(WHITESPACE); + codeToken = mTokenizer->nextToken(WHITESPACE); + } + + std::optional code = parseInt(codeToken.c_str()); + if (!code) { + LOGE("%s: Expected key %s number, got '%s'.", mTokenizer->getLocation().c_str(), + mapUsage ? "usage" : "scan code", codeToken.c_str()); + return BAD_VALUE; + } + std::unordered_map &map = + mapUsage ? mMap->mKeysByUsageCode : mMap->mKeysByScanCode; + if (map.find(*code) != map.end()) { + LOGE("%s: Duplicate entry for key %s '%s'.", mTokenizer->getLocation().c_str(), + mapUsage ? "usage" : "scan code", codeToken.c_str()); + return BAD_VALUE; + } + + mTokenizer->skipDelimiters(WHITESPACE); + String8 keyCodeToken = mTokenizer->nextToken(WHITESPACE); + std::optional keyCode = InputEventLookup::getKeyCodeByLabel(keyCodeToken.c_str()); + + if (!keyCode) { +// LOGW("%s: Unknown key code label %s", mTokenizer->getLocation().c_str(), +// keyCodeToken.c_str()); + // Do not crash at this point because there may be more flags afterwards that need parsing. + } + + uint32_t flags = 0; + for (;;) { + mTokenizer->skipDelimiters(WHITESPACE); + if (mTokenizer->isEol() || mTokenizer->peekChar() == '#') break; + + String8 flagToken = mTokenizer->nextToken(WHITESPACE); + std::optional flag = InputEventLookup::getKeyFlagByLabel(flagToken.c_str()); + if (!flag) { + LOGE("%s: Expected key flag label, got '%s'.", mTokenizer->getLocation().c_str(), + flagToken.c_str()); + return BAD_VALUE; + } + if (flags & *flag) { + LOGE("%s: Duplicate key flag '%s'.", mTokenizer->getLocation().c_str(), + flagToken.c_str()); + return BAD_VALUE; + } + flags |= *flag; + } + + ALOGD_IF(DEBUG_PARSER, "Parsed key %s: code=%d, keyCode=%d, flags=0x%08x.", + mapUsage ? "usage" : "scan code", *code, *keyCode, flags); + + // The key code may be unknown so only insert a key if it is known. + if (keyCode) { + Key key; + key.keyCode = *keyCode; + key.flags = flags; + map.insert({*code, key}); + } + + return NO_ERROR; + } + + status_t KeyLayoutMap::Parser::parseAxis() { + String8 scanCodeToken = mTokenizer->nextToken(WHITESPACE); + std::optional scanCode = parseInt(scanCodeToken.c_str()); + if (!scanCode) { + LOGE("%s: Expected axis scan code number, got '%s'.", mTokenizer->getLocation().c_str(), + scanCodeToken.c_str()); + return BAD_VALUE; + } + if (mMap->mAxes.find(*scanCode) != mMap->mAxes.end()) { + LOGE("%s: Duplicate entry for axis scan code '%s'.", mTokenizer->getLocation().c_str(), + scanCodeToken.c_str()); + return BAD_VALUE; + } + + AxisInfo axisInfo; + + mTokenizer->skipDelimiters(WHITESPACE); + String8 token = mTokenizer->nextToken(WHITESPACE); + if (token == "invert") { + axisInfo.mode = AxisInfo::MODE_INVERT; + + mTokenizer->skipDelimiters(WHITESPACE); + String8 axisToken = mTokenizer->nextToken(WHITESPACE); + std::optional axis = InputEventLookup::getAxisByLabel(axisToken.c_str()); + if (!axis) { + LOGE("%s: Expected inverted axis label, got '%s'.", + mTokenizer->getLocation().c_str(), + axisToken.c_str()); + return BAD_VALUE; + } + axisInfo.axis = *axis; + } else if (token == "split") { + axisInfo.mode = AxisInfo::MODE_SPLIT; + + mTokenizer->skipDelimiters(WHITESPACE); + String8 splitToken = mTokenizer->nextToken(WHITESPACE); + std::optional splitValue = parseInt(splitToken.c_str()); + if (!splitValue) { + LOGE("%s: Expected split value, got '%s'.", mTokenizer->getLocation().c_str(), + splitToken.c_str()); + return BAD_VALUE; + } + axisInfo.splitValue = *splitValue; + + mTokenizer->skipDelimiters(WHITESPACE); + String8 lowAxisToken = mTokenizer->nextToken(WHITESPACE); + std::optional axis = InputEventLookup::getAxisByLabel(lowAxisToken.c_str()); + if (!axis) { + LOGE("%s: Expected low axis label, got '%s'.", mTokenizer->getLocation().c_str(), + lowAxisToken.c_str()); + return BAD_VALUE; + } + axisInfo.axis = *axis; + + mTokenizer->skipDelimiters(WHITESPACE); + String8 highAxisToken = mTokenizer->nextToken(WHITESPACE); + std::optional highAxis = InputEventLookup::getAxisByLabel(highAxisToken.c_str()); + if (!highAxis) { + LOGE("%s: Expected high axis label, got '%s'.", mTokenizer->getLocation().c_str(), + highAxisToken.c_str()); + return BAD_VALUE; + } + axisInfo.highAxis = *highAxis; + } else { + std::optional axis = InputEventLookup::getAxisByLabel(token.c_str()); + if (!axis) { + LOGE("%s: Expected axis label, 'split' or 'invert', got '%s'.", + mTokenizer->getLocation().c_str(), token.c_str()); + return BAD_VALUE; + } + axisInfo.axis = *axis; + } + + for (;;) { + mTokenizer->skipDelimiters(WHITESPACE); + if (mTokenizer->isEol() || mTokenizer->peekChar() == '#') { + break; + } + String8 keywordToken = mTokenizer->nextToken(WHITESPACE); + if (keywordToken == "flat") { + mTokenizer->skipDelimiters(WHITESPACE); + String8 flatToken = mTokenizer->nextToken(WHITESPACE); + std::optional flatOverride = parseInt(flatToken.c_str()); + if (!flatOverride) { + LOGE("%s: Expected flat value, got '%s'.", mTokenizer->getLocation().c_str(), + flatToken.c_str()); + return BAD_VALUE; + } + axisInfo.flatOverride = *flatOverride; + } else { + LOGE("%s: Expected keyword 'flat', got '%s'.", mTokenizer->getLocation().c_str(), + keywordToken.c_str()); + return BAD_VALUE; + } + } + + ALOGD_IF(DEBUG_PARSER, + "Parsed axis: scanCode=%d, mode=%d, axis=%d, highAxis=%d, " + "splitValue=%d, flatOverride=%d.", + *scanCode, axisInfo.mode, axisInfo.axis, axisInfo.highAxis, axisInfo.splitValue, + axisInfo.flatOverride); + mMap->mAxes.insert({*scanCode, axisInfo}); + return NO_ERROR; + } + +// Parse the name of a required kernel config. +// The layout won't be used if the specified kernel config is not present +// Examples: +// requires_kernel_config CONFIG_HID_PLAYSTATION + status_t KeyLayoutMap::Parser::parseRequiredKernelConfig() { + String8 codeToken = mTokenizer->nextToken(WHITESPACE); + std::string configName = codeToken.c_str(); + + const auto result = mMap->mRequiredKernelConfigs.emplace(configName); + if (!result.second) { + LOGE("%s: Duplicate entry for required kernel config %s.", + mTokenizer->getLocation().c_str(), configName.c_str()); + return BAD_VALUE; + } + +// ALOGD_IF(DEBUG_PARSER, "Parsed required kernel config: name=%s", configName.c_str()); + return NO_ERROR; + } +} // namespace android diff --git a/sysbridge/src/main/cpp/android/input/KeyLayoutMap.h b/sysbridge/src/main/cpp/android/input/KeyLayoutMap.h new file mode 100644 index 0000000000..f5646f33e3 --- /dev/null +++ b/sysbridge/src/main/cpp/android/input/KeyLayoutMap.h @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include "../utils/Tokenizer.h" +#include "../libbase/result.h" +#include + +namespace android { + + struct AxisInfo { + enum Mode { + // Axis value is reported directly. + MODE_NORMAL = 0, + // Axis value should be inverted before reporting. + MODE_INVERT = 1, + // Axis value should be split into two axes + MODE_SPLIT = 2, + }; + + // Axis mode. + Mode mode; + + // Axis id. + // When split, this is the axis used for values smaller than the split position. + int32_t axis; + + // When split, this is the axis used for values after higher than the split position. + int32_t highAxis; + + // The split value, or 0 if not split. + int32_t splitValue; + + // The flat value, or -1 if none. + int32_t flatOverride; + + AxisInfo() : mode(MODE_NORMAL), axis(-1), highAxis(-1), splitValue(0), flatOverride(-1) { + } + }; + +/** + * Describes a mapping from keyboard scan codes and joystick axes to Android key codes and axes. + * + * This object is immutable after it has been loaded. + */ + class KeyLayoutMap { + public: + static base::Result> load(const std::string &filename, + const char *contents = nullptr); + + static base::Result> loadContents(const std::string &filename, + const char *contents); + + status_t mapKey(int32_t scanCode, int32_t usageCode, + int32_t *outKeyCode, uint32_t *outFlags) const; + + std::vector findScanCodesForKey(int32_t keyCode) const; + + std::optional findScanCodeForLed(int32_t ledCode) const; + + std::vector findUsageCodesForKey(int32_t keyCode) const; + + std::optional findUsageCodeForLed(int32_t ledCode) const; + + std::optional mapAxis(int32_t scanCode) const; + + virtual ~KeyLayoutMap(); + + private: + static base::Result> load(Tokenizer *tokenizer); + + struct Key { + int32_t keyCode; + uint32_t flags; + }; + + std::unordered_map mKeysByScanCode; + std::unordered_map mKeysByUsageCode; + std::unordered_map mAxes; + std::set mRequiredKernelConfigs; + std::string mLoadFileName; + + KeyLayoutMap(); + + const Key *getKey(int32_t scanCode, int32_t usageCode) const; + + class Parser { + KeyLayoutMap *mMap; + Tokenizer *mTokenizer; + + public: + Parser(KeyLayoutMap *map, Tokenizer *tokenizer); + + ~Parser(); + + status_t parse(); + + private: + status_t parseKey(); + + status_t parseAxis(); + + status_t parseRequiredKernelConfig(); + }; + }; + +} // namespace android diff --git a/sysbridge/src/main/cpp/android/libbase/errors.h b/sysbridge/src/main/cpp/android/libbase/errors.h new file mode 100644 index 0000000000..0ad4ecabd8 --- /dev/null +++ b/sysbridge/src/main/cpp/android/libbase/errors.h @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Portable error handling functions. This is only necessary for host-side +// code that needs to be cross-platform; code that is only run on Unix should +// just use errno and strerror() for simplicity. +// +// There is some complexity since Windows has (at least) three different error +// numbers, not all of which share the same type: +// * errno: for C runtime errors. +// * GetLastError(): Windows non-socket errors. +// * WSAGetLastError(): Windows socket errors. +// errno can be passed to strerror() on all platforms, but the other two require +// special handling to get the error string. Refer to Microsoft documentation +// to determine which error code to check for each function. + +#pragma once + +#include + +#include + +namespace android { +namespace base { + +// Returns a string describing the given system error code. |error_code| must +// be errno on Unix or GetLastError()/WSAGetLastError() on Windows. Passing +// errno on Windows has undefined behavior. +std::string SystemErrorCodeToString(int error_code); + +} // namespace base +} // namespace android + +// Convenient macros for evaluating a statement, checking if the result is error, and returning it +// to the caller. If it is ok then the inner value is unwrapped (if applicable) and returned. +// +// Usage with Result: +// +// Result getFoo() {...} +// +// Result getBar() { +// Foo foo = OR_RETURN(getFoo()); +// return Bar{foo}; +// } +// +// Usage with status_t: +// +// status_t getFoo(Foo*) {...} +// +// status_t getBar(Bar* bar) { +// Foo foo; +// OR_RETURN(getFoo(&foo)); +// *bar = Bar{foo}; +// return OK; +// } +// +// Actually this can be used for any type as long as the OkOrFail contract is satisfied. See +// below. +// If implicit conversion compilation errors occur involving a value type with a templated +// forwarding ref ctor, compilation with cpp20 or explicitly converting to the desired +// return type is required. +#define OR_RETURN(expr) \ + UNWRAP_OR_DO(__or_return_expr, expr, { return ok_or_fail::Fail(std::move(__or_return_expr)); }) + +// Same as OR_RETURN, but aborts if expr is a failure. +#if defined(__BIONIC__) +#define OR_FATAL(expr) \ + UNWRAP_OR_DO(__or_fatal_expr, expr, { \ + __assert(__FILE__, __LINE__, ok_or_fail::ErrorMessage(__or_fatal_expr).c_str()); \ + }) +#else +#define OR_FATAL(expr) \ + UNWRAP_OR_DO(__or_fatal_expr, expr, { \ + fprintf(stderr, "%s:%d: assertion \"%s\" failed", __FILE__, __LINE__, \ + ok_or_fail::ErrorMessage(__or_fatal_expr).c_str()); \ + abort(); \ + }) +#endif + +// Variant for use in gtests, which aborts the test function with an assertion failure on error. +// This is akin to ASSERT_OK_AND_ASSIGN for absl::Status, except the assignment is external. It +// assumes the user depends on libgmock and includes gtest/gtest.h. +#define OR_ASSERT_FAIL(expr) \ + UNWRAP_OR_DO(__or_assert_expr, expr, { \ + FAIL() << "Value of: " << #expr << "\n" \ + << " Actual: " << __or_assert_expr.error().message() << "\n" \ + << "Expected: is ok\n"; \ + }) + +// Generic macro to execute any statement(s) on error. Execution should never reach the end of them. +// result_var is assigned expr and is only visible to on_error_stmts. +#define UNWRAP_OR_DO(result_var, expr, on_error_stmts) \ + ({ \ + decltype(expr)&& result_var = (expr); \ + typedef android::base::OkOrFail> ok_or_fail; \ + if (!ok_or_fail::IsOk(result_var)) { \ + { \ + on_error_stmts; \ + } \ + __builtin_unreachable(); \ + } \ + ok_or_fail::Unwrap(std::move(result_var)); \ + }) + +namespace android { +namespace base { + +// The OkOrFail contract for a type T. This must be implemented for a type T if you want to use +// OR_RETURN(stmt) where stmt evalues to a value of type T. +template +struct OkOrFail { + // Checks if T is ok or fail. + static bool IsOk(const T&); + + // Turns T into the success value. + template + static U Unwrap(T&&); + + // Moves T into OkOrFail, so that we can convert it to other types + OkOrFail(T&& v); + OkOrFail() = delete; + OkOrFail(const T&) = delete; + + // And there need to be one or more conversion operators that turns the error value of T into a + // target type. For example, for T = Result, there can be ... + // + // // for the case where OR_RETURN is called in a function expecting E + // operator E()&& { return val_.error().code(); } + // + // // for the case where OR_RETURN is called in a function expecting Result + // template + // operator Result()&& { return val_.error(); } + + // And there needs to be a method that returns the string representation of the fail value. + // static const std::string& ErrorMessage(const T& v); + // or + // static std::string ErrorMessage(const T& v); +}; + +} // namespace base +} // namespace android diff --git a/sysbridge/src/main/cpp/android/libbase/expected.h b/sysbridge/src/main/cpp/android/libbase/expected.h new file mode 100644 index 0000000000..ddab9e5b57 --- /dev/null +++ b/sysbridge/src/main/cpp/android/libbase/expected.h @@ -0,0 +1,622 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include + +// android::base::expected is a partial implementation of C++23's std::expected +// for Android. +// +// Usage: +// using android::base::expected; +// using android::base::unexpected; +// +// expected safe_divide(double i, double j) { +// if (j == 0) return unexpected("divide by zero"); +// else return i / j; +// } +// +// void test() { +// auto q = safe_divide(10, 0); +// if (q.ok()) { printf("%f\n", q.value()); } +// else { printf("%s\n", q.error().c_str()); } +// } +// +// Once the Android platform has moved to C++23, this will be removed and +// android::base::expected will be type aliased to std::expected. +// + +namespace android { +namespace base { + +// Synopsis +template +class expected; + +template +class unexpected; +template +unexpected(E) -> unexpected; + +template +class bad_expected_access; + +template <> +class bad_expected_access; + +struct unexpect_t { + explicit unexpect_t() = default; +}; +inline constexpr unexpect_t unexpect{}; + +// macros for SFINAE +#define _ENABLE_IF(...) \ + , std::enable_if_t<(__VA_ARGS__)>* = nullptr + +// Define NODISCARD_EXPECTED to prevent expected from being +// ignored when used as a return value. This is off by default. +#ifdef NODISCARD_EXPECTED +#define _NODISCARD_ [[nodiscard]] +#else +#define _NODISCARD_ +#endif + +#define _EXPLICIT(cond) \ + _Pragma("clang diagnostic push") \ + _Pragma("clang diagnostic ignored \"-Wc++20-extensions\"") explicit(cond) \ + _Pragma("clang diagnostic pop") + +#define _COMMA , + +namespace expected_internal { + +template +struct remove_cvref { + using type = std::remove_cv_t>; +}; + +template +using remove_cvref_t = typename remove_cvref::type; + +// Can T be constructed from W (or W converted to T)? W can be lvalue or rvalue, +// const or not. +template +inline constexpr bool converts_from_any_cvref = + std::disjunction_v, std::is_convertible, + std::is_constructible, std::is_convertible, + std::is_constructible, std::is_convertible, + std::is_constructible, std::is_convertible>; + +template +struct is_expected : std::false_type {}; + +template +struct is_expected> : std::true_type {}; + +template +inline constexpr bool is_expected_v = is_expected::value; + +template +struct is_unexpected : std::false_type {}; + +template +struct is_unexpected> : std::true_type {}; + +template +inline constexpr bool is_unexpected_v = is_unexpected::value; + +// Constraints on constructing an expected from an expected +// related to T and U. UF is either "const U&" or "U". +template +inline constexpr bool convert_value_constraints = + std::is_constructible_v && + (std::is_same_v, bool> || !converts_from_any_cvref>); + +// Constraints on constructing an expected<..., E> from an expected +// related to E, G, and expected. GF is either "const G&" or "G". +template +inline constexpr bool convert_error_constraints = + std::is_constructible_v && + !std::is_constructible_v, expected&> && + !std::is_constructible_v, expected> && + !std::is_constructible_v, const expected&> && + !std::is_constructible_v, const expected>; + +// If an exception is thrown in expected::operator=, while changing the expected +// object between a value and an error, the expected object is supposed to +// retain its original value, which is only possible if certain constructors +// are noexcept. This implementation doesn't try to be exception-safe, but +// enforce these constraints anyway because std::expected also will enforce +// them, and we intend to switch to it eventually. +template +inline constexpr bool eh_assign_constraints = + std::is_nothrow_constructible_v || + std::is_nothrow_move_constructible_v || + std::is_nothrow_move_constructible_v; + +// Implement expected<..., E>::expected([const] unexpected [&/&&]). +#define _CONSTRUCT_EXPECTED_FROM_UNEXPECTED(GF, ParamType, forward_func) \ + template )> \ + constexpr _EXPLICIT((!std::is_convertible_v)) \ + expected(ParamType e) noexcept(std::is_nothrow_constructible_v) \ + : var_(std::in_place_index<1>, forward_func(e.error())) {} + +// Implement expected<..., E>::operator=([const] unexpected [&/&&]). +#define _ASSIGN_UNEXPECTED_TO_EXPECTED(GF, ParamType, forward_func, extra_constraints) \ + template && \ + std::is_assignable_v) && \ + extra_constraints> \ + constexpr expected& operator=(ParamType e) noexcept(std::is_nothrow_constructible_v && \ + std::is_nothrow_assignable_v) { \ + if (has_value()) { \ + var_.template emplace<1>(forward_func(e.error())); \ + } else { \ + error() = forward_func(e.error()); \ + } \ + return *this; \ + } + +} // namespace expected_internal + +// Class expected +template +class _NODISCARD_ expected { + static_assert(std::is_object_v && !std::is_array_v && + !std::is_same_v, std::in_place_t> && + !std::is_same_v, unexpect_t> && + !expected_internal::is_unexpected_v>, + "expected value type cannot be a reference, a function, an array, in_place_t, " + "unexpect_t, or unexpected"); + + public: + using value_type = T; + using error_type = E; + using unexpected_type = unexpected; + + template + using rebind = expected; + + // Delegate simple operations to the underlying std::variant. std::variant + // doesn't set noexcept well, at least for copy ctor/assign, so set it + // explicitly. Technically the copy/move assignment operators should also be + // deleted if neither T nor E satisfies is_nothrow_move_constructible_v, but + // that would require making these operator= methods into template functions. + constexpr expected() = default; + constexpr expected(const expected& rhs) noexcept( + std::is_nothrow_copy_constructible_v && std::is_nothrow_copy_constructible_v) = default; + constexpr expected(expected&& rhs) noexcept(std::is_nothrow_move_constructible_v && + std::is_nothrow_move_constructible_v) = default; + constexpr expected& operator=(const expected& rhs) noexcept( + std::is_nothrow_copy_constructible_v && std::is_nothrow_copy_assignable_v && + std::is_nothrow_copy_constructible_v && std::is_nothrow_copy_assignable_v) = default; + constexpr expected& operator=(expected&& rhs) noexcept( + std::is_nothrow_move_constructible_v && std::is_nothrow_move_assignable_v && + std::is_nothrow_move_constructible_v && std::is_nothrow_move_assignable_v) = default; + + // Construct this expected from a different expected type. +#define _CONVERTING_CTOR(UF, GF, ParamType, forward_func) \ + template && \ + expected_internal::convert_error_constraints)> \ + constexpr _EXPLICIT((!std::is_convertible_v || !std::is_convertible_v)) \ + expected(ParamType rhs) noexcept(std::is_nothrow_constructible_v && \ + std::is_nothrow_constructible_v) \ + : var_(rhs.has_value() ? variant_type(std::in_place_index<0>, forward_func(rhs.value())) \ + : variant_type(std::in_place_index<1>, forward_func(rhs.error()))) {} + + // NOLINTNEXTLINE(google-explicit-constructor) + _CONVERTING_CTOR(const U&, const G&, const expected&, ) + // NOLINTNEXTLINE(google-explicit-constructor) + _CONVERTING_CTOR(U, G, expected&&, std::move) + +#undef _CONVERTING_CTOR + + // Construct from (converted) success value, using a forwarding reference. + template , std::in_place_t> && + !std::is_same_v, expected> && + !expected_internal::is_unexpected_v> && + std::is_constructible_v && + (!std::is_same_v, bool> || + !expected_internal::is_expected_v>))> + constexpr _EXPLICIT((!std::is_convertible_v)) + // NOLINTNEXTLINE(google-explicit-constructor) + expected(U&& v) noexcept(std::is_nothrow_constructible_v) + : var_(std::in_place_index<0>, std::forward(v)) {} + + // NOLINTNEXTLINE(google-explicit-constructor) + _CONSTRUCT_EXPECTED_FROM_UNEXPECTED(const G&, const unexpected&, ) + // NOLINTNEXTLINE(google-explicit-constructor) + _CONSTRUCT_EXPECTED_FROM_UNEXPECTED(G, unexpected&&, std::move) + + // in_place_t construction + template )> + constexpr explicit expected(std::in_place_t, Args&&... args) + noexcept(std::is_nothrow_constructible_v) + : var_(std::in_place_index<0>, std::forward(args)...) {} + + // in_place_t with initializer_list construction + template &, Args...>)> + constexpr explicit expected(std::in_place_t, std::initializer_list il, Args&&... args) + noexcept(std::is_nothrow_constructible_v&, Args...>) + : var_(std::in_place_index<0>, il, std::forward(args)...) {} + + // unexpect_t construction + template )> + constexpr explicit expected(unexpect_t, Args&&... args) + noexcept(std::is_nothrow_constructible_v) + : var_(std::in_place_index<1>, unexpected_type(std::forward(args)...)) {} + + // unexpect_t with initializer_list construction + template &, Args...>)> + constexpr explicit expected(unexpect_t, std::initializer_list il, Args&&... args) + noexcept(std::is_nothrow_constructible_v&, Args...>) + : var_(std::in_place_index<1>, unexpected_type(il, std::forward(args)...)) {} + + // Assignment from (converted) success value, using a forwarding reference. + template > && + !expected_internal::is_unexpected_v> && + std::is_constructible_v && std::is_assignable_v && + expected_internal::eh_assign_constraints)> + constexpr expected& operator=(U&& v) noexcept(std::is_nothrow_constructible_v && + std::is_nothrow_assignable_v) { + if (has_value()) { + value() = std::forward(v); + } else { + var_.template emplace<0>(std::forward(v)); + } + return *this; + } + + _ASSIGN_UNEXPECTED_TO_EXPECTED(const G&, const unexpected&, , + (expected_internal::eh_assign_constraints)) + _ASSIGN_UNEXPECTED_TO_EXPECTED(G, unexpected&&, std::move, + (expected_internal::eh_assign_constraints)) + + // modifiers + template )> + constexpr T& emplace(Args&&... args) noexcept { + var_.template emplace<0>(std::forward(args)...); + return value(); + } + + template &, Args...>)> + constexpr T& emplace(std::initializer_list il, Args&&... args) noexcept { + var_.template emplace<0>(il, std::forward(args)...); + return value(); + } + + // Swap. This function takes a template argument so that _ENABLE_IF works. + template && + std::is_swappable_v && std::is_swappable_v && + std::is_move_constructible_v && std::is_move_constructible_v && + (std::is_nothrow_move_constructible_v || + std::is_nothrow_move_constructible_v))> + constexpr void swap(expected& rhs) noexcept(std::is_nothrow_move_constructible_v && + std::is_nothrow_swappable_v && + std::is_nothrow_move_constructible_v && + std::is_nothrow_swappable_v) { + var_.swap(rhs.var_); + } + + // observers + constexpr const T* operator->() const { return std::addressof(value()); } + constexpr T* operator->() { return std::addressof(value()); } + constexpr const T& operator*() const& { return value(); } + constexpr T& operator*() & { return value(); } + constexpr const T&& operator*() const&& { return std::move(std::get(var_)); } + constexpr T&& operator*() && { return std::move(std::get(var_)); } + + constexpr bool has_value() const noexcept { return var_.index() == 0; } + constexpr bool ok() const noexcept { return has_value(); } + constexpr explicit operator bool() const noexcept { return has_value(); } + + constexpr const T& value() const& { return std::get(var_); } + constexpr T& value() & { return std::get(var_); } + constexpr const T&& value() const&& { return std::move(std::get(var_)); } + constexpr T&& value() && { return std::move(std::get(var_)); } + + constexpr const E& error() const& { return std::get(var_).error(); } + constexpr E& error() & { return std::get(var_).error(); } + constexpr const E&& error() const&& { return std::move(std::get(var_)).error(); } + constexpr E&& error() && { return std::move(std::get(var_)).error(); } + + template && + std::is_convertible_v + )> + constexpr T value_or(U&& v) const& { + if (has_value()) return value(); + else return static_cast(std::forward(v)); + } + + template && + std::is_convertible_v + )> + constexpr T value_or(U&& v) && { + if (has_value()) return std::move(value()); + else return static_cast(std::forward(v)); + } + + // expected equality operators + template + friend constexpr bool operator==(const expected& x, const expected& y); + template + friend constexpr bool operator!=(const expected& x, const expected& y); + + // Comparison with unexpected + template + friend constexpr bool operator==(const expected&, const unexpected&); + template + friend constexpr bool operator==(const unexpected&, const expected&); + template + friend constexpr bool operator!=(const expected&, const unexpected&); + template + friend constexpr bool operator!=(const unexpected&, const expected&); + + private: + using variant_type = std::variant; + variant_type var_; +}; + +template +constexpr bool operator==(const expected& x, const expected& y) { + if (x.has_value() != y.has_value()) return false; + if (!x.has_value()) return x.error() == y.error(); + return *x == *y; +} + +template +constexpr bool operator!=(const expected& x, const expected& y) { + return !(x == y); +} + +// Comparison with unexpected +template +constexpr bool operator==(const expected& x, const unexpected& y) { + return !x.has_value() && (x.error() == y.error()); +} +template +constexpr bool operator==(const unexpected& x, const expected& y) { + return !y.has_value() && (x.error() == y.error()); +} +template +constexpr bool operator!=(const expected& x, const unexpected& y) { + return x.has_value() || (x.error() != y.error()); +} +template +constexpr bool operator!=(const unexpected& x, const expected& y) { + return y.has_value() || (x.error() != y.error()); +} + +template +class _NODISCARD_ expected { + public: + using value_type = void; + using error_type = E; + using unexpected_type = unexpected; + + template + using rebind = expected; + + // Delegate simple operations to the underlying std::variant. + constexpr expected() = default; + constexpr expected(const expected& rhs) noexcept(std::is_nothrow_copy_constructible_v) = + default; + constexpr expected(expected&& rhs) noexcept(std::is_nothrow_move_constructible_v) = default; + constexpr expected& operator=(const expected& rhs) noexcept( + std::is_nothrow_copy_constructible_v && std::is_nothrow_copy_assignable_v) = default; + constexpr expected& operator=(expected&& rhs) noexcept( + std::is_nothrow_move_constructible_v && std::is_nothrow_move_assignable_v) = default; + + // Construct this expected from a different expected type. +#define _CONVERTING_CTOR(GF, ParamType, forward_func) \ + template && \ + expected_internal::convert_error_constraints)> \ + constexpr _EXPLICIT((!std::is_convertible_v)) \ + expected(ParamType rhs) noexcept(std::is_nothrow_constructible_v) \ + : var_(rhs.has_value() ? variant_type(std::in_place_index<0>, std::monostate()) \ + : variant_type(std::in_place_index<1>, forward_func(rhs.error()))) {} + + // NOLINTNEXTLINE(google-explicit-constructor) + _CONVERTING_CTOR(const G&, const expected&, ) + // NOLINTNEXTLINE(google-explicit-constructor) + _CONVERTING_CTOR(G, expected&&, std::move) + +#undef _CONVERTING_CTOR + + // NOLINTNEXTLINE(google-explicit-constructor) + _CONSTRUCT_EXPECTED_FROM_UNEXPECTED(const G&, const unexpected&, ) + // NOLINTNEXTLINE(google-explicit-constructor) + _CONSTRUCT_EXPECTED_FROM_UNEXPECTED(G, unexpected&&, std::move) + + // in_place_t construction + constexpr explicit expected(std::in_place_t) noexcept {} + + // unexpect_t construction + template )> + constexpr explicit expected(unexpect_t, Args&&... args) + noexcept(std::is_nothrow_constructible_v) + : var_(std::in_place_index<1>, unexpected_type(std::forward(args)...)) {} + + // unexpect_t with initializer_list construction + template &, Args...>)> + constexpr explicit expected(unexpect_t, std::initializer_list il, Args&&... args) + noexcept(std::is_nothrow_constructible_v&, Args...>) + : var_(std::in_place_index<1>, unexpected_type(il, std::forward(args)...)) {} + + _ASSIGN_UNEXPECTED_TO_EXPECTED(const G&, const unexpected&, , true) + _ASSIGN_UNEXPECTED_TO_EXPECTED(G, unexpected&&, std::move, true) + + // modifiers + constexpr void emplace() noexcept { var_.template emplace<0>(std::monostate()); } + + // Swap. This function takes a template argument so that _ENABLE_IF works. + template && + std::is_swappable_v && std::is_move_constructible_v)> + constexpr void swap(expected& rhs) noexcept(std::is_nothrow_move_constructible_v && + std::is_nothrow_swappable_v) { + var_.swap(rhs.var_); + } + + // observers + constexpr bool has_value() const noexcept { return var_.index() == 0; } + constexpr bool ok() const noexcept { return has_value(); } + constexpr explicit operator bool() const noexcept { return has_value(); } + + constexpr void value() const& { if (!has_value()) std::get<0>(var_); } + + constexpr const E& error() const& { return std::get<1>(var_).error(); } + constexpr E& error() & { return std::get<1>(var_).error(); } + constexpr const E&& error() const&& { return std::move(std::get<1>(var_)).error(); } + constexpr E&& error() && { return std::move(std::get<1>(var_)).error(); } + + // expected equality operators + template + friend constexpr bool operator==(const expected& x, const expected& y); + + private: + using variant_type = std::variant; + variant_type var_; +}; + +template +constexpr bool operator==(const expected& x, const expected& y) { + if (x.has_value() != y.has_value()) return false; + if (!x.has_value()) return x.error() == y.error(); + return true; +} + +template +constexpr bool operator==(const expected& x, const expected& y) { + if (x.has_value() != y.has_value()) return false; + if (!x.has_value()) return x.error() == y.error(); + return false; +} + +template +constexpr bool operator==(const expected& x, const expected& y) { + if (x.has_value() != y.has_value()) return false; + if (!x.has_value()) return x.error() == y.error(); + return false; +} + +template &>().swap(std::declval&>()))> +constexpr void swap(expected& x, expected& y) noexcept(noexcept(x.swap(y))) { + x.swap(y); +} + +template +class unexpected { + static_assert(std::is_object_v && !std::is_array_v && !std::is_const_v && + !std::is_volatile_v && !expected_internal::is_unexpected_v, + "unexpected error type cannot be a reference, a function, an array, cv-qualified, " + "or unexpected"); + + public: + // constructors + constexpr unexpected(const unexpected&) = default; + constexpr unexpected(unexpected&&) = default; + + template , unexpected> && + !std::is_same_v, std::in_place_t> && + std::is_constructible_v)> + constexpr explicit unexpected(Err&& e) noexcept(std::is_nothrow_constructible_v) + : val_(std::forward(e)) {} + + template )> + constexpr explicit unexpected(std::in_place_t, Args&&... args) + noexcept(std::is_nothrow_constructible_v) + : val_(std::forward(args)...) {} + + template &, Args...>)> + constexpr explicit unexpected(std::in_place_t, std::initializer_list il, Args&&... args) + noexcept(std::is_nothrow_constructible_v&, Args...>) + : val_(il, std::forward(args)...) {} + + constexpr unexpected& operator=(const unexpected&) = default; + constexpr unexpected& operator=(unexpected&&) = default; + + // observer + constexpr const E& error() const& noexcept { return val_; } + constexpr E& error() & noexcept { return val_; } + constexpr const E&& error() const&& noexcept { return std::move(val_); } + constexpr E&& error() && noexcept { return std::move(val_); } + + // Swap. This function takes a template argument so that _ENABLE_IF works. + template && std::is_swappable_v)> + void swap(unexpected& other) noexcept(std::is_nothrow_swappable_v) { + // Make std::swap visible to provide swap for STL and builtin types, but use + // an unqualified swap to invoke argument-dependent lookup to find the swap + // functions for user-declared types. + using std::swap; + swap(val_, other.val_); + } + + template + friend constexpr bool + operator==(const unexpected& e1, const unexpected& e2); + template + friend constexpr bool + operator!=(const unexpected& e1, const unexpected& e2); + + private: + E val_; +}; + +template +constexpr bool +operator==(const unexpected& e1, const unexpected& e2) { + return e1.error() == e2.error(); +} + +template +constexpr bool +operator!=(const unexpected& e1, const unexpected& e2) { + return e1.error() != e2.error(); +} + +template )> +void swap(unexpected& x, unexpected& y) noexcept(noexcept(x.swap(y))) { + x.swap(y); +} + +// TODO: bad_expected_access class + +#undef _ENABLE_IF +#undef _NODISCARD_ +#undef _EXPLICIT +#undef _COMMA +#undef _CONSTRUCT_EXPECTED_FROM_UNEXPECTED +#undef _ASSIGN_UNEXPECTED_TO_EXPECTED + +} // namespace base +} // namespace android diff --git a/sysbridge/src/main/cpp/android/libbase/result.cpp b/sysbridge/src/main/cpp/android/libbase/result.cpp new file mode 100644 index 0000000000..c7163f270b --- /dev/null +++ b/sysbridge/src/main/cpp/android/libbase/result.cpp @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "result.h" + +namespace android { +namespace base { + +ResultError MakeResultErrorWithCode(std::string&& message, Errno code) { + return ResultError(std::move(message) + ": " + code.print(), code); +} + +} // namespace base +} // namespace android diff --git a/sysbridge/src/main/cpp/android/libbase/result.h b/sysbridge/src/main/cpp/android/libbase/result.h new file mode 100644 index 0000000000..68c93344f8 --- /dev/null +++ b/sysbridge/src/main/cpp/android/libbase/result.h @@ -0,0 +1,527 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Result is the type that is used to pass a success value of type T or an error code of type +// E, optionally together with an error message. T and E can be any type. If E is omitted it +// defaults to int, which is useful when errno(3) is used as the error code. +// +// Passing a success value or an error value: +// +// Result readFile() { +// std::string content; +// if (base::ReadFileToString("path", &content)) { +// return content; // ok case +// } else { +// return ErrnoError() << "failed to read"; // error case +// } +// } +// +// Checking the result and then unwrapping the value or propagating the error: +// +// Result hasAWord() { +// auto content = readFile(); +// if (!content.ok()) { +// return Error() << "failed to process: " << content.error(); +// } +// return (*content.find("happy") != std::string::npos); +// } +// +// Using custom error code type: +// +// enum class MyError { A, B }; // assume that this is the error code you already have +// +// // To use the error code with Result, define a wrapper class that provides the following +// operations and use the wrapper class as the second type parameter (E) when instantiating +// Result +// +// 1. default constructor +// 2. copy constructor / and move constructor if copying is expensive +// 3. conversion operator to the error code type +// 4. value() function that return the error code value +// 5. print() function that gives a string representation of the error ode value +// +// struct MyErrorWrapper { +// MyError val_; +// MyErrorWrapper() : val_(/* reasonable default value */) {} +// MyErrorWrapper(MyError&& e) : val_(std:forward(e)) {} +// operator const MyError&() const { return val_; } +// MyError value() const { return val_; } +// std::string print() const { +// switch(val_) { +// MyError::A: return "A"; +// MyError::B: return "B"; +// } +// } +// }; +// +// #define NewMyError(e) Error(MyError::e) +// +// Result val = NewMyError(A) << "some message"; +// +// Formatting the error message using fmtlib: +// +// Errorf("{} errors", num); // equivalent to Error() << num << " errors"; +// ErrnoErrorf("{} errors", num); // equivalent to ErrnoError() << num << " errors"; +// +// Returning success or failure, but not the value: +// +// Result doSomething() { +// if (success) return {}; +// else return Error() << "error occurred"; +// } +// +// Extracting error code: +// +// Result val = Error(3) << "some error occurred"; +// assert(3 == val.error().code()); +// + +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + +#include "errors.h" +#include "expected.h" + +namespace android { + namespace base { + +// Errno is a wrapper class for errno(3). Use this type instead of `int` when instantiating +// `Result` and `Error` template classes. This is required to distinguish errno from other +// integer-based error code types like `status_t`. + struct Errno { + Errno() : val_(0) {} + + Errno(int e) : val_(e) {} + + int value() const { return val_; } + + operator int() const { return value(); } + + const char *print() const { return strerror(value()); } + + int val_; + + // TODO(b/209929099): remove this conversion operator. This currently is needed to not break + // existing places where error().code() is used to construct enum values. + template>> + operator E() const { + return E(val_); + } + }; + + static_assert(std::is_trivially_copyable_v == true); + + template + struct ResultError { + template>> + ResultError(T &&message, P &&code) + : message_(std::forward(message)), code_(E(std::forward

(code))) {} + + ResultError(const ResultError &other) = default; + + ResultError(ResultError &&other) = default; + + ResultError &operator=(const ResultError &other) = default; + + ResultError &operator=(ResultError &&other) = default; + + template + // NOLINTNEXTLINE(google-explicit-constructor) + operator android::base::expected>() &&{ + return android::base::unexpected(std::move(*this)); + } + + template + // NOLINTNEXTLINE(google-explicit-constructor) + operator android::base::expected>() const &{ + return android::base::unexpected(*this); + } + + const std::string &message() const { return message_; } + + const E &code() const { return code_; } + + private: + std::string message_; + E code_; + }; + + template + auto format_as(ResultError error) { + return error.message(); + } + + template + struct ResultError { + template>> + ResultError(P &&code) : code_(E(std::forward

(code))) {} + + template + operator android::base::expected>() const { + return android::base::unexpected(ResultError(code_)); + } + + const E &code() const { return code_; } + + private: + E code_; + }; + + template + inline bool operator==(const ResultError &lhs, const ResultError &rhs) { + return lhs.message() == rhs.message() && lhs.code() == rhs.code(); + } + + template + inline bool operator!=(const ResultError &lhs, const ResultError &rhs) { + return !(lhs == rhs); + } + + template + inline std::ostream &operator<<(std::ostream &os, const ResultError &t) { + os << t.message(); + return os; + } + + namespace internal { +// Stream class that does nothing and is has zero (actually 1) size. It is used instead of +// std::stringstream when include_message is false so that we use less on stack. +// sizeof(std::stringstream) is 280 on arm64. + struct DoNothingStream { + template + DoNothingStream &operator<<(T &&) { + return *this; + } + + std::string str() const { return ""; } + }; + } // namespace internal + + template>> + class Error { + public: + Error() : code_(0), has_code_(false) {} + + template>> + // NOLINTNEXTLINE(google-explicit-constructor) + Error(P &&code) : code_(std::forward

(code)), has_code_(true) {} + + template>> + // NOLINTNEXTLINE(google-explicit-constructor) + operator android::base::expected>() const { + return android::base::unexpected(ResultError

(str(), static_cast

(code_))); + } + + template>> + // NOLINTNEXTLINE(google-explicit-constructor) + operator android::base::expected>() const { + return android::base::unexpected(ResultError(static_cast

(code_))); + } + + template + Error &operator<<(T &&t) { + static_assert(include_message, "<< not supported when include_message = false"); + // NOLINTNEXTLINE(bugprone-suspicious-semicolon) + if constexpr (std::is_same_v>, ResultError>) { + if (!has_code_) { + code_ = t.code(); + } + return (*this) << t.message(); + } + int saved = errno; + ss_ << t; + errno = saved; + return *this; + } + + const std::string str() const { + static_assert(include_message, "str() not supported when include_message = false"); + std::string str = ss_.str(); + if (has_code_) { + if (str.empty()) { + return code_.print(); + } + return std::move(str) + ": " + code_.print(); + } + return str; + } + + Error(const Error &) = delete; + + Error(Error &&) = delete; + + Error &operator=(const Error &) = delete; + + Error &operator=(Error &&) = delete; + + template + friend Error ErrorfImpl(const std::string &fmt, const Args &... args); + + template + friend Error ErrnoErrorfImpl(const std::string &fmt, const Args &... args); + + private: + Error(bool has_code, E code, const std::string &message) : code_(code), + has_code_(has_code) { + (*this) << message; + } + + std::conditional_t ss_; + E code_; + const bool has_code_; + }; + + inline Error ErrnoError() { + return Error(Errno{errno}); + } + + template + inline E ErrorCode(E code) { + return code; + } + +// Return the error code of the last ResultError object, if any. +// Otherwise, return `code` as it is. + template + inline E ErrorCode(E code, T &&t, const Args &... args) { + if constexpr (std::is_same_v>, ResultError>) { + return ErrorCode(t.code(), args...); + } + return ErrorCode(code, args...); + } + + __attribute__((noinline)) ResultError MakeResultErrorWithCode(std::string &&message, + Errno code); + + template + inline ResultError ErrorfImpl(const std::string &fmt, const Args &... args) { + return ResultError(fmt, ErrorCode(Errno{}, args...)); + } + + template + inline ResultError ErrnoErrorfImpl(const std::string &fmt, const Args &... args) { + Errno code{errno}; + return MakeResultErrorWithCode(std::string(fmt), code); + } + +#define Errorf(fmt, ...) android::base::ErrorfImpl(fmt, ##__VA_ARGS__) +#define ErrnoErrorf(fmt, ...) android::base::ErrnoErrorfImpl(fmt, ##__VA_ARGS__) + + template + using Result = android::base::expected>; + +// Specialization of android::base::OkOrFail for V = Result. See android-base/errors.h +// for the contract. + + namespace impl { + template + using Code = std::decay_t().error().code())>; + + template + using ErrorType = std::decay_t().error())>; + + template + constexpr bool IsNumeric = std::is_integral_v || std::is_floating_point_v || + (std::is_enum_v && std::is_convertible_v); + +// This base class exists to take advantage of shadowing +// We include the conversion in this base class so that if the conversion in NumericConversions +// overlaps, we (arbitrarily) choose the implementation in NumericConversions due to shadowing. + template + struct ConversionBase { + ErrorType error_; + + // T is a expected>. + operator T() const & { return unexpected(error_); } + + operator T() && { return unexpected(std::move(error_)); } + + operator Code() const { return error_.code(); } + }; + +// User defined conversions can be followed by numeric conversions +// Although we template specialize for the exact code type, we need +// specializations for conversions to all numeric types to avoid an +// ambiguous conversion sequence. + template + struct NumericConversions : public ConversionBase { + }; + + template + struct NumericConversions>> + > : public ConversionBase { +#pragma push_macro("SPECIALIZED_CONVERSION") +#define SPECIALIZED_CONVERSION(type) \ + operator expected>() const& { return unexpected(this->error_); } \ + operator expected>()&& { return unexpected(std::move(this->error_)); } + + SPECIALIZED_CONVERSION(int) + + SPECIALIZED_CONVERSION(short int) + + SPECIALIZED_CONVERSION(unsigned short int) + + SPECIALIZED_CONVERSION(unsigned int) + + SPECIALIZED_CONVERSION(long int) + + SPECIALIZED_CONVERSION(unsigned long int) + + SPECIALIZED_CONVERSION(long long int) + + SPECIALIZED_CONVERSION(unsigned long long int) + + SPECIALIZED_CONVERSION(bool) + + SPECIALIZED_CONVERSION(char) + + SPECIALIZED_CONVERSION(unsigned char) + + SPECIALIZED_CONVERSION(signed char) + + SPECIALIZED_CONVERSION(wchar_t) + + SPECIALIZED_CONVERSION(char16_t) + + SPECIALIZED_CONVERSION(char32_t) + + SPECIALIZED_CONVERSION(float) + + SPECIALIZED_CONVERSION(double) + + SPECIALIZED_CONVERSION(long double) + +#undef SPECIALIZED_CONVERSION +#pragma pop_macro("SPECIALIZED_CONVERSION") + // For debugging purposes + using IsNumericT = std::true_type; + }; + +#ifdef __cpp_concepts + template +// Define a concept which **any** type matches to + concept Universal = std::is_same_v; +#endif + +// A type that is never used. + struct Never { + }; + } // namespace impl + + template + struct OkOrFail> + : public impl::NumericConversions> { + using V = Result; + using Err = impl::ErrorType; + using C = impl::Code; + private: + OkOrFail(Err &&v) : impl::NumericConversions{std::move(v)} {} + + OkOrFail(const OkOrFail &other) = delete; + + OkOrFail(const OkOrFail &&other) = delete; + + public: + // Checks if V is ok or fail + static bool IsOk(const V &val) { return val.ok(); } + + // Turns V into a success value + static T Unwrap(V &&val) { + if constexpr (std::is_same_v) { + assert(IsOk(val)); + return; + } else { + return std::move(val.value()); + } + } + + // Consumes V when it's a fail value + static OkOrFail Fail(V &&v) { + assert(!IsOk(v)); + return OkOrFail{std::move(v.error())}; + } + + // We specialize as much as possible to avoid ambiguous conversion with templated expected ctor. + // We don't need this specialization if `C` is numeric because that case is already covered by + // `NumericConversions`. + operator Result, impl::Never, C>, E, include_message>() + const & { + return unexpected(this->error_); + } + + operator Result, impl::Never, C>, E, include_message>() && { + return unexpected(std::move(this->error_)); + } + +#ifdef __cpp_concepts + + // The idea here is to match this template method to any type (not simply trivial types). + // The reason for including a constraint is to take advantage of the fact that a constrained + // method always has strictly lower precedence than a non-constrained method in template + // specialization rules (thus avoiding ambiguity). So we use a universally matching constraint to + // mark this function as less preferable (but still accepting of all types). + template + operator Result() const &{ + return unexpected(this->error_); + } + + template + operator Result() &&{ + return unexpected(std::move(this->error_)); + } + +#else + template + operator Result() const& { + return unexpected(this->error_); + } + template + operator Result() && { + return unexpected(std::move(this->error_)); + } +#endif + + static const std::string &ErrorMessage(const V &val) { return val.error().message(); } + }; + +// Macros for testing the results of functions that return android::base::Result. These also work +// with base::android::expected. They assume the user depends on libgmock and includes +// gtest/gtest.h. For advanced matchers and customized error messages, see result-gmock.h. + +#define ASSERT_RESULT_OK(stmt) \ + if (const auto& tmp = (stmt); !tmp.ok()) \ + FAIL() << "Value of: " << #stmt << "\n" \ + << " Actual: " << tmp.error().message() << "\n" \ + << "Expected: is ok\n" + +#define EXPECT_RESULT_OK(stmt) \ + if (const auto& tmp = (stmt); !tmp.ok()) \ + ADD_FAILURE() << "Value of: " << #stmt << "\n" \ + << " Actual: " << tmp.error().message() << "\n" \ + << "Expected: is ok\n" + + } // namespace base +} // namespace android diff --git a/sysbridge/src/main/cpp/android/libbase/stringprintf.cpp b/sysbridge/src/main/cpp/android/libbase/stringprintf.cpp new file mode 100644 index 0000000000..d9eb0e501e --- /dev/null +++ b/sysbridge/src/main/cpp/android/libbase/stringprintf.cpp @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "stringprintf.h" + +#include + +#include + +namespace android { + namespace base { + + void StringAppendV(std::string *dst, const char *format, va_list ap) { + // First try with a small fixed size buffer + char space[1024] __attribute__((__uninitialized__)); + + // It's possible for methods that use a va_list to invalidate + // the data in it upon use. The fix is to make a copy + // of the structure before using it and use that copy instead. + va_list backup_ap; + va_copy(backup_ap, ap); + int result = vsnprintf(space, sizeof(space), format, backup_ap); + va_end(backup_ap); + + if (result < static_cast(sizeof(space))) { + if (result >= 0) { + // Normal case -- everything fit. + dst->append(space, result); + return; + } + + if (result < 0) { + // Just an error. + return; + } + } + + // Increase the buffer size to the size requested by vsnprintf, + // plus one for the closing \0. + int length = result + 1; + char *buf = new char[length]; + + // Restore the va_list before we use it again + va_copy(backup_ap, ap); + result = vsnprintf(buf, length, format, backup_ap); + va_end(backup_ap); + + if (result >= 0 && result < length) { + // It fit + dst->append(buf, result); + } + delete[] buf; + } + + std::string StringPrintf(const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + std::string result; + StringAppendV(&result, fmt, ap); + va_end(ap); + return result; + } + + void StringAppendF(std::string *dst, const char *format, ...) { + va_list ap; + va_start(ap, format); + StringAppendV(dst, format, ap); + va_end(ap); + } + + } // namespace base +} // namespace android diff --git a/sysbridge/src/main/cpp/android/libbase/stringprintf.h b/sysbridge/src/main/cpp/android/libbase/stringprintf.h new file mode 100644 index 0000000000..02cc100d1a --- /dev/null +++ b/sysbridge/src/main/cpp/android/libbase/stringprintf.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +namespace android { + namespace base { + +// These printf-like functions are implemented in terms of vsnprintf, so they +// use the same attribute for compile-time format string checking. + +// Returns a string corresponding to printf-like formatting of the arguments. + std::string + StringPrintf(const char *fmt, ...) __attribute__((__format__(__printf__, 1, 2))); + +// Appends a printf-like formatting of the arguments to 'dst'. + void StringAppendF(std::string *dst, const char *fmt, ...) + __attribute__((__format__(__printf__, 2, 3))); + +// Appends a printf-like formatting of the arguments to 'dst'. + void StringAppendV(std::string *dst, const char *format, va_list ap) + __attribute__((__format__(__printf__, 2, 0))); + + } // namespace base +} // namespace android diff --git a/sysbridge/src/main/cpp/android/liblog/log_main.h b/sysbridge/src/main/cpp/android/liblog/log_main.h new file mode 100644 index 0000000000..8fc5ff4e79 --- /dev/null +++ b/sysbridge/src/main/cpp/android/liblog/log_main.h @@ -0,0 +1,369 @@ +/* + * Copyright (C) 2005-2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + + +#include +#include + +#include + +__BEGIN_DECLS + +/* + * Normally we strip the effects of ALOGV (VERBOSE messages), + * LOG_FATAL and LOG_FATAL_IF (FATAL assert messages) from the + * release builds by defining NDEBUG. You can modify this (for + * example with "#define LOG_NDEBUG 0" at the top of your source + * file) to change that behavior. + */ + +#ifndef LOG_NDEBUG +#ifdef NDEBUG +#define LOG_NDEBUG 1 +#else +#define LOG_NDEBUG 0 +#endif +#endif + +/* --------------------------------------------------------------------- */ + +/* + * This file uses ", ## __VA_ARGS__" zero-argument token pasting to + * work around issues with debug-only syntax errors in assertions + * that are missing format strings. See commit + * 19299904343daf191267564fe32e6cd5c165cd42 + */ +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wgnu-zero-variadic-macro-arguments" +#endif + +/* + * Use __VA_ARGS__ if running a static analyzer, + * to avoid warnings of unused variables in __VA_ARGS__. + * Use constexpr function in C++ mode, so these macros can be used + * in other constexpr functions without warning. + */ +#ifdef __clang_analyzer__ +#ifdef __cplusplus +extern "C++" { +template +constexpr int __fake_use_va_args(Ts...) { + return 0; +} +} +#else +extern int __fake_use_va_args(int, ...); +#endif /* __cplusplus */ +#define __FAKE_USE_VA_ARGS(...) ((void)__fake_use_va_args(0, ##__VA_ARGS__)) +#else +#define __FAKE_USE_VA_ARGS(...) ((void)(0)) +#endif /* __clang_analyzer__ */ + +#ifndef __predict_false +#define __predict_false(exp) __builtin_expect((exp) != 0, 0) +#endif + +#define android_writeLog(prio, tag, text) __android_log_write(prio, tag, text) + +#define android_printLog(prio, tag, ...) \ + __android_log_print(prio, tag, __VA_ARGS__) + +#define android_vprintLog(prio, cond, tag, ...) \ + __android_log_vprint(prio, tag, __VA_ARGS__) + +/* + * Log macro that allows you to specify a number for the priority. + */ +#ifndef LOG_PRI +#define LOG_PRI(priority, tag, ...) android_printLog(priority, tag, __VA_ARGS__) +#endif + +/* + * Log macro that allows you to pass in a varargs ("args" is a va_list). + */ +#ifndef LOG_PRI_VA +#define LOG_PRI_VA(priority, tag, fmt, args) \ + android_vprintLog(priority, NULL, tag, fmt, args) +#endif + +/* --------------------------------------------------------------------- */ + +/* XXX Macros to work around syntax errors in places where format string + * arg is not passed to ALOG_ASSERT, LOG_ALWAYS_FATAL or LOG_ALWAYS_FATAL_IF + * (happens only in debug builds). + */ + +/* Returns 2nd arg. Used to substitute default value if caller's vararg list + * is empty. + */ +#define __android_second(dummy, second, ...) second + +/* If passed multiple args, returns ',' followed by all but 1st arg, otherwise + * returns nothing. + */ +#define __android_rest(first, ...) , ##__VA_ARGS__ + +#define android_printAssert(cond, tag, ...) \ + __android_log_assert(cond, tag, \ + __android_second(0, ##__VA_ARGS__, NULL) \ + __android_rest(__VA_ARGS__)) + +/* + * Log a fatal error. If the given condition fails, this stops program + * execution like a normal assertion, but also generating the given message. + * It is NOT stripped from release builds. Note that the condition test + * is -inverted- from the normal assert() semantics. + */ +#ifndef LOG_ALWAYS_FATAL_IF +#define LOG_ALWAYS_FATAL_IF(cond, ...) \ + ((__predict_false(cond)) ? (__FAKE_USE_VA_ARGS(__VA_ARGS__), \ + ((void)android_printAssert(#cond, LOG_TAG, ##__VA_ARGS__))) \ + : ((void)0)) +#endif + +#ifndef LOG_ALWAYS_FATAL +#define LOG_ALWAYS_FATAL(...) \ + (((void)android_printAssert(NULL, LOG_TAG, ##__VA_ARGS__))) +#endif + +/* + * Versions of LOG_ALWAYS_FATAL_IF and LOG_ALWAYS_FATAL that + * are stripped out of release builds. + */ + +#if LOG_NDEBUG + +#ifndef LOG_FATAL_IF +#define LOG_FATAL_IF(cond, ...) __FAKE_USE_VA_ARGS(__VA_ARGS__) +#endif +#ifndef LOG_FATAL +#define LOG_FATAL(...) __FAKE_USE_VA_ARGS(__VA_ARGS__) +#endif + +#else + +#ifndef LOG_FATAL_IF +#define LOG_FATAL_IF(cond, ...) LOG_ALWAYS_FATAL_IF(cond, ##__VA_ARGS__) +#endif +#ifndef LOG_FATAL +#define LOG_FATAL(...) LOG_ALWAYS_FATAL(__VA_ARGS__) +#endif + +#endif + +/* + * Assertion that generates a log message when the assertion fails. + * Stripped out of release builds. Uses the current LOG_TAG. + */ +#ifndef ALOG_ASSERT +#define ALOG_ASSERT(cond, ...) LOG_FATAL_IF(!(cond), ##__VA_ARGS__) +#endif + +/* --------------------------------------------------------------------- */ + +/* + * C/C++ logging functions. See the logging documentation for API details. + * + * We'd like these to be available from C code (in case we import some from + * somewhere), so this has a C interface. + * + * The output will be correct when the log file is shared between multiple + * threads and/or multiple processes so long as the operating system + * supports O_APPEND. These calls have mutex-protected data structures + * and so are NOT reentrant. Do not use LOG in a signal handler. + */ + +/* --------------------------------------------------------------------- */ + +/* + * Simplified macro to send a verbose log message using the current LOG_TAG. + */ +#ifndef ALOGV +#define __ALOGV(...) ((void)ALOG(LOG_VERBOSE, LOG_TAG, __VA_ARGS__)) +#if LOG_NDEBUG +#define ALOGV(...) \ + do { \ + __FAKE_USE_VA_ARGS(__VA_ARGS__); \ + if (false) { \ + __ALOGV(__VA_ARGS__); \ + } \ + } while (false) +#else +#define ALOGV(...) __ALOGV(__VA_ARGS__) +#endif +#endif + +#ifndef ALOGV_IF +#if LOG_NDEBUG +#define ALOGV_IF(cond, ...) __FAKE_USE_VA_ARGS(__VA_ARGS__) +#else +#define ALOGV_IF(cond, ...) \ + ((__predict_false(cond)) \ + ? (__FAKE_USE_VA_ARGS(__VA_ARGS__), (void)ALOG(LOG_VERBOSE, LOG_TAG, __VA_ARGS__)) \ + : ((void)0)) +#endif +#endif + +/* + * Simplified macro to send a debug log message using the current LOG_TAG. + */ +#ifndef ALOGD +#define ALOGD(...) ((void)ALOG(LOG_DEBUG, LOG_TAG, __VA_ARGS__)) +#endif + +#ifndef ALOGD_IF +#define ALOGD_IF(cond, ...) \ + ((__predict_false(cond)) \ + ? (__FAKE_USE_VA_ARGS(__VA_ARGS__), (void)ALOG(LOG_DEBUG, LOG_TAG, __VA_ARGS__)) \ + : ((void)0)) +#endif + +/* + * Simplified macro to send an info log message using the current LOG_TAG. + */ +#ifndef ALOGI +#define ALOGI(...) ((void)ALOG(LOG_INFO, LOG_TAG, __VA_ARGS__)) +#endif + +#ifndef ALOGI_IF +#define ALOGI_IF(cond, ...) \ + ((__predict_false(cond)) \ + ? (__FAKE_USE_VA_ARGS(__VA_ARGS__), (void)ALOG(LOG_INFO, LOG_TAG, __VA_ARGS__)) \ + : ((void)0)) +#endif + +/* + * Simplified macro to send a warning log message using the current LOG_TAG. + */ +#ifndef ALOGW +#define ALOGW(...) ((void)ALOG(LOG_WARN, LOG_TAG, __VA_ARGS__)) +#endif + +#ifndef ALOGW_IF +#define ALOGW_IF(cond, ...) \ + ((__predict_false(cond)) \ + ? (__FAKE_USE_VA_ARGS(__VA_ARGS__), (void)ALOG(LOG_WARN, LOG_TAG, __VA_ARGS__)) \ + : ((void)0)) +#endif + +/* + * Simplified macro to send an error log message using the current LOG_TAG. + */ +#ifndef ALOGE +#define ALOGE(...) ((void)ALOG(LOG_ERROR, LOG_TAG, __VA_ARGS__)) +#endif + +#ifndef ALOGE_IF +#define ALOGE_IF(cond, ...) \ + ((__predict_false(cond)) \ + ? (__FAKE_USE_VA_ARGS(__VA_ARGS__), (void)ALOG(LOG_ERROR, LOG_TAG, __VA_ARGS__)) \ + : ((void)0)) +#endif + +/* --------------------------------------------------------------------- */ + +/* + * Conditional based on whether the current LOG_TAG is enabled at + * verbose priority. + */ +#ifndef IF_ALOGV +#if LOG_NDEBUG +#define IF_ALOGV() if (false) +#else +#define IF_ALOGV() IF_ALOG(LOG_VERBOSE, LOG_TAG) +#endif +#endif + +/* + * Conditional based on whether the current LOG_TAG is enabled at + * debug priority. + */ +#ifndef IF_ALOGD +#define IF_ALOGD() IF_ALOG(LOG_DEBUG, LOG_TAG) +#endif + +/* + * Conditional based on whether the current LOG_TAG is enabled at + * info priority. + */ +#ifndef IF_ALOGI +#define IF_ALOGI() IF_ALOG(LOG_INFO, LOG_TAG) +#endif + +/* + * Conditional based on whether the current LOG_TAG is enabled at + * warn priority. + */ +#ifndef IF_ALOGW +#define IF_ALOGW() IF_ALOG(LOG_WARN, LOG_TAG) +#endif + +/* + * Conditional based on whether the current LOG_TAG is enabled at + * error priority. + */ +#ifndef IF_ALOGE +#define IF_ALOGE() IF_ALOG(LOG_ERROR, LOG_TAG) +#endif + +/* --------------------------------------------------------------------- */ + +/* + * Basic log message macro. + * + * Example: + * ALOG(LOG_WARN, NULL, "Failed with error %d", errno); + * + * The second argument may be NULL or "" to indicate the "global" tag. + */ +#ifndef ALOG +#define ALOG(priority, tag, ...) LOG_PRI(ANDROID_##priority, tag, __VA_ARGS__) +#endif + +/* + * Conditional given a desired logging priority and tag. + */ +#ifndef IF_ALOG +#define IF_ALOG(priority, tag) if (android_testLog(ANDROID_##priority, tag)) +#endif + +/* --------------------------------------------------------------------- */ + +/* + * IF_ALOG uses android_testLog, but IF_ALOG can be overridden. + * android_testLog will remain constant in its purpose as a wrapper + * for Android logging filter policy, and can be subject to + * change. It can be reused by the developers that override + * IF_ALOG as a convenient means to reimplement their policy + * over Android. + */ + +#if LOG_NDEBUG /* Production */ +#define android_testLog(prio, tag) \ + (__android_log_is_loggable_len(prio, tag, (tag) ? strlen(tag) : 0, ANDROID_LOG_DEBUG) != 0) +#else +#define android_testLog(prio, tag) \ + (__android_log_is_loggable_len(prio, tag, (tag) ? strlen(tag) : 0, ANDROID_LOG_VERBOSE) != 0) +#endif + +#if defined(__clang__) +#pragma clang diagnostic pop +#endif + +__END_DECLS diff --git a/sysbridge/src/main/cpp/android/ui/LogicalDisplayId.h b/sysbridge/src/main/cpp/android/ui/LogicalDisplayId.h new file mode 100644 index 0000000000..6cb99396ad --- /dev/null +++ b/sysbridge/src/main/cpp/android/ui/LogicalDisplayId.h @@ -0,0 +1,59 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include + +namespace android::ui { + +// Type-safe wrapper for a logical display id. + struct LogicalDisplayId : ftl::Constructible, + ftl::Equatable, + ftl::Orderable { + using Constructible::Constructible; + + constexpr auto val() const { return ftl::to_underlying(*this); } + + constexpr bool isValid() const { return val() >= 0; } + + std::string toString() const { return std::to_string(val()); } + + static const LogicalDisplayId INVALID; + static const LogicalDisplayId DEFAULT; + }; + + constexpr inline LogicalDisplayId LogicalDisplayId::INVALID{-1}; + constexpr inline LogicalDisplayId LogicalDisplayId::DEFAULT{0}; + + inline std::ostream &operator<<(std::ostream &stream, LogicalDisplayId displayId) { + return stream << displayId.val(); + } + +} // namespace android::ui + +namespace std { + template<> + struct hash { + size_t operator()(const android::ui::LogicalDisplayId &displayId) const { + return hash()(displayId.val()); + } + }; +} // namespace std \ No newline at end of file diff --git a/sysbridge/src/main/cpp/android/utils/Errors.h b/sysbridge/src/main/cpp/android/utils/Errors.h new file mode 100644 index 0000000000..22fb36d250 --- /dev/null +++ b/sysbridge/src/main/cpp/android/utils/Errors.h @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +namespace android { + +/** + * The type used to return success/failure from frameworks APIs. + * See the anonymous enum below for valid values. + */ +typedef int32_t status_t; + +/* + * Error codes. + * All error codes are negative values. + */ + +enum { + OK = 0, // Preferred constant for checking success. +#ifndef NO_ERROR + // Win32 #defines NO_ERROR as well. It has the same value, so there's no + // real conflict, though it's a bit awkward. + NO_ERROR = OK, // Deprecated synonym for `OK`. Prefer `OK` because it doesn't conflict with Windows. +#endif + + UNKNOWN_ERROR = (-2147483647-1), // INT32_MIN value + + NO_MEMORY = -ENOMEM, + INVALID_OPERATION = -ENOSYS, + BAD_VALUE = -EINVAL, + BAD_TYPE = (UNKNOWN_ERROR + 1), + NAME_NOT_FOUND = -ENOENT, + PERMISSION_DENIED = -EPERM, + NO_INIT = -ENODEV, + ALREADY_EXISTS = -EEXIST, + DEAD_OBJECT = -EPIPE, + FAILED_TRANSACTION = (UNKNOWN_ERROR + 2), +#if !defined(_WIN32) + BAD_INDEX = -EOVERFLOW, + NOT_ENOUGH_DATA = -ENODATA, + WOULD_BLOCK = -EWOULDBLOCK, + TIMED_OUT = -ETIMEDOUT, + UNKNOWN_TRANSACTION = -EBADMSG, +#else + BAD_INDEX = -E2BIG, + NOT_ENOUGH_DATA = (UNKNOWN_ERROR + 3), + WOULD_BLOCK = (UNKNOWN_ERROR + 4), + TIMED_OUT = (UNKNOWN_ERROR + 5), + UNKNOWN_TRANSACTION = (UNKNOWN_ERROR + 6), +#endif + FDS_NOT_ALLOWED = (UNKNOWN_ERROR + 7), + UNEXPECTED_NULL = (UNKNOWN_ERROR + 8), +}; + +// Human readable name of error +std::string statusToString(status_t status); + +} // namespace android diff --git a/sysbridge/src/main/cpp/android/utils/FileMap.cpp b/sysbridge/src/main/cpp/android/utils/FileMap.cpp new file mode 100644 index 0000000000..21b7c65574 --- /dev/null +++ b/sysbridge/src/main/cpp/android/utils/FileMap.cpp @@ -0,0 +1,270 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// +// Shared file mapping class. +// + +#define LOG_TAG "filemap" + +#include "FileMap.h" +#include "../../logging.h" +#include + +#if defined(__MINGW32__) && !defined(__USE_MINGW_ANSI_STDIO) +# define PRId32 "I32d" +# define PRIx32 "I32x" +# define PRId64 "I64d" +#else + +#include + +#endif + +#include +#include + +#if !defined(__MINGW32__) + +#include + +#endif + +#include +#include +#include +#include +#include + +using namespace android; + +/*static*/ long FileMap::mPageSize = -1; + +// Constructor. Create an empty object. +FileMap::FileMap(void) + : mFileName(nullptr), + mBasePtr(nullptr), + mBaseLength(0), + mDataPtr(nullptr), + mDataLength(0) +#if defined(__MINGW32__) +, + mFileHandle(INVALID_HANDLE_VALUE), + mFileMapping(NULL) +#endif +{ +} + +// Move Constructor. +FileMap::FileMap(FileMap &&other) noexcept + : mFileName(other.mFileName), + mBasePtr(other.mBasePtr), + mBaseLength(other.mBaseLength), + mDataOffset(other.mDataOffset), + mDataPtr(other.mDataPtr), + mDataLength(other.mDataLength) +#if defined(__MINGW32__) +, + mFileHandle(other.mFileHandle), + mFileMapping(other.mFileMapping) +#endif +{ + other.mFileName = nullptr; + other.mBasePtr = nullptr; + other.mDataPtr = nullptr; +#if defined(__MINGW32__) + other.mFileHandle = INVALID_HANDLE_VALUE; + other.mFileMapping = NULL; +#endif +} + +// Move assign operator. +FileMap &FileMap::operator=(FileMap &&other) noexcept { + mFileName = other.mFileName; + mBasePtr = other.mBasePtr; + mBaseLength = other.mBaseLength; + mDataOffset = other.mDataOffset; + mDataPtr = other.mDataPtr; + mDataLength = other.mDataLength; + other.mFileName = nullptr; + other.mBasePtr = nullptr; + other.mDataPtr = nullptr; +#if defined(__MINGW32__) + mFileHandle = other.mFileHandle; + mFileMapping = other.mFileMapping; + other.mFileHandle = INVALID_HANDLE_VALUE; + other.mFileMapping = NULL; +#endif + return *this; +} + +// Destructor. +FileMap::~FileMap(void) { + if (mFileName != nullptr) { + free(mFileName); + } +#if defined(__MINGW32__) + if (mBasePtr && UnmapViewOfFile(mBasePtr) == 0) { + LOGD("UnmapViewOfFile(%p) failed, error = %lu\n", mBasePtr, + GetLastError() ); + } + if (mFileMapping != NULL) { + CloseHandle(mFileMapping); + } +#else + if (mBasePtr && munmap(mBasePtr, mBaseLength) != 0) { + LOGD("munmap(%p, %zu) failed\n", mBasePtr, mBaseLength); + } +#endif +} + + +// Create a new mapping on an open file. +// +// Closing the file descriptor does not unmap the pages, so we don't +// claim ownership of the fd. +// +// Returns "false" on failure. +bool FileMap::create(const char *origFileName, int fd, off64_t offset, size_t length, + bool readOnly) { +#if defined(__MINGW32__) + int adjust; + off64_t adjOffset; + size_t adjLength; + + if (mPageSize == -1) { + SYSTEM_INFO si; + + GetSystemInfo( &si ); + mPageSize = si.dwAllocationGranularity; + } + + DWORD protect = readOnly ? PAGE_READONLY : PAGE_READWRITE; + + mFileHandle = (HANDLE) _get_osfhandle(fd); + mFileMapping = CreateFileMapping( mFileHandle, NULL, protect, 0, 0, NULL); + if (mFileMapping == NULL) { + LOGE("CreateFileMapping(%p, %lx) failed with error %lu\n", + mFileHandle, protect, GetLastError() ); + return false; + } + + adjust = offset % mPageSize; + adjOffset = offset - adjust; + adjLength = length + adjust; + + mBasePtr = MapViewOfFile( mFileMapping, + readOnly ? FILE_MAP_READ : FILE_MAP_ALL_ACCESS, + 0, + (DWORD)(adjOffset), + adjLength ); + if (mBasePtr == NULL) { + LOGE("MapViewOfFile(%" PRId64 ", %zu) failed with error %lu\n", + adjOffset, adjLength, GetLastError() ); + CloseHandle(mFileMapping); + mFileMapping = NULL; + return false; + } +#else // !defined(__MINGW32__) + assert(fd >= 0); + assert(offset >= 0); + assert(length > 0); + + // init on first use + if (mPageSize == -1) { + mPageSize = sysconf(_SC_PAGESIZE); + if (mPageSize == -1) { + LOGE("could not get _SC_PAGESIZE\n"); + return false; + } + } + + int adjust = offset % mPageSize; + off64_t adjOffset = offset - adjust; + size_t adjLength; + if (__builtin_add_overflow(length, adjust, &adjLength)) { + LOGE("adjusted length overflow: length %zu adjust %d", length, adjust); + return false; + } + + int flags = MAP_SHARED; + int prot = PROT_READ; + if (!readOnly) prot |= PROT_WRITE; + + void *ptr = mmap64(nullptr, adjLength, prot, flags, fd, adjOffset); + if (ptr == MAP_FAILED) { + if (errno == EINVAL && length == 0) { + ptr = nullptr; + adjust = 0; + } else { + LOGE("mmap(%lld,%zu) failed: %s\n", (long long) adjOffset, adjLength, strerror(errno)); + return false; + } + } + mBasePtr = ptr; +#endif // !defined(__MINGW32__) + + mFileName = origFileName != nullptr ? strdup(origFileName) : nullptr; + mBaseLength = adjLength; + mDataOffset = offset; + mDataPtr = (char *) mBasePtr + adjust; + mDataLength = length; + + LOGV("MAP: base %p/%zu data %p/%zu\n", + mBasePtr, mBaseLength, mDataPtr, mDataLength); + + return true; +} + +// Provide guidance to the system. +#if !defined(_WIN32) + +int FileMap::advise(MapAdvice advice) { + int cc, sysAdvice; + + switch (advice) { + case NORMAL: + sysAdvice = MADV_NORMAL; + break; + case RANDOM: + sysAdvice = MADV_RANDOM; + break; + case SEQUENTIAL: + sysAdvice = MADV_SEQUENTIAL; + break; + case WILLNEED: + sysAdvice = MADV_WILLNEED; + break; + case DONTNEED: + sysAdvice = MADV_DONTNEED; + break; + default: + assert(false); + return -1; + } + + cc = madvise(mBasePtr, mBaseLength, sysAdvice); + if (cc != 0) + LOGW("madvise(%d) failed: %s\n", sysAdvice, strerror(errno)); + return cc; +} + +#else +int FileMap::advise(MapAdvice /* advice */) +{ + return -1; +} +#endif diff --git a/sysbridge/src/main/cpp/android/utils/FileMap.h b/sysbridge/src/main/cpp/android/utils/FileMap.h new file mode 100644 index 0000000000..4b37a5f7f4 --- /dev/null +++ b/sysbridge/src/main/cpp/android/utils/FileMap.h @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// +// Encapsulate a shared file mapping. +// +#ifndef __LIBS_FILE_MAP_H +#define __LIBS_FILE_MAP_H + +#include + +#if defined(__MINGW32__) +// Ensure that we always pull in winsock2.h before windows.h +#if defined(_WIN32) +#include +#endif +#include +#endif + +namespace android { + +/* + * This represents a memory-mapped file. It might be the entire file or + * only part of it. This requires a little bookkeeping because the mapping + * needs to be aligned on page boundaries, and in some cases we'd like to + * have multiple references to the mapped area without creating additional + * maps. + * + * This always uses MAP_SHARED. + * + * TODO: we should be able to create a new FileMap that is a subset of + * an existing FileMap and shares the underlying mapped pages. Requires + * completing the refcounting stuff and possibly introducing the notion + * of a FileMap hierarchy. + */ + class FileMap { + public: + FileMap(void); + + FileMap(FileMap &&f) noexcept; + + FileMap &operator=(FileMap &&f) noexcept; + + /* + * Create a new mapping on an open file. + * + * Closing the file descriptor does not unmap the pages, so we don't + * claim ownership of the fd. + * + * Returns "false" on failure. + */ + bool create(const char *origFileName, int fd, + off64_t offset, size_t length, bool readOnly); + + ~FileMap(void); + + /* + * Return the name of the file this map came from, if known. + */ + const char *getFileName(void) const { return mFileName; } + + /* + * Get a pointer to the piece of the file we requested. + */ + void *getDataPtr(void) const { return mDataPtr; } + + /* + * Get the length we requested. + */ + size_t getDataLength(void) const { return mDataLength; } + + /* + * Get the data offset used to create this map. + */ + off64_t getDataOffset(void) const { return mDataOffset; } + + /* + * This maps directly to madvise() values, but allows us to avoid + * including everywhere. + */ + enum MapAdvice { + NORMAL, RANDOM, SEQUENTIAL, WILLNEED, DONTNEED + }; + + /* + * Apply an madvise() call to the entire file. + * + * Returns 0 on success, -1 on failure. + */ + int advise(MapAdvice advice); + + protected: + + private: + // these are not implemented + FileMap(const FileMap &src); + + const FileMap &operator=(const FileMap &src); + + char *mFileName; // original file name, if known + void *mBasePtr; // base of mmap area; page aligned + size_t mBaseLength; // length, measured from "mBasePtr" + off64_t mDataOffset; // offset used when map was created + void *mDataPtr; // start of requested data, offset from base + size_t mDataLength; // length, measured from "mDataPtr" +#if defined(__MINGW32__) + HANDLE mFileHandle; // Win32 file handle + HANDLE mFileMapping; // Win32 file mapping handle +#endif + + static long mPageSize; + }; + +} // namespace android + +#endif // __LIBS_FILE_MAP_H diff --git a/sysbridge/src/main/cpp/android/utils/SharedBuffer.cpp b/sysbridge/src/main/cpp/android/utils/SharedBuffer.cpp new file mode 100644 index 0000000000..8ee62e3bb3 --- /dev/null +++ b/sysbridge/src/main/cpp/android/utils/SharedBuffer.cpp @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2005 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "sharedbuffer" + +#include "SharedBuffer.h" + +#include +#include + +// --------------------------------------------------------------------------- + +namespace android { + + SharedBuffer *SharedBuffer::alloc(size_t size) { + SharedBuffer *sb = static_cast(malloc(sizeof(SharedBuffer) + size)); + if (sb) { + // Should be std::atomic_init(&sb->mRefs, 1); + // But that generates a warning with some compilers. + // The following is OK on Android-supported platforms. + sb->mRefs.store(1, std::memory_order_relaxed); + sb->mSize = size; + sb->mClientMetadata = 0; + } + return sb; + } + + + void SharedBuffer::dealloc(const SharedBuffer *released) { + free(const_cast(released)); + } + + SharedBuffer *SharedBuffer::edit() const { + if (onlyOwner()) { + return const_cast(this); + } + SharedBuffer *sb = alloc(mSize); + if (sb) { + memcpy(sb->data(), data(), size()); + release(); + } + return sb; + } + + SharedBuffer *SharedBuffer::editResize(size_t newSize) const { + if (onlyOwner()) { + SharedBuffer *buf = const_cast(this); + if (buf->mSize == newSize) return buf; + buf = (SharedBuffer *) realloc(reinterpret_cast(buf), + sizeof(SharedBuffer) + newSize); + if (buf != nullptr) { + buf->mSize = newSize; + return buf; + } + } + SharedBuffer *sb = alloc(newSize); + if (sb) { + const size_t mySize = mSize; + memcpy(sb->data(), data(), newSize < mySize ? newSize : mySize); + release(); + } + return sb; + } + + SharedBuffer *SharedBuffer::attemptEdit() const { + if (onlyOwner()) { + return const_cast(this); + } + return nullptr; + } + + SharedBuffer *SharedBuffer::reset(size_t new_size) const { + // cheap-o-reset. + SharedBuffer *sb = alloc(new_size); + if (sb) { + release(); + } + return sb; + } + + void SharedBuffer::acquire() const { + mRefs.fetch_add(1, std::memory_order_relaxed); + } + + int32_t SharedBuffer::release(uint32_t flags) const { + const bool useDealloc = ((flags & eKeepStorage) == 0); + if (onlyOwner()) { + // Since we're the only owner, our reference count goes to zero. + mRefs.store(0, std::memory_order_relaxed); + if (useDealloc) { + dealloc(this); + } + // As the only owner, our previous reference count was 1. + return 1; + } + // There's multiple owners, we need to use an atomic decrement. + int32_t prevRefCount = mRefs.fetch_sub(1, std::memory_order_release); + if (prevRefCount == 1) { + // We're the last reference, we need the acquire fence. + atomic_thread_fence(std::memory_order_acquire); + if (useDealloc) { + dealloc(this); + } + } + return prevRefCount; + } + + +}; // namespace android diff --git a/sysbridge/src/main/cpp/android/utils/SharedBuffer.h b/sysbridge/src/main/cpp/android/utils/SharedBuffer.h new file mode 100644 index 0000000000..c170df00e1 --- /dev/null +++ b/sysbridge/src/main/cpp/android/utils/SharedBuffer.h @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2005 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * DEPRECATED. DO NOT USE FOR NEW CODE. + */ + +#ifndef ANDROID_SHARED_BUFFER_H +#define ANDROID_SHARED_BUFFER_H + +#include +#include +#include + +// --------------------------------------------------------------------------- + +namespace android { + + class SharedBuffer { + public: + + /* flags to use with release() */ + enum { + eKeepStorage = 0x00000001 + }; + + /*! allocate a buffer of size 'size' and acquire() it. + * call release() to free it. + */ + static SharedBuffer *alloc(size_t size); + + /*! free the memory associated with the SharedBuffer. + * Fails if there are any users associated with this SharedBuffer. + * In other words, the buffer must have been release by all its + * users. + */ + static void dealloc(const SharedBuffer *released); + + //! access the data for read + inline const void *data() const; + + //! access the data for read/write + inline void *data(); + + //! get size of the buffer + inline size_t size() const; + + //! get back a SharedBuffer object from its data + static inline SharedBuffer *bufferFromData(void *data); + + //! get back a SharedBuffer object from its data + static inline const SharedBuffer *bufferFromData(const void *data); + + //! get the size of a SharedBuffer object from its data + static inline size_t sizeFromData(const void *data); + + //! edit the buffer (get a writtable, or non-const, version of it) + SharedBuffer *edit() const; + + //! edit the buffer, resizing if needed + SharedBuffer *editResize(size_t size) const; + + //! like edit() but fails if a copy is required + SharedBuffer *attemptEdit() const; + + //! resize and edit the buffer, loose it's content. + SharedBuffer *reset(size_t size) const; + + //! acquire/release a reference on this buffer + void acquire() const; + + /*! release a reference on this buffer, with the option of not + * freeing the memory associated with it if it was the last reference + * returns the previous reference count + */ + int32_t release(uint32_t flags = 0) const; + + //! returns wether or not we're the only owner + inline bool onlyOwner() const; + + + private: + inline SharedBuffer() {} + + inline ~SharedBuffer() {} + + SharedBuffer(const SharedBuffer &); + + SharedBuffer &operator=(const SharedBuffer &); + + // Must be sized to preserve correct alignment. + mutable std::atomic mRefs; + size_t mSize; + uint32_t mReserved; + public: + // mClientMetadata is reserved for client use. It is initialized to 0 + // and the clients can do whatever they want with it. Note that this is + // placed last so that it is adjcent to the buffer allocated. + uint32_t mClientMetadata; + }; + + static_assert(sizeof(SharedBuffer) % 8 == 0 + && (sizeof(size_t) > 4 || sizeof(SharedBuffer) == 16), + "SharedBuffer has unexpected size"); + +// --------------------------------------------------------------------------- + + const void *SharedBuffer::data() const { + return this + 1; + } + + void *SharedBuffer::data() { + return this + 1; + } + + size_t SharedBuffer::size() const { + return mSize; + } + + SharedBuffer *SharedBuffer::bufferFromData(void *data) { + return data ? static_cast(data) - 1 : nullptr; + } + + const SharedBuffer *SharedBuffer::bufferFromData(const void *data) { + return data ? static_cast(data) - 1 : nullptr; + } + + size_t SharedBuffer::sizeFromData(const void *data) { + return data ? bufferFromData(data)->mSize : 0; + } + + bool SharedBuffer::onlyOwner() const { + return (mRefs.load(std::memory_order_acquire) == 1); + } + +} // namespace android + +// --------------------------------------------------------------------------- + +#endif // ANDROID_VECTOR_H diff --git a/sysbridge/src/main/cpp/android/utils/String16.cpp b/sysbridge/src/main/cpp/android/utils/String16.cpp new file mode 100644 index 0000000000..0d61425d7c --- /dev/null +++ b/sysbridge/src/main/cpp/android/utils/String16.cpp @@ -0,0 +1,393 @@ +/* + * Copyright (C) 2005 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "String16.h" + +#include +#include + +#include "SharedBuffer.h" + +#define LIBUTILS_PRAGMA(arg) _Pragma(#arg) +#if defined(__clang__) +#define LIBUTILS_PRAGMA_FOR_COMPILER(arg) LIBUTILS_PRAGMA(clang arg) +#elif defined(__GNUC__) +#define LIBUTILS_PRAGMA_FOR_COMPILER(arg) LIBUTILS_PRAGMA(GCC arg) +#else +#define LIBUTILS_PRAGMA_FOR_COMPILER(arg) +#endif +#define LIBUTILS_IGNORE(warning_flag) \ + LIBUTILS_PRAGMA_FOR_COMPILER(diagnostic push) \ + LIBUTILS_PRAGMA_FOR_COMPILER(diagnostic ignored warning_flag) +#define LIBUTILS_IGNORE_END() LIBUTILS_PRAGMA_FOR_COMPILER(diagnostic pop) + +namespace android { + +static const StaticString16 emptyString(u""); +static inline char16_t* getEmptyString() { + return const_cast(emptyString.c_str()); +} + +// --------------------------------------------------------------------------- + +void* String16::alloc(size_t size) +{ + SharedBuffer* buf = SharedBuffer::alloc(size); + buf->mClientMetadata = kIsSharedBufferAllocated; + return buf; +} + +char16_t* String16::allocFromUTF8(const char* u8str, size_t u8len) +{ + if (u8len == 0) return getEmptyString(); + + const uint8_t* u8cur = (const uint8_t*) u8str; + + const ssize_t u16len = utf8_to_utf16_length(u8cur, u8len); + if (u16len < 0) { + return getEmptyString(); + } + + SharedBuffer* buf = static_cast(alloc(sizeof(char16_t) * (u16len + 1))); + if (buf) { + u8cur = (const uint8_t*) u8str; + char16_t* u16str = (char16_t*)buf->data(); + + utf8_to_utf16(u8cur, u8len, u16str, ((size_t) u16len) + 1); + + //printf("Created UTF-16 string from UTF-8 \"%s\":", in); + //printHexData(1, str, buf->size(), 16, 1); + //printf("\n"); + + return u16str; + } + + return getEmptyString(); +} + +char16_t* String16::allocFromUTF16(const char16_t* u16str, size_t u16len) { + if (u16len >= SIZE_MAX / sizeof(char16_t)) { + abort(); + } + + SharedBuffer* buf = static_cast(alloc((u16len + 1) * sizeof(char16_t))); + if (buf) { + char16_t* str = (char16_t*)buf->data(); + memcpy(str, u16str, u16len * sizeof(char16_t)); + str[u16len] = 0; + return str; + } + return getEmptyString(); +} + +// --------------------------------------------------------------------------- + +String16::String16() + : mString(getEmptyString()) +{ +} + +String16::String16(const String16& o) + : mString(o.mString) +{ + acquire(); +} + +String16::String16(String16&& o) noexcept + : mString(o.mString) +{ + o.mString = getEmptyString(); +} + +String16::String16(const String16& o, size_t len, size_t begin) + : mString(getEmptyString()) +{ + setTo(o, len, begin); +} + +String16::String16(const char16_t* o) : mString(allocFromUTF16(o, strlen16(o))) {} + +String16::String16(const char16_t* o, size_t len) : mString(allocFromUTF16(o, len)) {} + +String16::String16(const String8& o) : mString(allocFromUTF8(o.c_str(), o.size())) {} + +String16::String16(const char* o) + : mString(allocFromUTF8(o, strlen(o))) +{ +} + +String16::String16(const char* o, size_t len) + : mString(allocFromUTF8(o, len)) +{ +} + +String16::~String16() +{ + release(); +} + +String16& String16::operator=(String16&& other) noexcept { + release(); + mString = other.mString; + other.mString = getEmptyString(); + return *this; +} + +size_t String16::size() const +{ + if (isStaticString()) { + return staticStringSize(); + } else { + return SharedBuffer::sizeFromData(mString) / sizeof(char16_t) - 1; + } +} + +void String16::setTo(const String16& other) +{ + release(); + mString = other.mString; + acquire(); +} + +status_t String16::setTo(const String16& other, size_t len, size_t begin) +{ + const size_t N = other.size(); + if (begin >= N) { + release(); + mString = getEmptyString(); + return OK; + } + if ((begin+len) > N) len = N-begin; + if (begin == 0 && len == N) { + setTo(other); + return OK; + } + + return setTo(other.c_str() + begin, len); +} + +status_t String16::setTo(const char16_t* other) +{ + return setTo(other, strlen16(other)); +} + +status_t String16::setTo(const char16_t* other, size_t len) +{ + if (len >= SIZE_MAX / sizeof(char16_t)) { + abort(); + } + + SharedBuffer* buf = static_cast(editResize((len + 1) * sizeof(char16_t))); + if (buf) { + char16_t* str = (char16_t*)buf->data(); + memmove(str, other, len*sizeof(char16_t)); + str[len] = 0; + mString = str; + return OK; + } + return NO_MEMORY; +} + +status_t String16::append(const String16& other) { + return append(other.c_str(), other.size()); +} + +status_t String16::append(const char16_t* chrs, size_t otherLen) { + const size_t myLen = size(); + + if (myLen == 0) return setTo(chrs, otherLen); + + if (otherLen == 0) return OK; + + size_t size = myLen; + if (__builtin_add_overflow(size, otherLen, &size) || + __builtin_add_overflow(size, 1, &size) || + __builtin_mul_overflow(size, sizeof(char16_t), &size)) return NO_MEMORY; + + SharedBuffer* buf = static_cast(editResize(size)); + if (!buf) return NO_MEMORY; + + char16_t* str = static_cast(buf->data()); + memcpy(str + myLen, chrs, otherLen * sizeof(char16_t)); + str[myLen + otherLen] = 0; + mString = str; + return OK; +} + +status_t String16::insert(size_t pos, const char16_t* chrs) { + return insert(pos, chrs, strlen16(chrs)); +} + +status_t String16::insert(size_t pos, const char16_t* chrs, size_t otherLen) { + const size_t myLen = size(); + + if (myLen == 0) return setTo(chrs, otherLen); + + if (otherLen == 0) return OK; + + if (pos > myLen) pos = myLen; + + size_t size = myLen; + if (__builtin_add_overflow(size, otherLen, &size) || + __builtin_add_overflow(size, 1, &size) || + __builtin_mul_overflow(size, sizeof(char16_t), &size)) return NO_MEMORY; + + SharedBuffer* buf = static_cast(editResize(size)); + if (!buf) return NO_MEMORY; + + char16_t* str = static_cast(buf->data()); + if (pos < myLen) memmove(str + pos + otherLen, str + pos, (myLen - pos) * sizeof(char16_t)); + memcpy(str + pos, chrs, otherLen * sizeof(char16_t)); + str[myLen + otherLen] = 0; + mString = str; + return OK; +} + +ssize_t String16::findFirst(char16_t c) const +{ + const char16_t* str = string(); + const char16_t* p = str; + const char16_t* e = p + size(); + while (p < e) { + if (*p == c) { + return p-str; + } + p++; + } + return -1; +} + +ssize_t String16::findLast(char16_t c) const +{ + const char16_t* str = string(); + const char16_t* p = str; + const char16_t* e = p + size(); + while (p < e) { + e--; + if (*e == c) { + return e-str; + } + } + return -1; +} + +bool String16::startsWith(const String16& prefix) const +{ + const size_t ps = prefix.size(); + if (ps > size()) return false; + return strzcmp16(mString, ps, prefix.c_str(), ps) == 0; +} + +bool String16::startsWith(const char16_t* prefix) const +{ + const size_t ps = strlen16(prefix); + if (ps > size()) return false; + return strncmp16(mString, prefix, ps) == 0; +} + +bool String16::contains(const char16_t* chrs) const +{ + return strstr16(mString, chrs) != nullptr; +} + +void* String16::edit() { + SharedBuffer* buf; + if (isStaticString()) { + buf = static_cast(alloc((size() + 1) * sizeof(char16_t))); + if (buf) { + memcpy(buf->data(), mString, (size() + 1) * sizeof(char16_t)); + } + } else { + buf = SharedBuffer::bufferFromData(mString)->edit(); + buf->mClientMetadata = kIsSharedBufferAllocated; + } + return buf; +} + +void* String16::editResize(size_t newSize) { + SharedBuffer* buf; + if (isStaticString()) { + size_t copySize = (size() + 1) * sizeof(char16_t); + if (newSize < copySize) { + copySize = newSize; + } + buf = static_cast(alloc(newSize)); + if (buf) { + memcpy(buf->data(), mString, copySize); + } + } else { + buf = SharedBuffer::bufferFromData(mString)->editResize(newSize); + buf->mClientMetadata = kIsSharedBufferAllocated; + } + return buf; +} + +void String16::acquire() +{ + if (!isStaticString()) { + SharedBuffer::bufferFromData(mString)->acquire(); + } +} + +void String16::release() +{ + if (!isStaticString()) { + SharedBuffer::bufferFromData(mString)->release(); + } +} + +bool String16::isStaticString() const { + // See String16.h for notes on the memory layout of String16::StaticData and + // SharedBuffer. + LIBUTILS_IGNORE("-Winvalid-offsetof") + static_assert(sizeof(SharedBuffer) - offsetof(SharedBuffer, mClientMetadata) == 4); + LIBUTILS_IGNORE_END() + const uint32_t* p = reinterpret_cast(mString); + return (*(p - 1) & kIsSharedBufferAllocated) == 0; +} + +size_t String16::staticStringSize() const { + // See String16.h for notes on the memory layout of String16::StaticData and + // SharedBuffer. + LIBUTILS_IGNORE("-Winvalid-offsetof") + static_assert(sizeof(SharedBuffer) - offsetof(SharedBuffer, mClientMetadata) == 4); + LIBUTILS_IGNORE_END() + const uint32_t* p = reinterpret_cast(mString); + return static_cast(*(p - 1)); +} + +status_t String16::replaceAll(char16_t replaceThis, char16_t withThis) +{ + const size_t N = size(); + const char16_t* str = string(); + char16_t* edited = nullptr; + for (size_t i=0; i(edit()); + if (!buf) { + return NO_MEMORY; + } + edited = (char16_t*)buf->data(); + mString = str = edited; + } + edited[i] = withThis; + } + } + return OK; +} + +}; // namespace android diff --git a/sysbridge/src/main/cpp/android/utils/String16.h b/sysbridge/src/main/cpp/android/utils/String16.h new file mode 100644 index 0000000000..54c6d21ae6 --- /dev/null +++ b/sysbridge/src/main/cpp/android/utils/String16.h @@ -0,0 +1,411 @@ +/* + * Copyright (C) 2005 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_STRING16_H +#define ANDROID_STRING16_H + +#include +#include +#include + +#include "Errors.h" +#include "String8.h" +#include "TypeHelpers.h" + +#if __cplusplus >= 202002L +#include +#endif + +// --------------------------------------------------------------------------- + +namespace android { + +// --------------------------------------------------------------------------- + +template +class StaticString16; + +// DO NOT USE: please use std::u16string + +//! This is a string holding UTF-16 characters. +class String16 +{ +public: + String16(); + String16(const String16& o); + String16(String16&& o) noexcept; + String16(const String16& o, + size_t len, + size_t begin=0); + explicit String16(const char16_t* o); + explicit String16(const char16_t* o, size_t len); + explicit String16(const String8& o); + explicit String16(const char* o); + explicit String16(const char* o, size_t len); + + ~String16(); + + inline const char16_t* c_str() const; + + size_t size() const; + inline bool empty() const; + + inline size_t length() const; + + void setTo(const String16& other); + status_t setTo(const char16_t* other); + status_t setTo(const char16_t* other, size_t len); + status_t setTo(const String16& other, + size_t len, + size_t begin=0); + + status_t append(const String16& other); + status_t append(const char16_t* other, size_t len); + + inline String16& operator=(const String16& other); + String16& operator=(String16&& other) noexcept; + + inline String16& operator+=(const String16& other); + inline String16 operator+(const String16& other) const; + + status_t insert(size_t pos, const char16_t* chrs); + status_t insert(size_t pos, + const char16_t* chrs, size_t len); + + ssize_t findFirst(char16_t c) const; + ssize_t findLast(char16_t c) const; + + bool startsWith(const String16& prefix) const; + bool startsWith(const char16_t* prefix) const; + + bool contains(const char16_t* chrs) const; + inline bool contains(const String16& other) const; + + status_t replaceAll(char16_t replaceThis, + char16_t withThis); + + inline int compare(const String16& other) const; + + inline bool operator<(const String16& other) const; + inline bool operator<=(const String16& other) const; + inline bool operator==(const String16& other) const; + inline bool operator!=(const String16& other) const; + inline bool operator>=(const String16& other) const; + inline bool operator>(const String16& other) const; +#if __cplusplus >= 202002L + inline std::strong_ordering operator<=>(const String16& other) const; +#endif + + inline bool operator<(const char16_t* other) const; + inline bool operator<=(const char16_t* other) const; + inline bool operator==(const char16_t* other) const; + inline bool operator!=(const char16_t* other) const; + inline bool operator>=(const char16_t* other) const; + inline bool operator>(const char16_t* other) const; +#if __cplusplus >= 202002L + inline std::strong_ordering operator<=>(const char16_t* other) const; +#endif + + inline operator const char16_t*() const; + + // Implicit cast to std::u16string is not implemented on purpose - u16string_view is much + // lighter and if one needs, they can still create u16string from u16string_view. + inline operator std::u16string_view() const; + + // Static and non-static String16 behave the same for the users, so + // this method isn't of much use for the users. It is public for testing. + bool isStaticString() const; + + private: + /* + * A flag indicating the type of underlying buffer. + */ + static constexpr uint32_t kIsSharedBufferAllocated = 0x80000000; + + /* + * alloc() returns void* so that SharedBuffer class is not exposed. + */ + static void* alloc(size_t size); + static char16_t* allocFromUTF8(const char* u8str, size_t u8len); + static char16_t* allocFromUTF16(const char16_t* u16str, size_t u16len); + + /* + * edit() and editResize() return void* so that SharedBuffer class + * is not exposed. + */ + void* edit(); + void* editResize(size_t new_size); + + void acquire(); + void release(); + + size_t staticStringSize() const; + + const char16_t* mString; + +protected: + /* + * Data structure used to allocate static storage for static String16. + * + * Note that this data structure and SharedBuffer are used interchangably + * as the underlying data structure for a String16. Therefore, the layout + * of this data structure must match the part in SharedBuffer that is + * visible to String16. + */ + template + struct StaticData { + // The high bit of 'size' is used as a flag. + static_assert(N - 1 < kIsSharedBufferAllocated, "StaticString16 too long!"); + constexpr StaticData() : size(N - 1), data{0} {} + const uint32_t size; + char16_t data[N]; + + constexpr StaticData(const StaticData&) = default; + }; + + /* + * Helper function for constructing a StaticData object. + */ + template + static constexpr const StaticData makeStaticData(const char16_t (&s)[N]) { + StaticData r; + // The 'size' field is at the same location where mClientMetadata would + // be for a SharedBuffer. We do NOT set kIsSharedBufferAllocated flag + // here. + for (size_t i = 0; i < N - 1; ++i) r.data[i] = s[i]; + return r; + } + + template + explicit constexpr String16(const StaticData& s) : mString(s.data) {} + +// These symbols are for potential backward compatibility with prebuilts. To be removed. +#ifdef ENABLE_STRING16_OBSOLETE_METHODS +public: +#else +private: +#endif + inline const char16_t* string() const; +}; + +// String16 can be trivially moved using memcpy() because moving does not +// require any change to the underlying SharedBuffer contents or reference count. +ANDROID_TRIVIAL_MOVE_TRAIT(String16) + +static inline std::ostream& operator<<(std::ostream& os, const String16& str) { + os << String8(str); + return os; +} + +// --------------------------------------------------------------------------- + +/* + * A StaticString16 object is a specialized String16 object. Instead of holding + * the string data in a ref counted SharedBuffer object, it holds data in a + * buffer within StaticString16 itself. Note that this buffer is NOT ref + * counted and is assumed to be available for as long as there is at least a + * String16 object using it. Therefore, one must be extra careful to NEVER + * assign a StaticString16 to a String16 that outlives the StaticString16 + * object. + * + * THE SAFEST APPROACH IS TO USE StaticString16 ONLY AS GLOBAL VARIABLES. + * + * A StaticString16 SHOULD NEVER APPEAR IN APIs. USE String16 INSTEAD. + */ +template +class StaticString16 : public String16 { +public: + constexpr StaticString16(const char16_t (&s)[N]) : String16(mData), mData(makeStaticData(s)) {} + + constexpr StaticString16(const StaticString16& other) + : String16(mData), mData(other.mData) {} + + constexpr StaticString16(const StaticString16&&) = delete; + + // There is no reason why one would want to 'new' a StaticString16. Delete + // it to discourage misuse. + static void* operator new(std::size_t) = delete; + +private: + const StaticData mData; +}; + +template +StaticString16(const F&)->StaticString16; + +// --------------------------------------------------------------------------- +// No user servicable parts below. + +inline int compare_type(const String16& lhs, const String16& rhs) +{ + return lhs.compare(rhs); +} + +inline int strictly_order_type(const String16& lhs, const String16& rhs) +{ + return compare_type(lhs, rhs) < 0; +} + +inline const char16_t* String16::c_str() const +{ + return mString; +} + +inline const char16_t* String16::string() const +{ + return mString; +} + +inline bool String16::empty() const +{ + return length() == 0; +} + +inline size_t String16::length() const +{ + return size(); +} + +inline bool String16::contains(const String16& other) const +{ + return contains(other.c_str()); +} + +inline String16& String16::operator=(const String16& other) +{ + setTo(other); + return *this; +} + +inline String16& String16::operator+=(const String16& other) +{ + append(other); + return *this; +} + +inline String16 String16::operator+(const String16& other) const +{ + String16 tmp(*this); + tmp += other; + return tmp; +} + +inline int String16::compare(const String16& other) const +{ + return strzcmp16(mString, size(), other.mString, other.size()); +} + +inline bool String16::operator<(const String16& other) const +{ + return strzcmp16(mString, size(), other.mString, other.size()) < 0; +} + +inline bool String16::operator<=(const String16& other) const +{ + return strzcmp16(mString, size(), other.mString, other.size()) <= 0; +} + +inline bool String16::operator==(const String16& other) const +{ + return strzcmp16(mString, size(), other.mString, other.size()) == 0; +} + +inline bool String16::operator!=(const String16& other) const +{ + return strzcmp16(mString, size(), other.mString, other.size()) != 0; +} + +inline bool String16::operator>=(const String16& other) const +{ + return strzcmp16(mString, size(), other.mString, other.size()) >= 0; +} + +inline bool String16::operator>(const String16& other) const +{ + return strzcmp16(mString, size(), other.mString, other.size()) > 0; +} + +#if __cplusplus >= 202002L +inline std::strong_ordering String16::operator<=>(const String16& other) const { + int result = strzcmp16(mString, size(), other.mString, other.size()); + if (result == 0) { + return std::strong_ordering::equal; + } else if (result < 0) { + return std::strong_ordering::less; + } else { + return std::strong_ordering::greater; + } +} +#endif + +inline bool String16::operator<(const char16_t* other) const +{ + return strcmp16(mString, other) < 0; +} + +inline bool String16::operator<=(const char16_t* other) const +{ + return strcmp16(mString, other) <= 0; +} + +inline bool String16::operator==(const char16_t* other) const +{ + return strcmp16(mString, other) == 0; +} + +inline bool String16::operator!=(const char16_t* other) const +{ + return strcmp16(mString, other) != 0; +} + +inline bool String16::operator>=(const char16_t* other) const +{ + return strcmp16(mString, other) >= 0; +} + +inline bool String16::operator>(const char16_t* other) const +{ + return strcmp16(mString, other) > 0; +} + +#if __cplusplus >= 202002L +inline std::strong_ordering String16::operator<=>(const char16_t* other) const { + int result = strcmp16(mString, other); + if (result == 0) { + return std::strong_ordering::equal; + } else if (result < 0) { + return std::strong_ordering::less; + } else { + return std::strong_ordering::greater; + } +} +#endif + +inline String16::operator const char16_t*() const +{ + return mString; +} + +inline String16::operator std::u16string_view() const +{ + return {mString, length()}; +} + +} // namespace android + +// --------------------------------------------------------------------------- + +#endif // ANDROID_STRING16_H diff --git a/sysbridge/src/main/cpp/android/utils/String8.cpp b/sysbridge/src/main/cpp/android/utils/String8.cpp new file mode 100644 index 0000000000..3f8d9d634a --- /dev/null +++ b/sysbridge/src/main/cpp/android/utils/String8.cpp @@ -0,0 +1,453 @@ +/* + * Copyright (C) 2005 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "String8.h" + +#include +#include "String16.h" + +#include +#include + +#include +#include +#include + +#include "SharedBuffer.h" + +/* + * Functions outside android is below the namespace android, since they use + * functions and constants in android namespace. + */ + +// --------------------------------------------------------------------------- + +namespace android { + + static inline char *getEmptyString() { + static SharedBuffer *gEmptyStringBuf = [] { + SharedBuffer *buf = SharedBuffer::alloc(1); + char *str = static_cast(buf->data()); + *str = 0; + return buf; + }(); + + gEmptyStringBuf->acquire(); + return static_cast(gEmptyStringBuf->data()); + } + +// --------------------------------------------------------------------------- + + static char *allocFromUTF8(const char *in, size_t len) { + if (len > 0) { + if (len == SIZE_MAX) { + return nullptr; + } + SharedBuffer *buf = SharedBuffer::alloc(len + 1); +// ALOG_ASSERT(buf, "Unable to allocate shared buffer"); + if (buf) { + char *str = (char *) buf->data(); + memcpy(str, in, len); + str[len] = 0; + return str; + } + return nullptr; + } + + return getEmptyString(); + } + + static char *allocFromUTF16(const char16_t *in, size_t len) { + if (len == 0) return getEmptyString(); + + // Allow for closing '\0' + const ssize_t resultStrLen = utf16_to_utf8_length(in, len) + 1; + if (resultStrLen < 1) { + return getEmptyString(); + } + + SharedBuffer *buf = SharedBuffer::alloc(resultStrLen); +// ALOG_ASSERT(buf, "Unable to allocate shared buffer"); + if (!buf) { + return getEmptyString(); + } + + char *resultStr = (char *) buf->data(); + utf16_to_utf8(in, len, resultStr, resultStrLen); + return resultStr; + } + + static char *allocFromUTF32(const char32_t *in, size_t len) { + if (len == 0) { + return getEmptyString(); + } + + const ssize_t resultStrLen = utf32_to_utf8_length(in, len) + 1; + if (resultStrLen < 1) { + return getEmptyString(); + } + + SharedBuffer *buf = SharedBuffer::alloc(resultStrLen); +// ALOG_ASSERT(buf, "Unable to allocate shared buffer"); + if (!buf) { + return getEmptyString(); + } + + char *resultStr = (char *) buf->data(); + utf32_to_utf8(in, len, resultStr, resultStrLen); + + return resultStr; + } + +// --------------------------------------------------------------------------- + + String8::String8() + : mString(getEmptyString()) { + } + + String8::String8(const String8 &o) + : mString(o.mString) { + SharedBuffer::bufferFromData(mString)->acquire(); + } + + String8::String8(const char *o) + : mString(allocFromUTF8(o, strlen(o))) { + if (mString == nullptr) { + mString = getEmptyString(); + } + } + + String8::String8(const char *o, size_t len) + : mString(allocFromUTF8(o, len)) { + if (mString == nullptr) { + mString = getEmptyString(); + } + } + + String8::String8(const String16 &o) : mString(allocFromUTF16(o.c_str(), o.size())) {} + + String8::String8(const char16_t *o) + : mString(allocFromUTF16(o, strlen16(o))) { + } + + String8::String8(const char16_t *o, size_t len) + : mString(allocFromUTF16(o, len)) { + } + + String8::String8(const char32_t *o) + : mString(allocFromUTF32(o, std::char_traits::length(o))) {} + + String8::String8(const char32_t *o, size_t len) + : mString(allocFromUTF32(o, len)) { + } + + String8::~String8() { + SharedBuffer::bufferFromData(mString)->release(); + } + + size_t String8::length() const { + return SharedBuffer::sizeFromData(mString) - 1; + } + + String8 String8::format(const char *fmt, ...) { + va_list args; + va_start(args, fmt); + + String8 result(formatV(fmt, args)); + + va_end(args); + return result; + } + + String8 String8::formatV(const char *fmt, va_list args) { + String8 result; + result.appendFormatV(fmt, args); + return result; + } + + void String8::clear() { + SharedBuffer::bufferFromData(mString)->release(); + mString = getEmptyString(); + } + + void String8::setTo(const String8 &other) { + SharedBuffer::bufferFromData(other.mString)->acquire(); + SharedBuffer::bufferFromData(mString)->release(); + mString = other.mString; + } + + status_t String8::setTo(const char *other) { + const char *newString = allocFromUTF8(other, strlen(other)); + SharedBuffer::bufferFromData(mString)->release(); + mString = newString; + if (mString) return OK; + + mString = getEmptyString(); + return NO_MEMORY; + } + + status_t String8::setTo(const char *other, size_t len) { + const char *newString = allocFromUTF8(other, len); + SharedBuffer::bufferFromData(mString)->release(); + mString = newString; + if (mString) return OK; + + mString = getEmptyString(); + return NO_MEMORY; + } + + status_t String8::setTo(const char16_t *other, size_t len) { + const char *newString = allocFromUTF16(other, len); + SharedBuffer::bufferFromData(mString)->release(); + mString = newString; + if (mString) return OK; + + mString = getEmptyString(); + return NO_MEMORY; + } + + status_t String8::setTo(const char32_t *other, size_t len) { + const char *newString = allocFromUTF32(other, len); + SharedBuffer::bufferFromData(mString)->release(); + mString = newString; + if (mString) return OK; + + mString = getEmptyString(); + return NO_MEMORY; + } + + status_t String8::append(const String8 &other) { + const size_t otherLen = other.bytes(); + if (bytes() == 0) { + setTo(other); + return OK; + } else if (otherLen == 0) { + return OK; + } + + return real_append(other.c_str(), otherLen); + } + + status_t String8::append(const char *other) { + return append(other, strlen(other)); + } + + status_t String8::append(const char *other, size_t otherLen) { + if (bytes() == 0) { + return setTo(other, otherLen); + } else if (otherLen == 0) { + return OK; + } + + return real_append(other, otherLen); + } + + status_t String8::appendFormat(const char *fmt, ...) { + va_list args; + va_start(args, fmt); + + status_t result = appendFormatV(fmt, args); + + va_end(args); + return result; + } + + status_t String8::appendFormatV(const char *fmt, va_list args) { + int n, result = OK; + va_list tmp_args; + + /* args is undefined after vsnprintf. + * So we need a copy here to avoid the + * second vsnprintf access undefined args. + */ + va_copy(tmp_args, args); + n = vsnprintf(nullptr, 0, fmt, tmp_args); + va_end(tmp_args); + + if (n < 0) return UNKNOWN_ERROR; + + if (n > 0) { + size_t oldLength = length(); + if (static_cast(n) > std::numeric_limits::max() - 1 || + oldLength > std::numeric_limits::max() - n - 1) { + return NO_MEMORY; + } + char *buf = lockBuffer(oldLength + n); + if (buf) { + vsnprintf(buf + oldLength, n + 1, fmt, args); + } else { + result = NO_MEMORY; + } + } + return result; + } + + status_t String8::real_append(const char *other, size_t otherLen) { + const size_t myLen = bytes(); + + SharedBuffer *buf; + size_t newLen; + if (__builtin_add_overflow(myLen, otherLen, &newLen) || + __builtin_add_overflow(newLen, 1, &newLen) || + (buf = SharedBuffer::bufferFromData(mString)->editResize(newLen)) == nullptr) { + return NO_MEMORY; + } + + char *str = (char *) buf->data(); + mString = str; + str += myLen; + memcpy(str, other, otherLen); + str[otherLen] = '\0'; + return OK; + } + + char *String8::lockBuffer(size_t size) { + SharedBuffer *buf = SharedBuffer::bufferFromData(mString) + ->editResize(size + 1); + if (buf) { + char *str = (char *) buf->data(); + mString = str; + return str; + } + return nullptr; + } + + void String8::unlockBuffer() { + unlockBuffer(strlen(mString)); + } + + status_t String8::unlockBuffer(size_t size) { + if (size != this->size()) { + SharedBuffer *buf = SharedBuffer::bufferFromData(mString) + ->editResize(size + 1); + if (!buf) { + return NO_MEMORY; + } + + char *str = (char *) buf->data(); + str[size] = 0; + mString = str; + } + + return OK; + } + + ssize_t String8::find(const char *other, size_t start) const { + size_t len = size(); + if (start >= len) { + return -1; + } + const char *s = mString + start; + const char *p = strstr(s, other); + return p ? p - mString : -1; + } + + bool String8::removeAll(const char *other) { +// ALOG_ASSERT(other, "String8::removeAll() requires a non-NULL string"); + + if (*other == '\0') + return true; + + ssize_t index = find(other); + if (index < 0) return false; + + char *buf = lockBuffer(size()); + if (!buf) return false; // out of memory + + size_t skip = strlen(other); + size_t len = size(); + size_t tail = index; + while (size_t(index) < len) { + ssize_t next = find(other, index + skip); + if (next < 0) { + next = len; + } + + memmove(buf + tail, buf + index + skip, next - index - skip); + tail += next - index - skip; + index = next; + } + unlockBuffer(tail); + return true; + } + + void String8::toLower() { + const size_t length = size(); + if (length == 0) return; + + char *buf = lockBuffer(length); + for (size_t i = length; i > 0; --i) { + *buf = static_cast(tolower(*buf)); + buf++; + } + unlockBuffer(length); + } + +// --------------------------------------------------------------------------- +// Path functions + +// TODO: we should remove all the path functions from String8 +#if defined(_WIN32) +#define OS_PATH_SEPARATOR '\\' +#else +#define OS_PATH_SEPARATOR '/' +#endif + + String8 String8::getPathDir(void) const { + const char *cp; + const char *const str = mString; + + cp = strrchr(str, OS_PATH_SEPARATOR); + if (cp == nullptr) + return String8(""); + else + return String8(str, cp - str); + } + +/* + * Helper function for finding the start of an extension in a pathname. + * + * Returns a pointer inside mString, or NULL if no extension was found. + */ + static const char *find_extension(const char *str) { + const char *lastSlash; + const char *lastDot; + + // only look at the filename + lastSlash = strrchr(str, OS_PATH_SEPARATOR); + if (lastSlash == nullptr) + lastSlash = str; + else + lastSlash++; + + // find the last dot + lastDot = strrchr(lastSlash, '.'); + if (lastDot == nullptr) + return nullptr; + + // looks good, ship it + return lastDot; + } + + String8 String8::getPathExtension(void) const { + auto ext = find_extension(mString); + if (ext != nullptr) + return String8(ext); + else + return String8(""); + } + +}; // namespace android diff --git a/sysbridge/src/main/cpp/android/utils/String8.h b/sysbridge/src/main/cpp/android/utils/String8.h new file mode 100644 index 0000000000..8875750834 --- /dev/null +++ b/sysbridge/src/main/cpp/android/utils/String8.h @@ -0,0 +1,381 @@ +/* + * Copyright (C) 2005 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_STRING8_H +#define ANDROID_STRING8_H + +#include +#include +#include // for strcmp +#include +#include +#include "Errors.h" +#include "Unicode.h" +#include "TypeHelpers.h" + +// --------------------------------------------------------------------------- + +namespace android { + + class String16; + +// DO NOT USE: please use std::string + +//! This is a string holding UTF-8 characters. Does not allow the value more +// than 0x10FFFF, which is not valid unicode codepoint. + class String8 { + public: + String8(); + + String8(const String8 &o); + + explicit String8(const char *o); + + explicit String8(const char *o, size_t numChars); + + explicit String8(std::string_view o); + + explicit String8(const String16 &o); + + explicit String8(const char16_t *o); + + explicit String8(const char16_t *o, size_t numChars); + + explicit String8(const char32_t *o); + + explicit String8(const char32_t *o, size_t numChars); + + ~String8(); + + static String8 format(const char *fmt, ...) __attribute__((format (printf, 1, 2))); + + static String8 formatV(const char *fmt, va_list args); + + inline const char *c_str() const; + + inline size_t size() const; + + inline size_t bytes() const; + + inline bool empty() const; + + size_t length() const; + + void clear(); + + void setTo(const String8 &other); + + status_t setTo(const char *other); + + status_t setTo(const char *other, size_t numChars); + + status_t setTo(const char16_t *other, size_t numChars); + + status_t setTo(const char32_t *other, + size_t length); + + status_t append(const String8 &other); + + status_t append(const char *other); + + status_t append(const char *other, size_t numChars); + + status_t appendFormat(const char *fmt, ...) + __attribute__((format (printf, 2, 3))); + + status_t appendFormatV(const char *fmt, va_list args); + + inline String8 &operator=(const String8 &other); + + inline String8 &operator=(const char *other); + + inline String8 &operator+=(const String8 &other); + + inline String8 operator+(const String8 &other) const; + + inline String8 &operator+=(const char *other); + + inline String8 operator+(const char *other) const; + + inline int compare(const String8 &other) const; + + inline bool operator<(const String8 &other) const; + + inline bool operator<=(const String8 &other) const; + + inline bool operator==(const String8 &other) const; + + inline bool operator!=(const String8 &other) const; + + inline bool operator>=(const String8 &other) const; + + inline bool operator>(const String8 &other) const; + +#if __cplusplus >= 202002L + inline std::strong_ordering operator<=>(const String8& other) const; +#endif + + inline bool operator<(const char *other) const; + + inline bool operator<=(const char *other) const; + + inline bool operator==(const char *other) const; + + inline bool operator!=(const char *other) const; + + inline bool operator>=(const char *other) const; + + inline bool operator>(const char *other) const; + +#if __cplusplus >= 202002L + inline std::strong_ordering operator<=>(const char* other) const; +#endif + + inline operator const char *() const; + + inline explicit operator std::string_view() const; + + char *lockBuffer(size_t size); + + void unlockBuffer(); + + status_t unlockBuffer(size_t size); + + // return the index of the first byte of other in this at or after + // start, or -1 if not found + ssize_t find(const char *other, size_t start = 0) const; + + inline ssize_t find(const String8 &other, size_t start = 0) const; + + // return true if this string contains the specified substring + inline bool contains(const char *other) const; + + inline bool contains(const String8 &other) const; + + // removes all occurrence of the specified substring + // returns true if any were found and removed + bool removeAll(const char *other); + + inline bool removeAll(const String8 &other); + + void toLower(); + + private: + String8 getPathDir(void) const; + + String8 getPathExtension(void) const; + + status_t real_append(const char *other, size_t numChars); + + const char *mString; + +// These symbols are for potential backward compatibility with prebuilts. To be removed. +#ifdef ENABLE_STRING8_OBSOLETE_METHODS + public: +#else + private: +#endif + + inline const char *string() const; + + inline bool isEmpty() const; + }; + +// String8 can be trivially moved using memcpy() because moving does not +// require any change to the underlying SharedBuffer contents or reference count. + ANDROID_TRIVIAL_MOVE_TRAIT(String8) + + static inline std::ostream &operator<<(std::ostream &os, const String8 &str) { + os << str.c_str(); + return os; + } + +// --------------------------------------------------------------------------- +// No user servicable parts below. + + inline int compare_type(const String8 &lhs, const String8 &rhs) { + return lhs.compare(rhs); + } + + inline int strictly_order_type(const String8 &lhs, const String8 &rhs) { + return compare_type(lhs, rhs) < 0; + } + + inline const char *String8::c_str() const { + return mString; + } + + inline const char *String8::string() const { + return mString; + } + + inline size_t String8::size() const { + return length(); + } + + inline bool String8::empty() const { + return length() == 0; + } + + inline bool String8::isEmpty() const { + return length() == 0; + } + + inline size_t String8::bytes() const { + return length(); + } + + inline ssize_t String8::find(const String8 &other, size_t start) const { + return find(other.c_str(), start); + } + + inline bool String8::contains(const char *other) const { + return find(other) >= 0; + } + + inline bool String8::contains(const String8 &other) const { + return contains(other.c_str()); + } + + inline bool String8::removeAll(const String8 &other) { + return removeAll(other.c_str()); + } + + inline String8 &String8::operator=(const String8 &other) { + setTo(other); + return *this; + } + + inline String8 &String8::operator=(const char *other) { + setTo(other); + return *this; + } + + inline String8 &String8::operator+=(const String8 &other) { + append(other); + return *this; + } + + inline String8 String8::operator+(const String8 &other) const { + String8 tmp(*this); + tmp += other; + return tmp; + } + + inline String8 &String8::operator+=(const char *other) { + append(other); + return *this; + } + + inline String8 String8::operator+(const char *other) const { + String8 tmp(*this); + tmp += other; + return tmp; + } + + inline int String8::compare(const String8 &other) const { + return strcmp(mString, other.mString); + } + + inline bool String8::operator<(const String8 &other) const { + return strcmp(mString, other.mString) < 0; + } + + inline bool String8::operator<=(const String8 &other) const { + return strcmp(mString, other.mString) <= 0; + } + + inline bool String8::operator==(const String8 &other) const { + return strcmp(mString, other.mString) == 0; + } + + inline bool String8::operator!=(const String8 &other) const { + return strcmp(mString, other.mString) != 0; + } + + inline bool String8::operator>=(const String8 &other) const { + return strcmp(mString, other.mString) >= 0; + } + + inline bool String8::operator>(const String8 &other) const { + return strcmp(mString, other.mString) > 0; + } + +#if __cplusplus >= 202002L + inline std::strong_ordering String8::operator<=>(const String8& other) const { + int result = strcmp(mString, other.mString); + if (result == 0) { + return std::strong_ordering::equal; + } else if (result < 0) { + return std::strong_ordering::less; + } else { + return std::strong_ordering::greater; + } + } +#endif + + inline bool String8::operator<(const char *other) const { + return strcmp(mString, other) < 0; + } + + inline bool String8::operator<=(const char *other) const { + return strcmp(mString, other) <= 0; + } + + inline bool String8::operator==(const char *other) const { + return strcmp(mString, other) == 0; + } + + inline bool String8::operator!=(const char *other) const { + return strcmp(mString, other) != 0; + } + + inline bool String8::operator>=(const char *other) const { + return strcmp(mString, other) >= 0; + } + + inline bool String8::operator>(const char *other) const { + return strcmp(mString, other) > 0; + } + +#if __cplusplus >= 202002L + inline std::strong_ordering String8::operator<=>(const char* other) const { + int result = strcmp(mString, other); + if (result == 0) { + return std::strong_ordering::equal; + } else if (result < 0) { + return std::strong_ordering::less; + } else { + return std::strong_ordering::greater; + } + } +#endif + + inline String8::operator const char *() const { + return mString; + } + + inline String8::String8(std::string_view o) : String8(o.data(), o.length()) {} + + inline String8::operator std::string_view() const { + return {mString, length()}; + } + +} // namespace android + +// --------------------------------------------------------------------------- + +#endif // ANDROID_STRING8_H diff --git a/sysbridge/src/main/cpp/android/utils/Tokenizer.cpp b/sysbridge/src/main/cpp/android/utils/Tokenizer.cpp new file mode 100644 index 0000000000..e9ad7bebf3 --- /dev/null +++ b/sysbridge/src/main/cpp/android/utils/Tokenizer.cpp @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "../../logging.h" +#include "Tokenizer.h" +#include "Errors.h" +#include "FileMap.h" +#include "String8.h" +#include +#include +#include +#include + +#ifndef DEBUG_TOKENIZER +// Enables debug output for the tokenizer. +#define DEBUG_TOKENIZER 0 +#endif + +namespace android { + + static inline bool isDelimiter(char ch, const char *delimiters) { + return strchr(delimiters, ch) != nullptr; + } + + Tokenizer::Tokenizer(const String8 &filename, FileMap *fileMap, char *buffer, + bool ownBuffer, size_t length) : + mFilename(filename), mFileMap(fileMap), + mBuffer(buffer), mOwnBuffer(ownBuffer), mLength(length), + mCurrent(buffer), mLineNumber(1) { + } + + Tokenizer::~Tokenizer() { + delete mFileMap; + if (mOwnBuffer) { + delete[] mBuffer; + } + } + + status_t Tokenizer::open(const String8 &filename, Tokenizer **outTokenizer) { + *outTokenizer = nullptr; + + int result = OK; + int fd = ::open(filename.c_str(), O_RDONLY); + if (fd < 0) { + result = -errno; + LOGE("Error opening file '%s': %s", filename.c_str(), strerror(errno)); + } else { + struct stat stat; + if (fstat(fd, &stat)) { + result = -errno; + LOGE("Error getting size of file '%s': %s", filename.c_str(), strerror(errno)); + } else { + size_t length = size_t(stat.st_size); + + FileMap *fileMap = new FileMap(); + bool ownBuffer = false; + char *buffer; + if (fileMap->create(nullptr, fd, 0, length, true)) { + fileMap->advise(FileMap::SEQUENTIAL); + buffer = static_cast(fileMap->getDataPtr()); + } else { + delete fileMap; + fileMap = nullptr; + + // Fall back to reading into a buffer since we can't mmap files in sysfs. + // The length we obtained from stat is wrong too (it will always be 4096) + // so we must trust that read will read the entire file. + buffer = new char[length]; + ownBuffer = true; + ssize_t nrd = read(fd, buffer, length); + if (nrd < 0) { + result = -errno; + LOGE("Error reading file '%s': %s", filename.c_str(), strerror(errno)); + delete[] buffer; + buffer = nullptr; + } else { + length = size_t(nrd); + } + } + + if (!result) { + *outTokenizer = new Tokenizer(filename, fileMap, buffer, ownBuffer, length); + } + } + close(fd); + } + return result; + } + + status_t Tokenizer::fromContents(const String8 &filename, + const char *contents, Tokenizer **outTokenizer) { + *outTokenizer = new Tokenizer(filename, nullptr, + const_cast(contents), false, strlen(contents)); + return OK; + } + + String8 Tokenizer::getLocation() const { + String8 result; + result.appendFormat("%s:%d", mFilename.c_str(), mLineNumber); + return result; + } + + String8 Tokenizer::peekRemainderOfLine() const { + const char *end = getEnd(); + const char *eol = mCurrent; + while (eol != end) { + char ch = *eol; + if (ch == '\n') { + break; + } + eol += 1; + } + return String8(mCurrent, eol - mCurrent); + } + + String8 Tokenizer::nextToken(const char *delimiters) { +#if DEBUG_TOKENIZER + LOGD("nextToken"); +#endif + const char *end = getEnd(); + const char *tokenStart = mCurrent; + while (mCurrent != end) { + char ch = *mCurrent; + if (ch == '\n' || isDelimiter(ch, delimiters)) { + break; + } + mCurrent += 1; + } + return String8(tokenStart, mCurrent - tokenStart); + } + + void Tokenizer::nextLine() { +#if DEBUG_TOKENIZER + LOGD("nextLine"); +#endif + const char *end = getEnd(); + while (mCurrent != end) { + char ch = *(mCurrent++); + if (ch == '\n') { + mLineNumber += 1; + break; + } + } + } + + void Tokenizer::skipDelimiters(const char *delimiters) { +#if DEBUG_TOKENIZER + LOGD("skipDelimiters"); +#endif + const char *end = getEnd(); + while (mCurrent != end) { + char ch = *mCurrent; + if (ch == '\n' || !isDelimiter(ch, delimiters)) { + break; + } + mCurrent += 1; + } + } + +} // namespace android diff --git a/sysbridge/src/main/cpp/android/utils/Tokenizer.h b/sysbridge/src/main/cpp/android/utils/Tokenizer.h new file mode 100644 index 0000000000..dee36a7bb0 --- /dev/null +++ b/sysbridge/src/main/cpp/android/utils/Tokenizer.h @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _UTILS_TOKENIZER_H +#define _UTILS_TOKENIZER_H + +#include +#include +#include "FileMap.h" +#include "Errors.h" +#include "String8.h" + +namespace android { + +/** + * A simple tokenizer for loading and parsing ASCII text files line by line. + */ +class Tokenizer { + Tokenizer(const String8& filename, FileMap* fileMap, char* buffer, + bool ownBuffer, size_t length); + +public: + ~Tokenizer(); + + /** + * Opens a file and maps it into memory. + * + * Returns OK and a tokenizer for the file, if successful. + * Otherwise returns an error and sets outTokenizer to NULL. + */ + static status_t open(const String8& filename, Tokenizer** outTokenizer); + + /** + * Prepares to tokenize the contents of a string. + * + * Returns OK and a tokenizer for the string, if successful. + * Otherwise returns an error and sets outTokenizer to NULL. + */ + static status_t fromContents(const String8& filename, + const char* contents, Tokenizer** outTokenizer); + + /** + * Returns true if at the end of the file. + */ + inline bool isEof() const { return mCurrent == getEnd(); } + + /** + * Returns true if at the end of the line or end of the file. + */ + inline bool isEol() const { return isEof() || *mCurrent == '\n'; } + + /** + * Gets the name of the file. + */ + inline String8 getFilename() const { return mFilename; } + + /** + * Gets a 1-based line number index for the current position. + */ + inline int32_t getLineNumber() const { return mLineNumber; } + + /** + * Formats a location string consisting of the filename and current line number. + * Returns a string like "MyFile.txt:33". + */ + String8 getLocation() const; + + /** + * Gets the character at the current position. + * Returns null at end of file. + */ + inline char peekChar() const { return isEof() ? '\0' : *mCurrent; } + + /** + * Gets the remainder of the current line as a string, excluding the newline character. + */ + String8 peekRemainderOfLine() const; + + /** + * Gets the character at the current position and advances past it. + * Returns null at end of file. + */ + inline char nextChar() { return isEof() ? '\0' : *(mCurrent++); } + + /** + * Gets the next token on this line stopping at the specified delimiters + * or the end of the line whichever comes first and advances past it. + * Also stops at embedded nulls. + * Returns the token or an empty string if the current character is a delimiter + * or is at the end of the line. + */ + String8 nextToken(const char* delimiters); + + /** + * Advances to the next line. + * Does nothing if already at the end of the file. + */ + void nextLine(); + + /** + * Skips over the specified delimiters in the line. + * Also skips embedded nulls. + */ + void skipDelimiters(const char* delimiters); + +private: + Tokenizer(const Tokenizer& other); // not copyable + + String8 mFilename; + FileMap* mFileMap; + char* mBuffer; + bool mOwnBuffer; + size_t mLength; + + const char* mCurrent; + int32_t mLineNumber; + + inline const char* getEnd() const { return mBuffer + mLength; } + +}; + +} // namespace android + +#endif // _UTILS_TOKENIZER_H diff --git a/sysbridge/src/main/cpp/android/utils/TypeHelpers.h b/sysbridge/src/main/cpp/android/utils/TypeHelpers.h new file mode 100644 index 0000000000..d867a9a46c --- /dev/null +++ b/sysbridge/src/main/cpp/android/utils/TypeHelpers.h @@ -0,0 +1,342 @@ +/* + * Copyright (C) 2005 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_TYPE_HELPERS_H +#define ANDROID_TYPE_HELPERS_H + +#include +#include + +#include +#include +#include +#include + +// --------------------------------------------------------------------------- + +namespace android { + +/* + * Types traits + */ + +template struct trait_trivial_ctor { enum { value = false }; }; +template struct trait_trivial_dtor { enum { value = false }; }; +template struct trait_trivial_copy { enum { value = false }; }; +template struct trait_trivial_move { enum { value = false }; }; +template struct trait_pointer { enum { value = false }; }; +template struct trait_pointer { enum { value = true }; }; + +template +struct traits { + enum { + // whether this type is a pointer + is_pointer = trait_pointer::value, + // whether this type's constructor is a no-op + has_trivial_ctor = is_pointer || trait_trivial_ctor::value, + // whether this type's destructor is a no-op + has_trivial_dtor = is_pointer || trait_trivial_dtor::value, + // whether this type type can be copy-constructed with memcpy + has_trivial_copy = is_pointer || trait_trivial_copy::value, + // whether this type can be moved with memmove + has_trivial_move = is_pointer || trait_trivial_move::value + }; +}; + +template +struct aggregate_traits { + enum { + is_pointer = false, + has_trivial_ctor = + traits::has_trivial_ctor && traits::has_trivial_ctor, + has_trivial_dtor = + traits::has_trivial_dtor && traits::has_trivial_dtor, + has_trivial_copy = + traits::has_trivial_copy && traits::has_trivial_copy, + has_trivial_move = + traits::has_trivial_move && traits::has_trivial_move + }; +}; + +#define ANDROID_TRIVIAL_CTOR_TRAIT( T ) \ + template<> struct trait_trivial_ctor< T > { enum { value = true }; }; + +#define ANDROID_TRIVIAL_DTOR_TRAIT( T ) \ + template<> struct trait_trivial_dtor< T > { enum { value = true }; }; + +#define ANDROID_TRIVIAL_COPY_TRAIT( T ) \ + template<> struct trait_trivial_copy< T > { enum { value = true }; }; + +#define ANDROID_TRIVIAL_MOVE_TRAIT( T ) \ + template<> struct trait_trivial_move< T > { enum { value = true }; }; + +#define ANDROID_BASIC_TYPES_TRAITS( T ) \ + ANDROID_TRIVIAL_CTOR_TRAIT( T ) \ + ANDROID_TRIVIAL_DTOR_TRAIT( T ) \ + ANDROID_TRIVIAL_COPY_TRAIT( T ) \ + ANDROID_TRIVIAL_MOVE_TRAIT( T ) + +// --------------------------------------------------------------------------- + +/* + * basic types traits + */ + +ANDROID_BASIC_TYPES_TRAITS( void ) +ANDROID_BASIC_TYPES_TRAITS( bool ) +ANDROID_BASIC_TYPES_TRAITS( char ) +ANDROID_BASIC_TYPES_TRAITS( unsigned char ) +ANDROID_BASIC_TYPES_TRAITS( short ) +ANDROID_BASIC_TYPES_TRAITS( unsigned short ) +ANDROID_BASIC_TYPES_TRAITS( int ) +ANDROID_BASIC_TYPES_TRAITS( unsigned int ) +ANDROID_BASIC_TYPES_TRAITS( long ) +ANDROID_BASIC_TYPES_TRAITS( unsigned long ) +ANDROID_BASIC_TYPES_TRAITS( long long ) +ANDROID_BASIC_TYPES_TRAITS( unsigned long long ) +ANDROID_BASIC_TYPES_TRAITS( float ) +ANDROID_BASIC_TYPES_TRAITS( double ) + +template struct trait_trivial_ctor { enum { value = true }; }; +template struct trait_trivial_dtor { enum { value = true }; }; +template struct trait_trivial_copy { enum { value = true }; }; +template struct trait_trivial_move { enum { value = true }; }; + +// --------------------------------------------------------------------------- + + +/* + * compare and order types + */ + +template inline +int strictly_order_type(const TYPE& lhs, const TYPE& rhs) { + return (lhs < rhs) ? 1 : 0; +} + +template inline +int compare_type(const TYPE& lhs, const TYPE& rhs) { + return strictly_order_type(rhs, lhs) - strictly_order_type(lhs, rhs); +} + +/* + * create, destroy, copy and move types... + */ + +template inline +void construct_type(TYPE* p, size_t n) { + if (!traits::has_trivial_ctor) { + while (n > 0) { + n--; + new(p++) TYPE; + } + } +} + +template inline +void destroy_type(TYPE* p, size_t n) { + if (!traits::has_trivial_dtor) { + while (n > 0) { + n--; + p->~TYPE(); + p++; + } + } +} + +template +typename std::enable_if::has_trivial_copy>::type +inline +copy_type(TYPE* d, const TYPE* s, size_t n) { + memcpy(d,s,n*sizeof(TYPE)); +} + +template +typename std::enable_if::has_trivial_copy>::type +inline +copy_type(TYPE* d, const TYPE* s, size_t n) { + while (n > 0) { + n--; + new(d) TYPE(*s); + d++, s++; + } +} + +template inline +void splat_type(TYPE* where, const TYPE* what, size_t n) { + if (!traits::has_trivial_copy) { + while (n > 0) { + n--; + new(where) TYPE(*what); + where++; + } + } else { + while (n > 0) { + n--; + *where++ = *what; + } + } +} + +template +struct use_trivial_move : public std::integral_constant::has_trivial_dtor && traits::has_trivial_copy) + || traits::has_trivial_move +> {}; + +template +typename std::enable_if::value>::type +inline +move_forward_type(TYPE* d, const TYPE* s, size_t n = 1) { + memmove(reinterpret_cast(d), s, n * sizeof(TYPE)); +} + +template +typename std::enable_if::value>::type +inline +move_forward_type(TYPE* d, const TYPE* s, size_t n = 1) { + d += n; + s += n; + while (n > 0) { + n--; + --d, --s; + if (!traits::has_trivial_copy) { + new(d) TYPE(*s); + } else { + *d = *s; + } + if (!traits::has_trivial_dtor) { + s->~TYPE(); + } + } +} + +template +typename std::enable_if::value>::type +inline +move_backward_type(TYPE* d, const TYPE* s, size_t n = 1) { + memmove(reinterpret_cast(d), s, n * sizeof(TYPE)); +} + +template +typename std::enable_if::value>::type +inline +move_backward_type(TYPE* d, const TYPE* s, size_t n = 1) { + while (n > 0) { + n--; + if (!traits::has_trivial_copy) { + new(d) TYPE(*s); + } else { + *d = *s; + } + if (!traits::has_trivial_dtor) { + s->~TYPE(); + } + d++, s++; + } +} + +// --------------------------------------------------------------------------- + +/* + * a key/value pair + */ + +template +struct key_value_pair_t { + typedef KEY key_t; + typedef VALUE value_t; + + KEY key; + VALUE value; + key_value_pair_t() { } + key_value_pair_t(const key_value_pair_t& o) : key(o.key), value(o.value) { } + key_value_pair_t& operator=(const key_value_pair_t& o) { + key = o.key; + value = o.value; + return *this; + } + key_value_pair_t(const KEY& k, const VALUE& v) : key(k), value(v) { } + explicit key_value_pair_t(const KEY& k) : key(k) { } + inline bool operator < (const key_value_pair_t& o) const { + return strictly_order_type(key, o.key); + } + inline const KEY& getKey() const { + return key; + } + inline const VALUE& getValue() const { + return value; + } +}; + +template +struct trait_trivial_ctor< key_value_pair_t > +{ enum { value = aggregate_traits::has_trivial_ctor }; }; +template +struct trait_trivial_dtor< key_value_pair_t > +{ enum { value = aggregate_traits::has_trivial_dtor }; }; +template +struct trait_trivial_copy< key_value_pair_t > +{ enum { value = aggregate_traits::has_trivial_copy }; }; +template +struct trait_trivial_move< key_value_pair_t > +{ enum { value = aggregate_traits::has_trivial_move }; }; + +// --------------------------------------------------------------------------- + +/* + * Hash codes. + */ +typedef uint32_t hash_t; + +template +hash_t hash_type(const TKey& key); + +/* Built-in hash code specializations */ +#define ANDROID_INT32_HASH(T) \ + template <> inline hash_t hash_type(const T& value) { return hash_t(value); } +#define ANDROID_INT64_HASH(T) \ + template <> inline hash_t hash_type(const T& value) { \ + return hash_t((value >> 32) ^ value); } +#define ANDROID_REINTERPRET_HASH(T, R) \ + template <> inline hash_t hash_type(const T& value) { \ + R newValue; \ + static_assert(sizeof(newValue) == sizeof(value), "size mismatch"); \ + memcpy(&newValue, &value, sizeof(newValue)); \ + return hash_type(newValue); \ + } + +ANDROID_INT32_HASH(bool) +ANDROID_INT32_HASH(int8_t) +ANDROID_INT32_HASH(uint8_t) +ANDROID_INT32_HASH(int16_t) +ANDROID_INT32_HASH(uint16_t) +ANDROID_INT32_HASH(int32_t) +ANDROID_INT32_HASH(uint32_t) +ANDROID_INT64_HASH(int64_t) +ANDROID_INT64_HASH(uint64_t) +ANDROID_REINTERPRET_HASH(float, uint32_t) +ANDROID_REINTERPRET_HASH(double, uint64_t) + +template inline hash_t hash_type(T* const & value) { + return hash_type(uintptr_t(value)); +} + +} // namespace android + +// --------------------------------------------------------------------------- + +#endif // ANDROID_TYPE_HELPERS_H diff --git a/sysbridge/src/main/cpp/android/utils/Unicode.cpp b/sysbridge/src/main/cpp/android/utils/Unicode.cpp new file mode 100644 index 0000000000..49179cc181 --- /dev/null +++ b/sysbridge/src/main/cpp/android/utils/Unicode.cpp @@ -0,0 +1,538 @@ +/* + * Copyright (C) 2005 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "unicode" + +#include +#include "Unicode.h" +#include "../../logging.h" +#include "../liblog/log_main.h" + +#include + +extern "C" { + +static const char32_t kByteMask = 0x000000BF; +static const char32_t kByteMark = 0x00000080; + +// Surrogates aren't valid for UTF-32 characters, so define some +// constants that will let us screen them out. +static const char32_t kUnicodeSurrogateHighStart = 0x0000D800; +// Unused, here for completeness: +// static const char32_t kUnicodeSurrogateHighEnd = 0x0000DBFF; +// static const char32_t kUnicodeSurrogateLowStart = 0x0000DC00; +static const char32_t kUnicodeSurrogateLowEnd = 0x0000DFFF; +static const char32_t kUnicodeSurrogateStart = kUnicodeSurrogateHighStart; +static const char32_t kUnicodeSurrogateEnd = kUnicodeSurrogateLowEnd; +static const char32_t kUnicodeMaxCodepoint = 0x0010FFFF; + +// Mask used to set appropriate bits in first byte of UTF-8 sequence, +// indexed by number of bytes in the sequence. +// 0xxxxxxx +// -> (00-7f) 7bit. Bit mask for the first byte is 0x00000000 +// 110yyyyx 10xxxxxx +// -> (c0-df)(80-bf) 11bit. Bit mask is 0x000000C0 +// 1110yyyy 10yxxxxx 10xxxxxx +// -> (e0-ef)(80-bf)(80-bf) 16bit. Bit mask is 0x000000E0 +// 11110yyy 10yyxxxx 10xxxxxx 10xxxxxx +// -> (f0-f7)(80-bf)(80-bf)(80-bf) 21bit. Bit mask is 0x000000F0 +static const char32_t kFirstByteMark[] = { + 0x00000000, 0x00000000, 0x000000C0, 0x000000E0, 0x000000F0 +}; + +// -------------------------------------------------------------------------- +// UTF-32 +// -------------------------------------------------------------------------- + +/** + * Return number of UTF-8 bytes required for the character. If the character + * is invalid, return size of 0. + */ +static inline size_t utf32_codepoint_utf8_length(char32_t srcChar) { + // Figure out how many bytes the result will require. + if (srcChar < 0x00000080) { + return 1; + } else if (srcChar < 0x00000800) { + return 2; + } else if (srcChar < 0x00010000) { + if ((srcChar < kUnicodeSurrogateStart) || (srcChar > kUnicodeSurrogateEnd)) { + return 3; + } else { + // Surrogates are invalid UTF-32 characters. + return 0; + } + } + // Max code point for Unicode is 0x0010FFFF. + else if (srcChar <= kUnicodeMaxCodepoint) { + return 4; + } else { + // Invalid UTF-32 character. + return 0; + } +} + +// Write out the source character to . + +static inline void utf32_codepoint_to_utf8(uint8_t *dstP, char32_t srcChar, size_t bytes) { + dstP += bytes; + switch (bytes) { /* note: everything falls through. */ + case 4: + *--dstP = (uint8_t) ((srcChar | kByteMark) & kByteMask); + srcChar >>= 6; + [[fallthrough]]; + case 3: + *--dstP = (uint8_t) ((srcChar | kByteMark) & kByteMask); + srcChar >>= 6; + [[fallthrough]]; + case 2: + *--dstP = (uint8_t) ((srcChar | kByteMark) & kByteMask); + srcChar >>= 6; + [[fallthrough]]; + case 1: + *--dstP = (uint8_t) (srcChar | kFirstByteMark[bytes]); + } +} + +static inline int32_t utf32_at_internal(const char *cur, size_t *num_read) { + const char first_char = *cur; + if ((first_char & 0x80) == 0) { // ASCII + *num_read = 1; + return *cur; + } + cur++; + char32_t mask, to_ignore_mask; + size_t num_to_read = 0; + char32_t utf32 = first_char; + for (num_to_read = 1, mask = 0x40, to_ignore_mask = 0xFFFFFF80; + (first_char & mask); + num_to_read++, to_ignore_mask |= mask, mask >>= 1) { + // 0x3F == 00111111 + utf32 = (utf32 << 6) + (*cur++ & 0x3F); + } + to_ignore_mask |= mask; + utf32 &= ~(to_ignore_mask << (6 * (num_to_read - 1))); + + *num_read = num_to_read; + return static_cast(utf32); +} + +int32_t utf32_from_utf8_at(const char *src, size_t src_len, size_t index, size_t *next_index) { + if (index >= src_len) { + return -1; + } + size_t unused_index; + if (next_index == nullptr) { + next_index = &unused_index; + } + size_t num_read; + int32_t ret = utf32_at_internal(src + index, &num_read); + if (ret >= 0) { + *next_index = index + num_read; + } + + return ret; +} + +ssize_t utf32_to_utf8_length(const char32_t *src, size_t src_len) { + if (src == nullptr || src_len == 0) { + return -1; + } + + size_t ret = 0; + const char32_t *end = src + src_len; + while (src < end) { + size_t char_len = utf32_codepoint_utf8_length(*src++); + if (SSIZE_MAX - char_len < ret) { + // If this happens, we would overflow the ssize_t type when + // returning from this function, so we cannot express how + // long this string is in an ssize_t. +// android_errorWriteLog(0x534e4554, "37723026"); + return -1; + } + ret += char_len; + } + return ret; +} + +void utf32_to_utf8(const char32_t *src, size_t src_len, char *dst, size_t dst_len) { + if (src == nullptr || src_len == 0 || dst == nullptr) { + return; + } + + const char32_t *cur_utf32 = src; + const char32_t *end_utf32 = src + src_len; + char *cur = dst; + while (cur_utf32 < end_utf32) { + size_t len = utf32_codepoint_utf8_length(*cur_utf32); + LOG_ALWAYS_FATAL_IF(dst_len < len, "%zu < %zu", dst_len, len); + utf32_codepoint_to_utf8((uint8_t *) cur, *cur_utf32++, len); + cur += len; + dst_len -= len; + } + LOG_ALWAYS_FATAL_IF(dst_len < 1, "dst_len < 1: %zu < 1", dst_len); + *cur = '\0'; +} + +// -------------------------------------------------------------------------- +// UTF-16 +// -------------------------------------------------------------------------- + +int strcmp16(const char16_t *s1, const char16_t *s2) { + char16_t ch; + int d = 0; + + while (1) { + d = (int) (ch = *s1++) - (int) *s2++; + if (d || !ch) + break; + } + + return d; +} + +int strncmp16(const char16_t *s1, const char16_t *s2, size_t n) { + char16_t ch; + int d = 0; + + if (n == 0) { + return 0; + } + + do { + d = (int) (ch = *s1++) - (int) *s2++; + if (d || !ch) { + break; + } + } while (--n); + + return d; +} + +size_t strlen16(const char16_t *s) { + const char16_t *ss = s; + while (*ss) + ss++; + return ss - s; +} + +size_t strnlen16(const char16_t *s, size_t maxlen) { + const char16_t *ss = s; + + /* Important: the maxlen test must precede the reference through ss; + since the byte beyond the maximum may segfault */ + while ((maxlen > 0) && *ss) { + ss++; + maxlen--; + } + return ss - s; +} + +char16_t *strstr16(const char16_t *src, const char16_t *target) { + const char16_t needle = *target; + if (needle == '\0') return (char16_t *) src; + + const size_t target_len = strlen16(++target); + do { + do { + if (*src == '\0') { + return nullptr; + } + } while (*src++ != needle); + } while (strncmp16(src, target, target_len) != 0); + src--; + + return (char16_t *) src; +} + +int strzcmp16(const char16_t *s1, size_t n1, const char16_t *s2, size_t n2) { + const char16_t *e1 = s1 + n1; + const char16_t *e2 = s2 + n2; + + while (s1 < e1 && s2 < e2) { + const int d = (int) *s1++ - (int) *s2++; + if (d) { + return d; + } + } + + return n1 < n2 + ? (0 - (int) *s2) + : (n1 > n2 + ? ((int) *s1 - 0) + : 0); +} + +// is_any_surrogate() returns true if w is either a high or low surrogate +static constexpr bool is_any_surrogate(char16_t w) { + return (w & 0xf800) == 0xd800; +} + +// is_surrogate_pair() returns true if w1 and w2 form a valid surrogate pair +static constexpr bool is_surrogate_pair(char16_t w1, char16_t w2) { + return ((w1 & 0xfc00) == 0xd800) && ((w2 & 0xfc00) == 0xdc00); +} + +// TODO: currently utf16_to_utf8_length() returns -1 if src_len == 0, +// which is inconsistent with utf8_to_utf16_length(), here we keep the +// current behavior as intended not to break compatibility +ssize_t utf16_to_utf8_length(const char16_t *src, size_t src_len) { + if (src == nullptr || src_len == 0) + return -1; + + const char16_t *const end = src + src_len; + const char16_t *in = src; + size_t utf8_len = 0; + + while (in < end) { + char16_t w = *in++; + if (w < 0x0080) [[likely]] { + utf8_len += 1; + continue; + } + if (w < 0x0800) [[likely]] { + utf8_len += 2; + continue; + } + if (!is_any_surrogate(w)) [[likely]] { + utf8_len += 3; + continue; + } + if (in < end && is_surrogate_pair(w, *in)) { + utf8_len += 4; + in++; + continue; + } + /* skip if at the end of the string or invalid surrogate pair */ + } + return (in == end && utf8_len < SSIZE_MAX) ? utf8_len : -1; +} + +void utf16_to_utf8(const char16_t *src, size_t src_len, char *dst, size_t dst_len) { + if (src == nullptr || src_len == 0 || dst == nullptr) { + return; + } + + const char16_t *in = src; + const char16_t *const in_end = src + src_len; + char *out = dst; + const char *const out_end = dst + dst_len; + char16_t w2; + + auto err_out = [&out, &out_end, &dst_len]() { + LOG_ALWAYS_FATAL_IF(out >= out_end, + "target utf8 string size %zu too short", dst_len); + }; + + while (in < in_end) { + char16_t w = *in++; + if (w < 0x0080) [[likely]] { + if (out + 1 > out_end) + return err_out(); + *out++ = (char) (w & 0xff); + continue; + } + if (w < 0x0800) [[likely]] { + if (out + 2 > out_end) + return err_out(); + *out++ = (char) (0xc0 | ((w >> 6) & 0x1f)); + *out++ = (char) (0x80 | ((w >> 0) & 0x3f)); + continue; + } + if (!is_any_surrogate(w)) [[likely]] { + if (out + 3 > out_end) + return err_out(); + *out++ = (char) (0xe0 | ((w >> 12) & 0xf)); + *out++ = (char) (0x80 | ((w >> 6) & 0x3f)); + *out++ = (char) (0x80 | ((w >> 0) & 0x3f)); + continue; + } + /* surrogate pair */ + if (in < in_end && (w2 = *in, is_surrogate_pair(w, w2))) { + if (out + 4 > out_end) + return err_out(); + char32_t dw = (char32_t) (0x10000 + ((w - 0xd800) << 10) + (w2 - 0xdc00)); + *out++ = (char) (0xf0 | ((dw >> 18) & 0x07)); + *out++ = (char) (0x80 | ((dw >> 12) & 0x3f)); + *out++ = (char) (0x80 | ((dw >> 6) & 0x3f)); + *out++ = (char) (0x80 | ((dw >> 0) & 0x3f)); + in++; + } + /* We reach here in two cases: + * 1) (in == in_end), which means end of the input string + * 2) (w2 & 0xfc00) != 0xdc00, which means invalid surrogate pair + * In either case, we intentionally do nothing and skip + */ + } + *out = '\0'; + return; +} + +// -------------------------------------------------------------------------- +// UTF-8 +// -------------------------------------------------------------------------- + +static char32_t utf8_4b_to_utf32(uint8_t c1, uint8_t c2, uint8_t c3, uint8_t c4) { + return ((c1 & 0x07) << 18) | ((c2 & 0x3f) << 12) | ((c3 & 0x3f) << 6) | (c4 & 0x3f); +} + +// TODO: current behavior of converting UTF8 to UTF-16 has a few issues below +// +// 1. invalid trailing bytes (i.e. not b'10xxxxxx) are treated as valid trailing +// bytes and follows normal conversion rules +// 2. invalid leading byte (b'10xxxxxx) is treated as a valid single UTF-8 byte +// 3. invalid leading byte (b'11111xxx) is treated as a valid leading byte +// (same as b'11110xxx) for a 4-byte UTF-8 sequence +// 4. an invalid 4-byte UTF-8 sequence that translates to a codepoint < U+10000 +// will be converted as a valid UTF-16 character +// +// We keep the current behavior as is but with warnings logged, so as not to +// break compatibility. However, this needs to be addressed later. + +ssize_t utf8_to_utf16_length(const uint8_t *u8str, size_t u8len, bool overreadIsFatal) { + if (u8str == nullptr) + return -1; + + const uint8_t *const in_end = u8str + u8len; + const uint8_t *in = u8str; + size_t utf16_len = 0; + + while (in < in_end) { + uint8_t c = *in; + utf16_len++; + if ((c & 0x80) == 0) [[likely]] { + in++; + continue; + } + if (c < 0xc0) [[unlikely]] { + LOGW("Invalid UTF-8 leading byte: 0x%02x", c); + in++; + continue; + } + if (c < 0xe0) [[likely]] { + in += 2; + continue; + } + if (c < 0xf0) [[likely]] { + in += 3; + continue; + } else { + uint8_t c2, c3, c4; + if (c >= 0xf8) [[unlikely]] { + LOGW("Invalid UTF-8 leading byte: 0x%02x", c); + } + c2 = in[1]; + c3 = in[2]; + c4 = in[3]; + if (utf8_4b_to_utf32(c, c2, c3, c4) >= 0x10000) { + utf16_len++; + } + in += 4; + continue; + } + } + if (in == in_end) { + return utf16_len < SSIZE_MAX ? utf16_len : -1; + } + if (overreadIsFatal) + LOG_ALWAYS_FATAL("Attempt to overread computing length of utf8 string"); + return -1; +} + +char16_t *utf8_to_utf16(const uint8_t *u8str, size_t u8len, char16_t *u16str, size_t u16len) { + // A value > SSIZE_MAX is probably a negative value returned as an error and casted. + LOG_ALWAYS_FATAL_IF(u16len == 0 || u16len > SSIZE_MAX, "u16len is %zu", u16len); + char16_t *end = utf8_to_utf16_no_null_terminator(u8str, u8len, u16str, u16len - 1); + *end = 0; + return end; +} + +char16_t *utf8_to_utf16_no_null_terminator( + const uint8_t *src, size_t srcLen, char16_t *dst, size_t dstLen) { + if (src == nullptr || srcLen == 0 || dstLen == 0) { + return dst; + } + // A value > SSIZE_MAX is probably a negative value returned as an error and casted. + LOG_ALWAYS_FATAL_IF(dstLen > SSIZE_MAX, "dstLen is %zu", dstLen); + + const uint8_t *const in_end = src + srcLen; + const uint8_t *in = src; + const char16_t *const out_end = dst + dstLen; + char16_t *out = dst; + uint8_t c, c2, c3, c4; + char32_t w; + + auto err_in = [&c, &out]() { + LOGW("Unended UTF-8 byte: 0x%02x", c); + return out; + }; + + while (in < in_end && out < out_end) { + c = *in++; + if ((c & 0x80) == 0) [[likely]] { + *out++ = (char16_t) (c); + continue; + } + if (c < 0xc0) [[unlikely]] { + ALOGW("Invalid UTF-8 leading byte: 0x%02x", c); + *out++ = (char16_t) (c); + continue; + } + if (c < 0xe0) [[likely]] { + if (in + 1 > in_end) [[unlikely]] { + return err_in(); + } + c2 = *in++; + *out++ = (char16_t) (((c & 0x1f) << 6) | (c2 & 0x3f)); + continue; + } + if (c < 0xf0) [[likely]] { + if (in + 2 > in_end) [[unlikely]] { + return err_in(); + } + c2 = *in++; + c3 = *in++; + *out++ = (char16_t) (((c & 0x0f) << 12) | + ((c2 & 0x3f) << 6) | (c3 & 0x3f)); + continue; + } else { + if (in + 3 > in_end) [[unlikely]] { + return err_in(); + } + if (c >= 0xf8) [[unlikely]] { + LOGW("Invalid UTF-8 leading byte: 0x%02x", c); + } + // Multiple UTF16 characters with surrogates + c2 = *in++; + c3 = *in++; + c4 = *in++; + w = utf8_4b_to_utf32(c, c2, c3, c4); + if (w < 0x10000) [[unlikely]] { + *out++ = (char16_t) (w); + } else { + if (out + 2 > out_end) [[unlikely]] { + // Ooops.... not enough room for this surrogate pair. + return out; + } + *out++ = (char16_t) (((w - 0x10000) >> 10) + 0xd800); + *out++ = (char16_t) (((w - 0x10000) & 0x3ff) + 0xdc00); + } + continue; + } + } + return out; +} + +} diff --git a/sysbridge/src/main/cpp/android/utils/Unicode.h b/sysbridge/src/main/cpp/android/utils/Unicode.h new file mode 100644 index 0000000000..d60d5d6ba6 --- /dev/null +++ b/sysbridge/src/main/cpp/android/utils/Unicode.h @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2005 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef ANDROID_UNICODE_H +#define ANDROID_UNICODE_H + +#include +#include + +extern "C" { + +// Standard string functions on char16_t strings. +int strcmp16(const char16_t *, const char16_t *); +int strncmp16(const char16_t *s1, const char16_t *s2, size_t n); +size_t strlen16(const char16_t *); +size_t strnlen16(const char16_t *, size_t); +char16_t *strstr16(const char16_t*, const char16_t*); + +// Version of comparison that supports embedded NULs. +// This is different than strncmp() because we don't stop +// at a nul character and consider the strings to be different +// if the lengths are different (thus we need to supply the +// lengths of both strings). This can also be used when +// your string is not nul-terminated as it will have the +// equivalent result as strcmp16 (unlike strncmp16). +int strzcmp16(const char16_t *s1, size_t n1, const char16_t *s2, size_t n2); + +/** + * Measure the length of a UTF-32 string in UTF-8. If the string is invalid + * such as containing a surrogate character, -1 will be returned. + */ +ssize_t utf32_to_utf8_length(const char32_t *src, size_t src_len); + +/** + * Stores a UTF-8 string converted from "src" in "dst", if "dst_length" is not + * large enough to store the string, the part of the "src" string is stored + * into "dst" as much as possible. See the examples for more detail. + * Returns the size actually used for storing the string. + * dst" is not nul-terminated when dst_len is fully used (like strncpy). + * + * \code + * Example 1 + * "src" == \u3042\u3044 (\xE3\x81\x82\xE3\x81\x84) + * "src_len" == 2 + * "dst_len" >= 7 + * -> + * Returned value == 6 + * "dst" becomes \xE3\x81\x82\xE3\x81\x84\0 + * (note that "dst" is nul-terminated) + * + * Example 2 + * "src" == \u3042\u3044 (\xE3\x81\x82\xE3\x81\x84) + * "src_len" == 2 + * "dst_len" == 5 + * -> + * Returned value == 3 + * "dst" becomes \xE3\x81\x82\0 + * (note that "dst" is nul-terminated, but \u3044 is not stored in "dst" + * since "dst" does not have enough size to store the character) + * + * Example 3 + * "src" == \u3042\u3044 (\xE3\x81\x82\xE3\x81\x84) + * "src_len" == 2 + * "dst_len" == 6 + * -> + * Returned value == 6 + * "dst" becomes \xE3\x81\x82\xE3\x81\x84 + * (note that "dst" is NOT nul-terminated, like strncpy) + * \endcode + */ +void utf32_to_utf8(const char32_t* src, size_t src_len, char* dst, size_t dst_len); + +/** + * Returns the unicode value at "index". + * Returns -1 when the index is invalid (equals to or more than "src_len"). + * If returned value is positive, it is able to be converted to char32_t, which + * is unsigned. Then, if "next_index" is not NULL, the next index to be used is + * stored in "next_index". "next_index" can be NULL. + */ +int32_t utf32_from_utf8_at(const char *src, size_t src_len, size_t index, size_t *next_index); + + +/** + * Returns the UTF-8 length of UTF-16 string "src". + */ +ssize_t utf16_to_utf8_length(const char16_t *src, size_t src_len); + +/** + * Converts a UTF-16 string to UTF-8. The destination buffer must be large + * enough to fit the UTF-16 as measured by utf16_to_utf8_length with an added + * NUL terminator. + */ +void utf16_to_utf8(const char16_t* src, size_t src_len, char* dst, size_t dst_len); + +/** + * Returns the UTF-16 length of UTF-8 string "src". Returns -1 in case + * it's invalid utf8. No buffer over-read occurs because of bound checks. Using overreadIsFatal you + * can ask to log a message and fail in case the invalid utf8 could have caused an override if no + * bound checks were used (otherwise -1 is returned). + */ +ssize_t utf8_to_utf16_length(const uint8_t* src, size_t srcLen, bool overreadIsFatal = false); + +/** + * Convert UTF-8 to UTF-16 including surrogate pairs. + * Returns a pointer to the end of the string (where a NUL terminator might go + * if you wanted to add one). At most dstLen characters are written; it won't emit half a surrogate + * pair. If dstLen == 0 nothing is written and dst is returned. If dstLen > SSIZE_MAX it aborts + * (this being probably a negative number returned as an error and casted to unsigned). + */ +char16_t* utf8_to_utf16_no_null_terminator( + const uint8_t* src, size_t srcLen, char16_t* dst, size_t dstLen); + +/** + * Convert UTF-8 to UTF-16 including surrogate pairs. At most dstLen - 1 + * characters are written; it won't emit half a surrogate pair; and a NUL terminator is appended + * after. dstLen - 1 can be measured beforehand using utf8_to_utf16_length. Aborts if dstLen == 0 + * (at least one character is needed for the NUL terminator) or dstLen > SSIZE_MAX (the latter + * case being likely a negative number returned as an error and casted to unsigned) . Returns a + * pointer to the NUL terminator. + */ +char16_t *utf8_to_utf16( + const uint8_t* src, size_t srcLen, char16_t* dst, size_t dstLen); + +} + +#endif diff --git a/sysbridge/src/main/cpp/cgroup.cpp b/sysbridge/src/main/cpp/cgroup.cpp new file mode 100644 index 0000000000..2fe9af242b --- /dev/null +++ b/sysbridge/src/main/cpp/cgroup.cpp @@ -0,0 +1,73 @@ +#include +#include +#include +#include + +namespace cgroup { + + static ssize_t fdgets(char *buf, const size_t size, int fd) { + ssize_t len = 0; + buf[0] = '\0'; + while (len < size - 1) { + ssize_t ret = read(fd, buf + len, 1); + if (ret < 0) + return -1; + if (ret == 0) + break; + if (buf[len] == '\0' || buf[len++] == '\n') { + break; + } + } + buf[len] = '\0'; + buf[size - 1] = '\0'; + return len; + } + + int get_cgroup(int pid, int *cuid, int *cpid) { + char buf[PATH_MAX]; + snprintf(buf, PATH_MAX, "/proc/%d/cgroup", pid); + + int fd = open(buf, O_RDONLY); + if (fd == -1) + return -1; + + while (fdgets(buf, PATH_MAX, fd) > 0) { + if (sscanf(buf, "%*d:cpuacct:/uid_%d/pid_%d", cuid, cpid) == 2) { + close(fd); + return 0; + } + } + close(fd); + return -1; + } + + static int switch_cgroup(int pid, int cuid, int cpid, const char *name) { + char buf[PATH_MAX]; + if (cuid != -1 && cpid != -1) { + snprintf(buf, PATH_MAX, "/acct/uid_%d/pid_%d/%s", cuid, cpid, name); + } else { + snprintf(buf, PATH_MAX, "/acct/%s", name); + } + + int fd = open(buf, O_WRONLY | O_APPEND); + if (fd == -1) + return -1; + + snprintf(buf, PATH_MAX, "%d\n", pid); + if (write(fd, buf, strlen(buf)) == -1) { + close(fd); + return -1; + } + + close(fd); + return 0; + } + + int switch_cgroup(int pid, int cuid, int cpid) { + int res = 0; + res += switch_cgroup(pid, cuid, cpid, "cgroup.procs"); + res += switch_cgroup(pid, cuid, cpid, "tasks"); + return res; + } + +} \ No newline at end of file diff --git a/sysbridge/src/main/cpp/cgroup.h b/sysbridge/src/main/cpp/cgroup.h new file mode 100644 index 0000000000..361a0b1919 --- /dev/null +++ b/sysbridge/src/main/cpp/cgroup.h @@ -0,0 +1,9 @@ +#ifndef CGROUP_H +#define CGROUP_H + +namespace cgroup { + int get_cgroup(int pid, int* cuid, int *cpid); + int switch_cgroup(int pid, int cuid, int cpid); +} + +#endif // CGROUP_H diff --git a/sysbridge/src/main/cpp/libevdev/Makefile.am b/sysbridge/src/main/cpp/libevdev/Makefile.am new file mode 100644 index 0000000000..f577900827 --- /dev/null +++ b/sysbridge/src/main/cpp/libevdev/Makefile.am @@ -0,0 +1,42 @@ +lib_LTLIBRARIES=libevdev.la + +AM_CPPFLAGS = $(GCC_CFLAGS) $(GCOV_CFLAGS) -I$(top_srcdir)/include -I$(top_srcdir) +AM_LDFLAGS = $(GCOV_LDFLAGS) + +libevdev_la_SOURCES = \ + libevdev.h \ + libevdev-int.h \ + libevdev-util.h \ + libevdev-uinput.c \ + libevdev-uinput.h \ + libevdev-uinput-int.h \ + libevdev.c \ + libevdev-names.c \ + ../include/linux/input.h \ + ../include/linux/uinput.h \ + ../include/linux/@OS@/input-event-codes.h \ + ../include/linux/@OS@/input.h \ + ../include/linux/@OS@/uinput.h + +libevdev_la_LDFLAGS = \ + $(AM_LDFLAGS) \ + -version-info $(LIBEVDEV_LT_VERSION) \ + -Wl,--version-script="$(srcdir)/libevdev.sym" \ + $(GNU_LD_FLAGS) + +EXTRA_libevdev_la_DEPENDENCIES = $(srcdir)/libevdev.sym + +libevdevincludedir = $(includedir)/libevdev-1.0/libevdev +libevdevinclude_HEADERS = libevdev.h libevdev-uinput.h + +event-names.h: Makefile make-event-names.py + $(PYTHON) $(srcdir)/make-event-names.py $(top_srcdir)/include/linux/@OS@/input.h $(top_srcdir)/include/linux/@OS@/input-event-codes.h > $@ + + +EXTRA_DIST = make-event-names.py libevdev.sym ../include +CLEANFILES = event-names.h +BUILT_SOURCES = event-names.h + +if GCOV_ENABLED +CLEANFILES += *.gcno +endif diff --git a/sysbridge/src/main/cpp/libevdev/libevdev-int.h b/sysbridge/src/main/cpp/libevdev/libevdev-int.h new file mode 100644 index 0000000000..3f47dfba15 --- /dev/null +++ b/sysbridge/src/main/cpp/libevdev/libevdev-int.h @@ -0,0 +1,317 @@ +// SPDX-License-Identifier: MIT +/* + * Copyright © 2013 Red Hat, Inc. + */ + +#ifndef LIBEVDEV_INT_H +#define LIBEVDEV_INT_H + +#include +#include +#include +#include +#include "libevdev.h" +#include "libevdev-util.h" + +#define MAX_NAME 256 +#define ABS_MT_MIN ABS_MT_SLOT +#define ABS_MT_MAX ABS_MT_TOOL_Y +#define ABS_MT_CNT (ABS_MT_MAX - ABS_MT_MIN + 1) +#define LIBEVDEV_EXPORT __attribute__((visibility("default"))) +#define ALIAS(_to) __attribute__((alias(#_to))) + +/** + * Sync state machine: + * default state: SYNC_NONE + * + * SYNC_NONE → SYN_DROPPED or forced sync → SYNC_NEEDED + * SYNC_NEEDED → libevdev_next_event(LIBEVDEV_READ_FLAG_SYNC) → SYNC_IN_PROGRESS + * SYNC_NEEDED → libevdev_next_event(LIBEVDEV_READ_FLAG_SYNC_NONE) → SYNC_NONE + * SYNC_IN_PROGRESS → libevdev_next_event(LIBEVDEV_READ_FLAG_SYNC_NONE) → SYNC_NONE + * SYNC_IN_PROGRESS → no sync events left → SYNC_NONE + * + */ +enum SyncState { + SYNC_NONE, + SYNC_NEEDED, + SYNC_IN_PROGRESS, +}; + +/** + * Internal only: log data used to send messages to the respective log + * handler. We re-use the same struct for a global and inside + * struct libevdev. + * For the global, device_handler is NULL, for per-device instance + * global_handler is NULL. + */ +struct logdata { + enum libevdev_log_priority priority; /** minimum logging priority */ + libevdev_log_func_t global_handler; /** global handler function */ + libevdev_device_log_func_t device_handler; /** per-device handler function */ + void *userdata; /** user-defined data pointer */ +}; + +struct libevdev { + int fd; + bool initialized; + char *name; + char *phys; + char *uniq; + struct input_id ids; + int driver_version; + unsigned long bits[NLONGS(EV_CNT)]; + unsigned long props[NLONGS(INPUT_PROP_CNT)]; + unsigned long key_bits[NLONGS(KEY_CNT)]; + unsigned long rel_bits[NLONGS(REL_CNT)]; + unsigned long abs_bits[NLONGS(ABS_CNT)]; + unsigned long led_bits[NLONGS(LED_CNT)]; + unsigned long msc_bits[NLONGS(MSC_CNT)]; + unsigned long sw_bits[NLONGS(SW_CNT)]; + unsigned long rep_bits[NLONGS(REP_CNT)]; /* convenience, always 1 */ + unsigned long ff_bits[NLONGS(FF_CNT)]; + unsigned long snd_bits[NLONGS(SND_CNT)]; + unsigned long key_values[NLONGS(KEY_CNT)]; + unsigned long led_values[NLONGS(LED_CNT)]; + unsigned long sw_values[NLONGS(SW_CNT)]; + struct input_absinfo abs_info[ABS_CNT]; + int *mt_slot_vals; /* [num_slots * ABS_MT_CNT] */ + int num_slots; /**< valid slots in mt_slot_vals */ + int current_slot; + int rep_values[REP_CNT]; + + enum SyncState sync_state; + enum libevdev_grab_mode grabbed; + + struct input_event *queue; + size_t queue_size; /**< size of queue in elements */ + size_t queue_next; /**< next event index */ + size_t queue_nsync; /**< number of sync events */ + + struct timeval last_event_time; + + struct logdata log; +}; + +#define log_msg_cond(dev, priority, ...) \ + do { \ + if (_libevdev_log_priority(dev) >= priority) \ + _libevdev_log_msg(dev, priority, __FILE__, __LINE__, __func__, __VA_ARGS__); \ + } while(0) + +#define log_error(dev, ...) log_msg_cond(dev, LIBEVDEV_LOG_ERROR, __VA_ARGS__) +#define log_info(dev, ...) log_msg_cond(dev, LIBEVDEV_LOG_INFO, __VA_ARGS__) +#define log_dbg(dev, ...) log_msg_cond(dev, LIBEVDEV_LOG_DEBUG, __VA_ARGS__) +#define log_bug(dev, ...) log_msg_cond(dev, LIBEVDEV_LOG_ERROR, "BUG: "__VA_ARGS__) + +extern void +_libevdev_log_msg(const struct libevdev *dev, + enum libevdev_log_priority priority, + const char *file, int line, const char *func, + const char *format, ...) LIBEVDEV_ATTRIBUTE_PRINTF(6, 7); + +extern enum libevdev_log_priority +_libevdev_log_priority(const struct libevdev *dev); + +static inline void +init_event(struct libevdev *dev, struct input_event *ev, int type, int code, int value) { + ev->input_event_sec = dev->last_event_time.tv_sec; + ev->input_event_usec = dev->last_event_time.tv_usec; + ev->type = type; + ev->code = code; + ev->value = value; +} + +/** + * @return a pointer to the next element in the queue, or NULL if the queue + * is full. + */ +static inline struct input_event * +queue_push(struct libevdev *dev) { + if (dev->queue_next >= dev->queue_size) + return NULL; + + return &dev->queue[dev->queue_next++]; +} + +static inline bool +queue_push_event(struct libevdev *dev, unsigned int type, + unsigned int code, int value) { + struct input_event *ev = queue_push(dev); + + if (ev) + init_event(dev, ev, type, code, value); + + return ev != NULL; +} + +/** + * Set ev to the last element in the queue, removing it from the queue. + * + * @return 0 on success, 1 if the queue is empty. + */ +static inline int +queue_pop(struct libevdev *dev, struct input_event *ev) { + if (dev->queue_next == 0) + return 1; + + *ev = dev->queue[--dev->queue_next]; + + return 0; +} + +static inline int +queue_peek(struct libevdev *dev, size_t idx, struct input_event *ev) { + if (dev->queue_next == 0 || idx > dev->queue_next) + return 1; + *ev = dev->queue[idx]; + return 0; +} + +/** + * Shift the first n elements into ev and return the number of elements + * shifted. + * ev must be large enough to store n elements. + * + * @param ev The buffer to copy into, or NULL + * @return The number of elements in ev. + */ +static inline int +queue_shift_multiple(struct libevdev *dev, size_t n, struct input_event *ev) { + size_t remaining; + + if (dev->queue_next == 0) + return 0; + + remaining = dev->queue_next; + n = min(n, remaining); + remaining -= n; + + if (ev) + memcpy(ev, dev->queue, n * sizeof(*ev)); + + memmove(dev->queue, &dev->queue[n], remaining * sizeof(*dev->queue)); + + dev->queue_next = remaining; + return n; +} + +/** + * Set ev to the first element in the queue, shifting everything else + * forward by one. + * + * @return 0 on success, 1 if the queue is empty. + */ +static inline int +queue_shift(struct libevdev *dev, struct input_event *ev) { + return queue_shift_multiple(dev, 1, ev) == 1 ? 0 : 1; +} + +static inline int +queue_alloc(struct libevdev *dev, size_t size) { + if (size == 0) + return -ENOMEM; + + dev->queue = calloc(size, sizeof(struct input_event)); + if (!dev->queue) + return -ENOMEM; + + dev->queue_size = size; + dev->queue_next = 0; + return 0; +} + +static inline void +queue_free(struct libevdev *dev) { + free(dev->queue); + dev->queue_size = 0; + dev->queue_next = 0; +} + +static inline size_t +queue_num_elements(struct libevdev *dev) { + return dev->queue_next; +} + +static inline size_t +queue_size(struct libevdev *dev) { + return dev->queue_size; +} + +static inline size_t +queue_num_free_elements(struct libevdev *dev) { + if (dev->queue_size == 0) + return 0; + + return dev->queue_size - dev->queue_next; +} + +static inline struct input_event * +queue_next_element(struct libevdev *dev) { + if (dev->queue_next == dev->queue_size) + return NULL; + + return &dev->queue[dev->queue_next]; +} + +static inline int +queue_set_num_elements(struct libevdev *dev, size_t nelem) { + if (nelem > dev->queue_size) + return 1; + + dev->queue_next = nelem; + + return 0; +} + +#define max_mask(uc, lc) \ + case EV_##uc: \ + *mask = dev->lc##_bits; \ + max = libevdev_event_type_get_max(type); \ + break; + +static inline int +type_to_mask_const(const struct libevdev *dev, unsigned int type, const unsigned long **mask) { + int max; + + switch (type) { + max_mask(ABS, abs); + max_mask(REL, rel); + max_mask(KEY, key); + max_mask(LED, led); + max_mask(MSC, msc); + max_mask(SW, sw); + max_mask(FF, ff); + max_mask(REP, rep); + max_mask(SND, snd); + default: + max = -1; + break; + } + + return max; +} + +static inline int +type_to_mask(struct libevdev *dev, unsigned int type, unsigned long **mask) { + int max; + + switch (type) { + max_mask(ABS, abs); + max_mask(REL, rel); + max_mask(KEY, key); + max_mask(LED, led); + max_mask(MSC, msc); + max_mask(SW, sw); + max_mask(FF, ff); + max_mask(REP, rep); + max_mask(SND, snd); + default: + max = -1; + break; + } + + return max; +} + +#undef max_mask +#endif diff --git a/sysbridge/src/main/cpp/libevdev/libevdev-names.c b/sysbridge/src/main/cpp/libevdev/libevdev-names.c new file mode 100644 index 0000000000..f8c991b84a --- /dev/null +++ b/sysbridge/src/main/cpp/libevdev/libevdev-names.c @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: MIT +/* + * Copyright © 2013 David Herrmann + */ + +#include +#include +#include +#include + +#include "libevdev-int.h" +#include "libevdev-util.h" +#include "libevdev.h" + +#include "event-names.h" + +struct name_lookup { + const char *name; + size_t len; +}; + +static int cmp_entry(const void *vlookup, const void *ventry) +{ + const struct name_lookup *lookup = vlookup; + const struct name_entry *entry = ventry; + int r; + + r = strncmp(lookup->name, entry->name, lookup->len); + if (!r) { + if (entry->name[lookup->len]) + r = -1; + else + r = 0; + } + + return r; +} + +static const struct name_entry* +lookup_name(const struct name_entry *array, size_t asize, + struct name_lookup *lookup) +{ + const struct name_entry *entry; + + entry = bsearch(lookup, array, asize, sizeof(*array), cmp_entry); + if (!entry) + return NULL; + + return entry; +} + +LIBEVDEV_EXPORT int +libevdev_event_type_from_name(const char *name) +{ + return libevdev_event_type_from_name_n(name, strlen(name)); +} + +LIBEVDEV_EXPORT int +libevdev_event_type_from_name_n(const char *name, size_t len) +{ + struct name_lookup lookup; + const struct name_entry *entry; + + lookup.name = name; + lookup.len = len; + + entry = lookup_name(ev_names, ARRAY_LENGTH(ev_names), &lookup); + + return entry ? (int)entry->value : -1; +} + +static int type_from_prefix(const char *name, ssize_t len) +{ + const char *e; + size_t i; + ssize_t l; + + /* MAX_ is not allowed, even though EV_MAX exists */ + if (startswith(name, len, "MAX_", 4)) + return -1; + /* BTN_ is special as there is no EV_BTN type */ + if (startswith(name, len, "BTN_", 4)) + return EV_KEY; + /* FF_STATUS_ is special as FF_ is a prefix of it, so test it first */ + if (startswith(name, len, "FF_STATUS_", 10)) + return EV_FF_STATUS; + + for (i = 0; i < ARRAY_LENGTH(ev_names); ++i) { + /* skip EV_ prefix so @e is suffix of [EV_]XYZ */ + e = &ev_names[i].name[3]; + l = strlen(e); + + /* compare prefix and test for trailing _ */ + if (len > l && startswith(name, len, e, l) && name[l] == '_') + return ev_names[i].value; + } + + return -1; +} + +LIBEVDEV_EXPORT int +libevdev_event_code_from_name(unsigned int type, const char *name) +{ + return libevdev_event_code_from_name_n(type, name, strlen(name)); +} + +LIBEVDEV_EXPORT int +libevdev_event_code_from_name_n(unsigned int type, const char *name, size_t len) +{ + struct name_lookup lookup; + const struct name_entry *entry; + int real_type; + + /* verify that @name is really of type @type */ + real_type = type_from_prefix(name, len); + if (real_type < 0 || (unsigned int)real_type != type) + return -1; + + /* now look up the name @name and return the constant */ + lookup.name = name; + lookup.len = len; + + entry = lookup_name(code_names, ARRAY_LENGTH(code_names), &lookup); + + return entry ? (int)entry->value : -1; +} + +LIBEVDEV_EXPORT int +libevdev_event_value_from_name(unsigned int type, unsigned int code, const char *name) +{ + return libevdev_event_value_from_name_n(type, code, name, strlen(name)); +} + +LIBEVDEV_EXPORT int +libevdev_event_value_from_name_n(unsigned int type, unsigned int code, const char *name, size_t len) +{ + struct name_lookup lookup; + const struct name_entry *entry; + + if (type != EV_ABS || code != ABS_MT_TOOL_TYPE) + return -1; + + lookup.name = name; + lookup.len = len; + + entry = lookup_name(tool_type_names, ARRAY_LENGTH(tool_type_names), &lookup); + + return entry ? (int)entry->value : -1; +} + +LIBEVDEV_EXPORT int +libevdev_property_from_name(const char *name) +{ + return libevdev_property_from_name_n(name, strlen(name)); +} + +LIBEVDEV_EXPORT int +libevdev_property_from_name_n(const char *name, size_t len) +{ + struct name_lookup lookup; + const struct name_entry *entry; + + lookup.name = name; + lookup.len = len; + + entry = lookup_name(prop_names, ARRAY_LENGTH(prop_names), &lookup); + + return entry ? (int)entry->value : -1; +} + +LIBEVDEV_EXPORT int +libevdev_event_code_from_code_name(const char *name) +{ + return libevdev_event_code_from_code_name_n(name, strlen(name)); +} + +LIBEVDEV_EXPORT int +libevdev_event_code_from_code_name_n(const char *name, size_t len) +{ + const struct name_entry *entry; + struct name_lookup lookup; + + /* now look up the name @name and return the constant */ + lookup.name = name; + lookup.len = len; + + entry = lookup_name(code_names, ARRAY_LENGTH(code_names), &lookup); + + return entry ? (int)entry->value : -1; +} + +LIBEVDEV_EXPORT int +libevdev_event_type_from_code_name(const char *name) +{ + return libevdev_event_type_from_code_name_n(name, strlen(name)); +} + +LIBEVDEV_EXPORT int +libevdev_event_type_from_code_name_n(const char *name, size_t len) +{ + const struct name_entry *entry; + struct name_lookup lookup; + + /* First look up if the name exists, we dont' want to return a valid + * type for an invalid code name */ + lookup.name = name; + lookup.len = len; + + entry = lookup_name(code_names, ARRAY_LENGTH(code_names), &lookup); + + return entry ? type_from_prefix(name, len) : -1; +} diff --git a/sysbridge/src/main/cpp/libevdev/libevdev-uinput-int.h b/sysbridge/src/main/cpp/libevdev/libevdev-uinput-int.h new file mode 100644 index 0000000000..c6bf015497 --- /dev/null +++ b/sysbridge/src/main/cpp/libevdev/libevdev-uinput-int.h @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +/* + * Copyright © 2013 Red Hat, Inc. + */ + +struct libevdev_uinput { + int fd; /**< file descriptor to uinput */ + int fd_is_managed; /**< do we need to close it? */ + char *name; /**< device name */ + char *syspath; /**< /sys path */ + char *devnode; /**< device node */ + time_t ctime[2]; /**< before/after UI_DEV_CREATE */ +}; diff --git a/sysbridge/src/main/cpp/libevdev/libevdev-uinput.c b/sysbridge/src/main/cpp/libevdev/libevdev-uinput.c new file mode 100644 index 0000000000..cdce8dead4 --- /dev/null +++ b/sysbridge/src/main/cpp/libevdev/libevdev-uinput.c @@ -0,0 +1,494 @@ +// SPDX-License-Identifier: MIT +/* + * Copyright © 2013 Red Hat, Inc. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "libevdev-int.h" +#include "libevdev-uinput-int.h" +#include "libevdev-uinput.h" +#include "libevdev-util.h" +#include "libevdev.h" + +#ifndef UINPUT_IOCTL_BASE +#define UINPUT_IOCTL_BASE 'U' +#endif + +#ifndef UI_SET_PROPBIT +#define UI_SET_PROPBIT _IOW(UINPUT_IOCTL_BASE, 110, int) +#endif + +static struct libevdev_uinput * +alloc_uinput_device(const char *name) { + struct libevdev_uinput *uinput_dev; + + uinput_dev = calloc(1, sizeof(struct libevdev_uinput)); + if (uinput_dev) { + uinput_dev->name = strdup(name); + uinput_dev->fd = -1; + } + + return uinput_dev; +} + +static inline int +set_abs(const struct libevdev *dev, int fd, unsigned int code) { + const struct input_absinfo *abs = libevdev_get_abs_info(dev, code); + struct uinput_abs_setup abs_setup = {0}; + int rc; + + abs_setup.code = code; + abs_setup.absinfo = *abs; + rc = ioctl(fd, UI_ABS_SETUP, &abs_setup); + return rc; +} + +static int +set_evbits(const struct libevdev *dev, int fd, struct uinput_user_dev *uidev) { + int rc = 0; + unsigned int type; + + for (type = 0; type < EV_CNT; type++) { + unsigned int code; + int max; + int uinput_bit; + const unsigned long *mask; + + if (!libevdev_has_event_type(dev, type)) + continue; + + rc = ioctl(fd, UI_SET_EVBIT, type); + if (rc == -1) + break; + + /* uinput can't set EV_REP */ + if (type == EV_REP) + continue; + + max = type_to_mask_const(dev, type, &mask); + if (max == -1) + continue; + + switch (type) { + case EV_KEY: + uinput_bit = UI_SET_KEYBIT; + break; + case EV_REL: + uinput_bit = UI_SET_RELBIT; + break; + case EV_ABS: + uinput_bit = UI_SET_ABSBIT; + break; + case EV_MSC: + uinput_bit = UI_SET_MSCBIT; + break; + case EV_LED: + uinput_bit = UI_SET_LEDBIT; + break; + case EV_SND: + uinput_bit = UI_SET_SNDBIT; + break; + case EV_FF: + uinput_bit = UI_SET_FFBIT; + break; + case EV_SW: + uinput_bit = UI_SET_SWBIT; + break; + default: + rc = -1; + errno = EINVAL; + goto out; + } + + for (code = 0; code <= (unsigned int) max; code++) { + if (!libevdev_has_event_code(dev, type, code)) + continue; + + rc = ioctl(fd, uinput_bit, code); + if (rc == -1) + goto out; + + if (type == EV_ABS) { + if (uidev == NULL) { + rc = set_abs(dev, fd, code); + if (rc != 0) + goto out; + } else { + const struct input_absinfo *abs = + libevdev_get_abs_info(dev, code); + + uidev->absmin[code] = abs->minimum; + uidev->absmax[code] = abs->maximum; + uidev->absfuzz[code] = abs->fuzz; + uidev->absflat[code] = abs->flat; + /* uinput has no resolution in the + * device struct */ + } + } + } + + } + + out: + return rc; +} + +static int +set_props(const struct libevdev *dev, int fd) { + unsigned int prop; + int rc = 0; + + for (prop = 0; prop <= INPUT_PROP_MAX; prop++) { + if (!libevdev_has_property(dev, prop)) + continue; + + rc = ioctl(fd, UI_SET_PROPBIT, prop); + if (rc == -1) { + /* If UI_SET_PROPBIT is not supported, treat -EINVAL + * as success. The kernel only sends -EINVAL for an + * invalid ioctl, invalid INPUT_PROP_MAX or if the + * ioctl is called on an already created device. The + * last two can't happen here. + */ + if (errno == EINVAL) + rc = 0; + break; + } + } + return rc; +} + +LIBEVDEV_EXPORT int +libevdev_uinput_get_fd(const struct libevdev_uinput *uinput_dev) { + return uinput_dev->fd; +} + +#ifdef __FreeBSD__ +/* + * FreeBSD does not have anything similar to sysfs. + * Set libevdev_uinput->syspath to NULL unconditionally. + * Look up the device nodes directly instead of via sysfs, as this matches what + * is returned by the UI_GET_SYSNAME ioctl() on FreeBSD. + */ +static int +fetch_syspath_and_devnode(struct libevdev_uinput *uinput_dev) +{ +#define DEV_INPUT_DIR "/dev/input/" + int rc; + char buf[sizeof(DEV_INPUT_DIR) + 64] = DEV_INPUT_DIR; + + rc = ioctl(uinput_dev->fd, + UI_GET_SYSNAME(sizeof(buf) - strlen(DEV_INPUT_DIR)), + &buf[strlen(DEV_INPUT_DIR)]); + if (rc == -1) + return -1; + + uinput_dev->syspath = NULL; + uinput_dev->devnode = strdup(buf); + + return 0; +#undef DEV_INPUT_DIR +} + +#else /* !__FreeBSD__ */ + +static int is_event_device(const struct dirent *dent) { + return strncmp("event", dent->d_name, 5) == 0; +} + +static char * +fetch_device_node(const char *path) { + char *devnode = NULL; + struct dirent **namelist; + int ndev, i; + + ndev = scandir(path, &namelist, is_event_device, alphasort); + if (ndev <= 0) + return NULL; + + /* ndev should only ever be 1 */ + + for (i = 0; i < ndev; i++) { + if (!devnode && asprintf(&devnode, "/dev/input/%s", namelist[i]->d_name) == -1) + devnode = NULL; + free(namelist[i]); + } + + free(namelist); + + return devnode; +} + +static int is_input_device(const struct dirent *dent) { + return strncmp("input", dent->d_name, 5) == 0; +} + +static int +fetch_syspath_and_devnode(struct libevdev_uinput *uinput_dev) { +#define SYS_INPUT_DIR "/sys/devices/virtual/input/" + struct dirent **namelist; + int ndev, i; + int rc; + char buf[sizeof(SYS_INPUT_DIR) + 64] = SYS_INPUT_DIR; + + rc = ioctl(uinput_dev->fd, + UI_GET_SYSNAME(sizeof(buf) - strlen(SYS_INPUT_DIR)), + &buf[strlen(SYS_INPUT_DIR)]); + if (rc != -1) { + uinput_dev->syspath = strdup(buf); + uinput_dev->devnode = fetch_device_node(buf); + return 0; + } + + ndev = scandir(SYS_INPUT_DIR, &namelist, is_input_device, alphasort); + if (ndev <= 0) + return -1; + + for (i = 0; i < ndev; i++) { + int fd, len; + struct stat st; + + rc = snprintf(buf, sizeof(buf), "%s%s/name", + SYS_INPUT_DIR, + namelist[i]->d_name); + if (rc < 0 || (size_t) rc >= sizeof(buf)) { + continue; + } + + /* created within time frame */ + fd = open(buf, O_RDONLY); + if (fd < 0) + continue; + + /* created before UI_DEV_CREATE, or after it finished */ + if (fstat(fd, &st) == -1 || + st.st_ctime < uinput_dev->ctime[0] || + st.st_ctime > uinput_dev->ctime[1]) { + close(fd); + continue; + } + + len = read(fd, buf, sizeof(buf)); + close(fd); + if (len <= 0) + continue; + + buf[len - 1] = '\0'; /* file contains \n */ + if (strcmp(buf, uinput_dev->name) == 0) { + if (uinput_dev->syspath) { + /* FIXME: could descend into bit comparison here */ + log_info(NULL, "multiple identical devices found. syspath is unreliable\n"); + break; + } + + rc = snprintf(buf, sizeof(buf), "%s%s", + SYS_INPUT_DIR, + namelist[i]->d_name); + + if (rc < 0 || (size_t) rc >= sizeof(buf)) { + log_error(NULL, "Invalid syspath, syspath is unreliable\n"); + break; + } + + uinput_dev->syspath = strdup(buf); + uinput_dev->devnode = fetch_device_node(buf); + } + } + + for (i = 0; i < ndev; i++) + free(namelist[i]); + free(namelist); + + return uinput_dev->devnode ? 0 : -1; +#undef SYS_INPUT_DIR +} + +#endif /* __FreeBSD__*/ + +static int +uinput_create_write(const struct libevdev *dev, int fd) { + int rc; + struct uinput_user_dev uidev; + + memset(&uidev, 0, sizeof(uidev)); + + strncpy(uidev.name, libevdev_get_name(dev), UINPUT_MAX_NAME_SIZE - 1); + uidev.id.vendor = libevdev_get_id_vendor(dev); + uidev.id.product = libevdev_get_id_product(dev); + uidev.id.bustype = libevdev_get_id_bustype(dev); + uidev.id.version = libevdev_get_id_version(dev); + + if (set_evbits(dev, fd, &uidev) != 0) + goto error; + if (set_props(dev, fd) != 0) + goto error; + + rc = write(fd, &uidev, sizeof(uidev)); + if (rc < 0) { + goto error; + } else if ((size_t) rc < sizeof(uidev)) { + errno = EINVAL; + goto error; + } + + errno = 0; + + error: + return -errno; +} + +static int +uinput_create_DEV_SETUP(const struct libevdev *dev, int fd, + struct libevdev_uinput *new_device) { + int rc; + struct uinput_setup setup; + + if (set_evbits(dev, fd, NULL) != 0) + goto error; + if (set_props(dev, fd) != 0) + goto error; + + memset(&setup, 0, sizeof(setup)); + strncpy(setup.name, libevdev_get_name(dev), UINPUT_MAX_NAME_SIZE - 1); + setup.id.vendor = libevdev_get_id_vendor(dev); + setup.id.product = libevdev_get_id_product(dev); + setup.id.bustype = libevdev_get_id_bustype(dev); + setup.id.version = libevdev_get_id_version(dev); + setup.ff_effects_max = libevdev_has_event_type(dev, EV_FF) ? 10 : 0; + + rc = ioctl(fd, UI_DEV_SETUP, &setup); + if (rc == 0) + errno = 0; + error: + return -errno; +} + +LIBEVDEV_EXPORT int +libevdev_uinput_create_from_device(const struct libevdev *dev, int fd, + struct libevdev_uinput **uinput_dev) { + int rc; + struct libevdev_uinput *new_device; + int close_fd_on_error = (fd == LIBEVDEV_UINPUT_OPEN_MANAGED); + unsigned int uinput_version = 0; + + new_device = alloc_uinput_device(libevdev_get_name(dev)); + if (!new_device) + return -ENOMEM; + + if (fd == LIBEVDEV_UINPUT_OPEN_MANAGED) { + fd = open("/dev/uinput", O_RDWR | O_CLOEXEC); + if (fd < 0) + goto error; + + new_device->fd_is_managed = 1; + } else if (fd < 0) { + log_bug(NULL, "Invalid fd %d\n", fd); + errno = EBADF; + goto error; + } + + if (ioctl(fd, UI_GET_VERSION, &uinput_version) == 0 && + uinput_version >= 5) + rc = uinput_create_DEV_SETUP(dev, fd, new_device); + else + rc = uinput_create_write(dev, fd); + + if (rc != 0) + goto error; + + /* ctime notes time before/after ioctl to help us filter out devices + when traversing /sys/devices/virtual/input to find the device + node. + + this is in seconds, so ctime[0]/[1] will almost always be + identical but /sys doesn't give us sub-second ctime so... + */ + new_device->ctime[0] = time(NULL); + + rc = ioctl(fd, UI_DEV_CREATE, NULL); + if (rc == -1) + goto error; + + new_device->ctime[1] = time(NULL); + new_device->fd = fd; + + if (fetch_syspath_and_devnode(new_device) == -1) { + log_error(NULL, "unable to fetch syspath or device node.\n"); + errno = ENODEV; + goto error; + } + + *uinput_dev = new_device; + + return 0; + + error: + rc = -errno; + libevdev_uinput_destroy(new_device); + if (fd != -1 && close_fd_on_error) + close(fd); + return rc; +} + +LIBEVDEV_EXPORT void +libevdev_uinput_destroy(struct libevdev_uinput *uinput_dev) { + if (!uinput_dev) + return; + + if (uinput_dev->fd >= 0) { + (void) ioctl(uinput_dev->fd, UI_DEV_DESTROY, NULL); + if (uinput_dev->fd_is_managed) + close(uinput_dev->fd); + } + free(uinput_dev->syspath); + free(uinput_dev->devnode); + free(uinput_dev->name); + free(uinput_dev); +} + +LIBEVDEV_EXPORT const char * +libevdev_uinput_get_syspath(struct libevdev_uinput *uinput_dev) { + return uinput_dev->syspath; +} + +LIBEVDEV_EXPORT const char * +libevdev_uinput_get_devnode(struct libevdev_uinput *uinput_dev) { + return uinput_dev->devnode; +} + +LIBEVDEV_EXPORT int +libevdev_uinput_write_event(const struct libevdev_uinput *uinput_dev, + unsigned int type, + unsigned int code, + int value) { + struct input_event ev = { + .input_event_sec = 0, + .input_event_usec = 0, + .type = type, + .code = code, + .value = value + }; + int fd = libevdev_uinput_get_fd(uinput_dev); + int rc, max; + + if (type > EV_MAX) + return -EINVAL; + + max = libevdev_event_type_get_max(type); + if (max == -1 || code > (unsigned int) max) + return -EINVAL; + + rc = write(fd, &ev, sizeof(ev)); + + return rc < 0 ? -errno : 0; +} diff --git a/sysbridge/src/main/cpp/libevdev/libevdev-uinput.h b/sysbridge/src/main/cpp/libevdev/libevdev-uinput.h new file mode 100644 index 0000000000..2919788905 --- /dev/null +++ b/sysbridge/src/main/cpp/libevdev/libevdev-uinput.h @@ -0,0 +1,255 @@ +/* SPDX-License-Identifier: MIT */ +/* + * Copyright © 2013 Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#ifndef LIBEVDEV_UINPUT_H +#define LIBEVDEV_UINPUT_H + +#ifdef __cplusplus +extern "C" { +#endif + +struct libevdev_uinput; + +/** + * @defgroup uinput uinput device creation + * + * Creation of uinput devices based on existing libevdev devices. These functions + * help to create uinput devices that emulate libevdev devices. In the simplest + * form it serves to duplicate an existing device: + * + * @code + * int err; + * int fd, uifd; + * struct libevdev *dev; + * struct libevdev_uinput *uidev; + * + * fd = open("/dev/input/event0", O_RDONLY); + * if (fd < 0) + * return -errno; + * + * err = libevdev_new_from_fd(fd, &dev); + * if (err != 0) + * return err; + * + * uifd = open("/dev/uinput", O_RDWR); + * if (uifd < 0) + * return -errno; + * + * err = libevdev_uinput_create_from_device(dev, uifd, &uidev); + * if (err != 0) + * return err; + * + * // post a REL_X event + * err = libevdev_uinput_write_event(uidev, EV_REL, REL_X, -1); + * if (err != 0) + * return err; + * err = libevdev_uinput_write_event(uidev, EV_SYN, SYN_REPORT, 0); + * if (err != 0) + * return err; + * + * libevdev_uinput_destroy(uidev); + * libevdev_free(dev); + * close(uifd); + * close(fd); + * + * @endcode + * + * Alternatively, a device can be constructed from scratch: + * + * @code + * int err; + * struct libevdev *dev; + * struct libevdev_uinput *uidev; + * + * dev = libevdev_new(); + * libevdev_set_name(dev, "test device"); + * libevdev_enable_event_type(dev, EV_REL); + * libevdev_enable_event_code(dev, EV_REL, REL_X, NULL); + * libevdev_enable_event_code(dev, EV_REL, REL_Y, NULL); + * libevdev_enable_event_type(dev, EV_KEY); + * libevdev_enable_event_code(dev, EV_KEY, BTN_LEFT, NULL); + * libevdev_enable_event_code(dev, EV_KEY, BTN_MIDDLE, NULL); + * libevdev_enable_event_code(dev, EV_KEY, BTN_RIGHT, NULL); + * + * err = libevdev_uinput_create_from_device(dev, + * LIBEVDEV_UINPUT_OPEN_MANAGED, + * &uidev); + * if (err != 0) + * return err; + * + * // ... do something ... + * + * libevdev_uinput_destroy(uidev); + * + * @endcode + */ + +enum libevdev_uinput_open_mode { + /* intentionally -2 to avoid code like below from accidentally working: + fd = open("/dev/uinput", O_RDWR); // fails, fd is -1 + libevdev_uinput_create_from_device(dev, fd, &uidev); // may hide the error */ + LIBEVDEV_UINPUT_OPEN_MANAGED = -2 /**< let libevdev open and close @c /dev/uinput */ +}; + +/** + * @ingroup uinput + * + * Create a uinput device based on the given libevdev device. The uinput device + * will be an exact copy of the libevdev device, minus the bits that uinput doesn't + * allow to be set. + * + * If uinput_fd is @ref LIBEVDEV_UINPUT_OPEN_MANAGED, libevdev_uinput_create_from_device() + * will open @c /dev/uinput in read/write mode and manage the file descriptor. + * Otherwise, uinput_fd must be opened by the caller and opened with the + * appropriate permissions. + * + * The device's lifetime is tied to the uinput file descriptor, closing it will + * destroy the uinput device. You should call libevdev_uinput_destroy() before + * closing the file descriptor to free allocated resources. + * A file descriptor can only create one uinput device at a time; the second device + * will fail with -EINVAL. + * + * You don't need to keep the file descriptor variable around, + * libevdev_uinput_get_fd() will return it when needed. + * + * @note Due to limitations in the uinput kernel module, REP_DELAY and + * REP_PERIOD will default to the kernel defaults, not to the ones set in the + * source device. + * + * @note On FreeBSD, if the UI_GET_SYSNAME ioctl() fails, there is no other way + * to get a device, and the function call will fail. + * + * @param dev The device to duplicate + * @param uinput_fd @ref LIBEVDEV_UINPUT_OPEN_MANAGED or a file descriptor to @c /dev/uinput, + * @param[out] uinput_dev The newly created libevdev device. + * + * @return 0 on success or a negative errno on failure. On failure, the value of + * uinput_dev is unmodified. + * + * @see libevdev_uinput_destroy + */ +int libevdev_uinput_create_from_device(const struct libevdev *dev, + int uinput_fd, + struct libevdev_uinput **uinput_dev); + +/** + * @ingroup uinput + * + * Destroy a previously created uinput device and free associated memory. + * + * If the device was opened with @ref LIBEVDEV_UINPUT_OPEN_MANAGED, + * libevdev_uinput_destroy() also closes the file descriptor. Otherwise, the + * fd is left as-is and must be closed by the caller. + * + * @param uinput_dev A previously created uinput device. + */ +void libevdev_uinput_destroy(struct libevdev_uinput *uinput_dev); + +/** + * @ingroup uinput + * + * Return the file descriptor used to create this uinput device. This is the + * fd pointing to /dev/uinput. This file descriptor may be used to write + * events that are emitted by the uinput device. + * Closing this file descriptor will destroy the uinput device, you should + * call libevdev_uinput_destroy() first to free allocated resources. + * + * @param uinput_dev A previously created uinput device. + * + * @return The file descriptor used to create this device + */ +int libevdev_uinput_get_fd(const struct libevdev_uinput *uinput_dev); + +/** + * @ingroup uinput + * + * Return the syspath representing this uinput device. If the UI_GET_SYSNAME + * ioctl is not available, libevdev makes an educated guess. + * The UI_GET_SYSNAME ioctl is available since Linux 3.15. + * + * The syspath returned is the one of the input node itself + * (e.g. /sys/devices/virtual/input/input123), not the syspath of the device + * node returned with libevdev_uinput_get_devnode(). + * + * @note This function may return NULL if UI_GET_SYSNAME is not available. + * In that case, libevdev uses ctime and the device name to guess devices. + * To avoid false positives, wait at least 1.5s between creating devices that + * have the same name. + * + * @note FreeBSD does not have sysfs, on FreeBSD this function always returns + * NULL. + * + * @param uinput_dev A previously created uinput device. + * @return The syspath for this device, including the preceding /sys + * + * @see libevdev_uinput_get_devnode + */ +const char *libevdev_uinput_get_syspath(struct libevdev_uinput *uinput_dev); + +/** + * @ingroup uinput + * + * Return the device node representing this uinput device. + * + * This relies on libevdev_uinput_get_syspath() to provide a valid syspath. + * See libevdev_uinput_get_syspath() for more details. + * + * @note This function may return NULL. libevdev may have to guess the + * syspath and the device node. See libevdev_uinput_get_syspath() for details. + * + * @note On FreeBSD, this function can not return NULL. libudev uses the + * UI_GET_SYSNAME ioctl to get the device node on this platform and if that + * fails, the call to libevdev_uinput_create_from_device() fails. + * + * @param uinput_dev A previously created uinput device. + * @return The device node for this device, in the form of /dev/input/eventN + * + * @see libevdev_uinput_get_syspath + */ +const char *libevdev_uinput_get_devnode(struct libevdev_uinput *uinput_dev); + +/** + * @ingroup uinput + * + * Post an event through the uinput device. It is the caller's responsibility + * that any event sequence is terminated with an EV_SYN/SYN_REPORT/0 event. + * Otherwise, listeners on the device node will not see the events until the + * next EV_SYN event is posted. + * + * @param uinput_dev A previously created uinput device. + * @param type Event type (EV_ABS, EV_REL, etc.) + * @param code Event code (ABS_X, REL_Y, etc.) + * @param value The event value + * @return 0 on success or a negative errno on error + */ +int libevdev_uinput_write_event(const struct libevdev_uinput *uinput_dev, + unsigned int type, + unsigned int code, + int value); + +#ifdef __cplusplus +} +#endif + +#endif /* LIBEVDEV_UINPUT_H */ diff --git a/sysbridge/src/main/cpp/libevdev/libevdev-util.h b/sysbridge/src/main/cpp/libevdev/libevdev-util.h new file mode 100644 index 0000000000..380636d2b8 --- /dev/null +++ b/sysbridge/src/main/cpp/libevdev/libevdev-util.h @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT +/* + * Copyright © 2013 Red Hat, Inc. + */ + +#ifndef _UTIL_H_ +#define _UTIL_H_ + +#include +#include + +#define LONG_BITS (sizeof(long) * 8) +#define NLONGS(x) (((x) + LONG_BITS - 1) / LONG_BITS) +#define ARRAY_LENGTH(a) (sizeof(a) / (sizeof((a)[0]))) +#define unlikely(x) (__builtin_expect(!!(x),0)) + +#undef min +#undef max +#ifdef __GNUC__ +#define min(a, b) \ + ({ __typeof__ (a) _a = (a); \ + __typeof__ (b) _b = (b); \ + _a > _b ? _b : _a; \ + }) +#define max(a, b) \ + ({ __typeof__ (a) _a = (a); \ + __typeof__ (b) _b = (b); \ + _a > _b ? _a : _b; \ + }) +#else +#define min(a,b) ((a) > (b) ? (b) : (a)) +#define max(a,b) ((a) > (b) ? (a) : (b)) +#endif + +static inline bool +startswith(const char *str, size_t len, const char *prefix, size_t plen) { + return len >= plen && !strncmp(str, prefix, plen); +} + +static inline int +bit_is_set(const unsigned long *array, int bit) { + return !!(array[bit / LONG_BITS] & (1LL << (bit % LONG_BITS))); +} + +static inline void +set_bit(unsigned long *array, int bit) { + array[bit / LONG_BITS] |= (1LL << (bit % LONG_BITS)); +} + +static inline void +clear_bit(unsigned long *array, int bit) { + array[bit / LONG_BITS] &= ~(1LL << (bit % LONG_BITS)); +} + +static inline void +set_bit_state(unsigned long *array, int bit, int state) { + if (state) + set_bit(array, bit); + else + clear_bit(array, bit); +} + +#endif diff --git a/sysbridge/src/main/cpp/libevdev/libevdev.c b/sysbridge/src/main/cpp/libevdev/libevdev.c new file mode 100644 index 0000000000..78f8f3cfae --- /dev/null +++ b/sysbridge/src/main/cpp/libevdev/libevdev.c @@ -0,0 +1,1848 @@ +// SPDX-License-Identifier: MIT +/* + * Copyright © 2013 Red Hat, Inc. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "libevdev-int.h" +#include "libevdev-util.h" +#include "libevdev.h" + +#include "event-names.h" + +#define MAXEVENTS 64 + +enum event_filter_status { + EVENT_FILTER_NONE, /**< Event untouched by filters */ + EVENT_FILTER_MODIFIED, /**< Event was modified */ + EVENT_FILTER_DISCARD, /**< Discard current event */ +}; + +/* Keeps a record of touches during SYN_DROPPED */ +enum touch_state { + TOUCH_OFF, + TOUCH_STARTED, /* Started during SYN_DROPPED */ + TOUCH_STOPPED, /* Stopped during SYN_DROPPED */ + TOUCH_ONGOING, /* Existed before, still have same tracking ID */ + TOUCH_CHANGED, /* Existed before but have new tracking ID now, so + stopped + started in that slot */ +}; + +struct slot_change_state { + enum touch_state state; + unsigned long axes[NLONGS(ABS_CNT)]; /* bitmask for updated axes */ +}; + +static int sync_mt_state(struct libevdev *dev, + struct slot_change_state changes_out[dev->num_slots]); + +static int +update_key_state(struct libevdev *dev, const struct input_event *e); + +static inline int * +slot_value(const struct libevdev *dev, int slot, int axis) { + if (unlikely(slot > dev->num_slots)) { + log_bug(dev, "Slot %d exceeds number of slots (%d)\n", slot, dev->num_slots); + slot = 0; + } + if (unlikely(axis < ABS_MT_MIN || axis > ABS_MT_MAX)) { + log_bug(dev, "MT axis %d is outside the valid range [%d,%d]\n", + axis, ABS_MT_MIN, ABS_MT_MAX); + axis = ABS_MT_MIN; + } + return &dev->mt_slot_vals[slot * ABS_MT_CNT + axis - ABS_MT_MIN]; +} + +static int +init_event_queue(struct libevdev *dev) { + const int MIN_QUEUE_SIZE = 256; + int nevents = 1; /* terminating SYN_REPORT */ + int nslots; + unsigned int type, code; + + /* count the number of axes, keys, etc. to get a better idea at how + many events per EV_SYN we could possibly get. That's the max we + may get during SYN_DROPPED too. Use double that, just so we have + room for events while syncing a device. + */ + for (type = EV_KEY; type < EV_MAX; type++) { + int max = libevdev_event_type_get_max(type); + for (code = 0; max > 0 && code < (unsigned int) max; code++) { + if (libevdev_has_event_code(dev, type, code)) + nevents++; + } + } + + nslots = libevdev_get_num_slots(dev); + if (nslots > 1) { + int num_mt_axes = 0; + + for (code = ABS_MT_SLOT; code <= ABS_MAX; code++) { + if (libevdev_has_event_code(dev, EV_ABS, code)) + num_mt_axes++; + } + + /* We already counted the first slot in the initial count */ + nevents += num_mt_axes * (nslots - 1); + } + + return queue_alloc(dev, max(MIN_QUEUE_SIZE, nevents * 2)); +} + +static void +libevdev_dflt_log_func(enum libevdev_log_priority priority, + void *data, + const char *file, int line, const char *func, + const char *format, va_list args) { + const char *prefix; + switch (priority) { + case LIBEVDEV_LOG_ERROR: + prefix = "libevdev error"; + break; + case LIBEVDEV_LOG_INFO: + prefix = "libevdev info"; + break; + case LIBEVDEV_LOG_DEBUG: + prefix = "libevdev debug"; + break; + default: + prefix = "libevdev INVALID LOG PRIORITY"; + break; + } + /* default logging format: + libevev error in libevdev_some_func: blah blah + libevev info in libevdev_some_func: blah blah + libevev debug in file.c:123:libevdev_some_func: blah blah + */ + + fprintf(stderr, "%s in ", prefix); + if (priority == LIBEVDEV_LOG_DEBUG) + fprintf(stderr, "%s:%d:", file, line); + fprintf(stderr, "%s: ", func); + vfprintf(stderr, format, args); +} + +static void +fix_invalid_absinfo(const struct libevdev *dev, + int axis, + struct input_absinfo *abs_info) { + /* + * The reported absinfo for ABS_MT_TRACKING_ID is sometimes + * uninitialized for certain mtk-soc, due to init code mangling + * in the vendor kernel. + */ + if (axis == ABS_MT_TRACKING_ID && + abs_info->maximum == abs_info->minimum) { + abs_info->minimum = -1; + abs_info->maximum = 0xFFFF; + log_bug(dev, + "Device \"%s\" has invalid ABS_MT_TRACKING_ID range", + dev->name); + } +} + +/* + * Global logging settings. + */ +static struct logdata log_data = { + .priority = LIBEVDEV_LOG_INFO, + .global_handler = libevdev_dflt_log_func, + .userdata = NULL, +}; + +void +_libevdev_log_msg(const struct libevdev *dev, + enum libevdev_log_priority priority, + const char *file, int line, const char *func, + const char *format, ...) { + va_list args; + + if (dev && dev->log.device_handler) { + /** + * if both global handler and device handler are set + * we've set up the handlers wrong. And that means we'll + * likely get the printf args wrong and cause all sorts of + * mayhem. Seppuku is called for. + */ + if (unlikely(dev->log.global_handler)) + abort(); + + if (priority > dev->log.priority) + return; + } else if (!log_data.global_handler || priority > log_data.priority) { + return; + } else if (unlikely(log_data.device_handler)) { + abort(); /* Seppuku, see above */ + } + + va_start(args, format); + if (dev && dev->log.device_handler) + dev->log.device_handler(dev, priority, dev->log.userdata, file, line, func, format, args); + else + log_data.global_handler(priority, log_data.userdata, file, line, func, format, args); + va_end(args); +} + +static void +libevdev_reset(struct libevdev *dev) { + enum libevdev_log_priority pri = dev->log.priority; + libevdev_device_log_func_t handler = dev->log.device_handler; + + free(dev->name); + free(dev->phys); + free(dev->uniq); + free(dev->mt_slot_vals); + memset(dev, 0, sizeof(*dev)); + dev->fd = -1; + dev->initialized = false; + dev->num_slots = -1; + dev->current_slot = -1; + dev->grabbed = LIBEVDEV_UNGRAB; + dev->sync_state = SYNC_NONE; + dev->log.priority = pri; + dev->log.device_handler = handler; + libevdev_enable_event_type(dev, EV_SYN); +} + +LIBEVDEV_EXPORT struct libevdev * +libevdev_new(void) { + struct libevdev *dev; + + dev = calloc(1, sizeof(*dev)); + if (!dev) + return NULL; + + libevdev_reset(dev); + + return dev; +} + +LIBEVDEV_EXPORT int +libevdev_new_from_fd(int fd, struct libevdev **dev) { + struct libevdev *d; + int rc; + + d = libevdev_new(); + if (!d) + return -ENOMEM; + + rc = libevdev_set_fd(d, fd); + if (rc < 0) + libevdev_free(d); + else + *dev = d; + return rc; +} + +LIBEVDEV_EXPORT void +libevdev_free(struct libevdev *dev) { + if (!dev) + return; + + queue_free(dev); + libevdev_reset(dev); + free(dev); +} + +LIBEVDEV_EXPORT void +libevdev_set_log_function(libevdev_log_func_t logfunc, void *data) { + log_data.global_handler = logfunc; + log_data.userdata = data; +} + +LIBEVDEV_EXPORT void +libevdev_set_log_priority(enum libevdev_log_priority priority) { + if (priority > LIBEVDEV_LOG_DEBUG) + priority = LIBEVDEV_LOG_DEBUG; + log_data.priority = priority; +} + +LIBEVDEV_EXPORT enum libevdev_log_priority +libevdev_get_log_priority(void) { + return log_data.priority; +} + +LIBEVDEV_EXPORT void +libevdev_set_device_log_function(struct libevdev *dev, + libevdev_device_log_func_t logfunc, + enum libevdev_log_priority priority, + void *data) { + if (!dev) { + log_bug(NULL, "device must not be NULL\n"); + return; + } + + dev->log.priority = priority; + dev->log.device_handler = logfunc; + dev->log.userdata = data; +} + +enum libevdev_log_priority +_libevdev_log_priority(const struct libevdev *dev) { + if (dev && dev->log.device_handler) + return dev->log.priority; + return libevdev_get_log_priority(); +} + +LIBEVDEV_EXPORT int +libevdev_change_fd(struct libevdev *dev, int fd) { + if (!dev->initialized) { + log_bug(dev, "device not initialized. call libevdev_set_fd() first\n"); + return -1; + } + dev->fd = fd; + dev->grabbed = LIBEVDEV_UNGRAB; + return 0; +} + +static void +reset_tracking_ids(struct libevdev *dev) { + if (dev->num_slots == -1 || + !libevdev_has_event_code(dev, EV_ABS, ABS_MT_TRACKING_ID)) + return; + + for (int slot = 0; slot < dev->num_slots; slot++) + libevdev_set_slot_value(dev, slot, ABS_MT_TRACKING_ID, -1); +} + +static inline void +free_slots(struct libevdev *dev) { + dev->num_slots = -1; + free(dev->mt_slot_vals); + dev->mt_slot_vals = NULL; +} + +static int +init_slots(struct libevdev *dev) { + const struct input_absinfo *abs_info; + int rc = 0; + + free(dev->mt_slot_vals); + dev->mt_slot_vals = NULL; + + /* devices with ABS_RESERVED aren't MT devices, + see the documentation for multitouch-related + functions for more details */ + if (libevdev_has_event_code(dev, EV_ABS, ABS_RESERVED) || + !libevdev_has_event_code(dev, EV_ABS, ABS_MT_SLOT)) { + if (dev->num_slots != -1) { + free_slots(dev); + } + return rc; + } + + abs_info = libevdev_get_abs_info(dev, ABS_MT_SLOT); + + free_slots(dev); + dev->num_slots = abs_info->maximum + 1; + dev->mt_slot_vals = calloc(dev->num_slots * ABS_MT_CNT, sizeof(int)); + if (!dev->mt_slot_vals) { + rc = -ENOMEM; + goto out; + } + dev->current_slot = abs_info->value; + + reset_tracking_ids(dev); + out: + return rc; +} + +LIBEVDEV_EXPORT int +libevdev_set_fd(struct libevdev *dev, int fd) { + int rc; + int i; + char buf[256]; + + if (dev->initialized) { + log_bug(dev, "device already initialized.\n"); + return -EBADF; + } + + if (fd < 0) { + return -EBADF; + } + + libevdev_reset(dev); + + rc = ioctl(fd, EVIOCGBIT(0, sizeof(dev->bits)), dev->bits); + if (rc < 0) + goto out; + + memset(buf, 0, sizeof(buf)); + rc = ioctl(fd, EVIOCGNAME(sizeof(buf) - 1), buf); + if (rc < 0) + goto out; + + free(dev->name); + dev->name = strdup(buf); + if (!dev->name) { + errno = ENOMEM; + goto out; + } + + free(dev->phys); + dev->phys = NULL; + memset(buf, 0, sizeof(buf)); + rc = ioctl(fd, EVIOCGPHYS(sizeof(buf) - 1), buf); + if (rc < 0) { + /* uinput has no phys */ + if (errno != ENOENT) + goto out; + } else { + dev->phys = strdup(buf); + if (!dev->phys) { + errno = ENOMEM; + goto out; + } + } + + free(dev->uniq); + dev->uniq = NULL; + memset(buf, 0, sizeof(buf)); + rc = ioctl(fd, EVIOCGUNIQ(sizeof(buf) - 1), buf); + if (rc < 0) { + if (errno != ENOENT) + goto out; + } else { + dev->uniq = strdup(buf); + if (!dev->uniq) { + errno = ENOMEM; + goto out; + } + } + + rc = ioctl(fd, EVIOCGID, &dev->ids); + if (rc < 0) + goto out; + + rc = ioctl(fd, EVIOCGVERSION, &dev->driver_version); + if (rc < 0) + goto out; + + /* Built on a kernel with props, running against a kernel without property + support. This should not be a fatal case, we'll be missing properties but other + than that everything is as expected. + */ + rc = ioctl(fd, EVIOCGPROP(sizeof(dev->props)), dev->props); + if (rc < 0 && errno != EINVAL) + goto out; + + rc = ioctl(fd, EVIOCGBIT(EV_REL, sizeof(dev->rel_bits)), dev->rel_bits); + if (rc < 0) + goto out; + + rc = ioctl(fd, EVIOCGBIT(EV_ABS, sizeof(dev->abs_bits)), dev->abs_bits); + if (rc < 0) + goto out; + + rc = ioctl(fd, EVIOCGBIT(EV_LED, sizeof(dev->led_bits)), dev->led_bits); + if (rc < 0) + goto out; + + rc = ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(dev->key_bits)), dev->key_bits); + if (rc < 0) + goto out; + + rc = ioctl(fd, EVIOCGBIT(EV_SW, sizeof(dev->sw_bits)), dev->sw_bits); + if (rc < 0) + goto out; + + rc = ioctl(fd, EVIOCGBIT(EV_MSC, sizeof(dev->msc_bits)), dev->msc_bits); + if (rc < 0) + goto out; + + rc = ioctl(fd, EVIOCGBIT(EV_FF, sizeof(dev->ff_bits)), dev->ff_bits); + if (rc < 0) + goto out; + + rc = ioctl(fd, EVIOCGBIT(EV_SND, sizeof(dev->snd_bits)), dev->snd_bits); + if (rc < 0) + goto out; + + rc = ioctl(fd, EVIOCGKEY(sizeof(dev->key_values)), dev->key_values); + if (rc < 0) + goto out; + + rc = ioctl(fd, EVIOCGLED(sizeof(dev->led_values)), dev->led_values); + if (rc < 0) + goto out; + + rc = ioctl(fd, EVIOCGSW(sizeof(dev->sw_values)), dev->sw_values); + if (rc < 0) + goto out; + + /* rep is a special case, always set it to 1 for both values if EV_REP is set */ + if (bit_is_set(dev->bits, EV_REP)) { + for (i = 0; i < REP_CNT; i++) + set_bit(dev->rep_bits, i); + rc = ioctl(fd, EVIOCGREP, dev->rep_values); + if (rc < 0) + goto out; + } + + for (i = ABS_X; i <= ABS_MAX; i++) { + if (bit_is_set(dev->abs_bits, i)) { + struct input_absinfo abs_info; + rc = ioctl(fd, EVIOCGABS(i), &abs_info); + if (rc < 0) + goto out; + + fix_invalid_absinfo(dev, i, &abs_info); + + dev->abs_info[i] = abs_info; + } + } + + dev->fd = fd; + + rc = init_slots(dev); + if (rc != 0) + goto out; + + if (dev->num_slots != -1) { + struct slot_change_state unused[dev->num_slots]; + sync_mt_state(dev, unused); + } + + rc = init_event_queue(dev); + if (rc < 0) { + dev->fd = -1; + return -rc; + } + + /* not copying key state because we won't know when we'll start to + * use this fd and key's are likely to change state by then. + * Same with the valuators, really, but they may not change. + */ + + dev->initialized = true; + out: + if (rc) + libevdev_reset(dev); + return rc ? -errno : 0; +} + +LIBEVDEV_EXPORT int +libevdev_get_fd(const struct libevdev *dev) { + return dev->fd; +} + +static int +sync_key_state(struct libevdev *dev) { + int rc; + int i; + unsigned long keystate[NLONGS(KEY_CNT)] = {0}; + + rc = ioctl(dev->fd, EVIOCGKEY(sizeof(keystate)), keystate); + if (rc < 0) + goto out; + + for (i = 0; i < KEY_CNT; i++) { + int old, new; + old = bit_is_set(dev->key_values, i); + new = bit_is_set(keystate, i); + if (old ^ new) + queue_push_event(dev, EV_KEY, i, new ? 1 : 0); + } + + memcpy(dev->key_values, keystate, rc); + + rc = 0; + out: + return rc ? -errno : 0; +} + +static int +sync_sw_state(struct libevdev *dev) { + int rc; + int i; + unsigned long swstate[NLONGS(SW_CNT)] = {0}; + + rc = ioctl(dev->fd, EVIOCGSW(sizeof(swstate)), swstate); + if (rc < 0) + goto out; + + for (i = 0; i < SW_CNT; i++) { + int old, new; + old = bit_is_set(dev->sw_values, i); + new = bit_is_set(swstate, i); + if (old ^ new) + queue_push_event(dev, EV_SW, i, new ? 1 : 0); + } + + memcpy(dev->sw_values, swstate, rc); + + rc = 0; + out: + return rc ? -errno : 0; +} + +static int +sync_led_state(struct libevdev *dev) { + int rc; + int i; + unsigned long ledstate[NLONGS(LED_CNT)] = {0}; + + rc = ioctl(dev->fd, EVIOCGLED(sizeof(ledstate)), ledstate); + if (rc < 0) + goto out; + + for (i = 0; i < LED_CNT; i++) { + int old, new; + old = bit_is_set(dev->led_values, i); + new = bit_is_set(ledstate, i); + if (old ^ new) { + queue_push_event(dev, EV_LED, i, new ? 1 : 0); + } + } + + memcpy(dev->led_values, ledstate, rc); + + rc = 0; + out: + return rc ? -errno : 0; +} + +static int +sync_abs_state(struct libevdev *dev) { + int rc; + int i; + + for (i = ABS_X; i < ABS_CNT; i++) { + struct input_absinfo abs_info; + + if (i >= ABS_MT_MIN && i <= ABS_MT_MAX) + continue; + + if (!bit_is_set(dev->abs_bits, i)) + continue; + + rc = ioctl(dev->fd, EVIOCGABS(i), &abs_info); + if (rc < 0) + goto out; + + if (dev->abs_info[i].value != abs_info.value) { + queue_push_event(dev, EV_ABS, i, abs_info.value); + dev->abs_info[i].value = abs_info.value; + } + } + + rc = 0; + out: + return rc ? -errno : 0; +} + +static int +sync_mt_state(struct libevdev *dev, + struct slot_change_state changes_out[dev->num_slots]) { +#define MAX_SLOTS 256 + int rc = 0; + struct slot_change_state changes[MAX_SLOTS] = {0}; + unsigned int nslots = min(MAX_SLOTS, dev->num_slots); + + for (int axis = ABS_MT_MIN; axis <= ABS_MT_MAX; axis++) { + /* EVIOCGMTSLOTS required format */ + struct mt_sync_state { + uint32_t code; + int32_t val[MAX_SLOTS]; + } mt_state; + + if (axis == ABS_MT_SLOT || + !libevdev_has_event_code(dev, EV_ABS, axis)) + continue; + + mt_state.code = axis; + rc = ioctl(dev->fd, EVIOCGMTSLOTS(sizeof(mt_state)), &mt_state); + if (rc < 0) + goto out; + + for (unsigned int slot = 0; slot < nslots; slot++) { + int val_before = *slot_value(dev, slot, axis), + val_after = mt_state.val[slot]; + + if (axis == ABS_MT_TRACKING_ID) { + if (val_before == -1 && val_after != -1) { + changes[slot].state = TOUCH_STARTED; + } else if (val_before != -1 && val_after == -1) { + changes[slot].state = TOUCH_STOPPED; + } else if (val_before != -1 && val_after != -1 && + val_before == val_after) { + changes[slot].state = TOUCH_ONGOING; + } else if (val_before != -1 && val_after != -1 && + val_before != val_after) { + changes[slot].state = TOUCH_CHANGED; + } else { + changes[slot].state = TOUCH_OFF; + } + } + + if (val_before == val_after) + continue; + + *slot_value(dev, slot, axis) = val_after; + + set_bit(changes[slot].axes, axis); + /* note that this slot has updates */ + set_bit(changes[slot].axes, ABS_MT_SLOT); + } + } + + if (dev->num_slots > MAX_SLOTS) + memset(changes_out, 0, sizeof(*changes) * dev->num_slots); + + memcpy(changes_out, changes, sizeof(*changes) * nslots); + out: + return rc; +} + +static void +terminate_slots(struct libevdev *dev, + const struct slot_change_state changes[dev->num_slots], + int *last_reported_slot) { + const unsigned int map[] = {BTN_TOOL_FINGER, BTN_TOOL_DOUBLETAP, + BTN_TOOL_TRIPLETAP, BTN_TOOL_QUADTAP, + BTN_TOOL_QUINTTAP}; + bool touches_stopped = false; + int ntouches_before = 0, ntouches_after = 0; + + /* For BTN_TOOL_* emulation, we need to know how many touches we had + * before and how many we have left once we terminate all the ones + * that changed and all the ones that stopped. + */ + for (int slot = 0; slot < dev->num_slots; slot++) { + switch (changes[slot].state) { + case TOUCH_OFF: + break; + case TOUCH_CHANGED: + case TOUCH_STOPPED: + queue_push_event(dev, EV_ABS, ABS_MT_SLOT, slot); + queue_push_event(dev, EV_ABS, ABS_MT_TRACKING_ID, -1); + + *last_reported_slot = slot; + touches_stopped = true; + ntouches_before++; + break; + case TOUCH_ONGOING: + ntouches_before++; + ntouches_after++; + break; + case TOUCH_STARTED: + break; + } + } + + /* If any of the touches stopped, we need to split the sync state + into two frames - one with all the stopped touches, one with the + new touches starting (if any) */ + if (touches_stopped) { + /* Send through the required BTN_TOOL_ 0 and 1 events for + * the previous and current number of fingers. And update + * our own key state accordingly, so that during the second + * sync event frame sync_key_state() sets everything correctly + * for the *real* number of touches. + */ + if (ntouches_before > 0 && ntouches_before <= 5) { + struct input_event ev = { + .type = EV_KEY, + .code = map[ntouches_before - 1], + .value = 0, + }; + queue_push_event(dev, ev.type, ev.code, ev.value); + update_key_state(dev, &ev); + } + + if (ntouches_after > 0 && ntouches_after <= 5) { + struct input_event ev = { + .type = EV_KEY, + .code = map[ntouches_after - 1], + .value = 1, + }; + queue_push_event(dev, ev.type, ev.code, ev.value); + update_key_state(dev, &ev); + } + + queue_push_event(dev, EV_SYN, SYN_REPORT, 0); + } +} + +static int +push_mt_sync_events(struct libevdev *dev, + const struct slot_change_state changes[dev->num_slots], + int last_reported_slot) { + struct input_absinfo abs_info; + int rc; + + for (int slot = 0; slot < dev->num_slots; slot++) { + bool have_slot_event = false; + + if (!bit_is_set(changes[slot].axes, ABS_MT_SLOT)) + continue; + + for (int axis = ABS_MT_MIN; axis <= ABS_MT_MAX; axis++) { + if (axis == ABS_MT_SLOT || + !libevdev_has_event_code(dev, EV_ABS, axis)) + continue; + + if (bit_is_set(changes[slot].axes, axis)) { + /* We already sent the tracking id -1 in + * terminate_slots so don't do that again. There + * may be other axes like ABS_MT_TOOL_TYPE that + * need to be synced despite no touch being active */ + if (axis == ABS_MT_TRACKING_ID && + *slot_value(dev, slot, axis) == -1) + continue; + + if (!have_slot_event) { + queue_push_event(dev, EV_ABS, ABS_MT_SLOT, slot); + last_reported_slot = slot; + have_slot_event = true; + } + + queue_push_event(dev, EV_ABS, axis, + *slot_value(dev, slot, axis)); + } + } + } + + /* add one last slot event to make sure the client is on the same + slot as the kernel */ + + rc = ioctl(dev->fd, EVIOCGABS(ABS_MT_SLOT), &abs_info); + if (rc < 0) + goto out; + + dev->current_slot = abs_info.value; + + if (dev->current_slot != last_reported_slot) + queue_push_event(dev, EV_ABS, ABS_MT_SLOT, dev->current_slot); + + rc = 0; + out: + return rc ? -errno : 0; +} + +static int +read_more_events(struct libevdev *dev) { + int free_elem; + int len; + struct input_event *next; + + free_elem = queue_num_free_elements(dev); + if (free_elem <= 0) + return 0; + + next = queue_next_element(dev); + len = read(dev->fd, next, free_elem * sizeof(struct input_event)); + if (len < 0) + return -errno; + + if (len > 0 && len % sizeof(struct input_event) != 0) + return -EINVAL; + + if (len > 0) { + int nev = len / sizeof(struct input_event); + queue_set_num_elements(dev, queue_num_elements(dev) + nev); + } + + return 0; +} + +static inline void +drain_events(struct libevdev *dev) { + int rc; + size_t nelem; + int iterations = 0; + const int max_iterations = 8; /* EVDEV_BUF_PACKETS in + kernel/drivers/input/evedev.c */ + + queue_shift_multiple(dev, queue_num_elements(dev), NULL); + + do { + rc = read_more_events(dev); + if (rc == -EAGAIN) + return; + + if (rc < 0) { + log_error(dev, "Failed to drain events before sync.\n"); + return; + } + + nelem = queue_num_elements(dev); + queue_shift_multiple(dev, nelem, NULL); + } while (iterations++ < max_iterations && nelem >= queue_size(dev)); + + /* Our buffer should be roughly the same or bigger than the kernel + buffer in most cases, so we usually don't expect to recurse. If + we do, make sure we stop after max_iterations and proceed with + what we have. This could happen if events queue up faster than + we can drain them. + */ + if (iterations >= max_iterations) + log_info(dev, "Unable to drain events, buffer size mismatch.\n"); +} + +static int +sync_state(struct libevdev *dev) { + int rc = 0; + bool want_mt_sync = false; + int last_reported_slot = 0; + struct slot_change_state changes[dev->num_slots > 0 ? dev->num_slots : 1]; + + memset(changes, 0, sizeof(changes)); + + /* see section "Discarding events before synchronizing" in + * libevdev/libevdev.h */ + drain_events(dev); + + /* We generate one or two event frames during sync. + * The first one (if it exists) terminates all slots that have + * either terminated during SYN_DROPPED or changed their tracking + * ID. + * + * The second frame syncs everything up to the current state of the + * device - including re-starting those slots that have a changed + * tracking id. + */ + if (dev->num_slots > -1 && + libevdev_has_event_code(dev, EV_ABS, ABS_MT_SLOT)) { + want_mt_sync = true; + rc = sync_mt_state(dev, changes); + if (rc == 0) + terminate_slots(dev, changes, &last_reported_slot); + else + want_mt_sync = false; + } + + if (libevdev_has_event_type(dev, EV_KEY)) + rc = sync_key_state(dev); + if (libevdev_has_event_type(dev, EV_LED)) + rc = sync_led_state(dev); + if (libevdev_has_event_type(dev, EV_SW)) + rc = sync_sw_state(dev); + if (rc == 0 && libevdev_has_event_type(dev, EV_ABS)) + rc = sync_abs_state(dev); + if (rc == 0 && want_mt_sync) + push_mt_sync_events(dev, changes, last_reported_slot); + + dev->queue_nsync = queue_num_elements(dev); + + if (dev->queue_nsync > 0) { + queue_push_event(dev, EV_SYN, SYN_REPORT, 0); + dev->queue_nsync++; + } + + return rc; +} + +static int +update_key_state(struct libevdev *dev, const struct input_event *e) { + if (!libevdev_has_event_type(dev, EV_KEY)) + return 1; + + if (e->code > KEY_MAX) + return 1; + + set_bit_state(dev->key_values, e->code, e->value != 0); + + return 0; +} + +static int +update_mt_state(struct libevdev *dev, const struct input_event *e) { + if (e->code == ABS_MT_SLOT && dev->num_slots > -1) { + int i; + dev->current_slot = e->value; + /* sync abs_info with the current slot values */ + for (i = ABS_MT_SLOT + 1; i <= ABS_MT_MAX; i++) { + if (libevdev_has_event_code(dev, EV_ABS, i)) + dev->abs_info[i].value = *slot_value(dev, dev->current_slot, i); + } + + return 0; + } + + if (dev->current_slot == -1) + return 1; + + *slot_value(dev, dev->current_slot, e->code) = e->value; + + return 0; +} + +static int +update_abs_state(struct libevdev *dev, const struct input_event *e) { + if (!libevdev_has_event_type(dev, EV_ABS)) + return 1; + + if (e->code > ABS_MAX) + return 1; + + if (e->code >= ABS_MT_MIN && e->code <= ABS_MT_MAX) + update_mt_state(dev, e); + + dev->abs_info[e->code].value = e->value; + + return 0; +} + +static int +update_led_state(struct libevdev *dev, const struct input_event *e) { + if (!libevdev_has_event_type(dev, EV_LED)) + return 1; + + if (e->code > LED_MAX) + return 1; + + set_bit_state(dev->led_values, e->code, e->value != 0); + + return 0; +} + +static int +update_sw_state(struct libevdev *dev, const struct input_event *e) { + if (!libevdev_has_event_type(dev, EV_SW)) + return 1; + + if (e->code > SW_MAX) + return 1; + + set_bit_state(dev->sw_values, e->code, e->value != 0); + + return 0; +} + +static int +update_state(struct libevdev *dev, const struct input_event *e) { + int rc = 0; + + switch (e->type) { + case EV_SYN: + case EV_REL: + break; + case EV_KEY: + rc = update_key_state(dev, e); + break; + case EV_ABS: + rc = update_abs_state(dev, e); + break; + case EV_LED: + rc = update_led_state(dev, e); + break; + case EV_SW: + rc = update_sw_state(dev, e); + break; + } + + dev->last_event_time.tv_sec = e->input_event_sec; + dev->last_event_time.tv_usec = e->input_event_usec; + + return rc; +} + +/** + * Sanitize/modify events where needed. + */ +static inline enum event_filter_status +sanitize_event(const struct libevdev *dev, + struct input_event *ev, + enum SyncState sync_state) { + if (!libevdev_has_event_code(dev, ev->type, ev->code)) + return EVENT_FILTER_DISCARD; + + if (unlikely(dev->num_slots > -1 && + libevdev_event_is_code(ev, EV_ABS, ABS_MT_SLOT) && + (ev->value < 0 || ev->value >= dev->num_slots))) { + log_bug(dev, "Device \"%s\" received an invalid slot index %d." + "Capping to announced max slot number %d.\n", + dev->name, ev->value, dev->num_slots - 1); + ev->value = dev->num_slots - 1; + return EVENT_FILTER_MODIFIED; + + /* Drop any invalid tracking IDs, they are only supposed to go from + N to -1 or from -1 to N. Never from -1 to -1, or N to M. Very + unlikely to ever happen from a real device. + */ + } + + if (unlikely(sync_state == SYNC_NONE && + dev->num_slots > -1 && + libevdev_event_is_code(ev, EV_ABS, ABS_MT_TRACKING_ID) && + ((ev->value == -1 && + *slot_value(dev, dev->current_slot, ABS_MT_TRACKING_ID) == -1) || + (ev->value != -1 && + *slot_value(dev, dev->current_slot, ABS_MT_TRACKING_ID) != -1)))) { + log_bug(dev, "Device \"%s\" received a double tracking ID %d in slot %d.\n", + dev->name, ev->value, dev->current_slot); + return EVENT_FILTER_DISCARD; + } + + return EVENT_FILTER_NONE; +} + +LIBEVDEV_EXPORT int +libevdev_next_event(struct libevdev *dev, unsigned int flags, struct input_event *ev) { + int rc = LIBEVDEV_READ_STATUS_SUCCESS; + enum event_filter_status filter_status; + const unsigned int valid_flags = LIBEVDEV_READ_FLAG_NORMAL | + LIBEVDEV_READ_FLAG_SYNC | + LIBEVDEV_READ_FLAG_FORCE_SYNC | + LIBEVDEV_READ_FLAG_BLOCKING; + + if (!dev->initialized) { + log_bug(dev, "device not initialized. call libevdev_set_fd() first\n"); + return -EBADF; + } + + if (dev->fd < 0) + return -EBADF; + + if ((flags & valid_flags) == 0) { + log_bug(dev, "invalid flags %#x.\n", flags); + return -EINVAL; + } + + if (flags & LIBEVDEV_READ_FLAG_SYNC) { + if (dev->sync_state == SYNC_NEEDED) { + rc = sync_state(dev); + if (rc != 0) + return rc; + dev->sync_state = SYNC_IN_PROGRESS; + } + + if (dev->queue_nsync == 0) { + dev->sync_state = SYNC_NONE; + return -EAGAIN; + } + + } else if (dev->sync_state != SYNC_NONE) { + struct input_event e; + + /* call update_state for all events here, otherwise the library has the wrong view + of the device too */ + while (queue_shift(dev, &e) == 0) { + dev->queue_nsync--; + if (sanitize_event(dev, &e, dev->sync_state) != EVENT_FILTER_DISCARD) + update_state(dev, &e); + } + + dev->sync_state = SYNC_NONE; + } + + /* Always read in some more events. Best case this smoothes over a potential SYN_DROPPED, + worst case we don't read fast enough and end up with SYN_DROPPED anyway. + + Except if the fd is in blocking mode and we still have events from the last read, don't + read in any more. + */ + do { + if (queue_num_elements(dev) == 0) { + rc = read_more_events(dev); + if (rc < 0 && rc != -EAGAIN) + goto out; + } + + if (flags & LIBEVDEV_READ_FLAG_FORCE_SYNC) { + dev->sync_state = SYNC_NEEDED; + rc = LIBEVDEV_READ_STATUS_SYNC; + goto out; + } + + if (queue_shift(dev, ev) != 0) + return -EAGAIN; + + filter_status = sanitize_event(dev, ev, dev->sync_state); + if (filter_status != EVENT_FILTER_DISCARD) + update_state(dev, ev); + + /* if we disabled a code, get the next event instead */ + } while (filter_status == EVENT_FILTER_DISCARD || + !libevdev_has_event_code(dev, ev->type, ev->code)); + + rc = LIBEVDEV_READ_STATUS_SUCCESS; + if (ev->type == EV_SYN && ev->code == SYN_DROPPED) { + dev->sync_state = SYNC_NEEDED; + rc = LIBEVDEV_READ_STATUS_SYNC; + } + + if (flags & LIBEVDEV_READ_FLAG_SYNC && dev->queue_nsync > 0) { + dev->queue_nsync--; + rc = LIBEVDEV_READ_STATUS_SYNC; + if (dev->queue_nsync == 0) + dev->sync_state = SYNC_NONE; + } + + out: + return rc; +} + +LIBEVDEV_EXPORT int +libevdev_has_event_pending(struct libevdev *dev) { + struct pollfd fds = {dev->fd, POLLIN, 0}; + int rc; + + if (!dev->initialized) { + log_bug(dev, "device not initialized. call libevdev_set_fd() first\n"); + return -EBADF; + } + + if (dev->fd < 0) + return -EBADF; + + if (queue_num_elements(dev) != 0) + return 1; + + rc = poll(&fds, 1, 0); + return (rc >= 0) ? rc : -errno; +} + +LIBEVDEV_EXPORT const char * +libevdev_get_name(const struct libevdev *dev) { + return dev->name ? dev->name : ""; +} + +LIBEVDEV_EXPORT const char * +libevdev_get_phys(const struct libevdev *dev) { + return dev->phys; +} + +LIBEVDEV_EXPORT const char * +libevdev_get_uniq(const struct libevdev *dev) { + return dev->uniq; +} + +#define STRING_SETTER(field) \ +LIBEVDEV_EXPORT void libevdev_set_##field(struct libevdev *dev, const char *field) \ +{ \ + if (field == NULL) \ + return; \ + free(dev->field); \ + dev->field = strdup(field); \ +} + +STRING_SETTER(name) + +STRING_SETTER(phys) + +STRING_SETTER(uniq) + +#define PRODUCT_GETTER(name) \ +LIBEVDEV_EXPORT int libevdev_get_id_##name(const struct libevdev *dev) \ +{ \ + return dev->ids.name; \ +} + +PRODUCT_GETTER(product) + +PRODUCT_GETTER(vendor) + +PRODUCT_GETTER(bustype) + +PRODUCT_GETTER(version) + +#define PRODUCT_SETTER(field) \ +LIBEVDEV_EXPORT void libevdev_set_id_##field(struct libevdev *dev, int field) \ +{ \ + dev->ids.field = field;\ +} + +PRODUCT_SETTER(product) + +PRODUCT_SETTER(vendor) + +PRODUCT_SETTER(bustype) + +PRODUCT_SETTER(version) + +LIBEVDEV_EXPORT int +libevdev_get_driver_version(const struct libevdev *dev) { + return dev->driver_version; +} + +LIBEVDEV_EXPORT int +libevdev_has_property(const struct libevdev *dev, unsigned int prop) { + return (prop <= INPUT_PROP_MAX) && bit_is_set(dev->props, prop); +} + +LIBEVDEV_EXPORT int +libevdev_enable_property(struct libevdev *dev, unsigned int prop) { + if (prop > INPUT_PROP_MAX) + return -1; + + set_bit(dev->props, prop); + return 0; +} + +LIBEVDEV_EXPORT int +libevdev_disable_property(struct libevdev *dev, unsigned int prop) { + if (prop > INPUT_PROP_MAX) + return -1; + + clear_bit(dev->props, prop); + return 0; +} + +LIBEVDEV_EXPORT int +libevdev_has_event_type(const struct libevdev *dev, unsigned int type) { + return type == EV_SYN || (type <= EV_MAX && bit_is_set(dev->bits, type)); +} + +LIBEVDEV_EXPORT int +libevdev_has_event_code(const struct libevdev *dev, unsigned int type, unsigned int code) { + const unsigned long *mask = NULL; + int max; + + if (!libevdev_has_event_type(dev, type)) + return 0; + + if (type == EV_SYN) + return 1; + + max = type_to_mask_const(dev, type, &mask); + + if (max == -1 || code > (unsigned int) max) + return 0; + + return bit_is_set(mask, code); +} + +LIBEVDEV_EXPORT int +libevdev_get_event_value(const struct libevdev *dev, unsigned int type, unsigned int code) { + int value = 0; + + if (!libevdev_has_event_type(dev, type) || !libevdev_has_event_code(dev, type, code)) + return 0; + + switch (type) { + case EV_ABS: + value = dev->abs_info[code].value; + break; + case EV_KEY: + value = bit_is_set(dev->key_values, code); + break; + case EV_LED: + value = bit_is_set(dev->led_values, code); + break; + case EV_SW: + value = bit_is_set(dev->sw_values, code); + break; + case EV_REP: + switch (code) { + case REP_DELAY: + libevdev_get_repeat(dev, &value, NULL); + break; + case REP_PERIOD: + libevdev_get_repeat(dev, NULL, &value); + break; + default: + value = 0; + break; + } + break; + default: + value = 0; + break; + } + + return value; +} + +LIBEVDEV_EXPORT int +libevdev_set_event_value(struct libevdev *dev, unsigned int type, unsigned int code, int value) { + int rc = 0; + struct input_event e; + + if (!libevdev_has_event_type(dev, type) || !libevdev_has_event_code(dev, type, code)) + return -1; + + e.type = type; + e.code = code; + e.value = value; + + if (sanitize_event(dev, &e, SYNC_NONE) != EVENT_FILTER_NONE) + return -1; + + switch (type) { + case EV_ABS: + rc = update_abs_state(dev, &e); + break; + case EV_KEY: + rc = update_key_state(dev, &e); + break; + case EV_LED: + rc = update_led_state(dev, &e); + break; + case EV_SW: + rc = update_sw_state(dev, &e); + break; + default: + rc = -1; + break; + } + + return rc; +} + +LIBEVDEV_EXPORT int +libevdev_fetch_event_value(const struct libevdev *dev, unsigned int type, unsigned int code, + int *value) { + if (libevdev_has_event_type(dev, type) && + libevdev_has_event_code(dev, type, code)) { + *value = libevdev_get_event_value(dev, type, code); + return 1; + } + + return 0; +} + +LIBEVDEV_EXPORT int +libevdev_get_slot_value(const struct libevdev *dev, unsigned int slot, unsigned int code) { + if (!libevdev_has_event_type(dev, EV_ABS) || !libevdev_has_event_code(dev, EV_ABS, code)) + return 0; + + if (dev->num_slots < 0 || slot >= (unsigned int) dev->num_slots) + return 0; + + if (code > ABS_MT_MAX || code < ABS_MT_MIN) + return 0; + + return *slot_value(dev, slot, code); +} + +LIBEVDEV_EXPORT int +libevdev_set_slot_value(struct libevdev *dev, unsigned int slot, unsigned int code, int value) { + if (!libevdev_has_event_type(dev, EV_ABS) || !libevdev_has_event_code(dev, EV_ABS, code)) + return -1; + + if (dev->num_slots == -1 || slot >= (unsigned int) dev->num_slots) + return -1; + + if (code > ABS_MT_MAX || code < ABS_MT_MIN) + return -1; + + if (code == ABS_MT_SLOT) { + if (value < 0 || value >= libevdev_get_num_slots(dev)) + return -1; + dev->current_slot = value; + } + + *slot_value(dev, slot, code) = value; + + return 0; +} + +LIBEVDEV_EXPORT int +libevdev_fetch_slot_value(const struct libevdev *dev, unsigned int slot, unsigned int code, + int *value) { + if (libevdev_has_event_type(dev, EV_ABS) && + libevdev_has_event_code(dev, EV_ABS, code) && + dev->num_slots >= 0 && + slot < (unsigned int) dev->num_slots) { + *value = libevdev_get_slot_value(dev, slot, code); + return 1; + } + + return 0; +} + +LIBEVDEV_EXPORT int +libevdev_get_num_slots(const struct libevdev *dev) { + return dev->num_slots; +} + +LIBEVDEV_EXPORT int +libevdev_get_current_slot(const struct libevdev *dev) { + return dev->current_slot; +} + +LIBEVDEV_EXPORT const struct input_absinfo * +libevdev_get_abs_info(const struct libevdev *dev, unsigned int code) { + if (!libevdev_has_event_type(dev, EV_ABS) || + !libevdev_has_event_code(dev, EV_ABS, code)) + return NULL; + + return &dev->abs_info[code]; +} + +#define ABS_GETTER(name) \ +LIBEVDEV_EXPORT int libevdev_get_abs_##name(const struct libevdev *dev, unsigned int code) \ +{ \ + const struct input_absinfo *absinfo = libevdev_get_abs_info(dev, code); \ + return absinfo ? absinfo->name : 0; \ +} + +ABS_GETTER(maximum) + +ABS_GETTER(minimum) + +ABS_GETTER(fuzz) + +ABS_GETTER(flat) + +ABS_GETTER(resolution) + +#define ABS_SETTER(field) \ +LIBEVDEV_EXPORT void libevdev_set_abs_##field(struct libevdev *dev, unsigned int code, int val) \ +{ \ + if (!libevdev_has_event_code(dev, EV_ABS, code)) \ + return; \ + dev->abs_info[code].field = val; \ +} + +ABS_SETTER(maximum) + +ABS_SETTER(minimum) + +ABS_SETTER(fuzz) + +ABS_SETTER(flat) + +ABS_SETTER(resolution) + +LIBEVDEV_EXPORT void +libevdev_set_abs_info(struct libevdev *dev, unsigned int code, const struct input_absinfo *abs) { + if (!libevdev_has_event_code(dev, EV_ABS, code)) + return; + + dev->abs_info[code] = *abs; +} + +LIBEVDEV_EXPORT int +libevdev_enable_event_type(struct libevdev *dev, unsigned int type) { + int max; + + if (type > EV_MAX) + return -1; + + if (libevdev_has_event_type(dev, type)) + return 0; + + max = libevdev_event_type_get_max(type); + if (max == -1) + return -1; + + set_bit(dev->bits, type); + + if (type == EV_REP) { + int delay = 0, period = 0; + libevdev_enable_event_code(dev, EV_REP, REP_DELAY, &delay); + libevdev_enable_event_code(dev, EV_REP, REP_PERIOD, &period); + } + return 0; +} + +LIBEVDEV_EXPORT int +libevdev_disable_event_type(struct libevdev *dev, unsigned int type) { + int max; + + if (type > EV_MAX || type == EV_SYN) + return -1; + + max = libevdev_event_type_get_max(type); + if (max == -1) + return -1; + + clear_bit(dev->bits, type); + + return 0; +} + +LIBEVDEV_EXPORT int +libevdev_enable_event_code(struct libevdev *dev, unsigned int type, + unsigned int code, const void *data) { + unsigned int max; + unsigned long *mask = NULL; + + if (libevdev_enable_event_type(dev, type)) + return -1; + + switch (type) { + case EV_SYN: + return 0; + case EV_ABS: + case EV_REP: + if (data == NULL) + return -1; + break; + default: + if (data != NULL) + return -1; + break; + } + + max = type_to_mask(dev, type, &mask); + + if (code > max || (int) max == -1) + return -1; + + set_bit(mask, code); + + if (type == EV_ABS) { + const struct input_absinfo *abs = data; + dev->abs_info[code] = *abs; + if (code == ABS_MT_SLOT) { + if (init_slots(dev) != 0) + return -1; + } else if (code == ABS_MT_TRACKING_ID) { + reset_tracking_ids(dev); + } + } else if (type == EV_REP) { + const int *value = data; + dev->rep_values[code] = *value; + } + + return 0; +} + +LIBEVDEV_EXPORT int +libevdev_disable_event_code(struct libevdev *dev, unsigned int type, unsigned int code) { + unsigned int max; + unsigned long *mask = NULL; + + if (type > EV_MAX || type == EV_SYN) + return -1; + + max = type_to_mask(dev, type, &mask); + + if (code > max || (int) max == -1) + return -1; + + clear_bit(mask, code); + + if (type == EV_ABS) { + if (code == ABS_MT_SLOT) { + if (init_slots(dev) != 0) + return -1; + } else if (code == ABS_MT_TRACKING_ID) { + reset_tracking_ids(dev); + } + } + + return 0; +} + +LIBEVDEV_EXPORT int +libevdev_kernel_set_abs_info(struct libevdev *dev, unsigned int code, + const struct input_absinfo *abs) { + int rc; + + if (!dev->initialized) { + log_bug(dev, "device not initialized. call libevdev_set_fd() first\n"); + return -EBADF; + } + + if (dev->fd < 0) + return -EBADF; + + if (code > ABS_MAX) + return -EINVAL; + + rc = ioctl(dev->fd, EVIOCSABS(code), abs); + if (rc < 0) + rc = -errno; + else + rc = libevdev_enable_event_code(dev, EV_ABS, code, abs); + + return rc; +} + +LIBEVDEV_EXPORT int +libevdev_grab(struct libevdev *dev, enum libevdev_grab_mode grab) { + int rc = 0; + + if (!dev->initialized) { + log_bug(dev, "device not initialized. call libevdev_set_fd() first\n"); + return -EBADF; + } + + if (dev->fd < 0) + return -EBADF; + + if (grab != LIBEVDEV_GRAB && grab != LIBEVDEV_UNGRAB) { + log_bug(dev, "invalid grab parameter %#x\n", grab); + return -EINVAL; + } + + if (grab == dev->grabbed) + return 0; + + if (grab == LIBEVDEV_GRAB) + rc = ioctl(dev->fd, EVIOCGRAB, (void *) 1); + else if (grab == LIBEVDEV_UNGRAB) + rc = ioctl(dev->fd, EVIOCGRAB, (void *) 0); + + if (rc == 0) + dev->grabbed = grab; + + return rc < 0 ? -errno : 0; +} + +LIBEVDEV_EXPORT int +libevdev_event_is_type(const struct input_event *ev, unsigned int type) { + return type < EV_CNT && ev->type == type; +} + +LIBEVDEV_EXPORT int +libevdev_event_is_code(const struct input_event *ev, unsigned int type, unsigned int code) { + int max; + + if (!libevdev_event_is_type(ev, type)) + return 0; + + max = libevdev_event_type_get_max(type); + return (max > -1 && code <= (unsigned int) max && ev->code == code); +} + +LIBEVDEV_EXPORT const char * +libevdev_event_type_get_name(unsigned int type) { + if (type > EV_MAX) + return NULL; + + return ev_map[type]; +} + +LIBEVDEV_EXPORT const char * +libevdev_event_code_get_name(unsigned int type, unsigned int code) { + int max = libevdev_event_type_get_max(type); + + if (max == -1 || code > (unsigned int) max) + return NULL; + + return event_type_map[type][code]; +} + +LIBEVDEV_EXPORT const char * +libevdev_event_value_get_name(unsigned int type, + unsigned int code, + int value) { + /* This is a simplified version because nothing else + is an enum like ABS_MT_TOOL_TYPE so we don't need + a generic lookup */ + if (type != EV_ABS || code != ABS_MT_TOOL_TYPE) + return NULL; + + if (value < 0 || value > MT_TOOL_MAX) + return NULL; + + return mt_tool_map[value]; +} + +LIBEVDEV_EXPORT const char * +libevdev_property_get_name(unsigned int prop) { + if (prop > INPUT_PROP_MAX) + return NULL; + + return input_prop_map[prop]; +} + +LIBEVDEV_EXPORT int +libevdev_event_type_get_max(unsigned int type) { + if (type > EV_MAX) + return -1; + + return ev_max[type]; +} + +LIBEVDEV_EXPORT int +libevdev_get_repeat(const struct libevdev *dev, int *delay, int *period) { + if (!libevdev_has_event_type(dev, EV_REP)) + return -1; + + if (delay != NULL) + *delay = dev->rep_values[REP_DELAY]; + if (period != NULL) + *period = dev->rep_values[REP_PERIOD]; + + return 0; +} + +LIBEVDEV_EXPORT int +libevdev_kernel_set_led_value(struct libevdev *dev, unsigned int code, + enum libevdev_led_value value) { + return libevdev_kernel_set_led_values(dev, code, value, -1); +} + +LIBEVDEV_EXPORT int +libevdev_kernel_set_led_values(struct libevdev *dev, ...) { + struct input_event ev[LED_MAX + 1]; + enum libevdev_led_value val; + va_list args; + int code; + int rc = 0; + size_t nleds = 0; + + if (!dev->initialized) { + log_bug(dev, "device not initialized. call libevdev_set_fd() first\n"); + return -EBADF; + } + + if (dev->fd < 0) + return -EBADF; + + memset(ev, 0, sizeof(ev)); + + va_start(args, dev); + code = va_arg(args, unsigned int); + while (code != -1) { + if (code > LED_MAX) { + rc = -EINVAL; + break; + } + val = va_arg(args, enum libevdev_led_value); + if (val != LIBEVDEV_LED_ON && val != LIBEVDEV_LED_OFF) { + rc = -EINVAL; + break; + } + + if (libevdev_has_event_code(dev, EV_LED, code)) { + struct input_event *e = ev; + + while (e->type > 0 && e->code != code) + e++; + + if (e->type == 0) + nleds++; + e->type = EV_LED; + e->code = code; + e->value = (val == LIBEVDEV_LED_ON); + } + code = va_arg(args, unsigned int); + } + va_end(args); + + if (rc == 0 && nleds > 0) { + ev[nleds].type = EV_SYN; + ev[nleds++].code = SYN_REPORT; + + rc = write(libevdev_get_fd(dev), ev, nleds * sizeof(ev[0])); + if (rc > 0) { + nleds--; /* last is EV_SYN */ + while (nleds--) + update_led_state(dev, &ev[nleds]); + } + rc = (rc != -1) ? 0 : -errno; + } + + return rc; +} + +LIBEVDEV_EXPORT int +libevdev_set_clock_id(struct libevdev *dev, int clockid) { + if (!dev->initialized) { + log_bug(dev, "device not initialized. call libevdev_set_fd() first\n"); + return -EBADF; + } + + if (dev->fd < 0) + return -EBADF; + + return ioctl(dev->fd, EVIOCSCLOCKID, &clockid) ? -errno : 0; +} diff --git a/sysbridge/src/main/cpp/libevdev/libevdev.h b/sysbridge/src/main/cpp/libevdev/libevdev.h new file mode 100644 index 0000000000..b4b34ead63 --- /dev/null +++ b/sysbridge/src/main/cpp/libevdev/libevdev.h @@ -0,0 +1,2387 @@ +/* SPDX-License-Identifier: MIT */ +/* + * Copyright © 2013 Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * + */ + +#ifndef LIBEVDEV_H +#define LIBEVDEV_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#define LIBEVDEV_ATTRIBUTE_PRINTF(_format, _args) __attribute__ ((format (printf, _format, _args))) + +/** + * @mainpage + * + * **libevdev** is a library for handling evdev kernel devices. It abstracts + * the \ref ioctls through type-safe interfaces and provides functions to change + * the appearance of the device. + * + * Development + * =========== + * The git repository is available here: + * + * - https://gitlab.freedesktop.org/libevdev/libevdev + * + * Development of libevdev is discussed on + * [input-tools@lists.freedesktop.org](http://lists.freedesktop.org/mailman/listinfo/input-tools). + * Please submit patches, questions or general comments there. + * + * Handling events and SYN_DROPPED + * =============================== + * + * libevdev provides an interface for handling events, including most notably + * `SYN_DROPPED` events. `SYN_DROPPED` events are sent by the kernel when the + * process does not read events fast enough and the kernel is forced to drop + * some events. This causes the device to get out of sync with the process' + * view of it. libevdev handles this by telling the caller that a * `SYN_DROPPED` + * has been received and that the state of the device is different to what is + * to be expected. It then provides the delta between the previous state and + * the actual state of the device as a set of events. See + * libevdev_next_event() and @ref syn_dropped for more information on how + * `SYN_DROPPED` is handled. + * + * Signal safety + * ============= + * + * libevdev is signal-safe for the majority of its operations, i.e. many of + * its functions are safe to be called from within a signal handler. + * Check the API documentation to make sure, unless explicitly stated a call + * is not signal safe. + * + * Device handling + * =============== + * + * A libevdev context is valid for a given file descriptor and its + * duration. Closing the file descriptor will not destroy the libevdev device + * but libevdev will not be able to read further events. + * + * libevdev does not attempt duplicate detection. Initializing two libevdev + * devices for the same fd is valid and behaves the same as for two different + * devices. + * + * libevdev does not handle the file descriptors directly, it merely uses + * them. The caller is responsible for opening the file descriptors, setting + * them to `O_NONBLOCK` and handling permissions. A caller should drain any + * events pending on the file descriptor before passing it to libevdev. + * + * Where does libevdev sit? + * ======================== + * + * libevdev is essentially a `read(2)` on steroids for `/dev/input/eventX` + * devices. It sits below the process that handles input events, in between + * the kernel and that process. In the simplest case, e.g. an evtest-like tool + * the stack would look like this: + * + * kernel → libevdev → evtest + * + * For X.Org input modules, the stack would look like this: + * + * kernel → libevdev → xf86-input-evdev → X server → X client + * + * For anything using libinput (e.g. most Wayland compositors), the stack + * the stack would look like this: + * + * kernel → libevdev → libinput → Compositor → Wayland client + * + * libevdev does **not** have knowledge of X clients or Wayland clients, it is + * too low in the stack. + * + * Example + * ======= + * Below is a simple example that shows how libevdev could be used. This example + * opens a device, checks for relative axes and a left mouse button and if it + * finds them monitors the device to print the event. + * + * @code + * struct libevdev *dev = NULL; + * int fd; + * int rc = 1; + * + * fd = open("/dev/input/event0", O_RDONLY|O_NONBLOCK); + * rc = libevdev_new_from_fd(fd, &dev); + * if (rc < 0) { + * fprintf(stderr, "Failed to init libevdev (%s)\n", strerror(-rc)); + * exit(1); + * } + * printf("Input device name: \"%s\"\n", libevdev_get_name(dev)); + * printf("Input device ID: bus %#x vendor %#x product %#x\n", + * libevdev_get_id_bustype(dev), + * libevdev_get_id_vendor(dev), + * libevdev_get_id_product(dev)); + * if (!libevdev_has_event_type(dev, EV_REL) || + * !libevdev_has_event_code(dev, EV_KEY, BTN_LEFT)) { + * printf("This device does not look like a mouse\n"); + * exit(1); + * } + * + * do { + * struct input_event ev; + * rc = libevdev_next_event(dev, LIBEVDEV_READ_FLAG_NORMAL, &ev); + * if (rc == 0) + * printf("Event: %s %s %d\n", + * libevdev_event_type_get_name(ev.type), + * libevdev_event_code_get_name(ev.type, ev.code), + * ev.value); + * } while (rc == 1 || rc == 0 || rc == -EAGAIN); + * @endcode + * + * A more complete example is available with the libevdev-events tool here: + * https://gitlab.freedesktop.org/libevdev/libevdev/blob/master/tools/libevdev-events.c + * + * Backwards compatibility with older kernel + * ========================================= + * libevdev attempts to build and run correctly on a number of kernel versions. + * If features required are not available, libevdev attempts to work around them + * in the most reasonable way. For more details see \ref backwardscompatibility. + * + * License information + * =================== + * libevdev is licensed under the + * [MIT license](http://cgit.freedesktop.org/libevdev/tree/COPYING). + * + * Bindings + * =================== + * - Python: https://gitlab.freedesktop.org/libevdev/python-libevdev + * - Haskell: http://hackage.haskell.org/package/evdev + * - Rust: https://crates.io/crates/evdev-rs + * + * Reporting bugs + * ============== + * Please report bugs in the freedesktop.org GitLab instance: + * https://gitlab.freedesktop.org/libevdev/libevdev/issues/ + */ + +/** + * @page syn_dropped SYN_DROPPED handling + * + * This page describes how libevdev handles `SYN_DROPPED` events. + * + * Receiving `SYN_DROPPED` events + * ============================== + * + * The kernel sends evdev events separated by an event of type `EV_SYN` and + * code `SYN_REPORT`. Such an event marks the end of a frame of hardware + * events. The number of events between `SYN_REPORT` events is arbitrary and + * depends on the hardware. An example event sequence may look like this: + * @code + * EV_ABS ABS_X 9 + * EV_ABS ABS_Y 8 + * EV_SYN SYN_REPORT 0 + * ------------------------ + * EV_ABS ABS_X 10 + * EV_ABS ABS_Y 10 + * EV_KEY BTN_TOUCH 1 + * EV_SYN SYN_REPORT 0 + * ------------------------ + * EV_ABS ABS_X 11 + * EV_SYN SYN_REPORT 0 + * @endcode + * + * Events are handed to the client buffer as they appear, the kernel adjusts + * the buffer size to handle at least one full event. In the normal case, + * the client reads the event and the kernel can place the next event in the + * buffer. If the client is not fast enough, the kernel places an event of + * type `EV_SYN` and code `SYN_DROPPED` into the buffer, effectively notifying + * the client that some events were lost. The above example event sequence + * may look like this (note the missing/repeated events): + * @code + * EV_ABS ABS_X 9 + * EV_ABS ABS_Y 8 + * EV_SYN SYN_REPORT 0 + * ------------------------ + * EV_ABS ABS_X 10 + * EV_ABS ABS_Y 10 + * EV_SYN SYN_DROPPED 0 + * EV_ABS ABS_Y 15 + * EV_SYN SYN_REPORT 0 + * ------------------------ + * EV_ABS ABS_X 11 + * EV_KEY BTN_TOUCH 0 + * EV_SYN SYN_REPORT 0 + * @endcode + * + * A `SYN_DROPPED` event may be recieved at any time in the event sequence. + * When a `SYN_DROPPED` event is received, the client must: + * * discard all events since the last `SYN_REPORT` + * * discard all events until including the next `SYN_REPORT` + * These event are part of incomplete event frames. + * + * Synchronizing the state of the device + * ===================================== + * + * The handling of the device after a `SYN_DROPPED` depends on the available + * event codes. For all event codes of type `EV_REL`, no handling is + * necessary, there is no state attached. For all event codes of type + * `EV_KEY`, `EV_SW`, `EV_LED` and `EV_SND`, the matching @ref ioctls retrieve the + * current state. The caller must then compare the last-known state to the + * retrieved state and handle the deltas accordingly. + * libevdev simplifies this approach: if the state of the device has + * changed, libevdev generates an event for each code with the new value and + * passes it to the caller during libevdev_next_event() if + * @ref LIBEVDEV_READ_FLAG_SYNC is set. + * + * For events of type `EV_ABS` and an event code less than `ABS_MT_SLOT`, the + * handling of state changes is as described above. For events between + * `ABS_MT_SLOT` and `ABS_MAX`, the event handling differs. + * Slots are the vehicles to transport information for multiple simultaneous + * touchpoints on a device. Slots are re-used once a touchpoint has ended. + * The kernel sends an `ABS_MT_SLOT` event whenever the current slot + * changes; any event in the above axis range applies only to the currently + * active slot. + * Thus, an event sequence from a slot-capable device may look like this: + * @code + * EV_ABS ABS_MT_POSITION_Y 10 + * EV_ABS ABS_MT_SLOT 1 + * EV_ABS ABS_MT_POSITION_X 100 + * EV_ABS ABS_MT_POSITION_Y 80 + * EV_SYN SYN_REPORT 0 + * @endcode + * Note the lack of `ABS_MT_SLOT`: the first `ABS_MT_POSITION_Y` applies to + * a slot opened previously, and is the only axis that changed for that + * slot. The touchpoint in slot 1 now has position `100/80`. + * The kernel does not provide events if a value does not change, and does + * not send `ABS_MT_SLOT` events if the slot does not change, or none of the + * values within a slot changes. A client must thus keep the state for each + * slot. + * + * If a `SYN_DROPPED` is received, the client must sync all slots + * individually and update its internal state. libevdev simplifies this by + * generating multiple events: + * * for each slot on the device, libevdev generates an + * `ABS_MT_SLOT` event with the value set to the slot number + * * for each event code between `ABS_MT_SLOT + 1` and `ABS_MAX` that changed + * state for this slot, libevdev generates an event for the new state + * * libevdev sends a final `ABS_MT_SLOT` event for the current slot as + * seen by the kernel + * * libevdev terminates this sequence with an `EV_SYN SYN_REPORT` event + * + * An example event sequence for such a sync may look like this: + * @code + * EV_ABS ABS_MT_SLOT 0 + * EV_ABS ABS_MT_POSITION_Y 10 + * EV_ABS ABS_MT_SLOT 1 + * EV_ABS ABS_MT_POSITION_X 100 + * EV_ABS ABS_MT_POSITION_Y 80 + * EV_ABS ABS_MT_SLOT 2 + * EV_ABS ABS_MT_POSITION_Y 8 + * EV_ABS ABS_MT_PRESSURE 12 + * EV_ABS ABS_MT_SLOT 1 + * EV_SYN SYN_REPORT 0 + * @endcode + * Note the terminating `ABS_MT_SLOT` event, this indicates that the kernel + * currently has slot 1 active. + * + * Synchronizing ABS_MT_TRACKING_ID + * ================================ + * + * The event code `ABS_MT_TRACKING_ID` is used to denote the start and end of + * a touch point within a slot. An `ABS_MT_TRACKING_ID` of zero or greater + * denotes the start of a touchpoint, an `ABS_MT_TRACKING_ID` of -1 denotes + * the end of a touchpoint within this slot. During `SYN_DROPPED`, a touch + * point may have ended and re-started within a slot - a client must check + * the `ABS_MT_TRACKING_ID`. libevdev simplifies this by emulating extra + * events if the `ABS_MT_TRACKING_ID` has changed: + * * if the `ABS_MT_TRACKING_ID` was valid and is -1, libevdev enqueues an + * `ABS_MT_TRACKING_ID` event with value -1. + * * if the `ABS_MT_TRACKING_ID` was -1 and is now a valid ID, libevdev + * enqueues an `ABS_MT_TRACKING_ID` event with the current value. + * * if the `ABS_MT_TRACKING_ID` was a valid ID and is now a different valid + * ID, libevev enqueues an `ABS_MT_TRACKING_ID` event with value -1 and + * another `ABS_MT_TRACKING_ID` event with the new value. + * + * An example event sequence for such a sync may look like this: + * @code + * EV_ABS ABS_MT_SLOT 0 + * EV_ABS ABS_MT_TRACKING_ID -1 + * EV_ABS ABS_MT_SLOT 2 + * EV_ABS ABS_MT_TRACKING_ID -1 + * EV_SYN SYN_REPORT 0 + * ------------------------ + * EV_ABS ABS_MT_SLOT 1 + * EV_ABS ABS_MT_POSITION_X 100 + * EV_ABS ABS_MT_POSITION_Y 80 + * EV_ABS ABS_MT_SLOT 2 + * EV_ABS ABS_MT_TRACKING_ID 45 + * EV_ABS ABS_MT_POSITION_Y 8 + * EV_ABS ABS_MT_PRESSURE 12 + * EV_ABS ABS_MT_SLOT 1 + * EV_SYN SYN_REPORT 0 + * @endcode + * Note how the touchpoint in slot 0 was terminated, the touchpoint in slot + * 2 was terminated and then started with a new `ABS_MT_TRACKING_ID`. The touchpoint + * in slot 1 maintained the same `ABS_MT_TRACKING_ID` and only updated the + * coordinates. Slot 1 is the currently active slot. + * + * In the case of a `SYN_DROPPED` event, a touch point may be invisible to a + * client if it started after `SYN_DROPPED` and finished before the client + * handles events again. The below example shows an example event sequence + * and what libevdev sees in the case of a `SYN_DROPPED` event: + * @code + * + * kernel | userspace + * | + * EV_ABS ABS_MT_SLOT 0 | EV_ABS ABS_MT_SLOT 0 + * EV_ABS ABS_MT_TRACKING_ID -1 | EV_ABS ABS_MT_TRACKING_ID -1 + * EV_SYN SYN_REPORT 0 | EV_SYN SYN_REPORT 0 + * ------------------------ | ------------------------ + * EV_ABS ABS_MT_TRACKING_ID 30 | + * EV_ABS ABS_MT_POSITION_X 100 | + * EV_ABS ABS_MT_POSITION_Y 80 | + * EV_SYN SYN_REPORT 0 | SYN_DROPPED + * ------------------------ | + * EV_ABS ABS_MT_TRACKING_ID -1 | + * EV_SYN SYN_REPORT 0 | + * ------------------------ | ------------------------ + * EV_ABS ABS_MT_SLOT 1 | EV_ABS ABS_MT_SLOT 1 + * EV_ABS ABS_MT_POSITION_X 90 | EV_ABS ABS_MT_POSITION_X 90 + * EV_ABS ABS_MT_POSITION_Y 10 | EV_ABS ABS_MT_POSITION_Y 10 + * EV_SYN SYN_REPORT 0 | EV_SYN SYN_REPORT 0 + * @endcode + * If such an event sequence occurs, libevdev will send all updated axes + * during the sync process. Axis events may thus be generated for devices + * without a currently valid `ABS_MT_TRACKING_ID`. Specifically for the above + * example, the client would receive the following event sequence: + * @code + * EV_ABS ABS_MT_SLOT 0 ← LIBEVDEV_READ_FLAG_NORMAL + * EV_ABS ABS_MT_TRACKING_ID -1 + * EV_SYN SYN_REPORT 0 + * ------------------------ + * EV_SYN SYN_DROPPED 0 → LIBEVDEV_READ_STATUS_SYNC + * ------------------------ + * EV_ABS ABS_MT_POSITION_X 100 ← LIBEVDEV_READ_FLAG_SYNC + * EV_ABS ABS_MT_POSITION_Y 80 + * EV_SYN SYN_REPORT 0 + * ----------------------------- → -EGAIN + * EV_ABS ABS_MT_SLOT 1 ← LIBEVDEV_READ_FLAG_NORMAL + * EV_ABS ABS_MT_POSITION_X 90 + * EV_ABS ABS_MT_POSITION_Y 10 + * EV_SYN SYN_REPORT 0 + * ------------------- + * @endcode + * The axis events do not reflect the position of a current touch point, a + * client must take care not to generate a new touch point based on those + * updates. + * + * Discarding events before synchronizing + * ===================================== + * + * The kernel implements the client buffer as a ring buffer. `SYN_DROPPED` + * events are handled when the buffer is full and a new event is received + * from a device. All existing events are discarded, a `SYN_DROPPED` is added + * to the buffer followed by the actual device event. Further events will be + * appended to the buffer until it is either read by the client, or filled + * again, at which point the sequence repeats. + * + * When the client reads the buffer, the buffer will thus always consist of + * exactly one `SYN_DROPPED` event followed by an unspecified number of real + * events. The data the ioctls return is the current state of the device, + * i.e. the state after all these events have been processed. For example, + * assume the buffer contains the following sequence: + * + * @code + * EV_SYN SYN_DROPPED + * EV_ABS ABS_X 1 + * EV_SYN SYN_REPORT 0 + * EV_ABS ABS_X 2 + * EV_SYN SYN_REPORT 0 + * EV_ABS ABS_X 3 + * EV_SYN SYN_REPORT 0 + * EV_ABS ABS_X 4 + * EV_SYN SYN_REPORT 0 + * EV_ABS ABS_X 5 + * EV_SYN SYN_REPORT 0 + * EV_ABS ABS_X 6 + * EV_SYN SYN_REPORT 0 + * @endcode + * An ioctl at any time in this sequence will return a value of 6 for ABS_X. + * + * libevdev discards all events after a `SYN_DROPPED` to ensure the events + * during @ref LIBEVDEV_READ_FLAG_SYNC represent the last known state of the + * device. This loses some granularity of the events especially as the time + * between the `SYN_DROPPED` and the sync process increases. It does however + * avoid spurious cursor movements. In the above example, the event sequence + * by libevdev is: + * @code + * EV_SYN SYN_DROPPED + * EV_ABS ABS_X 6 + * EV_SYN SYN_REPORT 0 + * @endcode + */ + +/** + * @page backwardscompatibility Compatibility and Behavior across kernel versions + * + * This page describes libevdev's behavior when the build-time kernel and the + * run-time kernel differ in their feature set. + * + * With the exception of event names, libevdev defines features that may be + * missing on older kernels and building on such kernels will not disable + * features. Running libevdev on a kernel that is missing some feature will + * change libevdev's behavior. In most cases, the new behavior should be + * obvious, but it is spelled out below in detail. + * + * Minimum requirements + * ==================== + * libevdev requires a 2.6.36 kernel as minimum. Specifically, it requires + * kernel-support for `ABS_MT_SLOT`. + * + * Event and input property names + * ============================== + * Event names and input property names are defined at build-time by the + * linux/input.h shipped with libevdev. + * The list of event names is compiled at build-time, any events not defined + * at build time will not resolve. Specifically, + * libevdev_event_code_get_name() for an undefined type or code will + * always return `NULL`. Likewise, libevdev_property_get_name() will return NULL + * for properties undefined at build-time. + * + * Input properties + * ================ + * If the kernel does not support input properties, specifically the + * `EVIOCGPROPS` ioctl, libevdev does not expose input properties to the caller. + * Specifically, libevdev_has_property() will always return 0 unless the + * property has been manually set with libevdev_enable_property(). + * + * This also applies to the libevdev-uinput code. If uinput does not honor + * `UI_SET_PROPBIT`, libevdev will continue without setting the properties on + * the device. + * + * MT slot behavior + * ================= + * If the kernel does not support the `EVIOCGMTSLOTS` ioctl, libevdev + * assumes all values in all slots are 0 and continues without an error. + * + * SYN_DROPPED behavior + * ==================== + * A kernel without `SYN_DROPPED` won't send such an event. libevdev_next_event() + * will never require the switch to sync mode. + */ + +/** + * @page ioctls evdev ioctls + * + * This page lists the status of the evdev-specific ioctls in libevdev. + * + *

+ *
EVIOCGVERSION:
+ *
supported, see libevdev_get_driver_version()
+ *
EVIOCGID:
+ *
supported, see libevdev_get_id_product(), libevdev_get_id_vendor(), + * libevdev_get_id_bustype(), libevdev_get_id_version()
+ *
EVIOCGREP:
+ *
supported, see libevdev_get_event_value())
+ *
EVIOCSREP:
+ *
supported, see libevdev_enable_event_code()
+ *
EVIOCGKEYCODE:
+ *
currently not supported
+ *
EVIOCSKEYCODE:
+ *
currently not supported
+ *
EVIOCGKEYCODE_V2:
+ *
currently not supported
+ *
EVIOCSKEYCODE_V2:
+ *
currently not supported
+ *
EVIOCGNAME:
+ *
supported, see libevdev_get_name()
+ *
EVIOCGPHYS:
+ *
supported, see libevdev_get_phys()
+ *
EVIOCGUNIQ:
+ *
supported, see libevdev_get_uniq()
+ *
EVIOCGPROP:
+ *
supported, see libevdev_has_property()
+ *
EVIOCGMTSLOTS:
+ *
supported, see libevdev_get_num_slots(), libevdev_get_slot_value()
+ *
EVIOCGKEY:
+ *
supported, see libevdev_has_event_code(), libevdev_get_event_value()
+ *
EVIOCGLED:
+ *
supported, see libevdev_has_event_code(), libevdev_get_event_value()
+ *
EVIOCGSND:
+ *
currently not supported
+ *
EVIOCGSW:
+ *
supported, see libevdev_has_event_code(), libevdev_get_event_value()
+ *
EVIOCGBIT:
+ *
supported, see libevdev_has_event_code(), libevdev_get_event_value()
+ *
EVIOCGABS:
+ *
supported, see libevdev_has_event_code(), libevdev_get_event_value(), + * libevdev_get_abs_info()
+ *
EVIOCSABS:
+ *
supported, see libevdev_kernel_set_abs_info()
+ *
EVIOCSFF:
+ *
currently not supported
+ *
EVIOCRMFF:
+ *
currently not supported
+ *
EVIOCGEFFECTS:
+ *
currently not supported
+ *
EVIOCGRAB:
+ *
supported, see libevdev_grab()
+ *
EVIOCSCLOCKID:
+ *
supported, see libevdev_set_clock_id()
+ *
EVIOCREVOKE:
+ *
currently not supported, see + * http://lists.freedesktop.org/archives/input-tools/2014-January/000688.html
+ *
EVIOCGMASK:
+ *
currently not supported
+ *
EVIOCSMASK:
+ *
currently not supported
+ *
+ * + */ + +/** + * @page kernel_header Kernel header + * + * libevdev provides its own copy of the Linux kernel header file and + * compiles against the definitions define here. Event type and event code + * names, etc. are taken from the file below: + * @include linux/input.h + */ + +/** + * @page static_linking Statically linking libevdev + * + * Statically linking libevdev.a is not recommended. Symbol visibility is + * difficult to control in a static library, so extra care must be taken to + * only use symbols that are explicitly exported. libevdev's API stability + * guarantee only applies to those symbols. + * + * If you do link libevdev statically, note that in addition to the exported + * symbols, libevdev reserves the _libevdev_* namespace. Do not use + * or create symbols with that prefix, they are subject to change at any + * time. + */ + +/** + * @page testing libevdev-internal test suite + * + * libevdev's internal test suite uses the + * [Check unit testing framework](http://check.sourceforge.net/). Tests are + * divided into test suites and test cases. Most tests create a uinput device, + * so you'll need to run as root, and your kernel must have + * `CONFIG_INPUT_UINPUT` enabled. + * + * To run a specific suite only: + * + * export CK_RUN_SUITE="suite name" + * + * To run a specific test case only: + * + * export CK_RUN_TEST="test case name" + * + * To get a list of all suites or tests: + * + * git grep "suite_create" + * git grep "tcase_create" + * + * By default, Check forks, making debugging harder. The test suite tries to detect + * if it is running inside gdb and disable forking. If that doesn't work for + * some reason, run gdb as below to avoid forking. + * + * sudo CK_FORK=no CK_RUN_TEST="test case name" gdb ./test/test-libevdev + * + * A special target `make gcov-report.txt` exists that runs gcov and leaves a + * `libevdev.c.gcov` file. Check that for test coverage. + * + * `make check` is hooked up to run the test and gcov (again, needs root). + * + * The test suite creates a lot of devices, very quickly. Add the following + * xorg.conf.d snippet to avoid the devices being added as X devices (at the + * time of writing, mutter can't handle these devices and exits after getting + * a BadDevice error). + * + * $ cat /etc/X11/xorg.conf.d/99-ignore-libevdev-devices.conf + * Section "InputClass" + * Identifier "Ignore libevdev test devices" + * MatchProduct "libevdev test device" + * Option "Ignore" "on" + * EndSection + * + */ + +/** + * @defgroup init Initialization and setup + * + * Initialization, initial setup and file descriptor handling. + * These functions are the main entry points for users of libevdev, usually a + * caller will use this series of calls: + * + * @code + * struct libevdev *dev; + * int err; + * + * dev = libevdev_new(); + * if (!dev) + * return ENOMEM; + * + * err = libevdev_set_fd(dev, fd); + * if (err < 0) + * printf("Failed (errno %d): %s\n", -err, strerror(-err)); + * + * libevdev_free(dev); + * @endcode + * + * libevdev_set_fd() is the central call and initializes the internal structs + * for the device at the given fd. libevdev functions will fail if called + * before libevdev_set_fd() unless documented otherwise. + */ + +/** + * @defgroup logging Library logging facilities + * + * libevdev provides two methods of logging library-internal messages. The + * old method is to provide a global log handler in + * libevdev_set_log_function(). The new method is to provide a per-context + * log handler in libevdev_set_device_log_function(). Developers are encouraged + * to use the per-context logging facilities over the global log handler as + * it provides access to the libevdev instance that caused a message, and is + * more flexible when libevdev is used from within a shared library. + * + * If a caller sets both the global log handler and a per-context log + * handler, each device with a per-context log handler will only invoke that + * log handler. + * + * @note To set a context-specific log handler, a context is needed. + * Thus developers are discouraged from using libevdev_new_from_fd() as + * important messages from the device initialization process may get lost. + * + * @note A context-specific handler cannot be used for libevdev's uinput + * devices. @ref uinput must use the global log handler. + */ + +/** + * @defgroup bits Querying device capabilities + * + * Abstraction functions to handle device capabilities, specifically + * device properties such as the name of the device and the bits + * representing the events supported by this device. + * + * The logical state returned may lag behind the physical state of the device. + * libevdev queries the device state on libevdev_set_fd() and then relies on + * the caller to parse events through libevdev_next_event(). If a caller does not + * use libevdev_next_event(), libevdev will not update the internal state of the + * device and thus returns outdated values. + */ + +/** + * @defgroup mt Multi-touch related functions + * Functions for querying multi-touch-related capabilities. MT devices + * following the kernel protocol B (using `ABS_MT_SLOT`) provide multiple touch + * points through so-called slots on the same axis. The slots are enumerated, + * a client reading from the device will first get an ABS_MT_SLOT event, then + * the values of axes changed in this slot. Multiple slots may be provided in + * before an `EV_SYN` event. + * + * As with @ref bits, the logical state of the device as seen by the library + * depends on the caller using libevdev_next_event(). + * + * The Linux kernel requires all axes on a device to have a semantic + * meaning, matching the axis names in linux/input.h. Some devices merely + * export a number of axes beyond the available axis list. For those + * devices, the multitouch information is invalid. Specifically, if a device + * provides the `ABS_MT_SLOT` axis AND also the `ABS_RESERVED` axis, the + * device is not treated as multitouch device. No slot information is + * available and the `ABS_MT` axis range for these devices is treated as all + * other `EV_ABS` axes. + * + * Note that because of limitations in the kernel API, such fake multitouch + * devices can not be reliably synced after a `SYN_DROPPED` event. libevdev + * ignores all `ABS_MT` axis values during the sync process and instead + * relies on the device to send the current axis value with the first event + * after `SYN_DROPPED`. + */ + +/** + * @defgroup kernel Modifying the appearance or capabilities of the device + * + * Modifying the set of events reported by this device. By default, the + * libevdev device mirrors the kernel device, enabling only those bits + * exported by the kernel. This set of functions enable or disable bits as + * seen from the caller. + * + * Enabling an event type or code does not affect event reporting - a + * software-enabled event will not be generated by the physical hardware. + * Disabling an event will prevent libevdev from routing such events to the + * caller. Enabling and disabling event types and codes is at the library + * level and thus only affects the caller. + * + * If an event type or code is enabled at kernel-level, future users of this + * device will see this event enabled. Currently there is no option of + * disabling an event type or code at kernel-level. + */ + +/** + * @defgroup misc Miscellaneous helper functions + * + * Functions for printing or querying event ranges. The list of names is + * compiled into libevdev and is independent of the run-time kernel. + * Likewise, the max for each event type is compiled in and does not check + * the kernel at run-time. + */ + +/** + * @defgroup events Event handling + * + * Functions to handle events and fetch the current state of the event. + * libevdev updates its internal state as the event is processed and forwarded + * to the caller. Thus, the libevdev state of the device should always be identical + * to the caller's state. It may however lag behind the actual state of the device. + */ + +/** + * @ingroup init + * + * Opaque struct representing an evdev device. + */ +struct libevdev; + +/** + * @ingroup events + */ +enum libevdev_read_flag { + LIBEVDEV_READ_FLAG_SYNC = 1, /**< Process data in sync mode */ + LIBEVDEV_READ_FLAG_NORMAL = 2, /**< Process data in normal mode */ + LIBEVDEV_READ_FLAG_FORCE_SYNC = 4, /**< Pretend the next event is a SYN_DROPPED and + require the caller to sync */ + LIBEVDEV_READ_FLAG_BLOCKING = 8 /**< The fd is not in O_NONBLOCK and a read may block */ +}; + +/** + * @ingroup init + * + * Initialize a new libevdev device. This function only allocates the + * required memory and initializes the struct to sane default values. + * To actually hook up the device to a kernel device, use + * libevdev_set_fd(). + * + * Memory allocated through libevdev_new() must be released by the + * caller with libevdev_free(). + * + * @see libevdev_set_fd + * @see libevdev_free + */ +struct libevdev *libevdev_new(void); + +/** + * @ingroup init + * + * Initialize a new libevdev device from the given fd. + * + * This is a shortcut for + * + * @code + * int err; + * struct libevdev *dev = libevdev_new(); + * err = libevdev_set_fd(dev, fd); + * @endcode + * + * @param fd A file descriptor to the device in O_RDWR or O_RDONLY mode. + * @param[out] dev The newly initialized evdev device. + * + * @return On success, 0 is returned and dev is set to the newly + * allocated struct. On failure, a negative errno is returned and the value + * of dev is undefined. + * + * @see libevdev_free + */ +int libevdev_new_from_fd(int fd, struct libevdev **dev); + +/** + * @ingroup init + * + * Clean up and free the libevdev struct. After completion, the struct + * libevdev is invalid and must not be used. + * + * Note that calling libevdev_free() does not close the file descriptor + * currently associated with this instance. + * + * @param dev The evdev device + * + * @note This function may be called before libevdev_set_fd(). + */ +void libevdev_free(struct libevdev *dev); + +/** + * @ingroup logging + */ +enum libevdev_log_priority { + LIBEVDEV_LOG_ERROR = 10, /**< critical errors and application bugs */ + LIBEVDEV_LOG_INFO = 20, /**< informational messages */ + LIBEVDEV_LOG_DEBUG = 30 /**< debug information */ +}; + +/** + * @ingroup logging + * + * Logging function called by library-internal logging. + * This function is expected to treat its input like printf would. + * + * @param priority Log priority of this message + * @param data User-supplied data pointer (see libevdev_set_log_function()) + * @param file libevdev source code file generating this message + * @param line libevdev source code line generating this message + * @param func libevdev source code function generating this message + * @param format printf-style format string + * @param args List of arguments + * + * @see libevdev_set_log_function + */ +typedef void (*libevdev_log_func_t)(enum libevdev_log_priority priority, + void *data, + const char *file, int line, + const char *func, + const char *format, va_list args) + LIBEVDEV_ATTRIBUTE_PRINTF(6, 0); + +/** + * @ingroup logging + * + * Set a printf-style logging handler for library-internal logging. The default + * logging function is to stdout. + * + * @note The global log handler is only called if no context-specific log + * handler has been set with libevdev_set_device_log_function(). + * + * @param logfunc The logging function for this device. If NULL, the current + * logging function is unset and no logging is performed. + * @param data User-specific data passed to the log handler. + * + * @note This function may be called before libevdev_set_fd(). + * + * @deprecated Use per-context logging instead, see + * libevdev_set_device_log_function(). + */ +void libevdev_set_log_function(libevdev_log_func_t logfunc, void *data); + +/** + * @ingroup logging + * + * Define the minimum level to be printed to the log handler. + * Messages higher than this level are printed, others are discarded. This + * is a global setting and applies to any future logging messages. + * + * @param priority Minimum priority to be printed to the log. + * + * @deprecated Use per-context logging instead, see + * libevdev_set_device_log_function(). + */ +void libevdev_set_log_priority(enum libevdev_log_priority priority); + +/** + * @ingroup logging + * + * Return the current log priority level. Messages higher than this level + * are printed, others are discarded. This is a global setting. + * + * @return the current log level + * + * @deprecated Use per-context logging instead, see + * libevdev_set_device_log_function(). + */ +enum libevdev_log_priority libevdev_get_log_priority(void); + +/** + * @ingroup logging + * + * Logging function called by library-internal logging for a specific + * libevdev context. This function is expected to treat its input like + * printf would. + * + * @param dev The evdev device + * @param priority Log priority of this message + * @param data User-supplied data pointer (see libevdev_set_log_function()) + * @param file libevdev source code file generating this message + * @param line libevdev source code line generating this message + * @param func libevdev source code function generating this message + * @param format printf-style format string + * @param args List of arguments + * + * @see libevdev_set_log_function + * @since 1.3 + */ +typedef void (*libevdev_device_log_func_t)(const struct libevdev *dev, + enum libevdev_log_priority priority, + void *data, + const char *file, int line, + const char *func, + const char *format, va_list args) + LIBEVDEV_ATTRIBUTE_PRINTF(7, 0); + +/** + * @ingroup logging + * + * Set a printf-style logging handler for library-internal logging for this + * device context. The default logging function is NULL, i.e. the global log + * handler is invoked. If a context-specific log handler is set, the global + * log handler is not invoked for this device. + * + * @note This log function applies for this device context only, even if + * another context exists for the same fd. + * + * @param dev The evdev device + * @param logfunc The logging function for this device. If NULL, the current + * logging function is unset and logging falls back to the global log + * handler, if any. + * @param priority Minimum priority to be printed to the log. + * @param data User-specific data passed to the log handler. + * + * @note This function may be called before libevdev_set_fd(). + * @since 1.3 + */ +void libevdev_set_device_log_function(struct libevdev *dev, + libevdev_device_log_func_t logfunc, + enum libevdev_log_priority priority, + void *data); + +/** + * @ingroup init + */ +enum libevdev_grab_mode { + LIBEVDEV_GRAB = 3, /**< Grab the device if not currently grabbed */ + LIBEVDEV_UNGRAB = 4 /**< Ungrab the device if currently grabbed */ +}; + +/** + * @ingroup init + * + * Grab or ungrab the device through a kernel EVIOCGRAB. This prevents other + * clients (including kernel-internal ones such as rfkill) from receiving + * events from this device. + * + * This is generally a bad idea. Don't do this. + * + * Grabbing an already grabbed device, or ungrabbing an ungrabbed device is + * a noop and always succeeds. + * + * A grab is an operation tied to a file descriptor, not a device. If a + * client changes the file descriptor with libevdev_change_fd(), it must + * also re-issue a grab with libevdev_grab(). + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param grab If true, grab the device. Otherwise ungrab the device. + * + * @return 0 if the device was successfully grabbed or ungrabbed, or a + * negative errno in case of failure. + */ +int libevdev_grab(struct libevdev *dev, enum libevdev_grab_mode grab); + +/** + * @ingroup init + * + * Set the fd for this struct and initialize internal data. + * The fd must be in O_RDONLY or O_RDWR mode. + * + * This function may only be called once per device. If the device changed and + * you need to re-read a device, use libevdev_free() and libevdev_new(). If + * you need to change the fd after closing and re-opening the same device, use + * libevdev_change_fd(). + * + * A caller should ensure that any events currently pending on the fd are + * drained before the file descriptor is passed to libevdev for + * initialization. Due to how the kernel's ioctl handling works, the initial + * device state will reflect the current device state *after* applying all + * events currently pending on the fd. Thus, if the fd is not drained, the + * state visible to the caller will be inconsistent with the events + * immediately available on the device. This does not affect state-less + * events like EV_REL. + * + * Unless otherwise specified, libevdev function behavior is undefined until + * a successful call to libevdev_set_fd(). + * + * @param dev The evdev device + * @param fd The file descriptor for the device + * + * @return 0 on success, or a negative errno on failure + * + * @see libevdev_change_fd + * @see libevdev_new + * @see libevdev_free + */ +int libevdev_set_fd(struct libevdev *dev, int fd); + +/** + * @ingroup init + * + * Change the fd for this device, without re-reading the actual device. If the fd + * changes after initializing the device, for example after a VT-switch in the + * X.org X server, this function updates the internal fd to the newly opened. + * No check is made that new fd points to the same device. If the device has + * changed, libevdev's behavior is undefined. + * + * libevdev does not sync itself after changing the fd and keeps the current + * device state. Use libevdev_next_event with the + * @ref LIBEVDEV_READ_FLAG_FORCE_SYNC flag to force a re-sync. + * + * The example code below illustrates how to force a re-sync of the + * library-internal state. Note that this code doesn't handle the events in + * the caller, it merely forces an update of the internal library state. + * @code + * struct input_event ev; + * libevdev_change_fd(dev, new_fd); + * libevdev_next_event(dev, LIBEVDEV_READ_FLAG_FORCE_SYNC, &ev); + * while (libevdev_next_event(dev, LIBEVDEV_READ_FLAG_SYNC, &ev) == LIBEVDEV_READ_STATUS_SYNC) + * ; // noop + * @endcode + * + * The fd may be open in O_RDONLY or O_RDWR. + * + * After changing the fd, the device is assumed ungrabbed and a caller must + * call libevdev_grab() again. + * + * It is an error to call this function before calling libevdev_set_fd(). + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param fd The new fd + * + * @return 0 on success, or -1 on failure. + * + * @see libevdev_set_fd + */ +int libevdev_change_fd(struct libevdev *dev, int fd); + +/** + * @ingroup init + * + * @param dev The evdev device + * + * @return The previously set fd, or -1 if none had been set previously. + * @note This function may be called before libevdev_set_fd(). + */ +int libevdev_get_fd(const struct libevdev *dev); + +/** + * @ingroup events + */ +enum libevdev_read_status { + /** + * libevdev_next_event() has finished without an error + * and an event is available for processing. + * + * @see libevdev_next_event + */ + LIBEVDEV_READ_STATUS_SUCCESS = 0, + /** + * Depending on the libevdev_next_event() read flag: + * * libevdev received a SYN_DROPPED from the device, and the caller should + * now resync the device, or, + * * an event has been read in sync mode. + * + * @see libevdev_next_event + */ + LIBEVDEV_READ_STATUS_SYNC = 1 +}; + +/** + * @ingroup events + * + * Get the next event from the device. This function operates in two different + * modes: normal mode or sync mode. + * + * In normal mode (when flags has @ref LIBEVDEV_READ_FLAG_NORMAL set), this + * function returns @ref LIBEVDEV_READ_STATUS_SUCCESS and returns the event + * in the argument @p ev. If no events are available at this + * time, it returns -EAGAIN and ev is undefined. + * + * If the current event is an EV_SYN SYN_DROPPED event, this function returns + * @ref LIBEVDEV_READ_STATUS_SYNC and ev is set to the EV_SYN event. + * The caller should now call this function with the + * @ref LIBEVDEV_READ_FLAG_SYNC flag set, to get the set of events that make up the + * device state delta. This function returns @ref LIBEVDEV_READ_STATUS_SYNC for + * each event part of that delta, until it returns -EAGAIN once all events + * have been synced. For more details on what libevdev does to sync after a + * SYN_DROPPED event, see @ref syn_dropped. + * + * If a device needs to be synced by the caller but the caller does not call + * with the @ref LIBEVDEV_READ_FLAG_SYNC flag set, all events from the diff are + * dropped after libevdev updates its internal state and event processing + * continues as normal. Note that the current slot and the state of touch + * points may have updated during the SYN_DROPPED event, it is strongly + * recommended that a caller ignoring all sync events calls + * libevdev_get_current_slot() and checks the ABS_MT_TRACKING_ID values for + * all slots. + * + * If a device has changed state without events being enqueued in libevdev, + * e.g. after changing the file descriptor, use the @ref + * LIBEVDEV_READ_FLAG_FORCE_SYNC flag. This triggers an internal sync of the + * device and libevdev_next_event() returns @ref LIBEVDEV_READ_STATUS_SYNC. + * Any state changes are available as events as described above. If + * @ref LIBEVDEV_READ_FLAG_FORCE_SYNC is set, the value of ev is undefined. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param flags Set of flags to determine behaviour. If @ref LIBEVDEV_READ_FLAG_NORMAL + * is set, the next event is read in normal mode. If @ref LIBEVDEV_READ_FLAG_SYNC is + * set, the next event is read in sync mode. + * @param ev On success, set to the current event. + * @return On failure, a negative errno is returned. + * @retval LIBEVDEV_READ_STATUS_SUCCESS One or more events were read of the + * device and ev points to the next event in the queue + * @retval -EAGAIN No events are currently available on the device + * @retval LIBEVDEV_READ_STATUS_SYNC A SYN_DROPPED event was received, or a + * synced event was returned and ev points to the SYN_DROPPED event + * + * @note This function is signal-safe. + */ +int libevdev_next_event(struct libevdev *dev, unsigned int flags, struct input_event *ev); + +/** + * @ingroup events + * + * Check if there are events waiting for us. This function does not read an + * event off the fd and may not access the fd at all. If there are events + * queued internally this function will return non-zero. If the internal + * queue is empty, this function will poll the file descriptor for data. + * + * This is a convenience function for simple processes, most complex programs + * are expected to use select(2) or poll(2) on the file descriptor. The kernel + * guarantees that if data is available, it is a multiple of sizeof(struct + * input_event), and thus calling libevdev_next_event() when select(2) or + * poll(2) return is safe. You do not need libevdev_has_event_pending() if + * you're using select(2) or poll(2). + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @return On failure, a negative errno is returned. + * @retval 0 No event is currently available + * @retval 1 One or more events are available on the fd + * + * @note This function is signal-safe. + */ +int libevdev_has_event_pending(struct libevdev *dev); + +/** + * @ingroup bits + * + * Retrieve the device's name, either as set by the caller or as read from + * the kernel. The string returned is valid until libevdev_free() or until + * libevdev_set_name(), whichever comes earlier. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * + * @return The device name as read off the kernel device. The name is never + * NULL but it may be the empty string. + * + * @note This function is signal-safe. + */ +const char *libevdev_get_name(const struct libevdev *dev); + +/** + * @ingroup kernel + * + * Change the device's name as returned by libevdev_get_name(). This + * function destroys the string previously returned by libevdev_get_name(), + * a caller must take care that no references are kept. + * + * @param dev The evdev device + * @param name The new, non-NULL, name to assign to this device. + * + * @note This function may be called before libevdev_set_fd(). A call to + * libevdev_set_fd() will overwrite any previously set value. + */ +void libevdev_set_name(struct libevdev *dev, const char *name); + +/** + * @ingroup bits + * + * Retrieve the device's physical location, either as set by the caller or + * as read from the kernel. The string returned is valid until + * libevdev_free() or until libevdev_set_phys(), whichever comes earlier. + * + * Virtual devices such as uinput devices have no phys location. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * + * @return The physical location of this device, or NULL if there is none + * + * @note This function is signal safe. + */ +const char *libevdev_get_phys(const struct libevdev *dev); + +/** + * @ingroup kernel + * + * Change the device's physical location as returned by libevdev_get_phys(). + * This function destroys the string previously returned by + * libevdev_get_phys(), a caller must take care that no references are kept. + * + * @param dev The evdev device + * @param phys The new phys to assign to this device. + * + * @note This function may be called before libevdev_set_fd(). A call to + * libevdev_set_fd() will overwrite any previously set value. + */ +void libevdev_set_phys(struct libevdev *dev, const char *phys); + +/** + * @ingroup bits + * + * Retrieve the device's unique identifier, either as set by the caller or + * as read from the kernel. The string returned is valid until + * libevdev_free() or until libevdev_set_uniq(), whichever comes earlier. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * + * @return The unique identifier for this device, or NULL if there is none + * + * @note This function is signal safe. + */ +const char *libevdev_get_uniq(const struct libevdev *dev); + +/** + * @ingroup kernel + * + * Change the device's unique identifier as returned by libevdev_get_uniq(). + * This function destroys the string previously returned by + * libevdev_get_uniq(), a caller must take care that no references are kept. + * + * @param dev The evdev device + * @param uniq The new uniq to assign to this device. + * + * @note This function may be called before libevdev_set_fd(). A call to + * libevdev_set_fd() will overwrite any previously set value. + */ +void libevdev_set_uniq(struct libevdev *dev, const char *uniq); + +/** + * @ingroup bits + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * + * @return The device's product ID + * + * @note This function is signal-safe. + */ +int libevdev_get_id_product(const struct libevdev *dev); + +/** + * @ingroup kernel + * + * @param dev The evdev device + * @param product_id The product ID to assign to this device + * + * @note This function may be called before libevdev_set_fd(). A call to + * libevdev_set_fd() will overwrite any previously set value. Even though + * the function accepts an int for product_id the value is truncated at 16 + * bits. + */ +void libevdev_set_id_product(struct libevdev *dev, int product_id); + +/** + * @ingroup bits + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * + * @return The device's vendor ID + * + * @note This function is signal-safe. + */ +int libevdev_get_id_vendor(const struct libevdev *dev); + +/** + * @ingroup kernel + * + * @param dev The evdev device + * @param vendor_id The vendor ID to assign to this device + * + * @note This function may be called before libevdev_set_fd(). A call to + * libevdev_set_fd() will overwrite any previously set value. Even though + * the function accepts an int for vendor_id the value is truncated at 16 + * bits. + */ +void libevdev_set_id_vendor(struct libevdev *dev, int vendor_id); + +/** + * @ingroup bits + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * + * @return The device's bus type + * + * @note This function is signal-safe. + */ +int libevdev_get_id_bustype(const struct libevdev *dev); + +/** + * @ingroup kernel + * + * @param dev The evdev device + * @param bustype The bustype to assign to this device + * + * @note This function may be called before libevdev_set_fd(). A call to + * libevdev_set_fd() will overwrite any previously set value. Even though + * the function accepts an int for bustype the value is truncated at 16 + * bits. + */ +void libevdev_set_id_bustype(struct libevdev *dev, int bustype); + +/** + * @ingroup bits + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * + * @return The device's firmware version + * + * @note This function is signal-safe. + */ +int libevdev_get_id_version(const struct libevdev *dev); + +/** + * @ingroup kernel + * + * @param dev The evdev device + * @param version The version to assign to this device + * + * @note This function may be called before libevdev_set_fd(). A call to + * libevdev_set_fd() will overwrite any previously set value. Even though + * the function accepts an int for version the value is truncated at 16 + * bits. + */ +void libevdev_set_id_version(struct libevdev *dev, int version); + +/** + * @ingroup bits + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * + * @return The driver version for this device + * + * @note This function is signal-safe. + */ +int libevdev_get_driver_version(const struct libevdev *dev); + +/** + * @ingroup bits + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param prop The input property to query for, one of INPUT_PROP_... + * + * @return 1 if the device provides this input property, or 0 otherwise. + * + * @note This function is signal-safe + */ +int libevdev_has_property(const struct libevdev *dev, unsigned int prop); + +/** + * @ingroup kernel + * + * @param dev The evdev device + * @param prop The input property to enable, one of INPUT_PROP_... + * + * @return 0 on success or -1 on failure + * + * @note This function may be called before libevdev_set_fd(). A call to + * libevdev_set_fd() will overwrite any previously set value. + */ +int libevdev_enable_property(struct libevdev *dev, unsigned int prop); + +/** + * @ingroup kernel + * + * @param dev The evdev device + * @param prop The input property to disable, one of INPUT_PROP_... + * + * @return 0 on success or -1 on failure + */ +int libevdev_disable_property(struct libevdev *dev, unsigned int prop); + +/** + * @ingroup bits + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param type The event type to query for, one of EV_SYN, EV_REL, etc. + * + * @return 1 if the device supports this event type, or 0 otherwise. + * + * @note This function is signal-safe. + */ +int libevdev_has_event_type(const struct libevdev *dev, unsigned int type); + +/** + * @ingroup bits + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param type The event type for the code to query (EV_SYN, EV_REL, etc.) + * @param code The event code to query for, one of ABS_X, REL_X, etc. + * + * @return 1 if the device supports this event type and code, or 0 otherwise. + * + * @note This function is signal-safe. + */ +int libevdev_has_event_code(const struct libevdev *dev, unsigned int type, unsigned int code); + +/** + * @ingroup bits + * + * Get the minimum axis value for the given axis, as advertised by the kernel. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param code The EV_ABS event code to query for, one of ABS_X, ABS_Y, etc. + * + * @return axis minimum for the given axis or 0 if the axis is invalid + * + * @note This function is signal-safe. + */ +int libevdev_get_abs_minimum(const struct libevdev *dev, unsigned int code); + +/** + * @ingroup bits + * + * Get the maximum axis value for the given axis, as advertised by the kernel. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param code The EV_ABS event code to query for, one of ABS_X, ABS_Y, etc. + * + * @return axis maximum for the given axis or 0 if the axis is invalid + * + * @note This function is signal-safe. + */ +int libevdev_get_abs_maximum(const struct libevdev *dev, unsigned int code); + +/** + * @ingroup bits + * + * Get the axis fuzz for the given axis, as advertised by the kernel. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param code The EV_ABS event code to query for, one of ABS_X, ABS_Y, etc. + * + * @return axis fuzz for the given axis or 0 if the axis is invalid + * + * @note This function is signal-safe. + */ +int libevdev_get_abs_fuzz(const struct libevdev *dev, unsigned int code); + +/** + * @ingroup bits + * + * Get the axis flat for the given axis, as advertised by the kernel. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param code The EV_ABS event code to query for, one of ABS_X, ABS_Y, etc. + * + * @return axis flat for the given axis or 0 if the axis is invalid + * + * @note This function is signal-safe. + */ +int libevdev_get_abs_flat(const struct libevdev *dev, unsigned int code); + +/** + * @ingroup bits + * + * Get the axis resolution for the given axis, as advertised by the kernel. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param code The EV_ABS event code to query for, one of ABS_X, ABS_Y, etc. + * + * @return axis resolution for the given axis or 0 if the axis is invalid + * + * @note This function is signal-safe. + */ +int libevdev_get_abs_resolution(const struct libevdev *dev, unsigned int code); + +/** + * @ingroup bits + * + * Get the axis info for the given axis, as advertised by the kernel. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param code The EV_ABS event code to query for, one of ABS_X, ABS_Y, etc. + * + * @return The input_absinfo for the given code, or NULL if the device does + * not support this event code. + * + * @note This function is signal-safe. + */ +const struct input_absinfo *libevdev_get_abs_info(const struct libevdev *dev, unsigned int code); + +/** + * @ingroup bits + * + * Behaviour of this function is undefined if the device does not provide + * the event. + * + * If the device supports ABS_MT_SLOT, the value returned for any ABS_MT_* + * event code is undefined. Use libevdev_get_slot_value() instead. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param type The event type for the code to query (EV_SYN, EV_REL, etc.) + * @param code The event code to query for, one of ABS_X, REL_X, etc. + * + * @return The current value of the event. + * + * @note This function is signal-safe. + * @note The value for ABS_MT_ events is undefined, use + * libevdev_get_slot_value() instead + * + * @see libevdev_get_slot_value + */ +int libevdev_get_event_value(const struct libevdev *dev, unsigned int type, unsigned int code); + +/** + * @ingroup kernel + * + * Set the value for a given event type and code. This only makes sense for + * some event types, e.g. setting the value for EV_REL is pointless. + * + * This is a local modification only affecting only this representation of + * this device. A future call to libevdev_get_event_value() will return this + * value, unless the value was overwritten by an event. + * + * If the device supports ABS_MT_SLOT, the value set for any ABS_MT_* + * event code is the value of the currently active slot. You should use + * libevdev_set_slot_value() instead. + * + * If the device supports ABS_MT_SLOT and the type is EV_ABS and the code is + * ABS_MT_SLOT, the value must be a positive number less then the number of + * slots on the device. Otherwise, libevdev_set_event_value() returns -1. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param type The event type for the code to query (EV_SYN, EV_REL, etc.) + * @param code The event code to set the value for, one of ABS_X, LED_NUML, etc. + * @param value The new value to set + * + * @return 0 on success, or -1 on failure. + * @retval -1 + * - the device does not have the event type or + * - code enabled, or the code is outside the, or + * - the code is outside the allowed limits for the given type, or + * - the type cannot be set, or + * - the value is not permitted for the given code. + * + * @see libevdev_set_slot_value + * @see libevdev_get_event_value + */ +int libevdev_set_event_value(struct libevdev *dev, unsigned int type, unsigned int code, int value); + +/** + * @ingroup bits + * + * Fetch the current value of the event type. This is a shortcut for + * + * @code + * if (libevdev_has_event_type(dev, t) && libevdev_has_event_code(dev, t, c)) + * val = libevdev_get_event_value(dev, t, c); + * @endcode + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param type The event type for the code to query (EV_SYN, EV_REL, etc.) + * @param code The event code to query for, one of ABS_X, REL_X, etc. + * @param[out] value The current value of this axis returned. + * + * @return If the device supports this event type and code, the return value is + * non-zero and value is set to the current value of this axis. Otherwise, + * 0 is returned and value is unmodified. + * + * @note This function is signal-safe. + * @note The value for ABS_MT_ events is undefined, use + * libevdev_fetch_slot_value() instead + * + * @see libevdev_fetch_slot_value + */ +int libevdev_fetch_event_value(const struct libevdev *dev, unsigned int type, unsigned int code, + int *value); + +/** + * @ingroup mt + * + * Return the current value of the code for the given slot. + * + * The return value is undefined for a slot exceeding the available slots on + * the device, for a code that is not in the permitted ABS_MT range or for a + * device that does not have slots. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param slot The numerical slot number, must be smaller than the total number + * of slots on this device + * @param code The event code to query for, one of ABS_MT_POSITION_X, etc. + * + * @note This function is signal-safe. + * @note The value for events other than ABS_MT_ is undefined, use + * libevdev_fetch_value() instead + * + * @see libevdev_get_event_value + */ +int libevdev_get_slot_value(const struct libevdev *dev, unsigned int slot, unsigned int code); + +/** + * @ingroup kernel + * + * Set the value for a given code for the given slot. + * + * This is a local modification only affecting only this representation of + * this device. A future call to libevdev_get_slot_value() will return this + * value, unless the value was overwritten by an event. + * + * This function does not set event values for axes outside the ABS_MT range, + * use libevdev_set_event_value() instead. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param slot The numerical slot number, must be smaller than the total number + * of slots on this device + * @param code The event code to set the value for, one of ABS_MT_POSITION_X, etc. + * @param value The new value to set + * + * @return 0 on success, or -1 on failure. + * @retval -1 + * - the device does not have the event code enabled, or + * - the code is outside the allowed limits for multitouch events, or + * - the slot number is outside the limits for this device, or + * - the device does not support multitouch events. + * + * @see libevdev_set_event_value + * @see libevdev_get_slot_value + */ +int libevdev_set_slot_value(struct libevdev *dev, unsigned int slot, unsigned int code, int value); + +/** + * @ingroup mt + * + * Fetch the current value of the code for the given slot. This is a shortcut for + * + * @code + * if (libevdev_has_event_type(dev, EV_ABS) && + * libevdev_has_event_code(dev, EV_ABS, c) && + * slot < device->number_of_slots) + * val = libevdev_get_slot_value(dev, slot, c); + * @endcode + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param slot The numerical slot number, must be smaller than the total number + * of slots on this * device + * @param[out] value The current value of this axis returned. + * + * @param code The event code to query for, one of ABS_MT_POSITION_X, etc. + * @return If the device supports this event code, the return value is + * non-zero and value is set to the current value of this axis. Otherwise, or + * if the event code is not an ABS_MT_* event code, 0 is returned and value + * is unmodified. + * + * @note This function is signal-safe. + */ +int libevdev_fetch_slot_value(const struct libevdev *dev, unsigned int slot, unsigned int code, + int *value); + +/** + * @ingroup mt + * + * Get the number of slots supported by this device. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * + * @return The number of slots supported, or -1 if the device does not provide + * any slots + * + * @note A device may provide ABS_MT_SLOT but a total number of 0 slots. Hence + * the return value of -1 for "device does not provide slots at all" + */ +int libevdev_get_num_slots(const struct libevdev *dev); + +/** + * @ingroup mt + * + * Get the currently active slot. This may differ from the value + * an ioctl may return at this time as events may have been read off the fd + * since changing the slot value but those events are still in the buffer + * waiting to be processed. The returned value is the value a caller would + * see if it were to process events manually one-by-one. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * + * @return the currently active slot (logically) + * + * @note This function is signal-safe. + */ +int libevdev_get_current_slot(const struct libevdev *dev); + +/** + * @ingroup kernel + * + * Change the minimum for the given EV_ABS event code, if the code exists. + * This function has no effect if libevdev_has_event_code() returns false for + * this code. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param code One of ABS_X, ABS_Y, ... + * @param val The new minimum for this axis + */ +void libevdev_set_abs_minimum(struct libevdev *dev, unsigned int code, int val); + +/** + * @ingroup kernel + * + * Change the maximum for the given EV_ABS event code, if the code exists. + * This function has no effect if libevdev_has_event_code() returns false for + * this code. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param code One of ABS_X, ABS_Y, ... + * @param val The new maxium for this axis + */ +void libevdev_set_abs_maximum(struct libevdev *dev, unsigned int code, int val); + +/** + * @ingroup kernel + * + * Change the fuzz for the given EV_ABS event code, if the code exists. + * This function has no effect if libevdev_has_event_code() returns false for + * this code. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param code One of ABS_X, ABS_Y, ... + * @param val The new fuzz for this axis + */ +void libevdev_set_abs_fuzz(struct libevdev *dev, unsigned int code, int val); + +/** + * @ingroup kernel + * + * Change the flat for the given EV_ABS event code, if the code exists. + * This function has no effect if libevdev_has_event_code() returns false for + * this code. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param code One of ABS_X, ABS_Y, ... + * @param val The new flat for this axis + */ +void libevdev_set_abs_flat(struct libevdev *dev, unsigned int code, int val); + +/** + * @ingroup kernel + * + * Change the resolution for the given EV_ABS event code, if the code exists. + * This function has no effect if libevdev_has_event_code() returns false for + * this code. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param code One of ABS_X, ABS_Y, ... + * @param val The new axis resolution + */ +void libevdev_set_abs_resolution(struct libevdev *dev, unsigned int code, int val); + +/** + * @ingroup kernel + * + * Change the abs info for the given EV_ABS event code, if the code exists. + * This function has no effect if libevdev_has_event_code() returns false for + * this code. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param code One of ABS_X, ABS_Y, ... + * @param abs The new absolute axis data (min, max, fuzz, flat, resolution) + */ +void +libevdev_set_abs_info(struct libevdev *dev, unsigned int code, const struct input_absinfo *abs); + +/** + * @ingroup kernel + * + * Forcibly enable an event type on this device, even if the underlying + * device does not support it. While this cannot make the device actually + * report such events, it will now return true for libevdev_has_event_type(). + * + * This is a local modification only affecting only this representation of + * this device. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param type The event type to enable (EV_ABS, EV_KEY, ...) + * + * @return 0 on success or -1 otherwise + * + * @see libevdev_has_event_type + */ +int libevdev_enable_event_type(struct libevdev *dev, unsigned int type); + +/** + * @ingroup kernel + * + * Forcibly disable an event type on this device, even if the underlying + * device provides it. This effectively mutes the respective set of + * events. libevdev will filter any events matching this type and none will + * reach the caller. libevdev_has_event_type() will return false for this + * type. + * + * In most cases, a caller likely only wants to disable a single code, not + * the whole type. Use libevdev_disable_event_code() for that. + * + * Disabling EV_SYN will not work. Don't shoot yourself in the foot. + * It hurts. + * + * This is a local modification only affecting only this representation of + * this device. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param type The event type to disable (EV_ABS, EV_KEY, ...) + * + * @return 0 on success or -1 otherwise + * + * @see libevdev_has_event_type + * @see libevdev_disable_event_type + */ +int libevdev_disable_event_type(struct libevdev *dev, unsigned int type); + +/** + * @ingroup kernel + * + * Forcibly enable an event code on this device, even if the underlying + * device does not support it. While this cannot make the device actually + * report such events, it will now return true for libevdev_has_event_code(). + * + * The last argument depends on the type and code: + * - If type is EV_ABS, data must be a pointer to a struct input_absinfo + * containing the data for this axis. + * - If type is EV_REP, data must be a pointer to a int containing the data + * for this axis + * - For all other types, the argument must be NULL. + * + * This function calls libevdev_enable_event_type() if necessary. + * + * This is a local modification only affecting only this representation of + * this device. + * + * If this function is called with a type of EV_ABS and EV_REP on a device + * that already has the given event code enabled, the values in data + * overwrite the previous values. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param type The event type to enable (EV_ABS, EV_KEY, ...) + * @param code The event code to enable (ABS_X, REL_X, etc.) + * @param data If type is EV_ABS, data points to a struct input_absinfo. If type is EV_REP, data + * points to an integer. Otherwise, data must be NULL. + * + * @return 0 on success or -1 otherwise + * + * @see libevdev_enable_event_type + */ +int libevdev_enable_event_code(struct libevdev *dev, unsigned int type, unsigned int code, + const void *data); + +/** + * @ingroup kernel + * + * Forcibly disable an event code on this device, even if the underlying + * device provides it. This effectively mutes the respective set of + * events. libevdev will filter any events matching this type and code and + * none will reach the caller. libevdev_has_event_code() will return false for + * this code. + * + * Disabling all event codes for a given type will not disable the event + * type. Use libevdev_disable_event_type() for that. + * + * This is a local modification only affecting only this representation of + * this device. + * + * Disabling codes of type EV_SYN will not work. Don't shoot yourself in the + * foot. It hurts. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param type The event type to disable (EV_ABS, EV_KEY, ...) + * @param code The event code to disable (ABS_X, REL_X, etc.) + * + * @return 0 on success or -1 otherwise + * + * @see libevdev_has_event_code + * @see libevdev_disable_event_type + */ +int libevdev_disable_event_code(struct libevdev *dev, unsigned int type, unsigned int code); + +/** + * @ingroup kernel + * + * Set the device's EV_ABS axis to the value defined in the abs + * parameter. This will be written to the kernel. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param code The EV_ABS event code to modify, one of ABS_X, ABS_Y, etc. + * @param abs Axis info to set the kernel axis to + * + * @return 0 on success, or a negative errno on failure + * + * @see libevdev_enable_event_code + */ +int libevdev_kernel_set_abs_info(struct libevdev *dev, unsigned int code, + const struct input_absinfo *abs); + +/** + * @ingroup kernel + */ +enum libevdev_led_value { + LIBEVDEV_LED_ON = 3, /**< Turn the LED on */ + LIBEVDEV_LED_OFF = 4 /**< Turn the LED off */ +}; + +/** + * @ingroup kernel + * + * Turn an LED on or off. Convenience function, if you need to modify multiple + * LEDs simultaneously, use libevdev_kernel_set_led_values() instead. + * + * @note enabling an LED requires write permissions on the device's file descriptor. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param code The EV_LED event code to modify, one of LED_NUML, LED_CAPSL, ... + * @param value Specifies whether to turn the LED on or off + * @return 0 on success, or a negative errno on failure + */ +int libevdev_kernel_set_led_value(struct libevdev *dev, unsigned int code, + enum libevdev_led_value value); + +/** + * @ingroup kernel + * + * Turn multiple LEDs on or off simultaneously. This function expects a pair + * of LED codes and values to set them to, terminated by a -1. For example, to + * switch the NumLock LED on but the CapsLock LED off, use: + * + * @code + * libevdev_kernel_set_led_values(dev, LED_NUML, LIBEVDEV_LED_ON, + * LED_CAPSL, LIBEVDEV_LED_OFF, + * -1); + * @endcode + * + * If any LED code or value is invalid, this function returns -EINVAL and no + * LEDs are modified. + * + * @note enabling an LED requires write permissions on the device's file descriptor. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param ... A pair of LED_* event codes and libevdev_led_value_t, followed by + * -1 to terminate the list. + * @return 0 on success, or a negative errno on failure + */ +int libevdev_kernel_set_led_values(struct libevdev *dev, ...); + +/** + * @ingroup kernel + * + * Set the clock ID to be used for timestamps. Further events from this device + * will report an event time based on the given clock. + * + * This is a modification only affecting this representation of + * this device. + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param clockid The clock to use for future events. Permitted values + * are CLOCK_MONOTONIC and CLOCK_REALTIME (the default). + * @return 0 on success, or a negative errno on failure + */ +int libevdev_set_clock_id(struct libevdev *dev, int clockid); + +/** + * @ingroup misc + * + * Helper function to check if an event is of a specific type. This is + * virtually the same as: + * + * ev->type == type + * + * with the exception that some sanity checks are performed to ensure type + * is valid. + * + * @note The ranges for types are compiled into libevdev. If the kernel + * changes the max value, libevdev will not automatically pick these up. + * + * @param ev The input event to check + * @param type Input event type to compare the event against (EV_REL, EV_ABS, + * etc.) + * + * @return 1 if the event type matches the given type, 0 otherwise (or if + * type is invalid) + */ +int libevdev_event_is_type(const struct input_event *ev, unsigned int type); + +/** + * @ingroup misc + * + * Helper function to check if an event is of a specific type and code. This + * is virtually the same as: + * + * ev->type == type && ev->code == code + * + * with the exception that some sanity checks are performed to ensure type and + * code are valid. + * + * @note The ranges for types and codes are compiled into libevdev. If the kernel + * changes the max value, libevdev will not automatically pick these up. + * + * @param ev The input event to check + * @param type Input event type to compare the event against (EV_REL, EV_ABS, + * etc.) + * @param code Input event code to compare the event against (ABS_X, REL_X, + * etc.) + * + * @return 1 if the event type matches the given type and code, 0 otherwise + * (or if type/code are invalid) + */ +int libevdev_event_is_code(const struct input_event *ev, unsigned int type, unsigned int code); + +/** + * @ingroup misc + * + * @param type The event type to return the name for. + * + * @return The name of the given event type (e.g. EV_ABS) or NULL for an + * invalid type + * + * @note The list of names is compiled into libevdev. If the kernel adds new + * defines for new event types, libevdev will not automatically pick these up. + */ +const char *libevdev_event_type_get_name(unsigned int type); + +/** + * @ingroup misc + * + * @param type The event type for the code to query (EV_SYN, EV_REL, etc.) + * @param code The event code to return the name for (e.g. ABS_X) + * + * @return The name of the given event code (e.g. ABS_X) or NULL for an + * invalid type or code + * + * @note The list of names is compiled into libevdev. If the kernel adds new + * defines for new event codes, libevdev will not automatically pick these up. + */ +const char *libevdev_event_code_get_name(unsigned int type, unsigned int code); + +/** + * @ingroup misc + * + * This function resolves the event value for a code. + * + * For almost all event codes this will return NULL as the value is just a + * numerical value. As of kernel 4.17, the only event code that will return + * a non-NULL value is EV_ABS/ABS_MT_TOOL_TYPE. + * + * @param type The event type for the value to query (EV_ABS, etc.) + * @param code The event code for the value to query (e.g. ABS_MT_TOOL_TYPE) + * @param value The event value to return the name for (e.g. MT_TOOL_PALM) + * + * @return The name of the given event value (e.g. MT_TOOL_PALM) or NULL for + * an invalid type or code or NULL for an axis that has numerical values + * only. + * + * @note The list of names is compiled into libevdev. If the kernel adds new + * defines for new event values, libevdev will not automatically pick these up. + */ +const char *libevdev_event_value_get_name(unsigned int type, + unsigned int code, + int value); + +/** + * @ingroup misc + * + * @param prop The input prop to return the name for (e.g. INPUT_PROP_BUTTONPAD) + * + * @return The name of the given input prop (e.g. INPUT_PROP_BUTTONPAD) or NULL for an + * invalid property + * + * @note The list of names is compiled into libevdev. If the kernel adds new + * defines for new properties libevdev will not automatically pick these up. + * @note On older kernels input properties may not be defined and + * libevdev_property_get_name() will always return NULL + */ +const char *libevdev_property_get_name(unsigned int prop); + +/** + * @ingroup misc + * + * @param type The event type to return the maximum for (EV_ABS, EV_REL, etc.). No max is defined for + * EV_SYN. + * + * @return The max value defined for the given event type, e.g. ABS_MAX for a type of EV_ABS, or -1 + * for an invalid type. + * + * @note The max value is compiled into libevdev. If the kernel changes the + * max value, libevdev will not automatically pick these up. + */ +int libevdev_event_type_get_max(unsigned int type); + +/** + * @ingroup misc + * + * Look up an event-type by its name. Event-types start with "EV_" followed by + * the name (eg., "EV_ABS"). The "EV_" prefix must be included in the name. It + * returns the constant assigned to the event-type or -1 if not found. + * + * @param name A non-NULL string describing an input-event type ("EV_KEY", + * "EV_ABS", ...), zero-terminated. + * + * @return The given type constant for the passed name or -1 if not found. + * + * @note EV_MAX is also recognized. + */ +int libevdev_event_type_from_name(const char *name); + +/** + * @ingroup misc + * + * Look up an event-type by its name. Event-types start with "EV_" followed by + * the name (eg., "EV_ABS"). The "EV_" prefix must be included in the name. It + * returns the constant assigned to the event-type or -1 if not found. + * + * @param name A non-NULL string describing an input-event type ("EV_KEY", + * "EV_ABS", ...). + * @param len The length of the passed string excluding any terminating 0 + * character. + * + * @return The given type constant for the passed name or -1 if not found. + * + * @note EV_MAX is also recognized. + */ +int libevdev_event_type_from_name_n(const char *name, size_t len); + +/** + * @ingroup misc + * + * Look up an event code by its type and name. Event codes start with a fixed + * prefix followed by their name (eg., "ABS_X"). The prefix must be included in + * the name. It returns the constant assigned to the event code or -1 if not + * found. + * + * You have to pass the event type where to look for the name. For instance, to + * resolve "ABS_X" you need to pass EV_ABS as type and "ABS_X" as string. + * Supported event codes are codes starting with SYN_, KEY_, BTN_, REL_, ABS_, + * MSC_, SND_, SW_, LED_, REP_, FF_. + * + * @param type The event type (EV_* constant) where to look for the name. + * @param name A non-NULL string describing an input-event code ("KEY_A", + * "ABS_X", "BTN_Y", ...), zero-terminated. + * + * @return The given code constant for the passed name or -1 if not found. + */ +int libevdev_event_code_from_name(unsigned int type, const char *name); + +/** + * @ingroup misc + * + * Look up an event code by its type and name. Event codes start with a fixed + * prefix followed by their name (eg., "ABS_X"). The prefix must be included in + * the name. It returns the constant assigned to the event code or -1 if not + * found. + * + * You have to pass the event type where to look for the name. For instance, to + * resolve "ABS_X" you need to pass EV_ABS as type and "ABS_X" as string. + * Supported event codes are codes starting with SYN_, KEY_, BTN_, REL_, ABS_, + * MSC_, SND_, SW_, LED_, REP_, FF_. + * + * @param type The event type (EV_* constant) where to look for the name. + * @param name A non-NULL string describing an input-event code ("KEY_A", + * "ABS_X", "BTN_Y", ...). + * @param len The length of the string in @p name excluding any terminating 0 + * character. + * + * @return The given code constant for the name or -1 if not found. + */ +int libevdev_event_code_from_name_n(unsigned int type, const char *name, + size_t len); + +/** + * @ingroup misc + * + * Look up an event value by its type, code and name. Event values start + * with a fixed prefix followed by their name (eg., "MT_TOOL_PALM"). The + * prefix must be included in the name. It returns the constant assigned + * to the event code or -1 if not found. + * + * You have to pass the event type and code where to look for the name. For + * instance, to resolve "MT_TOOL_PALM" you need to pass EV_ABS as type, + * ABS_MT_TOOL_TYPE as code and "MT_TOOL_PALM" as string. + * + * As of kernel 4.17, only EV_ABS/ABS_MT_TOOL_TYPE support name resolution. + * + * @param type The event type (EV_* constant) where to look for the name. + * @param code The event code (ABS_* constant) where to look for the name. + * @param name A non-NULL string describing an input-event value + * ("MT_TOOL_TYPE", ...) + * + * @return The given value constant for the name or -1 if not found. + */ +int libevdev_event_value_from_name(unsigned int type, unsigned int code, + const char *name); + +/** + * @ingroup misc + * + * Look up an event type for a event code name. For example, the name + * "ABS_Y" returns EV_ABS. For the lookup to succeed, the name must be + * unique, which is the case for all defines as of kernel 5.0 and likely to + * be the case in the future. + * + * This is equivalent to libevdev_event_type_from_name() but takes the code + * name instead of the type name. + * + * @param name A non-NULL string describing an input-event value + * ("ABS_X", "REL_Y", "KEY_A", ...) + * + * @return The given event code for the name or -1 if not found. + */ +int +libevdev_event_type_from_code_name(const char *name); + +/** + * @ingroup misc + * + * Look up an event type for a event code name. For example, the name + * "ABS_Y" returns EV_ABS. For the lookup to succeed, the name must be + * unique, which is the case for all defines as of kernel 5.0 and likely to + * be the case in the future. + * + * This is equivalent to libevdev_event_type_from_name_n() but takes the code + * name instead of the type name. + * + * @param name A non-NULL string describing an input-event value + * ("ABS_X", "REL_Y", "KEY_A", ...) + * @param len The length of the passed string excluding any terminating 0 + * character. + * + * @return The given event code for the name or -1 if not found. + */ +int +libevdev_event_type_from_code_name_n(const char *name, size_t len); + +/** + * @ingroup misc + * + * Look up an event code by its name. For example, the name "ABS_Y" + * returns 1. For the lookup to succeed, the name must be unique, which is + * the case for all defines as of kernel 5.0 and likely to be the case in + * the future. + * + * This is equivalent to libevdev_event_code_from_name() without the need + * for knowing the event type. + * + * @param name A non-NULL string describing an input-event value + * ("ABS_X", "REL_Y", "KEY_A", ...) + * + * @return The given event code for the name or -1 if not found. + */ +int +libevdev_event_code_from_code_name(const char *name); + +/** + * @ingroup misc + * + * Look up an event code by its name. For example, the name "ABS_Y" + * returns 1. For the lookup to succeed, the name must be unique, which is + * the case for all defines as of kernel 5.0 and likely to be the case in + * the future. + * + * This is equivalent to libevdev_event_code_from_name_n() without the need + * for knowing the event type. + * + * @param name A non-NULL string describing an input-event value + * ("ABS_X", "REL_Y", "KEY_A", ...) + * @param len The length of the passed string excluding any terminating 0 + * character. + * + * @return The given event code for the name or -1 if not found. + */ +int +libevdev_event_code_from_code_name_n(const char *name, size_t len); + +/** + * @ingroup misc + * + * Look up an event value by its type, code and name. Event values start + * with a fixed prefix followed by their name (eg., "MT_TOOL_PALM"). The + * prefix must be included in the name. It returns the constant assigned + * to the event code or -1 if not found. + * + * You have to pass the event type and code where to look for the name. For + * instance, to resolve "MT_TOOL_PALM" you need to pass EV_ABS as type, + * ABS_MT_TOOL_TYPE as code and "MT_TOOL_PALM" as string. + * + * As of kernel 4.17, only EV_ABS/ABS_MT_TOOL_TYPE support name resolution. + * + * @param type The event type (EV_* constant) where to look for the name. + * @param code The event code (ABS_* constant) where to look for the name. + * @param name A non-NULL string describing an input-event value + * ("MT_TOOL_TYPE", ...) + * @param len The length of the string in @p name excluding any terminating 0 + * character. + * + * @return The given value constant for the name or -1 if not found. + */ +int libevdev_event_value_from_name_n(unsigned int type, unsigned int code, + const char *name, size_t len); + +/** + * @ingroup misc + * + * Look up an input property by its name. Properties start with the fixed + * prefix "INPUT_PROP_" followed by their name (eg., "INPUT_PROP_POINTER"). + * The prefix must be included in the name. It returns the constant assigned + * to the property or -1 if not found. + * + * @param name A non-NULL string describing an input property + * + * @return The given code constant for the name or -1 if not found. + */ +int libevdev_property_from_name(const char *name); + +/** + * @ingroup misc + * + * Look up an input property by its name. Properties start with the fixed + * prefix "INPUT_PROP_" followed by their name (eg., "INPUT_PROP_POINTER"). + * The prefix must be included in the name. It returns the constant assigned + * to the property or -1 if not found. + * + * @param name A non-NULL string describing an input property + * @param len The length of the string in @p name excluding any terminating 0 + * character. + * + * @return The given code constant for the name or -1 if not found. + */ +int libevdev_property_from_name_n(const char *name, size_t len); + +/** + * @ingroup bits + * + * Get the repeat delay and repeat period values for this device. This + * function is a convenience function only, EV_REP is supported by + * libevdev_get_event_value(). + * + * @param dev The evdev device, already initialized with libevdev_set_fd() + * @param delay If not null, set to the repeat delay value + * @param period If not null, set to the repeat period value + * + * @return 0 on success, -1 if this device does not have repeat settings. + * + * @note This function is signal-safe + * + * @see libevdev_get_event_value + */ +int libevdev_get_repeat(const struct libevdev *dev, int *delay, int *period); + +/********* DEPRECATED SECTION *********/ +#if defined(__GNUC__) && __GNUC__ >= 4 +#define LIBEVDEV_DEPRECATED __attribute__ ((deprecated)) +#else +#define LIBEVDEV_DEPRECATED +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* LIBEVDEV_H */ diff --git a/sysbridge/src/main/cpp/libevdev/libevdev.sym b/sysbridge/src/main/cpp/libevdev/libevdev.sym new file mode 100644 index 0000000000..4161962dc8 --- /dev/null +++ b/sysbridge/src/main/cpp/libevdev/libevdev.sym @@ -0,0 +1,123 @@ +/* SPDX-License-Identifier: MIT */ +/* + * Copyright (c) 2013 David Herrmann + */ + +LIBEVDEV_1 { +global: + libevdev_change_fd; + libevdev_disable_event_code; + libevdev_disable_event_type; + libevdev_enable_event_code; + libevdev_enable_event_type; + libevdev_enable_property; + libevdev_event_code_from_name; + libevdev_event_code_from_name_n; + libevdev_event_code_get_name; + libevdev_event_is_code; + libevdev_event_is_type; + libevdev_event_type_from_name; + libevdev_event_type_from_name_n; + libevdev_event_type_get_max; + libevdev_event_type_get_name; + libevdev_fetch_event_value; + libevdev_fetch_slot_value; + libevdev_free; + libevdev_get_abs_flat; + libevdev_get_abs_fuzz; + libevdev_get_abs_info; + libevdev_get_abs_maximum; + libevdev_get_abs_minimum; + libevdev_get_abs_resolution; + libevdev_get_current_slot; + libevdev_get_driver_version; + libevdev_get_event_value; + libevdev_get_fd; + libevdev_get_id_bustype; + libevdev_get_id_product; + libevdev_get_id_vendor; + libevdev_get_id_version; + libevdev_get_log_priority; + libevdev_get_name; + libevdev_get_num_slots; + libevdev_get_phys; + libevdev_get_repeat; + libevdev_get_slot_value; + libevdev_get_uniq; + libevdev_grab; + libevdev_has_event_code; + libevdev_has_event_pending; + libevdev_has_event_type; + libevdev_has_property; + libevdev_kernel_set_abs_info; + libevdev_kernel_set_led_value; + libevdev_kernel_set_led_values; + libevdev_new; + libevdev_new_from_fd; + libevdev_next_event; + libevdev_property_get_name; + libevdev_set_abs_flat; + libevdev_set_abs_fuzz; + libevdev_set_abs_info; + libevdev_set_abs_maximum; + libevdev_set_abs_minimum; + libevdev_set_abs_resolution; + libevdev_set_clock_id; + libevdev_set_event_value; + libevdev_set_fd; + libevdev_set_id_bustype; + libevdev_set_id_product; + libevdev_set_id_vendor; + libevdev_set_id_version; + libevdev_set_log_function; + libevdev_set_log_priority; + libevdev_set_name; + libevdev_set_phys; + libevdev_set_slot_value; + libevdev_set_uniq; + libevdev_uinput_create_from_device; + libevdev_uinput_destroy; + libevdev_uinput_get_devnode; + libevdev_uinput_get_fd; + libevdev_uinput_get_syspath; + libevdev_uinput_write_event; + +local: + *; +}; + +LIBEVDEV_1_3 { +global: + libevdev_set_device_log_function; + libevdev_property_from_name; + libevdev_property_from_name_n; + +local: + *; +} LIBEVDEV_1; + +LIBEVDEV_1_6 { +global: + libevdev_event_value_get_name; + libevdev_event_value_from_name; + libevdev_event_value_from_name_n; +local: + *; +} LIBEVDEV_1_3; + +LIBEVDEV_1_7 { +global: + libevdev_event_code_from_code_name; + libevdev_event_code_from_code_name_n; + libevdev_event_type_from_code_name; + libevdev_event_type_from_code_name_n; +local: + *; +} LIBEVDEV_1_6; + +LIBEVDEV_1_10 { +global: + libevdev_disable_property; +local: + *; +} LIBEVDEV_1_7; diff --git a/sysbridge/src/main/cpp/libevdev/make-event-names.py b/sysbridge/src/main/cpp/libevdev/make-event-names.py new file mode 100755 index 0000000000..743b4b58b1 --- /dev/null +++ b/sysbridge/src/main/cpp/libevdev/make-event-names.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +# +# Parses linux/input.h scanning for #define KEY_FOO 134 +# Prints C header files or Python files that can be used as +# mapping and lookup tables. +# + +import re +import sys + + +class Bits(object): + def __init__(self): + self.max_codes = {} + + +prefixes = [ + "EV_", + "REL_", + "ABS_", + "KEY_", + "BTN_", + "LED_", + "SND_", + "MSC_", + "SW_", + "FF_", + "SYN_", + "REP_", + "INPUT_PROP_", + "MT_TOOL_", +] + +duplicates = [ + "EV_VERSION", + "BTN_MISC", + "BTN_MOUSE", + "BTN_JOYSTICK", + "BTN_GAMEPAD", + "BTN_DIGI", + "BTN_WHEEL", + "BTN_TRIGGER_HAPPY", + "SW_MAX", + "REP_MAX", +] + +btn_additional = [ + [0, "BTN_A"], + [0, "BTN_B"], + [0, "BTN_X"], + [0, "BTN_Y"], +] + +code_prefixes = [ + "REL_", + "ABS_", + "KEY_", + "BTN_", + "LED_", + "SND_", + "MSC_", + "SW_", + "FF_", + "SYN_", + "REP_", +] + + +def print_bits(bits, prefix): + if not hasattr(bits, prefix): + return + print("static const char * const %s_map[%s_MAX + 1] = {" % (prefix, prefix.upper())) + for val, name in sorted(list(getattr(bits, prefix).items())): + print(" [%s] = \"%s\"," % (name, name)) + if prefix == "key": + for val, name in sorted(list(getattr(bits, "btn").items())): + print(" [%s] = \"%s\"," % (name, name)) + print("};") + print("") + + +def print_map(bits): + print("static const char * const * const event_type_map[EV_MAX + 1] = {") + + for prefix in prefixes: + if prefix in ["BTN_", "EV_", "INPUT_PROP_", "MT_TOOL_"]: + continue + print(" [EV_%s] = %s_map," % (prefix[:-1], prefix[:-1].lower())) + + print("};") + print("") + + print("#if __clang__") + print("#pragma clang diagnostic push") + print("#pragma clang diagnostic ignored \"-Winitializer-overrides\"") + print("#elif __GNUC__") + print("#pragma GCC diagnostic push") + print("#pragma GCC diagnostic ignored \"-Woverride-init\"") + print("#endif") + print("static const int ev_max[EV_MAX + 1] = {") + for val in range(bits.max_codes["EV_MAX"] + 1): + if val in bits.ev: + prefix = bits.ev[val][3:] + if prefix + "_" in prefixes: + print(" %s_MAX," % prefix) + continue + print(" -1,") + print("};") + print("#if __clang__") + print("#pragma clang diagnostic pop /* \"-Winitializer-overrides\" */") + print("#elif __GNUC__") + print("#pragma GCC diagnostic pop /* \"-Woverride-init\" */") + print("#endif") + print("") + + +def print_lookup(bits, prefix): + if not hasattr(bits, prefix): + return + + names = sorted(list(getattr(bits, prefix).items())) + if prefix == "btn": + names = names + btn_additional + + # We need to manually add the _MAX codes because some are + # duplicates + maxname = "%s_MAX" % (prefix.upper()) + if maxname in duplicates: + names.append((bits.max_codes[maxname], maxname)) + + for val, name in sorted(names, key=lambda e: e[1]): + print(" { .name = \"%s\", .value = %s }," % (name, name)) + + +def print_lookup_table(bits): + print("struct name_entry {") + print(" const char *name;") + print(" unsigned int value;") + print("};") + print("") + print("static const struct name_entry tool_type_names[] = {") + print_lookup(bits, "mt_tool") + print("};") + print("") + print("static const struct name_entry ev_names[] = {") + print_lookup(bits, "ev") + print("};") + print("") + + print("static const struct name_entry code_names[] = {") + for prefix in sorted(code_prefixes, key=lambda e: e): + print_lookup(bits, prefix[:-1].lower()) + print("};") + print("") + print("static const struct name_entry prop_names[] = {") + print_lookup(bits, "input_prop") + print("};") + print("") + + +def print_mapping_table(bits): + print("/* THIS FILE IS GENERATED, DO NOT EDIT */") + print("") + print("#ifndef EVENT_NAMES_H") + print("#define EVENT_NAMES_H") + print("") + + for prefix in prefixes: + if prefix == "BTN_": + continue + print_bits(bits, prefix[:-1].lower()) + + print_map(bits) + print_lookup_table(bits) + + print("#endif /* EVENT_NAMES_H */") + + +def parse_define(bits, line): + m = re.match(r"^#define\s+(\w+)\s+(\w+)", line) + if m is None: + return + + name = m.group(1) + + try: + value = int(m.group(2), 0) + except ValueError: + return + + for prefix in prefixes: + if not name.startswith(prefix): + continue + + if name.endswith("_MAX"): + bits.max_codes[name] = value + + if name in duplicates: + return + + attrname = prefix[:-1].lower() + + if not hasattr(bits, attrname): + setattr(bits, attrname, {}) + b = getattr(bits, attrname) + b[value] = name + + +def parse(lines): + bits = Bits() + for line in lines: + if not line.startswith("#define"): + continue + parse_define(bits, line) + + return bits + + +def usage(prog): + print("Usage: %s ".format(prog)) + + +if __name__ == "__main__": + if len(sys.argv) <= 1: + usage(sys.argv[0]) + sys.exit(2) + + from itertools import chain + lines = chain(*[open(f).readlines() for f in sys.argv[1:]]) + bits = parse(lines) + print_mapping_table(bits) diff --git a/sysbridge/src/main/cpp/libevdev_jni.cpp b/sysbridge/src/main/cpp/libevdev_jni.cpp new file mode 100644 index 0000000000..e51847aa9d --- /dev/null +++ b/sysbridge/src/main/cpp/libevdev_jni.cpp @@ -0,0 +1,706 @@ +#include +#include +#include +#include +#include +#include +#include "libevdev/libevdev.h" +#include "libevdev/libevdev-uinput.h" +#include "logging.h" +#include "android/input/KeyLayoutMap.h" +#include "android/libbase/result.h" +#include "android/input/InputDevice.h" +#include "aidl/io/github/sds100/keymapper/sysbridge/IEvdevCallback.h" +#include +#include +#include +#include +#include +#include +#include + +using aidl::io::github::sds100::keymapper::sysbridge::IEvdevCallback; + +enum CommandType { + STOP +}; + +struct Command { + CommandType type; +}; + +struct DeviceContext { + struct libevdev *evdev; + struct libevdev_uinput *uinputDev; + struct android::KeyLayoutMap keyLayoutMap; + char devicePath[256]; + int fd; +}; + +static int epollFd = -1; +static std::mutex epollMutex; + +static int commandEventFd = -1; +static std::queue commandQueue; +static std::mutex commandMutex; + +// This maps the file descriptor of an evdev device to its context. +static std::map *evdevDevices = new std::map(); +static std::mutex evdevDevicesMutex; +static std::map *fdToDevicePath = new std::map(); + +#define DEBUG_PROBE false + +jint JNI_OnLoad(JavaVM *vm, void *reserved) { + evdevDevices = new std::map(); + return JNI_VERSION_1_6; +} + +extern "C" +JNIEXPORT jboolean JNICALL +Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_grabEvdevDeviceNative(JNIEnv *env, + jobject thiz, + jstring jDevicePath) { + + const char *devicePath = env->GetStringUTFChars(jDevicePath, nullptr); + if (devicePath == nullptr) { + return false; + } + + bool result = false; + + { + std::lock_guard epollLock(epollMutex); + if (epollFd == -1) { + LOGE("Epoll is not initialized. Cannot grab evdev device."); + return false; + } + + // Lock to prevent concurrent grab/ungrab operations on the same device + std::lock_guard lock(evdevDevicesMutex); + + // Check if device is already grabbed + if (evdevDevices->contains(devicePath)) { + LOGW("Device %s is already grabbed. Maybe it is a virtual uinput device.", + devicePath); + env->ReleaseStringUTFChars(jDevicePath, devicePath); + return false; + } + + // Perform synchronous grab operation + struct libevdev *dev = nullptr; + + // MUST be NONBLOCK so that the loop reading the evdev events eventually returns + // due to an EAGAIN error. + int fd = open(devicePath, O_RDONLY | O_NONBLOCK); + if (fd == -1) { + LOGE("Failed to open device %s: %s", devicePath, strerror(errno)); + env->ReleaseStringUTFChars(jDevicePath, devicePath); + return false; + } + + int rc = libevdev_new_from_fd(fd, &dev); + if (rc != 0) { + LOGE("Failed to create libevdev device from %s: %s", devicePath, strerror(errno)); + close(fd); + env->ReleaseStringUTFChars(jDevicePath, devicePath); + return false; + } + + rc = libevdev_grab(dev, LIBEVDEV_GRAB); + if (rc < 0) { + LOGE("Failed to grab evdev device %s: %s", + libevdev_get_name(dev), strerror(-rc)); + libevdev_free(dev); + close(fd); + env->ReleaseStringUTFChars(jDevicePath, devicePath); + return false; + } + + // Create a dummy InputDeviceIdentifier for key layout loading + android::InputDeviceIdentifier identifier; + identifier.name = std::string(libevdev_get_name(dev)); + identifier.bus = libevdev_get_id_bustype(dev); + identifier.vendor = libevdev_get_id_vendor(dev); + identifier.product = libevdev_get_id_product(dev); + + std::string klPath = android::getInputDeviceConfigurationFilePathByDeviceIdentifier( + identifier, android::InputDeviceConfigurationFileType::KEY_LAYOUT); + + auto klResult = android::KeyLayoutMap::load(klPath, nullptr); + + if (!klResult.ok()) { + LOGE("key layout map not found for device %s", libevdev_get_name(dev)); + libevdev_grab(dev, LIBEVDEV_UNGRAB); + libevdev_free(dev); + close(fd); + env->ReleaseStringUTFChars(jDevicePath, devicePath); + return false; + } + + struct libevdev_uinput *uinputDev = nullptr; + int uinputFd = open("/dev/uinput", O_RDWR); + if (uinputFd < 0) { + LOGE("Failed to open /dev/uinput to clone the device."); + libevdev_grab(dev, LIBEVDEV_UNGRAB); + libevdev_free(dev); + close(fd); + env->ReleaseStringUTFChars(jDevicePath, devicePath); + return false; + } + + rc = libevdev_uinput_create_from_device(dev, uinputFd, &uinputDev); + + if (rc < 0) { + LOGE("Failed to create uinput device from evdev device %s: %s", + libevdev_get_name(dev), strerror(-rc)); + close(uinputFd); + libevdev_grab(dev, LIBEVDEV_UNGRAB); + libevdev_free(dev); + close(fd); + env->ReleaseStringUTFChars(jDevicePath, devicePath); + return false; + } + + DeviceContext context = { + dev, + uinputDev, + *klResult.value(), + {}, // Initialize devicePath array + fd + }; + + strcpy(context.devicePath, devicePath); + + // Already checked at the start of the method whether epoll was running + struct epoll_event event{}; + event.events = EPOLLIN; + event.data.fd = fd; + + if (epoll_ctl(epollFd, EPOLL_CTL_ADD, fd, &event) == -1) { + LOGE("Failed to add new device to epoll: %s", strerror(errno)); + libevdev_uinput_destroy(uinputDev); + libevdev_grab(dev, LIBEVDEV_UNGRAB); + libevdev_free(dev); + close(fd); + env->ReleaseStringUTFChars(jDevicePath, devicePath); + return false; + } + + evdevDevices->insert_or_assign(devicePath, context); + fdToDevicePath->insert_or_assign(fd, devicePath); + result = true; + + LOGI("Grabbed device %s, %s", libevdev_get_name(dev), context.devicePath); + } + + env->ReleaseStringUTFChars(jDevicePath, devicePath); + return result; +} + +struct timeval powerButtonDownTime = {0, 0}; + +/** + * @return Whether the events were all handled by the callback. If the callback dies then this + * returns false. + */ +bool onEpollEvdevEvent(DeviceContext *deviceContext, IEvdevCallback *callback) { + struct input_event inputEvent{}; + + int rc = libevdev_next_event(deviceContext->evdev, LIBEVDEV_READ_FLAG_NORMAL, &inputEvent); + + if (rc < 0) { + if (rc != -EAGAIN) { + LOGE("libevdev_next_event failed with error %d: %s", rc, strerror(-rc)); + } + return rc == -EAGAIN; + } + + do { + if (rc == LIBEVDEV_READ_STATUS_SUCCESS) { // rc == 0 + int32_t outKeycode = -1; + uint32_t outFlags = -1; + + deviceContext->keyLayoutMap.mapKey(inputEvent.code, 0, &outKeycode, &outFlags); + + // 26 = KEYCODE_POWER + if (inputEvent.code == KEY_POWER || outKeycode == 26) { + if (inputEvent.value == 1) { + // Down click + powerButtonDownTime = inputEvent.time; + } else if (inputEvent.value == 0) { + // Up click + + // If held down for 10 seconds or more, kill system bridge. + if (inputEvent.time.tv_sec - powerButtonDownTime.tv_sec >= 10) { + callback->onEmergencyKillSystemBridge(); + exit(0); + } + } + } + + bool returnValue; + ndk::ScopedAStatus callbackResult = callback->onEvdevEvent(deviceContext->devicePath, + inputEvent.time.tv_sec, + inputEvent.time.tv_usec, + inputEvent.type, + inputEvent.code, + inputEvent.value, + outKeycode, + &returnValue); + + if (!callbackResult.isOk()) { + return false; + } + + if (!returnValue) { + libevdev_uinput_write_event(deviceContext->uinputDev, + inputEvent.type, + inputEvent.code, + inputEvent.value); + } + + rc = libevdev_next_event(deviceContext->evdev, LIBEVDEV_READ_FLAG_NORMAL, &inputEvent); + + } else if (rc == LIBEVDEV_READ_STATUS_SYNC) { + rc = libevdev_next_event(deviceContext->evdev, + LIBEVDEV_READ_FLAG_NORMAL | LIBEVDEV_READ_FLAG_SYNC, + &inputEvent); + } + + if (rc < 0 && rc != -EAGAIN) { + LOGE("libevdev_next_event failed with error %d: %s", rc, strerror(-rc)); + return false; + } + } while (rc != -EAGAIN); + + return true; +} + +// Set this to some upper limit. It is unlikely that Key Mapper will be polling +// more than a few evdev devices at once. +static int MAX_EPOLL_EVENTS = 100; + +extern "C" +JNIEXPORT void JNICALL +Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_startEvdevEventLoop(JNIEnv *env, + jobject thiz, + jobject jCallbackBinder) { + std::unique_lock epollLock(epollMutex); + + if (epollFd != -1 || commandEventFd != -1) { + LOGE("The evdev event loop has already started."); + return; + } + + epollFd = epoll_create1(EPOLL_CLOEXEC); + if (epollFd == -1) { + LOGE("Failed to create epoll fd: %s", strerror(errno)); + return; + } + + commandEventFd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); + if (commandEventFd == -1) { + LOGE("Failed to create command eventfd: %s", strerror(errno)); + close(epollFd); + return; + } + + struct epoll_event event{}; + event.events = EPOLLIN; + event.data.fd = commandEventFd; + + if (epoll_ctl(epollFd, EPOLL_CTL_ADD, commandEventFd, &event) == -1) { + LOGE("Failed to add command eventfd to epoll: %s", strerror(errno)); + close(epollFd); + close(commandEventFd); + epollLock.unlock(); + return; + } + + epollLock.unlock(); + + AIBinder *callbackAIBinder = AIBinder_fromJavaBinder(env, jCallbackBinder); + const ::ndk::SpAIBinder spBinder(callbackAIBinder); + std::shared_ptr callback = IEvdevCallback::fromBinder(spBinder); + + struct epoll_event events[MAX_EPOLL_EVENTS]; + bool running = true; + + LOGI("Start evdev event loop"); + + ndk::ScopedAStatus callbackResult = callback->onEvdevEventLoopStarted(); + + if (!callbackResult.isOk()) { + LOGE("Callback is dead. Not starting evdev loop."); + return; + } + + while (running) { + int n = epoll_wait(epollFd, events, MAX_EPOLL_EVENTS, -1); + + for (int i = 0; i < n; ++i) { + int fd = events[i].data.fd; + if (fd == commandEventFd) { + uint64_t val; + ssize_t s = read(commandEventFd, &val, sizeof(val)); + + if (s < 0) { + LOGE("Error reading from command event fd: %s", strerror(errno)); + } + + std::vector commandsToProcess; + { + std::lock_guard lock(commandMutex); + while (!commandQueue.empty()) { + commandsToProcess.push_back(commandQueue.front()); + commandQueue.pop(); + } + } + + // Process commands without holding the mutex + for (const auto &cmd: commandsToProcess) { + if (cmd.type == STOP) { + running = false; + break; + } + } + } else { + if ((events[i].events & (EPOLLHUP | EPOLLERR))) { + LOGI("Device disconnected, removing from epoll."); + epoll_ctl(epollFd, EPOLL_CTL_DEL, fd, nullptr); + + std::lock_guard lock(evdevDevicesMutex); + auto it = fdToDevicePath->find(fd); + if (it != fdToDevicePath->end()) { + evdevDevices->erase(it->second); + fdToDevicePath->erase(it); + } + } else { + std::lock_guard lock(evdevDevicesMutex); + auto it = fdToDevicePath->find(fd); + if (it != fdToDevicePath->end()) { + DeviceContext *dc = &evdevDevices->at(it->second); + // If handling the evdev event fails then stop the event loop + // and ungrab all the devices. + bool result = onEpollEvdevEvent(dc, callback.get()); + + if (!result) { + running = false; + break; + } + } + } + } + } + } + + // Cleanup + std::lock_guard lock(evdevDevicesMutex); + + for (auto const &[path, dc]: *evdevDevices) { + libevdev_uinput_destroy(dc.uinputDev); + libevdev_grab(dc.evdev, LIBEVDEV_UNGRAB); + libevdev_free(dc.evdev); + } + + evdevDevices->clear(); + close(commandEventFd); + commandEventFd = -1; + close(epollFd); + epollFd = -1; + + LOGI("Stopped evdev event loop"); +} + +extern "C" +JNIEXPORT jboolean JNICALL +Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_ungrabEvdevDeviceNative(JNIEnv *env, + jobject thiz, + jstring jDevicePath) { + const char *devicePath = env->GetStringUTFChars(jDevicePath, nullptr); + if (devicePath == nullptr) { + return false; + } + + bool result = false; + + { + // Lock to prevent concurrent grab/ungrab operations + std::lock_guard lock(evdevDevicesMutex); + + auto it = evdevDevices->find(devicePath); + if (it != evdevDevices->end()) { + // Remove from epoll first (if event loop is running) + if (epollFd != -1) { + if (epoll_ctl(epollFd, EPOLL_CTL_DEL, it->second.fd, nullptr) == -1) { + LOGW("Failed to remove device from epoll: %s", strerror(errno)); + // Continue with ungrab even if epoll removal fails + } + } + + // Do this before freeing the evdev file descriptor + libevdev_uinput_destroy(it->second.uinputDev); + + // Ungrab the device + libevdev_grab(it->second.evdev, LIBEVDEV_UNGRAB); + + // Free resources + libevdev_free(it->second.evdev); + + // Remove from device map + evdevDevices->erase(it); + fdToDevicePath->erase(it->second.fd); + result = true; + + LOGI("Ungrabbed device %s", devicePath); + } + + if (!result) { + LOGW("Device %s was not found in grabbed devices list", devicePath); + } + } + + env->ReleaseStringUTFChars(jDevicePath, devicePath); + return result; +} + + +extern "C" +JNIEXPORT void JNICALL +Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_stopEvdevEventLoop(JNIEnv *env, + jobject thiz) { + if (commandEventFd == -1) { + return; + } + + Command cmd = {STOP}; + + std::lock_guard lock(commandMutex); + commandQueue.push(cmd); + + // Notify the event loop + uint64_t val = 1; + ssize_t written = write(commandEventFd, &val, sizeof(val)); + if (written < 0) { + LOGE("Failed to write to commandEventFd: %s", strerror(errno)); + } +} + +extern "C" +JNIEXPORT jboolean JNICALL +Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_writeEvdevEventNative(JNIEnv *env, + jobject thiz, + jstring jDevicePath, + jint type, + jint code, + jint value) { + const char *devicePath = env->GetStringUTFChars(jDevicePath, nullptr); + if (devicePath == nullptr) { + return false; + } + + bool result = false; + { + auto it = evdevDevices->find(devicePath); + if (it != evdevDevices->end()) { + int rc = libevdev_uinput_write_event(it->second.uinputDev, type, code, value); + if (rc == 0) { + rc = libevdev_uinput_write_event(it->second.uinputDev, EV_SYN, SYN_REPORT, 0); + } + result = (rc == 0); + } + } + + env->ReleaseStringUTFChars(jDevicePath, devicePath); + return result; +} +extern "C" +JNIEXPORT jboolean JNICALL +Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_ungrabAllEvdevDevicesNative( + JNIEnv *env, + jobject thiz) { + + { + // Lock to prevent concurrent grab/ungrab operations + std::lock_guard lock(evdevDevicesMutex); + std::lock_guard epollLock(epollMutex); + + // Create a copy of the iterator to avoid issues with erasing during iteration + auto devicesCopy = *evdevDevices; + + for (const auto &pair: devicesCopy) { + const DeviceContext &context = pair.second; + + // Remove from epoll first (if event loop is running) + if (epollFd != -1) { + if (epoll_ctl(epollFd, EPOLL_CTL_DEL, context.fd, nullptr) == -1) { + LOGW("Failed to remove device %s from epoll: %s", context.devicePath, + strerror(errno)); + // Continue with ungrab even if epoll removal fails + } + } + + // Do this before freeing the evdev file descriptor + libevdev_uinput_destroy(context.uinputDev); + + // Ungrab the device + libevdev_grab(context.evdev, LIBEVDEV_UNGRAB); + + // Free resources + libevdev_free(context.evdev); + + LOGI("Ungrabbed device %s", context.devicePath); + } + + // Clear all devices from the map + evdevDevices->clear(); + fdToDevicePath->clear(); + } + + return true; +} + +// Helper function to create a Java EvdevDeviceHandle object +jobject +createEvdevDeviceHandle(JNIEnv *env, const char *path, const char *name, int bus, int vendor, + int product) { + // Find the EvdevDeviceHandle class + jclass evdevDeviceHandleClass = env->FindClass( + "io/github/sds100/keymapper/common/models/EvdevDeviceHandle"); + if (evdevDeviceHandleClass == nullptr) { + LOGE("Failed to find EvdevDeviceHandle class"); + return nullptr; + } + + // Get the constructor + jmethodID constructor = env->GetMethodID(evdevDeviceHandleClass, "", + "(Ljava/lang/String;Ljava/lang/String;III)V"); + if (constructor == nullptr) { + LOGE("Failed to find EvdevDeviceHandle constructor"); + return nullptr; + } + + // Create Java strings + jstring jPath = env->NewStringUTF(path); + jstring jName = env->NewStringUTF(name); + + // Create the object + jobject evdevDeviceHandle = env->NewObject(evdevDeviceHandleClass, constructor, jPath, jName, + bus, vendor, product); + + // Clean up local references + env->DeleteLocalRef(jPath); + env->DeleteLocalRef(jName); + env->DeleteLocalRef(evdevDeviceHandleClass); + + return evdevDeviceHandle; +} + +extern "C" +JNIEXPORT jobjectArray JNICALL +Java_io_github_sds100_keymapper_sysbridge_service_SystemBridge_getEvdevDevicesNative(JNIEnv *env, + jobject thiz) { + DIR *dir = opendir("/dev/input"); + + if (dir == nullptr) { + LOGE("Failed to open /dev/input directory"); + return nullptr; + } + + std::vector deviceHandles; + struct dirent *entry; + + std::lock_guard lock(evdevDevicesMutex); + while ((entry = readdir(dir)) != nullptr) { + // Skip . and .. entries + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { + continue; + } + + char fullPath[256]; + snprintf(fullPath, sizeof(fullPath), "/dev/input/%s", entry->d_name); + + bool ignoreDevice = false; + + { + // Ignore this device if it is a uinput device we created + for (const auto &pair: *evdevDevices) { + DeviceContext context = pair.second; + const char *uinputDevicePath = libevdev_uinput_get_devnode(context.uinputDev); + + if (strcmp(fullPath, uinputDevicePath) == 0) { + LOGW("Ignoring uinput device %s.", uinputDevicePath); + ignoreDevice = true; + break; + } + } + } + + if (ignoreDevice) { + continue; + } + + int fd = open(fullPath, O_RDONLY); + + if (fd == -1) { + continue; + } + + struct libevdev *dev = nullptr; + int status = libevdev_new_from_fd(fd, &dev); + + if (status != 0) { + LOGE("Failed to open libevdev device from path %s: %s", fullPath, strerror(errno)); + close(fd); + continue; + } + + const char *devName = libevdev_get_name(dev); + int devVendor = libevdev_get_id_vendor(dev); + int devProduct = libevdev_get_id_product(dev); + int devBus = libevdev_get_id_bustype(dev); + + if (DEBUG_PROBE) { + LOGD("Evdev device: %s, bus: %d, vendor: %d, product: %d, path: %s", + devName, devBus, devVendor, devProduct, fullPath); + } + + // Create EvdevDeviceHandle object + jobject deviceHandle = createEvdevDeviceHandle(env, fullPath, devName, devBus, devVendor, + devProduct); + if (deviceHandle != nullptr) { + deviceHandles.push_back(deviceHandle); + } + + libevdev_free(dev); + close(fd); + } + + closedir(dir); + + // Create the Java array + jclass evdevDeviceHandleClass = env->FindClass( + "io/github/sds100/keymapper/common/models/EvdevDeviceHandle"); + if (evdevDeviceHandleClass == nullptr) { + LOGE("Failed to find EvdevDeviceHandle class for array creation"); + return nullptr; + } + + jobjectArray result = env->NewObjectArray(deviceHandles.size(), evdevDeviceHandleClass, + nullptr); + if (result == nullptr) { + LOGE("Failed to create EvdevDeviceHandle array"); + env->DeleteLocalRef(evdevDeviceHandleClass); + return nullptr; + } + + // Fill the array + for (size_t i = 0; i < deviceHandles.size(); i++) { + env->SetObjectArrayElement(result, i, deviceHandles[i]); + env->DeleteLocalRef(deviceHandles[i]); // Clean up local reference + } + + env->DeleteLocalRef(evdevDeviceHandleClass); + return result; +} \ No newline at end of file diff --git a/sysbridge/src/main/cpp/logging.h b/sysbridge/src/main/cpp/logging.h new file mode 100644 index 0000000000..0bc5310dc8 --- /dev/null +++ b/sysbridge/src/main/cpp/logging.h @@ -0,0 +1,30 @@ +#ifndef _LOGGING_H +#define _LOGGING_H + +#include +#include "android/log.h" + +#ifndef LOG_TAG +#define LOG_TAG "KeyMapperSystemBridge" +#endif + +#ifndef NO_LOG +#ifndef NO_DEBUG_LOG +#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) +#else +#define LOGD(...) +#endif +#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__) +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) +#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) +#define PLOGE(fmt, args...) LOGE(fmt " failed with %d: %s", ##args, errno, strerror(errno)) +#else +#define LOGD(...) +#define LOGV(...) +#define LOGI(...) +#define LOGW(...) +#define LOGE(...) +#define PLOGE(fmt, args...) +#endif +#endif // _LOGGING_H diff --git a/sysbridge/src/main/cpp/misc.cpp b/sysbridge/src/main/cpp/misc.cpp new file mode 100644 index 0000000000..e7d2f8ccd7 --- /dev/null +++ b/sysbridge/src/main/cpp/misc.cpp @@ -0,0 +1,195 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "misc.h" + +ssize_t fdgets(char *buf, const size_t size, int fd) { + ssize_t len = 0; + buf[0] = '\0'; + while (len < size - 1) { + ssize_t ret = read(fd, buf + len, 1); + if (ret < 0) + return -1; + if (ret == 0) + break; + if (buf[len] == '\0' || buf[len++] == '\n') { + buf[len] = '\0'; + break; + } + } + buf[len] = '\0'; + buf[size - 1] = '\0'; + return len; +} + +int get_proc_name(int pid, char *name, size_t _size) { + int fd; + ssize_t __size; + + char buf[1024]; + snprintf(buf, sizeof(buf), "/proc/%d/cmdline", pid); + if (access(buf, R_OK) == -1 || (fd = open(buf, O_RDONLY)) == -1) + return 1; + if ((__size = fdgets(buf, sizeof(buf), fd)) == 0) { + snprintf(buf, sizeof(buf), "/proc/%d/comm", pid); + close(fd); + if (access(buf, R_OK) == -1 || (fd = open(buf, O_RDONLY)) == -1) + return 1; + __size = fdgets(buf, sizeof(buf), fd); + } + close(fd); + + if (__size < _size) { + strncpy(name, buf, static_cast(__size)); + name[__size] = '\0'; + } else { + strncpy(name, buf, _size); + name[_size] = '\0'; + } + + return 0; +} + +int is_num(const char *s) { + size_t len = strlen(s); + for (size_t i = 0; i < len; ++i) + if (s[i] < '0' || s[i] > '9') + return 0; + return 1; +} + +int copyfileat(int src_path_fd, const char *src_path, int dst_path_fd, const char *dst_path) { + int src_fd; + int dst_fd; + struct stat stat_buf{}; + int64_t size_remaining; + size_t count; + ssize_t result; + + if ((src_fd = openat(src_path_fd, src_path, O_RDONLY)) == -1) + return -1; + + if (fstat(src_fd, &stat_buf) == -1) + return -1; + + dst_fd = openat(dst_path_fd, dst_path, O_WRONLY | O_CREAT | O_TRUNC, stat_buf.st_mode); + if (dst_fd == -1) { + close(src_fd); + return -1; + } + + size_remaining = stat_buf.st_size; + for (;;) { + if (size_remaining > 0x7ffff000) + count = 0x7ffff000; + else + count = static_cast(size_remaining); + + result = sendfile(dst_fd, src_fd, nullptr, count); + if (result == -1) { + close(src_fd); + close(dst_fd); + unlink(dst_path); + return -1; + } + + size_remaining -= result; + if (size_remaining == 0) { + close(src_fd); + close(dst_fd); + return 0; + } + } +} + +int copyfile(const char *src_path, const char *dst_path) { + return copyfileat(0, src_path, 0, dst_path); +} + +uintptr_t memsearch(const uintptr_t start, const uintptr_t end, const void *value, size_t size) { + uintptr_t _start = start; + while (true) { + if (_start + size >= end) + return 0; + + if (memcmp((const void *) _start, value, size) == 0) + return _start; + + _start += 1; + } +} + +int switch_mnt_ns(int pid) { + char mnt[32]; + snprintf(mnt, sizeof(mnt), "/proc/%d/ns/mnt", pid); + if (access(mnt, R_OK) == -1) return -1; + + int fd = open(mnt, O_RDONLY); + if (fd < 0) return -1; + + int res = setns(fd, 0); + close(fd); + return res; +} + +void foreach_proc(foreach_proc_function *func) { + DIR *dir; + struct dirent *entry; + + if (!(dir = opendir("/proc"))) + return; + + while ((entry = readdir(dir))) { + if (entry->d_type != DT_DIR) continue; + if (!is_num(entry->d_name)) continue; + pid_t pid = atoi(entry->d_name); + func(pid); + } + + closedir(dir); +} + +char *trim(char *str) { + size_t len = 0; + char *frontp = str; + char *endp = nullptr; + + if (str == nullptr) { return nullptr; } + if (str[0] == '\0') { return str; } + + len = strlen(str); + endp = str + len; + + /* Move the front and back pointers to address the first non-whitespace + * characters from each end. + */ + while (isspace((unsigned char) *frontp)) { ++frontp; } + if (endp != frontp) { + while (isspace((unsigned char) *(--endp)) && endp != frontp) {} + } + + if (str + len - 1 != endp) + *(endp + 1) = '\0'; + else if (frontp != str && endp == frontp) + *str = '\0'; + + /* Shift the string so that it starts at str so that if it's dynamically + * allocated, we can still free it on the returned pointer. Note the reuse + * of endp to mean the front of the string buffer now. + */ + endp = str; + if (frontp != str) { + while (*frontp) { *endp++ = *frontp++; } + *endp = '\0'; + } + + return str; +} \ No newline at end of file diff --git a/sysbridge/src/main/cpp/misc.h b/sysbridge/src/main/cpp/misc.h new file mode 100644 index 0000000000..3ab3e87d4a --- /dev/null +++ b/sysbridge/src/main/cpp/misc.h @@ -0,0 +1,14 @@ +#ifndef MISC_H +#define MISC_H + +int copyfile(const char *src_path, const char *dst_path); +uintptr_t memsearch(const uintptr_t start, const uintptr_t end, const void *value, size_t size); +int switch_mnt_ns(int pid); +int get_proc_name(int pid, char *name, size_t _size); + +using foreach_proc_function = void(pid_t); +void foreach_proc(foreach_proc_function *func); + +char *trim(char *str); + +#endif // MISC_H diff --git a/sysbridge/src/main/cpp/selinux.cpp b/sysbridge/src/main/cpp/selinux.cpp new file mode 100644 index 0000000000..9b9ebc63cf --- /dev/null +++ b/sysbridge/src/main/cpp/selinux.cpp @@ -0,0 +1,105 @@ +#include +#include +#include +#include +#include +#include +#include +#include "selinux.h" + +namespace se { + + static int __getcon(char **context) { + int fd = open("/proc/self/attr/current", O_RDONLY | O_CLOEXEC); + if (fd < 0) + return fd; + + char *buf; + size_t size; + int errno_hold; + ssize_t ret; + + size = sysconf(_SC_PAGE_SIZE); + buf = (char *) malloc(size); + if (!buf) { + ret = -1; + goto out; + } + memset(buf, 0, size); + + do { + ret = read(fd, buf, size - 1); + } while (ret < 0 && errno == EINTR); + if (ret < 0) + goto out2; + + if (ret == 0) { + *context = nullptr; + goto out2; + } + + *context = strdup(buf); + if (!(*context)) { + ret = -1; + goto out2; + } + ret = 0; + out2: + free(buf); + out: + errno_hold = errno; + close(fd); + errno = errno_hold; + return 0; + } + + static int __setcon(const char *ctx) { + int fd = open("/proc/self/attr/current", O_WRONLY | O_CLOEXEC); + if (fd < 0) + return fd; + size_t len = strlen(ctx) + 1; + ssize_t rc = write(fd, ctx, len); + close(fd); + return rc != len; + } + + static int __setfilecon(const char *path, const char *ctx) { + int rc = syscall(__NR_setxattr, path, "security.selinux"/*XATTR_NAME_SELINUX*/, ctx, + strlen(ctx) + 1, 0); + if (rc) { + errno = -rc; + return -1; + } + return 0; + } + + static int __selinux_check_access(const char *scon, const char *tcon, + const char *tclass, const char *perm, void *auditdata) { + return 0; + } + + static void __freecon(char *con) { + free(con); + } + + getcon_t *getcon = __getcon; + setcon_t *setcon = __setcon; + setfilecon_t *setfilecon = __setfilecon; + selinux_check_access_t *selinux_check_access = __selinux_check_access; + freecon_t *freecon = __freecon; + + void init() { + if (access("/system/lib/libselinux.so", F_OK) != 0 && access("/system/lib64/libselinux.so", F_OK) != 0) + return; + + void *handle = dlopen("libselinux.so", RTLD_LAZY | RTLD_LOCAL); + if (handle == nullptr) + return; + + getcon = (getcon_t *) dlsym(handle, "getcon"); + setcon = (setcon_t *) dlsym(handle, "setcon"); + setfilecon = (setfilecon_t *) dlsym(handle, "setfilecon"); + selinux_check_access = (selinux_check_access_t *) dlsym(handle, "selinux_check_access"); + freecon = (freecon_t *) (dlsym(handle, "freecon")); + } +} diff --git a/sysbridge/src/main/cpp/selinux.h b/sysbridge/src/main/cpp/selinux.h new file mode 100644 index 0000000000..f5e47eb291 --- /dev/null +++ b/sysbridge/src/main/cpp/selinux.h @@ -0,0 +1,21 @@ +#ifndef SELINUX_H +#define SELINUX_H + +namespace se { + void init(); + + using getcon_t = int(char **); + using setcon_t = int(const char *); + using setfilecon_t = int(const char *, const char *); + using selinux_check_access_t = int(const char *, const char *, const char *, const char *, + void *); + using freecon_t = void(char *); + + extern getcon_t *getcon; + extern setcon_t *setcon; + extern setfilecon_t *setfilecon; + extern selinux_check_access_t *selinux_check_access; + extern freecon_t *freecon; +} + +#endif // SELINUX_H diff --git a/sysbridge/src/main/cpp/starter.cpp b/sysbridge/src/main/cpp/starter.cpp new file mode 100644 index 0000000000..5b5b0c4411 --- /dev/null +++ b/sysbridge/src/main/cpp/starter.cpp @@ -0,0 +1,324 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "android.h" +#include "misc.h" +#include "selinux.h" +#include "cgroup.h" +#include "logging.h" + +#ifdef DEBUG +#define JAVA_DEBUGGABLE +#endif + +#define perrorf(...) fprintf(stderr, __VA_ARGS__) + +#define EXIT_FATAL_SET_CLASSPATH 3 +#define EXIT_FATAL_FORK 4 +#define EXIT_FATAL_APP_PROCESS 5 +#define EXIT_FATAL_UID 6 +#define EXIT_FATAL_PM_PATH 7 +#define EXIT_FATAL_KILL 9 +#define EXIT_FATAL_BINDER_BLOCKED_BY_SELINUX 10 + +#define SERVER_NAME "keymapper_sysbridge" +#define SERVER_CLASS_PATH "io.github.sds100.keymapper.sysbridge.service.SystemBridge" + +#if defined(__arm__) +#define ABI "armeabi-v7a" +#elif defined(__i386__) +#define ABI "x86" +#elif defined(__x86_64__) +#define ABI "x86_64" +#elif defined(__aarch64__) +#define ABI "arm64-v8a" +#endif + +static void run_server(const char *apk_path, const char *lib_path, const char *main_class, + const char *process_name, const char *package_name, + const char *version_code) { + if (setenv("CLASSPATH", apk_path, true)) { + LOGE("can't set CLASSPATH\n"); + exit(EXIT_FATAL_SET_CLASSPATH); + } + +#define ARG(v) char **v = nullptr; \ + char buf_##v[PATH_MAX]; \ + size_t v_size = 0; \ + uintptr_t v_current = 0; +#define ARG_PUSH(v, arg) v_size += sizeof(char *); \ +if (v == nullptr) { \ + v = (char **) malloc(v_size); \ +} else { \ + v = (char **) realloc(v, v_size);\ +} \ +v_current = (uintptr_t) v + v_size - sizeof(char *); \ +*((char **) v_current) = arg ? strdup(arg) : nullptr; + +#define ARG_END(v) ARG_PUSH(v, nullptr) + +#define ARG_PUSH_FMT(v, fmt, ...) snprintf(buf_##v, PATH_MAX, fmt, __VA_ARGS__); \ + ARG_PUSH(v, buf_##v) + +#ifdef JAVA_DEBUGGABLE +#define ARG_PUSH_DEBUG_ONLY(v, arg) ARG_PUSH(v, arg) +#define ARG_PUSH_DEBUG_VM_PARAMS(v) \ + if (android::GetApiLevel() >= 30) { \ + ARG_PUSH(v, "-Xcompiler-option"); \ + ARG_PUSH(v, "--debuggable"); \ + ARG_PUSH(v, "-XjdwpProvider:adbconnection"); \ + ARG_PUSH(v, "-XjdwpOptions:suspend=n,server=y"); \ + } else if (android::GetApiLevel() >= 28) { \ + ARG_PUSH(v, "-Xcompiler-option"); \ + ARG_PUSH(v, "--debuggable"); \ + ARG_PUSH(v, "-XjdwpProvider:internal"); \ + ARG_PUSH(v, "-XjdwpOptions:transport=dt_android_adb,suspend=n,server=y"); \ + } else { \ + ARG_PUSH(v, "-Xcompiler-option"); \ + ARG_PUSH(v, "--debuggable"); \ + ARG_PUSH(v, "-agentlib:jdwp=transport=dt_android_adb,suspend=n,server=y"); \ + } +#else +#define ARG_PUSH_DEBUG_VM_PARAMS(v) +#define ARG_PUSH_DEBUG_ONLY(v, arg) +#endif + + ARG(argv) + ARG_PUSH(argv, "/system/bin/app_process") + ARG_PUSH_FMT(argv, "-Djava.class.path=%s", apk_path) + ARG_PUSH_FMT(argv, "-Dkeymapper_sysbridge.library.path=%s", lib_path) + ARG_PUSH_FMT(argv, "-Dkeymapper_sysbridge.package=%s", package_name) + ARG_PUSH_FMT(argv, "-Dkeymapper_sysbridge.version_code=%s", version_code) + ARG_PUSH_DEBUG_VM_PARAMS(argv) + ARG_PUSH(argv, "/system/bin") + ARG_PUSH_FMT(argv, "--nice-name=%s", process_name) + ARG_PUSH(argv, main_class) + ARG_PUSH_DEBUG_ONLY(argv, "--debug") + ARG_END(argv) + + LOGD("exec app_process"); + + if (execvp((const char *) argv[0], argv)) { + exit(EXIT_FATAL_APP_PROCESS); + } +} + +static void start_server(const char *apk_path, const char *lib_path, const char *main_class, + const char *process_name, const char *package_name, + const char *version_code) { + + if (daemon(false, false) == 0) { + LOGD("child"); + run_server(apk_path, lib_path, main_class, process_name, package_name, version_code); + } else { + perrorf("fatal: can't fork\n"); + exit(EXIT_FATAL_FORK); + } +} + +static int check_selinux(const char *s, const char *t, const char *c, const char *p) { + int res = se::selinux_check_access(s, t, c, p, nullptr); +#ifndef DEBUG + if (res != 0) { +#endif + printf("info: selinux_check_access %s %s %s %s: %d\n", s, t, c, p, res); + fflush(stdout); +#ifndef DEBUG + } +#endif + return res; +} + +static int switch_cgroup() { + int s_cuid, s_cpid; + int spid = getpid(); + + if (cgroup::get_cgroup(spid, &s_cuid, &s_cpid) != 0) { + printf("warn: can't read cgroup\n"); + fflush(stdout); + return -1; + } + + printf("info: cgroup is /uid_%d/pid_%d\n", s_cuid, s_cpid); + fflush(stdout); + + if (cgroup::switch_cgroup(spid, -1, -1) != 0) { + printf("warn: can't switch cgroup\n"); + fflush(stdout); + return -1; + } + + if (cgroup::get_cgroup(spid, &s_cuid, &s_cpid) != 0) { + printf("info: switch cgroup succeeded\n"); + fflush(stdout); + return 0; + } + + printf("warn: can't switch self, current cgroup is /uid_%d/pid_%d\n", s_cuid, s_cpid); + fflush(stdout); + return -1; +} + +char *context = nullptr; + +int starter_main(int argc, char *argv[]) { + char *apk_path = nullptr; + char *lib_path = nullptr; + char *package_name = nullptr; + char *version_code = nullptr; + + // Get the apk path from the program arguments. This gets the path by setting the + // start of the apk path array to after the "--apk=" by offsetting by 6 characters. + for (int i = 0; i < argc; ++i) { + if (strncmp(argv[i], "--apk=", 6) == 0) { + apk_path = argv[i] + 6; + } else if (strncmp(argv[i], "--lib=", 6) == 0) { + lib_path = argv[i] + 6; + } else if (strncmp(argv[i], "--package=", 10) == 0) { + package_name = argv[i] + 10; + } else if (strncmp(argv[i], "--version_code=", 15) == 0) { + version_code = argv[i] + 15; + } + } + + printf("info: apk path = %s\n", apk_path); + printf("info: lib path = %s\n", lib_path); + printf("info: package name = %s\n", package_name); + printf("info: version code = %s\n", version_code); + + int uid = getuid(); + if (uid != 0 && uid != 2000) { + perrorf("fatal: run system bridge from non root nor adb user (uid=%d).\n", uid); + exit(EXIT_FATAL_UID); + } + + se::init(); + + if (uid == 0) { + chown("/data/local/tmp/keymapper_sysbridge_starter", 2000, 2000); + se::setfilecon("/data/local/tmp/keymapper_sysbridge_starter", + "u:object_r:shell_data_file:s0"); + switch_cgroup(); + + int sdkLevel = 0; + char buf[PROP_VALUE_MAX + 1]; + if (__system_property_get("ro.build.version.sdk", buf) > 0) + sdkLevel = atoi(buf); + + if (sdkLevel >= 29) { + printf("info: switching mount namespace to init...\n"); + switch_mnt_ns(1); + } + } + + if (uid == 0) { + if (se::getcon(&context) == 0) { + int res = 0; + + res |= check_selinux("u:r:untrusted_app:s0", context, "binder", "call"); + res |= check_selinux("u:r:untrusted_app:s0", context, "binder", "transfer"); + + if (res != 0) { + perrorf("fatal: the su you are using does not allow app (u:r:untrusted_app:s0) to connect to su (%s) with binder.\n", + context); + exit(EXIT_FATAL_BINDER_BLOCKED_BY_SELINUX); + } + se::freecon(context); + } + } + + mkdir("/data/local/tmp/keymapper_sysbridge", 0707); + chmod("/data/local/tmp/keymapper_sysbridge", 0707); + if (uid == 0) { + chown("/data/local/tmp/keymapper_sysbridge", 2000, 2000); + se::setfilecon("/data/local/tmp/keymapper_sysbridge", "u:object_r:shell_data_file:s0"); + } + + printf("info: starter begin\n"); + fflush(stdout); + + // kill old server + printf("info: killing old process...\n"); + fflush(stdout); + + foreach_proc([](pid_t pid) { + if (pid == getpid()) return; + + char name[1024]; + if (get_proc_name(pid, name, 1024) != 0) return; + + if (strcmp(SERVER_NAME, name) != 0) return; + + if (kill(pid, SIGKILL) == 0) + printf("info: killed %d (%s)\n", pid, name); + else if (errno == EPERM) { + perrorf("fatal: can't kill %d, please try to stop existing sysbridge from app first.\n", + pid); + exit(EXIT_FATAL_KILL); + } else { + printf("warn: failed to kill %d (%s)\n", pid, name); + } + }); + + if (access(apk_path, R_OK) == 0) { + printf("info: use apk path from argv\n"); + fflush(stdout); + } + + if (access(lib_path, R_OK) == 0) { + printf("info: use lib path from argv\n"); + fflush(stdout); + } + + if (!apk_path) { + perrorf("fatal: can't get path of manager\n"); + exit(EXIT_FATAL_PM_PATH); + } + + if (!lib_path) { + perrorf("fatal: can't get path of native libraries\n"); + exit(EXIT_FATAL_PM_PATH); + } + + printf("info: apk path is %s\n", apk_path); + printf("info: lib path is %s\n", lib_path); + if (access(apk_path, R_OK) != 0) { + perrorf("fatal: can't access manager %s\n", apk_path); + exit(EXIT_FATAL_PM_PATH); + } + + printf("info: starting server...\n"); + fflush(stdout); + LOGD("start_server"); + start_server(apk_path, lib_path, SERVER_CLASS_PATH, SERVER_NAME, package_name, version_code); + exit(EXIT_SUCCESS); +} + +using main_func = int (*)(int, char *[]); + +static main_func applet_main[] = {starter_main, nullptr}; + +int main(int argc, char **argv) { + std::string_view base = basename(argv[0]); + + LOGD("applet %s", base.data()); + + constexpr const char *applet_names[] = {"keymapper_sysbridge_starter", nullptr}; + + for (int i = 0; applet_names[i]; ++i) { + if (base == applet_names[i]) { + return (*applet_main[i])(argc, argv); + } + } + + return 1; +} diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/SystemBridgeHiltModule.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/SystemBridgeHiltModule.kt new file mode 100644 index 0000000000..d5bd0430b8 --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/SystemBridgeHiltModule.kt @@ -0,0 +1,30 @@ +package io.github.sds100.keymapper.sysbridge + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import io.github.sds100.keymapper.sysbridge.adb.AdbManager +import io.github.sds100.keymapper.sysbridge.adb.AdbManagerImpl +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManagerImpl +import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupController +import io.github.sds100.keymapper.sysbridge.service.SystemBridgeSetupControllerImpl +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class SystemBridgeHiltModule { + + @Singleton + @Binds + abstract fun bindSystemBridgeSetupController(impl: SystemBridgeSetupControllerImpl): SystemBridgeSetupController + + @Singleton + @Binds + abstract fun bindSystemBridgeManager(impl: SystemBridgeConnectionManagerImpl): SystemBridgeConnectionManager + + @Singleton + @Binds + abstract fun bindAdbManager(impl: AdbManagerImpl): AdbManager +} \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbClient.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbClient.kt new file mode 100644 index 0000000000..0c0fc936a6 --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbClient.kt @@ -0,0 +1,221 @@ +package io.github.sds100.keymapper.sysbridge.adb + +import io.github.sds100.keymapper.common.utils.KMResult +import io.github.sds100.keymapper.common.utils.Success +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.ADB_AUTH_RSAPUBLICKEY +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.ADB_AUTH_SIGNATURE +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_AUTH +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_CLSE +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_CNXN +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_MAXDATA +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_OKAY +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_OPEN +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_STLS +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_STLS_VERSION +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_VERSION +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_WRTE +import kotlinx.coroutines.delay +import timber.log.Timber +import java.io.Closeable +import java.io.DataInputStream +import java.io.DataOutputStream +import java.net.ConnectException +import java.net.Socket +import java.net.SocketException +import java.nio.ByteBuffer +import java.nio.ByteOrder +import javax.net.ssl.SSLProtocolException +import javax.net.ssl.SSLSocket + +private const val TAG = "AdbClient" + +internal class AdbClient(private val host: String, private val port: Int, private val key: AdbKey) : + Closeable { + + private var socket: Socket? = null + private lateinit var plainInputStream: DataInputStream + private lateinit var plainOutputStream: DataOutputStream + + private var useTls = false + + private lateinit var tlsSocket: SSLSocket + private lateinit var tlsInputStream: DataInputStream + private lateinit var tlsOutputStream: DataOutputStream + + private val inputStream get() = if (useTls) tlsInputStream else plainInputStream + private val outputStream get() = if (useTls) tlsOutputStream else plainOutputStream + + suspend fun connect(): KMResult { + Timber.d("Connecting to ADB at $host:$port") + var connectAttemptCounter = 0 + + // Try to connect to the client multiple times in case the server hasn't started up + // yet + while (socket == null && connectAttemptCounter < 5) { + try { + socket = Socket(host, port) + } catch (_: ConnectException) { + delay(1000) + connectAttemptCounter++ + continue + } + } + + if (socket == null) { + return AdbError.ConnectionError + } + + socket!!.tcpNoDelay = true + plainInputStream = DataInputStream(socket!!.getInputStream()) + plainOutputStream = DataOutputStream(socket!!.getOutputStream()) + + try { + write(A_CNXN, A_VERSION, A_MAXDATA, "host::") + + var message = read() + if (message.command == A_STLS) { + write(A_STLS, A_STLS_VERSION, 0) + + val sslContext = key.sslContext + tlsSocket = + sslContext.socketFactory.createSocket(socket, host, port, true) as SSLSocket + tlsSocket.startHandshake() + Timber.d("Handshake succeeded.") + + tlsInputStream = DataInputStream(tlsSocket.inputStream) + tlsOutputStream = DataOutputStream(tlsSocket.outputStream) + useTls = true + + message = read() + } else if (message.command == A_AUTH) { + write(A_AUTH, ADB_AUTH_SIGNATURE, 0, key.sign(message.data)) + + message = read() + if (message.command != A_CNXN) { + write(A_AUTH, ADB_AUTH_RSAPUBLICKEY, 0, key.adbPublicKey) + message = read() + } + } + + if (message.command != A_CNXN) { + error("not A_CNXN") + } + } catch (e: SSLProtocolException) { + // This can be thrown if the encryption keys mismatch + return AdbError.SslHandshakeError + } catch (e: SocketException) { + return AdbError.ConnectionError + } + + return Success(Unit) + } + + fun shellCommand(command: String): ByteArray { + val localId = 1 + write(A_OPEN, localId, 0, "shell:$command") + + var message = read() + when (message.command) { + A_OKAY -> { + while (true) { + message = read() + val remoteId = message.arg0 + if (message.command == A_WRTE) { + if (message.data_length > 0) { + return message.data!! + } + write(A_OKAY, localId, remoteId) + } else if (message.command == A_CLSE) { + write(A_CLSE, localId, remoteId) + break + } else { + error("not A_WRTE or A_CLSE") + } + } + } + + A_CLSE -> { + val remoteId = message.arg0 + write(A_CLSE, localId, remoteId) + } + + else -> { + error("not A_OKAY or A_CLSE") + } + } + + error("No response from adb?") + } + + private fun write(command: Int, arg0: Int, arg1: Int, data: ByteArray? = null) = write( + AdbMessage(command, arg0, arg1, data) + ) + + private fun write(command: Int, arg0: Int, arg1: Int, data: String) = write( + AdbMessage( + command, + arg0, + arg1, + data + ) + ) + + private fun write(message: AdbMessage) { + outputStream.write(message.toByteArray()) + outputStream.flush() + } + + private fun read(): AdbMessage { + val buffer = + ByteBuffer.allocate(AdbMessage.Companion.HEADER_LENGTH).order(ByteOrder.LITTLE_ENDIAN) + + inputStream.readFully(buffer.array(), 0, 24) + + val command = buffer.int + val arg0 = buffer.int + val arg1 = buffer.int + val dataLength = buffer.int + val checksum = buffer.int + val magic = buffer.int + val data: ByteArray? + if (dataLength >= 0) { + data = ByteArray(dataLength) + inputStream.readFully(data, 0, dataLength) + } else { + data = null + } + val message = AdbMessage(command, arg0, arg1, dataLength, checksum, magic, data) + message.validateOrThrow() + return message + } + + override fun close() { + try { + plainInputStream.close() + } catch (e: Throwable) { + } + try { + plainOutputStream.close() + } catch (e: Throwable) { + } + try { + socket?.close() + } catch (e: Exception) { + } + + if (useTls) { + try { + tlsInputStream.close() + } catch (e: Throwable) { + } + try { + tlsOutputStream.close() + } catch (e: Throwable) { + } + try { + tlsSocket.close() + } catch (e: Exception) { + } + } + } +} diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbError.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbError.kt new file mode 100644 index 0000000000..d6da78ca4a --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbError.kt @@ -0,0 +1,12 @@ +package io.github.sds100.keymapper.sysbridge.adb + +import io.github.sds100.keymapper.common.utils.KMError + +sealed class AdbError : KMError() { + data object PairingError : AdbError() + data object ServerNotFound : AdbError() + data object KeyCreationError : AdbError() + data object ConnectionError : AdbError() + data object SslHandshakeError : AdbError() + data class Unknown(val exception: kotlin.Exception) : AdbError() +} \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbException.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbException.kt new file mode 100644 index 0000000000..486118ad5a --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbException.kt @@ -0,0 +1,16 @@ +package io.github.sds100.keymapper.sysbridge.adb + +@Suppress("NOTHING_TO_INLINE") +internal inline fun adbError(message: Any): Nothing = throw AdbException(message.toString()) + +internal open class AdbException : Exception { + + constructor(message: String, cause: Throwable?) : super(message, cause) + constructor(message: String) : super(message) + constructor(cause: Throwable) : super(cause) + constructor() +} + +internal class AdbInvalidPairingCodeException : AdbException() + +internal class AdbKeyException(cause: Throwable) : AdbException(cause) diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbKey.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbKey.kt new file mode 100644 index 0000000000..14e9ba1767 --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbKey.kt @@ -0,0 +1,395 @@ +package io.github.sds100.keymapper.sysbridge.adb + +import android.annotation.SuppressLint +import android.content.SharedPreferences +import android.os.Build +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import android.util.Base64 +import androidx.annotation.RequiresApi +import androidx.core.content.edit +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo +import org.bouncycastle.cert.X509v3CertificateBuilder +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder +import org.conscrypt.Conscrypt +import rikka.core.ktx.unsafeLazy +import java.io.ByteArrayInputStream +import java.math.BigInteger +import java.net.Socket +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.security.Key +import java.security.KeyFactory +import java.security.KeyPairGenerator +import java.security.KeyStore +import java.security.Principal +import java.security.PrivateKey +import java.security.SecureRandom +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import java.security.interfaces.RSAPrivateKey +import java.security.interfaces.RSAPublicKey +import java.security.spec.PKCS8EncodedKeySpec +import java.security.spec.RSAKeyGenParameterSpec +import java.security.spec.RSAPublicKeySpec +import java.util.Date +import java.util.Locale +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.spec.GCMParameterSpec +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLEngine +import javax.net.ssl.X509ExtendedKeyManager +import javax.net.ssl.X509ExtendedTrustManager + +private const val TAG = "AdbKey" + +@RequiresApi(Build.VERSION_CODES.M) +internal class AdbKey(private val adbKeyStore: AdbKeyStore, name: String) { + + companion object { + + private const val ANDROID_KEYSTORE = "AndroidKeyStore" + private const val ENCRYPTION_KEY_ALIAS = "_adbkey_encryption_key_" + private const val TRANSFORMATION = "AES/GCM/NoPadding" + + private const val IV_SIZE_IN_BYTES = 12 + private const val TAG_SIZE_IN_BYTES = 16 + + private val PADDING = byteArrayOf( + 0x00, 0x01, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0x00, + 0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1a, 0x05, 0x00, + 0x04, 0x14, + ) + } + + private val encryptionKey: Key + + private val privateKey: RSAPrivateKey + private val publicKey: RSAPublicKey + private val certificate: X509Certificate + + init { + this.encryptionKey = getOrCreateEncryptionKey() + ?: error("Failed to generate encryption key with AndroidKeyManager.") + + this.privateKey = getOrCreatePrivateKey() + this.publicKey = KeyFactory.getInstance("RSA").generatePublic( + RSAPublicKeySpec( + privateKey.modulus, + RSAKeyGenParameterSpec.F4, + ), + ) as RSAPublicKey + + val signer = JcaContentSignerBuilder("SHA256withRSA").build(privateKey) + val x509Certificate = X509v3CertificateBuilder( + X500Name("CN=00"), + BigInteger.ONE, + Date(0), + Date(2461449600 * 1000), + Locale.ROOT, + X500Name("CN=00"), + SubjectPublicKeyInfo.getInstance(publicKey.encoded), + ).build(signer) + this.certificate = CertificateFactory.getInstance("X.509") + .generateCertificate(ByteArrayInputStream(x509Certificate.encoded)) as X509Certificate + +// Log.d(TAG, privateKey.toString()) + } + + val adbPublicKey: ByteArray by unsafeLazy { + publicKey.adbEncoded(name) + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun getOrCreateEncryptionKey(): Key? { + val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE) + keyStore.load(null) + + return keyStore.getKey(ENCRYPTION_KEY_ALIAS, null) ?: run { + val parameterSpec = KeyGenParameterSpec.Builder( + ENCRYPTION_KEY_ALIAS, + KeyProperties.PURPOSE_DECRYPT or KeyProperties.PURPOSE_ENCRYPT, + ) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .setKeySize(256) + .build() + val keyGenerator = + KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE) + keyGenerator.init(parameterSpec) + keyGenerator.generateKey() + } + } + + private fun encrypt(plaintext: ByteArray, aad: ByteArray?): ByteArray? { + if (plaintext.size > Int.MAX_VALUE - IV_SIZE_IN_BYTES - TAG_SIZE_IN_BYTES) { + return null + } + val ciphertext = ByteArray(IV_SIZE_IN_BYTES + plaintext.size + TAG_SIZE_IN_BYTES) + val cipher = Cipher.getInstance(TRANSFORMATION) + cipher.init(Cipher.ENCRYPT_MODE, encryptionKey) + cipher.updateAAD(aad) + cipher.doFinal(plaintext, 0, plaintext.size, ciphertext, IV_SIZE_IN_BYTES) + System.arraycopy(cipher.iv, 0, ciphertext, 0, IV_SIZE_IN_BYTES) + return ciphertext + } + + private fun decrypt(ciphertext: ByteArray, aad: ByteArray?): ByteArray? { + if (ciphertext.size < IV_SIZE_IN_BYTES + TAG_SIZE_IN_BYTES) { + return null + } + val params = GCMParameterSpec(8 * TAG_SIZE_IN_BYTES, ciphertext, 0, IV_SIZE_IN_BYTES) + val cipher = Cipher.getInstance(TRANSFORMATION) + cipher.init(Cipher.DECRYPT_MODE, encryptionKey, params) + cipher.updateAAD(aad) + return cipher.doFinal(ciphertext, IV_SIZE_IN_BYTES, ciphertext.size - IV_SIZE_IN_BYTES) + } + + private fun getOrCreatePrivateKey(): RSAPrivateKey { + var privateKey: RSAPrivateKey? = null + + val aad = ByteArray(16) + "adbkey".toByteArray().copyInto(aad) + + var ciphertext = adbKeyStore.get() + if (ciphertext != null) { + try { + val plaintext = decrypt(ciphertext, aad) + + val keyFactory = KeyFactory.getInstance("RSA") + privateKey = + keyFactory.generatePrivate(PKCS8EncodedKeySpec(plaintext)) as RSAPrivateKey + } catch (e: Exception) { + } + } + if (privateKey == null) { + val keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA) + keyPairGenerator.initialize(RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4)) + val keyPair = keyPairGenerator.generateKeyPair() + privateKey = keyPair.private as RSAPrivateKey + + ciphertext = encrypt(privateKey.encoded, aad) + if (ciphertext != null) { + adbKeyStore.put(ciphertext) + } + } + return privateKey + } + + fun sign(data: ByteArray?): ByteArray { + val cipher = Cipher.getInstance("RSA/ECB/NoPadding") + cipher.init(Cipher.ENCRYPT_MODE, privateKey) + cipher.update(PADDING) + return cipher.doFinal(data) + } + + private val keyManager + get() = object : X509ExtendedKeyManager() { + private val alias = "key" + + override fun chooseClientAlias( + keyTypes: Array, + issuers: Array?, + socket: Socket?, + ): String? { +// Log.d( +// TAG, +// "chooseClientAlias: keyType=${keyTypes.contentToString()}, issuers=${issuers?.contentToString()}", +// ) + for (keyType in keyTypes) { + if (keyType == "RSA") return alias + } + return null + } + + override fun getCertificateChain(alias: String?): Array? { +// Log.d(TAG, "getCertificateChain: alias=$alias") + return if (alias == this.alias) arrayOf(certificate) else null + } + + override fun getPrivateKey(alias: String?): PrivateKey? { +// Log.d(TAG, "getPrivateKey: alias=$alias") + return if (alias == this.alias) privateKey else null + } + + override fun getClientAliases( + keyType: String?, + issuers: Array?, + ): Array? { + return null + } + + override fun getServerAliases( + keyType: String, + issuers: Array?, + ): Array? { + return null + } + + override fun chooseServerAlias( + keyType: String, + issuers: Array?, + socket: Socket?, + ): String? { + return null + } + } + + private val trustManager + get() = + @RequiresApi(Build.VERSION_CODES.R) + object : X509ExtendedTrustManager() { + + @SuppressLint("TrustAllX509TrustManager") + override fun checkClientTrusted( + chain: Array?, + authType: String?, + socket: Socket?, + ) { + } + + @SuppressLint("TrustAllX509TrustManager") + override fun checkClientTrusted( + chain: Array?, + authType: String?, + engine: SSLEngine?, + ) { + } + + @SuppressLint("TrustAllX509TrustManager") + override fun checkClientTrusted( + chain: Array?, + authType: String?, + ) { + } + + @SuppressLint("TrustAllX509TrustManager") + override fun checkServerTrusted( + chain: Array?, + authType: String?, + socket: Socket?, + ) { + } + + @SuppressLint("TrustAllX509TrustManager") + override fun checkServerTrusted( + chain: Array?, + authType: String?, + engine: SSLEngine?, + ) { + } + + @SuppressLint("TrustAllX509TrustManager") + override fun checkServerTrusted( + chain: Array?, + authType: String?, + ) { + } + + override fun getAcceptedIssuers(): Array { + return emptyArray() + } + } + + @delegate:RequiresApi(Build.VERSION_CODES.R) + val sslContext: SSLContext by unsafeLazy { + val sslContext = SSLContext.getInstance("TLSv1.3", Conscrypt.newProvider()) + sslContext.init( + arrayOf(keyManager), + arrayOf(trustManager), + SecureRandom(), + ) + sslContext + } +} + +interface AdbKeyStore { + + fun put(bytes: ByteArray) + + fun get(): ByteArray? +} + +class PreferenceAdbKeyStore(private val preference: SharedPreferences) : AdbKeyStore { + + private val preferenceKey = "adbkey" + + override fun put(bytes: ByteArray) { + preference.edit { putString(preferenceKey, String(Base64.encode(bytes, Base64.NO_WRAP))) } + } + + override fun get(): ByteArray? { + if (!preference.contains(preferenceKey)) return null + return Base64.decode(preference.getString(preferenceKey, null), Base64.NO_WRAP) + } +} + +const val ANDROID_PUBKEY_MODULUS_SIZE = 2048 / 8 +const val ANDROID_PUBKEY_MODULUS_SIZE_WORDS = ANDROID_PUBKEY_MODULUS_SIZE / 4 +const val RSAPublicKey_Size = 524 + +private fun BigInteger.toAdbEncoded(): IntArray { + // little-endian integer with padding zeros in the end + + val endcoded = IntArray(ANDROID_PUBKEY_MODULUS_SIZE_WORDS) + val r32 = BigInteger.ZERO.setBit(32) + + var tmp = this.add(BigInteger.ZERO) + for (i in 0 until ANDROID_PUBKEY_MODULUS_SIZE_WORDS) { + val out = tmp.divideAndRemainder(r32) + tmp = out[0] + endcoded[i] = out[1].toInt() + } + return endcoded +} + +private fun RSAPublicKey.adbEncoded(name: String): ByteArray { + // https://cs.android.com/android/platform/superproject/+/android-10.0.0_r30:system/core/libcrypto_utils/android_pubkey.c + + /* + typedef struct RSAPublicKey { + uint32_t modulus_size_words; // ANDROID_PUBKEY_MODULUS_SIZE + uint32_t n0inv; // n0inv = -1 / N[0] mod 2^32 + uint8_t modulus[ANDROID_PUBKEY_MODULUS_SIZE]; + uint8_t rr[ANDROID_PUBKEY_MODULUS_SIZE]; // rr = (2^(rsa_size)) ^ 2 mod N + uint32_t exponent; + } RSAPublicKey; + */ + + val r32 = BigInteger.ZERO.setBit(32) + val n0inv = modulus.remainder(r32).modInverse(r32).negate() + val r = BigInteger.ZERO.setBit(ANDROID_PUBKEY_MODULUS_SIZE * 8) + val rr = r.modPow(BigInteger.valueOf(2), modulus) + + val buffer = ByteBuffer.allocate(RSAPublicKey_Size).order(ByteOrder.LITTLE_ENDIAN) + buffer.putInt(ANDROID_PUBKEY_MODULUS_SIZE_WORDS) + buffer.putInt(n0inv.toInt()) + modulus.toAdbEncoded().forEach { buffer.putInt(it) } + rr.toAdbEncoded().forEach { buffer.putInt(it) } + buffer.putInt(publicExponent.toInt()) + + val base64Bytes = Base64.encode(buffer.array(), Base64.NO_WRAP) + val nameBytes = " $name\u0000".toByteArray() + val bytes = ByteArray(base64Bytes.size + nameBytes.size) + base64Bytes.copyInto(bytes) + nameBytes.copyInto(bytes, base64Bytes.size) + return bytes +} diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbManager.kt new file mode 100644 index 0000000000..1e4514b975 --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbManager.kt @@ -0,0 +1,125 @@ +package io.github.sds100.keymapper.sysbridge.adb + +import android.content.Context +import android.os.Build +import android.preference.PreferenceManager +import androidx.annotation.RequiresApi +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.sds100.keymapper.common.utils.KMError +import io.github.sds100.keymapper.common.utils.KMResult +import io.github.sds100.keymapper.common.utils.Success +import io.github.sds100.keymapper.common.utils.success +import io.github.sds100.keymapper.common.utils.then +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + + +@RequiresApi(Build.VERSION_CODES.R) +@Singleton +class AdbManagerImpl @Inject constructor( + @ApplicationContext private val ctx: Context, +) : AdbManager { + companion object { + private const val LOCALHOST = "127.0.0.1" + } + + private val commandMutex: Mutex = Mutex() + private val pairMutex: Mutex = Mutex() + + private val adbConnectMdns: AdbMdns by lazy { AdbMdns(ctx, AdbServiceType.TLS_CONNECT) } + private val adbPairMdns: AdbMdns by lazy { AdbMdns(ctx, AdbServiceType.TLS_PAIR) } + + override suspend fun executeCommand(command: String): KMResult { + Timber.d("Execute ADB command: $command") + + val result = withContext(Dispatchers.IO) { + return@withContext commandMutex.withLock { + val port = adbConnectMdns.discoverPort() + + if (port == null || port == -1) { + return@withLock AdbError.ServerNotFound + } + + val adbKey = getAdbKey() + + // Recreate a new client every time in case the port changes during the lifetime + // of AdbManager + val client: AdbClient = when (adbKey) { + is KMError -> return@withLock adbKey + is Success -> AdbClient(LOCALHOST, port, adbKey.value) + } + + return@withLock with(client) { + connect().then { + try { + client.shellCommand(command).success() + } catch (e: Exception) { + Timber.e(e) + AdbError.Unknown(e) + } + } + }.then { String(it).success() } + } + } + + Timber.i("Execute command result: $result") + + return result + } + + override suspend fun pair(code: String): KMResult { + return pairMutex.withLock { + val port = adbPairMdns.discoverPort() + + if (port == null) { + return@withLock AdbError.ServerNotFound + } + + return@withLock getAdbKey().then { key -> + val pairingClient = AdbPairingClient(LOCALHOST, port, code, key) + + with(pairingClient) { + try { + withContext(Dispatchers.IO) { + start() + } + Timber.i("Successfully paired with wireless ADB on port $port with code $code") + Success(Unit) + } catch (e: Exception) { + e.printStackTrace() + Timber.e("Failed to pair with wireless ADB on port $port with code $code: $e") + AdbError.PairingError + } + } + } + } + } + + private fun getAdbKey(): KMResult { + try { + return AdbKey( + PreferenceAdbKeyStore(PreferenceManager.getDefaultSharedPreferences(ctx)), + "keymapper", + ).success() + } catch (e: Throwable) { + Timber.e(e) + return AdbError.KeyCreationError + } + } +} + +interface AdbManager { + /** + * Execute an ADB command + */ + @RequiresApi(Build.VERSION_CODES.R) + suspend fun executeCommand(command: String): KMResult + + @RequiresApi(Build.VERSION_CODES.R) + suspend fun pair(code: String): KMResult +} \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMdns.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMdns.kt new file mode 100644 index 0000000000..3a3fbf868c --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMdns.kt @@ -0,0 +1,207 @@ +package io.github.sds100.keymapper.sysbridge.adb + +import android.content.Context +import android.net.nsd.NsdManager +import android.net.nsd.NsdServiceInfo +import android.os.Build +import android.os.ext.SdkExtensions +import androidx.annotation.RequiresApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.trySendBlocking +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout +import timber.log.Timber +import java.io.IOException +import java.net.InetSocketAddress +import java.net.NetworkInterface +import java.net.ServerSocket + +/** + * This uses mDNS to scan for the ADB pairing and connection ports. + */ +@RequiresApi(Build.VERSION_CODES.R) +internal class AdbMdns( + ctx: Context, + private val serviceType: AdbServiceType, +) { + + private val nsdManager: NsdManager = ctx.getSystemService(NsdManager::class.java) + + private val serviceDiscoveredChannel: Channel = Channel(capacity = 10) + + /** + * Only one service can be resolved at a time. + * A null value is sent if the service failed to resolve. + */ + private val serviceResolvedChannel: Channel = Channel(capacity = 1) + + private val isDiscovering: MutableStateFlow = MutableStateFlow(false) + private val discoveredPort: MutableStateFlow = MutableStateFlow(null) + private val discoverMutex: Mutex = Mutex() + + private val resolveListener: NsdManager.ResolveListener = object : NsdManager.ResolveListener { + override fun onResolveFailed(nsdServiceInfo: NsdServiceInfo, i: Int) { + serviceResolvedChannel.trySendBlocking(null) + } + + override fun onServiceResolved(nsdServiceInfo: NsdServiceInfo) { + Timber.d("onServiceResolved: ${nsdServiceInfo.serviceName} ${nsdServiceInfo.host} ${nsdServiceInfo.port} ${nsdServiceInfo.serviceType}") + serviceResolvedChannel.trySendBlocking(nsdServiceInfo) + } + + override fun onResolutionStopped(serviceInfo: NsdServiceInfo) { + super.onResolutionStopped(serviceInfo) + + serviceResolvedChannel.trySendBlocking(null) + } + + override fun onStopResolutionFailed(serviceInfo: NsdServiceInfo, errorCode: Int) { + super.onStopResolutionFailed(serviceInfo, errorCode) + + serviceResolvedChannel.trySendBlocking(null) + } + } + + private val discoveryListener: NsdManager.DiscoveryListener = + object : NsdManager.DiscoveryListener { + override fun onDiscoveryStarted(serviceType: String) { + Timber.d("onDiscoveryStarted: $serviceType") + isDiscovering.update { true } + } + + override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) { + Timber.d("onStartDiscoveryFailed: $serviceType, $errorCode") + isDiscovering.update { false } + } + + override fun onDiscoveryStopped(serviceType: String) { + Timber.d("onDiscoveryStopped: $serviceType") + isDiscovering.update { false } + } + + override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) { + Timber.d("onStopDiscoveryFailed: $serviceType, $errorCode") + isDiscovering.update { false } + } + + override fun onServiceFound(serviceInfo: NsdServiceInfo) { + Timber.d("onServiceFound: ${serviceInfo.serviceName} ${serviceInfo.host} ${serviceInfo.port} ${serviceInfo.serviceType}") + + // You can only resolve one service at a time and they can take some time to resolve. + serviceDiscoveredChannel.trySend(serviceInfo) + } + + override fun onServiceLost(serviceInfo: NsdServiceInfo) { + Timber.d("onServiceLost: ${serviceInfo.serviceName}") + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + suspend fun discoverPort(): Int? { + discoverMutex.withLock { + val currentPort = discoveredPort.value + + if (currentPort == null || !isPortAvailable(currentPort)) { + val port = withContext(Dispatchers.IO) { + discoverPortInternal() + } + discoveredPort.value = port + return port + } else { + return currentPort + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + private suspend fun discoverPortInternal(): Int? { + var port: Int? = null + + cleanup() + + // Wait for it to stop discovering + isDiscovering.first { !it } + + nsdManager.discoverServices( + serviceType.id, + NsdManager.PROTOCOL_DNS_SD, + discoveryListener + ) + + try { + withTimeout(10000L) { + while (port == null) { + val service = serviceDiscoveredChannel.receive() + nsdManager.resolveService(service, resolveListener) + + val resolvedService = serviceResolvedChannel.receive() + + if (resolvedService == null) { + continue + } + + val isLocalNetwork = NetworkInterface.getNetworkInterfaces() + .asSequence() + .any { networkInterface -> + networkInterface.inetAddresses + .asSequence() + .any { resolvedService.host.hostAddress == it.hostAddress } + } + + if (isLocalNetwork && isPortAvailable(resolvedService.port)) { + Timber.d("Discovered ADB port: ${resolvedService.port}") + port = resolvedService.port + } + } + } + + } catch (e: Exception) { + Timber.e(e, "Failed to discover ADB port") + } finally { + cleanup() + } + + return port + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun cleanup() { + runCatching { + if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.TIRAMISU) >= 7) { + nsdManager.stopServiceResolution(resolveListener) + } + } + + runCatching { + nsdManager.stopServiceDiscovery(discoveryListener) + } + + // Clear the resolve channel if there is anything left. + while (!serviceResolvedChannel.isEmpty) { + serviceResolvedChannel.tryReceive() + } + + // Clear the discovered channel if there is anything left. + while (!serviceDiscoveredChannel.isEmpty) { + serviceDiscoveredChannel.tryReceive() + } + } + + private fun isPortAvailable(port: Int): Boolean { + return try { + ServerSocket().use { + it.bind(InetSocketAddress("127.0.0.1", port), 1) + false + } + } catch (e: IOException) { + true + } + } +} diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMessage.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMessage.kt new file mode 100644 index 0000000000..19a1d40d1b --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbMessage.kt @@ -0,0 +1,132 @@ +package io.github.sds100.keymapper.sysbridge.adb + +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_AUTH +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_CLSE +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_CNXN +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_OKAY +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_OPEN +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_STLS +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_SYNC +import io.github.sds100.keymapper.sysbridge.adb.AdbProtocol.A_WRTE +import java.nio.ByteBuffer +import java.nio.ByteOrder + +internal class AdbMessage( + val command: Int, + val arg0: Int, + val arg1: Int, + val data_length: Int, + val data_crc32: Int, + val magic: Int, + val data: ByteArray? +) { + + constructor(command: Int, arg0: Int, arg1: Int, data: String) : this( + command, + arg0, + arg1, + "$data\u0000".toByteArray()) + + constructor(command: Int, arg0: Int, arg1: Int, data: ByteArray?) : this( + command, + arg0, + arg1, + data?.size ?: 0, + crc32(data), + (command.toLong() xor 0xFFFFFFFF).toInt(), + data) + + fun validate(): Boolean { + if (command != magic xor -0x1) return false + if (data_length != 0 && crc32(data) != data_crc32) return false + return true + } + + fun validateOrThrow() { + if (!validate()) throw IllegalArgumentException("bad message ${this.toStringShort()}") + } + + fun toByteArray(): ByteArray { + val length = HEADER_LENGTH + (data?.size ?: 0) + return ByteBuffer.allocate(length).apply { + order(ByteOrder.LITTLE_ENDIAN) + putInt(command) + putInt(arg0) + putInt(arg1) + putInt(data_length) + putInt(data_crc32) + putInt(magic) + if (data != null) { + put(data) + } + }.array() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AdbMessage + + if (command != other.command) return false + if (arg0 != other.arg0) return false + if (arg1 != other.arg1) return false + if (data_length != other.data_length) return false + if (data_crc32 != other.data_crc32) return false + if (magic != other.magic) return false + if (data != null) { + if (other.data == null) return false + if (!data.contentEquals(other.data)) return false + } else if (other.data != null) return false + + return true + } + + override fun hashCode(): Int { + var result = command + result = 31 * result + arg0 + result = 31 * result + arg1 + result = 31 * result + data_length + result = 31 * result + data_crc32 + result = 31 * result + magic + result = 31 * result + (data?.contentHashCode() ?: 0) + return result + } + + override fun toString(): String { + return "AdbMessage(${toStringShort()})" + } + + fun toStringShort(): String { + val commandString = when (command) { + A_SYNC -> "A_SYNC" + A_CNXN -> "A_CNXN" + A_AUTH -> "A_AUTH" + A_OPEN -> "A_OPEN" + A_OKAY -> "A_OKAY" + A_CLSE -> "A_CLSE" + A_WRTE -> "A_WRTE" + A_STLS -> "A_STLS" + else -> command.toString() + } + return "command=$commandString, arg0=$arg0, arg1=$arg1, data_length=$data_length, data_crc32=$data_crc32, magic=$magic, data=${data?.contentToString()}" + } + + companion object { + + const val HEADER_LENGTH = 24 + + + private fun crc32(data: ByteArray?): Int { + if (data == null) return 0 + var res = 0 + for (b in data) { + if (b >= 0) + res += b + else + res += b + 256 + } + return res + } + } +} diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbPairingClient.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbPairingClient.kt new file mode 100644 index 0000000000..bea2fe9e9b --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbPairingClient.kt @@ -0,0 +1,330 @@ +package io.github.sds100.keymapper.sysbridge.adb + +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi +import org.conscrypt.Conscrypt +import java.io.Closeable +import java.io.DataInputStream +import java.io.DataOutputStream +import java.net.Socket +import java.nio.ByteBuffer +import java.nio.ByteOrder +import javax.net.ssl.SSLSocket + +private const val TAG = "AdbPairClient" + +private const val kCurrentKeyHeaderVersion = 1.toByte() +private const val kMinSupportedKeyHeaderVersion = 1.toByte() +private const val kMaxSupportedKeyHeaderVersion = 1.toByte() +private const val kMaxPeerInfoSize = 8192 +private const val kMaxPayloadSize = kMaxPeerInfoSize * 2 + +private const val kExportedKeyLabel = "adb-label\u0000" +private const val kExportedKeySize = 64 + +private const val kPairingPacketHeaderSize = 6 + +private class PeerInfo( + val type: Byte, + data: ByteArray, +) { + + val data = ByteArray(kMaxPeerInfoSize - 1) + + init { + data.copyInto(this.data, 0, 0, data.size.coerceAtMost(kMaxPeerInfoSize - 1)) + } + + enum class Type(val value: Byte) { + ADB_RSA_PUB_KEY(0.toByte()), + ADB_DEVICE_GUID(0.toByte()), + } + + fun writeTo(buffer: ByteBuffer) { + buffer.run { + put(type) + put(data) + } + +// Log.d(TAG, "write PeerInfo ${toStringShort()}") + } + + override fun toString(): String { + return "PeerInfo(${toStringShort()})" + } + + fun toStringShort(): String { + return "type=$type, data=${data.contentToString()}" + } + + companion object { + + fun readFrom(buffer: ByteBuffer): PeerInfo { + val type = buffer.get() + val data = ByteArray(kMaxPeerInfoSize - 1) + buffer.get(data) + return PeerInfo(type, data) + } + } +} + +private class PairingPacketHeader( + val version: Byte, + val type: Byte, + val payload: Int, +) { + + enum class Type(val value: Byte) { + SPAKE2_MSG(0.toByte()), + PEER_INFO(1.toByte()), + } + + fun writeTo(buffer: ByteBuffer) { + buffer.run { + put(version) + put(type) + putInt(payload) + } + +// Log.d(TAG, "write PairingPacketHeader ${toStringShort()}") + } + + override fun toString(): String { + return "PairingPacketHeader(${toStringShort()})" + } + + fun toStringShort(): String { + return "version=${version.toInt()}, type=${type.toInt()}, payload=$payload" + } + + companion object { + + fun readFrom(buffer: ByteBuffer): PairingPacketHeader? { + val version = buffer.get() + val type = buffer.get() + val payload = buffer.int + + if (version < kMinSupportedKeyHeaderVersion || version > kMaxSupportedKeyHeaderVersion) { + Log.e( + TAG, + "PairingPacketHeader version mismatch (us=$kCurrentKeyHeaderVersion them=$version)", + ) + return null + } + if (type != Type.SPAKE2_MSG.value && type != Type.PEER_INFO.value) { + Log.e(TAG, "Unknown PairingPacket type=$type") + return null + } + if (payload <= 0 || payload > kMaxPayloadSize) { + Log.e(TAG, "header payload not within a safe payload size (size=$payload)") + return null + } + + val header = PairingPacketHeader(version, type, payload) + Log.d(TAG, "read PairingPacketHeader ${header.toStringShort()}") + return header + } + } +} + +private class PairingContext private constructor(private val nativePtr: Long) { + + val msg: ByteArray + + init { + msg = nativeMsg(nativePtr) + } + + fun initCipher(theirMsg: ByteArray) = nativeInitCipher(nativePtr, theirMsg) + + fun encrypt(`in`: ByteArray) = nativeEncrypt(nativePtr, `in`) + + fun decrypt(`in`: ByteArray) = nativeDecrypt(nativePtr, `in`) + + fun destroy() = nativeDestroy(nativePtr) + + private external fun nativeMsg(nativePtr: Long): ByteArray + + private external fun nativeInitCipher(nativePtr: Long, theirMsg: ByteArray): Boolean + + private external fun nativeEncrypt(nativePtr: Long, inbuf: ByteArray): ByteArray? + + private external fun nativeDecrypt(nativePtr: Long, inbuf: ByteArray): ByteArray? + + private external fun nativeDestroy(nativePtr: Long) + + companion object { + + fun create(password: ByteArray): PairingContext? { + val nativePtr = nativeConstructor(true, password) + return if (nativePtr != 0L) PairingContext(nativePtr) else null + } + + @JvmStatic + private external fun nativeConstructor(isClient: Boolean, password: ByteArray): Long + } +} + +@RequiresApi(Build.VERSION_CODES.R) +internal class AdbPairingClient( + private val host: String, + private val port: Int, + private val pairCode: String, + private val key: AdbKey, +) : Closeable { + + private enum class State { + Ready, + ExchangingMsgs, + ExchangingPeerInfo, + Stopped, + } + + private lateinit var socket: Socket + private lateinit var inputStream: DataInputStream + private lateinit var outputStream: DataOutputStream + + private val peerInfo: PeerInfo = PeerInfo(PeerInfo.Type.ADB_RSA_PUB_KEY.value, key.adbPublicKey) + private lateinit var pairingContext: PairingContext + private var state: State = State.Ready + + fun start(): Boolean { + setupTlsConnection() + + state = State.ExchangingMsgs + + if (!doExchangeMsgs()) { + state = State.Stopped + return false + } + + state = State.ExchangingPeerInfo + + if (!doExchangePeerInfo()) { + state = State.Stopped + return false + } + + state = State.Stopped + return true + } + + private fun setupTlsConnection() { + socket = Socket(host, port) + socket.tcpNoDelay = true + + val sslContext = key.sslContext + + val sslSocket = sslContext.socketFactory.createSocket(socket, host, port, true) as SSLSocket + sslSocket.startHandshake() +// Log.d(TAG, "Handshake succeeded.") + + inputStream = DataInputStream(sslSocket.inputStream) + outputStream = DataOutputStream(sslSocket.outputStream) + + val pairCodeBytes = pairCode.toByteArray() + val keyMaterial = Conscrypt.exportKeyingMaterial(sslSocket, kExportedKeyLabel, null, kExportedKeySize) + val passwordBytes = ByteArray(pairCode.length + keyMaterial.size) + pairCodeBytes.copyInto(passwordBytes) + keyMaterial.copyInto(passwordBytes, pairCodeBytes.size) + + val pairingContext = PairingContext.create(passwordBytes) + checkNotNull(pairingContext) { "Unable to create PairingContext." } + this.pairingContext = pairingContext + } + + private fun createHeader( + type: PairingPacketHeader.Type, + payloadSize: Int, + ): PairingPacketHeader { + return PairingPacketHeader(kCurrentKeyHeaderVersion, type.value, payloadSize) + } + + private fun readHeader(): PairingPacketHeader? { + val bytes = ByteArray(kPairingPacketHeaderSize) + inputStream.readFully(bytes) + val buffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN) + return PairingPacketHeader.readFrom(buffer) + } + + private fun writeHeader(header: PairingPacketHeader, payload: ByteArray) { + val buffer = ByteBuffer.allocate(kPairingPacketHeaderSize).order(ByteOrder.BIG_ENDIAN) + header.writeTo(buffer) + + outputStream.write(buffer.array()) + outputStream.write(payload) +// Log.d(TAG, "write payload, size=${payload.size}") + } + + private fun doExchangeMsgs(): Boolean { + val msg = pairingContext.msg + val size = msg.size + + val ourHeader = createHeader(PairingPacketHeader.Type.SPAKE2_MSG, size) + writeHeader(ourHeader, msg) + + val theirHeader = readHeader() ?: return false + if (theirHeader.type != PairingPacketHeader.Type.SPAKE2_MSG.value) return false + + val theirMessage = ByteArray(theirHeader.payload) + inputStream.readFully(theirMessage) + + return pairingContext.initCipher(theirMessage) + } + + private fun doExchangePeerInfo(): Boolean { + val buf = ByteBuffer.allocate(kMaxPeerInfoSize).order(ByteOrder.BIG_ENDIAN) + peerInfo.writeTo(buf) + + val outbuf = pairingContext.encrypt(buf.array()) ?: return false + + val ourHeader = createHeader(PairingPacketHeader.Type.PEER_INFO, outbuf.size) + writeHeader(ourHeader, outbuf) + + val theirHeader = readHeader() ?: return false + if (theirHeader.type != PairingPacketHeader.Type.PEER_INFO.value) return false + + val theirMessage = ByteArray(theirHeader.payload) + inputStream.readFully(theirMessage) + + val decrypted = + pairingContext.decrypt(theirMessage) ?: throw AdbInvalidPairingCodeException() + if (decrypted.size != kMaxPeerInfoSize) { +// Log.e(TAG, "Got size=${decrypted.size} PeerInfo.size=$kMaxPeerInfoSize") + return false + } + PeerInfo.readFrom(ByteBuffer.wrap(decrypted)) +// Log.d(TAG, theirPeerInfo.toString()) + return true + } + + override fun close() { + try { + inputStream.close() + } catch (e: Throwable) { + } + try { + outputStream.close() + } catch (e: Throwable) { + } + try { + socket.close() + } catch (e: Exception) { + } + + if (state != State.Ready) { + pairingContext.destroy() + } + } + + companion object { + + init { + System.loadLibrary("adb") + } + + @JvmStatic + external fun available(): Boolean + } +} diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbProtocol.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbProtocol.kt new file mode 100644 index 0000000000..d3d142156c --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbProtocol.kt @@ -0,0 +1,22 @@ +package io.github.sds100.keymapper.sysbridge.adb + +internal object AdbProtocol { + + const val A_SYNC = 0x434e5953 + const val A_CNXN = 0x4e584e43 + const val A_AUTH = 0x48545541 + const val A_OPEN = 0x4e45504f + const val A_OKAY = 0x59414b4f + const val A_CLSE = 0x45534c43 + const val A_WRTE = 0x45545257 + const val A_STLS = 0x534C5453 + + const val A_VERSION = 0x01000000 + const val A_MAXDATA = 4096 + + const val A_STLS_VERSION = 0x01000000 + + const val ADB_AUTH_TOKEN = 1 + const val ADB_AUTH_SIGNATURE = 2 + const val ADB_AUTH_RSAPUBLICKEY = 3 +} \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbServiceType.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbServiceType.kt new file mode 100644 index 0000000000..ee3a49347f --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/adb/AdbServiceType.kt @@ -0,0 +1,5 @@ +package io.github.sds100.keymapper.sysbridge.adb + +enum class AdbServiceType(val id: String) { + TLS_CONNECT("_adb-tls-connect._tcp"), TLS_PAIR("_adb-tls-pairing._tcp") +} \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/ktx/Log.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/ktx/Log.kt new file mode 100644 index 0000000000..b951be5e1c --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/ktx/Log.kt @@ -0,0 +1,24 @@ +@file:Suppress("NOTHING_TO_INLINE") + +package io.github.sds100.keymapper.sysbridge.ktx + +import android.util.Log + +inline val T.TAG: String + get() = + T::class.java.simpleName.let { + if (it.isBlank()) throw IllegalStateException("tag is empty") + if (it.length > 23) it.substring(0, 23) else it + } + +inline fun T.logv(message: String, throwable: Throwable? = null) = logv(TAG, message, throwable) +inline fun T.logi(message: String, throwable: Throwable? = null) = logi(TAG, message, throwable) +inline fun T.logw(message: String, throwable: Throwable? = null) = logw(TAG, message, throwable) +inline fun T.logd(message: String, throwable: Throwable? = null) = logd(TAG, message, throwable) +inline fun T.loge(message: String, throwable: Throwable? = null) = loge(TAG, message, throwable) + +inline fun T.logv(tag: String, message: String, throwable: Throwable? = null) = Log.v(tag, message, throwable) +inline fun T.logi(tag: String, message: String, throwable: Throwable? = null) = Log.i(tag, message, throwable) +inline fun T.logw(tag: String, message: String, throwable: Throwable? = null) = Log.w(tag, message, throwable) +inline fun T.logd(tag: String, message: String, throwable: Throwable? = null) = Log.d(tag, message, throwable) +inline fun T.loge(tag: String, message: String, throwable: Throwable? = null) = Log.e(tag, message, throwable) \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt new file mode 100644 index 0000000000..39548be3a4 --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionManager.kt @@ -0,0 +1,240 @@ +package io.github.sds100.keymapper.sysbridge.manager + +import android.Manifest +import android.annotation.SuppressLint +import android.content.Context +import android.content.pm.PackageManager.PERMISSION_GRANTED +import android.os.Build +import android.os.DeadObjectException +import android.os.IBinder +import android.os.IBinder.DeathRecipient +import android.os.Process +import android.os.RemoteException +import android.os.SystemClock +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.content.ContextCompat +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.sds100.keymapper.common.BuildConfigProvider +import io.github.sds100.keymapper.common.utils.KMError +import io.github.sds100.keymapper.common.utils.KMResult +import io.github.sds100.keymapper.common.utils.SettingsUtils +import io.github.sds100.keymapper.common.utils.Success +import io.github.sds100.keymapper.common.utils.onFailure +import io.github.sds100.keymapper.common.utils.success +import io.github.sds100.keymapper.sysbridge.ISystemBridge +import io.github.sds100.keymapper.sysbridge.ktx.TAG +import io.github.sds100.keymapper.sysbridge.starter.SystemBridgeStarter +import io.github.sds100.keymapper.sysbridge.utils.SystemBridgeError +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +/** + * This class handles starting, stopping and (dis)connecting to the system bridge. + */ +@Singleton +class SystemBridgeConnectionManagerImpl @Inject constructor( + @ApplicationContext private val ctx: Context, + private val coroutineScope: CoroutineScope, + private val starter: SystemBridgeStarter, + private val buildConfigProvider: BuildConfigProvider, +) : SystemBridgeConnectionManager { + + private val systemBridgeLock: Any = Any() + private var systemBridgeFlow: MutableStateFlow = MutableStateFlow(null) + + override val connectionState: MutableStateFlow = + MutableStateFlow( + SystemBridgeConnectionState.Disconnected( + time = SystemClock.elapsedRealtime(), isExpected = true + ) + ) + private var isExpectedDeath: Boolean = false + + private val deathRecipient: DeathRecipient = DeathRecipient { + synchronized(systemBridgeLock) { + Timber.e("System Bridge has died") + + systemBridgeFlow.update { null } + + connectionState.update { + SystemBridgeConnectionState.Disconnected( + time = SystemClock.elapsedRealtime(), + isExpected = isExpectedDeath + ) + } + + isExpectedDeath = false + } + } + + private var startJob: Job? = null + + fun pingBinder(): Boolean { + synchronized(systemBridgeLock) { + return systemBridgeFlow.value?.asBinder()?.pingBinder() == true + } + } + + /** + * This is called by the SystemBridgeBinderProvider content provider. + */ + @SuppressLint("LogNotTimber") + fun onBinderReceived(binder: IBinder) { + val systemBridge = ISystemBridge.Stub.asInterface(binder) + + synchronized(systemBridgeLock) { + if (systemBridge.versionCode == buildConfigProvider.versionCode) { + // Only link to death if it is the same version code so restarting it + // doesn't send a death message + systemBridge.asBinder().linkToDeath(deathRecipient, 0) + + this.systemBridgeFlow.update { systemBridge } + + // Only turn on the ADB options to prevent killing if it is running under + // the ADB shell user + if (systemBridge.processUid == Process.SHELL_UID) { + preventSystemBridgeKilling(systemBridge) + } + + connectionState.update { + SystemBridgeConnectionState.Connected( + time = SystemClock.elapsedRealtime(), + ) + } + } else { + coroutineScope.launch(Dispatchers.IO) { + // Can not use Timber because the content provider is called before the application's + // onCreate where the Timber Tree is installed. The content provider then + // calls this message. + Log.w( + TAG, + "System Bridge version mismatch! Restarting it. App: ${buildConfigProvider.versionCode}, System Bridge: ${systemBridge.versionCode}" + ) + + restartSystemBridge(systemBridge) + } + } + } + } + + @SuppressLint("LogNotTimber") + private suspend fun restartSystemBridge(systemBridge: ISystemBridge) { + starter.startSystemBridge(executeCommand = { command -> + try { + systemBridge.executeCommand(command)!!.success() + } catch (_: DeadObjectException) { + // This exception is expected since it is killing the system bridge + Success("") + } catch (e: Exception) { + KMError.Exception(e) + } + }).onFailure { error -> + Log.e(TAG, "Failed to restart System Bridge: $error") + } + } + + override fun run(block: (ISystemBridge) -> T): KMResult { + try { + val systemBridge = systemBridgeFlow.value ?: return SystemBridgeError.Disconnected + + return Success(block(systemBridge)) + } catch (e: RemoteException) { + return KMError.Exception(e) + } + } + + override fun stopSystemBridge() { + synchronized(systemBridgeLock) { + isExpectedDeath = true + + try { + systemBridgeFlow.value?.destroy() + } catch (e: RemoteException) { + // This is expected to throw an exception because the destroy() method kills + // the process. + } + } + } + + @RequiresApi(Build.VERSION_CODES.R) + override fun startWithAdb() { + if (startJob?.isActive == true) { + Timber.i("System Bridge is already starting") + return + } + + startJob = coroutineScope.launch { + starter.startWithAdb() + } + } + + private fun preventSystemBridgeKilling(systemBridge: ISystemBridge) { + val deviceId: Int = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + ctx.deviceId + } else { + -1 + } + + systemBridge.grantPermission(Manifest.permission.WRITE_SECURE_SETTINGS, deviceId) + Timber.i("Granted WRITE_SECURE_SETTINGS permission with System Bridge") + + if (ContextCompat.checkSelfPermission( + ctx, + Manifest.permission.WRITE_SECURE_SETTINGS + ) == PERMISSION_GRANTED + ) { + // Disable automatic revoking of ADB pairings + SettingsUtils.putGlobalSetting( + ctx, + "adb_allowed_connection_time", + 0 + ) + + // Enable USB debugging so the Shell user can keep running in the background + // even when disconnected from the WiFi network + SettingsUtils.putGlobalSetting( + ctx, + "adb_enabled", + 1 + ) + } + } + + override fun startWithRoot() { + if (startJob?.isActive == true) { + Timber.i("System Bridge is already starting") + return + } + + startJob = coroutineScope.launch { + starter.startWithRoot() + } + } + + override fun startWithShizuku() { + starter.startWithShizuku() + } +} + +@SuppressLint("ObsoleteSdkInt") +@RequiresApi(Build.VERSION_CODES.Q) +interface SystemBridgeConnectionManager { + val connectionState: StateFlow + + fun run(block: (ISystemBridge) -> T): KMResult + fun stopSystemBridge() + + fun startWithRoot() + fun startWithShizuku() + fun startWithAdb() +} \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionState.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionState.kt new file mode 100644 index 0000000000..76cfdc8ee6 --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/manager/SystemBridgeConnectionState.kt @@ -0,0 +1,19 @@ +package io.github.sds100.keymapper.sysbridge.manager + +sealed class SystemBridgeConnectionState() { + /** + * The time that this connection state was created. This uses SystemClock.elapsedRealtime() + */ + abstract val time: Long + + data class Connected(override val time: Long) : SystemBridgeConnectionState() + + data class Disconnected( + override val time: Long, + /** + * Whether the disconnection was expected. E.g the user stopped it or the app is + * opening for the first time + */ + val isExpected: Boolean + ) : SystemBridgeConnectionState() +} \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/BinderContainer.java b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/BinderContainer.java new file mode 100644 index 0000000000..b14a24670e --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/BinderContainer.java @@ -0,0 +1,44 @@ +package io.github.sds100.keymapper.sysbridge.provider; + +import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.RestrictTo; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; + +@RestrictTo(LIBRARY_GROUP_PREFIX) +public class BinderContainer implements Parcelable { + + public static final Creator CREATOR = new Creator() { + @Override + public BinderContainer createFromParcel(Parcel source) { + return new BinderContainer(source); + } + + @Override + public BinderContainer[] newArray(int size) { + return new BinderContainer[size]; + } + }; + public IBinder binder; + + public BinderContainer(IBinder binder) { + this.binder = binder; + } + + protected BinderContainer(Parcel in) { + this.binder = in.readStrongBinder(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeStrongBinder(this.binder); + } +} diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/SystemBridgeBinderProvider.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/SystemBridgeBinderProvider.kt new file mode 100644 index 0000000000..05fbad5610 --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/provider/SystemBridgeBinderProvider.kt @@ -0,0 +1,114 @@ +package io.github.sds100.keymapper.sysbridge.provider + +import android.content.ContentProvider +import android.content.ContentValues +import android.database.Cursor +import android.net.Uri +import android.os.Bundle +import androidx.core.os.BundleCompat +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.android.EntryPointAccessors +import dagger.hilt.components.SingletonComponent +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManagerImpl + +/** + * Taken from the ShizukuProvider class. + * + * This provider receives the Binder from the system bridge. When app process starts, + * the system bridge (it runs under adb/root) will send the binder to client apps with this provider. + */ +internal class SystemBridgeBinderProvider : ContentProvider() { + companion object { + // For receive Binder from Shizuku + const val METHOD_SEND_BINDER: String = "sendBinder" + + const val EXTRA_BINDER = "io.github.sds100.keymapper.sysbridge.EXTRA_BINDER" + } + + private val systemBridgeManager: SystemBridgeConnectionManagerImpl by lazy { + val appContext = context?.applicationContext ?: throw IllegalStateException() + val hiltEntryPoint = + EntryPointAccessors.fromApplication( + appContext, + SystemBridgeProviderEntryPoint::class.java + ) + + hiltEntryPoint.systemBridgeManager() + } + + override fun onCreate(): Boolean { + return true + } + + override fun call(method: String, arg: String?, extras: Bundle?): Bundle? { + if (extras == null) { + return null + } + + extras.classLoader = BinderContainer::class.java.getClassLoader() + + val reply = Bundle() + + when (method) { + METHOD_SEND_BINDER -> { + handleSendBinder(extras) + } + } + + return reply + } + + private fun handleSendBinder(extras: Bundle) { + if (systemBridgeManager.pingBinder()) { + return + } + + val container: BinderContainer? = BundleCompat.getParcelable( + extras, EXTRA_BINDER, + BinderContainer::class.java + ) + + if (container != null && container.binder != null) { + systemBridgeManager.onBinderReceived(container.binder) + } + } + + // no other provider methods + override fun query( + uri: Uri, + projection: Array?, + selection: String?, + selectionArgs: Array?, + sortOrder: String? + ): Cursor? { + return null + } + + override fun getType(uri: Uri): String? { + return null + } + + override fun insert(uri: Uri, values: ContentValues?): Uri? { + return null + } + + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { + return 0 + } + + override fun update( + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array? + ): Int { + return 0 + } + + @EntryPoint + @InstallIn(SingletonComponent::class) + interface SystemBridgeProviderEntryPoint { + fun systemBridgeManager(): SystemBridgeConnectionManagerImpl + } +} \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt new file mode 100644 index 0000000000..7dc83038a1 --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridge.kt @@ -0,0 +1,579 @@ +package io.github.sds100.keymapper.sysbridge.service + +import android.annotation.SuppressLint +import android.bluetooth.IBluetoothManager +import android.content.AttributionSource +import android.content.Context +import android.content.IContentProvider +import android.content.pm.ApplicationInfo +import android.content.pm.IPackageManager +import android.content.pm.PackageManager +import android.hardware.input.IInputManager +import android.net.IConnectivityManager +import android.net.wifi.IWifiManager +import android.nfc.INfcAdapter +import android.nfc.NfcAdapterApis +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.os.IBinder +import android.os.Looper +import android.os.Process +import android.os.ServiceManager +import android.permission.IPermissionManager +import android.permission.PermissionManagerApis +import android.util.Log +import android.view.InputEvent +import com.android.internal.telephony.ITelephony +import io.github.sds100.keymapper.common.models.EvdevDeviceHandle +import io.github.sds100.keymapper.common.utils.UserHandleUtils +import io.github.sds100.keymapper.sysbridge.IEvdevCallback +import io.github.sds100.keymapper.sysbridge.ISystemBridge +import io.github.sds100.keymapper.sysbridge.provider.BinderContainer +import io.github.sds100.keymapper.sysbridge.provider.SystemBridgeBinderProvider +import io.github.sds100.keymapper.sysbridge.utils.IContentProviderUtils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import rikka.hidden.compat.ActivityManagerApis +import rikka.hidden.compat.DeviceIdleControllerApis +import rikka.hidden.compat.PackageManagerApis +import rikka.hidden.compat.UserManagerApis +import rikka.hidden.compat.adapter.ProcessObserverAdapter +import kotlin.system.exitProcess + + +@SuppressLint("LogNotTimber") +internal class SystemBridge : ISystemBridge.Stub() { + + external fun grabEvdevDeviceNative(devicePath: String): Boolean + + external fun ungrabEvdevDeviceNative(devicePath: String): Boolean + external fun ungrabAllEvdevDevicesNative(): Boolean + external fun writeEvdevEventNative( + devicePath: String, + type: Int, + code: Int, + value: Int + ): Boolean + + external fun getEvdevDevicesNative(): Array + + external fun startEvdevEventLoop(callback: IBinder) + external fun stopEvdevEventLoop() + + companion object { + private const val TAG: String = "KeyMapperSystemBridge" + private val systemBridgePackageName: String? = + System.getProperty("keymapper_sysbridge.package") + + private val systemBridgeVersionCode: Int = + System.getProperty("keymapper_sysbridge.version_code")!!.toInt() + + private const val KEYMAPPER_CHECK_INTERVAL_MS = 60 * 1000L // 1 minute + private const val DATA_ENABLED_REASON_USER: Int = 0 + + @JvmStatic + fun main(args: Array) { + @Suppress("DEPRECATION") + Looper.prepareMainLooper() + SystemBridge() + Looper.loop() + } + + private fun waitSystemService(name: String?) { + var count = 0 + + while (ServiceManager.getService(name) == null) { + if (count == 5) { + throw IllegalStateException("Failed to get $name system service") + } + + try { + Thread.sleep(1000) + count++ + } catch (e: InterruptedException) { + Log.w(TAG, e.message, e) + } + } + } + } + + private val processObserver = object : ProcessObserverAdapter() { + + // This is used as a proxy for detecting the Key Mapper process has started. + // It is called when ANY foreground activities check so don't execute anything + // long running. + override fun onForegroundActivitiesChanged( + pid: Int, + uid: Int, + foregroundActivities: Boolean + ) { + if (evdevCallback?.asBinder()?.pingBinder() != true) { + evdevCallbackDeathRecipient.binderDied() + } + + // Do not send the binder if the app is not in the foreground. + if (!foregroundActivities) { + return + } + + if (getKeyMapperPackageInfo() == null) { + Log.i(TAG, "Key Mapper app not installed - exiting") + destroy() + } else { + synchronized(sendBinderLock) { + if (evdevCallback == null) { + Log.i(TAG, "Key Mapper process started, send binder to app") + mainHandler.post { + sendBinderToApp() + } + } + } + } + } + } + + private val sendBinderLock: Any = Any() + + private val coroutineScope: CoroutineScope = MainScope() + private val mainHandler = Handler(Looper.myLooper()!!) + + private val keyMapperCheckLock: Any = Any() + private var keyMapperCheckJob: Job? = null + + private val evdevCallbackLock: Any = Any() + private var evdevCallback: IEvdevCallback? = null + private val evdevCallbackDeathRecipient: IBinder.DeathRecipient = IBinder.DeathRecipient { + Log.i(TAG, "EvdevCallback binder died") + evdevCallback = null + + coroutineScope.launch(Dispatchers.Default) { + stopEvdevEventLoop() + } + + // Start periodic check for Key Mapper installation + startKeyMapperPeriodicCheck() + } + + private val inputManager: IInputManager + private val wifiManager: IWifiManager? + private val permissionManager: IPermissionManager + private val telephonyManager: ITelephony? + private val packageManager: IPackageManager + private val bluetoothManager: IBluetoothManager? + private val nfcAdapter: INfcAdapter? + private val connectivityManager: IConnectivityManager? + + private val processPackageName: String = when (Process.myUid()) { + Process.ROOT_UID -> "root" + Process.SHELL_UID -> "com.android.shell" + else -> throw IllegalStateException("SystemBridge must run as root or shell user") + } + + init { + val libraryPath = System.getProperty("keymapper_sysbridge.library.path") + @SuppressLint("UnsafeDynamicallyLoadedCode") + System.load("$libraryPath/libevdev.so") + + Log.i(TAG, "SystemBridge starting... Version code $versionCode") + + waitSystemService("package") + packageManager = IPackageManager.Stub.asInterface(ServiceManager.getService("package")) + + waitSystemService(Context.ACTIVITY_SERVICE) + waitSystemService(Context.USER_SERVICE) + waitSystemService(Context.APP_OPS_SERVICE) + waitSystemService("permissionmgr") + permissionManager = + IPermissionManager.Stub.asInterface(ServiceManager.getService("permissionmgr")) + + waitSystemService(Context.INPUT_SERVICE) + inputManager = + IInputManager.Stub.asInterface(ServiceManager.getService(Context.INPUT_SERVICE)) + + if (hasSystemFeature(PackageManager.FEATURE_WIFI)) { + waitSystemService(Context.WIFI_SERVICE) + wifiManager = + IWifiManager.Stub.asInterface(ServiceManager.getService(Context.WIFI_SERVICE)) + } else { + wifiManager = null + } + + if (hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) { + waitSystemService(Context.TELEPHONY_SERVICE) + telephonyManager = + ITelephony.Stub.asInterface(ServiceManager.getService(Context.TELEPHONY_SERVICE)) + } else { + telephonyManager = null + } + + if (hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)) { + waitSystemService("bluetooth_manager") + bluetoothManager = + IBluetoothManager.Stub.asInterface(ServiceManager.getService("bluetooth_manager")) + } else { + bluetoothManager = null + } + + if (hasSystemFeature(PackageManager.FEATURE_NFC)) { + waitSystemService(Context.NFC_SERVICE) + nfcAdapter = + INfcAdapter.Stub.asInterface(ServiceManager.getService(Context.NFC_SERVICE)) + } else { + nfcAdapter = null + } + + waitSystemService(Context.CONNECTIVITY_SERVICE) + connectivityManager = + IConnectivityManager.Stub.asInterface(ServiceManager.getService(Context.CONNECTIVITY_SERVICE)) + + val applicationInfo = getKeyMapperPackageInfo() + + if (applicationInfo == null) { + destroy() + } + + ActivityManagerApis.registerProcessObserver(processObserver) + + // Try sending the binder to the app when its started. + mainHandler.post { + sendBinderToApp() + } + + Log.i(TAG, "SystemBridge started complete. Version code $versionCode") + } + + private fun hasSystemFeature(name: String): Boolean { + return packageManager.hasSystemFeature(name, 0) + } + + private fun getKeyMapperPackageInfo(): ApplicationInfo? = + PackageManagerApis.getApplicationInfoNoThrow(systemBridgePackageName, 0, 0) + + private fun startKeyMapperPeriodicCheck() { + synchronized(keyMapperCheckLock) { + keyMapperCheckJob?.cancel() + + Log.i(TAG, "Starting periodic Key Mapper installation check") + + keyMapperCheckJob = coroutineScope.launch(Dispatchers.Default) { + try { + while (true) { + if (getKeyMapperPackageInfo() == null) { + Log.i(TAG, "Key Mapper not installed - exiting") + destroy() + break + } else { + // While Key Mapper is still installed but not bound, then periodically + // check if it has uninstalled + delay(KEYMAPPER_CHECK_INTERVAL_MS) + } + } + } finally { + } + } + } + } + + private fun stopKeyMapperPeriodicCheck() { + synchronized(keyMapperCheckLock) { + keyMapperCheckJob?.cancel() + keyMapperCheckJob = null + Log.i(TAG, "Stopped periodic Key Mapper installation check") + } + } + + override fun destroy() { + Log.i(TAG, "SystemBridge destroyed") + + // Must be last line in this method because it halts the JVM. + exitProcess(0) + } + + override fun registerEvdevCallback(callback: IEvdevCallback?) { + callback ?: return + + Log.i(TAG, "Register evdev callback") + + // Stop periodic check since Key Mapper has reconnected + stopKeyMapperPeriodicCheck() + + val binder = callback.asBinder() + + if (this.evdevCallback != null) { + unregisterEvdevCallback() + } + + synchronized(evdevCallbackLock) { + this.evdevCallback = callback + binder.linkToDeath(evdevCallbackDeathRecipient, 0) + } + + coroutineScope.launch(Dispatchers.IO) { + mainHandler.post { + startEvdevEventLoop(binder) + } + } + } + + override fun unregisterEvdevCallback() { + synchronized(evdevCallbackLock) { + evdevCallback?.asBinder()?.unlinkToDeath(evdevCallbackDeathRecipient, 0) + evdevCallback = null + stopEvdevEventLoop() + } + } + + override fun grabEvdevDevice(devicePath: String?): Boolean { + devicePath ?: return false + return grabEvdevDeviceNative(devicePath) + } + + override fun grabEvdevDeviceArray(devicePath: Array?): Boolean { + devicePath ?: return false + + for (path in devicePath) { + Log.i(TAG, "Grabbing evdev device $path") + grabEvdevDeviceNative(path) + + } + + return true + } + + override fun ungrabEvdevDevice(devicePath: String?): Boolean { + devicePath ?: return false + ungrabEvdevDeviceNative(devicePath) + return true + } + + override fun ungrabAllEvdevDevices(): Boolean { + ungrabAllEvdevDevicesNative() + return true + } + + override fun injectInputEvent(event: InputEvent?, mode: Int): Boolean { + return inputManager.injectInputEvent(event, mode) + } + + override fun getEvdevInputDevices(): Array? { + return getEvdevDevicesNative() + } + + override fun setWifiEnabled(enable: Boolean): Boolean { + if (wifiManager == null) { + throw UnsupportedOperationException("WiFi not supported") + } + + return wifiManager.setWifiEnabled(processPackageName, enable) + } + + override fun writeEvdevEvent(devicePath: String?, type: Int, code: Int, value: Int): Boolean { + devicePath ?: return false + return writeEvdevEventNative(devicePath, type, code, value) + } + + override fun getProcessUid(): Int { + return Process.myUid() + } + + override fun grantPermission(permission: String?, deviceId: Int) { + val userId = UserHandleUtils.getCallingUserId() + + PermissionManagerApis.grantPermission( + permissionManager, + systemBridgePackageName ?: return, + permission ?: return, + deviceId, + userId + ) + } + + private fun sendBinderToApp(): Boolean { + // Only support Key Mapper running in a single Android user for now so just send + // it to the first user that accepts the binder. + for (userId in UserManagerApis.getUserIdsNoThrow()) { + if (sendBinderToAppInUser(userId)) { + return true + } + } + + return false + } + + /** + * @return Whether it was sent successfully with a reply from the app. + */ + private fun sendBinderToAppInUser(userId: Int): Boolean { + try { + DeviceIdleControllerApis.addPowerSaveTempWhitelistApp( + systemBridgePackageName, + 30 * 1000, + userId, + 316, /* PowerExemptionManager#REASON_SHELL */"shell" + ) + } catch (tr: Throwable) { + Log.e(TAG, tr.toString()) + } + + val providerName = "$systemBridgePackageName.sysbridge" + var provider: IContentProvider? = null + + val token: IBinder? = null + + try { + provider = ActivityManagerApis.getContentProviderExternal( + providerName, + userId, + token, + providerName + ) + if (provider == null) { + Log.e(TAG, "provider is null $providerName $userId") + return false + } + + if (!provider.asBinder().pingBinder()) { + Log.e(TAG, "provider is dead $providerName $userId") + return false + } + + val extra = Bundle() + extra.putParcelable( + SystemBridgeBinderProvider.EXTRA_BINDER, + BinderContainer(this) + ) + + val reply: Bundle? = IContentProviderUtils.callCompat( + provider, + null, + providerName, + "sendBinder", + null, + extra + ) + if (reply != null) { + Log.i(TAG, "Send binder to user app $systemBridgePackageName in user $userId") + // Stop periodic check since connection is successful + stopKeyMapperPeriodicCheck() + return true + } else { + Log.w( + TAG, + "Failed to send binder to user app $systemBridgePackageName in user $userId" + ) + } + } catch (tr: Throwable) { + Log.e( + TAG, + "Failed to send binder to user app $systemBridgePackageName in user $userId", + tr + ) + } finally { + if (provider != null) { + try { + ActivityManagerApis.removeContentProviderExternal(providerName, token) + } catch (tr: Throwable) { + Log.w(TAG, "Failed to remove content provider $providerName", tr) + } + } + } + + return false + } + + override fun executeCommand(command: String?): String { + command ?: throw IllegalArgumentException("command is null") + + Log.i(TAG, "Executing command: $command") + + val process = Runtime.getRuntime().exec(command) + + val out = with(process.inputStream.bufferedReader()) { + readText() + } + + val err = with(process.errorStream.bufferedReader()) { + readText() + } + + process.waitFor() + + return "$out\n$err" + } + + override fun getVersionCode(): Int { + return systemBridgeVersionCode + } + + override fun setDataEnabled(subId: Int, enable: Boolean) { + if (telephonyManager == null) { + throw UnsupportedOperationException("Telephony not supported") + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + telephonyManager.setDataEnabledForReason( + subId, + DATA_ENABLED_REASON_USER, + enable, + processPackageName + ) + } else { + telephonyManager.setUserDataEnabled(subId, enable) + } + } + + override fun setBluetoothEnabled(enable: Boolean) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + throw UnsupportedOperationException("Bluetooth enable/disable requires Android 12 or higher. Otherwise use the SDK's BluetoothAdapter which allows enable/disable.") + } + + if (bluetoothManager == null) { + throw UnsupportedOperationException("Bluetooth not supported") + } + + val attributionSourceBuilder = AttributionSource.Builder(Process.myUid()) + .setAttributionTag("KeyMapperSystemBridge") + .setPackageName(processPackageName) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + attributionSourceBuilder.setPid(Process.myPid()) + } + + val attributionSource = attributionSourceBuilder.build() + + if (enable) { + bluetoothManager.enable(attributionSource) + } else { + bluetoothManager.disable(attributionSource, true) + } + } + + override fun setNfcEnabled(enable: Boolean) { + if (nfcAdapter == null) { + throw UnsupportedOperationException("NFC not supported") + } + + if (enable) { + NfcAdapterApis.enable(nfcAdapter, processPackageName) + } else { + NfcAdapterApis.disable( + adapter = nfcAdapter, + saveState = true, + packageName = processPackageName + ) + } + } + + override fun setAirplaneMode(enable: Boolean) { + if (connectivityManager == null) { + throw UnsupportedOperationException("ConnectivityManager not supported") + } + + connectivityManager.setAirplaneMode(enable) + } +} \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt new file mode 100644 index 0000000000..a613f939f8 --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupController.kt @@ -0,0 +1,349 @@ +package io.github.sds100.keymapper.sysbridge.service + +import android.Manifest +import android.annotation.SuppressLint +import android.app.ActivityManager +import android.content.ActivityNotFoundException +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import android.provider.Settings +import android.service.quicksettings.TileService +import androidx.annotation.RequiresApi +import androidx.annotation.RequiresPermission +import androidx.core.content.ContextCompat +import androidx.core.content.getSystemService +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.sds100.keymapper.common.KeyMapperClassProvider +import io.github.sds100.keymapper.common.utils.KMResult +import io.github.sds100.keymapper.common.utils.SettingsUtils +import io.github.sds100.keymapper.common.utils.isSuccess +import io.github.sds100.keymapper.common.utils.onSuccess +import io.github.sds100.keymapper.sysbridge.adb.AdbManager +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import kotlinx.coroutines.withTimeoutOrNull +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class SystemBridgeSetupControllerImpl @Inject constructor( + @ApplicationContext private val ctx: Context, + private val coroutineScope: CoroutineScope, + private val adbManager: AdbManager, + private val keyMapperClassProvider: KeyMapperClassProvider, + private val connectionManager: SystemBridgeConnectionManager, +) : SystemBridgeSetupController { + + companion object { + private const val DEVELOPER_OPTIONS_SETTING = "development_settings_enabled" + private const val ADB_WIRELESS_SETTING = "adb_wifi_enabled" + } + + private val activityManager: ActivityManager by lazy { ctx.getSystemService()!! } + + override val isDeveloperOptionsEnabled: MutableStateFlow = + MutableStateFlow(getDeveloperOptionsEnabled()) + + override val isWirelessDebuggingEnabled: MutableStateFlow = + MutableStateFlow(getWirelessDebuggingEnabled()) + + // Use a SharedFlow so that the same value can be emitted repeatedly. + override val setupAssistantStep: MutableSharedFlow = MutableSharedFlow() + private val setupAssistantStepState = + setupAssistantStep.stateIn(coroutineScope, SharingStarted.Eagerly, null) + + private val isAdbPairedResult: MutableStateFlow = MutableStateFlow(null) + private var isAdbPairedJob: Job? = null + + private var autoStartJob: Job? = null + + init { + // Automatically go back to the Key Mapper app when turning on wireless debugging + coroutineScope.launch { + val uri = Settings.Global.getUriFor(ADB_WIRELESS_SETTING) + SettingsUtils.settingsCallbackFlow(ctx, uri).collect { + isWirelessDebuggingEnabled.update { getWirelessDebuggingEnabled() } + + // Only go back if the user is currently setting up the wireless debugging step. + // This stops Key Mapper going back if they are turning on wireless debugging + // for another reason. + if (isWirelessDebuggingEnabled.value && setupAssistantStepState.value == SystemBridgeSetupStep.WIRELESS_DEBUGGING) { + getKeyMapperAppTask()?.moveToFront() + } + } + } + + coroutineScope.launch { + val uri = Settings.Global.getUriFor(DEVELOPER_OPTIONS_SETTING) + SettingsUtils.settingsCallbackFlow(ctx, uri).collect { + isDeveloperOptionsEnabled.update { getDeveloperOptionsEnabled() } + + if (isDeveloperOptionsEnabled.value && setupAssistantStepState.value == SystemBridgeSetupStep.DEVELOPER_OPTIONS) { + getKeyMapperAppTask()?.moveToFront() + } + } + } + } + + override fun startWithRoot() { + coroutineScope.launch { + connectionManager.startWithRoot() + } + } + + override fun startWithShizuku() { + connectionManager.startWithShizuku() + } + + /** + * If Key Mapper has WRITE_SECURE_SETTINGS permission then it can turn on wireless debugging + * and ADB and then start the system bridge. + */ + @RequiresApi(Build.VERSION_CODES.R) + override fun autoStartWithAdb() { + autoStartJob?.cancel() + + autoStartJob = coroutineScope.launch { + if (!canWriteGlobalSettings()) { + Timber.w("Cannot auto start with ADB. WRITE_SECURE_SETTINGS permission not granted") + return@launch + } + + if (connectionManager.connectionState.value !is SystemBridgeConnectionState.Disconnected) { + Timber.w("Not auto starting. System Bridge is already connected.") + return@launch + } + + SettingsUtils.putGlobalSetting(ctx, DEVELOPER_OPTIONS_SETTING, 1) + + try { + withTimeout(5000L) { isDeveloperOptionsEnabled.first { it } } + } catch (_: TimeoutCancellationException) { + return@launch + } + + if (isAdbPaired()) { + // This is IMPORTANT. First turn on ADB before enabling wireless debugging because + // turning on developer options just before can cause the Shell to be killed once + // the system bridge is started. + SettingsUtils.putGlobalSetting(ctx, Settings.Global.ADB_ENABLED, 1) + SettingsUtils.putGlobalSetting(ctx, ADB_WIRELESS_SETTING, 1) + + // Wait for wireless debugging to be enabled before starting with ADB + try { + withTimeout(5000L) { isWirelessDebuggingEnabled.first { it } } + } catch (_: TimeoutCancellationException) { + return@launch + } + + startWithAdb() + + // Wait for the service to connect before turning off wireless debugging + withTimeoutOrNull(5000L) { + connectionManager.connectionState + .filterIsInstance() + .first() + } + + // Disable wireless debugging when done + SettingsUtils.putGlobalSetting(ctx, ADB_WIRELESS_SETTING, 0) + } else { + Timber.e("Autostart failed. ADB not paired successfully.") + } + } + } + + @RequiresApi(Build.VERSION_CODES.R) + override fun startWithAdb() { + connectionManager.startWithAdb() + } + + @RequiresApi(Build.VERSION_CODES.R) + override fun launchPairingAssistant() { + launchWirelessDebuggingActivity() + + coroutineScope.launch { + setupAssistantStep.emit(SystemBridgeSetupStep.ADB_PAIRING) + } + } + + @RequiresApi(Build.VERSION_CODES.R) + override suspend fun pairWirelessAdb(code: String): KMResult { + return adbManager.pair(code).onSuccess { + // Clear the step if still at the pairing step. + if (setupAssistantStepState.value == SystemBridgeSetupStep.ADB_PAIRING) { + setupAssistantStep.emit(null) + } + } + } + + override fun enableDeveloperOptions() { + if (canWriteGlobalSettings()) { + SettingsUtils.putGlobalSetting(ctx, DEVELOPER_OPTIONS_SETTING, 1) + } else { + SettingsUtils.launchSettingsScreen( + ctx, + Settings.ACTION_DEVICE_INFO_SETTINGS, + "build_number" + ) + + coroutineScope.launch { + setupAssistantStep.emit(SystemBridgeSetupStep.DEVELOPER_OPTIONS) + } + } + } + + override fun enableWirelessDebugging() { + if (canWriteGlobalSettings()) { + SettingsUtils.putGlobalSetting(ctx, ADB_WIRELESS_SETTING, 1) + } else { + // This is the intent sent by the quick settings tile. Not all devices support this. + launchWirelessDebuggingActivity() + + coroutineScope.launch { + setupAssistantStep.emit(SystemBridgeSetupStep.WIRELESS_DEBUGGING) + } + } + } + + @RequiresApi(Build.VERSION_CODES.R) + override suspend fun isAdbPaired(): Boolean { + // Sometimes multiple calls to this function can happen in a short space of time + // so only run one job to check whether it is paired. + if (isAdbPairedJob == null || isAdbPairedJob?.isCompleted == true) { + isAdbPairedJob?.cancel() + isAdbPairedResult.value = null + + isAdbPairedJob = coroutineScope.launch { + if (!getWirelessDebuggingEnabled()) { + SettingsUtils.putGlobalSetting(ctx, ADB_WIRELESS_SETTING, 1) + } + + // Try running a command to see if the pairing is working correctly. + isAdbPairedResult.value = adbManager.executeCommand("sh").isSuccess + } + } + + // Wait for the next result + return isAdbPairedResult.filterNotNull().first() + } + + /** + * @return whether it opened the wireless debugging activity successfully. If it is + * false then developer options was launched. + */ + private fun launchWirelessDebuggingActivity(): Boolean { + val quickSettingsIntent = Intent(TileService.ACTION_QS_TILE_PREFERENCES).apply { + // Set the package name because this action can also resolve to a "Permission Controller" activity. + val packageName = "com.android.settings" + setPackage(packageName) + + putExtra( + Intent.EXTRA_COMPONENT_NAME, + ComponentName( + packageName, + "com.android.settings.development.qstile.DevelopmentTiles\$WirelessDebugging" + ) + ) + + addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_NO_HISTORY or + Intent.FLAG_ACTIVITY_CLEAR_TASK or + Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS + ) + } + + try { + ctx.startActivity(quickSettingsIntent) + return true + } catch (_: ActivityNotFoundException) { + SettingsUtils.launchSettingsScreen( + ctx, + Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS, + "toggle_adb_wireless" + ) + + return false + } + } + + fun invalidateSettings() { + isDeveloperOptionsEnabled.update { getDeveloperOptionsEnabled() } + isWirelessDebuggingEnabled.update { getWirelessDebuggingEnabled() } + } + + private fun getDeveloperOptionsEnabled(): Boolean { + try { + return SettingsUtils.getGlobalSetting(ctx, DEVELOPER_OPTIONS_SETTING) == 1 + } catch (_: Settings.SettingNotFoundException) { + return false + } + } + + private fun getWirelessDebuggingEnabled(): Boolean { + try { + return SettingsUtils.getGlobalSetting(ctx, ADB_WIRELESS_SETTING) == 1 + } catch (_: Settings.SettingNotFoundException) { + return false + } + } + + private fun getKeyMapperAppTask(): ActivityManager.AppTask? { + val task = activityManager.appTasks + .firstOrNull { it.taskInfo.topActivity?.className == keyMapperClassProvider.getMainActivity().name } + return task + } + + private fun canWriteGlobalSettings(): Boolean { + return ContextCompat.checkSelfPermission( + ctx, + Manifest.permission.WRITE_SECURE_SETTINGS + ) == PackageManager.PERMISSION_GRANTED + } +} + +@SuppressLint("ObsoleteSdkInt") +@RequiresApi(Build.VERSION_CODES.Q) +interface SystemBridgeSetupController { + val setupAssistantStep: Flow + + val isDeveloperOptionsEnabled: Flow + fun enableDeveloperOptions() + + val isWirelessDebuggingEnabled: Flow + fun enableWirelessDebugging() + + fun launchPairingAssistant() + + @RequiresApi(Build.VERSION_CODES.R) + suspend fun isAdbPaired(): Boolean + + @RequiresApi(Build.VERSION_CODES.R) + suspend fun pairWirelessAdb(code: String): KMResult + + fun startWithRoot() + fun startWithShizuku() + fun startWithAdb() + + @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS) + fun autoStartWithAdb() +} \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupStep.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupStep.kt new file mode 100644 index 0000000000..395369036e --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/service/SystemBridgeSetupStep.kt @@ -0,0 +1,12 @@ +package io.github.sds100.keymapper.sysbridge.service + +enum class SystemBridgeSetupStep(val stepIndex: Int) { + ACCESSIBILITY_SERVICE(stepIndex = 0), + NOTIFICATION_PERMISSION(stepIndex = 1), + DEVELOPER_OPTIONS(stepIndex = 2), + WIFI_NETWORK(stepIndex = 3), + WIRELESS_DEBUGGING(stepIndex = 4), + ADB_PAIRING(stepIndex = 5), + START_SERVICE(stepIndex = 6), + STARTED(stepIndex = 7) +} \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/shizuku/ShizukuStarterService.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/shizuku/ShizukuStarterService.kt new file mode 100644 index 0000000000..9797ebb4a7 --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/shizuku/ShizukuStarterService.kt @@ -0,0 +1,38 @@ +package io.github.sds100.keymapper.sysbridge.shizuku + +import android.annotation.SuppressLint +import android.util.Log +import io.github.sds100.keymapper.sysbridge.IShizukuStarterService +import kotlin.system.exitProcess + +@SuppressLint("LogNotTimber") +class ShizukuStarterService : IShizukuStarterService.Stub() { + companion object { + private val TAG = "ShizukuStarterService" + } + + override fun destroy() { + Log.i(TAG, "ShizukuStarterService destroyed") + + // Must be last line in this method because it halts the JVM. + exitProcess(0) + } + + override fun executeCommand(command: String?): String? { + command ?: return null + + val process = Runtime.getRuntime().exec(command) + + val out = with(process.inputStream.bufferedReader()) { + readText() + } + + val err = with(process.errorStream.bufferedReader()) { + readText() + } + + process.waitFor() + + return "$out\n$err" + } +} \ No newline at end of file diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt new file mode 100644 index 0000000000..4d7abbcda7 --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/starter/SystemBridgeStarter.kt @@ -0,0 +1,310 @@ +package io.github.sds100.keymapper.sysbridge.starter + +import android.content.ComponentName +import android.content.Context +import android.content.ServiceConnection +import android.os.Build +import android.os.DeadObjectException +import android.os.IBinder +import android.os.RemoteException +import android.os.UserManager +import android.system.ErrnoException +import android.system.Os +import androidx.annotation.RequiresApi +import com.topjohnwu.superuser.Shell +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.sds100.keymapper.common.BuildConfigProvider +import io.github.sds100.keymapper.common.utils.KMError +import io.github.sds100.keymapper.common.utils.KMResult +import io.github.sds100.keymapper.common.utils.Success +import io.github.sds100.keymapper.common.utils.onFailure +import io.github.sds100.keymapper.common.utils.then +import io.github.sds100.keymapper.sysbridge.BuildConfig +import io.github.sds100.keymapper.sysbridge.IShizukuStarterService +import io.github.sds100.keymapper.sysbridge.R +import io.github.sds100.keymapper.sysbridge.adb.AdbManager +import io.github.sds100.keymapper.sysbridge.ktx.loge +import io.github.sds100.keymapper.sysbridge.shizuku.ShizukuStarterService +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import rikka.core.os.FileUtils +import rikka.shizuku.Shizuku +import timber.log.Timber +import java.io.BufferedReader +import java.io.ByteArrayInputStream +import java.io.DataInputStream +import java.io.File +import java.io.FileOutputStream +import java.io.FileWriter +import java.io.IOException +import java.io.InputStreamReader +import java.io.PrintWriter +import java.util.zip.ZipFile +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class SystemBridgeStarter @Inject constructor( + @ApplicationContext private val ctx: Context, + private val adbManager: AdbManager, + private val buildConfigProvider: BuildConfigProvider +) { + private val userManager by lazy { ctx.getSystemService(UserManager::class.java)!! } + + private val apkPath = ctx.applicationInfo.sourceDir + private val libPath = ctx.applicationInfo.nativeLibraryDir + private val packageName = ctx.applicationInfo.packageName + private val startMutex: Mutex = Mutex() + + private val shizukuStarterConnection: ServiceConnection = object : ServiceConnection { + override fun onServiceConnected( + name: ComponentName?, + binder: IBinder? + ) { + Timber.i("Shizuku starter service connected") + + val service = IShizukuStarterService.Stub.asInterface(binder) + + Timber.i("Starting System Bridge with Shizuku starter service") + try { + runBlocking { + startSystemBridge(executeCommand = { command -> + val output = service.executeCommand(command) + + if (output == null) { + KMError.UnknownIOError + } else { + Success(output) + } + }) + } + + } catch (e: RemoteException) { + Timber.e("Exception starting with Shizuku starter service: $e") + } finally { + try { + service.destroy() + } catch (_: DeadObjectException) { + // Do nothing. Service is already dead. + } + } + } + + override fun onServiceDisconnected(name: ComponentName?) { + // Do nothing. The service is supposed to immediately kill itself + // after starting the command. + } + } + + fun startWithShizuku() { + if (!Shizuku.pingBinder()) { + Timber.w("Shizuku is not running. Cannot start System Bridge with Shizuku.") + return + } + + // Shizuku will start a service which will then start the System Bridge. Shizuku won't be + // used to start the System Bridge directly because native libraries need to be used + // and we want to limit the dependency on Shizuku as much as possible. Also, the System + // Bridge should still be running even if Shizuku dies. + val serviceComponentName = ComponentName(ctx, ShizukuStarterService::class.java) + val args = Shizuku.UserServiceArgs(serviceComponentName) + .daemon(false) + .processNameSuffix("service") + .debuggable(BuildConfig.DEBUG) + .version(buildConfigProvider.versionCode) + + try { + Shizuku.bindUserService( + args, + shizukuStarterConnection + ) + } catch (e: Exception) { + Timber.e("Exception when starting System Bridge with Shizuku. $e") + } + } + + @RequiresApi(Build.VERSION_CODES.R) + suspend fun startWithAdb(): KMResult { + if (!userManager.isUserUnlocked) { + return KMError.Exception(IllegalStateException("User is locked")) + } + + return startSystemBridge(executeCommand = adbManager::executeCommand) + .onFailure { error -> + Timber.w("Failed to start system bridge with ADB: $error") + } + } + + suspend fun startWithRoot() { + if (Shell.isAppGrantedRoot() != true) { + Timber.w("Root is not granted. Cannot start System Bridge with Root.") + return + } + + Timber.i("Starting System Bridge with root") + startSystemBridge(executeCommand = { command -> + val output = withContext(Dispatchers.IO) { + Shell.cmd(command).exec() + } + + if (output.isSuccess) { + Success(output.out.plus(output.err).joinToString("\n")) + } else { + KMError.UnknownIOError + } + }) + } + + suspend fun startSystemBridge(executeCommand: suspend (String) -> KMResult): KMResult { + startMutex.withLock { + val externalFilesParent = try { + ctx.getExternalFilesDir(null)?.parentFile + } catch (e: IOException) { + return KMError.UnknownIOError + } + + val outputStarterBinary = File(externalFilesParent, "starter") + val outputStarterScript = File(externalFilesParent, "start.sh") + withContext(Dispatchers.IO) { + copyNativeLibrary(outputStarterBinary) + + // Create the start.sh shell script + writeStarterScript( + outputStarterScript, + outputStarterBinary.absolutePath + ) + } + + val startCommand = + "sh ${outputStarterScript.absolutePath} --apk=$apkPath --lib=$libPath --package=$packageName --version_code=${buildConfigProvider.versionCode}" + + return executeCommand(startCommand).then { output -> + + // Adb on Android 11 has no permission to access Android/data so use /data/user_de. + if (output.contains("/Android/data/${ctx.packageName}/start.sh: Permission denied")) { + Timber.w( + "ADB has no permission to access Android/data/${ctx.packageName}/start.sh. Trying to use /data/user_de instead..." + ) + + startSystemBridgeFromProtectedStorage(executeCommand) + } else { + Success(output) + } + } + } + } + + private suspend fun startSystemBridgeFromProtectedStorage( + executeCommand: suspend (String) -> KMResult + ): KMResult { + val protectedStorageDir = + ctx.createDeviceProtectedStorageContext().filesDir.parentFile + + try { + Os.chmod(protectedStorageDir.absolutePath, 457 /* 0711 */) + } catch (e: ErrnoException) { + e.printStackTrace() + } + + try { + val outputStarterBinary = File(protectedStorageDir, "starter") + val outputStarterScript = File(protectedStorageDir, "start.sh") + + withContext(Dispatchers.IO) { + copyNativeLibrary(outputStarterBinary) + + writeStarterScript( + outputStarterScript, + outputStarterBinary.absolutePath + ) + } + + val startCommand = + "sh ${outputStarterScript.absolutePath} --apk=$apkPath --lib=$libPath --package=$packageName --version_code=${buildConfigProvider.versionCode}" + + // Make starter binary executable + try { + Os.chmod(outputStarterBinary.absolutePath, 420 /* 0644 */) + } catch (e: ErrnoException) { + e.printStackTrace() + } + + // Make starter script executable + try { + Os.chmod(outputStarterScript.absolutePath, 420 /* 0644 */) + } catch (e: ErrnoException) { + e.printStackTrace() + } + + return executeCommand(startCommand) + + } catch (e: IOException) { + loge("write files", e) + return KMError.UnknownIOError + } + } + + /** + * This extracts the library file from inside the apk and copies it to [out] File. + */ + private fun copyNativeLibrary(out: File) { + val expectedLibraryPath = "lib/${Build.SUPPORTED_ABIS[0]}/libsysbridge.so" + + // Open the apk so the library file can be found + with(ZipFile(apkPath)) { + val entries = entries() + + // Loop over all the file entries in the zip file + while (entries.hasMoreElements()) { + val entry = entries.nextElement() ?: break + + if (entry.name != expectedLibraryPath) { + continue + } + + val buf = ByteArray(entry.size.toInt()) + + // Read the native library into the buffer + with(DataInputStream(getInputStream(entry))) { + readFully(buf) + } + + // Copy the buffer to the output file + with(FileOutputStream(out)) { + FileUtils.copy(ByteArrayInputStream(buf), this) + } + + break + } + } + } + + /** + * Write the start.sh shell script to the specified [out] file. The path to the starter + * binary will be substituted in the script with the [starterPath]. + */ + private fun writeStarterScript(out: File, starterPath: String) { + if (!out.exists()) { + out.createNewFile() + } + + val scriptInputStream = ctx.resources.openRawResource(R.raw.start) + + with(scriptInputStream) { + val reader = BufferedReader(InputStreamReader(this)) + + val outputWriter = PrintWriter(FileWriter(out)) + var line: String? + + while (reader.readLine().also { line = it } != null) { + outputWriter.println(line!!.replace("%%%STARTER_PATH%%%", starterPath)) + } + + outputWriter.flush() + outputWriter.close() + } + } +} diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/IContentProviderUtils.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/IContentProviderUtils.kt new file mode 100644 index 0000000000..a32c889554 --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/IContentProviderUtils.kt @@ -0,0 +1,41 @@ +package io.github.sds100.keymapper.sysbridge.utils + +import android.content.AttributionSource +import android.content.IContentProvider +import android.os.Build +import android.os.Bundle + +internal object IContentProviderUtils { + + @Throws(android.os.RemoteException::class) + fun callCompat( + provider: IContentProvider, + callingPkg: String?, + authority: String?, + method: String?, + arg: String?, + extras: Bundle? + ): Bundle? { + val result: Bundle? + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val uid = android.system.Os.getuid() + + result = provider.call( + (AttributionSource.Builder(uid)).setPackageName(callingPkg).build(), + authority, + method, + arg, + extras + ) + } else if (Build.VERSION.SDK_INT >= 30) { + result = + provider.call(callingPkg, null as String?, authority, method, arg, extras) + } else if (Build.VERSION.SDK_INT >= 29) { + result = provider.call(callingPkg, authority, method, arg, extras) + } else { + result = provider.call(callingPkg, method, arg, extras) + } + + return result + } +} diff --git a/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/SystemBridgeResult.kt b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/SystemBridgeResult.kt new file mode 100644 index 0000000000..54418fcb93 --- /dev/null +++ b/sysbridge/src/main/java/io/github/sds100/keymapper/sysbridge/utils/SystemBridgeResult.kt @@ -0,0 +1,7 @@ +package io.github.sds100.keymapper.sysbridge.utils + +import io.github.sds100.keymapper.common.utils.KMError + +sealed class SystemBridgeError : KMError() { + data object Disconnected : SystemBridgeError() +} \ No newline at end of file diff --git a/sysbridge/src/main/res/raw/start.sh b/sysbridge/src/main/res/raw/start.sh new file mode 100644 index 0000000000..49cc606183 --- /dev/null +++ b/sysbridge/src/main/res/raw/start.sh @@ -0,0 +1,52 @@ +#!/system/bin/sh + +SOURCE_PATH="%%%STARTER_PATH%%%" +STARTER_PATH="/data/local/tmp/keymapper_sysbridge_starter" + +echo "info: start.sh begin" + +recreate_tmp() { + echo "info: /data/local/tmp is possible broken, recreating..." + rm -rf /data/local/tmp + mkdir -p /data/local/tmp +} + +broken_tmp() { + echo "fatal: /data/local/tmp is broken, please try reboot the device or manually recreate it..." + exit 1 +} + +if [ -f "$SOURCE_PATH" ]; then + echo "info: attempt to copy starter from $SOURCE_PATH to $STARTER_PATH" + rm -f $STARTER_PATH + + cp "$SOURCE_PATH" $STARTER_PATH + res=$? + if [ $res -ne 0 ]; then + recreate_tmp + cp "$SOURCE_PATH" $STARTER_PATH + + res=$? + if [ $res -ne 0 ]; then + broken_tmp + fi + fi + + chmod 700 $STARTER_PATH + chown 2000 $STARTER_PATH + chgrp 2000 $STARTER_PATH +fi + +if [ -f $STARTER_PATH ]; then + echo "info: exec $STARTER_PATH" + # Pass apk path, library path, package name, version code + $STARTER_PATH "$1" "$2" "$3" "$4" + result=$? + if [ ${result} -ne 0 ]; then + echo "info: keymapper_sysbridge_starter exit with non-zero value $result" + else + echo "info: keymapper_sysbridge_starter exit with 0" + fi +else + echo "Starter file not exist, please open Key Mapper and try again." +fi diff --git a/system/build.gradle.kts b/system/build.gradle.kts index 6c133fa974..b70b399c4d 100644 --- a/system/build.gradle.kts +++ b/system/build.gradle.kts @@ -40,11 +40,10 @@ dependencies { implementation(project(":common")) implementation(project(":data")) implementation(project(":systemstubs")) + implementation(project(":sysbridge")) - // kotlin stuff implementation(libs.kotlinx.coroutines.android) implementation(libs.kotlinx.serialization.json) - implementation(libs.androidx.core.ktx) implementation(libs.jakewharton.timber) implementation(libs.dagger.hilt.android) @@ -58,4 +57,5 @@ dependencies { implementation(libs.rikka.shizuku.provider) implementation(libs.androidx.datastore.preferences) implementation(libs.androidx.preference.ktx) + implementation(libs.github.topjohnwu.libsu) } 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 08d09239a9..39a66a2ee4 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 @@ -52,6 +52,7 @@ import io.github.sds100.keymapper.system.ringtones.RingtoneAdapter import io.github.sds100.keymapper.system.root.SuAdapter import io.github.sds100.keymapper.system.root.SuAdapterImpl import io.github.sds100.keymapper.system.shell.ShellAdapter +import io.github.sds100.keymapper.system.shell.SimpleShell import io.github.sds100.keymapper.system.shizuku.ShizukuAdapter import io.github.sds100.keymapper.system.shizuku.ShizukuAdapterImpl import io.github.sds100.keymapper.system.url.AndroidOpenUrlAdapter @@ -167,7 +168,7 @@ abstract class SystemHiltModule { @Singleton @Binds - abstract fun provideShellAdapter(impl: Shell): ShellAdapter + abstract fun provideShellAdapter(impl: SimpleShell): ShellAdapter @Singleton @Binds diff --git a/system/src/main/java/io/github/sds100/keymapper/system/airplanemode/AirplaneModeAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/airplanemode/AirplaneModeAdapter.kt index 6c38f9f3a4..bc0bfcfc11 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/airplanemode/AirplaneModeAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/airplanemode/AirplaneModeAdapter.kt @@ -4,6 +4,6 @@ import io.github.sds100.keymapper.common.utils.KMResult interface AirplaneModeAdapter { fun isEnabled(): Boolean - fun enable(): KMResult<*> - fun disable(): KMResult<*> + suspend fun enable(): KMResult<*> + suspend fun disable(): KMResult<*> } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/airplanemode/AndroidAirplaneModeAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/airplanemode/AndroidAirplaneModeAdapter.kt index df7c4a7c5c..d44b2cde60 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/airplanemode/AndroidAirplaneModeAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/airplanemode/AndroidAirplaneModeAdapter.kt @@ -1,31 +1,52 @@ package io.github.sds100.keymapper.system.airplanemode import android.content.Context +import android.os.Build import android.provider.Settings +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult -import io.github.sds100.keymapper.common.utils.onSuccess -import io.github.sds100.keymapper.system.SettingsUtils +import io.github.sds100.keymapper.common.utils.SettingsUtils +import io.github.sds100.keymapper.common.utils.Success +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager import io.github.sds100.keymapper.system.root.SuAdapter -import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import javax.inject.Singleton @Singleton class AndroidAirplaneModeAdapter @Inject constructor( - @ApplicationContext private val context: Context, - val suAdapter: SuAdapter, + @ApplicationContext private val ctx: Context, + private val systemBridgeConnectionManager: SystemBridgeConnectionManager, + private val suAdapter: SuAdapter ) : AirplaneModeAdapter { - private val ctx = context.applicationContext - override fun enable(): KMResult<*> = - suAdapter.execute("settings put global airplane_mode_on 1").onSuccess { - broadcastAirplaneModeChanged(false) + override suspend fun enable(): KMResult<*> { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + systemBridgeConnectionManager.run { bridge -> bridge.setAirplaneMode(true) } + } else { + val success = SettingsUtils.putGlobalSetting(ctx, Settings.Global.AIRPLANE_MODE_ON, 1) + broadcastAirplaneModeChanged(true) + if (success) { + Success(Unit) + } else { + KMError.FailedToModifySystemSetting(Settings.Global.AIRPLANE_MODE_ON) + } } + } - override fun disable(): KMResult<*> = - suAdapter.execute("settings put global airplane_mode_on 0").onSuccess { - broadcastAirplaneModeChanged(false) + override suspend fun disable(): KMResult<*> { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + systemBridgeConnectionManager.run { bridge -> bridge.setAirplaneMode(false) } + } else { + val success = SettingsUtils.putGlobalSetting(ctx, Settings.Global.AIRPLANE_MODE_ON, 0) + if (success) { + broadcastAirplaneModeChanged(false) + Success(Unit) + } else { + KMError.FailedToModifySystemSetting(Settings.Global.AIRPLANE_MODE_ON) + } } + } override fun isEnabled(): Boolean = SettingsUtils.getGlobalSetting(ctx, Settings.Global.AIRPLANE_MODE_ON) == 1 diff --git a/system/src/main/java/io/github/sds100/keymapper/system/bluetooth/AndroidBluetoothAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/bluetooth/AndroidBluetoothAdapter.kt index 060bc13655..838a455be7 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/bluetooth/AndroidBluetoothAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/bluetooth/AndroidBluetoothAdapter.kt @@ -8,12 +8,14 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageManager +import android.os.Build import androidx.core.content.ContextCompat import androidx.core.content.getSystemService import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -26,6 +28,7 @@ import javax.inject.Singleton class AndroidBluetoothAdapter @Inject constructor( @ApplicationContext private val context: Context, private val coroutineScope: CoroutineScope, + private val systemBridgeConnectionManager: SystemBridgeConnectionManager ) : io.github.sds100.keymapper.system.bluetooth.BluetoothAdapter { private val bluetoothManager: BluetoothManager? = context.getSystemService() @@ -45,6 +48,7 @@ class AndroidBluetoothAdapter @Inject constructor( onReceiveIntent(intent) } } + init { IntentFilter().apply { // these broadcasts can't be received from a manifest declared receiver on Android 8.0+ @@ -65,7 +69,6 @@ class AndroidBluetoothAdapter @Inject constructor( fun onReceiveIntent(intent: Intent) { when (intent.action) { BluetoothDevice.ACTION_ACL_CONNECTED -> { - val device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) ?: return @@ -144,9 +147,13 @@ class AndroidBluetoothAdapter @Inject constructor( return KMError.SystemFeatureNotSupported(PackageManager.FEATURE_BLUETOOTH) } - adapter.enable() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S_V2) { + return systemBridgeConnectionManager.run { bridge -> bridge.setBluetoothEnabled(true) } + } else { + adapter.enable() + return Success(Unit) + } - return Success(Unit) } override fun disable(): KMResult<*> { @@ -154,8 +161,11 @@ class AndroidBluetoothAdapter @Inject constructor( return KMError.SystemFeatureNotSupported(PackageManager.FEATURE_BLUETOOTH) } - adapter.disable() - - return Success(Unit) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S_V2) { + return systemBridgeConnectionManager.run { bridge -> bridge.setBluetoothEnabled(false) } + } else { + adapter.disable() + return Success(Unit) + } } } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt index 4de8e4696a..72bd687201 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/devices/AndroidDevicesAdapter.kt @@ -8,14 +8,17 @@ import android.os.Handler import android.os.Looper import android.view.InputDevice import androidx.core.content.getSystemService +import dagger.hilt.android.qualifiers.ApplicationContext +import io.github.sds100.keymapper.common.utils.InputDeviceInfo +import io.github.sds100.keymapper.common.utils.InputDeviceUtils import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult +import io.github.sds100.keymapper.common.utils.State import io.github.sds100.keymapper.common.utils.Success +import io.github.sds100.keymapper.common.utils.ifIsData import io.github.sds100.keymapper.system.bluetooth.BluetoothDeviceInfo import io.github.sds100.keymapper.system.permissions.Permission import io.github.sds100.keymapper.system.permissions.PermissionAdapter -import io.github.sds100.keymapper.common.utils.State -import io.github.sds100.keymapper.common.utils.ifIsData import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -25,7 +28,6 @@ import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import javax.inject.Singleton @@ -36,6 +38,7 @@ class AndroidDevicesAdapter @Inject constructor( private val permissionAdapter: PermissionAdapter, private val coroutineScope: CoroutineScope, ) : DevicesAdapter { + private val ctx = context.applicationContext private val inputManager = ctx.getSystemService() @@ -137,6 +140,10 @@ class AndroidDevicesAdapter @Inject constructor( return KMError.DeviceNotFound(descriptor) } + override fun getInputDevice(deviceId: Int): InputDeviceInfo? { + return InputDevice.getDevice(deviceId)?.let { InputDeviceUtils.createInputDeviceInfo(it) } + } + private fun updateInputDevices() { val devices = mutableListOf() diff --git a/system/src/main/java/io/github/sds100/keymapper/system/devices/DevicesAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/devices/DevicesAdapter.kt index b244531a06..f82be90881 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/devices/DevicesAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/devices/DevicesAdapter.kt @@ -1,8 +1,9 @@ package io.github.sds100.keymapper.system.devices +import io.github.sds100.keymapper.common.utils.InputDeviceInfo import io.github.sds100.keymapper.common.utils.KMResult -import io.github.sds100.keymapper.system.bluetooth.BluetoothDeviceInfo import io.github.sds100.keymapper.common.utils.State +import io.github.sds100.keymapper.system.bluetooth.BluetoothDeviceInfo import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -16,4 +17,5 @@ interface DevicesAdapter { fun deviceHasKey(id: Int, keyCode: Int): Boolean fun getInputDeviceName(descriptor: String): KMResult + fun getInputDevice(deviceId: Int): InputDeviceInfo? } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/display/AndroidDisplayAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/display/AndroidDisplayAdapter.kt index 377ab9be1f..9067b25632 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/display/AndroidDisplayAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/display/AndroidDisplayAdapter.kt @@ -12,12 +12,12 @@ import androidx.core.content.ContextCompat import androidx.core.content.getSystemService import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.utils.KMError -import io.github.sds100.keymapper.common.utils.Orientation import io.github.sds100.keymapper.common.utils.KMResult +import io.github.sds100.keymapper.common.utils.Orientation +import io.github.sds100.keymapper.common.utils.SettingsUtils import io.github.sds100.keymapper.common.utils.SizeKM import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.common.utils.getRealDisplaySize -import io.github.sds100.keymapper.system.SettingsUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt deleted file mode 100644 index 5a680f9893..0000000000 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventInjector.kt +++ /dev/null @@ -1,27 +0,0 @@ -package io.github.sds100.keymapper.system.inputevents - -import android.view.KeyEvent -import io.github.sds100.keymapper.system.inputmethod.InputKeyModel - -interface InputEventInjector { - suspend fun inputKeyEvent(model: InputKeyModel) - - fun createInjectedKeyEvent( - eventTime: Long, - action: Int, - model: InputKeyModel, - ): KeyEvent { - return KeyEvent( - eventTime, - eventTime, - action, - model.keyCode, - model.repeat, - model.metaState, - model.deviceId, - model.scanCode, - 0, - model.source, - ) - } -} diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMEvdevEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMEvdevEvent.kt new file mode 100644 index 0000000000..424f869477 --- /dev/null +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMEvdevEvent.kt @@ -0,0 +1,36 @@ +package io.github.sds100.keymapper.system.inputevents + +import io.github.sds100.keymapper.common.models.EvdevDeviceHandle + +data class KMEvdevEvent( + val device: EvdevDeviceHandle, + val type: Int, + val code: Int, + val value: Int, + val androidCode: Int, + val timeSec: Long, + val timeUsec: Long +) : KMInputEvent { + + companion object { + const val TYPE_SYN_EVENT = 0 + const val TYPE_KEY_EVENT = 1 + const val TYPE_REL_EVENT = 2 + + const val VALUE_DOWN = 1 + const val VALUE_UP = 0 + } + + // Look at input-event-codes.h for where these are defined. + // EV_SYN + val isSynEvent: Boolean = type == TYPE_SYN_EVENT + + // EV_KEY + val isKeyEvent: Boolean = type == TYPE_KEY_EVENT + + // EV_REL + val isRelEvent: Boolean = type == TYPE_REL_EVENT + + val isDownEvent: Boolean = isKeyEvent && value == VALUE_DOWN + val isUpEvent: Boolean = isKeyEvent && value == VALUE_UP +} diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/MyMotionEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMGamePadEvent.kt similarity index 50% rename from system/src/main/java/io/github/sds100/keymapper/system/inputevents/MyMotionEvent.kt rename to system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMGamePadEvent.kt index 5e77e3b9f9..55fe8c0440 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/MyMotionEvent.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMGamePadEvent.kt @@ -1,29 +1,34 @@ package io.github.sds100.keymapper.system.inputevents import android.view.MotionEvent -import io.github.sds100.keymapper.system.devices.InputDeviceInfo -import io.github.sds100.keymapper.system.devices.InputDeviceUtils +import io.github.sds100.keymapper.common.utils.InputDeviceInfo +import io.github.sds100.keymapper.common.utils.InputDeviceUtils /** * This is our own abstraction over MotionEvent so that it is easier to write tests and read * values without relying on the Android SDK. */ -data class MyMotionEvent( +data class KMGamePadEvent( + val eventTime: Long, val metaState: Int, - val device: InputDeviceInfo?, + val device: InputDeviceInfo, val axisHatX: Float, val axisHatY: Float, - val isDpad: Boolean, -) { +) : KMInputEvent { + companion object { - fun fromMotionEvent(event: MotionEvent): MyMotionEvent { - return MyMotionEvent( + fun fromMotionEvent(event: MotionEvent): KMGamePadEvent? { + val device = event.device ?: return null + + return KMGamePadEvent( + eventTime = event.eventTime, metaState = event.metaState, - device = event.device?.let { InputDeviceUtils.createInputDeviceInfo(it) }, + device = InputDeviceUtils.createInputDeviceInfo(device), axisHatX = event.getAxisValue(MotionEvent.AXIS_HAT_X), axisHatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y), - isDpad = InputEventUtils.isDpadDevice(event), ) } } + + val deviceId: Int = device.id } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMInputEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMInputEvent.kt new file mode 100644 index 0000000000..1e12eaed08 --- /dev/null +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMInputEvent.kt @@ -0,0 +1,3 @@ +package io.github.sds100.keymapper.system.inputevents + +sealed interface KMInputEvent \ No newline at end of file diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt new file mode 100644 index 0000000000..2a77588af2 --- /dev/null +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KMKeyEvent.kt @@ -0,0 +1,40 @@ +package io.github.sds100.keymapper.system.inputevents + +import android.view.KeyEvent +import io.github.sds100.keymapper.common.utils.InputDeviceInfo +import io.github.sds100.keymapper.common.utils.InputDeviceUtils + +/** + * This is our own abstraction over KeyEvent so that it is easier to write tests and read + * values without relying on the Android SDK. + */ +data class KMKeyEvent( + val keyCode: Int, + val action: Int, + val metaState: Int, + val scanCode: Int, + val device: InputDeviceInfo, + val repeatCount: Int, + val source: Int, + val eventTime: Long, +) : KMInputEvent { + + companion object { + fun fromAndroidKeyEvent(keyEvent: KeyEvent): KMKeyEvent? { + val device = keyEvent.device ?: return null + + return KMKeyEvent( + keyCode = keyEvent.keyCode, + action = keyEvent.action, + metaState = keyEvent.metaState, + scanCode = keyEvent.scanCode, + device = InputDeviceUtils.createInputDeviceInfo(device), + repeatCount = keyEvent.repeatCount, + source = keyEvent.source, + eventTime = keyEvent.eventTime, + ) + } + } + + val deviceId: Int = device.id +} diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventUtils.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KeyEventUtils.kt similarity index 91% rename from system/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventUtils.kt rename to system/src/main/java/io/github/sds100/keymapper/system/inputevents/KeyEventUtils.kt index 401ba8e4c3..6e781f8fd1 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/InputEventUtils.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/KeyEventUtils.kt @@ -1,12 +1,9 @@ package io.github.sds100.keymapper.system.inputevents -import android.view.InputDevice -import android.view.InputEvent import android.view.KeyEvent import io.github.sds100.keymapper.common.utils.withFlag -object InputEventUtils { - +object KeyEventUtils { private val KEYCODES: IntArray = intArrayOf( KeyEvent.KEYCODE_SOFT_LEFT, KeyEvent.KEYCODE_SOFT_RIGHT, @@ -229,6 +226,7 @@ object InputEventUtils { KeyEvent.KEYCODE_RO, KeyEvent.KEYCODE_KANA, KeyEvent.KEYCODE_ASSIST, + KeyEvent.KEYCODE_POWER, KeyEvent.KEYCODE_BRIGHTNESS_DOWN, KeyEvent.KEYCODE_BRIGHTNESS_UP, KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK, @@ -327,32 +325,6 @@ object InputEventUtils { KeyEvent.KEYCODE_SCREENSHOT, ) - /** - * These are key code maps for the getevent command. These names aren't the same as the - * KeyEvent key codes in the Android SDK so these have to be manually whitelisted - * as people need. - */ - val GET_EVENT_LABEL_TO_KEYCODE: List> = listOf( - "KEY_VOLUMEDOWN" to KeyEvent.KEYCODE_VOLUME_DOWN, - "KEY_VOLUMEUP" to KeyEvent.KEYCODE_VOLUME_UP, - "KEY_MEDIA" to KeyEvent.KEYCODE_HEADSETHOOK, - "KEY_HEADSETHOOK" to KeyEvent.KEYCODE_HEADSETHOOK, - "KEY_CAMERA_FOCUS" to KeyEvent.KEYCODE_FOCUS, - "02fe" to KeyEvent.KEYCODE_CAMERA, - "00fa" to KeyEvent.KEYCODE_CAMERA, - - // This kernel key event code seems to be the Bixby button - // but different ROMs have different key maps and so - // it is reported as different Android key codes. - "02bf" to KeyEvent.KEYCODE_MENU, - "02bf" to KeyEvent.KEYCODE_ASSIST, - - "KEY_SEARCH" to KeyEvent.KEYCODE_SEARCH, - ) - - fun canDetectKeyWhenScreenOff(keyCode: Int): Boolean = - GET_EVENT_LABEL_TO_KEYCODE.any { it.second == keyCode } - val MODIFIER_KEYCODES: Set get() = setOf( KeyEvent.KEYCODE_SHIFT_LEFT, @@ -368,11 +340,6 @@ object InputEventUtils { KeyEvent.KEYCODE_FUNCTION, ) - /** - * Used for keyCode to scanCode fallback to go past possible keyCode values - */ - val KEYCODE_TO_SCANCODE_OFFSET: Int = 1000 - fun isModifierKey(keyCode: Int): Boolean = keyCode in MODIFIER_KEYCODES fun isGamepadKeyCode(keyCode: Int): Boolean { @@ -453,13 +420,10 @@ object InputEventUtils { code == KeyEvent.KEYCODE_DPAD_UP_LEFT || code == KeyEvent.KEYCODE_DPAD_UP_RIGHT || code == KeyEvent.KEYCODE_DPAD_DOWN_LEFT || - code == KeyEvent.KEYCODE_DPAD_DOWN_RIGHT + code == KeyEvent.KEYCODE_DPAD_DOWN_RIGHT || + code == KeyEvent.KEYCODE_DPAD_CENTER } - fun isDpadDevice(event: InputEvent): Boolean = - // Check that input comes from a device with directional pads. - event.source and InputDevice.SOURCE_DPAD != InputDevice.SOURCE_DPAD - fun isGamepadButton(keyCode: Int): Boolean { return when (keyCode) { KeyEvent.KEYCODE_BUTTON_A, @@ -498,4 +462,10 @@ object InputEventUtils { else -> false } } + + fun isKeyCodeUnknown(keyCode: Int): Boolean { + // The lowest key code is 1 (KEYCODE_SOFT_LEFT) + return keyCode > KeyEvent.getMaxKeyCode() || keyCode < 1 + } } + diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/MyKeyEvent.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/MyKeyEvent.kt deleted file mode 100644 index 6517eefa26..0000000000 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/MyKeyEvent.kt +++ /dev/null @@ -1,13 +0,0 @@ -package io.github.sds100.keymapper.system.inputevents - -import io.github.sds100.keymapper.system.devices.InputDeviceInfo - -data class MyKeyEvent( - val keyCode: Int, - val action: Int, - val metaState: Int, - val scanCode: Int, - val device: InputDeviceInfo?, - val repeatCount: Int, - val source: Int, -) diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputevents/Scancode.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/Scancode.kt new file mode 100644 index 0000000000..bc2d90fcd3 --- /dev/null +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputevents/Scancode.kt @@ -0,0 +1,635 @@ +package io.github.sds100.keymapper.system.inputevents + +object Scancode { + const val KEY_1 = 2 + const val KEY_2 = 3 + const val KEY_3 = 4 + const val KEY_4 = 5 + const val KEY_5 = 6 + const val KEY_6 = 7 + const val KEY_7 = 8 + const val KEY_8 = 9 + const val KEY_9 = 10 + const val KEY_0 = 11 + const val KEY_MINUS = 12 + const val KEY_EQUAL = 13 + const val KEY_BACKSPACE = 14 + const val KEY_TAB = 15 + const val KEY_Q = 16 + const val KEY_W = 17 + const val KEY_E = 18 + const val KEY_R = 19 + const val KEY_T = 20 + const val KEY_Y = 21 + const val KEY_U = 22 + const val KEY_I = 23 + const val KEY_O = 24 + const val KEY_P = 25 + const val KEY_LEFTBRACE = 26 + const val KEY_RIGHTBRACE = 27 + const val KEY_ENTER = 28 + const val KEY_LEFTCTRL = 29 + const val KEY_A = 30 + const val KEY_S = 31 + const val KEY_D = 32 + const val KEY_F = 33 + const val KEY_G = 34 + const val KEY_H = 35 + const val KEY_J = 36 + const val KEY_K = 37 + const val KEY_L = 38 + const val KEY_SEMICOLON = 39 + const val KEY_APOSTROPHE = 40 + const val KEY_GRAVE = 41 + const val KEY_LEFTSHIFT = 42 + const val KEY_BACKSLASH = 43 + const val KEY_Z = 44 + const val KEY_X = 45 + const val KEY_C = 46 + const val KEY_V = 47 + const val KEY_B = 48 + const val KEY_N = 49 + const val KEY_M = 50 + const val KEY_COMMA = 51 + const val KEY_DOT = 52 + const val KEY_SLASH = 53 + const val KEY_RIGHTSHIFT = 54 + const val KEY_KPASTERISK = 55 + const val KEY_LEFTALT = 56 + const val KEY_SPACE = 57 + const val KEY_CAPSLOCK = 58 + const val KEY_F1 = 59 + const val KEY_F2 = 60 + const val KEY_F3 = 61 + const val KEY_F4 = 62 + const val KEY_F5 = 63 + const val KEY_F6 = 64 + const val KEY_F7 = 65 + const val KEY_F8 = 66 + const val KEY_F9 = 67 + const val KEY_F10 = 68 + const val KEY_NUMLOCK = 69 + const val KEY_SCROLLLOCK = 70 + const val KEY_KP7 = 71 + const val KEY_KP8 = 72 + const val KEY_KP9 = 73 + const val KEY_KPMINUS = 74 + const val KEY_KP4 = 75 + const val KEY_KP5 = 76 + const val KEY_KP6 = 77 + const val KEY_KPPLUS = 78 + const val KEY_KP1 = 79 + const val KEY_KP2 = 80 + const val KEY_KP3 = 81 + const val KEY_KP0 = 82 + const val KEY_KPDOT = 83 + const val KEY_ZENKAKUHANKAKU = 85 + const val KEY_102ND = 86 + const val KEY_F11 = 87 + const val KEY_F12 = 88 + const val KEY_RO = 89 + const val KEY_KATAKANA = 90 + const val KEY_HIRAGANA = 91 + const val KEY_HENKAN = 92 + const val KEY_KATAKANAHIRAGANA = 93 + const val KEY_MUHENKAN = 94 + const val KEY_KPJPCOMMA = 95 + const val KEY_KPENTER = 96 + const val KEY_RIGHTCTRL = 97 + const val KEY_KPSLASH = 98 + const val KEY_SYSRQ = 99 + const val KEY_RIGHTALT = 100 + const val KEY_LINEFEED = 101 + const val KEY_HOME = 102 + const val KEY_UP = 103 + const val KEY_PAGEUP = 104 + const val KEY_LEFT = 105 + const val KEY_RIGHT = 106 + const val KEY_END = 107 + const val KEY_DOWN = 108 + const val KEY_PAGEDOWN = 109 + const val KEY_INSERT = 110 + const val KEY_DELETE = 111 + const val KEY_MACRO = 112 + const val KEY_MUTE = 113 + const val KEY_VOLUMEDOWN = 114 + const val KEY_VOLUMEUP = 115 + const val KEY_POWER = 116 + const val KEY_KPEQUAL = 117 + const val KEY_KPPLUSMINUS = 118 + const val KEY_PAUSE = 119 + const val KEY_SCALE = 120 + const val KEY_KPCOMMA = 121 + const val KEY_HANGEUL = 122 + const val KEY_HANGUEL = KEY_HANGEUL + const val KEY_HANJA = 123 + const val KEY_YEN = 124 + const val KEY_LEFTMETA = 125 + const val KEY_RIGHTMETA = 126 + const val KEY_COMPOSE = 127 + const val KEY_STOP = 128 + const val KEY_AGAIN = 129 + const val KEY_PROPS = 130 + const val KEY_UNDO = 131 + const val KEY_FRONT = 132 + const val KEY_COPY = 133 + const val KEY_OPEN = 134 + const val KEY_PASTE = 135 + const val KEY_FIND = 136 + const val KEY_CUT = 137 + const val KEY_HELP = 138 + const val KEY_MENU = 139 + const val KEY_CALC = 140 + const val KEY_SETUP = 141 + const val KEY_SLEEP = 142 + const val KEY_WAKEUP = 143 + const val KEY_FILE = 144 + const val KEY_SENDFILE = 145 + const val KEY_DELETEFILE = 146 + const val KEY_XFER = 147 + const val KEY_PROG1 = 148 + const val KEY_PROG2 = 149 + const val KEY_WWW = 150 + const val KEY_MSDOS = 151 + const val KEY_COFFEE = 152 + const val KEY_SCREENLOCK = KEY_COFFEE + const val KEY_ROTATE_DISPLAY = 153 + const val KEY_DIRECTION = KEY_ROTATE_DISPLAY + const val KEY_CYCLEWINDOWS = 154 + const val KEY_MAIL = 155 + const val KEY_BOOKMARKS = 156 + const val KEY_COMPUTER = 157 + const val KEY_BACK = 158 + const val KEY_FORWARD = 159 + const val KEY_CLOSECD = 160 + const val KEY_EJECTCD = 161 + const val KEY_EJECTCLOSECD = 162 + const val KEY_NEXTSONG = 163 + const val KEY_PLAYPAUSE = 164 + const val KEY_PREVIOUSSONG = 165 + const val KEY_STOPCD = 166 + const val KEY_RECORD = 167 + const val KEY_REWIND = 168 + const val KEY_PHONE = 169 + const val KEY_ISO = 170 + const val KEY_CONFIG = 171 + const val KEY_HOMEPAGE = 172 + const val KEY_REFRESH = 173 + const val KEY_EXIT = 174 + const val KEY_MOVE = 175 + const val KEY_EDIT = 176 + const val KEY_SCROLLUP = 177 + const val KEY_SCROLLDOWN = 178 + const val KEY_KPLEFTPAREN = 179 + const val KEY_KPRIGHTPAREN = 180 + const val KEY_NEW = 181 + const val KEY_REDO = 182 + const val KEY_F13 = 183 + const val KEY_F14 = 184 + const val KEY_F15 = 185 + const val KEY_F16 = 186 + const val KEY_F17 = 187 + const val KEY_F18 = 188 + const val KEY_F19 = 189 + const val KEY_F20 = 190 + const val KEY_F21 = 191 + const val KEY_F22 = 192 + const val KEY_F23 = 193 + const val KEY_F24 = 194 + const val KEY_PLAYCD = 200 + const val KEY_PAUSECD = 201 + const val KEY_PROG3 = 202 + const val KEY_PROG4 = 203 + const val KEY_ALL_APPLICATIONS = 204 + const val KEY_DASHBOARD = KEY_ALL_APPLICATIONS + const val KEY_SUSPEND = 205 + const val KEY_CLOSE = 206 + const val KEY_PLAY = 207 + const val KEY_FASTFORWARD = 208 + const val KEY_BASSBOOST = 209 + const val KEY_PRINT = 210 + const val KEY_HP = 211 + const val KEY_CAMERA = 212 + const val KEY_SOUND = 213 + const val KEY_QUESTION = 214 + const val KEY_EMAIL = 215 + const val KEY_CHAT = 216 + const val KEY_SEARCH = 217 + const val KEY_CONNECT = 218 + const val KEY_FINANCE = 219 + const val KEY_SPORT = 220 + const val KEY_SHOP = 221 + const val KEY_ALTERASE = 222 + const val KEY_CANCEL = 223 + const val KEY_BRIGHTNESSDOWN = 224 + const val KEY_BRIGHTNESSUP = 225 + const val KEY_MEDIA = 226 + const val KEY_SWITCHVIDEOMODE = 227 + const val KEY_KBDILLUMTOGGLE = 228 + const val KEY_KBDILLUMDOWN = 229 + const val KEY_KBDILLUMUP = 230 + const val KEY_SEND = 231 + const val KEY_REPLY = 232 + const val KEY_FORWARDMAIL = 233 + const val KEY_SAVE = 234 + const val KEY_DOCUMENTS = 235 + const val KEY_BATTERY = 236 + const val KEY_BLUETOOTH = 237 + const val KEY_WLAN = 238 + const val KEY_UWB = 239 + const val KEY_UNKNOWN = 240 + const val KEY_VIDEO_NEXT = 241 + const val KEY_VIDEO_PREV = 242 + const val KEY_BRIGHTNESS_CYCLE = 243 + const val KEY_BRIGHTNESS_AUTO = 244 + const val KEY_BRIGHTNESS_ZERO = KEY_BRIGHTNESS_AUTO + const val KEY_DISPLAY_OFF = 245 + const val KEY_WWAN = 246 + const val KEY_WIMAX = KEY_WWAN + const val KEY_RFKILL = 247 + const val KEY_MICMUTE = 248 + const val BTN_MISC = 0x100 + const val BTN_0 = 0x100 + const val BTN_1 = 0x101 + const val BTN_2 = 0x102 + const val BTN_3 = 0x103 + const val BTN_4 = 0x104 + const val BTN_5 = 0x105 + const val BTN_6 = 0x106 + const val BTN_7 = 0x107 + const val BTN_8 = 0x108 + const val BTN_9 = 0x109 + const val BTN_MOUSE = 0x110 + const val BTN_LEFT = 0x110 + const val BTN_RIGHT = 0x111 + const val BTN_MIDDLE = 0x112 + const val BTN_SIDE = 0x113 + const val BTN_EXTRA = 0x114 + const val BTN_FORWARD = 0x115 + const val BTN_BACK = 0x116 + const val BTN_TASK = 0x117 + const val BTN_JOYSTICK = 0x120 + const val BTN_TRIGGER = 0x120 + const val BTN_THUMB = 0x121 + const val BTN_THUMB2 = 0x122 + const val BTN_TOP = 0x123 + const val BTN_TOP2 = 0x124 + const val BTN_PINKIE = 0x125 + const val BTN_BASE = 0x126 + const val BTN_BASE2 = 0x127 + const val BTN_BASE3 = 0x128 + const val BTN_BASE4 = 0x129 + const val BTN_BASE5 = 0x12a + const val BTN_BASE6 = 0x12b + const val BTN_DEAD = 0x12f + const val BTN_GAMEPAD = 0x130 + const val BTN_SOUTH = 0x130 + const val BTN_A = BTN_SOUTH + const val BTN_EAST = 0x131 + const val BTN_B = BTN_EAST + const val BTN_C = 0x132 + const val BTN_NORTH = 0x133 + const val BTN_X = BTN_NORTH + const val BTN_WEST = 0x134 + const val BTN_Y = BTN_WEST + const val BTN_Z = 0x135 + const val BTN_TL = 0x136 + const val BTN_TR = 0x137 + const val BTN_TL2 = 0x138 + const val BTN_TR2 = 0x139 + const val BTN_SELECT = 0x13a + const val BTN_START = 0x13b + const val BTN_MODE = 0x13c + const val BTN_THUMBL = 0x13d + const val BTN_THUMBR = 0x13e + const val BTN_DIGI = 0x140 + const val BTN_TOOL_PEN = 0x140 + const val BTN_TOOL_RUBBER = 0x141 + const val BTN_TOOL_BRUSH = 0x142 + const val BTN_TOOL_PENCIL = 0x143 + const val BTN_TOOL_AIRBRUSH = 0x144 + const val BTN_TOOL_FINGER = 0x145 + const val BTN_TOOL_MOUSE = 0x146 + const val BTN_TOOL_LENS = 0x147 + const val BTN_TOOL_QUINTTAP = 0x148 + const val BTN_STYLUS3 = 0x149 + const val BTN_TOUCH = 0x14a + const val BTN_STYLUS = 0x14b + const val BTN_STYLUS2 = 0x14c + const val BTN_TOOL_DOUBLETAP = 0x14d + const val BTN_TOOL_TRIPLETAP = 0x14e + const val BTN_TOOL_QUADTAP = 0x14f + const val BTN_WHEEL = 0x150 + const val BTN_GEAR_DOWN = 0x150 + const val BTN_GEAR_UP = 0x151 + const val KEY_OK = 0x160 + const val KEY_SELECT = 0x161 + const val KEY_GOTO = 0x162 + const val KEY_CLEAR = 0x163 + const val KEY_POWER2 = 0x164 + const val KEY_OPTION = 0x165 + const val KEY_INFO = 0x166 + const val KEY_TIME = 0x167 + const val KEY_VENDOR = 0x168 + const val KEY_ARCHIVE = 0x169 + const val KEY_PROGRAM = 0x16a + const val KEY_CHANNEL = 0x16b + const val KEY_FAVORITES = 0x16c + const val KEY_EPG = 0x16d + const val KEY_PVR = 0x16e + const val KEY_MHP = 0x16f + const val KEY_LANGUAGE = 0x170 + const val KEY_TITLE = 0x171 + const val KEY_SUBTITLE = 0x172 + const val KEY_ANGLE = 0x173 + const val KEY_FULL_SCREEN = 0x174 + const val KEY_ZOOM = KEY_FULL_SCREEN + const val KEY_MODE = 0x175 + const val KEY_KEYBOARD = 0x176 + const val KEY_ASPECT_RATIO = 0x177 + const val KEY_SCREEN = KEY_ASPECT_RATIO + const val KEY_PC = 0x178 + const val KEY_TV = 0x179 + const val KEY_TV2 = 0x17a + const val KEY_VCR = 0x17b + const val KEY_VCR2 = 0x17c + const val KEY_SAT = 0x17d + const val KEY_SAT2 = 0x17e + const val KEY_CD = 0x17f + const val KEY_TAPE = 0x180 + const val KEY_RADIO = 0x181 + const val KEY_TUNER = 0x182 + const val KEY_PLAYER = 0x183 + const val KEY_TEXT = 0x184 + const val KEY_DVD = 0x185 + const val KEY_AUX = 0x186 + const val KEY_MP3 = 0x187 + const val KEY_AUDIO = 0x188 + const val KEY_VIDEO = 0x189 + const val KEY_DIRECTORY = 0x18a + const val KEY_LIST = 0x18b + const val KEY_MEMO = 0x18c + const val KEY_CALENDAR = 0x18d + const val KEY_RED = 0x18e + const val KEY_GREEN = 0x18f + const val KEY_YELLOW = 0x190 + const val KEY_BLUE = 0x191 + const val KEY_CHANNELUP = 0x192 + const val KEY_CHANNELDOWN = 0x193 + const val KEY_FIRST = 0x194 + const val KEY_LAST = 0x195 + const val KEY_AB = 0x196 + const val KEY_NEXT = 0x197 + const val KEY_RESTART = 0x198 + const val KEY_SLOW = 0x199 + const val KEY_SHUFFLE = 0x19a + const val KEY_BREAK = 0x19b + const val KEY_PREVIOUS = 0x19c + const val KEY_DIGITS = 0x19d + const val KEY_TEEN = 0x19e + const val KEY_TWEN = 0x19f + const val KEY_VIDEOPHONE = 0x1a0 + const val KEY_GAMES = 0x1a1 + const val KEY_ZOOMIN = 0x1a2 + const val KEY_ZOOMOUT = 0x1a3 + const val KEY_ZOOMRESET = 0x1a4 + const val KEY_WORDPROCESSOR = 0x1a5 + const val KEY_EDITOR = 0x1a6 + const val KEY_SPREADSHEET = 0x1a7 + const val KEY_GRAPHICSEDITOR = 0x1a8 + const val KEY_PRESENTATION = 0x1a9 + const val KEY_DATABASE = 0x1aa + const val KEY_NEWS = 0x1ab + const val KEY_VOICEMAIL = 0x1ac + const val KEY_ADDRESSBOOK = 0x1ad + const val KEY_MESSENGER = 0x1ae + const val KEY_DISPLAYTOGGLE = 0x1af + const val KEY_BRIGHTNESS_TOGGLE = KEY_DISPLAYTOGGLE + const val KEY_SPELLCHECK = 0x1b0 + const val KEY_LOGOFF = 0x1b1 + const val KEY_DOLLAR = 0x1b2 + const val KEY_EURO = 0x1b3 + const val KEY_FRAMEBACK = 0x1b4 + const val KEY_FRAMEFORWARD = 0x1b5 + const val KEY_CONTEXT_MENU = 0x1b6 + const val KEY_MEDIA_REPEAT = 0x1b7 + const val KEY_10CHANNELSUP = 0x1b8 + const val KEY_10CHANNELSDOWN = 0x1b9 + const val KEY_IMAGES = 0x1ba + const val KEY_NOTIFICATION_CENTER = 0x1bc + const val KEY_PICKUP_PHONE = 0x1bd + const val KEY_HANGUP_PHONE = 0x1be + const val KEY_DEL_EOL = 0x1c0 + const val KEY_DEL_EOS = 0x1c1 + const val KEY_INS_LINE = 0x1c2 + const val KEY_DEL_LINE = 0x1c3 + const val KEY_FN = 0x1d0 + const val KEY_FN_ESC = 0x1d1 + const val KEY_FN_F1 = 0x1d2 + const val KEY_FN_F2 = 0x1d3 + const val KEY_FN_F3 = 0x1d4 + const val KEY_FN_F4 = 0x1d5 + const val KEY_FN_F5 = 0x1d6 + const val KEY_FN_F6 = 0x1d7 + const val KEY_FN_F7 = 0x1d8 + const val KEY_FN_F8 = 0x1d9 + const val KEY_FN_F9 = 0x1da + const val KEY_FN_F10 = 0x1db + const val KEY_FN_F11 = 0x1dc + const val KEY_FN_F12 = 0x1dd + const val KEY_FN_1 = 0x1de + const val KEY_FN_2 = 0x1df + const val KEY_FN_D = 0x1e0 + const val KEY_FN_E = 0x1e1 + const val KEY_FN_F = 0x1e2 + const val KEY_FN_S = 0x1e3 + const val KEY_FN_B = 0x1e4 + const val KEY_FN_RIGHT_SHIFT = 0x1e5 + const val KEY_BRL_DOT1 = 0x1f1 + const val KEY_BRL_DOT2 = 0x1f2 + const val KEY_BRL_DOT3 = 0x1f3 + const val KEY_BRL_DOT4 = 0x1f4 + const val KEY_BRL_DOT5 = 0x1f5 + const val KEY_BRL_DOT6 = 0x1f6 + const val KEY_BRL_DOT7 = 0x1f7 + const val KEY_BRL_DOT8 = 0x1f8 + const val KEY_BRL_DOT9 = 0x1f9 + const val KEY_BRL_DOT10 = 0x1fa + const val KEY_NUMERIC_0 = 0x200 + const val KEY_NUMERIC_1 = 0x201 + const val KEY_NUMERIC_2 = 0x202 + const val KEY_NUMERIC_3 = 0x203 + const val KEY_NUMERIC_4 = 0x204 + const val KEY_NUMERIC_5 = 0x205 + const val KEY_NUMERIC_6 = 0x206 + const val KEY_NUMERIC_7 = 0x207 + const val KEY_NUMERIC_8 = 0x208 + const val KEY_NUMERIC_9 = 0x209 + const val KEY_NUMERIC_STAR = 0x20a + const val KEY_NUMERIC_POUND = 0x20b + const val KEY_NUMERIC_A = 0x20c + const val KEY_NUMERIC_B = 0x20d + const val KEY_NUMERIC_C = 0x20e + const val KEY_NUMERIC_D = 0x20f + const val KEY_CAMERA_FOCUS = 0x210 + const val KEY_WPS_BUTTON = 0x211 + const val KEY_TOUCHPAD_TOGGLE = 0x212 + const val KEY_TOUCHPAD_ON = 0x213 + const val KEY_TOUCHPAD_OFF = 0x214 + const val KEY_CAMERA_ZOOMIN = 0x215 + const val KEY_CAMERA_ZOOMOUT = 0x216 + const val KEY_CAMERA_UP = 0x217 + const val KEY_CAMERA_DOWN = 0x218 + const val KEY_CAMERA_LEFT = 0x219 + const val KEY_CAMERA_RIGHT = 0x21a + const val KEY_ATTENDANT_ON = 0x21b + const val KEY_ATTENDANT_OFF = 0x21c + const val KEY_ATTENDANT_TOGGLE = 0x21d + const val KEY_LIGHTS_TOGGLE = 0x21e + const val BTN_DPAD_UP = 0x220 + const val BTN_DPAD_DOWN = 0x221 + const val BTN_DPAD_LEFT = 0x222 + const val BTN_DPAD_RIGHT = 0x223 + const val KEY_ALS_TOGGLE = 0x230 + const val KEY_ROTATE_LOCK_TOGGLE = 0x231 + const val KEY_BUTTONCONFIG = 0x240 + const val KEY_TASKMANAGER = 0x241 + const val KEY_JOURNAL = 0x242 + const val KEY_CONTROLPANEL = 0x243 + const val KEY_APPSELECT = 0x244 + const val KEY_SCREENSAVER = 0x245 + const val KEY_VOICECOMMAND = 0x246 + const val KEY_ASSISTANT = 0x247 + const val KEY_KBD_LAYOUT_NEXT = 0x248 + const val KEY_EMOJI_PICKER = 0x249 + const val KEY_DICTATE = 0x24a + const val KEY_CAMERA_ACCESS_ENABLE = 0x24b + const val KEY_CAMERA_ACCESS_DISABLE = 0x24c + const val KEY_CAMERA_ACCESS_TOGGLE = 0x24d + const val KEY_BRIGHTNESS_MIN = 0x250 + const val KEY_BRIGHTNESS_MAX = 0x251 + const val KEY_KBDINPUTASSIST_PREV = 0x260 + const val KEY_KBDINPUTASSIST_NEXT = 0x261 + const val KEY_KBDINPUTASSIST_PREVGROUP = 0x262 + const val KEY_KBDINPUTASSIST_NEXTGROUP = 0x263 + const val KEY_KBDINPUTASSIST_ACCEPT = 0x264 + const val KEY_KBDINPUTASSIST_CANCEL = 0x265 + const val KEY_RIGHT_UP = 0x266 + const val KEY_RIGHT_DOWN = 0x267 + const val KEY_LEFT_UP = 0x268 + const val KEY_LEFT_DOWN = 0x269 + const val KEY_ROOT_MENU = 0x26a + const val KEY_MEDIA_TOP_MENU = 0x26b + const val KEY_NUMERIC_11 = 0x26c + const val KEY_NUMERIC_12 = 0x26d + const val KEY_AUDIO_DESC = 0x26e + const val KEY_3D_MODE = 0x26f + const val KEY_NEXT_FAVORITE = 0x270 + const val KEY_STOP_RECORD = 0x271 + const val KEY_PAUSE_RECORD = 0x272 + const val KEY_VOD = 0x273 + const val KEY_UNMUTE = 0x274 + const val KEY_FASTREVERSE = 0x275 + const val KEY_SLOWREVERSE = 0x276 + const val KEY_DATA = 0x277 + const val KEY_ONSCREEN_KEYBOARD = 0x278 + const val KEY_PRIVACY_SCREEN_TOGGLE = 0x279 + const val KEY_SELECTIVE_SCREENSHOT = 0x27a + const val KEY_NEXT_ELEMENT = 0x27b + const val KEY_PREVIOUS_ELEMENT = 0x27c + const val KEY_AUTOPILOT_ENGAGE_TOGGLE = 0x27d + const val KEY_MARK_WAYPOINT = 0x27e + const val KEY_SOS = 0x27f + const val KEY_NAV_CHART = 0x280 + const val KEY_FISHING_CHART = 0x281 + const val KEY_SINGLE_RANGE_RADAR = 0x282 + const val KEY_DUAL_RANGE_RADAR = 0x283 + const val KEY_RADAR_OVERLAY = 0x284 + const val KEY_TRADITIONAL_SONAR = 0x285 + const val KEY_CLEARVU_SONAR = 0x286 + const val KEY_SIDEVU_SONAR = 0x287 + const val KEY_NAV_INFO = 0x288 + const val KEY_BRIGHTNESS_MENU = 0x289 + const val KEY_MACRO1 = 0x290 + const val KEY_MACRO2 = 0x291 + const val KEY_MACRO3 = 0x292 + const val KEY_MACRO4 = 0x293 + const val KEY_MACRO5 = 0x294 + const val KEY_MACRO6 = 0x295 + const val KEY_MACRO7 = 0x296 + const val KEY_MACRO8 = 0x297 + const val KEY_MACRO9 = 0x298 + const val KEY_MACRO10 = 0x299 + const val KEY_MACRO11 = 0x29a + const val KEY_MACRO12 = 0x29b + const val KEY_MACRO13 = 0x29c + const val KEY_MACRO14 = 0x29d + const val KEY_MACRO15 = 0x29e + const val KEY_MACRO16 = 0x29f + const val KEY_MACRO17 = 0x2a0 + const val KEY_MACRO18 = 0x2a1 + const val KEY_MACRO19 = 0x2a2 + const val KEY_MACRO20 = 0x2a3 + const val KEY_MACRO21 = 0x2a4 + const val KEY_MACRO22 = 0x2a5 + const val KEY_MACRO23 = 0x2a6 + const val KEY_MACRO24 = 0x2a7 + const val KEY_MACRO25 = 0x2a8 + const val KEY_MACRO26 = 0x2a9 + const val KEY_MACRO27 = 0x2aa + const val KEY_MACRO28 = 0x2ab + const val KEY_MACRO29 = 0x2ac + const val KEY_MACRO30 = 0x2ad + const val KEY_MACRO_RECORD_START = 0x2b0 + const val KEY_MACRO_RECORD_STOP = 0x2b1 + const val KEY_MACRO_PRESET_CYCLE = 0x2b2 + const val KEY_MACRO_PRESET1 = 0x2b3 + const val KEY_MACRO_PRESET2 = 0x2b4 + const val KEY_MACRO_PRESET3 = 0x2b5 + const val KEY_KBD_LCD_MENU1 = 0x2b8 + const val KEY_KBD_LCD_MENU2 = 0x2b9 + const val KEY_KBD_LCD_MENU3 = 0x2ba + const val KEY_KBD_LCD_MENU4 = 0x2bb + const val KEY_KBD_LCD_MENU5 = 0x2bc + const val BTN_TRIGGER_HAPPY = 0x2c0 + const val BTN_TRIGGER_HAPPY1 = 0x2c0 + const val BTN_TRIGGER_HAPPY2 = 0x2c1 + const val BTN_TRIGGER_HAPPY3 = 0x2c2 + const val BTN_TRIGGER_HAPPY4 = 0x2c3 + const val BTN_TRIGGER_HAPPY5 = 0x2c4 + const val BTN_TRIGGER_HAPPY6 = 0x2c5 + const val BTN_TRIGGER_HAPPY7 = 0x2c6 + const val BTN_TRIGGER_HAPPY8 = 0x2c7 + const val BTN_TRIGGER_HAPPY9 = 0x2c8 + const val BTN_TRIGGER_HAPPY10 = 0x2c9 + const val BTN_TRIGGER_HAPPY11 = 0x2ca + const val BTN_TRIGGER_HAPPY12 = 0x2cb + const val BTN_TRIGGER_HAPPY13 = 0x2cc + const val BTN_TRIGGER_HAPPY14 = 0x2cd + const val BTN_TRIGGER_HAPPY15 = 0x2ce + const val BTN_TRIGGER_HAPPY16 = 0x2cf + const val BTN_TRIGGER_HAPPY17 = 0x2d0 + const val BTN_TRIGGER_HAPPY18 = 0x2d1 + const val BTN_TRIGGER_HAPPY19 = 0x2d2 + const val BTN_TRIGGER_HAPPY20 = 0x2d3 + const val BTN_TRIGGER_HAPPY21 = 0x2d4 + const val BTN_TRIGGER_HAPPY22 = 0x2d5 + const val BTN_TRIGGER_HAPPY23 = 0x2d6 + const val BTN_TRIGGER_HAPPY24 = 0x2d7 + const val BTN_TRIGGER_HAPPY25 = 0x2d8 + const val BTN_TRIGGER_HAPPY26 = 0x2d9 + const val BTN_TRIGGER_HAPPY27 = 0x2da + const val BTN_TRIGGER_HAPPY28 = 0x2db + const val BTN_TRIGGER_HAPPY29 = 0x2dc + const val BTN_TRIGGER_HAPPY30 = 0x2dd + const val BTN_TRIGGER_HAPPY31 = 0x2de + const val BTN_TRIGGER_HAPPY32 = 0x2df + const val BTN_TRIGGER_HAPPY33 = 0x2e0 + const val BTN_TRIGGER_HAPPY34 = 0x2e1 + const val BTN_TRIGGER_HAPPY35 = 0x2e2 + const val BTN_TRIGGER_HAPPY36 = 0x2e3 + const val BTN_TRIGGER_HAPPY37 = 0x2e4 + const val BTN_TRIGGER_HAPPY38 = 0x2e5 + const val BTN_TRIGGER_HAPPY39 = 0x2e6 + const val BTN_TRIGGER_HAPPY40 = 0x2e7 +} \ No newline at end of file diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/AndroidInputMethodAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/AndroidInputMethodAdapter.kt index 5f3598ae29..f748cd831d 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/AndroidInputMethodAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/AndroidInputMethodAdapter.kt @@ -17,6 +17,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.BuildConfigProvider import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult +import io.github.sds100.keymapper.common.utils.SettingsUtils import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.common.utils.onFailure import io.github.sds100.keymapper.common.utils.onSuccess @@ -24,7 +25,6 @@ import io.github.sds100.keymapper.common.utils.otherwise import io.github.sds100.keymapper.common.utils.then import io.github.sds100.keymapper.common.utils.valueOrNull import io.github.sds100.keymapper.system.JobSchedulerHelper -import io.github.sds100.keymapper.system.SettingsUtils import io.github.sds100.keymapper.system.SystemError import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceAdapter import io.github.sds100.keymapper.system.accessibility.AccessibilityServiceEvent diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/InputKeyModel.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/InputKeyModel.kt deleted file mode 100644 index 1142fef197..0000000000 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/InputKeyModel.kt +++ /dev/null @@ -1,14 +0,0 @@ -package io.github.sds100.keymapper.system.inputmethod - -import android.view.InputDevice -import io.github.sds100.keymapper.common.utils.InputEventType - -data class InputKeyModel( - val keyCode: Int, - val inputType: InputEventType = InputEventType.DOWN_UP, - val metaState: Int = 0, - val deviceId: Int = 0, - val scanCode: Int = 0, - val repeat: Int = 0, - val source: Int = InputDevice.SOURCE_UNKNOWN, -) diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyEventRelayServiceWrapper.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyEventRelayServiceWrapper.kt index 24dccae1b1..a6d1e94c93 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyEventRelayServiceWrapper.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyEventRelayServiceWrapper.kt @@ -9,20 +9,17 @@ import android.os.IBinder import android.os.RemoteException import android.view.KeyEvent import android.view.MotionEvent +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.api.IKeyEventRelayService import io.github.sds100.keymapper.api.IKeyEventRelayServiceCallback -import io.github.sds100.keymapper.system.inputmethod.KeyEventRelayServiceWrapper - -/** - * This handles connecting to the relay service and exposes an interface - * so other parts of the app can get a reference to the service even when it isn't - * bound yet. This class is copied to the Key Mapper GUI Keyboard app as well. - */ -class KeyEventRelayServiceWrapperImpl( - private val ctx: Context, - private val id: String, - private val servicePackageName: String, - private val callback: IKeyEventRelayServiceCallback, +import io.github.sds100.keymapper.common.BuildConfigProvider +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class KeyEventRelayServiceWrapperImpl @Inject constructor( + @ApplicationContext private val ctx: Context, + private val buildConfigProvider: BuildConfigProvider ) : KeyEventRelayServiceWrapper { private val keyEventRelayServiceLock: Any = Any() @@ -36,7 +33,6 @@ class KeyEventRelayServiceWrapperImpl( ) { synchronized(keyEventRelayServiceLock) { keyEventRelayService = IKeyEventRelayService.Stub.asInterface(service) - keyEventRelayService?.registerCallback(callback, id) } } @@ -51,14 +47,6 @@ class KeyEventRelayServiceWrapperImpl( } } - fun onCreate() { - bind() - } - - fun onDestroy() { - unbind() - } - override fun sendKeyEvent( event: KeyEvent, targetPackageName: String, @@ -93,11 +81,22 @@ class KeyEventRelayServiceWrapperImpl( } } - private fun bind() { + override fun registerClient(id: String, callback: IKeyEventRelayServiceCallback) { + keyEventRelayService?.registerCallback(callback, id) + } + + override fun unregisterClient(id: String) { + keyEventRelayService?.unregisterCallback(id) + } + + fun bind() { try { val relayServiceIntent = Intent() val component = - ComponentName(servicePackageName, "io.github.sds100.keymapper.api.KeyEventRelayService") + ComponentName( + buildConfigProvider.packageName, + "io.github.sds100.keymapper.api.KeyEventRelayService" + ) relayServiceIntent.setComponent(component) val isSuccess = ctx.bindService(relayServiceIntent, serviceConnection, Context.BIND_AUTO_CREATE) @@ -111,13 +110,8 @@ class KeyEventRelayServiceWrapperImpl( } } - private fun unbind() { - // Unregister the callback if this input method is unbinding - // from the relay service. This should not happen in onServiceDisconnected - // because the connection is already broken at that point and it - // will fail. + fun unbind() { try { - keyEventRelayService?.unregisterCallback(id) ctx.unbindService(serviceConnection) } catch (e: RemoteException) { // do nothing @@ -129,6 +123,8 @@ class KeyEventRelayServiceWrapperImpl( } interface KeyEventRelayServiceWrapper { + fun registerClient(id: String, callback: IKeyEventRelayServiceCallback) + fun unregisterClient(id: String) fun sendKeyEvent(event: KeyEvent, targetPackageName: String, callbackId: String): Boolean fun sendMotionEvent(event: MotionEvent, targetPackageName: String, callbackId: String): Boolean } \ No newline at end of file diff --git a/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeService.kt b/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeService.kt index 470d1707c5..d9cc83e40b 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeService.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/inputmethod/KeyMapperImeService.kt @@ -116,14 +116,8 @@ class KeyMapperImeService : InputMethodService() { } } - private val keyEventRelayServiceWrapper: KeyEventRelayServiceWrapperImpl by lazy { - KeyEventRelayServiceWrapperImpl( - ctx = this, - id = CALLBACK_ID_INPUT_METHOD, - servicePackageName = buildConfigProvider.packageName, - callback = keyEventReceiverCallback, - ) - } + @Inject + lateinit var keyEventRelayServiceWrapper: KeyEventRelayServiceWrapper override fun onCreate() { super.onCreate() @@ -141,7 +135,10 @@ class KeyMapperImeService : InputMethodService() { ContextCompat.RECEIVER_NOT_EXPORTED, ) - keyEventRelayServiceWrapper.onCreate() + keyEventRelayServiceWrapper.registerClient( + CALLBACK_ID_INPUT_METHOD, + keyEventReceiverCallback + ) } override fun onStartInput(attribute: EditorInfo?, restarting: Boolean) { @@ -211,7 +208,7 @@ class KeyMapperImeService : InputMethodService() { override fun onDestroy() { unregisterReceiver(broadcastReceiver) - keyEventRelayServiceWrapper.onDestroy() + keyEventRelayServiceWrapper.unregisterClient(CALLBACK_ID_INPUT_METHOD) super.onDestroy() } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt index 8c05233836..c59cb2041b 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt @@ -1,17 +1,26 @@ package io.github.sds100.keymapper.system.network +import android.content.ActivityNotFoundException import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest import android.net.wifi.WifiManager import android.os.Build +import android.provider.Settings +import android.telephony.SubscriptionManager import android.telephony.TelephonyManager import androidx.core.content.ContextCompat import androidx.core.content.getSystemService +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager import io.github.sds100.keymapper.system.root.SuAdapter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow @@ -25,7 +34,6 @@ import okhttp3.RequestBody.Companion.toRequestBody import okio.IOException import okio.use import timber.log.Timber -import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import javax.inject.Singleton @@ -33,10 +41,13 @@ import javax.inject.Singleton class AndroidNetworkAdapter @Inject constructor( @ApplicationContext private val context: Context, private val suAdapter: SuAdapter, + private val systemBridgeConnManager: SystemBridgeConnectionManager ) : NetworkAdapter { private val ctx = context.applicationContext private val wifiManager: WifiManager by lazy { ctx.getSystemService()!! } private val telephonyManager: TelephonyManager by lazy { ctx.getSystemService()!! } + private val connectivityManager: ConnectivityManager by lazy { ctx.getSystemService()!! } + private val httpClient: OkHttpClient by lazy { OkHttpClient() } private val broadcastReceiver = object : BroadcastReceiver() { @@ -51,24 +62,43 @@ class AndroidNetworkAdapter @Inject constructor( } WifiManager.NETWORK_STATE_CHANGED_ACTION -> { - connectedWifiSSIDFlow.update { connectedWifiSSID } + connectedWifiSSIDFlow.update { getWifiSSID() } } } } } - override val connectedWifiSSID: String? - get() = wifiManager.connectionInfo?.ssid?.let { ssid -> - if (ssid == WifiManager.UNKNOWN_SSID) { - null - } else { - ssid.removeSurrounding("\"") - } - } + override val connectedWifiSSIDFlow = MutableStateFlow(getWifiSSID()) + override val isWifiConnected: MutableStateFlow = MutableStateFlow(getIsWifiConnected()) - override val connectedWifiSSIDFlow = MutableStateFlow(connectedWifiSSID) private val isWifiEnabled = MutableStateFlow(isWifiEnabled()) + private val networkCallback: ConnectivityManager.NetworkCallback = + object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + isWifiConnected.update { getIsWifiConnected() } + } + + override fun onLost(network: Network) { + super.onLost(network) + // A network was lost. Check if we are still connected to *any* Wi-Fi. + // This is important because onLost is called for a specific network. + // If multiple Wi-Fi networks were available and one is lost, + // another might still be active. + isWifiConnected.update { getIsWifiConnected() } + } + + override fun onCapabilitiesChanged( + network: Network, + networkCapabilities: NetworkCapabilities + ) { + super.onCapabilitiesChanged(network, networkCapabilities) + + isWifiConnected.update { getIsWifiConnected() } + } + } + init { IntentFilter().apply { addAction(WifiManager.WIFI_STATE_CHANGED_ACTION) @@ -81,6 +111,12 @@ class AndroidNetworkAdapter @Inject constructor( ContextCompat.RECEIVER_EXPORTED, ) } + + val networkRequest = NetworkRequest.Builder() + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .build() + + connectivityManager.registerNetworkCallback(networkRequest, networkCallback) } override fun isWifiEnabled(): Boolean = wifiManager.isWifiEnabled @@ -89,7 +125,7 @@ class AndroidNetworkAdapter @Inject constructor( override fun enableWifi(): KMResult<*> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - return suAdapter.execute("svc wifi enable") + return systemBridgeConnManager.run { bridge -> bridge.setWifiEnabled(true) } } else { wifiManager.isWifiEnabled = true return Success(Unit) @@ -98,7 +134,7 @@ class AndroidNetworkAdapter @Inject constructor( override fun disableWifi(): KMResult<*> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - return suAdapter.execute("svc wifi disable") + return systemBridgeConnManager.run { bridge -> bridge.setWifiEnabled(false) } } else { wifiManager.isWifiEnabled = false return Success(Unit) @@ -106,16 +142,36 @@ class AndroidNetworkAdapter @Inject constructor( } override fun isMobileDataEnabled(): Boolean { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - return telephonyManager.isDataEnabled + return telephonyManager.isDataEnabled + } + + override fun enableMobileData(): KMResult<*> { + val subId = SubscriptionManager.getDefaultSubscriptionId() + + if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + throw IllegalStateException("No valid subscription ID") + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + return systemBridgeConnManager.run { bridge -> bridge.setDataEnabled(subId, true) } } else { - return telephonyManager.dataState == TelephonyManager.DATA_CONNECTED + return suAdapter.execute("svc data enable") } } - override fun enableMobileData(): KMResult<*> = suAdapter.execute("svc data enable") + override fun disableMobileData(): KMResult<*> { + val subId = SubscriptionManager.getDefaultSubscriptionId() + + if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + throw IllegalStateException("No valid subscription ID") + } - override fun disableMobileData(): KMResult<*> = suAdapter.execute("svc data disable") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + return systemBridgeConnManager.run { bridge -> bridge.setDataEnabled(subId, false) } + } else { + return suAdapter.execute("svc data disable") + } + } /** * @return Null on Android 10+ because there is no API to do this anymore. @@ -175,4 +231,42 @@ class AndroidNetworkAdapter @Inject constructor( return KMError.MalformedUrl } } + + override fun connectWifiNetwork() { + val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + Intent(Settings.Panel.ACTION_WIFI) + } else { + Intent(Settings.ACTION_WIFI_SETTINGS) + } + + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + try { + ctx.startActivity(intent) + } catch (e: ActivityNotFoundException) { + Timber.e(e, "Failed to start Wi-Fi settings activity") + } + } + + private fun getIsWifiConnected(): Boolean { + return connectivityManager.allNetworks.any { network -> + connectivityManager.getNetworkCapabilities(network) + ?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ?: false + } + } + + private fun getWifiSSID(): String? { + return wifiManager.connectionInfo?.ssid?.let { ssid -> + if (ssid == WifiManager.UNKNOWN_SSID) { + null + } else { + ssid.removeSurrounding("\"") + } + } + } + + fun invalidateState() { + connectedWifiSSIDFlow.update { getWifiSSID() } + isWifiConnected.update { getIsWifiConnected() } + } } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/network/NetworkAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/network/NetworkAdapter.kt index d7a1f04a7c..f7f8235510 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/network/NetworkAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/network/NetworkAdapter.kt @@ -5,14 +5,16 @@ import kotlinx.coroutines.flow.Flow interface NetworkAdapter { - val connectedWifiSSID: String? val connectedWifiSSIDFlow: Flow + val isWifiConnected: Flow + fun isWifiEnabled(): Boolean fun isWifiEnabledFlow(): Flow fun enableWifi(): KMResult<*> fun disableWifi(): KMResult<*> + fun connectWifiNetwork() fun isMobileDataEnabled(): Boolean diff --git a/system/src/main/java/io/github/sds100/keymapper/system/nfc/AndroidNfcAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/nfc/AndroidNfcAdapter.kt index 0f11120f17..4d43e26e4c 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/nfc/AndroidNfcAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/nfc/AndroidNfcAdapter.kt @@ -2,10 +2,12 @@ package io.github.sds100.keymapper.system.nfc import android.content.Context import android.nfc.NfcManager +import android.os.Build import androidx.core.content.getSystemService +import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.utils.KMResult +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager import io.github.sds100.keymapper.system.root.SuAdapter -import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import javax.inject.Singleton @@ -13,14 +15,27 @@ import javax.inject.Singleton class AndroidNfcAdapter @Inject constructor( @ApplicationContext private val context: Context, private val suAdapter: SuAdapter, + private val systemBridgeConnectionManager: SystemBridgeConnectionManager ) : NfcAdapter { private val ctx = context.applicationContext - private val nfcManager: NfcManager by lazy { ctx.getSystemService()!! } + private val nfcManager: NfcManager? by lazy { ctx.getSystemService() } - override fun isEnabled(): Boolean = nfcManager.defaultAdapter.isEnabled + override fun isEnabled(): Boolean = nfcManager?.defaultAdapter?.isEnabled ?: false - override fun enable(): KMResult<*> = suAdapter.execute("svc nfc enable") + override fun enable(): KMResult<*> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + return systemBridgeConnectionManager.run { bridge -> bridge.setNfcEnabled(true) } + } else { + return suAdapter.execute("svc nfc enable") + } + } - override fun disable(): KMResult<*> = suAdapter.execute("svc nfc disable") + override fun disable(): KMResult<*> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + return systemBridgeConnectionManager.run { bridge -> bridge.setNfcEnabled(false) } + } else { + return suAdapter.execute("svc nfc disable") + } + } } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationAdapter.kt index 0b61b3a015..f62834cdac 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationAdapter.kt @@ -1,16 +1,22 @@ package io.github.sds100.keymapper.system.notifications +import io.github.sds100.keymapper.common.notifications.KMNotificationAction import kotlinx.coroutines.flow.Flow - interface NotificationAdapter { /** * The string is the ID of the action. */ - val onNotificationActionClick: Flow + val onNotificationActionClick: Flow + + /** + * Emits text input from notification actions that support RemoteInput. + */ + val onNotificationRemoteInput: Flow fun showNotification(notification: NotificationModel) fun dismissNotification(notificationId: Int) fun createChannel(channel: NotificationChannelModel) fun deleteChannel(channelId: String) + fun openChannelSettings(channelId: String) } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationModel.kt b/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationModel.kt index 30bd5c1cb3..2b0e1c1f9a 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationModel.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationModel.kt @@ -1,6 +1,8 @@ package io.github.sds100.keymapper.system.notifications import androidx.annotation.DrawableRes +import androidx.core.app.NotificationCompat +import io.github.sds100.keymapper.common.notifications.KMNotificationAction data class NotificationModel( @@ -12,32 +14,26 @@ data class NotificationModel( /** * Null if nothing should happen when the notification is tapped. */ - val onClickAction: NotificationIntentType? = null, + val onClickAction: KMNotificationAction? = null, val showOnLockscreen: Boolean, val onGoing: Boolean, - val priority: Int, - val actions: List = emptyList(), - val autoCancel: Boolean = false, - val bigTextStyle: Boolean = false, -) { - data class Action(val text: String, val intentType: NotificationIntentType) -} + /** + * On Android Oreo and newer this does nothing because the channel priority is used. + */ + val priority: Int = NotificationCompat.PRIORITY_DEFAULT, -/** - * Due to restrictions on notification trampolines in Android 12+ you can't launch - * activities from a broadcast receiver in response to a notification action. - */ -sealed class NotificationIntentType { /** - * Broadcast an intent to the NotificationReceiver. + * Maps the action intent to the label string. */ - data class Broadcast(val action: String) : NotificationIntentType() + val actions: List> = emptyList(), /** - * Launch the main activity with the specified action in the intent. If it is null - * then it will just launch the activity without a custom action. + * Clicking on the notification will automatically dismiss it. */ - data class MainActivity(val customIntentAction: String? = null) : NotificationIntentType() + val autoCancel: Boolean = false, + val bigTextStyle: Boolean = false, + val silent: Boolean = false, + val showIndeterminateProgress: Boolean = false, + val timeout: Long? = null +) - data class Activity(val action: String) : NotificationIntentType() -} diff --git a/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationRemoteInput.kt b/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationRemoteInput.kt new file mode 100644 index 0000000000..9e536e8de4 --- /dev/null +++ b/system/src/main/java/io/github/sds100/keymapper/system/notifications/NotificationRemoteInput.kt @@ -0,0 +1,13 @@ +package io.github.sds100.keymapper.system.notifications + +import io.github.sds100.keymapper.common.notifications.KMNotificationAction + +/** + * Represents text input from a notification action with RemoteInput. + * @param intentAction The intent action that triggered the text input + * @param text The text that was inputted by the user + */ +data class NotificationRemoteInput( + val intentAction: KMNotificationAction.IntentAction, + val text: String +) \ No newline at end of file diff --git a/system/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt index b81b9f8d9a..df746e8621 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/permissions/AndroidPermissionAdapter.kt @@ -5,12 +5,12 @@ import android.app.NotificationManager import android.app.admin.DevicePolicyManager import android.content.ComponentName import android.content.Context -import android.content.pm.IPackageManager import android.content.pm.PackageManager.PERMISSION_GRANTED import android.os.Build import android.os.PowerManager import android.os.Process import android.permission.IPermissionManager +import android.permission.PermissionManagerApis import android.provider.Settings import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat @@ -19,18 +19,21 @@ import dagger.hilt.android.qualifiers.ApplicationContext import io.github.sds100.keymapper.common.BuildConfigProvider import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult +import io.github.sds100.keymapper.common.utils.firstBlocking import io.github.sds100.keymapper.common.utils.getIdentifier import io.github.sds100.keymapper.common.utils.onFailure import io.github.sds100.keymapper.common.utils.onSuccess import io.github.sds100.keymapper.common.utils.success +import io.github.sds100.keymapper.common.utils.then import io.github.sds100.keymapper.data.Keys import io.github.sds100.keymapper.data.repositories.PreferenceRepository +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionManager +import io.github.sds100.keymapper.sysbridge.manager.SystemBridgeConnectionState +import io.github.sds100.keymapper.sysbridge.utils.SystemBridgeError import io.github.sds100.keymapper.system.DeviceAdmin -import io.github.sds100.keymapper.system.SystemError -import io.github.sds100.keymapper.system.apps.PackageManagerAdapter import io.github.sds100.keymapper.system.notifications.NotificationReceiverAdapter import io.github.sds100.keymapper.system.root.SuAdapter -import io.github.sds100.keymapper.system.shizuku.ShizukuUtils +import io.github.sds100.keymapper.system.shizuku.ShizukuAdapter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -59,18 +62,14 @@ class AndroidPermissionAdapter @Inject constructor( private val suAdapter: SuAdapter, private val notificationReceiverAdapter: NotificationReceiverAdapter, private val preferenceRepository: PreferenceRepository, - private val packageManagerAdapter: PackageManagerAdapter, private val buildConfigProvider: BuildConfigProvider, + private val systemBridgeConnectionManager: SystemBridgeConnectionManager, + private val shizukuAdapter: ShizukuAdapter ) : PermissionAdapter { companion object { const val REQUEST_CODE_SHIZUKU_PERMISSION = 1 } - private val shizukuPackageManager: IPackageManager by lazy { - val binder = ShizukuBinderWrapper(SystemServiceHelper.getSystemService("package")) - IPackageManager.Stub.asInterface(binder) - } - private val shizukuPermissionManager: IPermissionManager by lazy { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { HiddenApiBypass.addHiddenApiExemptions( @@ -103,7 +102,7 @@ class AndroidPermissionAdapter @Inject constructor( .stateIn(coroutineScope, SharingStarted.Eagerly, false) init { - suAdapter.isGranted + suAdapter.isRootGranted .drop(1) .onEach { onPermissionsChanged() } .launchIn(coroutineScope) @@ -144,32 +143,55 @@ class AndroidPermissionAdapter @Inject constructor( return success() } - if (isGranted(Permission.SHIZUKU)) { - result = try { - grantPermissionWithShizuku(permissionName) + val deviceId: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + ctx.deviceId + } else { + -1 + } + + val isSystemBridgeConnected = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && + systemBridgeConnectionManager.connectionState.firstBlocking() is SystemBridgeConnectionState.Connected - // if successfully granted + if (isSystemBridgeConnected) { + result = systemBridgeConnectionManager.run { bridge -> + bridge.grantPermission(permissionName, deviceId) + }.then { if (ContextCompat.checkSelfPermission(ctx, permissionName) == PERMISSION_GRANTED) { success() } else { - KMError.Exception(Exception("Failed to grant permission with Shizuku.")) + KMError.Exception(Exception("Failed to grant permission with system bridge")) } - } catch (e: Exception) { - KMError.Exception(e) + } + } else if (shizukuAdapter.isStarted.value) { + val userId = Process.myUserHandle()!!.getIdentifier() + + PermissionManagerApis.grantPermission( + shizukuPermissionManager, buildConfigProvider.packageName, + permissionName, deviceId, userId + ) + + if (ContextCompat.checkSelfPermission(ctx, permissionName) == PERMISSION_GRANTED) { + result = success() + } else { + result = + KMError.Exception(Exception("Failed to grant permission with Shizuku.")) } } else if (isGranted(Permission.ROOT)) { suAdapter.execute( "pm grant ${buildConfigProvider.packageName} $permissionName", block = true, ) + if (ContextCompat.checkSelfPermission(ctx, permissionName) == PERMISSION_GRANTED) { result = success() } else { result = - KMError.Exception(Exception("Failed to grant permission with root. Key Mapper may not actually have root permission.")) + KMError.Exception(Exception("Failed to grant permission with root.")) } } else { - result = SystemError.PermissionDenied(Permission.SHIZUKU) + // The system bridge should be the default way to grant permissions. + result = SystemBridgeError.Disconnected } result.onSuccess { @@ -181,63 +203,8 @@ class AndroidPermissionAdapter @Inject constructor( return result } - private fun grantPermissionWithShizuku(permissionName: String) { - val userId = Process.myUserHandle()!!.getIdentifier() - - try { - // In revisions of Android 14 the method to grant permissions changed - // so try them all. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - try { - shizukuPermissionManager.grantRuntimePermission( - buildConfigProvider.packageName, - permissionName, - ctx.deviceId, - userId, - ) - } catch (_: NoSuchMethodError) { - try { - shizukuPermissionManager.grantRuntimePermission( - buildConfigProvider.packageName, - permissionName, - "0", - userId, - ) - } catch (_: NoSuchMethodError) { - shizukuPermissionManager.grantRuntimePermission( - buildConfigProvider.packageName, - permissionName, - userId, - ) - } - } - // In Android 11 this method was moved from IPackageManager to IPermissionManager. - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - shizukuPermissionManager.grantRuntimePermission( - buildConfigProvider.packageName, - permissionName, - userId, - ) - } else { - shizukuPackageManager.grantRuntimePermission( - buildConfigProvider.packageName, - permissionName, - userId, - ) - } - // The API may change in future Android versions so don't crash the whole app - // just for this shizuku permission feature. - } catch (_: NoSuchMethodError) { - } - } - override fun isGranted(permission: Permission): Boolean = when (permission) { - Permission.WRITE_SETTINGS -> - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - Settings.System.canWrite(ctx) - } else { - true - } + Permission.WRITE_SETTINGS -> Settings.System.canWrite(ctx) Permission.CAMERA -> ContextCompat.checkSelfPermission( @@ -257,11 +224,11 @@ class AndroidPermissionAdapter @Inject constructor( ) == PERMISSION_GRANTED Permission.ACCESS_NOTIFICATION_POLICY -> - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !neverRequestDndPermission.value) { + if (neverRequestDndPermission.value) { + true + } else { val notificationManager: NotificationManager = ctx.getSystemService()!! notificationManager.isNotificationPolicyAccessGranted - } else { - true } Permission.WRITE_SECURE_SETTINGS -> { @@ -281,21 +248,14 @@ class AndroidPermissionAdapter @Inject constructor( Manifest.permission.CALL_PHONE, ) == PERMISSION_GRANTED - Permission.ROOT -> suAdapter.isGranted.value + Permission.ROOT -> suAdapter.isRootGranted.value Permission.IGNORE_BATTERY_OPTIMISATION -> - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val ignoringOptimisations = - powerManager?.isIgnoringBatteryOptimizations(buildConfigProvider.packageName) - - ignoringOptimisations ?: false - } else { - true - } + powerManager?.isIgnoringBatteryOptimizations(buildConfigProvider.packageName) ?: false // this check is super quick (~0ms) so this doesn't need to be cached. Permission.SHIZUKU -> { - if (ShizukuUtils.isSupportedForSdkVersion() && Shizuku.getBinder() != null) { + if (Shizuku.getBinder() != null) { Shizuku.checkSelfPermission() == PERMISSION_GRANTED } else { false diff --git a/system/src/main/java/io/github/sds100/keymapper/system/root/SuAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/root/SuAdapter.kt index 081f1deafb..f65f7cb74b 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/root/SuAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/root/SuAdapter.kt @@ -1,63 +1,42 @@ package io.github.sds100.keymapper.system.root +import com.topjohnwu.superuser.Shell import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success import io.github.sds100.keymapper.common.utils.firstBlocking -import io.github.sds100.keymapper.data.Keys -import io.github.sds100.keymapper.data.repositories.PreferenceRepository import io.github.sds100.keymapper.system.SystemError import io.github.sds100.keymapper.system.permissions.Permission -import io.github.sds100.keymapper.system.shell.ShellAdapter -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import java.io.IOException -import java.io.InputStream +import kotlinx.coroutines.flow.update +import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @Singleton -class SuAdapterImpl @Inject constructor( - coroutineScope: CoroutineScope, - private val shell: ShellAdapter, - private val preferenceRepository: PreferenceRepository, -) : SuAdapter { - private var process: Process? = null +class SuAdapterImpl @Inject constructor() : SuAdapter { + override val isRootGranted: MutableStateFlow = MutableStateFlow(false) - override val isGranted: StateFlow = preferenceRepository.get(Keys.hasRootPermission) - .map { it ?: false } - .stateIn(coroutineScope, SharingStarted.Eagerly, false) - - override fun requestPermission(): Boolean { - preferenceRepository.set(Keys.hasRootPermission, true) - - // show the su prompt - shell.run("su") + init { + Shell.getShell() + invalidateIsRooted() + } - return true + override fun requestPermission() { + invalidateIsRooted() } override fun execute(command: String, block: Boolean): KMResult<*> { - if (!isGranted.firstBlocking()) { + if (!isRootGranted.firstBlocking()) { return SystemError.PermissionDenied(Permission.ROOT) } try { if (block) { - // Don't use the long running su process because that will block the thread indefinitely - shell.run("su", "-c", command, waitFor = true) + Shell.cmd(command).exec() } else { - if (process == null) { - process = ProcessBuilder("su").start() - } - - with(process!!.outputStream.bufferedWriter()) { - write("$command\n") - flush() - } + Shell.cmd(command).submit() } return Success(Unit) @@ -66,27 +45,27 @@ class SuAdapterImpl @Inject constructor( } } - override fun getCommandOutput(command: String): KMResult { - if (!isGranted.firstBlocking()) { - return SystemError.PermissionDenied(Permission.ROOT) - } - + fun invalidateIsRooted() { try { - val inputStream = shell.getShellCommandStdOut("su", "-c", command) - return Success(inputStream) - } catch (e: IOException) { - return KMError.UnknownIOError + // Close the shell so a new one is started without root permission. + Shell.getShell().waitAndClose() + val isRooted = Shell.isAppGrantedRoot() ?: false + isRootGranted.update { isRooted } + + if (isRooted) { + Timber.i("Root access granted") + } else { + Timber.i("Root access denied") + } + } catch (e: Exception) { + Timber.e("Exception invalidating root detection: $e") } } } interface SuAdapter { - val isGranted: StateFlow + val isRootGranted: StateFlow - /** - * @return whether root permission was granted successfully - */ - fun requestPermission(): Boolean + fun requestPermission() fun execute(command: String, block: Boolean = false): KMResult<*> - fun getCommandOutput(command: String): KMResult } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/shell/ShellAdapter.kt b/system/src/main/java/io/github/sds100/keymapper/system/shell/ShellAdapter.kt index f22b677ec6..56c8b9f083 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/shell/ShellAdapter.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/shell/ShellAdapter.kt @@ -1,10 +1,8 @@ package io.github.sds100.keymapper.system.shell import io.github.sds100.keymapper.common.utils.KMResult -import java.io.InputStream interface ShellAdapter { fun run(vararg command: String, waitFor: Boolean = false): Boolean fun execute(command: String): KMResult<*> - fun getShellCommandStdOut(vararg command: String): InputStream } diff --git a/system/src/main/java/io/github/sds100/keymapper/system/Shell.kt b/system/src/main/java/io/github/sds100/keymapper/system/shell/SimpleShell.kt similarity index 58% rename from system/src/main/java/io/github/sds100/keymapper/system/Shell.kt rename to system/src/main/java/io/github/sds100/keymapper/system/shell/SimpleShell.kt index 1b9d0b7535..14a280b4e1 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/Shell.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/shell/SimpleShell.kt @@ -1,16 +1,14 @@ -package io.github.sds100.keymapper.system +package io.github.sds100.keymapper.system.shell import io.github.sds100.keymapper.common.utils.KMError import io.github.sds100.keymapper.common.utils.KMResult import io.github.sds100.keymapper.common.utils.Success -import io.github.sds100.keymapper.system.shell.ShellAdapter import java.io.IOException -import java.io.InputStream import javax.inject.Inject import javax.inject.Singleton @Singleton -class Shell @Inject constructor() : ShellAdapter { +class SimpleShell @Inject constructor() : ShellAdapter { /** * @return whether the command was executed successfully */ @@ -26,18 +24,6 @@ class Shell @Inject constructor() : ShellAdapter { false } - /** - * Remember to close it after using it. - */ - @Throws(IOException::class) - override fun getShellCommandStdOut(vararg command: String): InputStream = Runtime.getRuntime().exec(command).inputStream - - /** - * Remember to close it after using it. - */ - @Throws(IOException::class) - fun getShellCommandStdErr(vararg command: String): InputStream = Runtime.getRuntime().exec(command).errorStream - override fun execute(command: String): KMResult<*> { try { Runtime.getRuntime().exec(command) @@ -47,4 +33,4 @@ class Shell @Inject constructor() : ShellAdapter { return KMError.Exception(e) } } -} +} \ No newline at end of file diff --git a/system/src/main/java/io/github/sds100/keymapper/system/shizuku/ShizukuInputEventInjector.kt b/system/src/main/java/io/github/sds100/keymapper/system/shizuku/ShizukuInputEventInjector.kt deleted file mode 100644 index e63b4e4590..0000000000 --- a/system/src/main/java/io/github/sds100/keymapper/system/shizuku/ShizukuInputEventInjector.kt +++ /dev/null @@ -1,57 +0,0 @@ -package io.github.sds100.keymapper.system.shizuku - -import android.annotation.SuppressLint -import android.content.Context -import android.hardware.input.IInputManager -import android.os.SystemClock -import android.view.KeyEvent -import io.github.sds100.keymapper.common.utils.InputEventType -import io.github.sds100.keymapper.system.inputevents.InputEventInjector -import io.github.sds100.keymapper.system.inputmethod.InputKeyModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import rikka.shizuku.ShizukuBinderWrapper -import rikka.shizuku.SystemServiceHelper -import timber.log.Timber - -@SuppressLint("PrivateApi") -class ShizukuInputEventInjector : InputEventInjector { - - companion object { - // private const val INJECT_INPUT_EVENT_MODE_ASYNC = 0 - - private const val INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2 - } - - private val iInputManager: IInputManager by lazy { - val binder = - ShizukuBinderWrapper(SystemServiceHelper.getSystemService(Context.INPUT_SERVICE)) - IInputManager.Stub.asInterface(binder) - } - - override suspend fun inputKeyEvent(model: InputKeyModel) { - Timber.d("Inject input event with Shizuku ${KeyEvent.keyCodeToString(model.keyCode)}, $model") - - val action = when (model.inputType) { - InputEventType.DOWN, InputEventType.DOWN_UP -> KeyEvent.ACTION_DOWN - InputEventType.UP -> KeyEvent.ACTION_UP - } - - val eventTime = SystemClock.uptimeMillis() - - val keyEvent = createInjectedKeyEvent(eventTime, action, model) - - withContext(Dispatchers.IO) { - // MUST wait for the application to finish processing the event before sending the next one. - // Otherwise, rapidly repeating input events will go in a big queue and all inputs - // into the application will be delayed or overloaded. - iInputManager.injectInputEvent(keyEvent, INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH) - - if (model.inputType == InputEventType.DOWN_UP) { - val upEvent = KeyEvent.changeAction(keyEvent, KeyEvent.ACTION_UP) - - iInputManager.injectInputEvent(upEvent, INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH) - } - } - } -} diff --git a/system/src/main/java/io/github/sds100/keymapper/system/shizuku/ShizukuUtils.kt b/system/src/main/java/io/github/sds100/keymapper/system/shizuku/ShizukuUtils.kt index df8a057936..856129b345 100644 --- a/system/src/main/java/io/github/sds100/keymapper/system/shizuku/ShizukuUtils.kt +++ b/system/src/main/java/io/github/sds100/keymapper/system/shizuku/ShizukuUtils.kt @@ -11,6 +11,4 @@ object ShizukuUtils { * Android 11 because a PC/mac isn't needed after every reboot to make it work. */ fun isRecommendedForSdkVersion(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R - - fun isSupportedForSdkVersion(): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M } diff --git a/systemstubs/build.gradle.kts b/systemstubs/build.gradle.kts index 98e0963f57..9c067452a1 100644 --- a/systemstubs/build.gradle.kts +++ b/systemstubs/build.gradle.kts @@ -1,6 +1,6 @@ plugins { - id("com.android.library") - id("org.jetbrains.kotlin.android") + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) } android { @@ -36,4 +36,5 @@ android { } dependencies { + implementation(libs.androidx.annotation.jvm) } diff --git a/systemstubs/src/main/aidl/android/bluetooth/IBluetoothManager.aidl b/systemstubs/src/main/aidl/android/bluetooth/IBluetoothManager.aidl new file mode 100644 index 0000000000..f4d1a4e530 --- /dev/null +++ b/systemstubs/src/main/aidl/android/bluetooth/IBluetoothManager.aidl @@ -0,0 +1,9 @@ +package android.bluetooth; +import android.content.AttributionSource; + +interface IBluetoothManager { + // Requires Android 13+ + boolean enable(in AttributionSource attributionSource); + // Requires Android 13+ + boolean disable(in AttributionSource attributionSource, boolean persist); +} \ No newline at end of file diff --git a/systemstubs/src/main/aidl/android/content/pm/IPackageManager.aidl b/systemstubs/src/main/aidl/android/content/pm/IPackageManager.aidl index 51f2382d8b..23f67f7321 100644 --- a/systemstubs/src/main/aidl/android/content/pm/IPackageManager.aidl +++ b/systemstubs/src/main/aidl/android/content/pm/IPackageManager.aidl @@ -2,4 +2,5 @@ package android.content.pm; interface IPackageManager { void grantRuntimePermission(String packageName, String permissionName, int userId); + boolean hasSystemFeature(String name, int version); } \ No newline at end of file diff --git a/systemstubs/src/main/aidl/android/hardware/input/IInputManager.aidl b/systemstubs/src/main/aidl/android/hardware/input/IInputManager.aidl index 0cbe5f383d..15eaf6e908 100644 --- a/systemstubs/src/main/aidl/android/hardware/input/IInputManager.aidl +++ b/systemstubs/src/main/aidl/android/hardware/input/IInputManager.aidl @@ -1,6 +1,8 @@ package android.hardware.input; +import android.view.InputDevice; interface IInputManager { boolean injectInputEvent(in InputEvent event, int mode); + InputDevice getInputDevice(int id); } \ No newline at end of file diff --git a/systemstubs/src/main/aidl/android/net/IConnectivityManager.aidl b/systemstubs/src/main/aidl/android/net/IConnectivityManager.aidl new file mode 100644 index 0000000000..6402a6e148 --- /dev/null +++ b/systemstubs/src/main/aidl/android/net/IConnectivityManager.aidl @@ -0,0 +1,5 @@ +package android.net; + +interface IConnectivityManager { + void setAirplaneMode(boolean enable); +} \ No newline at end of file diff --git a/systemstubs/src/main/aidl/android/net/wifi/IWifiManager.aidl b/systemstubs/src/main/aidl/android/net/wifi/IWifiManager.aidl new file mode 100644 index 0000000000..3aab61dd86 --- /dev/null +++ b/systemstubs/src/main/aidl/android/net/wifi/IWifiManager.aidl @@ -0,0 +1,5 @@ +package android.net.wifi; + +interface IWifiManager { + boolean setWifiEnabled(String packageName, boolean enable); +} \ No newline at end of file diff --git a/systemstubs/src/main/aidl/com/android/internal/telephony/ITelephony.aidl b/systemstubs/src/main/aidl/com/android/internal/telephony/ITelephony.aidl new file mode 100644 index 0000000000..c26a955b6b --- /dev/null +++ b/systemstubs/src/main/aidl/com/android/internal/telephony/ITelephony.aidl @@ -0,0 +1,9 @@ +package com.android.internal.telephony; + +interface ITelephony { + // Requires Android 12+ + void setDataEnabledForReason(int subId, int reason, boolean enable, String callingPackage); + + // Max Android 11 + void setUserDataEnabled(int subId, boolean enable); +} \ No newline at end of file diff --git a/systemstubs/src/main/java/android/nfc/INfcAdapter.java b/systemstubs/src/main/java/android/nfc/INfcAdapter.java new file mode 100644 index 0000000000..2e4b958915 --- /dev/null +++ b/systemstubs/src/main/java/android/nfc/INfcAdapter.java @@ -0,0 +1,24 @@ +package android.nfc; + +import android.os.Build; +import android.os.IBinder; + +import androidx.annotation.RequiresApi; + +public interface INfcAdapter extends android.os.IInterface { + boolean enable(); + + boolean disable(boolean saveState); + + @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + boolean enable(String pkg); + + @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + boolean disable(boolean saveState, String pkg); + + abstract class Stub extends android.os.Binder implements INfcAdapter { + public static INfcAdapter asInterface(IBinder obj) { + throw new RuntimeException("Stub!"); + } + } +} \ No newline at end of file diff --git a/systemstubs/src/main/java/android/nfc/NfcAdapterApis.kt b/systemstubs/src/main/java/android/nfc/NfcAdapterApis.kt new file mode 100644 index 0000000000..3f7dce07c9 --- /dev/null +++ b/systemstubs/src/main/java/android/nfc/NfcAdapterApis.kt @@ -0,0 +1,21 @@ +package android.nfc + +import android.os.Build + +object NfcAdapterApis { + fun enable(adapter: INfcAdapter, packageName: String) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + adapter.enable(packageName) + } else { + adapter.enable() + } + } + + fun disable(adapter: INfcAdapter, saveState: Boolean, packageName: String) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + adapter.disable(saveState, packageName) + } else { + adapter.disable(saveState) + } + } +} \ No newline at end of file diff --git a/systemstubs/src/main/java/android/permission/PermissionManagerApis.kt b/systemstubs/src/main/java/android/permission/PermissionManagerApis.kt new file mode 100644 index 0000000000..c5983b5a34 --- /dev/null +++ b/systemstubs/src/main/java/android/permission/PermissionManagerApis.kt @@ -0,0 +1,54 @@ +package android.permission + +import android.os.Build + +object PermissionManagerApis { + fun grantPermission( + permissionManager: IPermissionManager, + packageName: String, + permission: String, + deviceId: Int, + userId: Int + ) { + // In revisions of Android 14 the method to grant permissions changed + // so try them all. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + try { + permissionManager.grantRuntimePermission( + packageName, + permission, + deviceId, + userId, + ) + } catch (_: NoSuchMethodError) { + try { + permissionManager.grantRuntimePermission( + packageName, + permission, + "0", + userId, + ) + } catch (_: NoSuchMethodError) { + permissionManager.grantRuntimePermission( + packageName, + permission, + userId, + ) + } + } + // In Android 11 this method was moved from IPackageManager to IPermissionManager. + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + permissionManager.grantRuntimePermission( + packageName, + permission, + userId, + ) + } else { + permissionManager.grantRuntimePermission( + packageName, + permission, + userId, + ) + } + } +} \ No newline at end of file diff --git a/systemstubs/src/main/java/com/android/org/conscrypt/Conscrypt.java b/systemstubs/src/main/java/com/android/org/conscrypt/Conscrypt.java new file mode 100644 index 0000000000..14396b0421 --- /dev/null +++ b/systemstubs/src/main/java/com/android/org/conscrypt/Conscrypt.java @@ -0,0 +1,28 @@ +package com.android.org.conscrypt; + +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSocket; + +import androidx.annotation.RequiresApi; + +@RequiresApi(29) +public class Conscrypt { + + /** + * Exports a value derived from the TLS master secret as described in RFC 5705. + * + * @param label the label to use in calculating the exported value. This must be + * an ASCII-only string. + * @param context the application-specific context value to use in calculating the + * exported value. This may be {@code null} to use no application context, which is + * treated differently than an empty byte array. + * @param length the number of bytes of keying material to return. + * @return a value of the specified length, or {@code null} if the handshake has not yet + * completed or the connection has been closed. + * @throws SSLException if the value could not be exported. + */ + public static byte[] exportKeyingMaterial(SSLSocket socket, String label, byte[] context, + int length) throws SSLException { + throw new RuntimeException("STUB"); + } +}