Merge "Introduce centralized notification coordinator."
This commit is contained in:
committed by
Android (Google) Code Review
commit
3ed5cc4ce5
@@ -416,6 +416,11 @@ public abstract class JobService extends Service {
|
||||
* JobScheduler will not remember this notification after the job has finished running,
|
||||
* so apps must call this every time the job is started (if required or desired).
|
||||
*
|
||||
* <p>
|
||||
* If separate jobs use the same notification ID with this API, the most recently provided
|
||||
* notification will be shown to the user, and the
|
||||
* {@code jobEndNotificationPolicy} of the last job to stop will be applied.
|
||||
*
|
||||
* @param params The parameters identifying this job, as supplied to
|
||||
* the job in the {@link #onStartJob(JobParameters)} callback.
|
||||
* @param notificationId The ID for this notification, as per
|
||||
|
||||
@@ -193,6 +193,7 @@ class JobConcurrencyManager {
|
||||
}
|
||||
|
||||
private final Object mLock;
|
||||
private final JobNotificationCoordinator mNotificationCoordinator;
|
||||
private final JobSchedulerService mService;
|
||||
private final Context mContext;
|
||||
private final Handler mHandler;
|
||||
@@ -418,6 +419,7 @@ class JobConcurrencyManager {
|
||||
mLock = mService.getLock();
|
||||
mContext = service.getTestableContext();
|
||||
mInjector = injector;
|
||||
mNotificationCoordinator = new JobNotificationCoordinator();
|
||||
|
||||
mHandler = JobSchedulerBackgroundThread.getHandler();
|
||||
|
||||
@@ -451,7 +453,8 @@ class JobConcurrencyManager {
|
||||
ServiceManager.getService(BatteryStats.SERVICE_NAME));
|
||||
for (int i = 0; i < STANDARD_CONCURRENCY_LIMIT; i++) {
|
||||
mIdleContexts.add(
|
||||
mInjector.createJobServiceContext(mService, this, batteryStats,
|
||||
mInjector.createJobServiceContext(mService, this,
|
||||
mNotificationCoordinator, batteryStats,
|
||||
mService.mJobPackageTracker, mContext.getMainLooper()));
|
||||
}
|
||||
}
|
||||
@@ -1687,7 +1690,7 @@ class JobConcurrencyManager {
|
||||
|
||||
@NonNull
|
||||
private JobServiceContext createNewJobServiceContext() {
|
||||
return mInjector.createJobServiceContext(mService, this,
|
||||
return mInjector.createJobServiceContext(mService, this, mNotificationCoordinator,
|
||||
IBatteryStats.Stub.asInterface(
|
||||
ServiceManager.getService(BatteryStats.SERVICE_NAME)),
|
||||
mService.mJobPackageTracker, mContext.getMainLooper());
|
||||
@@ -2612,10 +2615,11 @@ class JobConcurrencyManager {
|
||||
static class Injector {
|
||||
@NonNull
|
||||
JobServiceContext createJobServiceContext(JobSchedulerService service,
|
||||
JobConcurrencyManager concurrencyManager, IBatteryStats batteryStats,
|
||||
JobConcurrencyManager concurrencyManager,
|
||||
JobNotificationCoordinator notificationCoordinator, IBatteryStats batteryStats,
|
||||
JobPackageTracker tracker, Looper looper) {
|
||||
return new JobServiceContext(service, concurrencyManager, batteryStats,
|
||||
tracker, looper);
|
||||
return new JobServiceContext(service, concurrencyManager, notificationCoordinator,
|
||||
batteryStats, tracker, looper);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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 com.android.server.job;
|
||||
|
||||
import static android.app.job.JobService.JOB_END_NOTIFICATION_POLICY_DETACH;
|
||||
import static android.app.job.JobService.JOB_END_NOTIFICATION_POLICY_REMOVE;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.app.Notification;
|
||||
import android.app.job.JobService;
|
||||
import android.content.pm.UserPackage;
|
||||
import android.os.UserHandle;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.ArraySet;
|
||||
import android.util.Slog;
|
||||
import android.util.SparseSetArray;
|
||||
|
||||
import com.android.server.LocalServices;
|
||||
import com.android.server.notification.NotificationManagerInternal;
|
||||
|
||||
class JobNotificationCoordinator {
|
||||
private static final String TAG = "JobNotificationCoordinator";
|
||||
|
||||
/**
|
||||
* Mapping of UserPackage -> {notificationId -> List<JobServiceContext>} to track which jobs
|
||||
* are associated with each app's notifications.
|
||||
*/
|
||||
private final ArrayMap<UserPackage, SparseSetArray<JobServiceContext>> mCurrentAssociations =
|
||||
new ArrayMap<>();
|
||||
/**
|
||||
* Set of NotificationDetails for each running job.
|
||||
*/
|
||||
private final ArrayMap<JobServiceContext, NotificationDetails> mNotificationDetails =
|
||||
new ArrayMap<>();
|
||||
|
||||
private static final class NotificationDetails {
|
||||
@NonNull
|
||||
public final UserPackage userPackage;
|
||||
public final int notificationId;
|
||||
public final int appPid;
|
||||
public final int appUid;
|
||||
@JobService.JobEndNotificationPolicy
|
||||
public final int jobEndNotificationPolicy;
|
||||
|
||||
NotificationDetails(@NonNull UserPackage userPackage, int appPid, int appUid,
|
||||
int notificationId,
|
||||
@JobService.JobEndNotificationPolicy int jobEndNotificationPolicy) {
|
||||
this.userPackage = userPackage;
|
||||
this.notificationId = notificationId;
|
||||
this.appPid = appPid;
|
||||
this.appUid = appUid;
|
||||
this.jobEndNotificationPolicy = jobEndNotificationPolicy;
|
||||
}
|
||||
}
|
||||
|
||||
private final NotificationManagerInternal mNotificationManagerInternal;
|
||||
|
||||
JobNotificationCoordinator() {
|
||||
mNotificationManagerInternal = LocalServices.getService(NotificationManagerInternal.class);
|
||||
}
|
||||
|
||||
void enqueueNotification(@NonNull JobServiceContext hostingContext, @NonNull String packageName,
|
||||
int callingPid, int callingUid, int notificationId, @NonNull Notification notification,
|
||||
@JobService.JobEndNotificationPolicy int jobEndNotificationPolicy) {
|
||||
validateNotification(packageName, callingUid, notification, jobEndNotificationPolicy);
|
||||
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);
|
||||
}
|
||||
final int userId = UserHandle.getUserId(callingUid);
|
||||
// TODO(260848384): ensure apps can't cancel the notification for user-initiated job
|
||||
// eg., by calling NotificationManager.cancel/All or deleting the notification channel
|
||||
mNotificationManagerInternal.enqueueNotification(
|
||||
packageName, packageName, callingUid, callingPid, /* tag */ null,
|
||||
notificationId, notification, userId);
|
||||
final UserPackage userPackage = UserPackage.of(userId, packageName);
|
||||
final NotificationDetails details = new NotificationDetails(
|
||||
userPackage, callingPid, callingUid, notificationId, jobEndNotificationPolicy);
|
||||
SparseSetArray<JobServiceContext> appNotifications = mCurrentAssociations.get(userPackage);
|
||||
if (appNotifications == null) {
|
||||
appNotifications = new SparseSetArray<>();
|
||||
mCurrentAssociations.put(userPackage, appNotifications);
|
||||
}
|
||||
appNotifications.add(notificationId, hostingContext);
|
||||
mNotificationDetails.put(hostingContext, details);
|
||||
}
|
||||
|
||||
void removeNotificationAssociation(@NonNull JobServiceContext hostingContext) {
|
||||
final NotificationDetails details = mNotificationDetails.remove(hostingContext);
|
||||
if (details == null) {
|
||||
return;
|
||||
}
|
||||
final SparseSetArray<JobServiceContext> associations =
|
||||
mCurrentAssociations.get(details.userPackage);
|
||||
if (associations == null || !associations.remove(details.notificationId, hostingContext)) {
|
||||
Slog.wtf(TAG, "Association data structures not in sync");
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
final String packageName = details.userPackage.packageName;
|
||||
mNotificationManagerInternal.cancelNotification(
|
||||
packageName, packageName, details.appUid, details.appPid, /* tag */ null,
|
||||
details.notificationId, UserHandle.getUserId(details.appUid));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateNotification(@NonNull String packageName, int callingUid,
|
||||
@NonNull Notification notification,
|
||||
@JobService.JobEndNotificationPolicy int jobEndNotificationPolicy) {
|
||||
if (notification == null) {
|
||||
throw new NullPointerException("notification");
|
||||
}
|
||||
if (notification.getSmallIcon() == null) {
|
||||
throw new IllegalArgumentException("small icon required");
|
||||
}
|
||||
if (null == mNotificationManagerInternal.getNotificationChannel(
|
||||
packageName, callingUid, notification.getChannelId())) {
|
||||
throw new IllegalArgumentException("invalid notification channel");
|
||||
}
|
||||
if (jobEndNotificationPolicy != JOB_END_NOTIFICATION_POLICY_DETACH
|
||||
&& jobEndNotificationPolicy != JOB_END_NOTIFICATION_POLICY_REMOVE) {
|
||||
throw new IllegalArgumentException("invalid job end notification policy");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,6 @@
|
||||
package com.android.server.job;
|
||||
|
||||
import static android.app.job.JobInfo.getPriorityString;
|
||||
import static android.app.job.JobService.JOB_END_NOTIFICATION_POLICY_DETACH;
|
||||
import static android.app.job.JobService.JOB_END_NOTIFICATION_POLICY_REMOVE;
|
||||
|
||||
import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_NONE;
|
||||
import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock;
|
||||
@@ -61,7 +59,6 @@ import com.android.internal.util.FrameworkStatsLog;
|
||||
import com.android.server.EventLogTags;
|
||||
import com.android.server.LocalServices;
|
||||
import com.android.server.job.controllers.JobStatus;
|
||||
import com.android.server.notification.NotificationManagerInternal;
|
||||
import com.android.server.tare.EconomicPolicy;
|
||||
import com.android.server.tare.EconomyManagerInternal;
|
||||
import com.android.server.tare.JobSchedulerEconomicPolicy;
|
||||
@@ -113,6 +110,7 @@ public final class JobServiceContext implements ServiceConnection {
|
||||
/** Make callbacks to {@link JobSchedulerService} to inform on job completion status. */
|
||||
private final JobCompletedListener mCompletedListener;
|
||||
private final JobConcurrencyManager mJobConcurrencyManager;
|
||||
private final JobNotificationCoordinator mNotificationCoordinator;
|
||||
private final JobSchedulerService mService;
|
||||
/** Used for service binding, etc. */
|
||||
private final Context mContext;
|
||||
@@ -121,7 +119,6 @@ public final class JobServiceContext implements ServiceConnection {
|
||||
private final EconomyManagerInternal mEconomyManagerInternal;
|
||||
private final JobPackageTracker mJobPackageTracker;
|
||||
private final PowerManager mPowerManager;
|
||||
private final NotificationManagerInternal mNotificationManagerInternal;
|
||||
private PowerManager.WakeLock mWakeLock;
|
||||
|
||||
// Execution state.
|
||||
@@ -174,11 +171,6 @@ public final class JobServiceContext implements ServiceConnection {
|
||||
/** The absolute maximum amount of time the job can run */
|
||||
private long mMaxExecutionTimeMillis;
|
||||
|
||||
private int mNotificationId;
|
||||
private Notification mNotification;
|
||||
private int mNotificationPid;
|
||||
private int mNotificationJobStopPolicy;
|
||||
|
||||
/**
|
||||
* The stop reason for a pending cancel. If there's not pending cancel, then the value should be
|
||||
* {@link JobParameters#STOP_REASON_UNDEFINED}.
|
||||
@@ -254,16 +246,17 @@ public final class JobServiceContext implements ServiceConnection {
|
||||
}
|
||||
|
||||
JobServiceContext(JobSchedulerService service, JobConcurrencyManager concurrencyManager,
|
||||
JobNotificationCoordinator notificationCoordinator,
|
||||
IBatteryStats batteryStats, JobPackageTracker tracker, Looper looper) {
|
||||
mContext = service.getContext();
|
||||
mLock = service.getLock();
|
||||
mService = service;
|
||||
mBatteryStats = batteryStats;
|
||||
mEconomyManagerInternal = LocalServices.getService(EconomyManagerInternal.class);
|
||||
mNotificationManagerInternal = LocalServices.getService(NotificationManagerInternal.class);
|
||||
mJobPackageTracker = tracker;
|
||||
mCallbackHandler = new JobServiceHandler(looper);
|
||||
mJobConcurrencyManager = concurrencyManager;
|
||||
mNotificationCoordinator = notificationCoordinator;
|
||||
mCompletedListener = service;
|
||||
mPowerManager = mContext.getSystemService(PowerManager.class);
|
||||
mAvailable = true;
|
||||
@@ -624,29 +617,10 @@ public final class JobServiceContext implements ServiceConnection {
|
||||
Slog.wtfStack(TAG, "Calling UID isn't the same as running job's UID...");
|
||||
throw new SecurityException("Can't post notification on behalf of another app");
|
||||
}
|
||||
if (notification == null) {
|
||||
throw new NullPointerException("notification");
|
||||
}
|
||||
if (notification.getSmallIcon() == null) {
|
||||
throw new IllegalArgumentException("small icon required");
|
||||
}
|
||||
final String callingPkgName = mRunningJob.getServiceComponent().getPackageName();
|
||||
if (null == mNotificationManagerInternal.getNotificationChannel(
|
||||
callingPkgName, callingUid, notification.getChannelId())) {
|
||||
throw new IllegalArgumentException("invalid notification channel");
|
||||
}
|
||||
if (jobEndNotificationPolicy != JOB_END_NOTIFICATION_POLICY_DETACH
|
||||
&& jobEndNotificationPolicy != JOB_END_NOTIFICATION_POLICY_REMOVE) {
|
||||
throw new IllegalArgumentException("invalid job end notification policy");
|
||||
}
|
||||
// TODO(260848384): ensure apps can't cancel the notification for user-initiated job
|
||||
mNotificationManagerInternal.enqueueNotification(
|
||||
callingPkgName, callingPkgName, callingUid, callingPid, /* tag */ null,
|
||||
notificationId, notification, UserHandle.getUserId(callingUid));
|
||||
mNotificationId = notificationId;
|
||||
mNotification = notification;
|
||||
mNotificationPid = callingPid;
|
||||
mNotificationJobStopPolicy = jobEndNotificationPolicy;
|
||||
mNotificationCoordinator.enqueueNotification(this, callingPkgName,
|
||||
callingPid, callingUid, notificationId,
|
||||
notification, jobEndNotificationPolicy);
|
||||
}
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(ident);
|
||||
@@ -1179,13 +1153,7 @@ public final class JobServiceContext implements ServiceConnection {
|
||||
JobSchedulerEconomicPolicy.ACTION_JOB_TIMEOUT,
|
||||
String.valueOf(mRunningJob.getJobId()));
|
||||
}
|
||||
if (mNotification != null
|
||||
&& mNotificationJobStopPolicy == JOB_END_NOTIFICATION_POLICY_REMOVE) {
|
||||
final String callingPkgName = completedJob.getServiceComponent().getPackageName();
|
||||
mNotificationManagerInternal.cancelNotification(
|
||||
callingPkgName, callingPkgName, completedJob.getUid(), mNotificationPid,
|
||||
/* tag */ null, mNotificationId, UserHandle.getUserId(completedJob.getUid()));
|
||||
}
|
||||
mNotificationCoordinator.removeNotificationAssociation(this);
|
||||
if (mWakeLock != null) {
|
||||
mWakeLock.release();
|
||||
}
|
||||
@@ -1203,7 +1171,6 @@ public final class JobServiceContext implements ServiceConnection {
|
||||
mPendingStopReason = JobParameters.STOP_REASON_UNDEFINED;
|
||||
mPendingInternalStopReason = 0;
|
||||
mPendingDebugStopReason = null;
|
||||
mNotification = null;
|
||||
removeOpTimeOutLocked();
|
||||
if (completedJob.isUserVisibleJob()) {
|
||||
mService.informObserversOfUserVisibleJobChange(this, completedJob, false);
|
||||
|
||||
@@ -113,7 +113,8 @@ public final class JobConcurrencyManagerTest {
|
||||
|
||||
@Override
|
||||
JobServiceContext createJobServiceContext(JobSchedulerService service,
|
||||
JobConcurrencyManager concurrencyManager, IBatteryStats batteryStats,
|
||||
JobConcurrencyManager concurrencyManager,
|
||||
JobNotificationCoordinator notificationCoordinator, IBatteryStats batteryStats,
|
||||
JobPackageTracker tracker, Looper looper) {
|
||||
final JobServiceContext context = mock(JobServiceContext.class);
|
||||
doAnswer((Answer<Boolean>) invocationOnMock -> {
|
||||
|
||||
@@ -0,0 +1,440 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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 com.android.server.job;
|
||||
|
||||
|
||||
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
|
||||
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
|
||||
import static com.android.dx.mockito.inline.extended.ExtendedMockito.inOrder;
|
||||
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
|
||||
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
|
||||
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.eq;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.job.JobService;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.UserHandle;
|
||||
|
||||
import com.android.server.LocalServices;
|
||||
import com.android.server.notification.NotificationManagerInternal;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.InOrder;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoSession;
|
||||
import org.mockito.quality.Strictness;
|
||||
|
||||
public class JobNotificationCoordinatorTest {
|
||||
private static final String TEST_PACKAGE = "com.android.test";
|
||||
private static final String NOTIFICATION_CHANNEL_ID = "validNotificationChannelId";
|
||||
|
||||
private MockitoSession mMockingSession;
|
||||
|
||||
@Mock
|
||||
private NotificationManagerInternal mNotificationManagerInternal;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mMockingSession = mockitoSession()
|
||||
.initMocks(this)
|
||||
.mockStatic(LocalServices.class)
|
||||
.strictness(Strictness.LENIENT)
|
||||
.startMocking();
|
||||
doReturn(mNotificationManagerInternal)
|
||||
.when(() -> LocalServices.getService(NotificationManagerInternal.class));
|
||||
doNothing().when(mNotificationManagerInternal)
|
||||
.enqueueNotification(anyString(), anyString(), anyInt(), anyInt(), any(),
|
||||
anyInt(), any(), anyInt());
|
||||
doNothing().when(mNotificationManagerInternal)
|
||||
.enqueueNotification(anyString(), anyString(), anyInt(), anyInt(), any(),
|
||||
anyInt(), any(), anyInt());
|
||||
doReturn(mock(NotificationChannel.class)).when(mNotificationManagerInternal)
|
||||
.getNotificationChannel(anyString(), anyInt(), eq(NOTIFICATION_CHANNEL_ID));
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
if (mMockingSession != null) {
|
||||
mMockingSession.finishMocking();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParameterValidation() {
|
||||
final JobNotificationCoordinator coordinator = new JobNotificationCoordinator();
|
||||
final JobServiceContext jsc = mock(JobServiceContext.class);
|
||||
final int uid = 10123;
|
||||
final int pid = 42;
|
||||
final int notificationId = 23;
|
||||
|
||||
try {
|
||||
coordinator.enqueueNotification(jsc, TEST_PACKAGE, pid, uid, notificationId, null,
|
||||
JobService.JOB_END_NOTIFICATION_POLICY_DETACH);
|
||||
fail("Successfully enqueued a null notification");
|
||||
} catch (NullPointerException e) {
|
||||
// Success
|
||||
}
|
||||
|
||||
Notification notification = createValidNotification();
|
||||
doReturn(null).when(notification).getSmallIcon();
|
||||
try {
|
||||
coordinator.enqueueNotification(jsc, TEST_PACKAGE, pid, uid, notificationId,
|
||||
notification, JobService.JOB_END_NOTIFICATION_POLICY_DETACH);
|
||||
fail("Successfully enqueued a notification with no small icon");
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Success
|
||||
}
|
||||
|
||||
notification = createValidNotification();
|
||||
doReturn(null).when(notification).getChannelId();
|
||||
try {
|
||||
coordinator.enqueueNotification(jsc, TEST_PACKAGE, pid, uid, notificationId,
|
||||
notification, JobService.JOB_END_NOTIFICATION_POLICY_DETACH);
|
||||
fail("Successfully enqueued a notification with no valid channel");
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Success
|
||||
}
|
||||
|
||||
notification = createValidNotification();
|
||||
try {
|
||||
coordinator.enqueueNotification(jsc, TEST_PACKAGE, pid, uid, notificationId,
|
||||
notification, Integer.MAX_VALUE);
|
||||
fail("Successfully enqueued a notification with an invalid job end notification "
|
||||
+ "policy");
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Success
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSingleJob_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);
|
||||
verify(mNotificationManagerInternal, never())
|
||||
.cancelNotification(anyString(), anyString(), anyInt(), anyInt(), any(),
|
||||
anyInt(), anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSingleJob_RemoveOnStop() {
|
||||
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_REMOVE);
|
||||
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);
|
||||
verify(mNotificationManagerInternal)
|
||||
.cancelNotification(eq(TEST_PACKAGE), eq(TEST_PACKAGE), eq(uid), eq(pid), any(),
|
||||
eq(notificationId), eq(UserHandle.getUserId(uid)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSingleJob_EnqueueDifferentNotificationId_DetachOnStop() {
|
||||
final JobNotificationCoordinator coordinator = new JobNotificationCoordinator();
|
||||
final JobServiceContext jsc = mock(JobServiceContext.class);
|
||||
final Notification notification1 = createValidNotification();
|
||||
final Notification notification2 = createValidNotification();
|
||||
final int uid = 10123;
|
||||
final int pid = 42;
|
||||
final int notificationId1 = 23;
|
||||
final int notificationId2 = 46;
|
||||
|
||||
InOrder inOrder = inOrder(mNotificationManagerInternal);
|
||||
|
||||
coordinator.enqueueNotification(jsc, TEST_PACKAGE, pid, uid, notificationId1, notification1,
|
||||
JobService.JOB_END_NOTIFICATION_POLICY_DETACH);
|
||||
inOrder.verify(mNotificationManagerInternal)
|
||||
.enqueueNotification(eq(TEST_PACKAGE), eq(TEST_PACKAGE), eq(uid), eq(pid), any(),
|
||||
eq(notificationId1), eq(notification1), eq(UserHandle.getUserId(uid)));
|
||||
|
||||
coordinator.enqueueNotification(jsc, TEST_PACKAGE, pid, uid, notificationId2, 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(notificationId2), eq(notification2), eq(UserHandle.getUserId(uid)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSingleJob_EnqueueDifferentNotificationId_RemoveOnStop() {
|
||||
final JobNotificationCoordinator coordinator = new JobNotificationCoordinator();
|
||||
final JobServiceContext jsc = mock(JobServiceContext.class);
|
||||
final Notification notification1 = createValidNotification();
|
||||
final Notification notification2 = createValidNotification();
|
||||
final int uid = 10123;
|
||||
final int pid = 42;
|
||||
final int notificationId1 = 23;
|
||||
final int notificationId2 = 46;
|
||||
|
||||
InOrder inOrder = inOrder(mNotificationManagerInternal);
|
||||
|
||||
coordinator.enqueueNotification(jsc, TEST_PACKAGE, pid, uid, notificationId1, notification1,
|
||||
JobService.JOB_END_NOTIFICATION_POLICY_REMOVE);
|
||||
inOrder.verify(mNotificationManagerInternal)
|
||||
.enqueueNotification(eq(TEST_PACKAGE), eq(TEST_PACKAGE), eq(uid), eq(pid), any(),
|
||||
eq(notificationId1), eq(notification1), eq(UserHandle.getUserId(uid)));
|
||||
|
||||
coordinator.enqueueNotification(jsc, TEST_PACKAGE, pid, uid, notificationId2, notification2,
|
||||
JobService.JOB_END_NOTIFICATION_POLICY_REMOVE);
|
||||
inOrder.verify(mNotificationManagerInternal)
|
||||
.cancelNotification(eq(TEST_PACKAGE), eq(TEST_PACKAGE), eq(uid), eq(pid), any(),
|
||||
eq(notificationId1), eq(UserHandle.getUserId(uid)));
|
||||
inOrder.verify(mNotificationManagerInternal)
|
||||
.enqueueNotification(eq(TEST_PACKAGE), eq(TEST_PACKAGE), eq(uid), eq(pid), any(),
|
||||
eq(notificationId2), eq(notification2), eq(UserHandle.getUserId(uid)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSingleJob_EnqueueSameNotificationId() {
|
||||
final JobNotificationCoordinator coordinator = new JobNotificationCoordinator();
|
||||
final JobServiceContext jsc = 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(jsc, 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(jsc, 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)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleJobs_sameApp_EnqueueDifferentNotificationId() {
|
||||
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 notificationId1 = 23;
|
||||
final int notificationId2 = 46;
|
||||
|
||||
InOrder inOrder = inOrder(mNotificationManagerInternal);
|
||||
|
||||
coordinator.enqueueNotification(jsc1, TEST_PACKAGE, pid, uid, notificationId1,
|
||||
notification1, JobService.JOB_END_NOTIFICATION_POLICY_REMOVE);
|
||||
inOrder.verify(mNotificationManagerInternal)
|
||||
.enqueueNotification(eq(TEST_PACKAGE), eq(TEST_PACKAGE), eq(uid), eq(pid), any(),
|
||||
eq(notificationId1), eq(notification1), eq(UserHandle.getUserId(uid)));
|
||||
|
||||
coordinator.enqueueNotification(jsc2, TEST_PACKAGE, pid, uid, notificationId2,
|
||||
notification2, JobService.JOB_END_NOTIFICATION_POLICY_REMOVE);
|
||||
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(notificationId2), eq(notification2), eq(UserHandle.getUserId(uid)));
|
||||
|
||||
// Remove the first job. Only the first notification should be removed.
|
||||
coordinator.removeNotificationAssociation(jsc1);
|
||||
inOrder.verify(mNotificationManagerInternal)
|
||||
.cancelNotification(eq(TEST_PACKAGE), eq(TEST_PACKAGE), eq(uid), eq(pid), any(),
|
||||
eq(notificationId1), eq(UserHandle.getUserId(uid)));
|
||||
inOrder.verify(mNotificationManagerInternal, never())
|
||||
.cancelNotification(anyString(), anyString(), anyInt(), anyInt(), any(),
|
||||
eq(notificationId2), anyInt());
|
||||
|
||||
coordinator.removeNotificationAssociation(jsc2);
|
||||
inOrder.verify(mNotificationManagerInternal)
|
||||
.cancelNotification(eq(TEST_PACKAGE), eq(TEST_PACKAGE), eq(uid), eq(pid), any(),
|
||||
eq(notificationId2), eq(UserHandle.getUserId(uid)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleJobs_sameApp_EnqueueSameNotificationId() {
|
||||
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_REMOVE);
|
||||
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);
|
||||
inOrder.verify(mNotificationManagerInternal, never())
|
||||
.cancelNotification(anyString(), anyString(), anyInt(), anyInt(), any(),
|
||||
anyInt(), anyInt());
|
||||
|
||||
coordinator.removeNotificationAssociation(jsc2);
|
||||
inOrder.verify(mNotificationManagerInternal)
|
||||
.cancelNotification(eq(TEST_PACKAGE), eq(TEST_PACKAGE), eq(uid), eq(pid), any(),
|
||||
eq(notificationId), eq(UserHandle.getUserId(uid)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleJobs_sameApp_DifferentUsers() {
|
||||
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 uid1 = 10123;
|
||||
final int uid2 = 1010123;
|
||||
final int pid = 42;
|
||||
final int notificationId = 23;
|
||||
|
||||
InOrder inOrder = inOrder(mNotificationManagerInternal);
|
||||
|
||||
coordinator.enqueueNotification(jsc1, TEST_PACKAGE, pid, uid1, notificationId,
|
||||
notification1, JobService.JOB_END_NOTIFICATION_POLICY_REMOVE);
|
||||
inOrder.verify(mNotificationManagerInternal)
|
||||
.enqueueNotification(eq(TEST_PACKAGE), eq(TEST_PACKAGE), eq(uid1), eq(pid), any(),
|
||||
eq(notificationId), eq(notification1), eq(UserHandle.getUserId(uid1)));
|
||||
|
||||
coordinator.enqueueNotification(jsc2, TEST_PACKAGE, pid, uid2, notificationId,
|
||||
notification2, JobService.JOB_END_NOTIFICATION_POLICY_REMOVE);
|
||||
inOrder.verify(mNotificationManagerInternal, never())
|
||||
.cancelNotification(anyString(), anyString(), anyInt(), anyInt(), any(),
|
||||
anyInt(), anyInt());
|
||||
inOrder.verify(mNotificationManagerInternal)
|
||||
.enqueueNotification(eq(TEST_PACKAGE), eq(TEST_PACKAGE), eq(uid2), eq(pid), any(),
|
||||
eq(notificationId), eq(notification2), eq(UserHandle.getUserId(uid2)));
|
||||
|
||||
// Remove the first job. Only the first notification should be removed.
|
||||
coordinator.removeNotificationAssociation(jsc1);
|
||||
inOrder.verify(mNotificationManagerInternal)
|
||||
.cancelNotification(eq(TEST_PACKAGE), eq(TEST_PACKAGE), eq(uid1), eq(pid), any(),
|
||||
eq(notificationId), eq(UserHandle.getUserId(uid1)));
|
||||
inOrder.verify(mNotificationManagerInternal, never())
|
||||
.cancelNotification(anyString(), anyString(), eq(uid2), anyInt(), any(),
|
||||
anyInt(), anyInt());
|
||||
|
||||
coordinator.removeNotificationAssociation(jsc2);
|
||||
inOrder.verify(mNotificationManagerInternal)
|
||||
.cancelNotification(eq(TEST_PACKAGE), eq(TEST_PACKAGE), eq(uid2), eq(pid), any(),
|
||||
eq(notificationId), eq(UserHandle.getUserId(uid2)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleJobs_differentApps() {
|
||||
final JobNotificationCoordinator coordinator = new JobNotificationCoordinator();
|
||||
final String pkg1 = "pkg1";
|
||||
final String pkg2 = "pkg2";
|
||||
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, pkg1, pid, uid, notificationId, notification1,
|
||||
JobService.JOB_END_NOTIFICATION_POLICY_REMOVE);
|
||||
inOrder.verify(mNotificationManagerInternal)
|
||||
.enqueueNotification(eq(pkg1), eq(pkg1), eq(uid), eq(pid), any(),
|
||||
eq(notificationId), eq(notification1), eq(UserHandle.getUserId(uid)));
|
||||
|
||||
coordinator.enqueueNotification(jsc2, pkg2, pid, uid, notificationId, notification2,
|
||||
JobService.JOB_END_NOTIFICATION_POLICY_REMOVE);
|
||||
inOrder.verify(mNotificationManagerInternal, never())
|
||||
.cancelNotification(anyString(), anyString(), anyInt(), anyInt(), any(),
|
||||
anyInt(), anyInt());
|
||||
inOrder.verify(mNotificationManagerInternal)
|
||||
.enqueueNotification(eq(pkg2), eq(pkg2), eq(uid), eq(pid), any(),
|
||||
eq(notificationId), eq(notification2), eq(UserHandle.getUserId(uid)));
|
||||
|
||||
// Remove the first job. Only the first notification should be removed.
|
||||
coordinator.removeNotificationAssociation(jsc1);
|
||||
inOrder.verify(mNotificationManagerInternal)
|
||||
.cancelNotification(eq(pkg1), eq(pkg1), eq(uid), eq(pid), any(),
|
||||
eq(notificationId), eq(UserHandle.getUserId(uid)));
|
||||
inOrder.verify(mNotificationManagerInternal, never())
|
||||
.cancelNotification(anyString(), anyString(), eq(uid), anyInt(), any(),
|
||||
anyInt(), anyInt());
|
||||
|
||||
coordinator.removeNotificationAssociation(jsc2);
|
||||
inOrder.verify(mNotificationManagerInternal)
|
||||
.cancelNotification(eq(pkg2), eq(pkg2), 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();
|
||||
doReturn(NOTIFICATION_CHANNEL_ID).when(notification).getChannelId();
|
||||
return notification;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user