Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6641ac54 authored by Julia Reynolds's avatar Julia Reynolds Committed by Android (Google) Code Review
Browse files

Merge "The quietening round 3: aging"

parents a2d2c48a 6a63d1bf
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package android.service.notification;

import android.app.NotificationChannel;
import android.app.NotificationChannelGroup;
import android.content.pm.ParceledListSlice;
import android.os.UserHandle;
import android.service.notification.NotificationStats;
import android.service.notification.IStatusBarNotificationHolder;
@@ -45,4 +46,5 @@ oneway interface INotificationListener
    // assistants only
    void onNotificationEnqueuedWithChannel(in IStatusBarNotificationHolder notificationHolder, in NotificationChannel channel);
    void onNotificationSnoozedUntilContext(in IStatusBarNotificationHolder notificationHolder, String snoozeCriterionId);
    void onNotificationsSeen(in List<String> keys);
}
+24 −0
Original line number Diff line number Diff line
@@ -148,6 +148,14 @@ public abstract class NotificationAssistantService extends NotificationListenerS
        onNotificationRemoved(sbn, rankingMap, reason);
    }

    /**
     * Implement this to know when a user has seen notifications, as triggered by
     * {@link #setNotificationsShown(String[])}.
     */
    public void onNotificationsSeen(List<String> keys) {

    }

    /**
     * Updates a notification.  N.B. this won’t cause
     * an existing notification to alert, but might allow a future update to
@@ -236,11 +244,20 @@ public abstract class NotificationAssistantService extends NotificationListenerS
            mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_SNOOZED,
                    args).sendToTarget();
        }

        @Override
        public void onNotificationsSeen(List<String> keys) {
            SomeArgs args = SomeArgs.obtain();
            args.arg1 = keys;
            mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATIONS_SEEN,
                    args).sendToTarget();
        }
    }

    private final class MyHandler extends Handler {
        public static final int MSG_ON_NOTIFICATION_ENQUEUED = 1;
        public static final int MSG_ON_NOTIFICATION_SNOOZED = 2;
        public static final int MSG_ON_NOTIFICATIONS_SEEN = 3;

        public MyHandler(Looper looper) {
            super(looper, null, false);
@@ -275,6 +292,13 @@ public abstract class NotificationAssistantService extends NotificationListenerS
                    onNotificationSnoozedUntilContext(sbn, snoozeCriterionId);
                    break;
                }
                case MSG_ON_NOTIFICATIONS_SEEN: {
                    SomeArgs args = (SomeArgs) msg.obj;
                    List<String> keys = (List<String>) args.arg1;
                    args.recycle();
                    onNotificationsSeen(keys);
                    break;
                }
            }
        }
    }
+6 −0
Original line number Diff line number Diff line
@@ -1331,6 +1331,12 @@ public abstract class NotificationListenerService extends Service {
            // no-op in the listener
        }

        @Override
        public void onNotificationsSeen(List<String> keys)
                throws RemoteException {
            // no-op in the listener
        }

        @Override
        public void onNotificationSnoozedUntilContext(
                IStatusBarNotificationHolder notificationHolder, String snoozeCriterionId)
+172 −0
Original line number Diff line number Diff line
/**
 * Copyright (C) 2018 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 android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.ext.services.notification.NotificationCategorizer.Category;
import android.net.Uri;
import android.util.ArraySet;
import android.util.Slog;

import java.util.Set;

public class AgingHelper {
    private final static String TAG = "AgingHelper";
    private final boolean DEBUG = false;

    private static final String AGING_ACTION = AgingHelper.class.getSimpleName() + ".EVALUATE";
    private static final int REQUEST_CODE_AGING = 1;
    private static final String AGING_SCHEME = "aging";
    private static final String EXTRA_KEY = "key";
    private static final String EXTRA_CATEGORY = "category";

    private static final int HOUR_MS = 1000 * 60 * 60;
    private static final int TWO_HOURS_MS = 2 * HOUR_MS;

    private Context mContext;
    private NotificationCategorizer mNotificationCategorizer;
    private AlarmManager mAm;
    private Callback mCallback;

    // The set of keys we've scheduled alarms for
    private Set<String> mAging = new ArraySet<>();

    public AgingHelper(Context context, NotificationCategorizer categorizer, Callback callback) {
        mNotificationCategorizer = categorizer;
        mContext = context;
        mAm = mContext.getSystemService(AlarmManager.class);
        mCallback = callback;

        IntentFilter filter = new IntentFilter(AGING_ACTION);
        filter.addDataScheme(AGING_SCHEME);
        mContext.registerReceiver(mBroadcastReceiver, filter);
    }

    // NAS lifecycle methods

    public void onNotificationSeen(NotificationEntry entry) {
        // user has strong opinions about this notification. we can't down rank it, so don't bother.
        if (entry.getChannel().isImportanceLocked()) {
            return;
        }

        @Category int category = mNotificationCategorizer.getCategory(entry);

        // already very low
        if (category == NotificationCategorizer.CATEGORY_MIN) {
            return;
        }

        if (entry.hasSeen()) {
            if (category == NotificationCategorizer.CATEGORY_ONGOING
                    || category > NotificationCategorizer.CATEGORY_REMINDER) {
                scheduleAging(entry.getSbn().getKey(), category, TWO_HOURS_MS);
            } else {
                scheduleAging(entry.getSbn().getKey(), category, HOUR_MS);
            }

            mAging.add(entry.getSbn().getKey());
        }
    }

    public void onNotificationPosted(NotificationEntry entry) {
        cancelAging(entry.getSbn().getKey());
    }

    public void onNotificationRemoved(String key) {
        cancelAging(key);
    }

    public void onDestroy() {
        mContext.unregisterReceiver(mBroadcastReceiver);
    }

    // Aging

    private void scheduleAging(String key, @Category int category, long duration) {
        if (mAging.contains(key)) {
            // already scheduled. Don't reset aging just because the user saw the noti again.
            return;
        }
        final PendingIntent pi = createPendingIntent(key, category);
        long time = System.currentTimeMillis() + duration;
        if (DEBUG) Slog.d(TAG, "Scheduling evaluate for " + key + " in ms: " + duration);
        mAm.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, time, pi);
    }

    private void cancelAging(String key) {
        final PendingIntent pi = createPendingIntent(key);
        mAm.cancel(pi);
        mAging.remove(key);
    }

    private Intent createBaseIntent(String key) {
        return new Intent(AGING_ACTION)
                .setData(new Uri.Builder().scheme(AGING_SCHEME).appendPath(key).build());
    }

    private Intent createAgingIntent(String key, @Category int category) {
        Intent intent = createBaseIntent(key);
        intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
                .putExtra(EXTRA_CATEGORY, category)
                .putExtra(EXTRA_KEY, key);
        return intent;
    }

    private PendingIntent createPendingIntent(String key, @Category int category) {
        return PendingIntent.getBroadcast(mContext,
                REQUEST_CODE_AGING,
                createAgingIntent(key, category),
                PendingIntent.FLAG_UPDATE_CURRENT);
    }

    private PendingIntent createPendingIntent(String key) {
        return PendingIntent.getBroadcast(mContext,
                REQUEST_CODE_AGING,
                createBaseIntent(key),
                PendingIntent.FLAG_UPDATE_CURRENT);
    }

    private void demote(String key, @Category int category) {
        int newImportance = IMPORTANCE_MIN;
        // TODO: Change "aged" importance based on category
        mCallback.sendAdjustment(key, newImportance);
    }

    protected interface Callback {
        void sendAdjustment(String key, int newImportance);
    }

    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (DEBUG) {
                Slog.d(TAG, "Reposting notification");
            }
            if (AGING_ACTION.equals(intent.getAction())) {
                demote(intent.getStringExtra(EXTRA_KEY), intent.getIntExtra(EXTRA_CATEGORY,
                        NotificationCategorizer.CATEGORY_EVERYTHING_ELSE));
            }
        }
    };
}
+77 −7
Original line number Diff line number Diff line
@@ -18,23 +18,28 @@ package android.ext.services.notification;

import static android.app.NotificationManager.IMPORTANCE_LOW;
import static android.app.NotificationManager.IMPORTANCE_MIN;
import static android.service.notification.Adjustment.KEY_IMPORTANCE;
import static android.service.notification.NotificationListenerService.Ranking
        .USER_SENTIMENT_NEGATIVE;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityThread;
import android.app.AlarmManager;
import android.app.INotificationManager;
import android.app.Notification;
import android.app.NotificationChannel;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.IPackageManager;
import android.database.ContentObserver;
import android.ext.services.notification.AgingHelper.Callback;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.UserHandle;
import android.os.storage.StorageManager;
import android.provider.Settings;
import android.service.notification.Adjustment;
@@ -64,6 +69,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
@@ -89,15 +95,17 @@ public class Assistant extends NotificationAssistantService {
    private int mStreakLimit;
    private SmartActionsHelper mSmartActionsHelper;
    private NotificationCategorizer mNotificationCategorizer;
    private AgingHelper mAgingHelper;

    // key : impressions tracker
    // TODO: prune deleted channels and apps
    final ArrayMap<String, ChannelImpressions> mkeyToImpressions = new ArrayMap<>();
    // SBN key : channel id
    ArrayMap<String, String> mLiveNotifications = new ArrayMap<>();
    private final ArrayMap<String, ChannelImpressions> mkeyToImpressions = new ArrayMap<>();
    // SBN key : entry
    protected ArrayMap<String, NotificationEntry> mLiveNotifications = new ArrayMap<>();

    private Ranking mFakeRanking = null;
    private AtomicFile mFile = null;
    private IPackageManager mPackageManager;
    protected SettingsObserver mSettingsObserver;

    public Assistant() {
@@ -108,9 +116,13 @@ public class Assistant extends NotificationAssistantService {
        super.onCreate();
        // Contexts are correctly hooked up by the creation step, which is required for the observer
        // to be hooked up/initialized.
        mPackageManager = ActivityThread.getPackageManager();
        mSettingsObserver = new SettingsObserver(mHandler);
        mSmartActionsHelper = new SmartActionsHelper();
        mNotificationCategorizer = new NotificationCategorizer();
        mAgingHelper = new AgingHelper(getContext(),
                mNotificationCategorizer,
                new AgingCallback());
    }

    private void loadFile() {
@@ -157,7 +169,7 @@ public class Assistant extends NotificationAssistantService {
        }
    }

    private void saveFile() throws IOException {
    private void saveFile() {
        AsyncTask.execute(() -> {
            final FileOutputStream stream;
            try {
@@ -200,6 +212,9 @@ public class Assistant extends NotificationAssistantService {
    public Adjustment onNotificationEnqueued(StatusBarNotification sbn,
            NotificationChannel channel) {
        if (DEBUG) Log.i(TAG, "ENQUEUED " + sbn.getKey() + " on " + channel.getId());
        if (!isForCurrentUser(sbn)) {
            return null;
        }
        NotificationEntry entry = new NotificationEntry(
                ActivityThread.getPackageManager(), sbn, channel);
        ArrayList<Notification.Action> actions =
@@ -222,7 +237,7 @@ public class Assistant extends NotificationAssistantService {
            signals.putCharSequenceArrayList(Adjustment.KEY_SMART_REPLIES, smartReplies);
        }
        if (mNotificationCategorizer.shouldSilence(entry)) {
            signals.putInt(Adjustment.KEY_IMPORTANCE, IMPORTANCE_LOW);
            signals.putInt(KEY_IMPORTANCE, IMPORTANCE_LOW);
        }

        return new Adjustment(
@@ -237,8 +252,13 @@ public class Assistant extends NotificationAssistantService {
    public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
        if (DEBUG) Log.i(TAG, "POSTED " + sbn.getKey());
        try {
            if (!isForCurrentUser(sbn)) {
                return;
            }
            Ranking ranking = getRanking(sbn.getKey(), rankingMap);
            if (ranking != null && ranking.getChannel() != null) {
                NotificationEntry entry = new NotificationEntry(mPackageManager,
                        sbn, ranking.getChannel());
                String key = getKey(
                        sbn.getPackageName(), sbn.getUserId(), ranking.getChannel().getId());
                ChannelImpressions ci = mkeyToImpressions.getOrDefault(key,
@@ -248,7 +268,8 @@ public class Assistant extends NotificationAssistantService {
                            sbn.getPackageName(), sbn.getKey(), sbn.getUserId()));
                }
                mkeyToImpressions.put(key, ci);
                mLiveNotifications.put(sbn.getKey(), ranking.getChannel().getId());
                mLiveNotifications.put(sbn.getKey(), entry);
                mAgingHelper.onNotificationPosted(entry);
            }
        } catch (Throwable e) {
            Log.e(TAG, "Error occurred processing post", e);
@@ -259,8 +280,11 @@ public class Assistant extends NotificationAssistantService {
    public void onNotificationRemoved(StatusBarNotification sbn, RankingMap rankingMap,
            NotificationStats stats, int reason) {
        try {
            if (!isForCurrentUser(sbn)) {
                return;
            }
            boolean updatedImpressions = false;
            String channelId = mLiveNotifications.remove(sbn.getKey());
            String channelId = mLiveNotifications.remove(sbn.getKey()).getChannel().getId();
            String key = getKey(sbn.getPackageName(), sbn.getUserId(), channelId);
            synchronized (mkeyToImpressions) {
                ChannelImpressions ci = mkeyToImpressions.getOrDefault(key,
@@ -301,6 +325,22 @@ public class Assistant extends NotificationAssistantService {
            String snoozeCriterionId) {
    }

    @Override
    public void onNotificationsSeen(List<String> keys) {
        if (keys == null) {
            return;
        }

        for (String key : keys) {
            NotificationEntry entry = mLiveNotifications.get(key);

            if (entry != null) {
                entry.setSeen();
                mAgingHelper.onNotificationSeen(entry);
            }
        }
    }

    @Override
    public void onListenerConnected() {
        if (DEBUG) Log.i(TAG, "CONNECTED");
@@ -318,6 +358,17 @@ public class Assistant extends NotificationAssistantService {
        }
    }

    @Override
    public void onListenerDisconnected() {
        if (mAgingHelper != null) {
            mAgingHelper.onDestroy();
        }
    }

    private boolean isForCurrentUser(StatusBarNotification sbn) {
        return sbn != null && sbn.getUserId() == UserHandle.myUserId();
    }

    protected String getKey(String pkg, int userId, String channelId) {
        return pkg + "|" + userId + "|" + channelId;
    }
@@ -360,6 +411,11 @@ public class Assistant extends NotificationAssistantService {
        mSystemContext = context;
    }

    @VisibleForTesting
    public void setPackageManager(IPackageManager pm) {
        mPackageManager = pm;
    }

    @VisibleForTesting
    public ChannelImpressions getImpressions(String key) {
        synchronized (mkeyToImpressions) {
@@ -380,6 +436,20 @@ public class Assistant extends NotificationAssistantService {
        return impressions;
    }

    protected final class AgingCallback implements Callback {
        @Override
        public void sendAdjustment(String key, int newImportance) {
            NotificationEntry entry = mLiveNotifications.get(key);
            if (entry != null) {
                Bundle bundle = new Bundle();
                bundle.putInt(KEY_IMPORTANCE, newImportance);
                Adjustment adjustment = new Adjustment(entry.getSbn().getPackageName(), key, bundle,
                        "aging", entry.getSbn().getUserId());
                adjustNotification(adjustment);
            }
        }
    }

    /**
     * Observer for updates on blocking helper threshold values.
     */
Loading