Snap for 10453563 from 3263e306750cd4f450799a37b5b3351a147901f1 to mainline-documentsui-release

Change-Id: I4b4a35d839aa0451435eac359aa0d2163cb3d9fb
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();
+    }
 }