blob: f535368bd35662c1eaad41709fda5e32406e300f [file] [log] [blame]
/**
* 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 android.ext.services.notification;
import static android.app.NotificationManager.IMPORTANCE_MIN;
import static android.service.notification.NotificationListenerService.Ranking
.USER_SENTIMENT_NEGATIVE;
import android.app.INotificationManager;
import android.content.Context;
import android.ext.services.R;
import android.os.Bundle;
import android.service.notification.Adjustment;
import android.service.notification.NotificationAssistantService;
import android.service.notification.NotificationStats;
import android.service.notification.StatusBarNotification;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Slog;
import java.util.ArrayList;
/**
* Notification assistant that provides guidance on notification channel blocking
*/
public class Assistant extends NotificationAssistantService {
private static final String TAG = "ExtAssistant";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private static final ArrayList<Integer> DISMISS_WITH_PREJUDICE = new ArrayList<>();
static {
DISMISS_WITH_PREJUDICE.add(REASON_CANCEL);
DISMISS_WITH_PREJUDICE.add(REASON_LISTENER_CANCEL);
}
// key : impressions tracker
// TODO: persist across reboots
ArrayMap<String, ChannelImpressions> mkeyToImpressions = new ArrayMap<>();
// SBN key : channel id
ArrayMap<String, String> mLiveNotifications = new ArrayMap<>();
private Ranking mFakeRanking = null;
@Override
public Adjustment onNotificationEnqueued(StatusBarNotification sbn) {
if (DEBUG) Log.i(TAG, "ENQUEUED " + sbn.getKey());
return null;
}
@Override
public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
if (DEBUG) Log.i(TAG, "POSTED " + sbn.getKey());
try {
Ranking ranking = getRanking(sbn.getKey(), rankingMap);
if (ranking != null && ranking.getChannel() != null) {
String key = getKey(
sbn.getPackageName(), sbn.getUserId(), ranking.getChannel().getId());
ChannelImpressions ci = mkeyToImpressions.getOrDefault(key,
new ChannelImpressions());
if (ranking.getImportance() > IMPORTANCE_MIN && ci.shouldTriggerBlock()) {
adjustNotification(createNegativeAdjustment(
sbn.getPackageName(), sbn.getKey(), sbn.getUserId()));
}
mkeyToImpressions.put(key, ci);
mLiveNotifications.put(sbn.getKey(), ranking.getChannel().getId());
}
} catch (Throwable e) {
Log.e(TAG, "Error occurred processing post", e);
}
}
@Override
public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap,
NotificationStats stats, int reason) {
try {
String channelId = mLiveNotifications.remove(sbn.getKey());
String key = getKey(sbn.getPackageName(), sbn.getUserId(), channelId);
ChannelImpressions ci = mkeyToImpressions.getOrDefault(key, new ChannelImpressions());
if (stats.hasSeen()) {
ci.incrementViews();
}
if (DISMISS_WITH_PREJUDICE.contains(reason)
&& !sbn.isAppGroup()
&& !sbn.getNotification().isGroupChild()
&& !stats.hasInteracted()
&& stats.getDismissalSurface() != NotificationStats.DISMISSAL_AOD
&& stats.getDismissalSurface() != NotificationStats.DISMISSAL_PEEK
&& stats.getDismissalSurface() != NotificationStats.DISMISSAL_OTHER) {
if (DEBUG) Log.i(TAG, "increment dismissals");
ci.incrementDismissals();
} else {
if (DEBUG) Slog.i(TAG, "reset streak");
ci.resetStreak();
}
mkeyToImpressions.put(key, ci);
} catch (Throwable e) {
Slog.e(TAG, "Error occurred processing removal", e);
}
}
@Override
public void onNotificationSnoozedUntilContext(StatusBarNotification sbn,
String snoozeCriterionId) {
}
@Override
public void onListenerConnected() {
if (DEBUG) Log.i(TAG, "CONNECTED");
try {
for (StatusBarNotification sbn : getActiveNotifications()) {
onNotificationPosted(sbn);
}
} catch (Throwable e) {
Log.e(TAG, "Error occurred on connection", e);
}
}
private String getKey(String pkg, int userId, String channelId) {
return pkg + "|" + userId + "|" + channelId;
}
private Ranking getRanking(String key, RankingMap rankingMap) {
if (mFakeRanking != null) {
return mFakeRanking;
}
Ranking ranking = new Ranking();
rankingMap.getRanking(key, ranking);
return ranking;
}
private Adjustment createNegativeAdjustment(String packageName, String key, int user) {
if (DEBUG) Log.d(TAG, "User probably doesn't want " + key);
Bundle signals = new Bundle();
signals.putInt(Adjustment.KEY_USER_SENTIMENT, USER_SENTIMENT_NEGATIVE);
return new Adjustment(packageName, key, signals,
getContext().getString(R.string.prompt_block_reason), user);
}
// for testing
protected void setFakeRanking(Ranking ranking) {
mFakeRanking = ranking;
}
protected void setNoMan(INotificationManager noMan) {
mNoMan = noMan;
}
protected void setContext(Context context) {
mSystemContext = context;
}
}