[Ravenwood] Mock UiAutomation to support permission APIs

On Ravenwood, since there are no "permissions", we can provide no-op
implementations for adopt/drop shell permissions method families in
UiAutomation. To prevent pulling in a lot of unnecessary internal
dependencies of UiAutomation, we simply inject a mock UiAutomation in
our Instrumentation class.

Flag: EXEMPT host test change only
Bug: 292141694
Test: atest RavenwoodBivalentTest
Test: $ANDROID_BUILD_TOP/frameworks/base/ravenwood/scripts/run-ravenwood-tests.sh
Change-Id: I8826a8c491efa5fc8d6cb10cf11c6dd677ad7f31
diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java
index 6a599ea..be5309c 100644
--- a/core/java/android/app/Instrumentation.java
+++ b/core/java/android/app/Instrumentation.java
@@ -48,6 +48,9 @@
 import android.os.TestLooperManager;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.ravenwood.annotation.RavenwoodKeep;
+import android.ravenwood.annotation.RavenwoodKeepPartialClass;
+import android.ravenwood.annotation.RavenwoodReplace;
 import android.util.AndroidRuntimeException;
 import android.util.Log;
 import android.view.Display;
@@ -80,7 +83,7 @@
  * implementation is described to the system through an AndroidManifest.xml's
  * <instrumentation> tag.
  */
