Snap for 10453563 from 3263e306750cd4f450799a37b5b3351a147901f1 to mainline-extservices-release
Change-Id: I20493e6776839a3db8aeefcbfa1a8b8c84ce7654
diff --git a/Android.bp b/Android.bp
index 87c0d8f..fe6d342 100644
--- a/Android.bp
+++ b/Android.bp
@@ -22,37 +22,21 @@
"proto/car_rotary_controller.proto",
],
}
-gensrcs {
+
+java_library {
name: "rotary-service-javastream-protos",
- depfile: true,
-
- tools: [
- "aprotoc",
- "protoc-gen-javastream",
- "soong_zip",
- ],
-
- cmd: "mkdir -p $(genDir)/$(in) " +
- "&& $(location aprotoc) " +
- " --plugin=$(location protoc-gen-javastream) " +
- " --dependency_out=$(depfile) " +
- " --javastream_out=$(genDir)/$(in) " +
- " -Iexternal/protobuf/src " +
- " -I . " +
- " $(in) " +
- "&& $(location soong_zip) -jar -o $(out) -C $(genDir)/$(in) -D $(genDir)/$(in)",
-
- srcs: [
- ":rotary-service-proto-source",
- ],
- output_extension: "srcjar",
+ proto: {
+ type: "stream",
+ },
+ srcs: [":rotary-service-proto-source"],
+ installable: false,
+ platform_apis: true,
}
android_app {
name: "CarRotaryController",
srcs: [
"src/**/*.java",
- ":rotary-service-javastream-protos",
],
resource_dirs: ["res"],
@@ -80,6 +64,7 @@
],
static_libs: [
"car-ui-lib",
+ "rotary-service-javastream-protos",
],
product_variables: {
pdk: {
@@ -95,7 +80,6 @@
srcs: [
"src/**/*.java",
- ":rotary-service-javastream-protos",
],
resource_dirs: [
@@ -116,6 +100,7 @@
],
static_libs: [
"car-ui-lib",
+ "rotary-service-javastream-protos",
],
product_variables: {
pdk: {
diff --git a/readme.md b/readme.md
index ad5745d..30902d7 100644
--- a/readme.md
+++ b/readme.md
@@ -43,3 +43,9 @@
```
adb shell cmd car_service inject-key 23
```
+
+To long click the controller center button, send down and up action seperately. For example:
+```
+adb shell cmd car_service inject-key 23 -a down && sleep 2 && adb shell cmd car_service inject-key 23 -a up
+```
+
diff --git a/src/com/android/car/rotary/Navigator.java b/src/com/android/car/rotary/Navigator.java
index a493a2f..c417958 100644
--- a/src/com/android/car/rotary/Navigator.java
+++ b/src/com/android/car/rotary/Navigator.java
@@ -559,11 +559,8 @@
}
boolean hasFocusableDescendant = false;
for (AccessibilityNodeInfo webView : webViews) {
- AccessibilityNodeInfo focusableDescendant = mTreeTraverser.depthFirstSearch(webView,
- Utils::canPerformFocus);
- if (focusableDescendant != null) {
+ if (webViewHasFocusableDescendants(webView)) {
hasFocusableDescendant = true;
- focusableDescendant.recycle();
break;
}
}
@@ -571,6 +568,20 @@
return hasFocusableDescendant;
}
+ private boolean webViewHasFocusableDescendants(@NonNull AccessibilityNodeInfo webView) {
+ AccessibilityNodeInfo focusableDescendant = mTreeTraverser.depthFirstSearch(webView,
+ Utils::canPerformFocus);
+ if (focusableDescendant == null) {
+ return false;
+ }
+ focusableDescendant.recycle();
+ return true;
+ }
+
+ private boolean isWebViewWithFocusableDescendants(@NonNull AccessibilityNodeInfo node) {
+ return Utils.isWebView(node) && webViewHasFocusableDescendants(node);
+ }
+
/**
* Adds all the {@code windows} in the given {@code direction} of the given {@code source}
* window to the given list if the {@code source} window is not an overlay. If it's an overlay
@@ -815,7 +826,7 @@
if (Utils.isFocusArea(node) || Utils.isFocusParkingView(node)) {
return bounds;
}
- if (Utils.canTakeFocus(node) || containsWebViewWithFocusableDescendants(node)) {
+ if (Utils.canTakeFocus(node) || isWebViewWithFocusableDescendants(node)) {
return Utils.getBoundsInScreen(node);
}
for (int i = 0; i < node.getChildCount(); i++) {
diff --git a/src/com/android/car/rotary/RotaryService.java b/src/com/android/car/rotary/RotaryService.java
index baa17fc..0ef9572 100644
--- a/src/com/android/car/rotary/RotaryService.java
+++ b/src/com/android/car/rotary/RotaryService.java
@@ -100,7 +100,6 @@
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityWindowInfo;
-import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.FrameLayout;
@@ -615,13 +614,20 @@
Context.MODE_PRIVATE);
mUserManager = getSystemService(UserManager.class);
+ mInputManager = getSystemService(InputManager.class);
+ mInputMethodManager = getSystemService(InputMethodManager.class);
+ if (mInputMethodManager == null) {
+ throw new IllegalStateException("Failed to get InputMethodManager");
+ }
+
mRotaryInputMethod = res.getString(R.string.rotary_input_method);
mDefaultTouchInputMethod = res.getString(R.string.default_touch_input_method);
- mTouchInputMethod = mPrefs.getString(TOUCH_INPUT_METHOD_PREFIX + mUserManager.getUserName(),
- mDefaultTouchInputMethod);
- if (mRotaryInputMethod != null
- && mRotaryInputMethod.equals(getCurrentIme())
- && isInstalledIme(mTouchInputMethod)) {
+ validateImeConfiguration(mDefaultTouchInputMethod);
+ mTouchInputMethod = mPrefs.getString(TOUCH_INPUT_METHOD_PREFIX
+ + mUserManager.getUserName(), mDefaultTouchInputMethod);
+ validateImeConfiguration(mTouchInputMethod);
+
+ if (mRotaryInputMethod != null && mRotaryInputMethod.equals(getCurrentIme())) {
// Switch from the rotary IME to the touch IME in case Android defaults to the rotary
// IME.
// TODO(b/169423887): Figure out how to configure the default IME through Android
@@ -670,6 +676,21 @@
}
/**
+ * Ensure that the IME configuration passed as argument is also available in
+ * {@link InputMethodManager}.
+ *
+ * @throws IllegalStateException if the ime configuration passed as argument is not available
+ * in {@link InputMethodManager}
+ */
+ private void validateImeConfiguration(String imeConfiguration) {
+ if (!Utils.isInstalledIme(imeConfiguration, mInputMethodManager)) {
+ throw new IllegalStateException(String.format("%s is not installed (run "
+ + "`dumpsys input_method` to list all available input methods)",
+ imeConfiguration));
+ }
+ }
+
+ /**
* {@inheritDoc}
* <p>
* We need to access WindowManager in onCreate() and
@@ -716,11 +737,6 @@
updateServiceInfo();
- mInputManager = getSystemService(InputManager.class);
- mInputMethodManager = getSystemService(InputMethodManager.class);
- if (mInputMethodManager == null) {
- L.w("Failed to get InputMethodManager");
- }
// Add an overlay to capture touch events.
addTouchOverlay();
@@ -2672,7 +2688,8 @@
}
}
- private void setInRotaryMode(boolean inRotaryMode) {
+ @VisibleForTesting
+ void setInRotaryMode(boolean inRotaryMode) {
mInRotaryMode = inRotaryMode;
if (!mInRotaryMode) {
setEditNode(null);
@@ -2700,7 +2717,7 @@
/** Switches to the rotary IME or the touch IME if needed. */
private void updateIme() {
String newIme = mInRotaryMode ? mRotaryInputMethod : mTouchInputMethod;
- if (mInRotaryMode && !isInstalledIme(newIme)) {
+ if (mInRotaryMode && !Utils.isInstalledIme(newIme, mInputMethodManager)) {
L.w("Rotary IME doesn't exist: " + newIme);
return;
}
@@ -2724,9 +2741,11 @@
if (mContentResolver == null) {
return;
}
+ String oldIme = getCurrentIme();
+ validateImeConfiguration(newIme);
boolean result =
Settings.Secure.putString(mContentResolver, DEFAULT_INPUT_METHOD, newIme);
- L.successOrFailure("Switching to IME: " + newIme, result);
+ L.successOrFailure("Switching IME from " + oldIme + " to " + newIme, result);
}
/**
@@ -2879,25 +2898,6 @@
mWindowCache.setNodeCopier(nodeCopier);
}
- /** Checks if the {@code componentName} is an installed input method. */
- private boolean isInstalledIme(@Nullable String componentName) {
- if (TextUtils.isEmpty(componentName) || mInputMethodManager == null) {
- return false;
- }
- // Use getInputMethodList() to get the installed input methods. Don't do that by fetching
- // ENABLED_INPUT_METHODS and DISABLED_SYSTEM_INPUT_METHODS from the secure setting,
- // because RotaryIME may not be included in any of them (b/229144904).
- ComponentName component = ComponentName.unflattenFromString(componentName);
- List<InputMethodInfo> imeList = mInputMethodManager.getInputMethodList();
- for (InputMethodInfo ime : imeList) {
- ComponentName imeComponent = ime.getComponent();
- if (component.equals(imeComponent)) {
- return true;
- }
- }
- return false;
- }
-
@VisibleForTesting
AccessibilityNodeInfo getFocusedNode() {
return mFocusedNode;
@@ -2980,5 +2980,4 @@
RotaryProtos.RotaryService.WINDOW_CACHE);
dumpOutputStream.flush();
}
-
}
diff --git a/src/com/android/car/rotary/Utils.java b/src/com/android/car/rotary/Utils.java
index 89bfa58..27f7028 100644
--- a/src/com/android/car/rotary/Utils.java
+++ b/src/com/android/car/rotary/Utils.java
@@ -30,11 +30,15 @@
import static com.android.car.ui.utils.RotaryConstants.ROTARY_VERTICALLY_SCROLLABLE;
import static com.android.car.ui.utils.RotaryConstants.TOP_BOUND_OFFSET_FOR_NUDGE;
+import android.content.ComponentName;
import android.graphics.Rect;
import android.os.Bundle;
+import android.text.TextUtils;
import android.view.SurfaceView;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityWindowInfo;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodManager;
import android.webkit.WebView;
import androidx.annotation.NonNull;
@@ -427,4 +431,24 @@
}
return null;
}
+
+ /** Checks if the {@code componentName} is an installed input method. */
+ static boolean isInstalledIme(@Nullable String componentName,
+ @NonNull InputMethodManager imm) {
+ if (TextUtils.isEmpty(componentName)) {
+ return false;
+ }
+ // Use getInputMethodList() to get the installed input methods. Don't do that by fetching
+ // ENABLED_INPUT_METHODS and DISABLED_SYSTEM_INPUT_METHODS from the secure setting,
+ // because RotaryIME may not be included in any of them (b/229144904).
+ ComponentName component = ComponentName.unflattenFromString(componentName);
+ List<InputMethodInfo> imeList = imm.getInputMethodList();
+ for (InputMethodInfo ime : imeList) {
+ ComponentName imeComponent = ime.getComponent();
+ if (component.equals(imeComponent)) {
+ return true;
+ }
+ }
+ return false;
+ }
}
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 373f042..5be8de8 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -32,5 +32,10 @@
aaptflags: ["--extra-packages com.android.car.rotary"],
- test_suites: ["device-tests"]
+ test_suites: [
+ "device-tests",
+ "automotive-tests",
+ ],
+
+ compile_multilib: "both",
}
diff --git a/tests/unit/src/com/android/car/rotary/RotaryServiceTest.java b/tests/unit/src/com/android/car/rotary/RotaryServiceTest.java
index 611b8af..7730eea 100644
--- a/tests/unit/src/com/android/car/rotary/RotaryServiceTest.java
+++ b/tests/unit/src/com/android/car/rotary/RotaryServiceTest.java
@@ -186,7 +186,8 @@
* button1 defaultFocus button3
* (focused)
* </pre>
- * and {@link RotaryService#mFocusedNode} is not initialized.
+ * {@link RotaryService#mFocusedNode} is not initialized,
+ * and {@link RotaryService#mInRotaryMode} is set to true.
*/
@Test
public void testInitFocus_focusOnAlreadyFocusedView() {
@@ -202,6 +203,8 @@
Activity activity = mActivityRule.getActivity();
Button button3 = activity.findViewById(R.id.button3);
button3.post(() -> button3.requestFocus());
+ // TODO(b/246423854): Find out why we need to setInRotaryMode(true) explicitly
+ mRotaryService.setInRotaryMode(true);
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
assertThat(button3.isFocused()).isTrue();
assertNull(mRotaryService.getFocusedNode());
@@ -359,7 +362,8 @@
* / \
* focusParkingView button2(focused)
* </pre>
- * and {@link RotaryService#mFocusedNode} is null.
+ * {@link RotaryService#mFocusedNode} is null,
+ * and {@link RotaryService#mInRotaryMode} is set to true.
*/
@Test
public void testInitFocus_focusOnHostNode() {
@@ -401,6 +405,8 @@
List<AccessibilityWindowInfo> windows = Collections.singletonList(window);
when(mRotaryService.getWindows()).thenReturn(windows);
+ // TODO(b/246423854): Find out why we need to setInRotaryMode(true) explicitly
+ mRotaryService.setInRotaryMode(true);
boolean consumed = mRotaryService.initFocus();
assertThat(mRotaryService.getFocusedNode()).isEqualTo(button2);
assertThat(consumed).isFalse();
diff --git a/tests/unit/src/com/android/car/rotary/UtilsTest.java b/tests/unit/src/com/android/car/rotary/UtilsTest.java
index e688350..91bdef3 100644
--- a/tests/unit/src/com/android/car/rotary/UtilsTest.java
+++ b/tests/unit/src/com/android/car/rotary/UtilsTest.java
@@ -22,15 +22,24 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.content.ComponentName;
import android.view.accessibility.AccessibilityNodeInfo;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodManager;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
-@RunWith(AndroidJUnit4.class)
-public class UtilsTest {
+import java.util.Collections;
+import java.util.List;
+
+@RunWith(MockitoJUnitRunner.class)
+public final class UtilsTest {
+
+ @Mock
+ private InputMethodManager mMockedInputMethodManager;
@Test
public void refreshNode_nodeIsNull_returnsNull() {
@@ -68,4 +77,21 @@
verify(input).recycle();
}
+
+ @Test
+ public void testIsInstalledIme_invalidImeConfigs() {
+ assertThat(Utils.isInstalledIme(null, mMockedInputMethodManager)).isFalse();
+ assertThat(Utils.isInstalledIme("blah/someIme", mMockedInputMethodManager)).isFalse();
+ }
+
+ @Test
+ public void testIsInstalledIme_validImeConfig() {
+ InputMethodInfo methodInfo = mock(InputMethodInfo.class);
+ when(methodInfo.getComponent()).thenReturn(
+ ComponentName.unflattenFromString("blah/someIme"));
+ List<InputMethodInfo> availableInputMethods = Collections.singletonList(methodInfo);
+ when(mMockedInputMethodManager.getInputMethodList()).thenReturn(availableInputMethods);
+
+ assertThat(Utils.isInstalledIme("blah/someIme", mMockedInputMethodManager)).isTrue();
+ }
}