1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.app.cts;
18 
19 import static android.app.UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES;
20 import static android.app.cts.NotificationManagerTest.toggleListenerAccess;
21 import static android.view.Display.DEFAULT_DISPLAY;
22 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
23 
24 import static com.google.common.truth.Truth.assertThat;
25 
26 import static junit.framework.Assert.assertTrue;
27 
28 import static org.junit.Assume.assumeTrue;
29 import static org.testng.Assert.assertThrows;
30 
31 import android.app.ActivityManager;
32 import android.app.Instrumentation;
33 import android.app.UiAutomation;
34 import android.app.cts.android.app.cts.tools.FutureServiceConnection;
35 import android.app.cts.android.app.cts.tools.NotificationHelper;
36 import android.app.stubs.TestNotificationListener;
37 import android.app.stubs.shared.FakeView;
38 import android.app.stubs.shared.ICloseSystemDialogsTestsService;
39 import android.content.BroadcastReceiver;
40 import android.content.ComponentName;
41 import android.content.ContentResolver;
42 import android.content.Context;
43 import android.content.Intent;
44 import android.content.IntentFilter;
45 import android.hardware.display.DisplayManager;
46 import android.os.Bundle;
47 import android.os.ConditionVariable;
48 import android.os.Handler;
49 import android.os.Looper;
50 import android.os.ResultReceiver;
51 import android.provider.Settings;
52 import android.server.wm.WindowManagerStateHelper;
53 import android.view.Display;
54 import android.view.WindowManager;
55 import android.view.WindowManager.LayoutParams;
56 
57 import androidx.test.platform.app.InstrumentationRegistry;
58 import androidx.test.runner.AndroidJUnit4;
59 
60 import com.android.compatibility.common.util.SystemUtil;
61 
62 import org.junit.After;
63 import org.junit.Before;
64 import org.junit.Test;
65 import org.junit.runner.RunWith;
66 
67 import java.util.concurrent.CompletableFuture;
68 import java.util.concurrent.TimeUnit;
69 
70 @RunWith(AndroidJUnit4.class)
71 public class CloseSystemDialogsTest {
72     private static final String TEST_SERVICE =
73             "android.app.stubs.shared.CloseSystemDialogsTestService";
74     private static final String APP_COMPAT_ENABLE = "enable";
75     private static final String APP_COMPAT_DISABLE = "disable";
76     private static final String APP_COMPAT_RESET = "reset";
77     private static final String ACTION_SENTINEL = "sentinel";
78     private static final String REASON = "test";
79     private static final long TIMEOUT_MS = 3000;
80     private static final String ACCESSIBILITY_SERVICE =
81             "android.app.stubs.shared.AppAccessibilityService";
82 
83     /**
84      * This test is not self-instrumenting, so we need to bind to the service in the instrumentation
85      * target package (instead of our package).
86      */
87     private static final String APP_SELF = "android.app.stubs";
88 
89     /**
90      * Use com.android.app1 instead of android.app.stubs because the latter is the target of
91      * instrumentation, hence it also has shell powers for {@link
92      * Intent#ACTION_CLOSE_SYSTEM_DIALOGS} and we don't want those powers under simulation.
93      */
94     private static final String APP_HELPER = "com.android.app4";
95 
96     private Instrumentation mInstrumentation;
97     private FutureServiceConnection mConnection;
98     private Context mContext;
99     private ContentResolver mResolver;
100     private ICloseSystemDialogsTestsService mService;
101     private volatile WindowManager mSawWindowManager;
102     private volatile Context mSawContext;
103     private volatile CompletableFuture<Void> mCloseSystemDialogsReceived;
104     private volatile ConditionVariable mSentinelReceived;
105     private volatile FakeView mFakeView;
106     private WindowManagerStateHelper mWindowState;
107     private IntentReceiver mIntentReceiver;
108     private Handler mMainHandler;
109     private TestNotificationListener mNotificationListener;
110     private NotificationHelper mNotificationHelper;
111     private String mPreviousHiddenApiPolicy;
112     private String mPreviousAccessibilityServices;
113     private String mPreviousAccessibilityEnabled;
114     private boolean mResetAccessibility;
115 
116 
117     @Before
setUp()118     public void setUp() throws Exception {
119         mInstrumentation = InstrumentationRegistry.getInstrumentation();
120         mContext = mInstrumentation.getTargetContext();
121         mResolver = mContext.getContentResolver();
122         mMainHandler = new Handler(Looper.getMainLooper());
123         toggleListenerAccess(mContext, true);
124         mNotificationListener = TestNotificationListener.getInstance();
125         mNotificationHelper = new NotificationHelper(mContext, () -> mNotificationListener);
126         mWindowState = new WindowManagerStateHelper();
127         enableUserFinal();
128 
129         // We need to test that a few hidden APIs are properly protected in the helper app. The
130         // helper app we're using doesn't have the checks disabled because it's not the target of
131         // instrumentation, see comment on APP_HELPER for details.
132         mPreviousHiddenApiPolicy = setHiddenApiPolicy("1");
133 
134         // Add a receiver that will verify if the intent was sent or not
135         mIntentReceiver = new IntentReceiver();
136         mCloseSystemDialogsReceived = new CompletableFuture<>();
137         IntentFilter filter = new IntentFilter();
138         filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
139         filter.addAction(ACTION_SENTINEL);
140         mContext.registerReceiver(mIntentReceiver, filter);
141 
142         // Add a view to verify if the view got the callback or not
143         mSawContext = getContextForSaw(mContext);
144         mSawWindowManager = mSawContext.getSystemService(WindowManager.class);
145         mMainHandler.post(() -> {
146             mFakeView = new FakeView(mSawContext);
147             mSawWindowManager.addView(mFakeView, new LayoutParams(TYPE_APPLICATION_OVERLAY));
148         });
149     }
150 
151     @After
tearDown()152     public void tearDown() throws Exception {
153         if (mConnection != null) {
154             mContext.unbindService(mConnection);
155         }
156         if (mResetAccessibility) {
157             setAccessibilityState(mPreviousAccessibilityEnabled, mPreviousAccessibilityServices);
158         }
159         mMainHandler.post(() -> mSawWindowManager.removeViewImmediate(mFakeView));
160         mContext.unregisterReceiver(mIntentReceiver);
161         resetUserFinal();
162         setHiddenApiPolicy(mPreviousHiddenApiPolicy);
163         compat(APP_COMPAT_RESET, ActivityManager.LOCK_DOWN_CLOSE_SYSTEM_DIALOGS, APP_HELPER);
164         compat(APP_COMPAT_RESET, "NOTIFICATION_TRAMPOLINE_BLOCK", APP_HELPER);
165         mNotificationListener.resetData();
166     }
167 
168     /** Intent.ACTION_CLOSE_SYSTEM_DIALOGS */
169 
170     @Test
testCloseSystemDialogs_whenTargetSdkCurrent_isBlockedAndThrows()171     public void testCloseSystemDialogs_whenTargetSdkCurrent_isBlockedAndThrows() throws Exception {
172         setTargetCurrent();
173         mService = getService(APP_HELPER);
174 
175         assertThrows(SecurityException.class, () -> mService.sendCloseSystemDialogsBroadcast());
176 
177         assertCloseSystemDialogsNotReceived();
178     }
179 
180     @Test
testCloseSystemDialogs_whenTargetSdk30_isBlockedButDoesNotThrow()181     public void testCloseSystemDialogs_whenTargetSdk30_isBlockedButDoesNotThrow() throws Exception {
182         mService = getService(APP_HELPER);
183 
184         mService.sendCloseSystemDialogsBroadcast();
185 
186         assertCloseSystemDialogsNotReceived();
187     }
188 
189     @Test
testCloseSystemDialogs_whenTestInstrumentedViaShell_isSent()190     public void testCloseSystemDialogs_whenTestInstrumentedViaShell_isSent() throws Exception {
191         mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
192 
193         assertCloseSystemDialogsReceived();
194     }
195 
196     @Test
testCloseSystemDialogs_whenRunningAsShell_isSent()197     public void testCloseSystemDialogs_whenRunningAsShell_isSent() throws Exception {
198         SystemUtil.runWithShellPermissionIdentity(
199                 () -> mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)));
200 
201         assertCloseSystemDialogsReceived();
202     }
203 
204     @Test
testCloseSystemDialogs_inTrampolineWhenTargetSdkCurrent_isBlockedAndThrows()205     public void testCloseSystemDialogs_inTrampolineWhenTargetSdkCurrent_isBlockedAndThrows()
206             throws Exception {
207         setTargetCurrent();
208         int notificationId = 42;
209         CompletableFuture<Integer> result = new CompletableFuture<>();
210         mService = getService(APP_HELPER);
211 
212         mService.postNotification(notificationId, new FutureReceiver(result),
213                 /* usePendingIntent */ false);
214 
215         mNotificationHelper.clickNotification(notificationId, /* searchAll */ true);
216         assertThat(result.get()).isEqualTo(
217                 ICloseSystemDialogsTestsService.RESULT_SECURITY_EXCEPTION);
218         assertCloseSystemDialogsNotReceived();
219     }
220 
221     @Test
testCloseSystemDialogs_inTrampolineWhenTargetSdk30_isSent()222     public void testCloseSystemDialogs_inTrampolineWhenTargetSdk30_isSent() throws Exception {
223         int notificationId = 43;
224         CompletableFuture<Integer> result = new CompletableFuture<>();
225         mService = getService(APP_HELPER);
226 
227         mService.postNotification(notificationId, new FutureReceiver(result),
228                 /* usePendingIntent */ false);
229 
230         mNotificationHelper.clickNotification(notificationId, /* searchAll */ true);
231         assertThat(result.get()).isEqualTo(ICloseSystemDialogsTestsService.RESULT_OK);
232         assertCloseSystemDialogsReceived();
233     }
234 
235     /** System doesn't throw on the PI's sender call stack. */
236     @Test
testCloseSystemDialogs_inTrampolineViaPendingIntentWhenTargetSdkCurrent_isBlocked()237     public void testCloseSystemDialogs_inTrampolineViaPendingIntentWhenTargetSdkCurrent_isBlocked()
238             throws Exception {
239         setTargetCurrent();
240         int notificationId = 44;
241         CompletableFuture<Integer> result = new CompletableFuture<>();
242         mService = getService(APP_HELPER);
243 
244         mService.postNotification(notificationId, new FutureReceiver(result),
245                 /* usePendingIntent */ true);
246 
247         mNotificationHelper.clickNotification(notificationId, /* searchAll */ true);
248         assertThat(result.get()).isEqualTo(ICloseSystemDialogsTestsService.RESULT_OK);
249         assertCloseSystemDialogsNotReceived();
250     }
251 
252     @Test
testCloseSystemDialogs_inTrampolineViaPendingIntentWhenTargetSdk30_isSent()253     public void testCloseSystemDialogs_inTrampolineViaPendingIntentWhenTargetSdk30_isSent()
254             throws Exception {
255         int notificationId = 45;
256         CompletableFuture<Integer> result = new CompletableFuture<>();
257         mService = getService(APP_HELPER);
258 
259         mService.postNotification(notificationId, new FutureReceiver(result),
260                 /* usePendingIntent */ true);
261 
262         mNotificationHelper.clickNotification(notificationId, /* searchAll */ true);
263         assertThat(result.get()).isEqualTo(ICloseSystemDialogsTestsService.RESULT_OK);
264         assertCloseSystemDialogsReceived();
265     }
266 
267     @Test
testCloseSystemDialogs_withWindowAboveShadeAndTargetSdk30_isSent()268     public void testCloseSystemDialogs_withWindowAboveShadeAndTargetSdk30_isSent()
269             throws Exception {
270         // Test is only applicable to devices that have a notification shade.
271         assumeTrue(mWindowState.hasNotificationShade());
272         mService = getService(APP_HELPER);
273         setAccessibilityService(APP_HELPER, ACCESSIBILITY_SERVICE);
274         assertTrue(mService.waitForAccessibilityServiceWindow(TIMEOUT_MS));
275 
276         mService.sendCloseSystemDialogsBroadcast();
277 
278         assertCloseSystemDialogsReceived();
279     }
280 
281     /** IWindowManager.closeSystemDialogs() */
282 
283     @Test
testCloseSystemDialogsViaWindowManager_whenTestInstrumentedViaShell_isSent()284     public void testCloseSystemDialogsViaWindowManager_whenTestInstrumentedViaShell_isSent()
285             throws Exception {
286         mService = getService(APP_SELF);
287 
288         mService.closeSystemDialogsViaWindowManager(REASON);
289 
290         assertThat(mFakeView.getNextCloseSystemDialogsCallReason(TIMEOUT_MS)).isEqualTo(REASON);
291     }
292 
293     @Test
testCloseSystemDialogsViaWindowManager_whenRunningAsShell_isSent()294     public void testCloseSystemDialogsViaWindowManager_whenRunningAsShell_isSent()
295             throws Exception {
296         mService = getService(APP_SELF);
297 
298         SystemUtil.runWithShellPermissionIdentity(
299                 () -> mService.closeSystemDialogsViaWindowManager(REASON));
300 
301         assertThat(mFakeView.getNextCloseSystemDialogsCallReason(TIMEOUT_MS)).isEqualTo(REASON);
302     }
303 
304     @Test
testCloseSystemDialogsViaWindowManager_whenTargetSdkCurrent_isBlockedAndThrows()305     public void testCloseSystemDialogsViaWindowManager_whenTargetSdkCurrent_isBlockedAndThrows()
306             throws Exception {
307         setTargetCurrent();
308         mService = getService(APP_HELPER);
309 
310         assertThrows(SecurityException.class,
311                 () -> mService.closeSystemDialogsViaWindowManager(REASON));
312 
313         assertThat(mFakeView.getNextCloseSystemDialogsCallReason(TIMEOUT_MS)).isEqualTo(null);
314     }
315 
316 
317     @Test
testCloseSystemDialogsViaWindowManager_whenTargetSdk30_isBlockedButDoesNotThrow()318     public void testCloseSystemDialogsViaWindowManager_whenTargetSdk30_isBlockedButDoesNotThrow()
319             throws Exception {
320         mService = getService(APP_HELPER);
321 
322         mService.closeSystemDialogsViaWindowManager(REASON);
323 
324         assertThat(mFakeView.getNextCloseSystemDialogsCallReason(TIMEOUT_MS)).isEqualTo(null);
325     }
326 
327     /** IActivityManager.closeSystemDialogs() */
328 
329     @Test
testCloseSystemDialogsViaActivityManager_whenTestInstrumentedViaShell_isSent()330     public void testCloseSystemDialogsViaActivityManager_whenTestInstrumentedViaShell_isSent()
331             throws Exception {
332         mService = getService(APP_SELF);
333 
334         mService.closeSystemDialogsViaActivityManager(REASON);
335 
336         assertThat(mFakeView.getNextCloseSystemDialogsCallReason(TIMEOUT_MS)).isEqualTo(REASON);
337         assertCloseSystemDialogsReceived();
338     }
339 
340     @Test
testCloseSystemDialogsViaActivityManager_whenRunningAsShell_isSent()341     public void testCloseSystemDialogsViaActivityManager_whenRunningAsShell_isSent()
342             throws Exception {
343         mService = getService(APP_SELF);
344 
345         SystemUtil.runWithShellPermissionIdentity(
346                 () -> mService.closeSystemDialogsViaActivityManager(REASON));
347 
348         assertThat(mFakeView.getNextCloseSystemDialogsCallReason(TIMEOUT_MS)).isEqualTo(REASON);
349         assertCloseSystemDialogsReceived();
350     }
351 
352     @Test
testCloseSystemDialogsViaActivityManager_whenTargetSdkCurrent_isBlockedAndThrows()353     public void testCloseSystemDialogsViaActivityManager_whenTargetSdkCurrent_isBlockedAndThrows()
354             throws Exception {
355         setTargetCurrent();
356         mService = getService(APP_HELPER);
357 
358         assertThrows(SecurityException.class,
359                 () -> mService.closeSystemDialogsViaActivityManager(REASON));
360 
361         assertThat(mFakeView.getNextCloseSystemDialogsCallReason(TIMEOUT_MS)).isEqualTo(null);
362         assertCloseSystemDialogsNotReceived();
363     }
364 
365     @Test
testCloseSystemDialogsViaActivityManager_whenTargetSdk30_isBlockedButDoesNotThrow()366     public void testCloseSystemDialogsViaActivityManager_whenTargetSdk30_isBlockedButDoesNotThrow()
367             throws Exception {
368         mService = getService(APP_HELPER);
369 
370         mService.closeSystemDialogsViaActivityManager(REASON);
371 
372         assertThat(mFakeView.getNextCloseSystemDialogsCallReason(TIMEOUT_MS)).isEqualTo(null);
373         assertCloseSystemDialogsNotReceived();
374     }
375 
setTargetCurrent()376     private void setTargetCurrent() {
377         // The helper app has targetSdk=30, opting-in to changes emulates targeting latest sdk.
378         compat(APP_COMPAT_ENABLE, ActivityManager.LOCK_DOWN_CLOSE_SYSTEM_DIALOGS, APP_HELPER);
379         compat(APP_COMPAT_ENABLE, "NOTIFICATION_TRAMPOLINE_BLOCK", APP_HELPER);
380     }
381 
assertCloseSystemDialogsNotReceived()382     private void assertCloseSystemDialogsNotReceived() {
383         // If both broadcasts are sent, they will be received in order here since they are both
384         // registered receivers in the "bg" queue in system_server and belong to the same app.
385         // This is guaranteed by a series of handlers that are the same in both cases and due to the
386         // fact that the binder that system_server uses to call into the app is the same (since the
387         // app is the same) and one-way calls on the same binder object are ordered.
388         mSentinelReceived = new ConditionVariable(false);
389         Intent intent = new Intent(ACTION_SENTINEL);
390         intent.setPackage(mContext.getPackageName());
391         mContext.sendBroadcast(intent);
392         mSentinelReceived.block();
393         assertThat(mCloseSystemDialogsReceived.isDone()).isFalse();
394     }
395 
assertCloseSystemDialogsReceived()396     private void assertCloseSystemDialogsReceived() throws Exception {
397         mCloseSystemDialogsReceived.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
398         // No TimeoutException thrown
399     }
400 
getService(String packageName)401     private ICloseSystemDialogsTestsService getService(String packageName) throws Exception {
402         ICloseSystemDialogsTestsService service =
403                 ICloseSystemDialogsTestsService.Stub.asInterface(
404                         connect(packageName).get(TIMEOUT_MS));
405         assertTrue("Can't call @hide methods", service.waitUntilReady(TIMEOUT_MS));
406         return service;
407     }
408 
connect(String packageName)409     private FutureServiceConnection connect(String packageName) {
410         if (mConnection != null) {
411             return mConnection;
412         }
413         mConnection = new FutureServiceConnection();
414         Intent intent = new Intent();
415         intent.setComponent(ComponentName.createRelative(packageName, TEST_SERVICE));
416         assertTrue(mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE));
417         return mConnection;
418     }
419 
setHiddenApiPolicy(String policy)420     private String setHiddenApiPolicy(String policy) throws Exception {
421         return SystemUtil.callWithShellPermissionIdentity(() -> {
422             String previous = Settings.Global.getString(mResolver,
423                     Settings.Global.HIDDEN_API_POLICY);
424             Settings.Global.putString(mResolver, Settings.Global.HIDDEN_API_POLICY, policy);
425             return previous;
426         });
427     }
428 
setAccessibilityService(String packageName, String service)429     private void setAccessibilityService(String packageName, String service) throws Exception {
430         setAccessibilityState("1", packageName + "/" + service);
431     }
432 
setAccessibilityState(String enabled, String services)433     private void setAccessibilityState(String enabled, String services) {
434         mResetAccessibility = true;
435         UiAutomation uiAutomation = mInstrumentation.getUiAutomation(
436                 FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES);
437         SystemUtil.runWithShellPermissionIdentity(uiAutomation, () -> {
438             mPreviousAccessibilityServices = Settings.Secure.getString(mResolver,
439                     Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
440             mPreviousAccessibilityEnabled = Settings.Secure.getString(mResolver,
441                     Settings.Secure.ACCESSIBILITY_ENABLED);
442             Settings.Secure.putString(mResolver, Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
443                     services);
444             Settings.Secure.putString(mResolver, Settings.Secure.ACCESSIBILITY_ENABLED, enabled);
445         });
446     }
447 
enableUserFinal()448     private static void enableUserFinal() {
449         SystemUtil.runShellCommand(
450                 "settings put global force_non_debuggable_final_build_for_compat 1");
451     }
452 
resetUserFinal()453     private static void resetUserFinal() {
454         SystemUtil.runShellCommand(
455                 "settings put global force_non_debuggable_final_build_for_compat 0");
456     }
457 
compat(String command, String changeId, String packageName)458     private static void compat(String command, String changeId, String packageName) {
459         SystemUtil.runShellCommand(
460                 String.format("am compat %s %s %s", command, changeId, packageName));
461     }
462 
compat(String command, long changeId, String packageName)463     private static void compat(String command, long changeId, String packageName) {
464         compat(command, Long.toString(changeId), packageName);
465     }
466 
getContextForSaw(Context context)467     private static Context getContextForSaw(Context context) {
468         DisplayManager displayManager = context.getSystemService(DisplayManager.class);
469         Display display = displayManager.getDisplay(DEFAULT_DISPLAY);
470         Context displayContext = context.createDisplayContext(display);
471         return displayContext.createWindowContext(TYPE_APPLICATION_OVERLAY, null);
472     }
473 
474     private class IntentReceiver extends BroadcastReceiver {
475         @Override
onReceive(Context context, Intent intent)476         public void onReceive(Context context, Intent intent) {
477             switch (intent.getAction()) {
478                 case Intent.ACTION_CLOSE_SYSTEM_DIALOGS:
479                     mCloseSystemDialogsReceived.complete(null);
480                     break;
481                 case ACTION_SENTINEL:
482                     mSentinelReceived.open();
483                     break;
484             }
485         }
486     }
487 
488     private class FutureReceiver extends ResultReceiver {
489         private final CompletableFuture<Integer> mFuture;
490 
FutureReceiver(CompletableFuture<Integer> future)491         FutureReceiver(CompletableFuture<Integer> future) {
492             super(mMainHandler);
493             mFuture = future;
494         }
495 
496         @Override
onReceiveResult(int resultCode, Bundle resultData)497         protected void onReceiveResult(int resultCode, Bundle resultData) {
498             mFuture.complete(resultCode);
499         }
500     }
501 }
502