mProviders.put(AccessibilityManagerWrapper.class,
() -> new AccessibilityManagerWrapper(mContext));
+ mProviders.put(ForegroundServiceController.class,
+ () -> new ForegroundServiceControllerImpl(mContext));
+
mProviders.put(UiOffloadThread.class, UiOffloadThread::new);
// Put all dependencies above here so the factory can override them if it wants.
--- /dev/null
+/*
+ * Copyright (C) 2017 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.systemui;
+
+import android.service.notification.StatusBarNotification;
+
+public interface ForegroundServiceController {
+ /**
+ * @param sbn notification that was just posted
+ * @param importance
+ */
+ void addNotification(StatusBarNotification sbn, int importance);
+
+ /**
+ * @param sbn notification that was just changed in some way
+ * @param newImportance
+ */
+ void updateNotification(StatusBarNotification sbn, int newImportance);
+
+ /**
+ * @param sbn notification that was just canceled
+ */
+ boolean removeNotification(StatusBarNotification sbn);
+
+ /**
+ * @param userId
+ * @return true if this user has services missing notifications and therefore needs a
+ * disclosure notification.
+ */
+ boolean isDungeonNeededForUser(int userId);
+
+ /**
+ * @param sbn
+ * @return true if sbn is the system-provided "dungeon" (list of running foreground services).
+ */
+ boolean isDungeonNotification(StatusBarNotification sbn);
+}
--- /dev/null
+/*
+ * Copyright (C) 2017 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.systemui;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.os.Bundle;
+import android.service.notification.StatusBarNotification;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.messages.nano.SystemMessageProto;
+
+import java.util.Arrays;
+
+/**
+ * Foreground service controller, a/k/a Dianne's Dungeon.
+ */
+public class ForegroundServiceControllerImpl
+ implements ForegroundServiceController {
+ private static final String TAG = "FgServiceController";
+ private static final boolean DBG = false;
+
+ private final SparseArray<UserServices> mUserServices = new SparseArray<>();
+ private final Object mMutex = new Object();
+
+ public ForegroundServiceControllerImpl(Context context) {
+ }
+
+ @Override
+ public boolean isDungeonNeededForUser(int userId) {
+ synchronized (mMutex) {
+ final UserServices services = mUserServices.get(userId);
+ if (services == null) return false;
+ return services.isDungeonNeeded();
+ }
+ }
+
+ @Override
+ public void addNotification(StatusBarNotification sbn, int importance) {
+ updateNotification(sbn, importance);
+ }
+
+ @Override
+ public boolean removeNotification(StatusBarNotification sbn) {
+ synchronized (mMutex) {
+ final UserServices userServices = mUserServices.get(sbn.getUserId());
+ if (userServices == null) {
+ if (DBG) {
+ Log.w(TAG, String.format(
+ "user %d with no known notifications got removeNotification for %s",
+ sbn.getUserId(), sbn));
+ }
+ return false;
+ }
+ if (isDungeonNotification(sbn)) {
+ // if you remove the dungeon entirely, we take that to mean there are
+ // no running services
+ userServices.setRunningServices(null);
+ return true;
+ } else {
+ // this is safe to call on any notification, not just FLAG_FOREGROUND_SERVICE
+ return userServices.removeNotification(sbn.getPackageName(), sbn.getKey());
+ }
+ }
+ }
+
+ @Override
+ public void updateNotification(StatusBarNotification sbn, int newImportance) {
+ synchronized (mMutex) {
+ UserServices userServices = mUserServices.get(sbn.getUserId());
+ if (userServices == null) {
+ userServices = new UserServices();
+ mUserServices.put(sbn.getUserId(), userServices);
+ }
+
+ if (isDungeonNotification(sbn)) {
+ final Bundle extras = sbn.getNotification().extras;
+ if (extras != null) {
+ final String[] svcs = extras.getStringArray(Notification.EXTRA_FOREGROUND_APPS);
+ userServices.setRunningServices(svcs); // null ok
+ }
+ } else {
+ userServices.removeNotification(sbn.getPackageName(), sbn.getKey());
+ if (0 != (sbn.getNotification().flags & Notification.FLAG_FOREGROUND_SERVICE)
+ && newImportance > NotificationManager.IMPORTANCE_MIN) {
+ userServices.addNotification(sbn.getPackageName(), sbn.getKey());
+ }
+ }
+ }
+ }
+
+ @Override
+ public boolean isDungeonNotification(StatusBarNotification sbn) {
+ return sbn.getId() == SystemMessageProto.SystemMessage.NOTE_FOREGROUND_SERVICES
+ && sbn.getTag() == null
+ && sbn.getPackageName().equals("android");
+ }
+
+ /**
+ * Struct to track relevant packages and notifications for a userid's foreground services.
+ */
+ private static class UserServices {
+ private String[] mRunning = null;
+ private ArrayMap<String, ArraySet<String>> mNotifications = new ArrayMap<>(1);
+ public void setRunningServices(String[] pkgs) {
+ mRunning = pkgs != null ? Arrays.copyOf(pkgs, pkgs.length) : null;
+ }
+ public void addNotification(String pkg, String key) {
+ if (mNotifications.get(pkg) == null) {
+ mNotifications.put(pkg, new ArraySet<String>());
+ }
+ mNotifications.get(pkg).add(key);
+ }
+ public boolean removeNotification(String pkg, String key) {
+ final boolean found;
+ final ArraySet<String> keys = mNotifications.get(pkg);
+ if (keys == null) {
+ found = false;
+ } else {
+ found = keys.remove(key);
+ if (keys.size() == 0) {
+ mNotifications.remove(pkg);
+ }
+ }
+ return found;
+ }
+ public boolean isDungeonNeeded() {
+ if (mRunning != null) {
+ for (String pkg : mRunning) {
+ final ArraySet<String> set = mNotifications.get(pkg);
+ if (set == null || set.size() == 0) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ }
+}
import android.content.pm.PackageManager;
import android.content.Context;
import android.graphics.drawable.Icon;
+import android.os.AsyncTask;
+import android.os.Bundle;
import android.os.RemoteException;
import android.os.SystemClock;
import android.service.notification.NotificationListenerService;
import android.Manifest;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.messages.nano.SystemMessageProto;
import com.android.internal.statusbar.StatusBarIcon;
import com.android.internal.util.NotificationColorUtil;
+import com.android.systemui.Dependency;
+import com.android.systemui.ForegroundServiceController;
import com.android.systemui.statusbar.notification.InflationException;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.phone.StatusBar;
mEntries.put(entry.notification.getKey(), entry);
}
mGroupManager.onEntryAdded(entry);
+
updateRankingAndSort(mRankingMap);
}
Collections.sort(mSortedAndFiltered, mRankingComparator);
}
+ /**
+ * @param sbn
+ * @return true if this notification should NOT be shown right now
+ */
public boolean shouldFilterOut(StatusBarNotification sbn) {
if (!(mEnvironment.isDeviceProvisioned() ||
showNotificationEvenIfUnprovisioned(sbn))) {
&& mGroupManager.isChildInGroupWithSummary(sbn)) {
return true;
}
+
+ final ForegroundServiceController fsc = Dependency.get(ForegroundServiceController.class);
+ if (fsc.isDungeonNotification(sbn) && !fsc.isDungeonNeededForUser(sbn.getUserId())) {
+ // this is a foreground-service disclosure for a user that does not need to show one
+ return true;
+ }
+
return false;
}
import android.animation.AnimatorListenerAdapter;
import android.annotation.NonNull;
import android.app.ActivityManager;
+import android.app.ActivityManager.StackId;
import android.app.ActivityOptions;
+import android.app.INotificationManager;
+import android.app.KeyguardManager;
import android.app.Notification;
+import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
+import android.app.RemoteInput;
import android.app.StatusBarManager;
+import android.app.TaskStackBuilder;
import android.app.admin.DevicePolicyManager;
import android.content.BroadcastReceiver;
import android.content.ComponentCallbacks2;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.IntentSender;
+import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.UserInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.metrics.LogMaker;
import android.net.Uri;
import android.os.AsyncTask;
+import android.os.Build;
import android.os.Bundle;
+import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.PowerManager;
import android.os.ServiceManager;
import android.os.SystemClock;
import android.os.SystemProperties;
-import android.os.SystemService;
import android.os.Trace;
import android.os.UserHandle;
import android.os.UserManager;
import android.os.Vibrator;
import android.provider.Settings;
+import android.service.notification.NotificationListenerService;
import android.service.notification.NotificationListenerService.RankingMap;
import android.service.notification.StatusBarNotification;
+import android.service.vr.IVrManager;
+import android.service.vr.IVrStateCallbacks;
+import android.text.TextUtils;
import android.util.ArraySet;
import android.util.DisplayMetrics;
import android.util.EventLog;
import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
import android.view.ContextThemeWrapper;
import android.view.Display;
+import android.view.IWindowManager;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.ThreadedRenderer;
import android.view.View;
+import android.view.ViewAnimationUtils;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.ViewStub;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.view.WindowManagerGlobal;
+import android.view.accessibility.AccessibilityManager;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.DateTimeView;
import android.widget.ImageView;
+import android.widget.RemoteViews;
import android.widget.TextView;
+import android.widget.Toast;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
+import com.android.internal.statusbar.IStatusBarService;
import com.android.internal.statusbar.NotificationVisibility;
import com.android.internal.statusbar.StatusBarIcon;
import com.android.internal.util.NotificationMessagingUtil;
+import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.KeyguardHostView.OnDismissAction;
import com.android.keyguard.KeyguardStatusView;
import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.keyguard.KeyguardUpdateMonitorCallback;
import com.android.keyguard.ViewMediatorCallback;
import com.android.systemui.ActivityStarterDelegate;
+import com.android.systemui.DejankUtils;
import com.android.systemui.DemoMode;
import com.android.systemui.Dependency;
import com.android.systemui.EventLogTags;
+import com.android.systemui.ForegroundServiceController;
import com.android.systemui.Interpolators;
import com.android.systemui.Prefs;
import com.android.systemui.R;
+import com.android.systemui.RecentsComponent;
+import com.android.systemui.SwipeHelper;
+import com.android.systemui.SystemUI;
import com.android.systemui.SystemUIFactory;
import com.android.systemui.UiOffloadThread;
import com.android.systemui.assist.AssistManager;
import com.android.systemui.fragments.FragmentHostManager;
import com.android.systemui.fragments.PluginFragmentListener;
import com.android.systemui.keyguard.KeyguardViewMediator;
-import com.android.systemui.plugins.qs.QS;
import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.plugins.qs.QS;
+import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem;
import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption;
import com.android.systemui.qs.QSFragment;
import com.android.systemui.qs.QSPanel;
import com.android.systemui.qs.QSTileHost;
+import com.android.systemui.recents.Recents;
import com.android.systemui.recents.ScreenPinningRequest;
import com.android.systemui.recents.events.EventBus;
import com.android.systemui.recents.events.activity.AppTransitionFinishedEvent;
import com.android.systemui.statusbar.policy.NetworkController;
import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
import com.android.systemui.statusbar.policy.PreviewInflater;
+import com.android.systemui.statusbar.policy.RemoteInputView;
import com.android.systemui.statusbar.policy.UserInfoController;
import com.android.systemui.statusbar.policy.UserInfoControllerImpl;
import com.android.systemui.statusbar.policy.UserSwitcherController;
import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
-import com.android.systemui.statusbar.stack.NotificationStackScrollLayout.OnChildLocationsChangedListener;
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout
+ .OnChildLocationsChangedListener;
+import com.android.systemui.statusbar.stack.StackStateAnimator;
+import com.android.systemui.util.NotificationChannels;
import com.android.systemui.util.leak.LeakDetector;
import com.android.systemui.volume.VolumeComponent;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import android.app.ActivityManager.StackId;
-import android.app.INotificationManager;
-import android.app.KeyguardManager;
-import android.app.NotificationChannel;
-import android.app.RemoteInput;
-import android.app.TaskStackBuilder;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.pm.UserInfo;
-import android.os.Build;
-import android.os.Handler;
-import android.service.notification.NotificationListenerService;
-import android.service.vr.IVrManager;
-import android.service.vr.IVrStateCallbacks;
-import android.text.TextUtils;
-import android.util.Slog;
-import android.util.SparseArray;
-import android.util.SparseBooleanArray;
-import android.view.IWindowManager;
-import android.view.ViewAnimationUtils;
-import android.view.accessibility.AccessibilityManager;
-import android.widget.RemoteViews;
-import android.widget.Toast;
-
-import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
-import com.android.internal.statusbar.IStatusBarService;
-import com.android.internal.widget.LockPatternUtils;
-import com.android.systemui.DejankUtils;
-import com.android.systemui.RecentsComponent;
-import com.android.systemui.SwipeHelper;
-import com.android.systemui.SystemUI;
-import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem;
-import com.android.systemui.recents.Recents;
-import com.android.systemui.statusbar.policy.RemoteInputView;
-import com.android.systemui.statusbar.stack.StackStateAnimator;
-import com.android.systemui.util.NotificationChannels;
-
import java.util.HashSet;
+import java.util.List;
import java.util.Locale;
+import java.util.Map;
import java.util.Set;
import java.util.Stack;
private ConfigurationListener mConfigurationListener;
private boolean mReinflateNotificationsOnUserSwitched;
private HashMap<String, Entry> mPendingNotifications = new HashMap<>();
+ private ForegroundServiceController mForegroundServiceController;
private void recycleAllVisibilityObjects(ArraySet<NotificationVisibility> array) {
final int N = array.size();
mDeviceProvisionedController = Dependency.get(DeviceProvisionedController.class);
mSystemServicesProxy = SystemServicesProxy.getInstance(mContext);
+ mForegroundServiceController = Dependency.get(ForegroundServiceController.class);
+
mWindowManager = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
mDisplay = mWindowManager.getDefaultDisplay();
updateDisplaySize();
}
}
abortExistingInflation(key);
+
+ mForegroundServiceController.addNotification(notification,
+ mNotificationData.getImportance(key));
+
mPendingNotifications.put(key, shadeEntry);
}
return;
}
+ if (entry != null) {
+ mForegroundServiceController.removeNotification(entry.notification);
+ }
+
if (entry != null && entry.row != null) {
entry.row.setRemoved();
mStackScroller.cleanUpViewState(entry.row);
entry.updateIcons(mContext, n);
inflateViews(entry, mStackScroller);
+ mForegroundServiceController.updateNotification(notification,
+ mNotificationData.getImportance(key));
+
boolean shouldPeek = shouldPeek(entry, notification);
boolean alertAgain = alertAgain(entry, n);
boolean isForCurrentUser = isNotificationForCurrentProfiles(notification);
Log.d(TAG, "notification is " + (isForCurrentUser ? "" : "not ") + "for you");
}
+
setAreThereNotifications();
}
--- /dev/null
+/*
+ * Copyright (C) 2017 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.systemui;
+
+import android.annotation.UserIdInt;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.service.notification.StatusBarNotification;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import com.android.internal.messages.nano.SystemMessageProto;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ForegroundServiceControllerTest extends SysuiTestCase {
+ public static @UserIdInt int USERID_ONE = 10; // UserManagerService.MIN_USER_ID;
+ public static @UserIdInt int USERID_TWO = USERID_ONE + 1;
+
+ private ForegroundServiceController fsc;
+
+ @Before
+ public void setUp() throws Exception {
+ fsc = new ForegroundServiceControllerImpl(mContext);
+ }
+
+ @Test
+ public void testNotificationCRUD() {
+ StatusBarNotification sbn_user1_app1_fg = makeMockFgSBN(USERID_ONE, "com.example.app1");
+ StatusBarNotification sbn_user2_app2_fg = makeMockFgSBN(USERID_TWO, "com.example.app2");
+ StatusBarNotification sbn_user1_app3_fg = makeMockFgSBN(USERID_ONE, "com.example.app3");
+ StatusBarNotification sbn_user1_app1 = makeMockSBN(USERID_ONE, "com.example.app1",
+ 5000, "monkeys", Notification.FLAG_AUTO_CANCEL);
+ StatusBarNotification sbn_user2_app1 = makeMockSBN(USERID_TWO, "com.example.app1",
+ 5000, "monkeys", Notification.FLAG_AUTO_CANCEL);
+
+ assertFalse(fsc.removeNotification(sbn_user1_app3_fg));
+ assertFalse(fsc.removeNotification(sbn_user2_app2_fg));
+ assertFalse(fsc.removeNotification(sbn_user1_app1_fg));
+ assertFalse(fsc.removeNotification(sbn_user1_app1));
+ assertFalse(fsc.removeNotification(sbn_user2_app1));
+
+ fsc.addNotification(sbn_user1_app1_fg, NotificationManager.IMPORTANCE_DEFAULT);
+ fsc.addNotification(sbn_user2_app2_fg, NotificationManager.IMPORTANCE_DEFAULT);
+ fsc.addNotification(sbn_user1_app3_fg, NotificationManager.IMPORTANCE_DEFAULT);
+ fsc.addNotification(sbn_user1_app1, NotificationManager.IMPORTANCE_DEFAULT);
+ fsc.addNotification(sbn_user2_app1, NotificationManager.IMPORTANCE_DEFAULT);
+
+ // these are never added to the tracker
+ assertFalse(fsc.removeNotification(sbn_user1_app1));
+ assertFalse(fsc.removeNotification(sbn_user2_app1));
+
+ fsc.updateNotification(sbn_user1_app1, NotificationManager.IMPORTANCE_DEFAULT);
+ fsc.updateNotification(sbn_user2_app1, NotificationManager.IMPORTANCE_DEFAULT);
+ // should still not be there
+ assertFalse(fsc.removeNotification(sbn_user1_app1));
+ assertFalse(fsc.removeNotification(sbn_user2_app1));
+
+ fsc.updateNotification(sbn_user2_app2_fg, NotificationManager.IMPORTANCE_DEFAULT);
+ fsc.updateNotification(sbn_user1_app3_fg, NotificationManager.IMPORTANCE_DEFAULT);
+ fsc.updateNotification(sbn_user1_app1_fg, NotificationManager.IMPORTANCE_DEFAULT);
+
+ assertTrue(fsc.removeNotification(sbn_user1_app3_fg));
+ assertFalse(fsc.removeNotification(sbn_user1_app3_fg));
+
+ assertTrue(fsc.removeNotification(sbn_user2_app2_fg));
+ assertFalse(fsc.removeNotification(sbn_user2_app2_fg));
+
+ assertTrue(fsc.removeNotification(sbn_user1_app1_fg));
+ assertFalse(fsc.removeNotification(sbn_user1_app1_fg));
+
+ assertFalse(fsc.removeNotification(sbn_user1_app1));
+ assertFalse(fsc.removeNotification(sbn_user2_app1));
+ }
+
+ @Test
+ public void testDungeonPredicate() {
+ StatusBarNotification sbn_user1_app1 = makeMockSBN(USERID_ONE, "com.example.app1",
+ 5000, "monkeys", Notification.FLAG_AUTO_CANCEL);
+ StatusBarNotification sbn_user1_dungeon = makeMockSBN(USERID_ONE, "android",
+ SystemMessageProto.SystemMessage.NOTE_FOREGROUND_SERVICES,
+ null, Notification.FLAG_NO_CLEAR);
+
+ assertTrue(fsc.isDungeonNotification(sbn_user1_dungeon));
+ assertFalse(fsc.isDungeonNotification(sbn_user1_app1));
+ }
+
+ @Test
+ public void testDungeonCRUD() {
+ StatusBarNotification sbn_user1_app1 = makeMockSBN(USERID_ONE, "com.example.app1",
+ 5000, "monkeys", Notification.FLAG_AUTO_CANCEL);
+ StatusBarNotification sbn_user1_dungeon = makeMockSBN(USERID_ONE, "android",
+ SystemMessageProto.SystemMessage.NOTE_FOREGROUND_SERVICES,
+ null, Notification.FLAG_NO_CLEAR);
+
+ fsc.addNotification(sbn_user1_app1, NotificationManager.IMPORTANCE_DEFAULT);
+ fsc.addNotification(sbn_user1_dungeon, NotificationManager.IMPORTANCE_DEFAULT);
+
+ fsc.removeNotification(sbn_user1_dungeon);
+ assertFalse(fsc.removeNotification(sbn_user1_app1));
+ }
+
+ @Test
+ public void testNeedsDungeonAfterRemovingUnrelatedNotification() {
+ final String PKG1 = "com.example.app100";
+
+ StatusBarNotification sbn_user1_app1 = makeMockSBN(USERID_ONE, PKG1,
+ 5000, "monkeys", Notification.FLAG_AUTO_CANCEL);
+ StatusBarNotification sbn_user1_app1_fg = makeMockFgSBN(USERID_ONE, PKG1);
+
+ // first add a normal notification
+ fsc.addNotification(sbn_user1_app1, NotificationManager.IMPORTANCE_DEFAULT);
+ // nothing required yet
+ assertFalse(fsc.isDungeonNeededForUser(USERID_ONE));
+ // now the app starts a fg service
+ fsc.addNotification(makeMockDungeon(USERID_ONE, new String[]{ PKG1 }),
+ NotificationManager.IMPORTANCE_DEFAULT);
+ assertTrue(fsc.isDungeonNeededForUser(USERID_ONE)); // should be required!
+ // add the fg notification
+ fsc.addNotification(sbn_user1_app1_fg, NotificationManager.IMPORTANCE_DEFAULT);
+ assertFalse(fsc.isDungeonNeededForUser(USERID_ONE)); // app1 has got it covered
+ // remove the boring notification
+ fsc.removeNotification(sbn_user1_app1);
+ assertFalse(fsc.isDungeonNeededForUser(USERID_ONE)); // app1 has STILL got it covered
+ assertTrue(fsc.removeNotification(sbn_user1_app1_fg));
+ assertTrue(fsc.isDungeonNeededForUser(USERID_ONE)); // should be required!
+ }
+
+ @Test
+ public void testSimpleAddRemove() {
+ final String PKG1 = "com.example.app1";
+ final String PKG2 = "com.example.app2";
+
+ StatusBarNotification sbn_user1_app1 = makeMockSBN(USERID_ONE, PKG1,
+ 5000, "monkeys", Notification.FLAG_AUTO_CANCEL);
+ fsc.addNotification(sbn_user1_app1, NotificationManager.IMPORTANCE_DEFAULT);
+
+ // no services are "running"
+ fsc.addNotification(makeMockDungeon(USERID_ONE, null),
+ NotificationManager.IMPORTANCE_DEFAULT);
+
+ assertFalse(fsc.isDungeonNeededForUser(USERID_ONE));
+ assertFalse(fsc.isDungeonNeededForUser(USERID_TWO));
+
+ fsc.updateNotification(makeMockDungeon(USERID_ONE, new String[]{PKG1}),
+ NotificationManager.IMPORTANCE_DEFAULT);
+ assertTrue(fsc.isDungeonNeededForUser(USERID_ONE)); // should be required!
+ assertFalse(fsc.isDungeonNeededForUser(USERID_TWO));
+
+ // switch to different package
+ fsc.updateNotification(makeMockDungeon(USERID_ONE, new String[]{PKG2}),
+ NotificationManager.IMPORTANCE_DEFAULT);
+ assertTrue(fsc.isDungeonNeededForUser(USERID_ONE));
+ assertFalse(fsc.isDungeonNeededForUser(USERID_TWO));
+
+ fsc.updateNotification(makeMockDungeon(USERID_TWO, new String[]{PKG1}),
+ NotificationManager.IMPORTANCE_DEFAULT);
+ assertTrue(fsc.isDungeonNeededForUser(USERID_ONE));
+ assertTrue(fsc.isDungeonNeededForUser(USERID_TWO)); // finally user2 needs one too
+
+ fsc.updateNotification(makeMockDungeon(USERID_ONE, new String[]{PKG2, PKG1}),
+ NotificationManager.IMPORTANCE_DEFAULT);
+ assertTrue(fsc.isDungeonNeededForUser(USERID_ONE));
+ assertTrue(fsc.isDungeonNeededForUser(USERID_TWO));
+
+ fsc.removeNotification(makeMockDungeon(USERID_ONE, null /*unused*/));
+ assertFalse(fsc.isDungeonNeededForUser(USERID_ONE));
+ assertTrue(fsc.isDungeonNeededForUser(USERID_TWO));
+
+ fsc.removeNotification(makeMockDungeon(USERID_TWO, null /*unused*/));
+ assertFalse(fsc.isDungeonNeededForUser(USERID_ONE));
+ assertFalse(fsc.isDungeonNeededForUser(USERID_TWO));
+ }
+
+ @Test
+ public void testDungeonBasic() {
+ final String PKG1 = "com.example.app0";
+
+ StatusBarNotification sbn_user1_app1 = makeMockSBN(USERID_ONE, PKG1,
+ 5000, "monkeys", Notification.FLAG_AUTO_CANCEL);
+ StatusBarNotification sbn_user1_app1_fg = makeMockFgSBN(USERID_ONE, PKG1);
+
+ fsc.addNotification(sbn_user1_app1, NotificationManager.IMPORTANCE_DEFAULT); // not fg
+ fsc.addNotification(makeMockDungeon(USERID_ONE, new String[]{ PKG1 }),
+ NotificationManager.IMPORTANCE_DEFAULT);
+ assertTrue(fsc.isDungeonNeededForUser(USERID_ONE)); // should be required!
+ fsc.addNotification(sbn_user1_app1_fg, NotificationManager.IMPORTANCE_DEFAULT);
+ assertFalse(fsc.isDungeonNeededForUser(USERID_ONE)); // app1 has got it covered
+ assertFalse(fsc.isDungeonNeededForUser(USERID_TWO));
+
+ // let's take out the other notification and see what happens.
+
+ fsc.removeNotification(sbn_user1_app1);
+ assertFalse(fsc.isDungeonNeededForUser(USERID_ONE)); // still covered by sbn_user1_app1_fg
+ assertFalse(fsc.isDungeonNeededForUser(USERID_TWO));
+
+ // let's attempt to downgrade the notification from FLAG_FOREGROUND and see what we get
+ StatusBarNotification sbn_user1_app1_fg_sneaky = makeMockFgSBN(USERID_ONE, PKG1);
+ sbn_user1_app1_fg_sneaky.getNotification().flags = 0;
+ fsc.updateNotification(sbn_user1_app1_fg_sneaky, NotificationManager.IMPORTANCE_DEFAULT);
+ assertTrue(fsc.isDungeonNeededForUser(USERID_ONE)); // should be required!
+ assertFalse(fsc.isDungeonNeededForUser(USERID_TWO));
+
+ // ok, ok, we'll put it back
+ sbn_user1_app1_fg_sneaky.getNotification().flags = Notification.FLAG_FOREGROUND_SERVICE;
+ fsc.updateNotification(sbn_user1_app1_fg, NotificationManager.IMPORTANCE_DEFAULT);
+ assertFalse(fsc.isDungeonNeededForUser(USERID_ONE));
+ assertFalse(fsc.isDungeonNeededForUser(USERID_TWO));
+
+ assertTrue(fsc.removeNotification(sbn_user1_app1_fg_sneaky));
+ assertTrue(fsc.isDungeonNeededForUser(USERID_ONE)); // should be required!
+ assertFalse(fsc.isDungeonNeededForUser(USERID_TWO));
+
+ // now let's test an upgrade
+ fsc.addNotification(sbn_user1_app1, NotificationManager.IMPORTANCE_DEFAULT);
+ assertTrue(fsc.isDungeonNeededForUser(USERID_ONE));
+ assertFalse(fsc.isDungeonNeededForUser(USERID_TWO));
+ sbn_user1_app1.getNotification().flags |= Notification.FLAG_FOREGROUND_SERVICE;
+ fsc.updateNotification(sbn_user1_app1,
+ NotificationManager.IMPORTANCE_DEFAULT); // this is now a fg notification
+
+ assertFalse(fsc.isDungeonNeededForUser(USERID_TWO));
+ assertFalse(fsc.isDungeonNeededForUser(USERID_ONE));
+
+ // remove it, make sure we're out of compliance again
+ assertTrue(fsc.removeNotification(sbn_user1_app1)); // was fg, should return true
+ assertFalse(fsc.removeNotification(sbn_user1_app1));
+ assertFalse(fsc.isDungeonNeededForUser(USERID_TWO));
+ assertTrue(fsc.isDungeonNeededForUser(USERID_ONE));
+
+ // finally, let's turn off the service
+ fsc.addNotification(makeMockDungeon(USERID_ONE, null),
+ NotificationManager.IMPORTANCE_DEFAULT);
+
+ assertFalse(fsc.isDungeonNeededForUser(USERID_ONE));
+ assertFalse(fsc.isDungeonNeededForUser(USERID_TWO));
+ }
+
+ private StatusBarNotification makeMockSBN(int userid, String pkg, int id, String tag,
+ int flags) {
+ final Notification n = mock(Notification.class);
+ n.flags = flags;
+ return makeMockSBN(userid, pkg, id, tag, n);
+ }
+ private StatusBarNotification makeMockSBN(int userid, String pkg, int id, String tag,
+ Notification n) {
+ final StatusBarNotification sbn = mock(StatusBarNotification.class);
+ when(sbn.getNotification()).thenReturn(n);
+ when(sbn.getId()).thenReturn(id);
+ when(sbn.getPackageName()).thenReturn(pkg);
+ when(sbn.getTag()).thenReturn(null);
+ when(sbn.getUserId()).thenReturn(userid);
+ when(sbn.getUser()).thenReturn(new UserHandle(userid));
+ when(sbn.getKey()).thenReturn("MOCK:"+userid+"|"+pkg+"|"+id+"|"+tag);
+ return sbn;
+ }
+ private StatusBarNotification makeMockFgSBN(int userid, String pkg) {
+ return makeMockSBN(userid, pkg, 1000, "foo", Notification.FLAG_FOREGROUND_SERVICE);
+ }
+ private StatusBarNotification makeMockDungeon(int userid, String[] pkgs) {
+ final Notification n = mock(Notification.class);
+ n.flags = Notification.FLAG_ONGOING_EVENT;
+ final Bundle extras = new Bundle();
+ if (pkgs != null) extras.putStringArray(Notification.EXTRA_FOREGROUND_APPS, pkgs);
+ n.extras = extras;
+ final StatusBarNotification sbn = makeMockSBN(userid, "android",
+ SystemMessageProto.SystemMessage.NOTE_FOREGROUND_SERVICES,
+ null, n);
+ sbn.getNotification().extras = extras;
+ return sbn;
+ }
+}