Merge "Add Coordinators to replace NotificationFilter"

This commit is contained in:
Beverly Tai
2019-12-04 18:25:09 +00:00
committed by Android (Google) Code Review
9 changed files with 468 additions and 11 deletions

View File

@@ -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,

View File

@@ -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<Integer> activeOps = mForegroundServiceController.getAppOps(

View File

@@ -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;

View File

@@ -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.
*

View File

@@ -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();
}
};
}

View File

@@ -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<String, Runnable> 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<Integer> 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;
}
}

View File

@@ -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)
}

View File

@@ -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();
}
};
}

View File

@@ -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<NotificationEntryListener> entryListenerCaptor =
ArgumentCaptor.forClass(NotificationEntryListener.class);
verify(mEntryManager).addNotificationEntryListener(