Skip to content

Commit 0da673f

Browse files
committed
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
1 parent f7a1956 commit 0da673f

File tree

3 files changed

+218
-45
lines changed

3 files changed

+218
-45
lines changed

core/java/android/app/INotificationManager.aidl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,8 @@ interface INotificationManager
3434
void cancelToast(String pkg, ITransientNotification callback);
3535
void enqueueNotificationWithTag(String pkg, String tag, int id, in Notification notification, inout int[] idReceived);
3636
void cancelNotificationWithTag(String pkg, String tag, int id);
37+
38+
void setNotificationsEnabledForPackage(String pkg, boolean enabled);
39+
boolean areNotificationsEnabledForPackage(String pkg);
3740
}
3841

packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -218,11 +218,6 @@ public class PhoneStatusBar extends BaseStatusBar {
218218

219219
private int mNavigationIconHints = 0;
220220

221-
// TODO(dsandler): codify this stuff in NotificationManager's header somewhere
222-
private int mDisplayMinScore = Notification.PRIORITY_LOW * 10;
223-
private int mIntruderMinScore = Notification.PRIORITY_HIGH * 10;
224-
private int mIntruderInImmersiveMinScore = Notification.PRIORITY_HIGH * 10 + 5;
225-
226221
private class ExpandedDialog extends Dialog {
227222
ExpandedDialog(Context context) {
228223
super(context, com.android.internal.R.style.Theme_Translucent_NoTitleBar);
@@ -2132,30 +2127,6 @@ void vibrate() {
21322127
vib.vibrate(250);
21332128
}
21342129

2135-
public int getScoreThreshold() {
2136-
return mDisplayMinScore;
2137-
}
2138-
2139-
public void setScoreThreshold(int score) {
2140-
// XXX HAX
2141-
if (mDisplayMinScore != score) {
2142-
this.mDisplayMinScore = score;
2143-
applyScoreThreshold();
2144-
}
2145-
}
2146-
2147-
private void applyScoreThreshold() {
2148-
int N = mNotificationData.size();
2149-
for (int i=0; i<N; i++) {
2150-
NotificationData.Entry entry = mNotificationData.get(i);
2151-
int vis = (entry.notification.score < mDisplayMinScore)
2152-
? View.GONE
2153-
: View.VISIBLE;
2154-
entry.row.setVisibility(vis);
2155-
entry.icon.setVisibility(vis);
2156-
}
2157-
}
2158-
21592130
Runnable mStartTracing = new Runnable() {
21602131
public void run() {
21612132
vibrate();

services/java/com/android/server/NotificationManagerService.java

Lines changed: 215 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616

1717
package com.android.server;
1818

19+
import com.android.internal.os.AtomicFile;
1920
import com.android.internal.statusbar.StatusBarNotification;
21+
import com.android.internal.util.FastXmlSerializer;
2022

2123
import android.app.ActivityManagerNative;
2224
import android.app.IActivityManager;
@@ -37,9 +39,10 @@
3739
import android.content.res.Resources;
3840
import android.database.ContentObserver;
3941
import android.media.AudioManager;
42+
import android.net.NetworkPolicy;
43+
import android.net.NetworkTemplate;
4044
import android.net.Uri;
4145
import android.os.Binder;
42-
import android.os.Bundle;
4346
import android.os.Handler;
4447
import android.os.IBinder;
4548
import android.os.Message;
@@ -53,14 +56,36 @@
5356
import android.util.EventLog;
5457
import android.util.Log;
5558
import android.util.Slog;
59+
import android.util.Xml;
5660
import android.view.accessibility.AccessibilityEvent;
5761
import android.view.accessibility.AccessibilityManager;
5862
import android.widget.Toast;
5963

64+
import java.io.File;
6065
import java.io.FileDescriptor;
66+
import java.io.FileInputStream;
67+
import java.io.FileNotFoundException;
68+
import java.io.FileOutputStream;
69+
import java.io.IOException;
6170
import java.io.PrintWriter;
6271
import java.util.ArrayList;
6372
import java.util.Arrays;
73+
import java.util.HashSet;
74+
75+
import libcore.io.IoUtils;
76+
77+
import org.xmlpull.v1.XmlPullParser;
78+
import org.xmlpull.v1.XmlPullParserException;
79+
import org.xmlpull.v1.XmlSerializer;
80+
81+
import static android.net.NetworkPolicyManager.POLICY_NONE;
82+
import static com.android.server.net.NetworkPolicyManagerService.XmlUtils.writeBooleanAttribute;
83+
import static com.android.server.net.NetworkPolicyManagerService.XmlUtils.writeIntAttribute;
84+
import static com.android.server.net.NetworkPolicyManagerService.XmlUtils.writeLongAttribute;
85+
import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
86+
import static org.xmlpull.v1.XmlPullParser.END_TAG;
87+
import static org.xmlpull.v1.XmlPullParser.START_TAG;
88+
6489

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

109+
private static final int JUNK_SCORE = -1000;
110+
private static final int NOTIFICATION_PRIORITY_MULTIPLIER = 10;
111+
private static final int SCORE_DISPLAY_THRESHOLD = Notification.PRIORITY_MIN * NOTIFICATION_PRIORITY_MULTIPLIER;
112+
113+
private static final boolean ENABLE_BLOCKED_NOTIFICATIONS = true;
114+
private static final boolean ENABLE_BLOCKED_TOASTS = true;
115+
84116
final Context mContext;
85117
final IActivityManager mAm;
86118
final IBinder mForegroundToken = new Binder();
@@ -115,6 +147,144 @@ public class NotificationManagerService extends INotificationManager.Stub
115147
private ArrayList<NotificationRecord> mLights = new ArrayList<NotificationRecord>();
116148
private NotificationRecord mLedNotification;
117149

150+
// Notification control database. For now just contains disabled packages.
151+
private AtomicFile mPolicyFile;
152+
private HashSet<String> mBlockedPackages = new HashSet<String>();
153+
154+
private static final int DB_VERSION = 1;
155+
156+
private static final String TAG_BODY = "notification-policy";
157+
private static final String ATTR_VERSION = "version";
158+
159+
private static final String TAG_BLOCKED_PKGS = "blocked-packages";
160+
private static final String TAG_PACKAGE = "package";
161+
private static final String ATTR_NAME = "name";
162+
163+
private void loadBlockDb() {
164+
synchronized(mBlockedPackages) {
165+
if (mPolicyFile == null) {
166+
File dir = new File("/data/system");
167+
mPolicyFile = new AtomicFile(new File(dir, "notification_policy.xml"));
168+
169+
mBlockedPackages.clear();
170+
171+
FileInputStream infile = null;
172+
try {
173+
infile = mPolicyFile.openRead();
174+
final XmlPullParser parser = Xml.newPullParser();
175+
parser.setInput(infile, null);
176+
177+
int type;
178+
String tag;
179+
int version = DB_VERSION;
180+
while ((type = parser.next()) != END_DOCUMENT) {
181+
tag = parser.getName();
182+
if (type == START_TAG) {
183+
if (TAG_BODY.equals(tag)) {
184+
version = Integer.parseInt(parser.getAttributeValue(null, ATTR_VERSION));
185+
} else if (TAG_BLOCKED_PKGS.equals(tag)) {
186+
while ((type = parser.next()) != END_DOCUMENT) {
187+
tag = parser.getName();
188+
if (TAG_PACKAGE.equals(tag)) {
189+
mBlockedPackages.add(parser.getAttributeValue(null, ATTR_NAME));
190+
} else if (TAG_BLOCKED_PKGS.equals(tag) && type == END_TAG) {
191+
break;
192+
}
193+
}
194+
}
195+
}
196+
}
197+
} catch (FileNotFoundException e) {
198+
// No data yet
199+
} catch (IOException e) {
200+
Log.wtf(TAG, "Unable to read blocked notifications database", e);
201+
} catch (NumberFormatException e) {
202+
Log.wtf(TAG, "Unable to parse blocked notifications database", e);
203+
} catch (XmlPullParserException e) {
204+
Log.wtf(TAG, "Unable to parse blocked notifications database", e);
205+
} finally {
206+
IoUtils.closeQuietly(infile);
207+
}
208+
}
209+
}
210+
}
211+
212+
private void writeBlockDb() {
213+
synchronized(mBlockedPackages) {
214+
FileOutputStream outfile = null;
215+
try {
216+
outfile = mPolicyFile.startWrite();
217+
218+
XmlSerializer out = new FastXmlSerializer();
219+
out.setOutput(outfile, "utf-8");
220+
221+
out.startDocument(null, true);
222+
223+
out.startTag(null, TAG_BODY); {
224+
out.attribute(null, ATTR_VERSION, String.valueOf(DB_VERSION));
225+
out.startTag(null, TAG_BLOCKED_PKGS); {
226+
// write all known network policies
227+
for (String pkg : mBlockedPackages) {
228+
out.startTag(null, TAG_PACKAGE); {
229+
out.attribute(null, ATTR_NAME, pkg);
230+
} out.endTag(null, TAG_PACKAGE);
231+
}
232+
} out.endTag(null, TAG_BLOCKED_PKGS);
233+
} out.endTag(null, TAG_BODY);
234+
235+
out.endDocument();
236+
237+
mPolicyFile.finishWrite(outfile);
238+
} catch (IOException e) {
239+
if (outfile != null) {
240+
mPolicyFile.failWrite(outfile);
241+
}
242+
}
243+
}
244+
}
245+
246+
public boolean areNotificationsEnabledForPackage(String pkg) {
247+
checkCallerIsSystem();
248+
return areNotificationsEnabledForPackageInt(pkg);
249+
}
250+
251+
// Unchecked. Not exposed via Binder, but can be called in the course of enqueue*().
252+
private boolean areNotificationsEnabledForPackageInt(String pkg) {
253+
final boolean enabled = !mBlockedPackages.contains(pkg);
254+
if (DBG) {
255+
Slog.v(TAG, "notifications are " + (enabled?"en":"dis") + "abled for " + pkg);
256+
}
257+
return enabled;
258+
}
259+
260+
public void setNotificationsEnabledForPackage(String pkg, boolean enabled) {
261+
checkCallerIsSystem();
262+
if (DBG) {
263+
Slog.v(TAG, (enabled?"en":"dis") + "abling notifications for " + pkg);
264+
}
265+
if (enabled) {
266+
mBlockedPackages.remove(pkg);
267+
} else {
268+
mBlockedPackages.add(pkg);
269+
270+
// Now, cancel any outstanding notifications that are part of a just-disabled app
271+
if (ENABLE_BLOCKED_NOTIFICATIONS) {
272+
synchronized (mNotificationList) {
273+
final int N = mNotificationList.size();
274+
for (int i=0; i<N; i++) {
275+
final NotificationRecord r = mNotificationList.get(i);
276+
if (r.pkg.equals(pkg)) {
277+
cancelNotificationLocked(r, false);
278+
}
279+
}
280+
}
281+
}
282+
// Don't bother canceling toasts, they'll go away soon enough.
283+
}
284+
writeBlockDb();
285+
}
286+
287+
118288
private static String idDebugString(Context baseContext, String packageName, int id) {
119289
Context c = null;
120290

@@ -405,6 +575,8 @@ public void update() {
405575
mToastQueue = new ArrayList<ToastRecord>();
406576
mHandler = new WorkerHandler();
407577

578+
loadBlockDb();
579+
408580
mStatusBar = statusBar;
409581
statusBar.setNotificationCallbacks(mNotificationCallbacks);
410582

@@ -465,6 +637,13 @@ public void enqueueToast(String pkg, ITransientNotification callback, int durati
465637
return ;
466638
}
467639

640+
final boolean isSystemToast = ("android".equals(pkg));
641+
642+
if (ENABLE_BLOCKED_TOASTS && !isSystemToast && !areNotificationsEnabledForPackageInt(pkg)) {
643+
Slog.e(TAG, "Suppressing toast from package " + pkg + " by user request.");
644+
return;
645+
}
646+
468647
synchronized (mToastQueue) {
469648
int callingPid = Binder.getCallingPid();
470649
long callingId = Binder.clearCallingIdentity();
@@ -479,7 +658,7 @@ record = mToastQueue.get(index);
479658
} else {
480659
// Limit the number of toasts that any given package except the android
481660
// package can enqueue. Prevents DOS attacks and deals with leaks.
482-
if (!"android".equals(pkg)) {
661+
if (!isSystemToast) {
483662
int count = 0;
484663
final int N = mToastQueue.size();
485664
for (int i=0; i<N; i++) {
@@ -675,11 +854,15 @@ private final static int clamp(int x, int low, int high) {
675854
public void enqueueNotificationInternal(String pkg, int callingUid, int callingPid,
676855
String tag, int id, Notification notification, int[] idOut)
677856
{
678-
checkIncomingCall(pkg);
857+
if (DBG) {
858+
Slog.v(TAG, "enqueueNotificationInternal: pkg=" + pkg + " id=" + id + " notification=" + notification);
859+
}
860+
checkCallerIsSystemOrSameApp(pkg);
861+
final boolean isSystemNotification = ("android".equals(pkg));
679862

680863
// Limit the number of notifications that any given package except the android
681864
// package can enqueue. Prevents DOS attacks and deals with leaks.
682-
if (!"android".equals(pkg)) {
865+
if (!isSystemNotification) {
683866
synchronized (mNotificationList) {
684867
int count = 0;
685868
final int N = mNotificationList.size();
@@ -717,7 +900,7 @@ public void enqueueNotificationInternal(String pkg, int callingUid, int callingP
717900
}
718901

719902
// === Scoring ===
720-
903+
721904
// 0. Sanitize inputs
722905
notification.priority = clamp(notification.priority, Notification.PRIORITY_MIN, Notification.PRIORITY_MAX);
723906
// Migrate notification flags to scores
@@ -726,19 +909,27 @@ public void enqueueNotificationInternal(String pkg, int callingUid, int callingP
726909
} else if (SCORE_ONGOING_HIGHER && 0 != (notification.flags & Notification.FLAG_ONGOING_EVENT)) {
727910
if (notification.priority < Notification.PRIORITY_HIGH) notification.priority = Notification.PRIORITY_HIGH;
728911
}
729-
912+
730913
// 1. initial score: buckets of 10, around the app
731-
int score = notification.priority * 10; //[-20..20]
914+
int score = notification.priority * NOTIFICATION_PRIORITY_MULTIPLIER; //[-20..20]
732915

733-
// 2. Consult oracles (external heuristics)
734-
// TODO(dsandler): oracles
916+
// 2. Consult external heuristics (TBD)
735917

736-
// 3. Apply local heuristics & overrides
918+
// 3. Apply local rules
737919

738920
// blocked apps
739-
// TODO(dsandler): add block db
740-
if (pkg.startsWith("com.test.spammer.")) {
741-
score = -1000;
921+
if (ENABLE_BLOCKED_NOTIFICATIONS && !isSystemNotification && !areNotificationsEnabledForPackageInt(pkg)) {
922+
score = JUNK_SCORE;
923+
Slog.e(TAG, "Suppressing notification from package " + pkg + " by user request.");
924+
}
925+
926+
if (DBG) {
927+
Slog.v(TAG, "Assigned score=" + score + " to " + notification);
928+
}
929+
930+
if (score < SCORE_DISPLAY_THRESHOLD) {
931+
// Notification will be blocked because the score is too low.
932+
return;
742933
}
743934

744935
synchronized (mNotificationList) {
@@ -1030,22 +1221,30 @@ public void cancelNotification(String pkg, int id) {
10301221
}
10311222

10321223
public void cancelNotificationWithTag(String pkg, String tag, int id) {
1033-
checkIncomingCall(pkg);
1224+
checkCallerIsSystemOrSameApp(pkg);
10341225
// Don't allow client applications to cancel foreground service notis.
10351226
cancelNotification(pkg, tag, id, 0,
10361227
Binder.getCallingUid() == Process.SYSTEM_UID
10371228
? 0 : Notification.FLAG_FOREGROUND_SERVICE, false);
10381229
}
10391230

10401231
public void cancelAllNotifications(String pkg) {
1041-
checkIncomingCall(pkg);
1232+
checkCallerIsSystemOrSameApp(pkg);
10421233

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

1048-
void checkIncomingCall(String pkg) {
1239+
void checkCallerIsSystem() {
1240+
int uid = Binder.getCallingUid();
1241+
if (uid == Process.SYSTEM_UID || uid == 0) {
1242+
return;
1243+
}
1244+
throw new SecurityException("Disallowed call for uid " + uid);
1245+
}
1246+
1247+
void checkCallerIsSystemOrSameApp(String pkg) {
10491248
int uid = Binder.getCallingUid();
10501249
if (uid == Process.SYSTEM_UID || uid == 0) {
10511250
return;

0 commit comments

Comments
 (0)