diff --git a/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java b/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java index c1a23c8ab0035..493186b2bd850 100644 --- a/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java +++ b/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java @@ -37,7 +37,7 @@ import javax.inject.Singleton; */ @Singleton public class ForegroundServiceController { - private static final int[] APP_OPS = new int[] {AppOpsManager.OP_CAMERA, + public static final int[] APP_OPS = new int[] {AppOpsManager.OP_CAMERA, AppOpsManager.OP_SYSTEM_ALERT_WINDOW, AppOpsManager.OP_RECORD_AUDIO, AppOpsManager.OP_COARSE_LOCATION, diff --git a/packages/SystemUI/src/com/android/systemui/ForegroundServiceNotificationListener.java b/packages/SystemUI/src/com/android/systemui/ForegroundServiceNotificationListener.java index b983966317f71..c4e276277df52 100644 --- a/packages/SystemUI/src/com/android/systemui/ForegroundServiceNotificationListener.java +++ b/packages/SystemUI/src/com/android/systemui/ForegroundServiceNotificationListener.java @@ -27,6 +27,8 @@ import android.util.Log; import com.android.internal.statusbar.NotificationVisibility; import com.android.systemui.statusbar.notification.NotificationEntryListener; import com.android.systemui.statusbar.notification.NotificationEntryManager; +import com.android.systemui.statusbar.notification.collection.NotifCollection; +import com.android.systemui.statusbar.notification.collection.NotifCollectionListener; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import javax.inject.Inject; @@ -46,7 +48,8 @@ public class ForegroundServiceNotificationListener { @Inject public ForegroundServiceNotificationListener(Context context, ForegroundServiceController foregroundServiceController, - NotificationEntryManager notificationEntryManager) { + NotificationEntryManager notificationEntryManager, + NotifCollection notifCollection) { mContext = context; mForegroundServiceController = foregroundServiceController; mEntryManager = notificationEntryManager; @@ -69,8 +72,24 @@ public class ForegroundServiceNotificationListener { removeNotification(entry.getSbn()); } }); - mEntryManager.addNotificationLifetimeExtender(new ForegroundServiceLifetimeExtender()); + + notifCollection.addCollectionListener(new NotifCollectionListener() { + @Override + public void onEntryAdded(NotificationEntry entry) { + addNotification(entry, entry.getImportance()); + } + + @Override + public void onEntryUpdated(NotificationEntry entry) { + updateNotification(entry, entry.getImportance()); + } + + @Override + public void onEntryRemoved(NotificationEntry entry, int reason, boolean removedByUser) { + removeNotification(entry.getSbn()); + } + }); } /** @@ -152,6 +171,8 @@ public class ForegroundServiceNotificationListener { true /* create if not found */); } + // TODO: remove this when fully migrated to the NewNotifPipeline (work done in + // ForegroundCoordinator) private void tagForeground(NotificationEntry entry) { final StatusBarNotification sbn = entry.getSbn(); ArraySet activeOps = mForegroundServiceController.getAppOps( diff --git a/packages/SystemUI/src/com/android/systemui/ForegroundServicesUserState.java b/packages/SystemUI/src/com/android/systemui/ForegroundServicesUserState.java index a8ae65490b284..2ef46dca317a2 100644 --- a/packages/SystemUI/src/com/android/systemui/ForegroundServicesUserState.java +++ b/packages/SystemUI/src/com/android/systemui/ForegroundServicesUserState.java @@ -24,7 +24,7 @@ import java.util.Arrays; /** * Struct to track relevant packages and notifications for a userid's foreground services. */ -class ForegroundServicesUserState { +public class ForegroundServicesUserState { // shelf life of foreground services before they go bad private static final long FG_SERVICE_GRACE_MILLIS = 5000; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index 3eb55ef6bc937..232fb6d3cf678 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -275,10 +275,6 @@ public final class NotificationEntry extends ListEntry { return mRanking.getSuppressedVisualEffects(); } - public boolean isSuspended() { - return mRanking.isSuspended(); - } - /** @see Ranking#canBubble() */ public boolean canBubble() { return mRanking.canBubble(); @@ -950,6 +946,15 @@ public final class NotificationEntry extends ListEntry { return Objects.equals(n.category, category); } + /** + * Whether or not this row represents a system notification. Note that if this is + * {@code null}, that means we were either unable to retrieve the info or have yet to + * retrieve the info. + */ + public Boolean isSystemNotification() { + return mIsSystemNotification; + } + /** * Set this notification to be sensitive. * diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DeviceProvisionedCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DeviceProvisionedCoordinator.java new file mode 100644 index 0000000000000..511aafc2d12a8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DeviceProvisionedCoordinator.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2019 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.statusbar.notification.collection.coordinator; + +import android.Manifest; +import android.app.AppGlobals; +import android.app.Notification; +import android.content.pm.IPackageManager; +import android.content.pm.PackageManager; +import android.os.RemoteException; +import android.service.notification.StatusBarNotification; + +import com.android.systemui.statusbar.notification.collection.NotifCollection; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.collection.listbuilder.NotifListBuilder; +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter; +import com.android.systemui.statusbar.policy.DeviceProvisionedController; + +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * Filters out most notifications when the device is unprovisioned. + * Special notifications with extra permissions and tags won't be filtered out even when the + * device is unprovisioned. + */ +@Singleton +public class DeviceProvisionedCoordinator implements Coordinator { + private static final String TAG = "DeviceProvisionedCoordinator"; + + private final DeviceProvisionedController mDeviceProvisionedController; + + @Inject + public DeviceProvisionedCoordinator(DeviceProvisionedController deviceProvisionedController) { + mDeviceProvisionedController = deviceProvisionedController; + } + + @Override + public void attach(NotifCollection notifCollection, NotifListBuilder notifListBuilder) { + mDeviceProvisionedController.addCallback(mDeviceProvisionedListener); + + notifListBuilder.addFilter(mNotifFilter); + } + + protected final NotifFilter mNotifFilter = new NotifFilter(TAG) { + @Override + public boolean shouldFilterOut(NotificationEntry entry, long now) { + return !mDeviceProvisionedController.isDeviceProvisioned() + && !showNotificationEvenIfUnprovisioned(entry.getSbn()); + } + }; + + /** + * Only notifications coming from packages with permission + * android.permission.NOTIFICATION_DURING_SETUP that also have special tags + * marking them as relevant for setup are allowed to show when device is unprovisioned + */ + private boolean showNotificationEvenIfUnprovisioned(StatusBarNotification sbn) { + final boolean hasPermission = checkUidPermission(AppGlobals.getPackageManager(), + Manifest.permission.NOTIFICATION_DURING_SETUP, + sbn.getUid()) == PackageManager.PERMISSION_GRANTED; + return hasPermission + && sbn.getNotification().extras.getBoolean(Notification.EXTRA_ALLOW_DURING_SETUP); + } + + private static int checkUidPermission(IPackageManager packageManager, String permission, + int uid) { + try { + return packageManager.checkUidPermission(permission, uid); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + private final DeviceProvisionedController.DeviceProvisionedListener mDeviceProvisionedListener = + new DeviceProvisionedController.DeviceProvisionedListener() { + @Override + public void onDeviceProvisionedChanged() { + mNotifFilter.invalidateList(); + } + }; +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ForegroundCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ForegroundCoordinator.java new file mode 100644 index 0000000000000..4803cf478327e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ForegroundCoordinator.java @@ -0,0 +1,243 @@ +/* + * Copyright (C) 2019 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.statusbar.notification.collection.coordinator; + +import android.app.Notification; +import android.os.Handler; +import android.os.UserHandle; +import android.service.notification.StatusBarNotification; +import android.util.ArraySet; + +import com.android.systemui.ForegroundServiceController; +import com.android.systemui.appops.AppOpsController; +import com.android.systemui.dagger.qualifiers.BgHandler; +import com.android.systemui.dagger.qualifiers.MainHandler; +import com.android.systemui.statusbar.notification.collection.NotifCollection; +import com.android.systemui.statusbar.notification.collection.NotifCollectionListener; +import com.android.systemui.statusbar.notification.collection.NotifLifetimeExtender; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.collection.listbuilder.NotifListBuilder; +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter; + +import java.util.HashMap; +import java.util.Map; + +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * Handles ForegroundService interactions with notifications. + * Tags notifications with appOps. + * Lifetime extends notifications associated with an ongoing ForegroundService. + * Filters out notifications that represent foreground services that are no longer running + * + * Previously this logic lived in + * frameworks/base/packages/SystemUI/src/com/android/systemui/ForegroundServiceController + * frameworks/base/packages/SystemUI/src/com/android/systemui/ForegroundServiceNotificationListener + * frameworks/base/packages/SystemUI/src/com/android/systemui/ForegroundServiceLifetimeExtender + */ +@Singleton +public class ForegroundCoordinator implements Coordinator { + private static final String TAG = "ForegroundNotificationCoordinator"; + + private final ForegroundServiceController mForegroundServiceController; + private final AppOpsController mAppOpsController; + private final Handler mMainHandler; + private final Handler mBgHandler; + + private NotifCollection mNotifCollection; + + @Inject + public ForegroundCoordinator( + ForegroundServiceController foregroundServiceController, + AppOpsController appOpsController, + @MainHandler Handler mainHandler, + @BgHandler Handler bgHandler) { + mForegroundServiceController = foregroundServiceController; + mAppOpsController = appOpsController; + mMainHandler = mainHandler; + mBgHandler = bgHandler; + } + + @Override + public void attach(NotifCollection notifCollection, NotifListBuilder notifListBuilder) { + mNotifCollection = notifCollection; + + // extend the lifetime of foreground notification services to show for at least 5 seconds + mNotifCollection.addNotificationLifetimeExtender(mForegroundLifetimeExtender); + + // listen for new notifications to add appOps + mNotifCollection.addCollectionListener(mNotifCollectionListener); + + // when appOps change, update any relevant notifications to update appOps for + mAppOpsController.addCallback(ForegroundServiceController.APP_OPS, this::onAppOpsChanged); + + // filter out foreground service notifications that aren't necessary anymore + notifListBuilder.addFilter(mNotifFilter); + } + + /** + * Filters out notifications that represent foreground services that are no longer running. + */ + protected final NotifFilter mNotifFilter = new NotifFilter(TAG) { + @Override + public boolean shouldFilterOut(NotificationEntry entry, long now) { + StatusBarNotification sbn = entry.getSbn(); + if (mForegroundServiceController.isDisclosureNotification(sbn) + && !mForegroundServiceController.isDisclosureNeededForUser(sbn.getUserId())) { + return true; + } + + if (mForegroundServiceController.isSystemAlertNotification(sbn)) { + final String[] apps = sbn.getNotification().extras.getStringArray( + Notification.EXTRA_FOREGROUND_APPS); + if (apps != null && apps.length >= 1) { + if (!mForegroundServiceController.isSystemAlertWarningNeeded( + sbn.getUserId(), apps[0])) { + return true; + } + } + } + return false; + } + }; + + /** + * Extends the lifetime of foreground notification services such that they show for at least + * five seconds + */ + private final NotifLifetimeExtender mForegroundLifetimeExtender = new NotifLifetimeExtender() { + private static final int MIN_FGS_TIME_MS = 5000; + private OnEndLifetimeExtensionCallback mEndCallback; + private Map mEndRunnables = new HashMap<>(); + + @Override + public String getName() { + return TAG; + } + + @Override + public void setCallback(OnEndLifetimeExtensionCallback callback) { + mEndCallback = callback; + } + + @Override + public boolean shouldExtendLifetime(NotificationEntry entry, int reason) { + if ((entry.getSbn().getNotification().flags + & Notification.FLAG_FOREGROUND_SERVICE) == 0) { + return false; + } + + final long currTime = System.currentTimeMillis(); + final boolean extendLife = currTime - entry.getSbn().getPostTime() < MIN_FGS_TIME_MS; + + if (extendLife) { + if (!mEndRunnables.containsKey(entry.getKey())) { + final Runnable runnable = new Runnable() { + @Override + public void run() { + mEndCallback.onEndLifetimeExtension(mForegroundLifetimeExtender, entry); + } + }; + mEndRunnables.put(entry.getKey(), runnable); + mBgHandler.postDelayed(runnable, MIN_FGS_TIME_MS + - (currTime - entry.getSbn().getPostTime())); + } + } + + return extendLife; + } + + @Override + public void cancelLifetimeExtension(NotificationEntry entry) { + if (mEndRunnables.containsKey(entry.getKey())) { + Runnable endRunnable = mEndRunnables.remove(entry.getKey()); + mBgHandler.removeCallbacks(endRunnable); + } + } + }; + + /** + * Adds appOps to incoming and updating notifications + */ + private NotifCollectionListener mNotifCollectionListener = new NotifCollectionListener() { + @Override + public void onEntryAdded(NotificationEntry entry) { + tagForeground(entry); + } + + @Override + public void onEntryUpdated(NotificationEntry entry) { + tagForeground(entry); + } + + private void tagForeground(NotificationEntry entry) { + final StatusBarNotification sbn = entry.getSbn(); + // note: requires that the ForegroundServiceController is updating their appOps first + ArraySet activeOps = mForegroundServiceController.getAppOps(sbn.getUserId(), + sbn.getPackageName()); + if (activeOps != null) { + synchronized (entry.mActiveAppOps) { + entry.mActiveAppOps.clear(); + entry.mActiveAppOps.addAll(activeOps); + } + } + } + }; + + /** + * Update the appOp for the posted notification associated with the current foreground service + * @param code code for appOp to add/remove + * @param uid of user the notification is sent to + * @param packageName package that created the notification + * @param active whether the appOpCode is active or not + */ + private void onAppOpsChanged(int code, int uid, String packageName, boolean active) { + int userId = UserHandle.getUserId(uid); + + // Update appOp if there's an associated posted notification: + final String foregroundKey = mForegroundServiceController.getStandardLayoutKey(userId, + packageName); + if (foregroundKey != null) { + final NotificationEntry entry = findNotificationEntryWithKey(foregroundKey); + if (entry != null + && uid == entry.getSbn().getUid() + && packageName.equals(entry.getSbn().getPackageName())) { + boolean changed; + synchronized (entry.mActiveAppOps) { + if (active) { + changed = entry.mActiveAppOps.add(code); + } else { + changed = entry.mActiveAppOps.remove(code); + } + } + if (changed) { + mMainHandler.post(mNotifFilter::invalidateList); + } + } + } + } + + private NotificationEntry findNotificationEntryWithKey(String key) { + for (NotificationEntry entry : mNotifCollection.getNotifs()) { + if (entry.getKey().equals(key)) { + return entry; + } + } + return null; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.java index 0d53a760dc96e..132471933bb27 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.java @@ -44,8 +44,15 @@ public class NotifCoordinators implements Dumpable { * Creates all the coordinators. */ @Inject - public NotifCoordinators(KeyguardCoordinator keyguardNotificationCoordinator) { - mCoordinators.add(keyguardNotificationCoordinator); + public NotifCoordinators( + KeyguardCoordinator keyguardCoordinator, + RankingCoordinator rankingCoordinator, + ForegroundCoordinator foregroundCoordinator, + DeviceProvisionedCoordinator deviceProvisionedCoordinator) { + mCoordinators.add(keyguardCoordinator); + mCoordinators.add(rankingCoordinator); + mCoordinators.add(foregroundCoordinator); + mCoordinators.add(deviceProvisionedCoordinator); // TODO: add new Coordinators here! (b/145134683, b/112656837) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java new file mode 100644 index 0000000000000..c390f96f004df --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2019 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.statusbar.notification.collection.coordinator; + +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.statusbar.notification.collection.NotifCollection; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.collection.listbuilder.NotifListBuilder; +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter; + +import javax.inject.Inject; +import javax.inject.Singleton; + +/** + * Filters out NotificationEntries based on its Ranking. + */ +@Singleton +public class RankingCoordinator implements Coordinator { + private static final String TAG = "RankingNotificationCoordinator"; + + private final StatusBarStateController mStatusBarStateController; + + @Inject + public RankingCoordinator(StatusBarStateController statusBarStateController) { + mStatusBarStateController = statusBarStateController; + } + + @Override + public void attach(NotifCollection notifCollection, NotifListBuilder notifListBuilder) { + mStatusBarStateController.addCallback(mStatusBarStateCallback); + + notifListBuilder.addFilter(mNotifFilter); + } + + /** + * Checks whether to filter out the given notification based the notification's Ranking object. + * NotifListBuilder invalidates the notification list each time the ranking is updated, + * so we don't need to explicitly invalidate this filter on ranking update. + */ + protected final NotifFilter mNotifFilter = new NotifFilter(TAG) { + @Override + public boolean shouldFilterOut(NotificationEntry entry, long now) { + // App suspended from Ranking + if (entry.getRanking().isSuspended()) { + return true; + } + + // Dozing + DND Settings from Ranking object + if (mStatusBarStateController.isDozing() && entry.shouldSuppressAmbient()) { + return true; + } + + if (!mStatusBarStateController.isDozing() && entry.shouldSuppressNotificationList()) { + return true; + } + + return false; + } + }; + + + private final StatusBarStateController.StateListener mStatusBarStateCallback = + new StatusBarStateController.StateListener() { + @Override + public void onDozingChanged(boolean isDozing) { + mNotifFilter.invalidateList(); + } + }; +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java index 0990e2274d8a0..4cb54728a64c5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java @@ -49,6 +49,7 @@ import com.android.systemui.appops.AppOpsController; import com.android.systemui.statusbar.NotificationEntryBuilder; import com.android.systemui.statusbar.notification.NotificationEntryListener; import com.android.systemui.statusbar.notification.NotificationEntryManager; +import com.android.systemui.statusbar.notification.collection.NotifCollection; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import junit.framework.Assert; @@ -70,6 +71,7 @@ public class ForegroundServiceControllerTest extends SysuiTestCase { @Mock private NotificationEntryManager mEntryManager; @Mock private AppOpsController mAppOpsController; @Mock private Handler mMainHandler; + @Mock private NotifCollection mNotifCollection; @Before public void setUp() throws Exception { @@ -79,7 +81,7 @@ public class ForegroundServiceControllerTest extends SysuiTestCase { MockitoAnnotations.initMocks(this); mFsc = new ForegroundServiceController(mEntryManager, mAppOpsController, mMainHandler); mListener = new ForegroundServiceNotificationListener( - mContext, mFsc, mEntryManager); + mContext, mFsc, mEntryManager, mNotifCollection); ArgumentCaptor entryListenerCaptor = ArgumentCaptor.forClass(NotificationEntryListener.class); verify(mEntryManager).addNotificationEntryListener(