RESTRICT AUTOMERGE
CTS for verifying toasts are not clickable
To verify this, we:
* Allow reflection via hidden_api_policy setting (to make sure we can construct
a clickable toast).
* Send broadcast to app to post a toast.
* App posts toast and waits for it to be displayed, communicating back
to test.
* Test clicks toast and waits for the tap to be propagated back.
* If click is received, test fails, otherwise, succeeds.
Bug: 128674520
Test: atest CtsWindowManagerDeviceTestCases:ToastTest
1) Passes with ag/9585932
2) Fails without ag/9585932
Change-Id: I6d2903477f8ea86c75d6415890f443414751ab84
diff --git a/tests/framework/base/windowmanager/app/AndroidManifest.xml b/tests/framework/base/windowmanager/app/AndroidManifest.xml
index 0b79756..67cafd4 100755
--- a/tests/framework/base/windowmanager/app/AndroidManifest.xml
+++ b/tests/framework/base/windowmanager/app/AndroidManifest.xml
@@ -532,6 +532,10 @@
<action android:name="android.service.vr.VrListenerService" />
</intent-filter>
</service>
+
+ <receiver
+ android:name=".ToastReceiver"
+ android:exported="true" />
</application>
</manifest>
diff --git a/tests/framework/base/windowmanager/app/res/layout/toast.xml b/tests/framework/base/windowmanager/app/res/layout/toast.xml
new file mode 100644
index 0000000..3663ab6
--- /dev/null
+++ b/tests/framework/base/windowmanager/app/res/layout/toast.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2019 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.
+ -->
+
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="#FFFFFFFF"
+ >
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:text="I'm a fullscreen toast!" />
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/tests/framework/base/windowmanager/app/src/android/server/wm/app/Components.java b/tests/framework/base/windowmanager/app/src/android/server/wm/app/Components.java
index 94b397f..734b482 100644
--- a/tests/framework/base/windowmanager/app/src/android/server/wm/app/Components.java
+++ b/tests/framework/base/windowmanager/app/src/android/server/wm/app/Components.java
@@ -168,6 +168,9 @@
public static final ComponentName LAUNCH_BROADCAST_RECEIVER =
component("LaunchBroadcastReceiver");
+ public static final ComponentName TOAST_RECEIVER =
+ component("ToastReceiver");
+
public static class LaunchBroadcastReceiver {
public static final String LAUNCH_BROADCAST_ACTION =
"android.server.wm.app.LAUNCH_BROADCAST_ACTION";
@@ -413,11 +416,16 @@
public static final String COMMAND_RESIZE_DISPLAY = "resize_display";
}
+ public static class ToastReceiver {
+ public static final String ACTION_TOAST_DISPLAYED = "toast_displayed";
+ public static final String ACTION_TOAST_TAP_DETECTED = "toast_tap_detected";
+ }
+
private static ComponentName component(String className) {
return component(Components.class, className);
}
- private static String getPackageName() {
+ public static String getPackageName() {
return getPackageName(Components.class);
}
}
diff --git a/tests/framework/base/windowmanager/app/src/android/server/wm/app/ToastReceiver.java b/tests/framework/base/windowmanager/app/src/android/server/wm/app/ToastReceiver.java
new file mode 100644
index 0000000..9ef6c3a
--- /dev/null
+++ b/tests/framework/base/windowmanager/app/src/android/server/wm/app/ToastReceiver.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2019 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.server.wm.app;
+
+import static android.server.wm.app.Components.ToastReceiver.ACTION_TOAST_DISPLAYED;
+import static android.server.wm.app.Components.ToastReceiver.ACTION_TOAST_TAP_DETECTED;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager.LayoutParams;
+import android.widget.Toast;
+
+import java.lang.ref.WeakReference;
+import java.lang.reflect.Field;
+
+public class ToastReceiver extends BroadcastReceiver {
+ private static final int DETECT_TOAST_TIMEOUT_MS = 15000;
+ private static final int DETECT_TOAST_POOLING_INTERVAL_MS = 200;
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Handler handler = new Handler();
+ Toast toast = getToast(context);
+ long deadline = SystemClock.uptimeMillis() + DETECT_TOAST_TIMEOUT_MS;
+ handler.post(
+ new DetectToastRunnable(
+ context.getApplicationContext(), toast.getView(), deadline, handler));
+ toast.show();
+ }
+
+ private Toast getToast(Context context) {
+ Context applicationContext = context.getApplicationContext();
+ View view = LayoutInflater.from(context).inflate(R.layout.toast, null);
+ view.setOnTouchListener((v, event) -> {
+ applicationContext.sendBroadcast(new Intent(ACTION_TOAST_TAP_DETECTED));
+ return false;
+ });
+ Toast toast = getClickableToast(context);
+ toast.setView(view);
+ toast.setGravity(Gravity.FILL_HORIZONTAL | Gravity.FILL_VERTICAL, 0, 0);
+ toast.setDuration(Toast.LENGTH_LONG);
+ return toast;
+ }
+
+ /**
+ * Purposely creating a toast without FLAG_NOT_TOUCHABLE in the client-side (via reflection) to
+ * test enforcement on the server-side.
+ */
+ private Toast getClickableToast(Context context) {
+ try {
+ Toast toast = new Toast(context);
+ Field tnField = Toast.class.getDeclaredField("mTN");
+ tnField.setAccessible(true);
+ Object tnObject = tnField.get(toast);
+ Field paramsField = Class.forName(
+ Toast.class.getCanonicalName() + "$TN").getDeclaredField("mParams");
+ paramsField.setAccessible(true);
+ LayoutParams params = (LayoutParams) paramsField.get(tnObject);
+ params.flags = LayoutParams.FLAG_KEEP_SCREEN_ON | LayoutParams.FLAG_NOT_FOCUSABLE;
+ return toast;
+ } catch (NoSuchFieldException | IllegalAccessException | ClassNotFoundException e) {
+ throw new IllegalStateException("Toast reflection failed", e);
+ }
+ }
+
+ private static class DetectToastRunnable implements Runnable {
+ private final Context mContext;
+ private final WeakReference<View> mToastViewRef;
+ private final long mDeadline;
+ private final Handler mHandler;
+
+ private DetectToastRunnable(
+ Context applicationContext, View toastView, long deadline, Handler handler) {
+ mContext = applicationContext;
+ mToastViewRef = new WeakReference<>(toastView);
+ mDeadline = deadline;
+ mHandler = handler;
+ }
+
+ @Override
+ public void run() {
+ View toastView = mToastViewRef.get();
+ if (SystemClock.uptimeMillis() > mDeadline || toastView == null) {
+ return;
+ }
+ if (toastView.getParent() != null) {
+ mContext.sendBroadcast(new Intent(ACTION_TOAST_DISPLAYED));
+ return;
+ }
+ mHandler.postDelayed(this, DETECT_TOAST_POOLING_INTERVAL_MS);
+ }
+ }
+}
diff --git a/tests/framework/base/windowmanager/src/android/server/wm/ToastTest.java b/tests/framework/base/windowmanager/src/android/server/wm/ToastTest.java
new file mode 100644
index 0000000..253ade4
--- /dev/null
+++ b/tests/framework/base/windowmanager/src/android/server/wm/ToastTest.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2019 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.server.wm;
+
+import static android.server.wm.app.Components.ToastReceiver.ACTION_TOAST_DISPLAYED;
+import static android.server.wm.app.Components.ToastReceiver.ACTION_TOAST_TAP_DETECTED;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.Looper;
+import android.platform.test.annotations.Presubmit;
+import android.provider.Settings;
+import android.server.wm.WindowManagerState.WindowState;
+import android.server.wm.app.Components;
+import android.view.WindowManager.LayoutParams;
+
+import com.android.compatibility.common.util.SystemUtil;
+
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+
+@Presubmit
+public class ToastTest extends ActivityManagerTestBase {
+ private static final String SETTING_HIDDEN_API_POLICY = "hidden_api_policy";
+ private static final long TOAST_DISPLAY_TIMEOUT_MS = 8000;
+ private static final long TOAST_TAP_TIMEOUT_MS = 3500;
+
+ /**
+ * Tests can be executed as soon as the device has booted. When that happens the broadcast queue
+ * is long and it takes some time to process the broadcast we just sent.
+ */
+ private static final long BROADCAST_DELIVERY_TIMEOUT_MS = 60000;
+
+ @Nullable
+ private String mPreviousHiddenApiPolicy;
+ private Map<String, ConditionVariable> mBroadcastsReceived;
+
+ private BroadcastReceiver mAppCommunicator = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ getBroadcastReceivedVariable(intent.getAction()).open();
+ }
+ };
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ SystemUtil.runWithShellPermissionIdentity(() -> {
+ ContentResolver resolver = mContext.getContentResolver();
+ mPreviousHiddenApiPolicy = Settings.Global.getString(resolver,
+ SETTING_HIDDEN_API_POLICY);
+ Settings.Global.putString(resolver, SETTING_HIDDEN_API_POLICY, "1");
+ });
+ // Stopping just in case, to make sure reflection is allowed
+ stopTestPackage(Components.getPackageName());
+
+ // These are parallel broadcasts, not affected by a busy queue
+ mBroadcastsReceived = Collections.synchronizedMap(new HashMap<>());
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(ACTION_TOAST_DISPLAYED);
+ filter.addAction(ACTION_TOAST_TAP_DETECTED);
+ mContext.registerReceiver(mAppCommunicator, filter);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mContext.unregisterReceiver(mAppCommunicator);
+ SystemUtil.runWithShellPermissionIdentity(() -> {
+ Settings.Global.putString(mContext.getContentResolver(), SETTING_HIDDEN_API_POLICY,
+ mPreviousHiddenApiPolicy);
+ });
+ super.tearDown();
+ }
+
+ @Test
+ public void testToastIsNotClickable() {
+ Intent intent = new Intent();
+ intent.setComponent(Components.TOAST_RECEIVER);
+ sendAndWaitForBroadcast(intent);
+ boolean toastDisplayed = getBroadcastReceivedVariable(ACTION_TOAST_DISPLAYED).block(
+ TOAST_DISPLAY_TIMEOUT_MS);
+ assertTrue("Toast not displayed on time", toastDisplayed);
+ WindowManagerState wmState = getAmWmState().getWmState();
+ wmState.computeState();
+ WindowState toastWindow = wmState.findFirstWindowWithType(LayoutParams.TYPE_TOAST);
+ assertNotNull("Couldn't retrieve toast window", toastWindow);
+
+ tapOnCenter(toastWindow.getContainingFrame(), toastWindow.getDisplayId());
+
+ boolean toastClicked = getBroadcastReceivedVariable(ACTION_TOAST_TAP_DETECTED).block(
+ TOAST_TAP_TIMEOUT_MS);
+ assertFalse("Toast tap detected", toastClicked);
+ }
+
+ private void sendAndWaitForBroadcast(Intent intent) {
+ assertNotEquals("Can't wait on main thread", Thread.currentThread(),
+ Looper.getMainLooper().getThread());
+
+ ConditionVariable broadcastDelivered = new ConditionVariable(false);
+ mContext.sendOrderedBroadcast(
+ intent,
+ null,
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ broadcastDelivered.open();
+ }
+ },
+ new Handler(Looper.getMainLooper()),
+ Activity.RESULT_OK,
+ null,
+ null);
+ broadcastDelivered.block(BROADCAST_DELIVERY_TIMEOUT_MS);
+ }
+
+ private ConditionVariable getBroadcastReceivedVariable(String action) {
+ return mBroadcastsReceived.computeIfAbsent(action, key -> new ConditionVariable());
+ }
+}
diff --git a/tests/framework/base/windowmanager/util/src/android/server/wm/ActivityManagerTestBase.java b/tests/framework/base/windowmanager/util/src/android/server/wm/ActivityManagerTestBase.java
index f7337e9..c99764e 100644
--- a/tests/framework/base/windowmanager/util/src/android/server/wm/ActivityManagerTestBase.java
+++ b/tests/framework/base/windowmanager/util/src/android/server/wm/ActivityManagerTestBase.java
@@ -500,11 +500,14 @@
injectMotion(downTime, upTime, MotionEvent.ACTION_UP, x, y, displayId);
}
+ protected void tapOnCenter(Rect bounds, int displayId) {
+ final int tapX = bounds.left + bounds.width() / 2;
+ final int tapY = bounds.top + bounds.height() / 2;
+ tapOnDisplay(tapX, tapY, displayId);
+ }
+
protected void tapOnStackCenter(ActivityManagerState.ActivityStack stack) {
- final Rect sideStackBounds = stack.getBounds();
- final int tapX = sideStackBounds.left + sideStackBounds.width() / 2;
- final int tapY = sideStackBounds.top + sideStackBounds.height() / 2;
- tapOnDisplay(tapX, tapY, stack.mDisplayId);
+ tapOnCenter(stack.getBounds(), stack.mDisplayId);
}
private static void injectMotion(long downTime, long eventTime, int action,