1 /*
2  * Copyright (C) 2022 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.jobscheduler.cts;
18 
19 import static android.app.job.JobInfo.NETWORK_TYPE_ANY;
20 import static android.jobscheduler.cts.TestAppInterface.TEST_APP_PACKAGE;
21 
22 import static com.android.compatibility.common.util.TestUtils.waitUntil;
23 
24 import static org.junit.Assert.assertTrue;
25 
26 import android.app.Notification;
27 import android.app.NotificationChannel;
28 import android.app.NotificationManager;
29 import android.app.job.JobInfo;
30 import android.app.job.JobParameters;
31 import android.app.job.JobService;
32 import android.content.pm.ApplicationInfo;
33 import android.jobscheduler.cts.UserInitiatedJobTest.WatchUidRunner;
34 import android.jobscheduler.cts.jobtestapp.TestJobSchedulerReceiver;
35 import android.provider.Settings;
36 import android.service.notification.StatusBarNotification;
37 
38 import androidx.test.InstrumentationRegistry;
39 import androidx.test.filters.LargeTest;
40 
41 import com.android.compatibility.common.util.AnrMonitor;
42 import com.android.compatibility.common.util.SystemUtil;
43 
44 import java.util.Collections;
45 import java.util.Map;
46 
47 /**
48  * Tests related to attaching notifications to jobs via
49  * {@link JobService#setNotification(JobParameters, int, Notification, int)}
50  */
51 public class NotificationTest extends BaseJobSchedulerTest {
52     private static final int JOB_ID = NotificationTest.class.hashCode();
53     private static final long DEFAULT_WAIT_TIMEOUT_MS = 2_000;
54     private static final String NOTIFICATION_CHANNEL_ID =
55             NotificationTest.class.getSimpleName() + "_channel";
56 
57     private NotificationManager mNotificationManager;
58     private NetworkingHelper mNetworkingHelper;
59 
60     @Override
setUp()61     public void setUp() throws Exception {
62         super.setUp();
63         mNotificationManager = getContext().getSystemService(NotificationManager.class);
64         NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID,
65                 NotificationTest.class.getSimpleName(), NotificationManager.IMPORTANCE_DEFAULT);
66         mNotificationManager.createNotificationChannel(channel);
67         mNetworkingHelper =
68                 new NetworkingHelper(InstrumentationRegistry.getInstrumentation(), mContext);
69     }
70 
71     @Override
tearDown()72     public void tearDown() throws Exception {
73         mJobScheduler.cancel(JOB_ID);
74         mNotificationManager.cancelAll();
75         mNetworkingHelper.tearDown();
76 
77         // The super method should be called at the end.
78         super.tearDown();
79     }
80 
testNotificationJobEndDetach()81     public void testNotificationJobEndDetach() throws Exception {
82         mNotificationManager.cancelAll();
83         final int notificationId = 123;
84         JobInfo jobInfo = new JobInfo.Builder(JOB_ID, kJobServiceComponent).build();
85 
86         Notification notification = new Notification.Builder(getContext(), NOTIFICATION_CHANNEL_ID)
87                 .setContentTitle("test title")
88                 .setSmallIcon(android.R.mipmap.sym_def_app_icon)
89                 .setContentText("test content")
90                 .build();
91 
92         kTestEnvironment.setExpectedExecutions(1);
93         kTestEnvironment.setContinueAfterStart();
94         kTestEnvironment.setNotificationAtStart(notificationId, notification,
95                 JobService.JOB_END_NOTIFICATION_POLICY_DETACH);
96         mJobScheduler.schedule(jobInfo);
97         assertTrue("Job didn't start", kTestEnvironment.awaitExecution());
98 
99         waitUntil("Notification wasn't posted", 15 /* seconds */,
100                 () -> {
101                     StatusBarNotification[] activeNotifications =
102                             mNotificationManager.getActiveNotifications();
103                     return activeNotifications.length == 1
104                             && activeNotifications[0].getId() == notificationId;
105                 });
106 
107         kTestEnvironment.setExpectedStopped();
108         mJobScheduler.cancel(JOB_ID);
109         assertTrue(kTestEnvironment.awaitStopped());
110 
111         Thread.sleep(1000); // Wait a bit for NotificationManager to catch up
112         // Notification should remain
113         StatusBarNotification[] activeNotifications = mNotificationManager.getActiveNotifications();
114         assertEquals(1, activeNotifications.length);
115         assertEquals(notificationId, activeNotifications[0].getId());
116     }
117 
testNotificationJobEndRemove()118     public void testNotificationJobEndRemove() throws Exception {
119         mNotificationManager.cancelAll();
120         final int notificationId = 123;
121         JobInfo jobInfo = new JobInfo.Builder(JOB_ID, kJobServiceComponent).build();
122 
123         Notification notification = new Notification.Builder(getContext(), NOTIFICATION_CHANNEL_ID)
124                 .setContentTitle("test title")
125                 .setSmallIcon(android.R.mipmap.sym_def_app_icon)
126                 .setContentText("test content")
127                 .build();
128 
129         kTestEnvironment.setExpectedExecutions(1);
130         kTestEnvironment.setContinueAfterStart();
131         kTestEnvironment.setNotificationAtStart(notificationId, notification,
132                 JobService.JOB_END_NOTIFICATION_POLICY_REMOVE);
133         mJobScheduler.schedule(jobInfo);
134         assertTrue("Job didn't start", kTestEnvironment.awaitExecution());
135 
136         waitUntil("Notification wasn't posted", 15 /* seconds */,
137                 () -> {
138                     StatusBarNotification[] activeNotifications =
139                             mNotificationManager.getActiveNotifications();
140                     return activeNotifications.length == 1
141                             && activeNotifications[0].getId() == notificationId;
142                 });
143 
144         kTestEnvironment.setExpectedStopped();
145         mJobScheduler.cancel(JOB_ID);
146         assertTrue(kTestEnvironment.awaitStopped());
147 
148         waitUntil("Notification wasn't removed", 15 /* seconds */,
149                 () -> {
150                     // Notification should be gone
151                     return mNotificationManager.getActiveNotifications().length == 0;
152                 });
153     }
154 
testNotificationRemovedOnForceStop()155     public void testNotificationRemovedOnForceStop() throws Exception {
156         mNetworkingHelper.setAllNetworksEnabled(true);
157         try (TestAppInterface mTestAppInterface = new TestAppInterface(mContext, JOB_ID);
158              TestNotificationListener.NotificationHelper notificationHelper =
159                      new TestNotificationListener.NotificationHelper(
160                              mContext, TestAppInterface.TEST_APP_PACKAGE)) {
161             mTestAppInterface.startAndKeepTestActivity(true);
162             mTestAppInterface.scheduleJob(
163                     Map.of(
164                             TestJobSchedulerReceiver.EXTRA_AS_USER_INITIATED, true,
165                             TestJobSchedulerReceiver.EXTRA_SET_NOTIFICATION, true
166                     ),
167                     Map.of(
168                             TestJobSchedulerReceiver.EXTRA_SET_NOTIFICATION_JOB_END_POLICY,
169                             JobService.JOB_END_NOTIFICATION_POLICY_DETACH,
170                             TestJobSchedulerReceiver.EXTRA_REQUIRED_NETWORK_TYPE, NETWORK_TYPE_ANY
171                     ));
172 
173             assertTrue("Job did not start after scheduling",
174                     mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT_MS));
175 
176             StatusBarNotification jobNotification = notificationHelper.getNotification();
177             assertNotNull(jobNotification);
178 
179             mTestAppInterface.forceStopApp();
180 
181             notificationHelper.assertNotificationsRemoved();
182         }
183     }
184 
testNotificationRemovedOnPackageRestriction()185     public void testNotificationRemovedOnPackageRestriction() throws Exception {
186         String initialActivityManagerConstants = null;
187         try (TestAppInterface testAppInterface = new TestAppInterface(mContext, JOB_ID);
188              TestNotificationListener.NotificationHelper notificationHelper =
189                      new TestNotificationListener.NotificationHelper(
190                              mContext, TestAppInterface.TEST_APP_PACKAGE)) {
191             initialActivityManagerConstants =
192                     Settings.Global.getString(getContext().getContentResolver(),
193                     Settings.Global.ACTIVITY_MANAGER_CONSTANTS);
194             SystemUtil.runShellCommand("am set-deterministic-uid-idle true");
195             // Set background_settle_time to 0 so that the transition from UID active to UID idle
196             // happens quickly.
197             Settings.Global.putString(getContext().getContentResolver(),
198                     Settings.Global.ACTIVITY_MANAGER_CONSTANTS, "background_settle_time=0");
199 
200             testAppInterface.setTestPackageRestricted(true);
201             testAppInterface.startAndKeepTestActivity(true);
202             testAppInterface.scheduleJob(
203                     Map.of(TestJobSchedulerReceiver.EXTRA_SET_NOTIFICATION, true),
204                     Map.of(
205                             TestJobSchedulerReceiver.EXTRA_SET_NOTIFICATION_JOB_END_POLICY,
206                             JobService.JOB_END_NOTIFICATION_POLICY_DETACH
207                     ));
208 
209             assertTrue("Job did not start after scheduling",
210                     testAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT_MS));
211 
212             StatusBarNotification jobNotification = notificationHelper.getNotification();
213             assertNotNull(jobNotification);
214 
215             final ApplicationInfo testAppInfo =
216                     mContext.getPackageManager().getApplicationInfo(TEST_APP_PACKAGE, 0);
217             try (WatchUidRunner uidWatcher = new WatchUidRunner(
218                     InstrumentationRegistry.getInstrumentation(), testAppInfo.uid)) {
219                 // Close the activity so the app isn't considered TOP.
220                 testAppInterface.closeActivity(true);
221                 uidWatcher.waitFor(UserInitiatedJobTest.WatchUidRunner.CMD_IDLE);
222                 Thread.sleep(1000); // Wait a bit for JS to process.
223             }
224 
225             assertTrue(testAppInterface.awaitJobStop(DEFAULT_WAIT_TIMEOUT_MS));
226             notificationHelper.assertNotificationsRemoved();
227         } finally {
228             Settings.Global.putString(getContext().getContentResolver(),
229                     Settings.Global.ACTIVITY_MANAGER_CONSTANTS, initialActivityManagerConstants);
230             SystemUtil.runShellCommand("am set-deterministic-uid-idle false");
231         }
232     }
233 
testNotificationRemovedOnTaskManagerStop()234     public void testNotificationRemovedOnTaskManagerStop() throws Exception {
235         mNetworkingHelper.setAllNetworksEnabled(true);
236         try (TestAppInterface mTestAppInterface = new TestAppInterface(mContext, JOB_ID);
237              TestNotificationListener.NotificationHelper notificationHelper =
238                      new TestNotificationListener.NotificationHelper(
239                              mContext, TestAppInterface.TEST_APP_PACKAGE)) {
240             mTestAppInterface.startAndKeepTestActivity(true);
241             mTestAppInterface.scheduleJob(
242                     Map.of(
243                             TestJobSchedulerReceiver.EXTRA_AS_USER_INITIATED, true,
244                             TestJobSchedulerReceiver.EXTRA_SET_NOTIFICATION, true
245                     ),
246                     Map.of(
247                             TestJobSchedulerReceiver.EXTRA_SET_NOTIFICATION_JOB_END_POLICY,
248                             JobService.JOB_END_NOTIFICATION_POLICY_DETACH,
249                             TestJobSchedulerReceiver.EXTRA_REQUIRED_NETWORK_TYPE, NETWORK_TYPE_ANY
250                     ));
251 
252             assertTrue("Job did not start after scheduling",
253                     mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT_MS));
254 
255             StatusBarNotification jobNotification = notificationHelper.getNotification();
256             assertNotNull(jobNotification);
257 
258             // Use the same stop reasons as a Task Manager stop.
259             mTestAppInterface.stopJob(JobParameters.STOP_REASON_USER,
260                     JobParameters.INTERNAL_STOP_REASON_USER_UI_STOP);
261 
262             notificationHelper.assertNotificationsRemoved();
263         }
264     }
265 
266     /**
267      * Test that an ANR happens if the app is required to show a notification
268      * but doesn't provide one.
269      */
testNotification_userInitiated_anrWhenNotProvided()270     public void testNotification_userInitiated_anrWhenNotProvided() throws Exception {
271         mNetworkingHelper.setAllNetworksEnabled(true);
272         try (TestAppInterface testAppInterface = new TestAppInterface(mContext, JOB_ID);
273              AnrMonitor monitor = AnrMonitor.start(InstrumentationRegistry.getInstrumentation(),
274                      TEST_APP_PACKAGE);
275              TestNotificationListener.NotificationHelper notificationHelper =
276                      new TestNotificationListener.NotificationHelper(mContext, TEST_APP_PACKAGE)) {
277 
278             testAppInterface.postUiInitiatingNotification(
279                     Map.of(
280                             TestJobSchedulerReceiver.EXTRA_AS_USER_INITIATED, true,
281                             TestJobSchedulerReceiver.EXTRA_SET_NOTIFICATION, false
282                     ),
283                     Map.of(TestJobSchedulerReceiver.EXTRA_REQUIRED_NETWORK_TYPE, NETWORK_TYPE_ANY));
284 
285             // Clicking on the notification should put the app into a BAL approved state.
286             notificationHelper.clickNotification();
287 
288             assertTrue("Job did not start after scheduling",
289                     testAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT_MS));
290 
291             // Confirm ANR
292             monitor.waitForAnrAndReturnUptime(30_000);
293         }
294     }
295 
296     /**
297      * Test that no ANR happens if the app is required to show a notification and it provides one.
298      */
299     @LargeTest
testNotification_userInitiated_noAnrWhenProvided()300     public void testNotification_userInitiated_noAnrWhenProvided() throws Exception {
301         mNetworkingHelper.setAllNetworksEnabled(true);
302         try (TestAppInterface testAppInterface = new TestAppInterface(mContext, JOB_ID);
303              AnrMonitor monitor = AnrMonitor.start(InstrumentationRegistry.getInstrumentation(),
304                      TEST_APP_PACKAGE);
305              TestNotificationListener.NotificationHelper notificationHelper =
306                      new TestNotificationListener.NotificationHelper(mContext, TEST_APP_PACKAGE)) {
307 
308             testAppInterface.postUiInitiatingNotification(
309                     Map.of(
310                             TestJobSchedulerReceiver.EXTRA_AS_USER_INITIATED, true,
311                             TestJobSchedulerReceiver.EXTRA_SET_NOTIFICATION, true
312                     ),
313                     Map.of(TestJobSchedulerReceiver.EXTRA_REQUIRED_NETWORK_TYPE, NETWORK_TYPE_ANY));
314 
315             // Clicking on the notification should put the app into a BAL approved state.
316             notificationHelper.clickNotification();
317 
318             assertTrue("Job did not start after scheduling",
319                     testAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT_MS));
320 
321             // Confirm no ANR
322             monitor.assertNoAnr(25_000);
323         }
324     }
325 
326     /**
327      * Test that no ANR happens if the app is not required to show a notification
328      * and it doesn't provide one.
329      */
330     @LargeTest
testNotification_regular_noAnrWhenNotProvided()331     public void testNotification_regular_noAnrWhenNotProvided() throws Exception {
332         try (TestAppInterface testAppInterface = new TestAppInterface(mContext, JOB_ID);
333              AnrMonitor monitor = AnrMonitor.start(InstrumentationRegistry.getInstrumentation(),
334                      TEST_APP_PACKAGE);
335              TestNotificationListener.NotificationHelper notificationHelper =
336                      new TestNotificationListener.NotificationHelper(mContext, TEST_APP_PACKAGE)) {
337 
338             testAppInterface.postUiInitiatingNotification(
339                     Map.of(TestJobSchedulerReceiver.EXTRA_SET_NOTIFICATION, false),
340                     Collections.emptyMap());
341 
342             notificationHelper.clickNotification();
343 
344             assertTrue("Job did not start after scheduling",
345                     testAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT_MS));
346 
347             // Confirm no ANR
348             monitor.assertNoAnr(25_000);
349         }
350     }
351 
testUserInitiatedJob_hasUijNotificationFlag()352     public void testUserInitiatedJob_hasUijNotificationFlag() throws Exception {
353         mNetworkingHelper.setAllNetworksEnabled(true);
354         try (TestAppInterface mTestAppInterface = new TestAppInterface(mContext, JOB_ID);
355              TestNotificationListener.NotificationHelper notificationHelper =
356                      new TestNotificationListener.NotificationHelper(
357                              mContext, TestAppInterface.TEST_APP_PACKAGE)) {
358             mTestAppInterface.startAndKeepTestActivity(true);
359             mTestAppInterface.scheduleJob(
360                     Map.of(
361                             TestJobSchedulerReceiver.EXTRA_AS_USER_INITIATED, true,
362                             TestJobSchedulerReceiver.EXTRA_SET_NOTIFICATION, true
363                     ),
364                     Map.of(TestJobSchedulerReceiver.EXTRA_REQUIRED_NETWORK_TYPE, NETWORK_TYPE_ANY));
365 
366             assertTrue("Job did not start after scheduling",
367                     mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT_MS));
368 
369             StatusBarNotification jobNotification = notificationHelper.getNotification();
370             assertNotNull(jobNotification);
371             assertTrue("A user-initiated job notification should have the UIJ flag",
372                     jobNotification.getNotification().isUserInitiatedJob());
373         }
374     }
375 
testNonUserInitiatedJob_doesNotHaveUijNotificationFlag()376     public void testNonUserInitiatedJob_doesNotHaveUijNotificationFlag() throws Exception {
377         try (TestAppInterface mTestAppInterface = new TestAppInterface(mContext, JOB_ID);
378              TestNotificationListener.NotificationHelper notificationHelper =
379                      new TestNotificationListener.NotificationHelper(
380                              mContext, TestAppInterface.TEST_APP_PACKAGE)) {
381             mTestAppInterface.startAndKeepTestActivity(true);
382             mTestAppInterface.scheduleJob(
383                     Map.of(TestJobSchedulerReceiver.EXTRA_SET_NOTIFICATION, true),
384                     Collections.emptyMap());
385 
386             assertTrue("Job did not start after scheduling",
387                     mTestAppInterface.awaitJobStart(DEFAULT_WAIT_TIMEOUT_MS));
388 
389             StatusBarNotification jobNotification = notificationHelper.getNotification();
390             assertNotNull(jobNotification);
391             assertFalse("A non user-initiated job notification should not have the UIJ flag",
392                     jobNotification.getNotification().isUserInitiatedJob());
393         }
394     }
395 
396     /**
397      * Test that a notification associated with a user-initiated job cannot be cancelled and that
398      * its notification channel cannot be deleted.
399      */
testUserInitiatedJobNotificationBehavior()400     public void testUserInitiatedJobNotificationBehavior() throws Exception {
401         mNotificationManager.cancelAll();
402         mNetworkingHelper.setAllNetworksEnabled(true);
403         startAndKeepTestActivity();
404         final int notificationId = 123;
405         JobInfo jobInfo = new JobInfo.Builder(JOB_ID, kJobServiceComponent)
406                 .setUserInitiated(true)
407                 .setRequiredNetworkType(NETWORK_TYPE_ANY)
408                 .build();
409 
410         Notification notification = new Notification.Builder(getContext(), NOTIFICATION_CHANNEL_ID)
411                 .setContentTitle("test title")
412                 .setSmallIcon(android.R.mipmap.sym_def_app_icon)
413                 .setContentText("test content")
414                 .build();
415 
416         kTestEnvironment.setExpectedExecutions(1);
417         kTestEnvironment.setContinueAfterStart();
418         kTestEnvironment.setNotificationAtStart(notificationId, notification,
419                 JobService.JOB_END_NOTIFICATION_POLICY_REMOVE);
420         mJobScheduler.schedule(jobInfo);
421         runSatisfiedJob(JOB_ID);
422         assertTrue("Job didn't start", kTestEnvironment.awaitExecution());
423 
424         waitUntil("Notification wasn't posted", 15 /* seconds */,
425                 () -> {
426                     StatusBarNotification[] activeNotifications =
427                             mNotificationManager.getActiveNotifications();
428                     return activeNotifications.length == 1
429                             && activeNotifications[0].getId() == notificationId;
430                 });
431 
432         mNotificationManager.cancel(notificationId);
433         waitUntil("A user-initiated job notification should not be cancellable by apps.",
434                 5 /* seconds */,
435                 () -> {
436                     StatusBarNotification[] activeNotifications =
437                             mNotificationManager.getActiveNotifications();
438                     return activeNotifications.length == 1
439                             && activeNotifications[0].getId() == notificationId;
440                 });
441 
442         try {
443             mNotificationManager.deleteNotificationChannel(NOTIFICATION_CHANNEL_ID);
444             fail("A notification channel associated with a user-initiated job "
445                     + "should not be cancellable by apps.");
446         } catch (SecurityException expected) {
447             assertNotNull(mNotificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID));
448         }
449     }
450 
451     /**
452      * Test that a notification associated with a non user-initiated job can be cancelled and that
453      * its notification channel can be deleted.
454      */
testNonUserInitiatedJobNotificationBehavior()455     public void testNonUserInitiatedJobNotificationBehavior() throws Exception {
456         mNotificationManager.cancelAll();
457         mNetworkingHelper.setAllNetworksEnabled(true);
458         startAndKeepTestActivity();
459         final int notificationId = 123;
460         JobInfo jobInfo = new JobInfo.Builder(JOB_ID, kJobServiceComponent).build();
461 
462         Notification notification = new Notification.Builder(getContext(), NOTIFICATION_CHANNEL_ID)
463                 .setContentTitle("test title")
464                 .setSmallIcon(android.R.mipmap.sym_def_app_icon)
465                 .setContentText("test content")
466                 .build();
467 
468         kTestEnvironment.setExpectedExecutions(1);
469         kTestEnvironment.setContinueAfterStart();
470         kTestEnvironment.setNotificationAtStart(notificationId, notification,
471                 JobService.JOB_END_NOTIFICATION_POLICY_REMOVE);
472         mJobScheduler.schedule(jobInfo);
473         runSatisfiedJob(JOB_ID);
474         assertTrue("Job didn't start", kTestEnvironment.awaitExecution());
475 
476         waitUntil("Notification wasn't posted", 15 /* seconds */,
477                 () -> {
478                     StatusBarNotification[] activeNotifications =
479                             mNotificationManager.getActiveNotifications();
480                     return activeNotifications.length == 1
481                             && activeNotifications[0].getId() == notificationId;
482                 });
483 
484         mNotificationManager.cancel(notificationId);
485         waitUntil("A non user-initiated job notification should be cancellable by apps.",
486                 15 /* seconds */,
487                 () -> {
488                     // Notification should be gone
489                     return mNotificationManager.getActiveNotifications().length == 0;
490                 });
491 
492         try {
493             mNotificationManager.deleteNotificationChannel(NOTIFICATION_CHANNEL_ID);
494             assertNull(mNotificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID));
495         } catch (SecurityException e) {
496             fail("A notification channel associated with a non user-initiated job "
497                     + "should be cancellable by apps.");
498         }
499     }
500 }
501