diff --git a/core/java/com/android/internal/notification/SystemNotificationChannels.java b/core/java/com/android/internal/notification/SystemNotificationChannels.java
index 797cf2b6de56b..d327180c6e9dd 100644
--- a/core/java/com/android/internal/notification/SystemNotificationChannels.java
+++ b/core/java/com/android/internal/notification/SystemNotificationChannels.java
@@ -135,7 +135,7 @@ public class SystemNotificationChannels {
channelsList.add(new NotificationChannel(
FOREGROUND_SERVICE,
context.getString(R.string.notification_channel_foreground_service),
- NotificationManager.IMPORTANCE_MIN));
+ NotificationManager.IMPORTANCE_LOW));
nm.createNotificationChannels(channelsList);
}
diff --git a/core/res/res/drawable/stat_sys_vitals.xml b/core/res/res/drawable/stat_sys_vitals.xml
new file mode 100644
index 0000000000000..213dd5fbed6e8
--- /dev/null
+++ b/core/res/res/drawable/stat_sys_vitals.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index bfd40bd704e1b..cd29c64df4d86 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3024,4 +3024,5 @@
+
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index 0cf8ff05932fa..8ba33c30e6006 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -264,6 +264,9 @@ public class Dependency extends SystemUI {
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.
diff --git a/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java b/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java
new file mode 100644
index 0000000000000..a2c9ab4871c28
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java
@@ -0,0 +1,49 @@
+/*
+ * 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);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/ForegroundServiceControllerImpl.java b/packages/SystemUI/src/com/android/systemui/ForegroundServiceControllerImpl.java
new file mode 100644
index 0000000000000..c930d567254a1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/ForegroundServiceControllerImpl.java
@@ -0,0 +1,156 @@
+/*
+ * 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 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> 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());
+ }
+ mNotifications.get(pkg).add(key);
+ }
+ public boolean removeNotification(String pkg, String key) {
+ final boolean found;
+ final ArraySet 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 set = mNotifications.get(pkg);
+ if (set == null || set.size() == 0) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java
index 1844946e75b6b..531437dff492f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java
@@ -24,6 +24,8 @@ import android.content.pm.IPackageManager;
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;
@@ -38,8 +40,11 @@ import android.widget.RemoteViews;
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;
@@ -339,6 +344,7 @@ public class NotificationData {
mEntries.put(entry.notification.getKey(), entry);
}
mGroupManager.onEntryAdded(entry);
+
updateRankingAndSort(mRankingMap);
}
@@ -466,6 +472,10 @@ public class NotificationData {
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))) {
@@ -487,6 +497,13 @@ public class NotificationData {
&& 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;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index 4e28e90a14028..aedecc516b645 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -36,11 +36,17 @@ import android.animation.Animator;
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;
@@ -49,8 +55,11 @@ import android.content.Context;
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;
@@ -75,7 +84,9 @@ import android.media.session.PlaybackState;
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;
@@ -83,54 +94,73 @@ import android.os.RemoteException;
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;
@@ -141,12 +171,14 @@ import com.android.systemui.doze.DozeLog;
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;
@@ -194,11 +226,15 @@ import com.android.systemui.statusbar.policy.KeyguardUserSwitcher;
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;
@@ -209,48 +245,10 @@ import java.util.ArrayList;
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;
@@ -718,6 +716,7 @@ public class StatusBar extends SystemUI implements DemoMode,
private ConfigurationListener mConfigurationListener;
private boolean mReinflateNotificationsOnUserSwitched;
private HashMap mPendingNotifications = new HashMap<>();
+ private ForegroundServiceController mForegroundServiceController;
private void recycleAllVisibilityObjects(ArraySet array) {
final int N = array.size();
@@ -761,6 +760,8 @@ public class StatusBar extends SystemUI implements DemoMode,
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();
@@ -1578,6 +1579,10 @@ public class StatusBar extends SystemUI implements DemoMode,
}
}
abortExistingInflation(key);
+
+ mForegroundServiceController.addNotification(notification,
+ mNotificationData.getImportance(key));
+
mPendingNotifications.put(key, shadeEntry);
}
@@ -1716,6 +1721,10 @@ public class StatusBar extends SystemUI implements DemoMode,
return;
}
+ if (entry != null) {
+ mForegroundServiceController.removeNotification(entry.notification);
+ }
+
if (entry != null && entry.row != null) {
entry.row.setRemoved();
mStackScroller.cleanUpViewState(entry.row);
@@ -6766,6 +6775,9 @@ public class StatusBar extends SystemUI implements DemoMode,
entry.updateIcons(mContext, n);
inflateViews(entry, mStackScroller);
+ mForegroundServiceController.updateNotification(notification,
+ mNotificationData.getImportance(key));
+
boolean shouldPeek = shouldPeek(entry, notification);
boolean alertAgain = alertAgain(entry, n);
@@ -6783,6 +6795,7 @@ public class StatusBar extends SystemUI implements DemoMode,
boolean isForCurrentUser = isNotificationForCurrentProfiles(notification);
Log.d(TAG, "notification is " + (isForCurrentUser ? "" : "not ") + "for you");
}
+
setAreThereNotifications();
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java
new file mode 100644
index 0000000000000..1f5255a0e869d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java
@@ -0,0 +1,296 @@
+/*
+ * 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;
+ }
+}
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 2680b425ff3cd..bad7091429a27 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -162,7 +162,7 @@ public final class ActiveServices {
/**
* Information about an app that is currently running one or more foreground services.
- * (This mapps directly to the running apps we show in the notification.)
+ * (This maps directly to the running apps we show in the notification.)
*/
static final class ActiveForegroundApp {
String mPackageName;
@@ -813,6 +813,7 @@ public final class ActiveServices {
String title;
String msg;
String[] pkgs;
+ long oldestStartTime = System.currentTimeMillis(); // now
if (active.size() == 1) {
intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.fromParts("package", active.get(0).mPackageName, null));
@@ -820,11 +821,13 @@ public final class ActiveServices {
R.string.foreground_service_app_in_background, active.get(0).mLabel);
msg = context.getString(R.string.foreground_service_tap_for_details);
pkgs = new String[] { active.get(0).mPackageName };
+ oldestStartTime = active.get(0).mStartTime;
} else {
intent = new Intent(Settings.ACTION_FOREGROUND_SERVICES_SETTINGS);
pkgs = new String[active.size()];
for (int i = 0; i < active.size(); i++) {
pkgs[i] = active.get(i).mPackageName;
+ oldestStartTime = Math.min(oldestStartTime, active.get(i).mStartTime);
}
intent.putExtra("packages", pkgs);
title = context.getString(
@@ -841,9 +844,10 @@ public final class ActiveServices {
new Notification.Builder(context,
SystemNotificationChannels.FOREGROUND_SERVICE)
.addExtras(notificationBundle)
- .setSmallIcon(R.drawable.ic_check_circle_24px)
+ .setSmallIcon(R.drawable.stat_sys_vitals)
.setOngoing(true)
- .setShowWhen(false)
+ .setShowWhen(oldestStartTime > 0)
+ .setWhen(oldestStartTime)
.setColor(context.getColor(
com.android.internal.R.color.system_notification_accent_color))
.setContentTitle(title)