blob: 3ef2aea43a0d8eba257c8b07aa8fc033fe1b9be8 [file] [log] [blame]
/*
* Copyright (C) 2016 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 android.ext.services.notification;
import static android.service.notification.NotificationListenerService.Ranking.IMPORTANCE_UNSPECIFIED;
import android.os.Bundle;
import android.service.notification.Adjustment;
import android.service.notification.NotificationRankerService;
import android.service.notification.StatusBarNotification;
import android.util.Log;
import android.util.Slog;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import android.ext.services.R;
/**
* Class that provides an updatable ranker module for the notification manager..
*/
public final class Ranker extends NotificationRankerService {
private static final String TAG = "RocketRanker";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private static final int AUTOBUNDLE_AT_COUNT = 4;
private static final String AUTOBUNDLE_KEY = "ranker_bundle";
// Map of package : notification keys. Only contains notifications that are not bundled
// by the app (aka no group or sort key).
Map<String, LinkedHashSet<String>> mUnbundledNotifications;
@Override
public Adjustment onNotificationEnqueued(StatusBarNotification sbn, int importance,
boolean user) {
if (DEBUG) Log.i(TAG, "ENQUEUED " + sbn.getKey());
return null;
}
@Override
public void onNotificationPosted(StatusBarNotification sbn) {
if (DEBUG) Log.i(TAG, "POSTED " + sbn.getKey());
try {
List<String> notificationsToBundle = new ArrayList<>();
if (!sbn.isGroup()) {
// Not grouped by the app, add to the list of notifications for the app;
// send bundling update if app exceeds the autobundling limit.
synchronized (mUnbundledNotifications) {
LinkedHashSet<String> notificationsForPackage
= mUnbundledNotifications.get(sbn.getPackageName());
if (notificationsForPackage == null) {
notificationsForPackage = new LinkedHashSet<>();
}
if (notificationsForPackage.contains(sbn.getKey())) {
return;
}
notificationsForPackage.add(sbn.getKey());
mUnbundledNotifications.put(sbn.getPackageName(), notificationsForPackage);
if (notificationsForPackage.size() >= AUTOBUNDLE_AT_COUNT) {
// Autobundle all but the most recently posted (not updated) notification.
int count = 0;
for (String key : notificationsForPackage) {
if (count < notificationsForPackage.size() - 1) {
notificationsToBundle.add(key);
}
count++;
}
}
}
if (notificationsToBundle.size() > 0) {
adjustAutobundlingSummary(sbn.getPackageName(), notificationsToBundle.get(0),
true);
adjustNotificationBundling(sbn.getPackageName(), notificationsToBundle, true);
}
} else {
// Grouped, but not by us. Send updates to unautobundle, if we bundled it.
maybeUnbundle(sbn, false);
}
} catch (Exception e) {
Slog.e(TAG, "Failure processing new notification", e);
}
}
@Override
public void onNotificationRemoved(StatusBarNotification sbn) {
try {
maybeUnbundle(sbn, true);
} catch (Exception e) {
Slog.e(TAG, "Error processing canceled notification", e);
}
}
/**
* Un-autobundles notifications that are now grouped by the app. Additionally cancels
* autobundling if the status change of this notification resulted in the loose notification
* count being under the limit.
*/
private void maybeUnbundle(StatusBarNotification sbn, boolean notificationGone) {
List<String> notificationsToUnAutobundle = new ArrayList<>();
boolean removeSummary = false;
synchronized (mUnbundledNotifications) {
LinkedHashSet<String> notificationsForPackage
= mUnbundledNotifications.get(sbn.getPackageName());
if (notificationsForPackage == null || notificationsForPackage.size() == 0) {
return;
}
if (notificationsForPackage.remove(sbn.getKey())) {
if (!notificationGone) {
// Add the current notification to the unbundling list if it still exists.
notificationsToUnAutobundle.add(sbn.getKey());
}
// If the status change of this notification has brought the number of loose
// notifications back below the limit, remove the summary and un-autobundle.
if (notificationsForPackage.size() == AUTOBUNDLE_AT_COUNT - 1) {
removeSummary = true;
for (String key : notificationsForPackage) {
notificationsToUnAutobundle.add(key);
}
}
}
}
if (notificationsToUnAutobundle.size() > 0) {
if (removeSummary) {
adjustAutobundlingSummary(sbn.getPackageName(), null, false);
}
adjustNotificationBundling(sbn.getPackageName(), notificationsToUnAutobundle, false);
}
}
@Override
public void onListenerConnected() {
if (DEBUG) Log.i(TAG, "CONNECTED");
mUnbundledNotifications = new HashMap<>();
for (StatusBarNotification sbn : getActiveNotifications()) {
onNotificationPosted(sbn);
}
}
private void adjustAutobundlingSummary(String packageName, String key, boolean summaryNeeded) {
Bundle signals = new Bundle();
if (summaryNeeded) {
signals.putBoolean(Adjustment.NEEDS_AUTOGROUPING_KEY, true);
signals.putString(Adjustment.GROUP_KEY_OVERRIDE_KEY, AUTOBUNDLE_KEY);
} else {
signals.putBoolean(Adjustment.NEEDS_AUTOGROUPING_KEY, false);
}
Adjustment adjustment = new Adjustment(packageName, key, IMPORTANCE_UNSPECIFIED, signals,
getContext().getString(R.string.notification_ranker_autobundle_explanation), null);
if (DEBUG) {
Log.i(TAG, "Summary update for: " + packageName + " "
+ (summaryNeeded ? "adding" : "removing"));
}
try {
adjustNotification(adjustment);
} catch (Exception e) {
Slog.e(TAG, "Adjustment failed", e);
}
}
private void adjustNotificationBundling(String packageName, List<String> keys, boolean bundle) {
List<Adjustment> adjustments = new ArrayList<>();
for (String key : keys) {
adjustments.add(createBundlingAdjustment(packageName, key, bundle));
if (DEBUG) Log.i(TAG, "Sending bundling adjustment for: " + key);
}
try {
adjustNotifications(adjustments);
} catch (Exception e) {
Slog.e(TAG, "Adjustments failed", e);
}
}
private Adjustment createBundlingAdjustment(String packageName, String key, boolean bundle) {
Bundle signals = new Bundle();
if (bundle) {
signals.putString(Adjustment.GROUP_KEY_OVERRIDE_KEY, AUTOBUNDLE_KEY);
} else {
signals.putString(Adjustment.GROUP_KEY_OVERRIDE_KEY, null);
}
return new Adjustment(packageName, key, IMPORTANCE_UNSPECIFIED, signals,
getContext().getString(R.string.notification_ranker_autobundle_explanation), null);
}
}