Merge "Remove job notifications on user stop." into udc-dev

This commit is contained in:
Kweku Adams
2023-02-23 17:59:29 +00:00
committed by Android (Google) Code Review
6 changed files with 92 additions and 14 deletions

View File

@@ -19,6 +19,7 @@ package android.app.job;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.TestApi;
import android.app.ActivityManager;
import android.app.usage.UsageStatsManager;
import android.compat.annotation.UnsupportedAppUsage;
@@ -102,6 +103,7 @@ public class JobParameters implements Parcelable {
* The user stopped the job via some UI (eg. Task Manager).
* @hide
*/
@TestApi
public static final int INTERNAL_STOP_REASON_USER_UI_STOP =
JobProtoEnums.INTERNAL_STOP_REASON_USER_UI_STOP; // 11.
/**

View File

@@ -21,6 +21,7 @@ import static android.app.job.JobService.JOB_END_NOTIFICATION_POLICY_REMOVE;
import android.annotation.NonNull;
import android.app.Notification;
import android.app.job.JobParameters;
import android.app.job.JobService;
import android.content.pm.UserPackage;
import android.os.UserHandle;
@@ -80,7 +81,7 @@ class JobNotificationCoordinator {
final NotificationDetails oldDetails = mNotificationDetails.get(hostingContext);
if (oldDetails != null && oldDetails.notificationId != notificationId) {
// App is switching notification IDs. Remove association with the old one.
removeNotificationAssociation(hostingContext);
removeNotificationAssociation(hostingContext, JobParameters.STOP_REASON_UNDEFINED);
}
final int userId = UserHandle.getUserId(callingUid);
// TODO(260848384): ensure apps can't cancel the notification for user-initiated job
@@ -100,7 +101,8 @@ class JobNotificationCoordinator {
mNotificationDetails.put(hostingContext, details);
}
void removeNotificationAssociation(@NonNull JobServiceContext hostingContext) {
void removeNotificationAssociation(@NonNull JobServiceContext hostingContext,
@JobParameters.StopReason int stopReason) {
final NotificationDetails details = mNotificationDetails.remove(hostingContext);
if (details == null) {
return;
@@ -114,7 +116,10 @@ class JobNotificationCoordinator {
ArraySet<JobServiceContext> associatedContexts = associations.get(details.notificationId);
if (associatedContexts == null || associatedContexts.isEmpty()) {
// No more jobs using this notification. Apply the final job stop policy.
if (details.jobEndNotificationPolicy == JOB_END_NOTIFICATION_POLICY_REMOVE) {
// If the user attempted to stop the job/app, then always remove the notification
// so the user doesn't get confused about the app state.
if (details.jobEndNotificationPolicy == JOB_END_NOTIFICATION_POLICY_REMOVE
|| stopReason == JobParameters.STOP_REASON_USER) {
final String packageName = details.userPackage.packageName;
mNotificationManagerInternal.cancelNotification(
packageName, packageName, details.appUid, details.appPid, /* tag */ null,

View File

@@ -670,6 +670,7 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler {
+ " [PACKAGE] [JOB_ID]");
pw.println(" Trigger immediate timeout of currently executing jobs, as if their");
pw.println(" execution timeout had expired.");
pw.println(" This is the equivalent of calling `stop -s 3 -i 3`.");
pw.println(" Options:");
pw.println(" -u or --user: specify which user's job is to be run; the default is");
pw.println(" all users");

View File

@@ -1373,7 +1373,7 @@ public final class JobServiceContext implements ServiceConnection {
JobSchedulerEconomicPolicy.ACTION_JOB_TIMEOUT,
String.valueOf(mRunningJob.getJobId()));
}
mNotificationCoordinator.removeNotificationAssociation(this);
mNotificationCoordinator.removeNotificationAssociation(this, reschedulingStopReason);
if (mWakeLock != null) {
mWakeLock.release();
}

View File

