Merge Android 14 QPR3 to AOSP main

Bug: 346855327
Merged-In: Ia7316092c90271d1d8347dd49abbd2247d8fec20
Change-Id: I766a8309ccca8b7ebfa6022c39842a98bc1980be
diff --git a/Android.bp b/Android.bp
index 856eec1..21f8b50 100644
--- a/Android.bp
+++ b/Android.bp
@@ -96,4 +96,6 @@
     ],
 
     aaptflags: ["--extra-packages com.android.car.rotary"],
+    // TODO(b/319708040): re-enable use_resource_processor
+    use_resource_processor: false,
 }
diff --git a/src/com/android/car/rotary/Navigator.java b/src/com/android/car/rotary/Navigator.java
index ccaf357..a77dd73 100644
--- a/src/com/android/car/rotary/Navigator.java
+++ b/src/com/android/car/rotary/Navigator.java
@@ -111,6 +111,11 @@
         mSurfaceViewHelper.clearHostApp(packageName);
     }
 
+    /** Returns whether it supports AAOS template apps. */
+    boolean supportTemplateApp() {
+        return mSurfaceViewHelper.supportTemplateApp();
+    }
+
     /** Adds the package name of the client app. */
     void addClientApp(@NonNull CharSequence clientAppPackageName) {
         mSurfaceViewHelper.addClientApp(clientAppPackageName);
@@ -185,7 +190,8 @@
             //    area),
             // 3. and nextCandidate is different from candidate (if sourceNode is the first
             //    focusable node in the window, searching backward will return sourceNode itself).
-            if (nextCandidate != null && currentFocusArea.equals(candidateFocusArea)
+            if (nextCandidate != null && currentFocusArea != null
+                    && currentFocusArea.equals(candidateFocusArea)
                     && !Utils.isFocusParkingView(nextCandidate)
                     && !nextCandidate.equals(candidate)) {
                 // We need to skip nextTargetNode if:
@@ -235,7 +241,6 @@
                 break;
             }
         }
-        currentFocusArea.recycle();
         candidate.recycle();
         if (sourceNode.equals(target)) {
             L.e("Wrap-around on the same node");
@@ -439,9 +444,27 @@
         // If the current focus area is an explicit focus area, use its focus area bounds to find
         // nudge target as usual. Otherwise, use the tailored bounds, which was added as the last
         // element of the list in maybeAddImplicitFocusArea().
-        Rect currentFocusAreaBounds = Utils.isFocusArea(currentFocusArea)
-                ? Utils.getBoundsInScreen(currentFocusArea)
-                : candidateFocusAreasBounds.get(candidateFocusAreasBounds.size() - 1);
+        Rect currentFocusAreaBounds;
+        if (Utils.isFocusArea(currentFocusArea)) {
+            currentFocusAreaBounds = Utils.getBoundsInScreen(currentFocusArea);
+        } else if (candidateFocusAreasBounds.size() > 0) {
+            currentFocusAreaBounds =
+                    candidateFocusAreasBounds.get(candidateFocusAreasBounds.size() - 1);
+        } else {
+            // TODO(b/323112198): this should never happen, but let's try to recover from this.
+            L.e("currentFocusArea is an implicit focus area but not added to"
+                    + " currentFocusAreaBounds");
+            L.d("sourceNode:" + sourceNode);
+            L.d("currentFocusArea:" + currentFocusArea);
+            AccessibilityNodeInfo root = getRoot(sourceNode);
+            Utils.printDescendants(root, Utils.LOG_INDENT);
+            Utils.recycleNode(root);
+
+            currentFocusArea.recycle();
+            currentFocusArea = getAncestorFocusArea(sourceNode);
+            currentFocusAreaBounds = Utils.getBoundsInScreen(currentFocusArea);
+            L.d("updated currentFocusArea:" + currentFocusArea);
+        }
 
         if (currentWindow.getType() != TYPE_INPUT_METHOD
                 || shouldNudgeOutOfIme(sourceNode, currentFocusArea, candidateFocusAreas,
@@ -972,7 +995,8 @@
         };
         AccessibilityNodeInfo result = mTreeTraverser.findNodeOrAncestor(node, isFocusAreaOrRoot);
         if (result == null || !Utils.isFocusArea(result)) {
-            L.w("Couldn't find ancestor focus area for given node: " + node);
+            L.w("Ancestor focus area for node " + node + " is not an explicit FocusArea: "
+                    + result);
         }
         return result;
     }
diff --git a/src/com/android/car/rotary/RotaryService.java b/src/com/android/car/rotary/RotaryService.java
index 899f4a3..c6a84bb 100644
--- a/src/com/android/car/rotary/RotaryService.java
+++ b/src/com/android/car/rotary/RotaryService.java
@@ -125,6 +125,8 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 import java.util.stream.Collectors;
 
 /**
@@ -564,6 +566,8 @@
 
     @Nullable private InputMethodManager mInputMethodManager;
 
+    private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
+
     private final BroadcastReceiver mAppInstallUninstallReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
@@ -622,9 +626,16 @@
 
         mRotaryInputMethod = res.getString(R.string.rotary_input_method);
         mDefaultTouchInputMethod = res.getString(R.string.default_touch_input_method);
+        L.d("mRotaryInputMethod:" + mRotaryInputMethod + ", mDefaultTouchInputMethod:"
+                + mDefaultTouchInputMethod);
         validateImeConfiguration(mDefaultTouchInputMethod);
         mTouchInputMethod = mPrefs.getString(TOUCH_INPUT_METHOD_PREFIX
                 + mUserManager.getUserName(), mDefaultTouchInputMethod);
+        if (mTouchInputMethod.isEmpty()) {
+            // Workaround for b/323013736.
+            L.e("mTouchInputMethod shouldn't be empty!");
+            mTouchInputMethod = mDefaultTouchInputMethod;
+        }
         validateImeConfiguration(mTouchInputMethod);
 
         if (mRotaryInputMethod != null && mRotaryInputMethod.equals(getCurrentIme())) {
@@ -1003,9 +1014,12 @@
                 // mTouchInputMethod and save it so we can switch back after switching to the rotary
                 // input method.
                 String inputMethod = getCurrentIme();
-                if (inputMethod != null && !inputMethod.equals(mRotaryInputMethod)) {
+                L.d("Current IME changed to " + inputMethod);
+                if (!TextUtils.isEmpty(inputMethod) && !inputMethod.equals(mRotaryInputMethod)) {
                     mTouchInputMethod = inputMethod;
                     String userName = mUserManager.getUserName();
+                    L.d("Save mTouchInputMethod(" + mTouchInputMethod + ") for user "
+                            + userName);
                     mPrefs.edit()
                             .putString(TOUCH_INPUT_METHOD_PREFIX + userName, mTouchInputMethod)
                             .apply();
@@ -1249,6 +1263,11 @@
         switch (mAfterScrollAction) {
             case FOCUS_PREVIOUS:
             case FOCUS_NEXT: {
+                if (mFocusedNode == null) {
+                    // TODO(326013682): find out why mFocusedNode is null.
+                    L.w("mFocusedNode is null after injecting scroll event");
+                    break;
+                }
                 if (mFocusedNode.equals(sourceNode)) {
                     break;
                 }
@@ -2033,11 +2052,19 @@
     private void onForegroundActivityChanged(@NonNull AccessibilityNodeInfo root,
             @NonNull AccessibilityWindowInfo window,
             @Nullable CharSequence packageName, @Nullable CharSequence className) {
-        // If the foreground app is a client app, store its package name.
-        AccessibilityNodeInfo surfaceView = mNavigator.findSurfaceViewInRoot(root);
-        if (surfaceView != null) {
-            mNavigator.addClientApp(surfaceView.getPackageName());
-            surfaceView.recycle();
+        if (mNavigator.supportTemplateApp()) {
+            // Check if there is a SurfaceView node to decide whether the foreground app is an
+            // AAOS template app. This is done on background thread to avoid ANR (b/322324727).
+            // TODO: find a better way to solve this to avoid potential race condition.
+            mExecutor.execute(() -> {
+                // If the foreground app is a client app, store its package name.
+                AccessibilityNodeInfo surfaceView =
+                        mNavigator.findSurfaceViewInRoot(root);
+                if (surfaceView != null) {
+                    mNavigator.addClientApp(surfaceView.getPackageName());
+                    surfaceView.recycle();
+                }
+            });
         }
 
         ComponentName newActivity = packageName != null && className != null
@@ -2109,6 +2136,7 @@
         }
         if (enable) {
             mFocusedNode = Utils.refreshNode(mFocusedNode);
+            L.v("After refresh, mFocusedNode is " + mFocusedNode);
             if (mFocusedNode == null) {
                 L.w("Failed to enter direct manipulation mode because mFocusedNode is no longer "
                         + "in view tree.");
@@ -2258,6 +2286,7 @@
      */
     private void refreshSavedNodes() {
         mFocusedNode = Utils.refreshNode(mFocusedNode);
+        L.v("After refresh, mFocusedNode is " + mFocusedNode);
         mEditNode = Utils.refreshNode(mEditNode);
         mLastTouchedNode = Utils.refreshNode(mLastTouchedNode);
         mFocusArea = Utils.refreshNode(mFocusArea);
@@ -2428,6 +2457,7 @@
      */
     private void maybeClearFocusInCurrentWindow(@Nullable AccessibilityNodeInfo targetFocus) {
         mFocusedNode = Utils.refreshNode(mFocusedNode);
+        L.v("After refresh, mFocusedNode is " + mFocusedNode);
         if (mFocusedNode == null
                 // No need to clear focus if mFocusedNode is not focused. However, when it's a node
                 // in a WebView or ComposeView, its state might not be up to date,
@@ -2519,7 +2549,8 @@
             fpv.recycle();
             return true;
         }
-        boolean result = performFocusAction(fpv);
+        // Don't call performFocusAction(fpv) because it might cause infinite loop (b/322137915).
+        boolean result = fpv.performAction(ACTION_FOCUS);
         if (!result) {
             L.w("Failed to perform ACTION_FOCUS on " + fpv);
         }
diff --git a/src/com/android/car/rotary/SurfaceViewHelper.java b/src/com/android/car/rotary/SurfaceViewHelper.java
index 6427896..484ce2d 100644
--- a/src/com/android/car/rotary/SurfaceViewHelper.java
+++ b/src/com/android/car/rotary/SurfaceViewHelper.java
@@ -78,6 +78,11 @@
         }
     }
 
+    /** Returns whether it supports AAOS template apps. */
+    boolean supportTemplateApp() {
+        return !TextUtils.isEmpty(mHostApp);
+    }
+
     /** Adds the package name of the client app. */
     void addClientApp(@NonNull CharSequence clientAppPackageName) {
         mClientApps.add(clientAppPackageName);
diff --git a/src/com/android/car/rotary/Utils.java b/src/com/android/car/rotary/Utils.java
index c78a40e..3668839 100644
--- a/src/com/android/car/rotary/Utils.java
+++ b/src/com/android/car/rotary/Utils.java
@@ -76,6 +76,7 @@
     static final String COMPOSE_VIEW_CLASS_NAME = "androidx.compose.ui.platform.ComposeView";
     @VisibleForTesting
     static final String SURFACE_VIEW_CLASS_NAME = SurfaceView.class.getName();
+    static final String LOG_INDENT = "    ";
 
     private static final int FIND_FOCUS_MAX_TRY_COUNT = 3;
 
@@ -471,4 +472,17 @@
         L.e("Failed to find focused node in " + root);
         return null;
     }
+
+    /** Prints the node and its descendants. */
+    static void printDescendants(@Nullable AccessibilityNodeInfo root, String indent) {
+        if (root == null) {
+            return;
+        }
+        L.d(indent + root);
+        for (int i = 0; i < root.getChildCount(); i++) {
+            AccessibilityNodeInfo child = root.getChild(i);
+            printDescendants(child, indent + LOG_INDENT);
+            Utils.recycleNode(child);
+        }
+    }
 }
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 08cad5a..46f8719 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -42,4 +42,6 @@
     ],
 
     compile_multilib: "both",
+    // TODO(b/319708040): re-enable use_resource_processor
+    use_resource_processor: false,
 }