diff --git a/app/src/main/java/com/valterc/ki2/data/input/KarooKey.java b/app/src/main/java/com/valterc/ki2/data/input/KarooKey.java index 6b522ad0..f3bd7206 100644 --- a/app/src/main/java/com/valterc/ki2/data/input/KarooKey.java +++ b/app/src/main/java/com/valterc/ki2/data/input/KarooKey.java @@ -3,9 +3,9 @@ import android.view.KeyEvent; /** - * Represents a Karoo hardware key and the corresponding KeyEvent keycode. + * Represents a Karoo hardware or virtual key and the corresponding KeyEvent keycode. */ -public enum KarooKey{ +public enum KarooKey { /** * Left top key. @@ -30,12 +30,22 @@ public enum KarooKey{ /** * None, unknown or unassigned key. */ - NONE(KeyEvent.KEYCODE_UNKNOWN); + NONE(KeyEvent.KEYCODE_UNKNOWN), + + /** + * Virtual key representing no action. + */ + VIRTUAL_NONE(10_000), + + /** + * Virtual key to switch the ride activity to the map page. + */ + VIRTUAL_SWITCH_TO_MAP_PAGE(VIRTUAL_NONE.keyCode + 1); public static KarooKey fromKeyCode(int keyCode) { - for (KarooKey s : KarooKey.values()) { - if (s.keyCode == keyCode) { - return s; + for (KarooKey karooKey : values()) { + if (karooKey.keyCode == keyCode) { + return karooKey; } } @@ -48,6 +58,10 @@ public static KarooKey fromKeyCode(int keyCode) { this.keyCode = keyCode; } + public final boolean isVirtual() { + return keyCode >= VIRTUAL_NONE.keyCode; + } + public final int getKeyCode() { return this.keyCode; } diff --git a/app/src/main/java/com/valterc/ki2/input/InputAdapter.java b/app/src/main/java/com/valterc/ki2/input/InputAdapter.java index b73d941f..cdacf8d2 100644 --- a/app/src/main/java/com/valterc/ki2/input/InputAdapter.java +++ b/app/src/main/java/com/valterc/ki2/input/InputAdapter.java @@ -20,12 +20,14 @@ public class InputAdapter { private final Context context; private final HashMap keyDownTimeMap; + private final VirtualInputAdapter virtualInputAdapter; private InputManager inputManager; private Method injectInputMethod; public InputAdapter(Context context) { this.context = context; this.keyDownTimeMap = new HashMap<>(); + this.virtualInputAdapter = new VirtualInputAdapter(); initInputManager(); } @@ -133,6 +135,11 @@ private void simulateLongKeyPress(KarooKey key, long eventTime) { } public void executeKeyEvent(KarooKeyEvent keyEvent) { + if (keyEvent.getKey().isVirtual()) { + virtualInputAdapter.handleVirtualKeyEvent(keyEvent); + return; + } + for (int i = 0; i < keyEvent.getReplicate(); i++) { long eventTime = SystemClock.uptimeMillis() + (long) ViewConfiguration.getKeyRepeatTimeout() * i; switch (keyEvent.getAction()) { diff --git a/app/src/main/java/com/valterc/ki2/input/InputManager.java b/app/src/main/java/com/valterc/ki2/input/InputManager.java index 14996455..4365677c 100644 --- a/app/src/main/java/com/valterc/ki2/input/InputManager.java +++ b/app/src/main/java/com/valterc/ki2/input/InputManager.java @@ -47,6 +47,7 @@ public class InputManager { preferenceToSwitchKeyMap.put("lap", (switchEvent, converter) -> new KarooKeyEvent(KarooKey.BACK, KeyAction.DOUBLE_PRESS, switchEvent.getRepeat())); preferenceToSwitchKeyMap.put("press_map_graph_zoom_out", (switchEvent, converter) -> new KarooKeyEvent(KarooKey.LEFT, KeyAction.SIMULATE_LONG_PRESS, switchEvent.getRepeat())); preferenceToSwitchKeyMap.put("press_map_graph_zoom_in", (switchEvent, converter) -> new KarooKeyEvent(KarooKey.RIGHT, KeyAction.SIMULATE_LONG_PRESS, switchEvent.getRepeat())); + preferenceToSwitchKeyMap.put("press_switch_to_map_page", (switchEvent, converter) -> new KarooKeyEvent(KarooKey.VIRTUAL_SWITCH_TO_MAP_PAGE, KeyAction.SINGLE_PRESS, switchEvent.getRepeat())); /* * Double press events @@ -77,6 +78,12 @@ public class InputManager { } return new KarooKeyEvent(KarooKey.BACK, KeyAction.DOUBLE_PRESS, switchEvent.getRepeat()); }); + preferenceToSwitchKeyMap.put("hold_short_single_switch_to_map_page", (switchEvent, converter) -> { + if (switchEvent.getCommand() != SwitchCommand.LONG_PRESS_DOWN) { + return null; + } + return new KarooKeyEvent(KarooKey.VIRTUAL_SWITCH_TO_MAP_PAGE, KeyAction.SINGLE_PRESS, switchEvent.getRepeat()); + }); } private final SharedPreferences preferences; diff --git a/app/src/main/java/com/valterc/ki2/input/VirtualInputAdapter.java b/app/src/main/java/com/valterc/ki2/input/VirtualInputAdapter.java new file mode 100644 index 00000000..c56b98db --- /dev/null +++ b/app/src/main/java/com/valterc/ki2/input/VirtualInputAdapter.java @@ -0,0 +1,38 @@ +package com.valterc.ki2.input; + +import android.annotation.SuppressLint; +import android.util.Log; + +import com.valterc.ki2.data.input.KarooKey; +import com.valterc.ki2.data.input.KarooKeyEvent; +import com.valterc.ki2.karoo.hooks.RideActivityHook; + +import java.util.HashMap; +import java.util.function.Consumer; + +@SuppressLint("LogNotTimber") +public class VirtualInputAdapter { + + private final HashMap> keyMapping; + + public VirtualInputAdapter() { + this.keyMapping = new HashMap<>(); + this.keyMapping.put(KarooKey.VIRTUAL_SWITCH_TO_MAP_PAGE, karooKeyEvent -> { + boolean result = RideActivityHook.switchToMapPage(); + if (!result) { + Log.w("KI2", "Unable to switch to map page"); + } + }); + } + + public void handleVirtualKeyEvent(KarooKeyEvent keyEvent) { + if (!keyEvent.getKey().isVirtual()) { + return; + } + + Consumer keyEventConsumer = keyMapping.get(keyEvent.getKey()); + if (keyEventConsumer != null) { + keyEventConsumer.accept(keyEvent); + } + } +} diff --git a/app/src/main/java/com/valterc/ki2/karoo/hooks/RideActivityHook.java b/app/src/main/java/com/valterc/ki2/karoo/hooks/RideActivityHook.java index 72b79b4f..5c07f5bd 100644 --- a/app/src/main/java/com/valterc/ki2/karoo/hooks/RideActivityHook.java +++ b/app/src/main/java/com/valterc/ki2/karoo/hooks/RideActivityHook.java @@ -5,16 +5,79 @@ import android.content.Context; import android.content.Intent; import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.Nullable; +import androidx.viewpager.widget.PagerAdapter; +import androidx.viewpager.widget.ViewPager; import com.valterc.ki2.utils.ActivityUtils; import com.valterc.ki2.utils.ProcessUtils; +import java.lang.reflect.Field; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import kotlin.Lazy; +import kotlin.LazyKt; + +@SuppressWarnings({"unchecked", "rawtypes"}) @SuppressLint("LogNotTimber") public final class RideActivityHook { private RideActivityHook() { } + private static Integer ID_VIEW_PAGER; + private static Field FIELD_PAGE_LIST; + + private static final Lazy> ENUM_PAGE_TYPE = LazyKt.lazy(() -> { + try { + return (Class) Class.forName("io.hammerhead.datamodels.profiles.PageType"); + } catch (Exception e) { + Log.w("KI2", "Unable to get page type enum", e); + } + + return null; + }); + + private static final Lazy> ENUM_PAGE_TYPE_MAP = LazyKt.lazy(() -> { + try { + Class classPageTypeEnum = ENUM_PAGE_TYPE.getValue(); + return Enum.valueOf(classPageTypeEnum, "MAP"); + } catch (Exception e) { + Log.w("KI2", "Unable to get page type map enum", e); + } + + return null; + }); + + private static final Lazy FIELD_PAGER_PAGE_TYPE = LazyKt.lazy(() -> { + try { + Class classPager = Class.forName("io.hammerhead.datamodels.profiles.Page"); + Field[] classPagerFields = classPager.getFields(); + + for (Field field : classPagerFields) { + if (field.getType() == ENUM_PAGE_TYPE.getValue()) { + field.setAccessible(true); + return field; + } + } + throw new Exception("Page type field not found"); + } catch (Exception e) { + Log.w("KI2", "Unable to get page type map enum", e); + } + + return null; + }); + + /** + * Indicates if the running code is inside the Ride activity process. + * + * @return True if the running process is dedicated from the Ride activity, False otherwise. + */ public static boolean isRideActivityProcess() { return "io.hammerhead.rideapp:io.hammerhead.rideapp.rideActivityProcess".equals(ProcessUtils.getProcessName()); } @@ -42,4 +105,116 @@ public static void tryHandlePreload(Context context) { } } + @Nullable + private static ViewPager getActivityViewPager() { + if (!RideActivityHook.isRideActivityProcess()) { + return null; + } + + Activity activity = ActivityUtils.getRunningActivity(); + if (activity == null) { + return null; + } + + if (ID_VIEW_PAGER != null) { + View viewViewPager = activity.findViewById(ID_VIEW_PAGER); + if (viewViewPager instanceof ViewPager) { + return (ViewPager) viewViewPager; + } + } + + ViewPager viewPager; + View view = activity.findViewById(android.R.id.content); + if (view instanceof ViewGroup) { + viewPager = tryFindViewPager((ViewGroup) view); + if (viewPager != null) { + ID_VIEW_PAGER = viewPager.getId(); + return viewPager; + } + } + + return null; + } + + private static ViewPager tryFindViewPager(ViewGroup viewGroup) { + Set childViewGroups = new HashSet<>(); + for (int i = 0; i < viewGroup.getChildCount(); i++) { + View childView = viewGroup.getChildAt(i); + if (childView instanceof ViewPager) { + return (ViewPager) childView; + } else if (childView instanceof ViewGroup) { + childViewGroups.add((ViewGroup) childView); + } + } + + for (ViewGroup childView : childViewGroups) { + ViewPager viewPager = tryFindViewPager(childView); + if (viewPager != null) { + return viewPager; + } + } + + return null; + } + + public static boolean switchToMapPage() { + ViewPager viewPager = getActivityViewPager(); + if (viewPager == null) { + Log.d("KI2", "Unable to get view pager"); + return false; + } + + PagerAdapter viewPagerAdapter = viewPager.getAdapter(); + if (viewPagerAdapter == null) { + Log.w("KI2", "View pager adapter is null"); + return false; + } + + if (FIELD_PAGE_LIST == null) { + Field[] fieldsPagerAdapter = viewPagerAdapter.getClass().getFields(); + for (Field field : fieldsPagerAdapter) { + if (Collection.class.isAssignableFrom(field.getType()) && + field.getGenericType().toString().contains("io.hammerhead.datamodels.profiles.Page")) { + FIELD_PAGE_LIST = field; + } + } + } + + if (FIELD_PAGE_LIST == null) { + Log.w("KI2", "Unable to get field with list of pages"); + return false; + } + + Collection pages; + + try { + pages = (Collection) FIELD_PAGE_LIST.get(viewPagerAdapter); + } catch (Exception e) { + Log.e("KI2", "Unable to get pages: " + e); + return false; + } + + if (pages == null) { + Log.w("KI2", "List of pages is null"); + return false; + } + + int index = 0; + for (Object page : pages) { + try { + Object pageType = FIELD_PAGER_PAGE_TYPE.getValue().get(page); + if (pageType == ENUM_PAGE_TYPE_MAP.getValue()) { + viewPager.setCurrentItem(index); + return true; + } + } catch (Exception e) { + Log.w("KI2", "Unable to check page type: " + e); + } + + index++; + } + + return false; + } + } diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 3c8530b0..78c37f2b 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -8,6 +8,7 @@ Mark Lap Map/Graph Zoom Out Map/Graph Zoom In + Switch to map page @@ -19,6 +20,7 @@ lap press_map_graph_zoom_out press_map_graph_zoom_in + press_switch_to_map_page @@ -30,6 +32,7 @@ Mark Lap Map/Graph Zoom Out Map/Graph Zoom In + Switch to map page Duplicate Single Press @@ -42,6 +45,7 @@ lap press_map_graph_zoom_out press_map_graph_zoom_in + press_switch_to_map_page double_press_duplicate_single_press @@ -52,6 +56,7 @@ Repeat Single Press Pause/Resume Ride/Confirm Mark Lap + Switch to map page @@ -61,6 +66,7 @@ repeat_single_press hold_short_single_pause_resume_confirm hold_short_single_lap + hold_short_single_switch_to_map_page