Test implicit package visibility from IMEs

This CL ensures that the active IME can have an implicit visibility to
the target app package, even if IME's ApplicationManifest.xml does not
have any <queries> entry with the target app package name.

One caveat when testing this behavior is that MockImeSession also
establishes other inter-process communication channels from the
calling process to the IME process, and this is sufficient for the
system to give an implicit visibility to MockIme to see the calling
package where MockImeSession is used.

This is why yet another package
  android.view.inputmethod.ctstestapp
is introduced in this CL because
  com.android.cts.mockime
has already been able to see
  android.view.inputmethod.cts
because of its use of MockImeSession.

Another caveat is that a special option
  <option name="force-queryable" value="false" />
is necessary in AndroidTest.xml when installing the test APK,
otherwise the APK will be installed with "--force-queryable" option,
which ends up allowing any other packages to see the test APK without
any restriction.

Bug: 152909969
Test: atest CtsInputMethodTestCases:PackageVisibilityTest
Test: atest CtsInputMethodTestCases:PackageVisibilityTest --instant
Change-Id: I3cfbea70489b7c3e6f82bb9e7f412541d6779de9
diff --git a/tests/inputmethod/Android.bp b/tests/inputmethod/Android.bp
index 9e1bef2..bca549a 100644
--- a/tests/inputmethod/Android.bp
+++ b/tests/inputmethod/Android.bp
@@ -25,6 +25,7 @@
     libs: ["android.test.runner.stubs"],
     static_libs: [
         "androidx.test.rules",
+        "androidx.test.uiautomator_uiautomator",
         "compatibility-device-util-axt",
         "ctstestrunner-axt",
         "CtsMockInputMethodLib",
diff --git a/tests/inputmethod/AndroidTest.xml b/tests/inputmethod/AndroidTest.xml
index f1ec1e0..fd2ea5d 100644
--- a/tests/inputmethod/AndroidTest.xml
+++ b/tests/inputmethod/AndroidTest.xml
@@ -48,6 +48,24 @@
         <option name="run-command" value="wm dismiss-keyguard" />
     </target_preparer>
 
+    <!--
+        A (separate) standalone test app APK is needed to test implicit app-visibility from the IME
+        process to the IME target process, because if the IME target process is directly interacting
+        with MockIme process via MockImeSession, then the system would already give the MockIme an
+        implicit app-visibility back to the test app.  To fully test app-visibility scenario,
+        MockImeSession cannot be used in the process where the focused Activity is hosted.
+    -->
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <!--
+            In order to simulate the scenario where the IME client process is normally
+            installed, explicitly set false here.  Otherwise, the test APP will be installed under
+            force-queryable mode, which makes the test useless.
+        -->
+        <option name="force-queryable" value="false" />
+        <option name="test-file-name" value="CtsInputMethodStandaloneTestApp.apk" />
+    </target_preparer>
+
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true" />
         <option name="test-file-name" value="CtsInputMethodTestCases.apk" />
diff --git a/tests/inputmethod/mockime/src/com/android/cts/mockime/ImeEvent.java b/tests/inputmethod/mockime/src/com/android/cts/mockime/ImeEvent.java
index 808a972..abfae95 100644
--- a/tests/inputmethod/mockime/src/com/android/cts/mockime/ImeEvent.java
+++ b/tests/inputmethod/mockime/src/com/android/cts/mockime/ImeEvent.java
@@ -38,6 +38,7 @@
         Integer,
         String,
         CharSequence,
+        Exception,
         Parcelable,
     }
 
@@ -74,6 +75,9 @@
         if (object instanceof CharSequence) {
             return ReturnType.CharSequence;
         }
+        if (object instanceof Exception) {
+            return ReturnType.Exception;
+        }
         if (object instanceof Parcelable) {
             return ReturnType.Parcelable;
         }
@@ -143,6 +147,9 @@
             case CharSequence:
                 bundle.putCharSequence("mReturnValue", getReturnCharSequenceValue());
                 break;
+            case Exception:
+                bundle.putSerializable("mReturnValue", getReturnExceptionValue());
+                break;
             case Parcelable:
                 bundle.putParcelable("mReturnValue", getReturnParcelableValue());
                 break;
@@ -186,6 +193,9 @@
             case CharSequence:
                 result = bundle.getCharSequence("mReturnValue");
                 break;
+            case Exception:
+                result = bundle.getSerializable("mReturnValue");
+                break;
             case Parcelable:
                 result = bundle.getParcelable("mReturnValue");
                 break;
@@ -364,6 +374,25 @@
         return (String) mReturnValue;
     }
 