-@android.ravenwood.annotation.RavenwoodKeepPartialClass
+@RavenwoodKeepPartialClass
 public class Instrumentation {
 
     /**
@@ -134,7 +137,7 @@
     private UiAutomation mUiAutomation;
     private final Object mAnimationCompleteLock = new Object();
 
-    @android.ravenwood.annotation.RavenwoodKeep
+    @RavenwoodKeep
     public Instrumentation() {
     }
 
@@ -145,7 +148,7 @@
      * reflection, but it will serve as noticeable discouragement from
      * doing such a thing.
      */
-    @android.ravenwood.annotation.RavenwoodKeep
+    @RavenwoodKeep
     private void checkInstrumenting(String method) {
         // Check if we have an instrumentation context, as init should only get called by
         // the system in startup processes that are being instrumented.
@@ -160,7 +163,7 @@
      *
      * @hide
      */
-    @android.ravenwood.annotation.RavenwoodKeep
+    @RavenwoodKeep
     public boolean isInstrumenting() {
         // Check if we have an instrumentation context, as init should only get called by
         // the system in startup processes that are being instrumented.
@@ -324,7 +327,7 @@
      * 
      * @see #getTargetContext
      */
-    @android.ravenwood.annotation.RavenwoodKeep
+    @RavenwoodKeep
     public Context getContext() {
         return mInstrContext;
     }
@@ -349,7 +352,7 @@
      * 
      * @see #getContext
      */
-    @android.ravenwood.annotation.RavenwoodKeep
+    @RavenwoodKeep
     public Context getTargetContext() {
         return mAppContext;
     }
@@ -2405,10 +2408,11 @@
      *
      * @hide
      */
-    @android.ravenwood.annotation.RavenwoodKeep
-    public final void basicInit(Context instrContext, Context appContext) {
+    @RavenwoodKeep
+    public final void basicInit(Context instrContext, Context appContext, UiAutomation ui) {
         mInstrContext = instrContext;
         mAppContext = appContext;
+        mUiAutomation = ui;
     }
 
     /** @hide */
@@ -2499,6 +2503,7 @@
      *
      * @see UiAutomation
      */
+    @RavenwoodKeep
     public UiAutomation getUiAutomation() {
         return getUiAutomation(0);
     }
@@ -2537,6 +2542,7 @@
      *
      * @see UiAutomation
      */
+    @RavenwoodReplace
     public UiAutomation getUiAutomation(@UiAutomationFlags int flags) {
         boolean mustCreateNewAutomation = (mUiAutomation == null) || (mUiAutomation.isDestroyed());
 
@@ -2567,11 +2573,15 @@
         return null;
     }
 
+    private UiAutomation getUiAutomation$ravenwood(@UiAutomationFlags int flags) {
+        return mUiAutomation;
+    }
+
     /**
      * Takes control of the execution of messages on the specified looper until
      * {@link TestLooperManager#release} is called.
      */
-    @android.ravenwood.annotation.RavenwoodKeep
+    @RavenwoodKeep
     public TestLooperManager acquireLooperManager(Looper looper) {
         checkInstrumenting("acquireLooperManager");
         return new TestLooperManager(looper);
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index 11b66fc..9629a87 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -154,6 +154,8 @@
         "framework-annotations-lib",
         "ravenwood-helper-framework-runtime",
         "ravenwood-helper-libcore-runtime",
+        "hoststubgen-helper-runtime.ravenwood",
+        "mockito-ravenwood-prebuilt",
     ],
     visibility: ["//frameworks/base"],
     jarjar_rules: ":ravenwood-services-jarjar-rules",
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
index 4ae5bd1..5894476 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
@@ -22,10 +22,14 @@
 import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_VERSION_JAVA_SYSPROP;
 
 import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
 
 import android.app.ActivityManager;
 import android.app.Instrumentation;
 import android.app.ResourcesManager;
+import android.app.UiAutomation;
 import android.content.res.Resources;
 import android.os.Binder;
 import android.os.Build;
@@ -40,6 +44,7 @@
 
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.hoststubgen.hosthelper.HostTestUtils;
 import com.android.internal.os.RuntimeInit;
 import com.android.ravenwood.RavenwoodRuntimeNative;
 import com.android.ravenwood.common.RavenwoodCommonUtils;
@@ -52,8 +57,10 @@
 import java.io.File;
 import java.io.IOException;
 import java.io.PrintStream;
+import java.util.Collections;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledFuture;
@@ -125,6 +132,9 @@
 
     private static RavenwoodConfig sConfig;
     private static RavenwoodSystemProperties sProps;
+    // TODO: use the real UiAutomation class instead of a mock
+    private static UiAutomation sMockUiAutomation;
+    private static Set<String> sAdoptedPermissions = Collections.emptySet();
     private static boolean sInitialized = false;
 
     /**
@@ -171,6 +181,7 @@
                 "androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner");
 
         assertMockitoVersion();
+        sMockUiAutomation = createMockUiAutomation();
     }
 
     /**
@@ -261,7 +272,7 @@
 
         // Prepare other fields.
         config.mInstrumentation = new Instrumentation();
-        config.mInstrumentation.basicInit(config.mInstContext, config.mTargetContext);
+        config.mInstrumentation.basicInit(instContext, targetContext, sMockUiAutomation);
         InstrumentationRegistry.registerInstance(config.mInstrumentation, Bundle.EMPTY);
 
         RavenwoodSystemServer.init(config);
@@ -300,12 +311,13 @@
         config.mInstrumentation = null;
         if (config.mInstContext != null) {
             ((RavenwoodContext) config.mInstContext).cleanUp();
+            config.mInstContext = null;
         }
         if (config.mTargetContext != null) {
             ((RavenwoodContext) config.mTargetContext).cleanUp();
+            config.mTargetContext = null;
         }
-        config.mInstContext = null;
-        config.mTargetContext = null;
+        sMockUiAutomation.dropShellPermissionIdentity();
 
         if (config.mProvideMainThread) {
             Looper.getMainLooper().quit();
@@ -407,6 +419,31 @@
                 () -> Class.forName("org.mockito.Matchers"));
     }
 
+    private static UiAutomation createMockUiAutomation() {
+        var mock = mock(UiAutomation.class, inv -> {
+            HostTestUtils.onThrowMethodCalled();
+            return null;
+        });
+        doAnswer(inv -> {
+            sAdoptedPermissions = UiAutomation.ALL_PERMISSIONS;
+            return null;
+        }).when(mock).adoptShellPermissionIdentity();
+        doAnswer(inv -> {
+            if (inv.getArgument(0) == null) {
+                sAdoptedPermissions = UiAutomation.ALL_PERMISSIONS;
+            } else {
+                sAdoptedPermissions = (Set) Set.of(inv.getArguments());
+            }
+            return null;
+        }).when(mock).adoptShellPermissionIdentity(any());
+        doAnswer(inv -> {
+            sAdoptedPermissions = Collections.emptySet();
+            return null;
+        }).when(mock).dropShellPermissionIdentity();
+        doAnswer(inv -> sAdoptedPermissions).when(mock).getAdoptedShellPermissions();
+        return mock;
+    }
+
     @SuppressWarnings("unused")  // Called from native code (ravenwood_sysprop.cpp)
     private static void checkSystemPropertyAccess(String key, boolean write) {
         boolean result = write ? sProps.isKeyWritable(key) : sProps.isKeyReadable(key);
diff --git a/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodUiAutomationTest.java b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodUiAutomationTest.java
new file mode 100644
index 0000000..eb94827
--- /dev/null
+++ b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodUiAutomationTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ravenwoodtest.bivalenttest;
+
+import static android.Manifest.permission.OVERRIDE_COMPAT_CHANGE_CONFIG;
+import static android.Manifest.permission.READ_COMPAT_CHANGE_CONFIG;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import android.app.Instrumentation;
+import android.app.UiAutomation;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.ravenwood.common.RavenwoodCommonUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodUiAutomationTest {
+
+    private Instrumentation mInstrumentation;
+
+    @Before
+    public void setup() {
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+    }
+
+    @Test
+    public void testGetUiAutomation() {
+        assertNotNull(mInstrumentation.getUiAutomation());
+    }
+
+    @Test
+    public void testGetUiAutomationWithFlags() {
+        assertNotNull(mInstrumentation.getUiAutomation(UiAutomation.FLAG_DONT_USE_ACCESSIBILITY));
+    }
+
+    @Test
+    public void testShellPermissionApis() {
+        var uiAutomation = mInstrumentation.getUiAutomation();
+        assertTrue(uiAutomation.getAdoptedShellPermissions().isEmpty());
+        uiAutomation.adoptShellPermissionIdentity();
+        assertEquals(uiAutomation.getAdoptedShellPermissions(), UiAutomation.ALL_PERMISSIONS);
+        uiAutomation.adoptShellPermissionIdentity((String[]) null);
+        assertEquals(uiAutomation.getAdoptedShellPermissions(), UiAutomation.ALL_PERMISSIONS);
+        uiAutomation.adoptShellPermissionIdentity(
+                OVERRIDE_COMPAT_CHANGE_CONFIG, READ_COMPAT_CHANGE_CONFIG);
+        assertEquals(uiAutomation.getAdoptedShellPermissions(),
+                Set.of(OVERRIDE_COMPAT_CHANGE_CONFIG, READ_COMPAT_CHANGE_CONFIG));
+        uiAutomation.dropShellPermissionIdentity();
+        assertTrue(uiAutomation.getAdoptedShellPermissions().isEmpty());
+    }
+
+    @Test
+    public void testUnsupportedMethod() {
+        // Only unsupported on Ravenwood
+        assumeTrue(RavenwoodCommonUtils.isOnRavenwood());
+        assertThrows(RuntimeException.class,
+                () -> mInstrumentation.getUiAutomation().executeShellCommand("echo ok"));
+    }
+}