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)