+     /**
+      * Retrieves a result that is known to be {@link Exception} or its subclasses.
+      *
+      * @param <T> {@link Exception} or its subclass.
+      * @return {@link Exception} object returned as a result of the command.
+      * @throws NullPointerException if the return value is {@code null}
+      * @throws ClassCastException if the return value is non-{@code null} object that is different
+      *                            from {@link Exception}
+     */
+    public <T extends Exception> T getReturnExceptionValue() {
+        if (mReturnType == ReturnType.Null) {
+            throw new NullPointerException();
+        }
+        if (mReturnType != ReturnType.Exception) {
+            throw new ClassCastException();
+        }
+        return (T) mReturnValue;
+    }
+
     /**
      * @return result value of this event.
      * @throws NullPointerException if the return value is {@code null}
@@ -380,6 +409,12 @@
         return (T) mReturnValue;
     }
 
+    /**
+     * @return {@code true} when the result value is an {@link Exception}.
+     */
+    public boolean isExceptionReturnValue() {
+        return mReturnType == ReturnType.Exception;
+    }
 
     /**
      * @return {@code true} when the result value is {@code null}.
diff --git a/tests/inputmethod/mockime/src/com/android/cts/mockime/MockIme.java b/tests/inputmethod/mockime/src/com/android/cts/mockime/MockIme.java
index 5be50de..4e5e60b 100644
--- a/tests/inputmethod/mockime/src/com/android/cts/mockime/MockIme.java
+++ b/tests/inputmethod/mockime/src/com/android/cts/mockime/MockIme.java
@@ -25,6 +25,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.PackageManager;
 import android.content.res.Configuration;
 import android.graphics.Bitmap;
 import android.inputmethodservice.InputMethodService;
@@ -287,6 +288,15 @@
                         sendDownUpKeyEvents(keyEventCode);
                         return ImeEvent.RETURN_VALUE_UNAVAILABLE;
                     }
+                    case "getApplicationInfo": {
+                        final String packageName = command.getExtras().getString("packageName");
+                        final int flags = command.getExtras().getInt("flags");
+                        try {
+                            return getPackageManager().getApplicationInfo(packageName, flags);
+                        } catch (PackageManager.NameNotFoundException e) {
+                            return e;
+                        }
+                    }
                     case "getDisplayId":
                         return getSystemService(WindowManager.class)
                                 .getDefaultDisplay().getDisplayId();
diff --git a/tests/inputmethod/mockime/src/com/android/cts/mockime/MockImeSession.java b/tests/inputmethod/mockime/src/com/android/cts/mockime/MockImeSession.java
index 699da4c..82f2085 100644
--- a/tests/inputmethod/mockime/src/com/android/cts/mockime/MockImeSession.java
+++ b/tests/inputmethod/mockime/src/com/android/cts/mockime/MockImeSession.java
@@ -975,6 +975,27 @@
         return callCommandInternal("sendDownUpKeyEvents", params);
     }
 
+    /**
+     * Lets {@link MockIme} call
+     * {@link android.content.pm.PackageManager#getApplicationInfo(String, int)} with the given
+     * {@code packageName} and {@code flags}.
+     *
+     * @param packageName the package name to be passed to
+     *                    {@link android.content.pm.PackageManager#getApplicationInfo(String, int)}.
+     * @param flags the flags to be passed to
+     *                    {@link android.content.pm.PackageManager#getApplicationInfo(String, int)}.
+     * @return {@link ImeCommand} object that can be passed to
+     *         {@link ImeEventStreamTestUtils#expectCommand(ImeEventStream, ImeCommand, long)} to
+     *         wait until this event is handled by {@link MockIme}.
+     */
+    @NonNull
+    public ImeCommand callGetApplicationInfo(@NonNull String packageName, int flags) {
+        final Bundle params = new Bundle();
+        params.putString("packageName", packageName);
+        params.putInt("flags", flags);
+        return callCommandInternal("getApplicationInfo", params);
+    }
+
     @NonNull
     public ImeCommand callGetDisplayId() {
         final Bundle params = new Bundle();
diff --git a/tests/inputmethod/src/android/view/inputmethod/cts/PackageVisibilityTest.java b/tests/inputmethod/src/android/view/inputmethod/cts/PackageVisibilityTest.java
new file mode 100644
index 0000000..7705c63
--- /dev/null
+++ b/tests/inputmethod/src/android/view/inputmethod/cts/PackageVisibilityTest.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2020 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 android.view.inputmethod.cts;
+
+import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
+import static com.android.cts.mockime.ImeEventStreamTestUtils.editorMatcher;
+import static com.android.cts.mockime.ImeEventStreamTestUtils.expectCommand;
+import static com.android.cts.mockime.ImeEventStreamTestUtils.expectEvent;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.SystemClock;
+import android.platform.test.annotations.AppModeFull;
+import android.platform.test.annotations.AppModeInstant;
+import android.view.inputmethod.cts.util.EndToEndImeTestBase;
+import android.view.inputmethod.cts.util.UnlockScreenRule;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.filters.MediumTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.Until;
+
+import com.android.cts.mockime.ImeCommand;
+import com.android.cts.mockime.ImeEvent;
+import com.android.cts.mockime.ImeEventStream;
+import com.android.cts.mockime.ImeSettings;
+import com.android.cts.mockime.MockImeSession;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.security.InvalidParameterException;
+import java.util.concurrent.TimeUnit;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public final class PackageVisibilityTest extends EndToEndImeTestBase {
+    static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5);
+
+    @Rule
+    public final UnlockScreenRule mUnlockScreenRule = new UnlockScreenRule();
+
+    private static final ComponentName TEST_ACTIVITY = new ComponentName(
+            "android.view.inputmethod.ctstestapp",
+            "android.view.inputmethod.ctstestapp.MainActivity");
+
+    private static final Uri TEST_ACTIVITY_URI =
+            Uri.parse("https://example.com/android/view/inputmethod/ctstestapp");
+
+    private static final String EXTRA_KEY_PRIVATE_IME_OPTIONS =
+            "android.view.inputmethod.ctstestapp.EXTRA_KEY_PRIVATE_IME_OPTIONS";
+
+    private static final String TEST_MARKER_PREFIX =
+            "android.view.inputmethod.cts.PackageVisibilityTest";
+
+    private static String getTestMarker() {
+        return TEST_MARKER_PREFIX + "/"  + SystemClock.elapsedRealtimeNanos();
+    }
+
+    @NonNull
+    private static Uri formatStringIntentParam(@NonNull Uri uri, @NonNull String key,
+            @Nullable String value) {
+        if (value == null) {
+            return uri;
+        }
+        return uri.buildUpon().appendQueryParameter(key, value).build();
+    }
+
+    @NonNull
+    private static String formatStringIntentParam(@NonNull String key, @Nullable String value) {
+        if (key.matches("[ \"']")) {
+            throw new InvalidParameterException("Unsupported character(s) in key=" + key);
+        }
+        if (value.matches("[ \"']")) {
+            throw new InvalidParameterException("Unsupported character(s) in value=" + value);
+        }
+        return value != null ? String.format(" --es %s %s", key, value) : "";
+    }
+
+    /**
+     * Launch the standalone version of the test {@link android.app.Activity} then wait for
+     * completions of launch.
+     *
+     * <p>Note: this method does not use
+     * {@link android.app.Instrumentation#startActivitySync(Intent)} because it does not work when
+     * both the calling process and the target process run under the instant app mode. Instead this
+     * method relies on adb command {@code adb shell am start} to work around that limitation.</p>
+     *
+     * @param instant {@code true} if the caller and the target is installed as instant apps.
+     * @param privateImeOptions If not {@code null},
+     *                          {@link android.view.inputmethod.EditorInfo#privateImeOptions} will
+     *                          in the test {@link android.app.Activity} will be set to this value.
+     * @param timeout timeout in milliseconds.
+     */
+    private void launchTestActivity(boolean instant, @Nullable String privateImeOptions,
+            long timeout) {
+        final String command;
+        if (instant) {
+            final Uri uri = formatStringIntentParam(
+                    TEST_ACTIVITY_URI, EXTRA_KEY_PRIVATE_IME_OPTIONS, privateImeOptions);
+            command = String.format("am start -a %s -c %s %s",
+                    Intent.ACTION_VIEW, Intent.CATEGORY_BROWSABLE, uri.toString());
+        } else {
+            command = String.format("am start -n %s",
+                    TEST_ACTIVITY.flattenToShortString())
+                    + formatStringIntentParam(EXTRA_KEY_PRIVATE_IME_OPTIONS, privateImeOptions);
+        }
+        runShellCommand(command);
+        UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+                .wait(Until.hasObject(By.pkg(TEST_ACTIVITY.getPackageName()).depth(0)), timeout);
+    }
+
+    @AppModeFull
+    @Test
+    public void testTargetPackageIsVisibleFromImeFull() throws Exception {
+        testTargetPackageIsVisibleFromIme(false /* instant */);
+    }
+
+    @AppModeInstant
+    @Test
+    public void testTargetPackageIsVisibleFromImeInstant() throws Exception {
+        // We need to explicitly check this condition in case tests are executed with atest command.
+        // See Bug 158617529 for details.
+        assumeTrue("This test should run when and only under the instant app mode.",
+                InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageManager()
+                        .isInstantApp());
+        testTargetPackageIsVisibleFromIme(true /* instant */);
+    }
+
+    private void testTargetPackageIsVisibleFromIme(boolean instant) throws Exception {
+        try (MockImeSession imeSession = MockImeSession.create(
+                InstrumentationRegistry.getInstrumentation().getContext(),
+                InstrumentationRegistry.getInstrumentation().getUiAutomation(),
+                new ImeSettings.Builder())) {
+            final ImeEventStream stream = imeSession.openEventStream();
+
+            final String marker = getTestMarker();
+            launchTestActivity(instant, marker, TIMEOUT);
+
+            expectEvent(stream, editorMatcher("onStartInput", marker), TIMEOUT);
+
+            final ImeCommand command = imeSession.callGetApplicationInfo(
+                    TEST_ACTIVITY.getPackageName(), PackageManager.GET_META_DATA);
+            final ImeEvent event = expectCommand(stream, command, TIMEOUT);
+
+            if (event.isNullReturnValue()) {
+                fail("getApplicationInfo() returned null.");
+            }
+            if (event.isExceptionReturnValue()) {
+                final Exception exception = event.getReturnExceptionValue();
+                fail(exception.toString());
+            }
+            final ApplicationInfo applicationInfoFromIme = event.getReturnParcelableValue();
+            assertEquals(TEST_ACTIVITY.getPackageName(), applicationInfoFromIme.packageName);
+        }
+    }
+}
diff --git a/tests/inputmethod/testapp/Android.bp b/tests/inputmethod/testapp/Android.bp
new file mode 100644
index 0000000..1d55077
--- /dev/null
+++ b/tests/inputmethod/testapp/Android.bp
@@ -0,0 +1,33 @@
+// Copyright (C) 2020 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.
+
+android_test_helper_app {
+    name: "CtsInputMethodStandaloneTestApp",
+    defaults: ["cts_defaults"],
+    sdk_version: "current",
+    // tag this module as a cts test artifact
+    test_suites: [
+        "cts",
+        "vts10",
+        "general-tests",
+    ],
+    compile_multilib: "both",
+    static_libs: [
+        "androidx.annotation_annotation",
+    ],
+    srcs: [
+        "src/**/*.java",
+        "src/**/I*.aidl",
+    ],
+}
diff --git a/tests/inputmethod/testapp/AndroidManifest.xml b/tests/inputmethod/testapp/AndroidManifest.xml
new file mode 100644
index 0000000..0f474205
--- /dev/null
+++ b/tests/inputmethod/testapp/AndroidManifest.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2020 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.view.inputmethod.ctstestapp"
+    android:targetSandboxVersion="2">
+
+    <application
+        android:label="CtsInputMethodStandaloneTestApp"
+        android:multiArch="true"
+        android:supportsRtl="true">
+        <activity
+            android:name=".MainActivity"
+            android:exported="true"
+            android:label="CtsInputMethodStandaloneTestActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:scheme="https" />
+                <data android:host="example.com" />
+                <data android:path="/android/view/inputmethod/ctstestapp" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
diff --git a/tests/inputmethod/testapp/src/android/view/inputmethod/ctstestapp/MainActivity.java b/tests/inputmethod/testapp/src/android/view/inputmethod/ctstestapp/MainActivity.java
new file mode 100644
index 0000000..58d5c42
--- /dev/null
+++ b/tests/inputmethod/testapp/src/android/view/inputmethod/ctstestapp/MainActivity.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2020 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 android.view.inputmethod.ctstestapp;
+
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE;
+
+import android.app.Activity;
+import android.net.Uri;
+import android.os.Bundle;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+
+import androidx.annotation.Nullable;
+
+/**
+ * A test {@link Activity} that automatically shows the input method.
+ */
+public final class MainActivity extends Activity {
+
+    private static final String EXTRA_KEY_PRIVATE_IME_OPTIONS =
+            "android.view.inputmethod.ctstestapp.EXTRA_KEY_PRIVATE_IME_OPTIONS";
+
+    @Nullable
+    private String getPrivateImeOptions() {
+        if (getPackageManager().isInstantApp()) {
+            final Uri uri = getIntent().getData();
+            if (uri == null || !uri.isHierarchical()) {
+                return null;
+            }
+            return uri.getQueryParameter(EXTRA_KEY_PRIVATE_IME_OPTIONS);
+        }
+        return getIntent().getStringExtra(EXTRA_KEY_PRIVATE_IME_OPTIONS);
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        final LinearLayout layout = new LinearLayout(this);
+        layout.setOrientation(LinearLayout.VERTICAL);
+        final EditText editText = new EditText(this);
+        editText.setHint("editText");
+        final String privateImeOptions = getPrivateImeOptions();
+        if (privateImeOptions != null) {
+            editText.setPrivateImeOptions(privateImeOptions);
+        }
+        editText.requestFocus();
+        layout.addView(editText);
+        getWindow().setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_VISIBLE);
+        setContentView(layout);
+    }
+}