@@ -725,6 +725,14 @@ package android.app.contentsuggestions {
}
package android.app.job {
public class JobParameters implements android.os.Parcelable {
field public static final int INTERNAL_STOP_REASON_USER_UI_STOP = 11; // 0xb
}
}
package android.app.prediction {
public final class AppPredictor {

View File

@@ -33,6 +33,7 @@ import static org.mockito.Mockito.verify;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.job.JobParameters;
import android.app.job.JobService;
import android.graphics.drawable.Icon;
import android.os.UserHandle;
@@ -145,7 +146,7 @@ public class JobNotificationCoordinatorTest {
.enqueueNotification(eq(TEST_PACKAGE), eq(TEST_PACKAGE), eq(uid), eq(pid), any(),
eq(notificationId), eq(notification), eq(UserHandle.getUserId(uid)));
coordinator.removeNotificationAssociation(jsc);
coordinator.removeNotificationAssociation(jsc, JobParameters.STOP_REASON_UNDEFINED);
verify(mNotificationManagerInternal, never())
.cancelNotification(anyString(), anyString(), anyInt(), anyInt(), any(),
anyInt(), anyInt());
@@ -166,7 +167,7 @@ public class JobNotificationCoordinatorTest {
.enqueueNotification(eq(TEST_PACKAGE), eq(TEST_PACKAGE), eq(uid), eq(pid), any(),
eq(notificationId), eq(notification), eq(UserHandle.getUserId(uid)));
coordinator.removeNotificationAssociation(jsc);
coordinator.removeNotificationAssociation(jsc, JobParameters.STOP_REASON_UNDEFINED);
verify(mNotificationManagerInternal)
.cancelNotification(eq(TEST_PACKAGE), eq(TEST_PACKAGE), eq(uid), eq(pid), any(),
eq(notificationId), eq(UserHandle.getUserId(uid)));
@@ -288,7 +289,7 @@ public class JobNotificationCoordinatorTest {
eq(notificationId2), eq(notification2), eq(UserHandle.getUserId(uid)));
// Remove the first job. Only the first notification should be removed.
coordinator.removeNotificationAssociation(jsc1);
coordinator.removeNotificationAssociation(jsc1, JobParameters.STOP_REASON_UNDEFINED);
inOrder.verify(mNotificationManagerInternal)
.cancelNotification(eq(TEST_PACKAGE), eq(TEST_PACKAGE), eq(uid), eq(pid), any(),
eq(notificationId1), eq(UserHandle.getUserId(uid)));
@@ -296,7 +297,7 @@ public class JobNotificationCoordinatorTest {
.cancelNotification(anyString(), anyString(), anyInt(), anyInt(), any(),
eq(notificationId2), anyInt());
coordinator.removeNotificationAssociation(jsc2);
coordinator.removeNotificationAssociation(jsc2, JobParameters.STOP_REASON_UNDEFINED);
inOrder.verify(mNotificationManagerInternal)
.cancelNotification(eq(TEST_PACKAGE), eq(TEST_PACKAGE), eq(uid), eq(pid), any(),
eq(notificationId2), eq(UserHandle.getUserId(uid)));
@@ -331,12 +332,12 @@ public class JobNotificationCoordinatorTest {
eq(notificationId), eq(notification2), eq(UserHandle.getUserId(uid)));
// Remove the first job. The notification shouldn't be touched because of the 2nd job.
coordinator.removeNotificationAssociation(jsc1);
coordinator.removeNotificationAssociation(jsc1, JobParameters.STOP_REASON_UNDEFINED);
inOrder.verify(mNotificationManagerInternal, never())
.cancelNotification(anyString(), anyString(), anyInt(), anyInt(), any(),
anyInt(), anyInt());
coordinator.removeNotificationAssociation(jsc2);
coordinator.removeNotificationAssociation(jsc2, JobParameters.STOP_REASON_UNDEFINED);
inOrder.verify(mNotificationManagerInternal)
.cancelNotification(eq(TEST_PACKAGE), eq(TEST_PACKAGE), eq(uid), eq(pid), any(),
eq(notificationId), eq(UserHandle.getUserId(uid)));
@@ -372,7 +373,7 @@ public class JobNotificationCoordinatorTest {
eq(notificationId), eq(notification2), eq(UserHandle.getUserId(uid2)));
// Remove the first job. Only the first notification should be removed.
coordinator.removeNotificationAssociation(jsc1);
coordinator.removeNotificationAssociation(jsc1, JobParameters.STOP_REASON_UNDEFINED);
inOrder.verify(mNotificationManagerInternal)
.cancelNotification(eq(TEST_PACKAGE), eq(TEST_PACKAGE), eq(uid1), eq(pid), any(),
eq(notificationId), eq(UserHandle.getUserId(uid1)));
@@ -380,7 +381,7 @@ public class JobNotificationCoordinatorTest {
.cancelNotification(anyString(), anyString(), eq(uid2), anyInt(), any(),
anyInt(), anyInt());
coordinator.removeNotificationAssociation(jsc2);
coordinator.removeNotificationAssociation(jsc2, JobParameters.STOP_REASON_UNDEFINED);
inOrder.verify(mNotificationManagerInternal)
.cancelNotification(eq(TEST_PACKAGE), eq(TEST_PACKAGE), eq(uid2), eq(pid), any(),
eq(notificationId), eq(UserHandle.getUserId(uid2)));
@@ -417,7 +418,7 @@ public class JobNotificationCoordinatorTest {
eq(notificationId), eq(notification2), eq(UserHandle.getUserId(uid)));
// Remove the first job. Only the first notification should be removed.
coordinator.removeNotificationAssociation(jsc1);
coordinator.removeNotificationAssociation(jsc1, JobParameters.STOP_REASON_UNDEFINED);
inOrder.verify(mNotificationManagerInternal)
.cancelNotification(eq(pkg1), eq(pkg1), eq(uid), eq(pid), any(),
eq(notificationId), eq(UserHandle.getUserId(uid)));
@@ -425,12 +426,73 @@ public class JobNotificationCoordinatorTest {
.cancelNotification(anyString(), anyString(), eq(uid), anyInt(), any(),
anyInt(), anyInt());
coordinator.removeNotificationAssociation(jsc2);
coordinator.removeNotificationAssociation(jsc2, JobParameters.STOP_REASON_UNDEFINED);
inOrder.verify(mNotificationManagerInternal)
.cancelNotification(eq(pkg2), eq(pkg2), eq(uid), eq(pid), any(),
eq(notificationId), eq(UserHandle.getUserId(uid)));
}
@Test
public void testUserStop_SingleJob_DetachOnStop() {
final JobNotificationCoordinator coordinator = new JobNotificationCoordinator();
final JobServiceContext jsc = mock(JobServiceContext.class);
final Notification notification = createValidNotification();
final int uid = 10123;
final int pid = 42;
final int notificationId = 23;
coordinator.enqueueNotification(jsc, TEST_PACKAGE, pid, uid, notificationId, notification,
JobService.JOB_END_NOTIFICATION_POLICY_DETACH);
verify(mNotificationManagerInternal)
.enqueueNotification(eq(TEST_PACKAGE), eq(TEST_PACKAGE), eq(uid), eq(pid), any(),
eq(notificationId), eq(notification), eq(UserHandle.getUserId(uid)));
coordinator.removeNotificationAssociation(jsc, JobParameters.STOP_REASON_USER);
verify(mNotificationManagerInternal)
.cancelNotification(eq(TEST_PACKAGE), eq(TEST_PACKAGE), eq(uid), eq(pid), any(),
eq(notificationId), eq(UserHandle.getUserId(uid)));
}
@Test
public void testUserStop_MultipleJobs_sameApp_EnqueueSameNotificationId_DetachOnStop() {
final JobNotificationCoordinator coordinator = new JobNotificationCoordinator();
final JobServiceContext jsc1 = mock(JobServiceContext.class);
final JobServiceContext jsc2 = mock(JobServiceContext.class);
final Notification notification1 = createValidNotification();
final Notification notification2 = createValidNotification();
final int uid = 10123;
final int pid = 42;
final int notificationId = 23;
InOrder inOrder = inOrder(mNotificationManagerInternal);
coordinator.enqueueNotification(jsc1, TEST_PACKAGE, pid, uid, notificationId, notification1,
JobService.JOB_END_NOTIFICATION_POLICY_DETACH);
inOrder.verify(mNotificationManagerInternal)
.enqueueNotification(eq(TEST_PACKAGE), eq(TEST_PACKAGE), eq(uid), eq(pid), any(),
eq(notificationId), eq(notification1), eq(UserHandle.getUserId(uid)));
coordinator.enqueueNotification(jsc2, TEST_PACKAGE, pid, uid, notificationId, notification2,
JobService.JOB_END_NOTIFICATION_POLICY_DETACH);
inOrder.verify(mNotificationManagerInternal, never())
.cancelNotification(anyString(), anyString(), anyInt(), anyInt(), any(),
anyInt(), anyInt());
inOrder.verify(mNotificationManagerInternal)
.enqueueNotification(eq(TEST_PACKAGE), eq(TEST_PACKAGE), eq(uid), eq(pid), any(),
eq(notificationId), eq(notification2), eq(UserHandle.getUserId(uid)));
// Remove the first job. The notification shouldn't be touched because of the 2nd job.
coordinator.removeNotificationAssociation(jsc1, JobParameters.STOP_REASON_USER);
inOrder.verify(mNotificationManagerInternal, never())
.cancelNotification(anyString(), anyString(), anyInt(), anyInt(), any(),
anyInt(), anyInt());
coordinator.removeNotificationAssociation(jsc2, JobParameters.STOP_REASON_USER);
inOrder.verify(mNotificationManagerInternal)
.cancelNotification(eq(TEST_PACKAGE), eq(TEST_PACKAGE), eq(uid), eq(pid), any(),
eq(notificationId), eq(UserHandle.getUserId(uid)));
}
private Notification createValidNotification() {
final Notification notification = mock(Notification.class);
doReturn(mock(Icon.class)).when(notification).getSmallIcon();