blob: da2c43ab74308ba9c440464476d7fe506fb5c780 [file] [log] [blame]
/*
* 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.app.cts;
import static android.app.UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES;
import static android.app.cts.NotificationManagerTest.toggleListenerAccess;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
import static com.google.common.truth.Truth.assertThat;
import static junit.framework.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
import static org.testng.Assert.assertThrows;
import android.Manifest;
import android.app.ActivityManager;
import android.app.Instrumentation;
import android.app.UiAutomation;
import android.app.cts.android.app.cts.tools.FutureServiceConnection;
import android.app.cts.android.app.cts.tools.NotificationHelper;
import android.app.stubs.TestNotificationListener;
import android.app.stubs.shared.FakeView;
import android.app.stubs.shared.ICloseSystemDialogsTestsService;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.display.DisplayManager;
import android.os.Bundle;
import android.os.ConditionVariable;
import android.os.Handler;
import android.os.Looper;
import android.os.Process;
import android.os.ResultReceiver;
import android.permission.PermissionManager;
import android.permission.cts.PermissionUtils;
import android.provider.Settings;
import android.server.wm.WindowManagerStateHelper;
import android.view.Display;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import com.android.compatibility.common.util.SystemUtil;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
@RunWith(AndroidJUnit4.class)
public class CloseSystemDialogsTest {
private static final String TEST_SERVICE =
"android.app.stubs.shared.CloseSystemDialogsTestService";
private static final String APP_COMPAT_ENABLE = "enable";
private static final String APP_COMPAT_DISABLE = "disable";
private static final String APP_COMPAT_RESET = "reset";
private static final String ACTION_SENTINEL = "sentinel";
private static final String REASON = "test";
private static final long TIMEOUT_MS = 3000;
private static final String ACCESSIBILITY_SERVICE =
"android.app.stubs.shared.AppAccessibilityService";
/**
* This test is not self-instrumenting, so we need to bind to the service in the instrumentation
* target package (instead of our package).
*/
private static final String APP_SELF = "android.app.stubs";
/**
* Use com.android.app1 instead of android.app.stubs because the latter is the target of
* instrumentation, hence it also has shell powers for {@link
* Intent#ACTION_CLOSE_SYSTEM_DIALOGS} and we don't want those powers under simulation.
*/
private static final String APP_HELPER = "com.android.app4";
private Instrumentation mInstrumentation;
private FutureServiceConnection mConnection;
private Context mContext;
private ContentResolver mResolver;
private ICloseSystemDialogsTestsService mService;
private volatile WindowManager mSawWindowManager;
private volatile Context mSawContext;
private volatile CompletableFuture<Void> mCloseSystemDialogsReceived;
private volatile ConditionVariable mSentinelReceived;
private volatile FakeView mFakeView;
private WindowManagerStateHelper mWindowState;
private IntentReceiver mIntentReceiver;
private Handler mMainHandler;
private TestNotificationListener mNotificationListener;
private NotificationHelper mNotificationHelper;
private String mPreviousHiddenApiPolicy;
private String mPreviousAccessibilityServices;
private String mPreviousAccessibilityEnabled;
private boolean mResetAccessibility;
@Before
public void setUp() throws Exception {
mInstrumentation = InstrumentationRegistry.getInstrumentation();
mContext = mInstrumentation.getTargetContext();
PermissionUtils.grantPermission(APP_SELF, Manifest.permission.POST_NOTIFICATIONS);
mResolver = mContext.getContentResolver();
mMainHandler = new Handler(Looper.getMainLooper());
toggleListenerAccess(mContext, true);
mNotificationListener = TestNotificationListener.getInstance();
mNotificationHelper = new NotificationHelper(mContext, () -> mNotificationListener);
mWindowState = new WindowManagerStateHelper();
enableUserFinal();
// We need to test that a few hidden APIs are properly protected in the helper app. The
// helper app we're using doesn't have the checks disabled because it's not the target of
// instrumentation, see comment on APP_HELPER for details.
mPreviousHiddenApiPolicy = setHiddenApiPolicy("1");
// Add a receiver that will verify if the intent was sent or not
mIntentReceiver = new IntentReceiver();
mCloseSystemDialogsReceived = new CompletableFuture<>();
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
filter.addAction(ACTION_SENTINEL);
mContext.registerReceiver(mIntentReceiver, filter);
// Add a view to verify if the view got the callback or not
mSawContext = getContextForSaw(mContext);
mSawWindowManager = mSawContext.getSystemService(WindowManager.class);
mMainHandler.post(() -> {
mFakeView = new FakeView(mSawContext);
mSawWindowManager.addView(mFakeView, new LayoutParams(TYPE_APPLICATION_OVERLAY));
});
}
@After
public void tearDown() throws Exception {
if (mConnection != null) {
mContext.unbindService(mConnection);
}
if (mResetAccessibility) {
setAccessibilityState(mPreviousAccessibilityEnabled, mPreviousAccessibilityServices);
}
mMainHandler.post(() -> mSawWindowManager.removeViewImmediate(mFakeView));
mContext.unregisterReceiver(mIntentReceiver);
resetUserFinal();
setHiddenApiPolicy(mPreviousHiddenApiPolicy);
compat(APP_COMPAT_RESET, ActivityManager.LOCK_DOWN_CLOSE_SYSTEM_DIALOGS, APP_HELPER);
compat(APP_COMPAT_RESET, "NOTIFICATION_TRAMPOLINE_BLOCK", APP_HELPER);
mNotificationListener.resetData();
// Use test API to prevent PermissionManager from killing the test process when revoking
// permission.
SystemUtil.runWithShellPermissionIdentity(
() -> mContext.getSystemService(PermissionManager.class)
.revokePostNotificationPermissionWithoutKillForTest(
mContext.getPackageName(),
Process.myUserHandle().getIdentifier()),
Manifest.permission.REVOKE_POST_NOTIFICATIONS_WITHOUT_KILL,
Manifest.permission.REVOKE_RUNTIME_PERMISSIONS);
}
/** Intent.ACTION_CLOSE_SYSTEM_DIALOGS */
@Test
public void testCloseSystemDialogs_whenTargetSdkCurrent_isBlockedAndThrows() throws Exception {
setTargetCurrent();
mService = getService(APP_HELPER);
assertThrows(SecurityException.class, () -> mService.sendCloseSystemDialogsBroadcast());
assertCloseSystemDialogsNotReceived();
}
@Test
public void testCloseSystemDialogs_whenTargetSdk30_isBlockedButDoesNotThrow() throws Exception {
mService = getService(APP_HELPER);
mService.sendCloseSystemDialogsBroadcast();
assertCloseSystemDialogsNotReceived();
}
@Test
public void testCloseSystemDialogs_whenTestInstrumentedViaShell_isSent() throws Exception {
mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
assertCloseSystemDialogsReceived();
}
@Test
public void testCloseSystemDialogs_whenRunningAsShell_isSent() throws Exception {
SystemUtil.runWithShellPermissionIdentity(
() -> mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)));
assertCloseSystemDialogsReceived();
}
@Test
public void testCloseSystemDialogs_inTrampolineWhenTargetSdkCurrent_isBlockedAndThrows()
throws Exception {
setTargetCurrent();
int notificationId = 42;
CompletableFuture<Integer> result = new CompletableFuture<>();
mService = getService(APP_HELPER);
mService.postNotification(notificationId, new FutureReceiver(result),
/* usePendingIntent */ false);
mNotificationHelper.clickNotification(notificationId, /* searchAll */ true);
assertThat(result.get()).isEqualTo(
ICloseSystemDialogsTestsService.RESULT_SECURITY_EXCEPTION);
assertCloseSystemDialogsNotReceived();
}
@Test
public void testCloseSystemDialogs_inTrampolineWhenTargetSdk30_isSent() throws Exception {
int notificationId = 43;
CompletableFuture<Integer> result = new CompletableFuture<>();
mService = getService(APP_HELPER);
mService.postNotification(notificationId, new FutureReceiver(result),
/* usePendingIntent */ false);
mNotificationHelper.clickNotification(notificationId, /* searchAll */ true);
assertThat(result.get()).isEqualTo(ICloseSystemDialogsTestsService.RESULT_OK);
assertCloseSystemDialogsReceived();
}
/** System doesn't throw on the PI's sender call stack. */
@Test
public void testCloseSystemDialogs_inTrampolineViaPendingIntentWhenTargetSdkCurrent_isBlocked()
throws Exception {
setTargetCurrent();
int notificationId = 44;
CompletableFuture<Integer> result = new CompletableFuture<>();
mService = getService(APP_HELPER);
mService.postNotification(notificationId, new FutureReceiver(result),
/* usePendingIntent */ true);
mNotificationHelper.clickNotification(notificationId, /* searchAll */ true);
assertThat(result.get()).isEqualTo(ICloseSystemDialogsTestsService.RESULT_OK);
assertCloseSystemDialogsNotReceived();
}
@Test
public void testCloseSystemDialogs_inTrampolineViaPendingIntentWhenTargetSdk30_isSent()
throws Exception {
int notificationId = 45;
CompletableFuture<Integer> result = new CompletableFuture<>();
mService = getService(APP_HELPER);
mService.postNotification(notificationId, new FutureReceiver(result),
/* usePendingIntent */ true);
mNotificationHelper.clickNotification(notificationId, /* searchAll */ true);
assertThat(result.get()).isEqualTo(ICloseSystemDialogsTestsService.RESULT_OK);
assertCloseSystemDialogsReceived();
}
@Test
public void testCloseSystemDialogs_withWindowAboveShadeAndTargetSdk30_isSent()
throws Exception {
// Test is only applicable to devices that have a notification shade.
assumeTrue(mWindowState.hasNotificationShade());
mService = getService(APP_HELPER);
setAccessibilityService(APP_HELPER, ACCESSIBILITY_SERVICE);
assertTrue(mService.waitForAccessibilityServiceWindow(TIMEOUT_MS));
mService.sendCloseSystemDialogsBroadcast();
assertCloseSystemDialogsReceived();
}
/** IWindowManager.closeSystemDialogs() */
@Test
public void testCloseSystemDialogsViaWindowManager_whenTestInstrumentedViaShell_isSent()
throws Exception {
mService = getService(APP_SELF);
mService.closeSystemDialogsViaWindowManager(REASON);
assertThat(mFakeView.getNextCloseSystemDialogsCallReason(TIMEOUT_MS)).isEqualTo(REASON);
}
@Test
public void testCloseSystemDialogsViaWindowManager_whenRunningAsShell_isSent()
throws Exception {
mService = getService(APP_SELF);
SystemUtil.runWithShellPermissionIdentity(
() -> mService.closeSystemDialogsViaWindowManager(REASON));
assertThat(mFakeView.getNextCloseSystemDialogsCallReason(TIMEOUT_MS)).isEqualTo(REASON);
}
@Test
public void testCloseSystemDialogsViaWindowManager_whenTargetSdkCurrent_isBlockedAndThrows()
throws Exception {
setTargetCurrent();
mService = getService(APP_HELPER);
assertThrows(SecurityException.class,
() -> mService.closeSystemDialogsViaWindowManager(REASON));
assertThat(mFakeView.getNextCloseSystemDialogsCallReason(TIMEOUT_MS)).isEqualTo(null);
}
@Test
public void testCloseSystemDialogsViaWindowManager_whenTargetSdk30_isBlockedButDoesNotThrow()
throws Exception {
mService = getService(APP_HELPER);
mService.closeSystemDialogsViaWindowManager(REASON);
assertThat(mFakeView.getNextCloseSystemDialogsCallReason(TIMEOUT_MS)).isEqualTo(null);
}
/** IActivityManager.closeSystemDialogs() */
@Test
public void testCloseSystemDialogsViaActivityManager_whenTestInstrumentedViaShell_isSent()
throws Exception {
mService = getService(APP_SELF);
mService.closeSystemDialogsViaActivityManager(REASON);
assertThat(mFakeView.getNextCloseSystemDialogsCallReason(TIMEOUT_MS)).isEqualTo(REASON);
assertCloseSystemDialogsReceived();
}
@Test
public void testCloseSystemDialogsViaActivityManager_whenRunningAsShell_isSent()
throws Exception {
mService = getService(APP_SELF);
SystemUtil.runWithShellPermissionIdentity(
() -> mService.closeSystemDialogsViaActivityManager(REASON));
assertThat(mFakeView.getNextCloseSystemDialogsCallReason(TIMEOUT_MS)).isEqualTo(REASON);
assertCloseSystemDialogsReceived();
}
@Test
public void testCloseSystemDialogsViaActivityManager_whenTargetSdkCurrent_isBlockedAndThrows()
throws Exception {
setTargetCurrent();
mService = getService(APP_HELPER);
assertThrows(SecurityException.class,
() -> mService.closeSystemDialogsViaActivityManager(REASON));
assertThat(mFakeView.getNextCloseSystemDialogsCallReason(TIMEOUT_MS)).isEqualTo(null);
assertCloseSystemDialogsNotReceived();
}
@Test
public void testCloseSystemDialogsViaActivityManager_whenTargetSdk30_isBlockedButDoesNotThrow()
throws Exception {
mService = getService(APP_HELPER);
mService.closeSystemDialogsViaActivityManager(REASON);
assertThat(mFakeView.getNextCloseSystemDialogsCallReason(TIMEOUT_MS)).isEqualTo(null);
assertCloseSystemDialogsNotReceived();
}
private void setTargetCurrent() {
// The helper app has targetSdk=30, opting-in to changes emulates targeting latest sdk.
compat(APP_COMPAT_ENABLE, ActivityManager.LOCK_DOWN_CLOSE_SYSTEM_DIALOGS, APP_HELPER);
compat(APP_COMPAT_ENABLE, "NOTIFICATION_TRAMPOLINE_BLOCK", APP_HELPER);
}
private void assertCloseSystemDialogsNotReceived() {
// If both broadcasts are sent, they will be received in order here since they are both
// registered receivers in the "bg" queue in system_server and belong to the same app.
// This is guaranteed by a series of handlers that are the same in both cases and due to the
// fact that the binder that system_server uses to call into the app is the same (since the
// app is the same) and one-way calls on the same binder object are ordered.
mSentinelReceived = new ConditionVariable(false);
Intent intent = new Intent(ACTION_SENTINEL);
intent.setPackage(mContext.getPackageName());
mContext.sendBroadcast(intent);
mSentinelReceived.block();
assertThat(mCloseSystemDialogsReceived.isDone()).isFalse();
}
private void assertCloseSystemDialogsReceived() throws Exception {
mCloseSystemDialogsReceived.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
// No TimeoutException thrown
}
private ICloseSystemDialogsTestsService getService(String packageName) throws Exception {
ICloseSystemDialogsTestsService service =
ICloseSystemDialogsTestsService.Stub.asInterface(
connect(packageName).get(TIMEOUT_MS));
assertTrue("Can't call @hide methods", service.waitUntilReady(TIMEOUT_MS));
return service;
}
private FutureServiceConnection connect(String packageName) {
if (mConnection != null) {
return mConnection;
}
mConnection = new FutureServiceConnection();
Intent intent = new Intent();
intent.setComponent(ComponentName.createRelative(packageName, TEST_SERVICE));
assertTrue(mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE));
return mConnection;
}
private String setHiddenApiPolicy(String policy) throws Exception {
return SystemUtil.callWithShellPermissionIdentity(() -> {
String previous = Settings.Global.getString(mResolver,
Settings.Global.HIDDEN_API_POLICY);
Settings.Global.putString(mResolver, Settings.Global.HIDDEN_API_POLICY, policy);
return previous;
});
}
private void setAccessibilityService(String packageName, String service) throws Exception {
setAccessibilityState("1", packageName + "/" + service);
}
private void setAccessibilityState(String enabled, String services) {
mResetAccessibility = true;
UiAutomation uiAutomation = mInstrumentation.getUiAutomation(
FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES);
SystemUtil.runWithShellPermissionIdentity(uiAutomation, () -> {
mPreviousAccessibilityServices = Settings.Secure.getString(mResolver,
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
mPreviousAccessibilityEnabled = Settings.Secure.getString(mResolver,
Settings.Secure.ACCESSIBILITY_ENABLED);
Settings.Secure.putString(mResolver, Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
services);
Settings.Secure.putString(mResolver, Settings.Secure.ACCESSIBILITY_ENABLED, enabled);
});
}
private static void enableUserFinal() {
SystemUtil.runShellCommand(
"settings put global force_non_debuggable_final_build_for_compat 1");
}
private static void resetUserFinal() {
SystemUtil.runShellCommand(
"settings put global force_non_debuggable_final_build_for_compat 0");
}
private static void compat(String command, String changeId, String packageName) {
SystemUtil.runShellCommand(
String.format("am compat %s %s %s", command, changeId, packageName));
}
private static void compat(String command, long changeId, String packageName) {
compat(command, Long.toString(changeId), packageName);
}
private static Context getContextForSaw(Context context) {
DisplayManager displayManager = context.getSystemService(DisplayManager.class);
Display display = displayManager.getDisplay(DEFAULT_DISPLAY);
Context displayContext = context.createDisplayContext(display);
return displayContext.createWindowContext(TYPE_APPLICATION_OVERLAY, null);
}
private class IntentReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case Intent.ACTION_CLOSE_SYSTEM_DIALOGS:
mCloseSystemDialogsReceived.complete(null);
break;
case ACTION_SENTINEL:
mSentinelReceived.open();
break;
}
}
}
private class FutureReceiver extends ResultReceiver {
private final CompletableFuture<Integer> mFuture;
FutureReceiver(CompletableFuture<Integer> future) {
super(mMainHandler);
mFuture = future;
}
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
mFuture.complete(resultCode);
}
}
}