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

Commit 0da673f0 authored by Daniel Sandler's avatar Daniel Sandler
Browse files

Notifications may now be disabled on a per-package basis.

When a package's ability to post notifications is disabled,
all outstanding notifications from that package are
immediately canceled, and the score of any future
notification from that package is set so low that the
notification manager won't even send it to the status bar.

No UI for this yet, but you can try it out:

  adb shell service call notification 8 s16 $PKG i32 (1|0)

Bug: 5547401

Change-Id: Ieccac5746b40f60debd902a45d1dedbc91dcdc89
parent f7a1956b
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -34,5 +34,8 @@ interface INotificationManager
    void cancelToast(String pkg, ITransientNotification callback);
    void enqueueNotificationWithTag(String pkg, String tag, int id, in Notification notification, inout int[] idReceived);
    void cancelNotificationWithTag(String pkg, String tag, int id);

    void setNotificationsEnabledForPackage(String pkg, boolean enabled);
    boolean areNotificationsEnabledForPackage(String pkg);
}
+0 −29
Original line number Diff line number Diff line
@@ -218,11 +218,6 @@ public class PhoneStatusBar extends BaseStatusBar {

    private int mNavigationIconHints = 0;

    // TODO(dsandler): codify this stuff in NotificationManager's header somewhere
    private int mDisplayMinScore             = Notification.PRIORITY_LOW * 10;
    private int mIntruderMinScore            = Notification.PRIORITY_HIGH * 10;
    private int mIntruderInImmersiveMinScore = Notification.PRIORITY_HIGH * 10 + 5;
    
    private class ExpandedDialog extends Dialog {
        ExpandedDialog(Context context) {
            super(context, com.android.internal.R.style.Theme_Translucent_NoTitleBar);
@@ -2132,30 +2127,6 @@ public class PhoneStatusBar extends BaseStatusBar {
        vib.vibrate(250);
    }

    public int getScoreThreshold() {
        return mDisplayMinScore;
    }

    public void setScoreThreshold(int score) {
        // XXX HAX
        if (mDisplayMinScore != score) {
            this.mDisplayMinScore = score;
            applyScoreThreshold();
        }
    }
    
    private void applyScoreThreshold() {
        int N = mNotificationData.size();
        for (int i=0; i<N; i++) {
            NotificationData.Entry entry = mNotificationData.get(i);
            int vis = (entry.notification.score < mDisplayMinScore)
                ? View.GONE
                : View.VISIBLE;
            entry.row.setVisibility(vis);
            entry.icon.setVisibility(vis);
        }
    }

    Runnable mStartTracing = new Runnable() {
        public void run() {
            vibrate();
+215 −16
Original line number Diff line number Diff line
@@ -16,7 +16,9 @@

package com.android.server;

import com.android.internal.os.AtomicFile;
import com.android.internal.statusbar.StatusBarNotification;
import com.android.internal.util.FastXmlSerializer;

import android.app.ActivityManagerNative;
import android.app.IActivityManager;
@@ -37,9 +39,10 @@ import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.media.AudioManager;
import android.net.NetworkPolicy;
import android.net.NetworkTemplate;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
@@ -53,14 +56,36 @@ import android.text.TextUtils;
import android.util.EventLog;
import android.util.Log;
import android.util.Slog;
import android.util.Xml;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.widget.Toast;

import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;

import libcore.io.IoUtils;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;

import static android.net.NetworkPolicyManager.POLICY_NONE;
import static com.android.server.net.NetworkPolicyManagerService.XmlUtils.writeBooleanAttribute;
import static com.android.server.net.NetworkPolicyManagerService.XmlUtils.writeIntAttribute;
import static com.android.server.net.NetworkPolicyManagerService.XmlUtils.writeLongAttribute;
import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
import static org.xmlpull.v1.XmlPullParser.END_TAG;
import static org.xmlpull.v1.XmlPullParser.START_TAG;


/** {@hide} */
public class NotificationManagerService extends INotificationManager.Stub
@@ -81,6 +106,13 @@ public class NotificationManagerService extends INotificationManager.Stub
    private static final int DEFAULT_STREAM_TYPE = AudioManager.STREAM_NOTIFICATION;
    private static final boolean SCORE_ONGOING_HIGHER = false;

    private static final int JUNK_SCORE = -1000;
    private static final int NOTIFICATION_PRIORITY_MULTIPLIER = 10;
    private static final int SCORE_DISPLAY_THRESHOLD = Notification.PRIORITY_MIN * NOTIFICATION_PRIORITY_MULTIPLIER;

    private static final boolean ENABLE_BLOCKED_NOTIFICATIONS = true;
    private static final boolean ENABLE_BLOCKED_TOASTS = true;

    final Context mContext;
    final IActivityManager mAm;
    final IBinder mForegroundToken = new Binder();
@@ -115,6 +147,144 @@ public class NotificationManagerService extends INotificationManager.Stub
    private ArrayList<NotificationRecord> mLights = new ArrayList<NotificationRecord>();
    private NotificationRecord mLedNotification;

    // Notification control database. For now just contains disabled packages.
    private AtomicFile mPolicyFile;
    private HashSet<String> mBlockedPackages = new HashSet<String>();

    private static final int DB_VERSION = 1;

    private static final String TAG_BODY = "notification-policy";
    private static final String ATTR_VERSION = "version";

    private static final String TAG_BLOCKED_PKGS = "blocked-packages";
    private static final String TAG_PACKAGE = "package";
    private static final String ATTR_NAME = "name";

    private void loadBlockDb() {
        synchronized(mBlockedPackages) {
            if (mPolicyFile == null) {
                File dir = new File("/data/system");
                mPolicyFile = new AtomicFile(new File(dir, "notification_policy.xml"));

                mBlockedPackages.clear();

                FileInputStream infile = null;
                try {
                    infile = mPolicyFile.openRead();
                    final XmlPullParser parser = Xml.newPullParser();
                    parser.setInput(infile, null);

                    int type;
                    String tag;
                    int version = DB_VERSION;
                    while ((type = parser.next()) != END_DOCUMENT) {
                        tag = parser.getName();
                        if (type == START_TAG) {
                            if (TAG_BODY.equals(tag)) {
                                version = Integer.parseInt(parser.getAttributeValue(null, ATTR_VERSION));
                            } else if (TAG_BLOCKED_PKGS.equals(tag)) {
                                while ((type = parser.next()) != END_DOCUMENT) {
                                    tag = parser.getName();
                                    if (TAG_PACKAGE.equals(tag)) {
                                        mBlockedPackages.add(parser.getAttributeValue(null, ATTR_NAME));
                                    } else if (TAG_BLOCKED_PKGS.equals(tag) && type == END_TAG) {
                                        break;
                                    }
                                }
                            }
                        }
                    }
                } catch (FileNotFoundException e) {
                    // No data yet
                } catch (IOException e) {
                    Log.wtf(TAG, "Unable to read blocked notifications database", e);
                } catch (NumberFormatException e) {
                    Log.wtf(TAG, "Unable to parse blocked notifications database", e);
                } catch (XmlPullParserException e) {
                    Log.wtf(TAG, "Unable to parse blocked notifications database", e);
                } finally {
                    IoUtils.closeQuietly(infile);
                }
            }
        }
    }

    private void writeBlockDb() {
        synchronized(mBlockedPackages) {
            FileOutputStream outfile = null;
            try {
                outfile = mPolicyFile.startWrite();

                XmlSerializer out = new FastXmlSerializer();
                out.setOutput(outfile, "utf-8");

                out.startDocument(null, true);

                out.startTag(null, TAG_BODY); {
                    out.attribute(null, ATTR_VERSION, String.valueOf(DB_VERSION));
                    out.startTag(null, TAG_BLOCKED_PKGS); {
                        // write all known network policies
                        for (String pkg : mBlockedPackages) {
                            out.startTag(null, TAG_PACKAGE); {
                                out.attribute(null, ATTR_NAME, pkg);
                            } out.endTag(null, TAG_PACKAGE);
                        }
                    } out.endTag(null, TAG_BLOCKED_PKGS);
                } out.endTag(null, TAG_BODY);

                out.endDocument();

                mPolicyFile.finishWrite(outfile);
            } catch (IOException e) {
                if (outfile != null) {
                    mPolicyFile.failWrite(outfile);
                }
            }
        }
    }

    public boolean areNotificationsEnabledForPackage(String pkg) {
        checkCallerIsSystem();
        return areNotificationsEnabledForPackageInt(pkg);
    }

    // Unchecked. Not exposed via Binder, but can be called in the course of enqueue*().
    private boolean areNotificationsEnabledForPackageInt(String pkg) {
        final boolean enabled = !mBlockedPackages.contains(pkg);
        if (DBG) {
            Slog.v(TAG, "notifications are " + (enabled?"en":"dis") + "abled for " + pkg);
        }
        return enabled;
    }

    public void setNotificationsEnabledForPackage(String pkg, boolean enabled) {
        checkCallerIsSystem();
        if (DBG) {
            Slog.v(TAG, (enabled?"en":"dis") + "abling notifications for " + pkg);
        }
        if (enabled) {
            mBlockedPackages.remove(pkg);
        } else {
            mBlockedPackages.add(pkg);

            // Now, cancel any outstanding notifications that are part of a just-disabled app
            if (ENABLE_BLOCKED_NOTIFICATIONS) {
                synchronized (mNotificationList) {
                    final int N = mNotificationList.size();
                    for (int i=0; i<N; i++) {
                        final NotificationRecord r = mNotificationList.get(i);
                        if (r.pkg.equals(pkg)) {
                            cancelNotificationLocked(r, false);
                        }
                    }
                }
            }
            // Don't bother canceling toasts, they'll go away soon enough.
        }
        writeBlockDb();
    }


    private static String idDebugString(Context baseContext, String packageName, int id) {
        Context c = null;

@@ -405,6 +575,8 @@ public class NotificationManagerService extends INotificationManager.Stub
        mToastQueue = new ArrayList<ToastRecord>();
        mHandler = new WorkerHandler();

        loadBlockDb();

        mStatusBar = statusBar;
        statusBar.setNotificationCallbacks(mNotificationCallbacks);

@@ -465,6 +637,13 @@ public class NotificationManagerService extends INotificationManager.Stub
            return ;
        }

        final boolean isSystemToast = ("android".equals(pkg));

        if (ENABLE_BLOCKED_TOASTS && !isSystemToast && !areNotificationsEnabledForPackageInt(pkg)) {
            Slog.e(TAG, "Suppressing toast from package " + pkg + " by user request.");
            return;
        }

        synchronized (mToastQueue) {
            int callingPid = Binder.getCallingPid();
            long callingId = Binder.clearCallingIdentity();
@@ -479,7 +658,7 @@ public class NotificationManagerService extends INotificationManager.Stub
                } else {
                    // Limit the number of toasts that any given package except the android
                    // package can enqueue.  Prevents DOS attacks and deals with leaks.
                    if (!"android".equals(pkg)) {
                    if (!isSystemToast) {
                        int count = 0;
                        final int N = mToastQueue.size();
                        for (int i=0; i<N; i++) {
@@ -675,11 +854,15 @@ public class NotificationManagerService extends INotificationManager.Stub
    public void enqueueNotificationInternal(String pkg, int callingUid, int callingPid,
            String tag, int id, Notification notification, int[] idOut)
    {
        checkIncomingCall(pkg);
        if (DBG) {
            Slog.v(TAG, "enqueueNotificationInternal: pkg=" + pkg + " id=" + id + " notification=" + notification);
        }
        checkCallerIsSystemOrSameApp(pkg);
        final boolean isSystemNotification = ("android".equals(pkg));

        // Limit the number of notifications that any given package except the android
        // package can enqueue.  Prevents DOS attacks and deals with leaks.
        if (!"android".equals(pkg)) {
        if (!isSystemNotification) {
            synchronized (mNotificationList) {
                int count = 0;
                final int N = mNotificationList.size();
@@ -728,17 +911,25 @@ public class NotificationManagerService extends INotificationManager.Stub
        }

        // 1. initial score: buckets of 10, around the app 
        int score = notification.priority * 10; //[-20..20]
        int score = notification.priority * NOTIFICATION_PRIORITY_MULTIPLIER; //[-20..20]

        // 2. Consult oracles (external heuristics)
        // TODO(dsandler): oracles
        // 2. Consult external heuristics (TBD)

        // 3. Apply local heuristics & overrides
        // 3. Apply local rules

        // blocked apps
        // TODO(dsandler): add block db
        if (pkg.startsWith("com.test.spammer.")) {
            score = -1000;
        if (ENABLE_BLOCKED_NOTIFICATIONS && !isSystemNotification && !areNotificationsEnabledForPackageInt(pkg)) {
            score = JUNK_SCORE;
            Slog.e(TAG, "Suppressing notification from package " + pkg + " by user request.");
        }

        if (DBG) {
            Slog.v(TAG, "Assigned score=" + score + " to " + notification);
        }

        if (score < SCORE_DISPLAY_THRESHOLD) {
            // Notification will be blocked because the score is too low.
            return;
        }

        synchronized (mNotificationList) {
@@ -1030,7 +1221,7 @@ public class NotificationManagerService extends INotificationManager.Stub
    }

    public void cancelNotificationWithTag(String pkg, String tag, int id) {
        checkIncomingCall(pkg);
        checkCallerIsSystemOrSameApp(pkg);
        // Don't allow client applications to cancel foreground service notis.
        cancelNotification(pkg, tag, id, 0,
                Binder.getCallingUid() == Process.SYSTEM_UID
@@ -1038,14 +1229,22 @@ public class NotificationManagerService extends INotificationManager.Stub
    }

    public void cancelAllNotifications(String pkg) {
        checkIncomingCall(pkg);
        checkCallerIsSystemOrSameApp(pkg);

        // Calling from user space, don't allow the canceling of actively
        // running foreground services.
        cancelAllNotificationsInt(pkg, 0, Notification.FLAG_FOREGROUND_SERVICE, true);
    }

    void checkIncomingCall(String pkg) {
    void checkCallerIsSystem() {
        int uid = Binder.getCallingUid();
        if (uid == Process.SYSTEM_UID || uid == 0) {
            return;
        }
        throw new SecurityException("Disallowed call for uid " + uid);
    }

    void checkCallerIsSystemOrSameApp(String pkg) {
        int uid = Binder.getCallingUid();
        if (uid == Process.SYSTEM_UID || uid == 0) {
            return;