From 5a9b8a2b9e9d62b6bf0dae6dad8938f8beb09919 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 4 Apr 2025 15:05:12 -0600 Subject: [PATCH 1/7] chore: bump version to 3.0.0-beta.5 --- app/version.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/version.properties b/app/version.properties index 993f2b53ec..4e9bc706c6 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,3 +1,3 @@ -VERSION_NAME=3.0.0-beta.4 -VERSION_CODE=91 +VERSION_NAME=3.0.0-beta.5 +VERSION_CODE=93 VERSION_NUM=0 \ No newline at end of file From 473626466397309756d018a0918e4bef86acdb70 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 4 Apr 2025 17:35:48 -0600 Subject: [PATCH 2/7] #1627 fix: open camera app action does not work when device is locked --- CHANGELOG.md | 8 +++++++ .../apps/AndroidPackageManagerAdapter.kt | 21 +++++++++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6adf831157..cac375f939 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## [3.0 Beta 5](https://github.com/sds100/KeyMapper/releases/tag/v3.0.0-beta.5) + +#### TO BE RELEASED + +## Bug fixes + +- #1627 open camera app action does not work when device is locked + ## [3.0 Beta 4](https://github.com/sds100/KeyMapper/releases/tag/v3.0.0-beta.4) #### 2 April 2025 diff --git a/app/src/main/java/io/github/sds100/keymapper/system/apps/AndroidPackageManagerAdapter.kt b/app/src/main/java/io/github/sds100/keymapper/system/apps/AndroidPackageManagerAdapter.kt index e6231120c3..de6061a27c 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/apps/AndroidPackageManagerAdapter.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/apps/AndroidPackageManagerAdapter.kt @@ -281,14 +281,31 @@ class AndroidPackageManagerAdapter( override fun launchCameraApp(): Result<*> { try { - Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA).apply { + /** + * See this guide on how the camera is launched with double press power button in + * SystemUI. https://cs.android.com/android/platform/superproject/+/master:frameworks/base/packages/SystemUI/docs/camera.md + * + * First launch the SECURE camera intent so the camera opens when the device + * is locked. + */ + Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK ctx.startActivity(this) } return Success(Unit) } catch (e: ActivityNotFoundException) { - return Error.NoCameraApp + // Just in case the camera app didn't implement the secure intent, try the normal one. + try { + Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + ctx.startActivity(this) + } + + return Success(Unit) + } catch (e: ActivityNotFoundException) { + return Error.NoCameraApp + } } } From 2ef7e50e3f8e1a54cd1c3323dd4c3ada143988c5 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 4 Apr 2025 19:34:37 -0600 Subject: [PATCH 3/7] chore: upgrade libraries --- app/build.gradle | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index f148f0d864..e9d6715d93 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -173,7 +173,7 @@ dependencies { // kotlin stuff implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0" // random stuff implementation "com.google.android.material:material:1.13.0-alpha12" @@ -189,7 +189,7 @@ dependencies { implementation "dev.rikka.shizuku:api:$shizuku_version" implementation "dev.rikka.shizuku:provider:$shizuku_version" implementation "org.lsposed.hiddenapibypass:hiddenapibypass:4.3" - proImplementation 'com.revenuecat.purchases:purchases:8.14.2' + proImplementation 'com.revenuecat.purchases:purchases:8.15.0' proImplementation "com.airbnb.android:lottie-compose:6.6.3" // splitties @@ -220,7 +220,7 @@ dependencies { implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" implementation "androidx.room:room-runtime:$room_version" implementation "androidx.viewpager2:viewpager2:1.1.0" - implementation "androidx.datastore:datastore-preferences:1.1.3" + implementation "androidx.datastore:datastore-preferences:1.1.4" implementation "androidx.core:core-splashscreen:1.0.1" implementation "androidx.activity:activity-compose:1.10.1" implementation "androidx.navigation:navigation-compose:2.8.9" @@ -228,7 +228,7 @@ dependencies { ksp "androidx.room:room-compiler:$room_version" // Compose - Dependency composeBom = platform('androidx.compose:compose-bom-beta:2025.03.00') + Dependency composeBom = platform('androidx.compose:compose-bom-beta:2025.03.01') implementation composeBom implementation 'androidx.compose.foundation:foundation' implementation "androidx.compose.ui:ui-android" @@ -254,12 +254,12 @@ dependencies { testImplementation "org.hamcrest:hamcrest-all:1.3" testImplementation "androidx.test.ext:junit-ktx:$androidXTestExtKotlinRunnerVersion" testImplementation "androidx.test:core-ktx:1.6.1" - testImplementation "org.robolectric:robolectric:4.12.1" + testImplementation "org.robolectric:robolectric:4.14.1" testImplementation "androidx.arch.core:core-testing:2.2.0" testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion" testImplementation "pl.pragmatists:JUnitParams:1.1.1" testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0" - testImplementation "org.mockito:mockito-core:5.2.0" + testImplementation "org.mockito:mockito-core:5.15.2" testImplementation "org.mockito:mockito-inline:5.2.0" androidTestImplementation "androidx.test.ext:junit:$androidXTestExtKotlinRunnerVersion" From 29200b5d6c8e26de2001f51efab4209bda0fb9c6 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 4 Apr 2025 20:59:05 -0600 Subject: [PATCH 4/7] #1625 create HTTP request action --- CHANGELOG.md | 6 +- app/build.gradle | 1 + app/src/main/assets/whats-new.txt | 1 + .../sds100/keymapper/actions/ActionData.kt | 11 + .../actions/ActionDataEntityMapper.kt | 39 ++- .../sds100/keymapper/actions/ActionId.kt | 2 + .../keymapper/actions/ActionUiHelper.kt | 1 + .../sds100/keymapper/actions/ActionUtils.kt | 7 +- .../sds100/keymapper/actions/ActionsScreen.kt | 1 + .../keymapper/actions/ChooseActionScreen.kt | 1 + .../keymapper/actions/CreateActionDelegate.kt | 16 + .../actions/HttpRequestBottomSheet.kt | 307 ++++++++++++++++++ .../actions/PerformActionsUseCase.kt | 12 + .../keymapper/data/entities/ActionEntity.kt | 4 + .../system/network/AndroidNetworkAdapter.kt | 40 ++- .../keymapper/system/network/HttpMethod.kt | 10 + .../system/network/NetworkAdapter.kt | 6 + .../util/ui/compose/KeyMapperDropdownMenu.kt | 62 ++++ app/src/main/res/values/strings.xml | 7 + docs/user-guide/actions.md | 4 +- 20 files changed, 533 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/io/github/sds100/keymapper/actions/HttpRequestBottomSheet.kt create mode 100644 app/src/main/java/io/github/sds100/keymapper/system/network/HttpMethod.kt create mode 100644 app/src/main/java/io/github/sds100/keymapper/util/ui/compose/KeyMapperDropdownMenu.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index cac375f939..be493ae0d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ ## [3.0 Beta 5](https://github.com/sds100/KeyMapper/releases/tag/v3.0.0-beta.5) -#### TO BE RELEASED +#### 4 April 2025 + +- #1625 HTTP Request action. + +### ## Bug fixes diff --git a/app/build.gradle b/app/build.gradle index e9d6715d93..8d92d65056 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -191,6 +191,7 @@ dependencies { implementation "org.lsposed.hiddenapibypass:hiddenapibypass:4.3" proImplementation 'com.revenuecat.purchases:purchases:8.15.0' proImplementation "com.airbnb.android:lottie-compose:6.6.3" + implementation("com.squareup.okhttp3:okhttp:4.12.0") // splitties implementation "com.louiscad.splitties:splitties-bitflags:$splitties_version" diff --git a/app/src/main/assets/whats-new.txt b/app/src/main/assets/whats-new.txt index 35aa74b350..8fceb7a8e2 100644 --- a/app/src/main/assets/whats-new.txt +++ b/app/src/main/assets/whats-new.txt @@ -5,6 +5,7 @@ Key Mapper 3.0 is here! 🎉 🗂️ Grouping key maps into folders with shared constraints. 🔦 You can now change the flashlight brightness. Tip: use the constraint for when the flashlight is showing to remap your volume buttons to change the brightness. +🛜 Send HTTP requests with a new action. ❤️ There are also tonnes of improvements to make your key mapping experience more enjoyable. diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt index 7bf9998c92..bc7f8bdea7 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt @@ -5,6 +5,7 @@ import io.github.sds100.keymapper.system.camera.CameraLens import io.github.sds100.keymapper.system.display.Orientation import io.github.sds100.keymapper.system.intents.IntentExtraModel import io.github.sds100.keymapper.system.intents.IntentTarget +import io.github.sds100.keymapper.system.network.HttpMethod import io.github.sds100.keymapper.system.volume.DndMode import io.github.sds100.keymapper.system.volume.RingerMode import io.github.sds100.keymapper.system.volume.VolumeStream @@ -828,4 +829,14 @@ sealed class ActionData : Comparable { object DeviceControls : ActionData() { override val id: ActionId = ActionId.DEVICE_CONTROLS } + + @Serializable + data class HttpRequest( + val description: String, + val method: HttpMethod, + val url: String, + val body: String, + ) : ActionData() { + override val id: ActionId = ActionId.HTTP_REQUEST + } } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt index c83b6f60b8..febbfa3e85 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt @@ -8,6 +8,7 @@ import io.github.sds100.keymapper.data.entities.getData import io.github.sds100.keymapper.system.camera.CameraLens import io.github.sds100.keymapper.system.intents.IntentExtraModel import io.github.sds100.keymapper.system.intents.IntentTarget +import io.github.sds100.keymapper.system.network.HttpMethod import io.github.sds100.keymapper.system.volume.DndMode import io.github.sds100.keymapper.system.volume.RingerMode import io.github.sds100.keymapper.system.volume.VolumeStream @@ -15,7 +16,6 @@ import io.github.sds100.keymapper.util.getKey import io.github.sds100.keymapper.util.success import io.github.sds100.keymapper.util.then import io.github.sds100.keymapper.util.valueOrNull -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import splitties.bitflags.hasFlag @@ -496,6 +496,27 @@ object ActionDataEntityMapper { ActionId.ANSWER_PHONE_CALL -> ActionData.AnswerCall ActionId.END_PHONE_CALL -> ActionData.EndCall ActionId.DEVICE_CONTROLS -> ActionData.DeviceControls + ActionId.HTTP_REQUEST -> { + val method = entity.extras.getData(ActionEntity.EXTRA_HTTP_METHOD).then { + HTTP_METHOD_MAP.getKey(it)!!.success() + }.valueOrNull() ?: return null + + val description = + entity.extras.getData(ActionEntity.EXTRA_HTTP_DESCRIPTION).valueOrNull() + ?: return null + + val url = entity.extras.getData(ActionEntity.EXTRA_HTTP_URL).valueOrNull() + ?: return null + + val body = entity.extras.getData(ActionEntity.EXTRA_HTTP_BODY).valueOrNull() ?: "" + + ActionData.HttpRequest( + description = description, + method = method, + url = url, + body = body, + ) + } } } @@ -713,6 +734,13 @@ object ActionDataEntityMapper { EntityExtra(ActionEntity.EXTRA_SOUND_FILE_DESCRIPTION, data.soundDescription), ) + is ActionData.HttpRequest -> listOf( + EntityExtra(ActionEntity.EXTRA_HTTP_DESCRIPTION, data.description), + EntityExtra(ActionEntity.EXTRA_HTTP_METHOD, HTTP_METHOD_MAP[data.method]!!), + EntityExtra(ActionEntity.EXTRA_HTTP_URL, data.url), + EntityExtra(ActionEntity.EXTRA_HTTP_BODY, data.body), + ) + else -> emptyList() } @@ -750,6 +778,14 @@ object ActionDataEntityMapper { IntentTarget.SERVICE to "SERVICE", ) + private val HTTP_METHOD_MAP = mapOf( + HttpMethod.GET to "GET", + HttpMethod.POST to "POST", + HttpMethod.PUT to "PUT", + HttpMethod.DELETE to "DELETE", + HttpMethod.PATCH to "PATCH", + ) + /** * DON'T CHANGE THESE */ @@ -865,5 +901,6 @@ object ActionDataEntityMapper { ActionId.ANSWER_PHONE_CALL to "answer_phone_call", ActionId.END_PHONE_CALL to "end_phone_call", ActionId.DEVICE_CONTROLS to "device_controls", + ActionId.HTTP_REQUEST to "http_request", ) } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionId.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionId.kt index c50e82757b..515474d744 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionId.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionId.kt @@ -129,4 +129,6 @@ enum class ActionId { ANSWER_PHONE_CALL, END_PHONE_CALL, DEVICE_CONTROLS, + + HTTP_REQUEST, } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt index bf9a9d70d7..755353b04d 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionUiHelper.kt @@ -528,6 +528,7 @@ class ActionUiHelper( ActionData.EndCall -> getString(R.string.action_end_call) ActionData.DeviceControls -> getString(R.string.action_device_controls) + is ActionData.HttpRequest -> action.description } fun getIcon(action: ActionData): ComposeIconInfo = when (action) { diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionUtils.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionUtils.kt index 8c3abdb624..f1f755074e 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionUtils.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionUtils.kt @@ -32,6 +32,7 @@ import androidx.compose.material.icons.outlined.FlashlightOff import androidx.compose.material.icons.outlined.FlashlightOn import androidx.compose.material.icons.outlined.Fullscreen import androidx.compose.material.icons.outlined.Home +import androidx.compose.material.icons.outlined.Http import androidx.compose.material.icons.outlined.Keyboard import androidx.compose.material.icons.outlined.KeyboardHide import androidx.compose.material.icons.outlined.Link @@ -220,6 +221,7 @@ object ActionUtils { ActionId.TEXT_PASTE -> ActionCategory.CONTENT ActionId.SCREENSHOT -> ActionCategory.CONTENT ActionId.URL -> ActionCategory.CONTENT + ActionId.HTTP_REQUEST -> ActionCategory.CONTENT ActionId.PHONE_CALL -> ActionCategory.TELEPHONY ActionId.ANSWER_PHONE_CALL -> ActionCategory.TELEPHONY @@ -339,6 +341,7 @@ object ActionUtils { ActionId.ANSWER_PHONE_CALL -> R.string.action_answer_call ActionId.END_PHONE_CALL -> R.string.action_end_call ActionId.DEVICE_CONTROLS -> R.string.action_device_controls + ActionId.HTTP_REQUEST -> R.string.action_http_request } @DrawableRes @@ -433,7 +436,6 @@ object ActionUtils { ActionId.CONSUME_KEY_EVENT -> null ActionId.OPEN_SETTINGS -> R.drawable.ic_outline_settings_24 ActionId.SHOW_POWER_MENU -> R.drawable.ic_outline_power_settings_new_24 - ActionId.APP -> R.drawable.ic_outline_android_24 ActionId.APP_SHORTCUT -> R.drawable.ic_outline_open_in_new_24 ActionId.KEY_CODE -> R.drawable.ic_q_24 @@ -451,6 +453,7 @@ object ActionUtils { ActionId.ANSWER_PHONE_CALL -> R.drawable.ic_outline_call_24 ActionId.END_PHONE_CALL -> R.drawable.ic_outline_call_end_24 ActionId.DEVICE_CONTROLS -> R.drawable.ic_home_automation + ActionId.HTTP_REQUEST -> null } fun getMinApi(id: ActionId): Int = when (id) { @@ -766,6 +769,7 @@ object ActionUtils { ActionId.ANSWER_PHONE_CALL -> Icons.Outlined.Call ActionId.END_PHONE_CALL -> Icons.Outlined.CallEnd ActionId.DEVICE_CONTROLS -> KeyMapperIcons.HomeIotDevice + ActionId.HTTP_REQUEST -> Icons.Outlined.Http } } @@ -817,6 +821,7 @@ fun ActionData.isEditable(): Boolean = when (this) { is ActionData.Text, is ActionData.Url, is ActionData.PhoneCall, + is ActionData.HttpRequest, -> true else -> false diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionsScreen.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionsScreen.kt index ee4821ee43..003235c51e 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionsScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionsScreen.kt @@ -68,6 +68,7 @@ fun ActionsScreen(modifier: Modifier = Modifier, viewModel: ConfigActionsViewMod EnableFlashlightActionBottomSheet(viewModel.createActionDelegate) ChangeFlashlightStrengthActionBottomSheet(viewModel.createActionDelegate) + HttpRequestBottomSheet(viewModel.createActionDelegate) ActionsScreen( modifier = modifier, diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ChooseActionScreen.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ChooseActionScreen.kt index 5715ad53a0..9001bf183b 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ChooseActionScreen.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ChooseActionScreen.kt @@ -68,6 +68,7 @@ fun ChooseActionScreen( EnableFlashlightActionBottomSheet(viewModel.createActionDelegate) ChangeFlashlightStrengthActionBottomSheet(viewModel.createActionDelegate) + HttpRequestBottomSheet(viewModel.createActionDelegate) ChooseActionScreen( modifier = modifier, diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/CreateActionDelegate.kt b/app/src/main/java/io/github/sds100/keymapper/actions/CreateActionDelegate.kt index 7547a03025..f90c762cf2 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/CreateActionDelegate.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/CreateActionDelegate.kt @@ -13,6 +13,7 @@ import io.github.sds100.keymapper.system.camera.CameraLensUtils import io.github.sds100.keymapper.system.display.Orientation import io.github.sds100.keymapper.system.display.OrientationUtils import io.github.sds100.keymapper.system.intents.ConfigIntentResult +import io.github.sds100.keymapper.system.network.HttpMethod import io.github.sds100.keymapper.system.volume.DndMode import io.github.sds100.keymapper.system.volume.DndModeUtils import io.github.sds100.keymapper.system.volume.RingerMode @@ -54,6 +55,8 @@ class CreateActionDelegate( null, ) + var httpRequestBottomSheetState: ActionData.HttpRequest? by mutableStateOf(null) + init { coroutineScope.launch { useCase.isFlashlightEnabled().collectLatest { enabled -> @@ -769,6 +772,19 @@ class CreateActionDelegate( ActionId.ANSWER_PHONE_CALL -> return ActionData.AnswerCall ActionId.END_PHONE_CALL -> return ActionData.EndCall ActionId.DEVICE_CONTROLS -> return ActionData.DeviceControls + ActionId.HTTP_REQUEST -> { + if (oldData == null) { + httpRequestBottomSheetState = ActionData.HttpRequest( + description = "", + method = HttpMethod.GET, + url = "", + body = "", + ) + } else { + httpRequestBottomSheetState = oldData as? ActionData.HttpRequest + } + return null + } } } } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/HttpRequestBottomSheet.kt b/app/src/main/java/io/github/sds100/keymapper/actions/HttpRequestBottomSheet.kt new file mode 100644 index 0000000000..17056bcdd8 --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/actions/HttpRequestBottomSheet.kt @@ -0,0 +1,307 @@ +package io.github.sds100.keymapper.actions + +import androidx.compose.foundation.layout.Arrangement +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.github.sds100.keymapper.R +import io.github.sds100.keymapper.compose.KeyMapperTheme +import io.github.sds100.keymapper.system.network.HttpMethod +import io.github.sds100.keymapper.util.ui.compose.KeyMapperDropdownMenu +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HttpRequestBottomSheet(delegate: CreateActionDelegate) { + val scope = rememberCoroutineScope() + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + if (delegate.httpRequestBottomSheetState != null) { + HttpRequestBottomSheet( + sheetState = sheetState, + onDismissRequest = { + delegate.httpRequestBottomSheetState = null + }, + state = delegate.httpRequestBottomSheetState!!, + onSelectMethod = { + delegate.httpRequestBottomSheetState = + delegate.httpRequestBottomSheetState?.copy(method = it) + }, + onDescriptionChanged = { + delegate.httpRequestBottomSheetState = + delegate.httpRequestBottomSheetState?.copy(description = it) + }, + onUrlChanged = { + delegate.httpRequestBottomSheetState = + delegate.httpRequestBottomSheetState?.copy(url = it) + }, + onBodyChanged = { + delegate.httpRequestBottomSheetState = + delegate.httpRequestBottomSheetState?.copy(body = it) + }, + onDoneClick = { + val result = delegate.httpRequestBottomSheetState ?: return@HttpRequestBottomSheet + delegate.httpRequestBottomSheetState = null + delegate.actionResult.update { result } + }, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun HttpRequestBottomSheet( + sheetState: SheetState, + onDismissRequest: () -> Unit, + state: ActionData.HttpRequest, + onSelectMethod: (HttpMethod) -> Unit = {}, + onDescriptionChanged: (String) -> Unit = {}, + onUrlChanged: (String) -> Unit = {}, + onBodyChanged: (String) -> Unit = {}, + onDoneClick: () -> Unit = {}, +) { + val scrollState = rememberScrollState() + val scope = rememberCoroutineScope() + var methodExpanded by rememberSaveable { mutableStateOf(false) } + + val descriptionEmptyErrorString = + stringResource(R.string.action_http_request_description_empty_error) + val urlEmptyErrorString = stringResource(R.string.action_http_request_url_empty_error) + + var descriptionError: String? by rememberSaveable { mutableStateOf(null) } + var urlError: String? by rememberSaveable { mutableStateOf(null) } + + ModalBottomSheet( + onDismissRequest = onDismissRequest, + sheetState = sheetState, + dragHandle = null, + ) { + Column( + modifier = Modifier.verticalScroll(scrollState), + ) { + Spacer(modifier = Modifier.height(16.dp)) + + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp), + textAlign = TextAlign.Center, + text = stringResource(R.string.action_http_request), + style = MaterialTheme.typography.headlineMedium, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + KeyMapperDropdownMenu( + modifier = Modifier + .wrapContentWidth() + .padding(horizontal = 16.dp), + expanded = methodExpanded, + onExpandedChange = { methodExpanded = it }, + value = state.method.toString(), + onValueChanged = { + onSelectMethod(HttpMethod.valueOf(it)) + }, + + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + value = state.description, + label = { Text(stringResource(R.string.action_http_request_description_label)) }, + onValueChange = { + descriptionError = null + onDescriptionChanged(it) + }, + maxLines = 1, + singleLine = true, + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.Sentences, + autoCorrectEnabled = true, + keyboardType = KeyboardType.Uri, + ), + isError = descriptionError != null, + supportingText = { + if (descriptionError != null) { + Text( + text = descriptionError!!, + color = MaterialTheme.colorScheme.error, + ) + } + }, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + value = state.url, + label = { Text(stringResource(R.string.action_http_request_url_label)) }, + onValueChange = { + urlError = null + onUrlChanged(it) + }, + maxLines = 1, + singleLine = true, + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.None, + autoCorrectEnabled = false, + keyboardType = KeyboardType.Uri, + ), + isError = urlError != null, + supportingText = { + if (urlError != null) { + Text( + text = urlError!!, + color = MaterialTheme.colorScheme.error, + ) + } + }, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + value = state.body, + label = { Text(stringResource(R.string.action_http_request_body_label)) }, + onValueChange = { + onBodyChanged(it) + }, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + OutlinedButton( + modifier = Modifier.weight(1f), + onClick = { + scope.launch { + sheetState.hide() + onDismissRequest() + } + }, + ) { + Text(stringResource(R.string.neg_cancel)) + } + + Spacer(modifier = Modifier.width(16.dp)) + + Button( + modifier = Modifier.weight(1f), + onClick = { + if (state.description.isBlank()) { + descriptionError = descriptionEmptyErrorString + } + + if (state.url.isBlank()) { + urlError = urlEmptyErrorString + } + + if (descriptionError == null && urlError == null) { + onDoneClick() + } + }, + ) { + Text(stringResource(R.string.pos_done)) + } + } + + Spacer(Modifier.height(16.dp)) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Composable +private fun PreviewEmpty() { + KeyMapperTheme { + val sheetState = SheetState( + skipPartiallyExpanded = true, + density = LocalDensity.current, + initialValue = SheetValue.Expanded, + ) + HttpRequestBottomSheet( + sheetState = sheetState, + onDismissRequest = {}, + state = ActionData.HttpRequest( + description = "", + method = HttpMethod.GET, + url = "", + body = "", + ), + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Composable +private fun PreviewFilled() { + KeyMapperTheme { + val sheetState = SheetState( + skipPartiallyExpanded = true, + density = LocalDensity.current, + initialValue = SheetValue.Expanded, + ) + HttpRequestBottomSheet( + sheetState = sheetState, + onDismissRequest = {}, + state = ActionData.HttpRequest( + description = "Example HTTP request", + method = HttpMethod.GET, + url = "https://example.com", + body = "Hello, world!", + ), + ) + } +} diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt index 5313492065..272387f893 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt @@ -813,6 +813,18 @@ class PerformActionsUseCaseImpl( extras = emptyList(), ) } + + is ActionData.HttpRequest -> { + coroutineScope.launch { + networkAdapter.sendHttpRequest( + method = action.method, + url = action.url, + body = action.body, + ).showErrorMessageOnFail() + } + + result = null + } } when (result) { diff --git a/app/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt b/app/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt index ee8b96ef7b..d912bc0eb1 100644 --- a/app/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt +++ b/app/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt @@ -84,6 +84,10 @@ data class ActionEntity( const val EXTRA_SOUND_FILE_DESCRIPTION = "extra_sound_file_description" const val EXTRA_INTENT_EXTRAS = "extra_intent_extras" const val EXTRA_FLASH_STRENGTH = "extra_flash_strength" + const val EXTRA_HTTP_METHOD = "extra_http_method" + const val EXTRA_HTTP_URL = "extra_http_url" + const val EXTRA_HTTP_BODY = "extra_http_body" + const val EXTRA_HTTP_DESCRIPTION = "extra_http_description" // DON'T CHANGE THESE. Used for JSON serialization and parsing. const val NAME_ACTION_TYPE = "type" diff --git a/app/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt b/app/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt index debff5a434..f021724fba 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt @@ -10,11 +10,20 @@ import android.telephony.TelephonyManager import androidx.core.content.ContextCompat import androidx.core.content.getSystemService import io.github.sds100.keymapper.system.root.SuAdapter +import io.github.sds100.keymapper.util.Error import io.github.sds100.keymapper.util.Result import io.github.sds100.keymapper.util.Success +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okio.IOException +import okio.use +import timber.log.Timber /** * Created by sds100 on 24/04/2021. @@ -24,9 +33,9 @@ class AndroidNetworkAdapter( private val suAdapter: SuAdapter, ) : NetworkAdapter { private val ctx = context.applicationContext - private val wifiManager: WifiManager by lazy { ctx.getSystemService()!! } private val telephonyManager: TelephonyManager by lazy { ctx.getSystemService()!! } + private val httpClient: OkHttpClient by lazy { OkHttpClient() } private val broadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { @@ -118,4 +127,33 @@ class AndroidNetworkAdapter( } ?: emptyList() } } + + override suspend fun sendHttpRequest(method: HttpMethod, url: String, body: String): Result<*> { + try { + val requestBody = when (method) { + HttpMethod.HEAD -> Request.Builder().head() + HttpMethod.PUT -> Request.Builder().put(body.toRequestBody()) + HttpMethod.POST -> Request.Builder().post(body.toRequestBody()) + HttpMethod.GET -> Request.Builder().get() + HttpMethod.DELETE -> Request.Builder().delete() + HttpMethod.PATCH -> Request.Builder().patch(body.toRequestBody()) + } + + val request = requestBody + .url("https://posttestserver.dev/p/kmr33yjcz5h38hkq/post") + .build() + + withContext(Dispatchers.IO) { httpClient.newCall(request).execute() } + .use { response -> + Timber.e(response.toString()) + if (!response.isSuccessful) { + return Error.UnknownIOError + } + + return Success(Unit) + } + } catch (e: IOException) { + return Error.UnknownIOError + } + } } diff --git a/app/src/main/java/io/github/sds100/keymapper/system/network/HttpMethod.kt b/app/src/main/java/io/github/sds100/keymapper/system/network/HttpMethod.kt new file mode 100644 index 0000000000..9c76e3838e --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/system/network/HttpMethod.kt @@ -0,0 +1,10 @@ +package io.github.sds100.keymapper.system.network + +enum class HttpMethod { + HEAD, + PUT, + POST, + GET, + DELETE, + PATCH, +} diff --git a/app/src/main/java/io/github/sds100/keymapper/system/network/NetworkAdapter.kt b/app/src/main/java/io/github/sds100/keymapper/system/network/NetworkAdapter.kt index 79df4d858c..ba9c4b3108 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/network/NetworkAdapter.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/network/NetworkAdapter.kt @@ -22,4 +22,10 @@ interface NetworkAdapter { fun disableMobileData(): Result<*> fun getKnownWifiSSIDs(): List? + + suspend fun sendHttpRequest( + method: HttpMethod, + url: String, + body: String, + ): Result<*> } diff --git a/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/KeyMapperDropdownMenu.kt b/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/KeyMapperDropdownMenu.kt new file mode 100644 index 0000000000..f293b15968 --- /dev/null +++ b/app/src/main/java/io/github/sds100/keymapper/util/ui/compose/KeyMapperDropdownMenu.kt @@ -0,0 +1,62 @@ +package io.github.sds100.keymapper.util.ui.compose + +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MenuAnchorType +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import io.github.sds100.keymapper.R +import io.github.sds100.keymapper.system.network.HttpMethod + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +fun KeyMapperDropdownMenu( + modifier: Modifier = Modifier, + expanded: Boolean, + onExpandedChange: (Boolean) -> Unit = {}, + value: String, + onValueChanged: (String) -> Unit = {}, +) { + ExposedDropdownMenuBox( + modifier = modifier, + expanded = expanded, + onExpandedChange = onExpandedChange, + ) { + TextField( + modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable), + value = value, + onValueChange = onValueChanged, + readOnly = true, + label = { Text(stringResource(R.string.action_http_request_method_label)) }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + colors = ExposedDropdownMenuDefaults.textFieldColors(), + ) + ExposedDropdownMenu( + matchTextFieldWidth = true, + expanded = expanded, + onDismissRequest = { onExpandedChange(false) }, + ) { + for (method in HttpMethod.entries) { + DropdownMenuItem( + text = { + Text( + method.toString(), + style = MaterialTheme.typography.bodyLarge, + ) + }, + onClick = { + onValueChanged(method.toString()) + onExpandedChange(false) + }, + contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding, + ) + } + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 47df1d9510..55fd27594a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1077,6 +1077,13 @@ Dismiss most recent notification Dismiss all notifications Device controls screen + HTTP request + HTTP Method + Description + Can not be empty! + URL + Can not be empty! + Request body (optional) diff --git a/docs/user-guide/actions.md b/docs/user-guide/actions.md index e1e2e879f5..022e539834 100644 --- a/docs/user-guide/actions.md +++ b/docs/user-guide/actions.md @@ -238,4 +238,6 @@ respectively. ### Dismiss all notifications (2.4.0+) -### Dismiss most recent notification (2.4.0+) \ No newline at end of file +### Dismiss most recent notification (2.4.0+) + +### HTTP Request (3.0.0+) \ No newline at end of file From 19e1b0cc7d8335eaefc74eb50c3d49050d1f1d75 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 4 Apr 2025 21:10:55 -0600 Subject: [PATCH 5/7] #1625 add an authorization header field --- .../sds100/keymapper/actions/ActionData.kt | 1 + .../actions/ActionDataEntityMapper.kt | 9 +++++++++ .../keymapper/actions/CreateActionDelegate.kt | 1 + .../actions/HttpRequestBottomSheet.kt | 18 ++++++++++++++++++ .../keymapper/actions/PerformActionsUseCase.kt | 1 + .../keymapper/data/entities/ActionEntity.kt | 1 + .../system/network/AndroidNetworkAdapter.kt | 15 ++++++++++++++- .../keymapper/system/network/NetworkAdapter.kt | 1 + app/src/main/res/values/strings.xml | 2 ++ 9 files changed, 48 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt index bc7f8bdea7..61f5ad89b0 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionData.kt @@ -836,6 +836,7 @@ sealed class ActionData : Comparable { val method: HttpMethod, val url: String, val body: String, + val authorizationHeader: String, ) : ActionData() { override val id: ActionId = ActionId.HTTP_REQUEST } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt b/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt index febbfa3e85..53a6515254 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/ActionDataEntityMapper.kt @@ -510,11 +510,16 @@ object ActionDataEntityMapper { val body = entity.extras.getData(ActionEntity.EXTRA_HTTP_BODY).valueOrNull() ?: "" + val authorizationHeader = + entity.extras.getData(ActionEntity.EXTRA_HTTP_AUTHORIZATION_HEADER) + .valueOrNull() ?: "" + ActionData.HttpRequest( description = description, method = method, url = url, body = body, + authorizationHeader = authorizationHeader, ) } } @@ -739,6 +744,10 @@ object ActionDataEntityMapper { EntityExtra(ActionEntity.EXTRA_HTTP_METHOD, HTTP_METHOD_MAP[data.method]!!), EntityExtra(ActionEntity.EXTRA_HTTP_URL, data.url), EntityExtra(ActionEntity.EXTRA_HTTP_BODY, data.body), + EntityExtra( + ActionEntity.EXTRA_HTTP_AUTHORIZATION_HEADER, + data.authorizationHeader, + ), ) else -> emptyList() diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/CreateActionDelegate.kt b/app/src/main/java/io/github/sds100/keymapper/actions/CreateActionDelegate.kt index f90c762cf2..164bfdde95 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/CreateActionDelegate.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/CreateActionDelegate.kt @@ -779,6 +779,7 @@ class CreateActionDelegate( method = HttpMethod.GET, url = "", body = "", + authorizationHeader = "", ) } else { httpRequestBottomSheetState = oldData as? ActionData.HttpRequest diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/HttpRequestBottomSheet.kt b/app/src/main/java/io/github/sds100/keymapper/actions/HttpRequestBottomSheet.kt index 17056bcdd8..d8f5cf19ca 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/HttpRequestBottomSheet.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/HttpRequestBottomSheet.kt @@ -214,6 +214,22 @@ private fun HttpRequestBottomSheet( Spacer(modifier = Modifier.height(16.dp)) + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + value = state.authorizationHeader, + label = { Text(stringResource(R.string.action_http_request_authorization_label)) }, + onValueChange = { + onBodyChanged(it) + }, + supportingText = { + Text(stringResource(R.string.action_http_request_authorization_supporting_text)) + }, + ) + + Spacer(modifier = Modifier.height(16.dp)) + Row( modifier = Modifier .fillMaxWidth() @@ -278,6 +294,7 @@ private fun PreviewEmpty() { method = HttpMethod.GET, url = "", body = "", + authorizationHeader = "", ), ) } @@ -301,6 +318,7 @@ private fun PreviewFilled() { method = HttpMethod.GET, url = "https://example.com", body = "Hello, world!", + authorizationHeader = "Bearer token", ), ) } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt b/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt index 272387f893..53c7efe637 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/PerformActionsUseCase.kt @@ -820,6 +820,7 @@ class PerformActionsUseCaseImpl( method = action.method, url = action.url, body = action.body, + authorizationHeader = action.authorizationHeader, ).showErrorMessageOnFail() } diff --git a/app/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt b/app/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt index d912bc0eb1..e99db4de73 100644 --- a/app/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt +++ b/app/src/main/java/io/github/sds100/keymapper/data/entities/ActionEntity.kt @@ -88,6 +88,7 @@ data class ActionEntity( const val EXTRA_HTTP_URL = "extra_http_url" const val EXTRA_HTTP_BODY = "extra_http_body" const val EXTRA_HTTP_DESCRIPTION = "extra_http_description" + const val EXTRA_HTTP_AUTHORIZATION_HEADER = "extra_http_authorization_header" // DON'T CHANGE THESE. Used for JSON serialization and parsing. const val NAME_ACTION_TYPE = "type" diff --git a/app/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt b/app/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt index f021724fba..dc33b5926c 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt @@ -18,6 +18,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.withContext +import okhttp3.Headers import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody @@ -128,7 +129,12 @@ class AndroidNetworkAdapter( } } - override suspend fun sendHttpRequest(method: HttpMethod, url: String, body: String): Result<*> { + override suspend fun sendHttpRequest( + method: HttpMethod, + url: String, + body: String, + authorizationHeader: String, + ): Result<*> { try { val requestBody = when (method) { HttpMethod.HEAD -> Request.Builder().head() @@ -139,8 +145,15 @@ class AndroidNetworkAdapter( HttpMethod.PATCH -> Request.Builder().patch(body.toRequestBody()) } + val headers = Headers.Builder() + + if (authorizationHeader.isNotBlank()) { + headers.add("Authorization", authorizationHeader) + } + val request = requestBody .url("https://posttestserver.dev/p/kmr33yjcz5h38hkq/post") + .headers(headers.build()) .build() withContext(Dispatchers.IO) { httpClient.newCall(request).execute() } diff --git a/app/src/main/java/io/github/sds100/keymapper/system/network/NetworkAdapter.kt b/app/src/main/java/io/github/sds100/keymapper/system/network/NetworkAdapter.kt index ba9c4b3108..e0e6e23210 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/network/NetworkAdapter.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/network/NetworkAdapter.kt @@ -27,5 +27,6 @@ interface NetworkAdapter { method: HttpMethod, url: String, body: String, + authorizationHeader: String, ): Result<*> } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 55fd27594a..ced9f72576 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1084,6 +1084,8 @@ URL Can not be empty! Request body (optional) + Authorization header (optional) + You must prepend \'Bearer\' if necessary From 141b41adcd9c79ea717cd7b8490116ec9137f2e5 Mon Sep 17 00:00:00 2001 From: sds100 Date: Fri, 4 Apr 2025 21:43:50 -0600 Subject: [PATCH 6/7] #1625 fix some bugs with http request action --- app/src/main/AndroidManifest.xml | 4 +++- .../io/github/sds100/keymapper/actions/ActionData.kt | 5 +++++ .../keymapper/actions/HttpRequestBottomSheet.kt | 12 +++++++++++- .../system/network/AndroidNetworkAdapter.kt | 7 +++++-- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c581f61ca5..78e07f48d0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -77,13 +77,15 @@ + { val authorizationHeader: String, ) : ActionData() { override val id: ActionId = ActionId.HTTP_REQUEST + + override fun toString(): String { + // Do not leak sensitive request info to logs. + return "HttpRequest(description=$description)" + } } } diff --git a/app/src/main/java/io/github/sds100/keymapper/actions/HttpRequestBottomSheet.kt b/app/src/main/java/io/github/sds100/keymapper/actions/HttpRequestBottomSheet.kt index d8f5cf19ca..29413ac385 100644 --- a/app/src/main/java/io/github/sds100/keymapper/actions/HttpRequestBottomSheet.kt +++ b/app/src/main/java/io/github/sds100/keymapper/actions/HttpRequestBottomSheet.kt @@ -73,6 +73,10 @@ fun HttpRequestBottomSheet(delegate: CreateActionDelegate) { delegate.httpRequestBottomSheetState = delegate.httpRequestBottomSheetState?.copy(body = it) }, + onAuthorizationChanged = { + delegate.httpRequestBottomSheetState = + delegate.httpRequestBottomSheetState?.copy(authorizationHeader = it) + }, onDoneClick = { val result = delegate.httpRequestBottomSheetState ?: return@HttpRequestBottomSheet delegate.httpRequestBottomSheetState = null @@ -92,6 +96,7 @@ private fun HttpRequestBottomSheet( onDescriptionChanged: (String) -> Unit = {}, onUrlChanged: (String) -> Unit = {}, onBodyChanged: (String) -> Unit = {}, + onAuthorizationChanged: (String) -> Unit = {}, onDoneClick: () -> Unit = {}, ) { val scrollState = rememberScrollState() @@ -221,11 +226,16 @@ private fun HttpRequestBottomSheet( value = state.authorizationHeader, label = { Text(stringResource(R.string.action_http_request_authorization_label)) }, onValueChange = { - onBodyChanged(it) + onAuthorizationChanged(it) }, supportingText = { Text(stringResource(R.string.action_http_request_authorization_supporting_text)) }, + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.None, + autoCorrectEnabled = false, + keyboardType = KeyboardType.Text, + ), ) Spacer(modifier = Modifier.height(16.dp)) diff --git a/app/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt b/app/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt index dc33b5926c..e44f3e8c5a 100644 --- a/app/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt +++ b/app/src/main/java/io/github/sds100/keymapper/system/network/AndroidNetworkAdapter.kt @@ -152,13 +152,15 @@ class AndroidNetworkAdapter( } val request = requestBody - .url("https://posttestserver.dev/p/kmr33yjcz5h38hkq/post") + .url(url) .headers(headers.build()) .build() withContext(Dispatchers.IO) { httpClient.newCall(request).execute() } .use { response -> - Timber.e(response.toString()) + // Keep this in. It is useful for debugging. + Timber.d(response.toString()) + if (!response.isSuccessful) { return Error.UnknownIOError } @@ -166,6 +168,7 @@ class AndroidNetworkAdapter( return Success(Unit) } } catch (e: IOException) { + Timber.e(e) return Error.UnknownIOError } } From 11cb63ead353282d0747f7c85415c7b151dd46c3 Mon Sep 17 00:00:00 2001 From: sds100 Date: Sun, 6 Apr 2025 21:47:09 -0600 Subject: [PATCH 7/7] bump version code and changelog date --- CHANGELOG.md | 2 +- app/version.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be493ae0d4..7544087766 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## [3.0 Beta 5](https://github.com/sds100/KeyMapper/releases/tag/v3.0.0-beta.5) -#### 4 April 2025 +#### 6 April 2025 - #1625 HTTP Request action. diff --git a/app/version.properties b/app/version.properties index 4e9bc706c6..f70c27a875 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,3 +1,3 @@ VERSION_NAME=3.0.0-beta.5 -VERSION_CODE=93 +VERSION_CODE=94 VERSION_NUM=0 \ No newline at end of file