1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.settings.notification;
17 
18 import static android.app.NotificationManager.IMPORTANCE_NONE;
19 import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED;
20 
21 import android.app.INotificationManager;
22 import android.app.NotificationChannel;
23 import android.app.NotificationChannelGroup;
24 import android.app.role.RoleManager;
25 import android.app.usage.IUsageStatsManager;
26 import android.app.usage.UsageEvents;
27 import android.content.ComponentName;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.pm.ApplicationInfo;
31 import android.content.pm.PackageInfo;
32 import android.content.pm.PackageManager;
33 import android.content.pm.ParceledListSlice;
34 import android.graphics.drawable.Drawable;
35 import android.os.RemoteException;
36 import android.os.ServiceManager;
37 import android.os.UserHandle;
38 import android.service.notification.NotifyingApp;
39 import android.text.format.DateUtils;
40 import android.util.IconDrawableFactory;
41 import android.util.Log;
42 
43 import androidx.annotation.VisibleForTesting;
44 
45 import com.android.settingslib.R;
46 import com.android.settingslib.Utils;
47 import com.android.settingslib.utils.StringUtil;
48 
49 import java.util.ArrayList;
50 import java.util.HashMap;
51 import java.util.List;
52 import java.util.Map;
53 
54 public class NotificationBackend {
55     private static final String TAG = "NotificationBackend";
56 
57     static IUsageStatsManager sUsageStatsManager = IUsageStatsManager.Stub.asInterface(
58             ServiceManager.getService(Context.USAGE_STATS_SERVICE));
59     private static final int DAYS_TO_CHECK = 7;
60     static INotificationManager sINM = INotificationManager.Stub.asInterface(
61             ServiceManager.getService(Context.NOTIFICATION_SERVICE));
62 
loadAppRow(Context context, PackageManager pm, ApplicationInfo app)63     public AppRow loadAppRow(Context context, PackageManager pm, ApplicationInfo app) {
64         final AppRow row = new AppRow();
65         row.pkg = app.packageName;
66         row.uid = app.uid;
67         try {
68             row.label = app.loadLabel(pm);
69         } catch (Throwable t) {
70             Log.e(TAG, "Error loading application label for " + row.pkg, t);
71             row.label = row.pkg;
72         }
73         row.icon = IconDrawableFactory.newInstance(context).getBadgedIcon(app);
74         row.banned = getNotificationsBanned(row.pkg, row.uid);
75         row.showBadge = canShowBadge(row.pkg, row.uid);
76         row.allowBubbles = canBubble(row.pkg, row.uid);
77         row.userId = UserHandle.getUserId(row.uid);
78         row.blockedChannelCount = getBlockedChannelCount(row.pkg, row.uid);
79         row.channelCount = getChannelCount(row.pkg, row.uid);
80         recordAggregatedUsageEvents(context, row);
81         return row;
82     }
83 
isBlockable(Context context, ApplicationInfo info)84     public boolean isBlockable(Context context, ApplicationInfo info) {
85         final boolean blocked = getNotificationsBanned(info.packageName, info.uid);
86         final boolean systemApp = isSystemApp(context, info);
87         return !systemApp || (systemApp && blocked);
88     }
89 
loadAppRow(Context context, PackageManager pm, RoleManager roleManager, PackageInfo app)90     public AppRow loadAppRow(Context context, PackageManager pm,
91             RoleManager roleManager, PackageInfo app) {
92         final AppRow row = loadAppRow(context, pm, app.applicationInfo);
93         recordCanBeBlocked(context, pm, roleManager, app, row);
94         return row;
95     }
96 
recordCanBeBlocked(Context context, PackageManager pm, RoleManager rm, PackageInfo app, AppRow row)97     void recordCanBeBlocked(Context context, PackageManager pm, RoleManager rm, PackageInfo app,
98             AppRow row) {
99         row.systemApp = Utils.isSystemPackage(context.getResources(), pm, app);
100         List<String> roles = rm.getHeldRolesFromController(app.packageName);
101         if (roles.contains(RoleManager.ROLE_DIALER)
102                 || roles.contains(RoleManager.ROLE_EMERGENCY)) {
103             row.systemApp = true;
104         }
105         final String[] nonBlockablePkgs = context.getResources().getStringArray(
106                 com.android.internal.R.array.config_nonBlockableNotificationPackages);
107         markAppRowWithBlockables(nonBlockablePkgs, row, app.packageName);
108     }
109 
markAppRowWithBlockables(String[] nonBlockablePkgs, AppRow row, String packageName)110     @VisibleForTesting static void markAppRowWithBlockables(String[] nonBlockablePkgs, AppRow row,
111             String packageName) {
112         if (nonBlockablePkgs != null) {
113             int N = nonBlockablePkgs.length;
114             for (int i = 0; i < N; i++) {
115                 String pkg = nonBlockablePkgs[i];
116                 if (pkg == null) {
117                     continue;
118                 } else if (pkg.contains(":")) {
119                     // handled by NotificationChannel.isImportanceLockedByOEM()
120                     continue;
121                 } else if (packageName.equals(nonBlockablePkgs[i])) {
122                     row.systemApp = row.lockedImportance = true;
123                 }
124             }
125         }
126     }
127 
isSystemApp(Context context, ApplicationInfo app)128     public boolean isSystemApp(Context context, ApplicationInfo app) {
129         try {
130             PackageInfo info = context.getPackageManager().getPackageInfo(
131                     app.packageName, PackageManager.GET_SIGNATURES);
132             RoleManager rm = context.getSystemService(RoleManager.class);
133             final AppRow row = new AppRow();
134             recordCanBeBlocked(context, context.getPackageManager(), rm, info, row);
135             return row.systemApp;
136         } catch (PackageManager.NameNotFoundException e) {
137             e.printStackTrace();
138         }
139         return false;
140     }
141 
getNotificationsBanned(String pkg, int uid)142     public boolean getNotificationsBanned(String pkg, int uid) {
143         try {
144             final boolean enabled = sINM.areNotificationsEnabledForPackage(pkg, uid);
145             return !enabled;
146         } catch (Exception e) {
147             Log.w(TAG, "Error calling NoMan", e);
148             return false;
149         }
150     }
151 
setNotificationsEnabledForPackage(String pkg, int uid, boolean enabled)152     public boolean setNotificationsEnabledForPackage(String pkg, int uid, boolean enabled) {
153         try {
154             if (onlyHasDefaultChannel(pkg, uid)) {
155                 NotificationChannel defaultChannel =
156                         getChannel(pkg, uid, NotificationChannel.DEFAULT_CHANNEL_ID);
157                 defaultChannel.setImportance(enabled ? IMPORTANCE_UNSPECIFIED : IMPORTANCE_NONE);
158                 updateChannel(pkg, uid, defaultChannel);
159             }
160             sINM.setNotificationsEnabledForPackage(pkg, uid, enabled);
161             return true;
162         } catch (Exception e) {
163             Log.w(TAG, "Error calling NoMan", e);
164             return false;
165         }
166     }
167 
canShowBadge(String pkg, int uid)168     public boolean canShowBadge(String pkg, int uid) {
169         try {
170             return sINM.canShowBadge(pkg, uid);
171         } catch (Exception e) {
172             Log.w(TAG, "Error calling NoMan", e);
173             return false;
174         }
175     }
176 
setShowBadge(String pkg, int uid, boolean showBadge)177     public boolean setShowBadge(String pkg, int uid, boolean showBadge) {
178         try {
179             sINM.setShowBadge(pkg, uid, showBadge);
180             return true;
181         } catch (Exception e) {
182             Log.w(TAG, "Error calling NoMan", e);
183             return false;
184         }
185     }
186 
canBubble(String pkg, int uid)187     public boolean canBubble(String pkg, int uid) {
188         try {
189             return sINM.areBubblesAllowedForPackage(pkg, uid);
190         } catch (Exception e) {
191             Log.w(TAG, "Error calling NoMan", e);
192             return false;
193         }
194     }
195 
setAllowBubbles(String pkg, int uid, boolean allow)196     public boolean setAllowBubbles(String pkg, int uid, boolean allow) {
197         try {
198             sINM.setBubblesAllowed(pkg, uid, allow);
199             return true;
200         } catch (Exception e) {
201             Log.w(TAG, "Error calling NoMan", e);
202             return false;
203         }
204     }
205 
206 
getChannel(String pkg, int uid, String channelId)207     public NotificationChannel getChannel(String pkg, int uid, String channelId) {
208         if (channelId == null) {
209             return null;
210         }
211         try {
212             return sINM.getNotificationChannelForPackage(pkg, uid, channelId, true);
213         } catch (Exception e) {
214             Log.w(TAG, "Error calling NoMan", e);
215             return null;
216         }
217     }
218 
getGroup(String pkg, int uid, String groupId)219     public NotificationChannelGroup getGroup(String pkg, int uid, String groupId) {
220         if (groupId == null) {
221             return null;
222         }
223         try {
224             return sINM.getNotificationChannelGroupForPackage(groupId, pkg, uid);
225         } catch (Exception e) {
226             Log.w(TAG, "Error calling NoMan", e);
227             return null;
228         }
229     }
230 
getGroups(String pkg, int uid)231     public ParceledListSlice<NotificationChannelGroup> getGroups(String pkg, int uid) {
232         try {
233             return sINM.getNotificationChannelGroupsForPackage(pkg, uid, false);
234         } catch (Exception e) {
235             Log.w(TAG, "Error calling NoMan", e);
236             return ParceledListSlice.emptyList();
237         }
238     }
239 
240     /**
241      * Returns all notification channels associated with the package and uid that will bypass DND
242      */
getNotificationChannelsBypassingDnd(String pkg, int uid)243     public ParceledListSlice<NotificationChannel> getNotificationChannelsBypassingDnd(String pkg,
244             int uid) {
245         try {
246             return sINM.getNotificationChannelsBypassingDnd(pkg, uid);
247         } catch (Exception e) {
248             Log.w(TAG, "Error calling NoMan", e);
249             return ParceledListSlice.emptyList();
250         }
251     }
252 
updateChannel(String pkg, int uid, NotificationChannel channel)253     public void updateChannel(String pkg, int uid, NotificationChannel channel) {
254         try {
255             sINM.updateNotificationChannelForPackage(pkg, uid, channel);
256         } catch (Exception e) {
257             Log.w(TAG, "Error calling NoMan", e);
258         }
259     }
260 
updateChannelGroup(String pkg, int uid, NotificationChannelGroup group)261     public void updateChannelGroup(String pkg, int uid, NotificationChannelGroup group) {
262         try {
263             sINM.updateNotificationChannelGroupForPackage(pkg, uid, group);
264         } catch (Exception e) {
265             Log.w(TAG, "Error calling NoMan", e);
266         }
267     }
268 
getDeletedChannelCount(String pkg, int uid)269     public int getDeletedChannelCount(String pkg, int uid) {
270         try {
271             return sINM.getDeletedChannelCount(pkg, uid);
272         } catch (Exception e) {
273             Log.w(TAG, "Error calling NoMan", e);
274             return 0;
275         }
276     }
277 
getBlockedChannelCount(String pkg, int uid)278     public int getBlockedChannelCount(String pkg, int uid) {
279         try {
280             return sINM.getBlockedChannelCount(pkg, uid);
281         } catch (Exception e) {
282             Log.w(TAG, "Error calling NoMan", e);
283             return 0;
284         }
285     }
286 
onlyHasDefaultChannel(String pkg, int uid)287     public boolean onlyHasDefaultChannel(String pkg, int uid) {
288         try {
289             return sINM.onlyHasDefaultChannel(pkg, uid);
290         } catch (Exception e) {
291             Log.w(TAG, "Error calling NoMan", e);
292             return false;
293         }
294     }
295 
getChannelCount(String pkg, int uid)296     public int getChannelCount(String pkg, int uid) {
297         try {
298             return sINM.getNumNotificationChannelsForPackage(pkg, uid, false);
299         } catch (Exception e) {
300             Log.w(TAG, "Error calling NoMan", e);
301             return 0;
302         }
303     }
304 
getNumAppsBypassingDnd(int uid)305     public int getNumAppsBypassingDnd(int uid) {
306         try {
307             return sINM.getAppsBypassingDndCount(uid);
308         } catch (Exception e) {
309             Log.w(TAG, "Error calling NoMan", e);
310             return 0;
311         }
312     }
313 
getBlockedAppCount()314     public int getBlockedAppCount() {
315         try {
316             return sINM.getBlockedAppCount(UserHandle.myUserId());
317         } catch (Exception e) {
318             Log.w(TAG, "Error calling NoMan", e);
319             return 0;
320         }
321     }
322 
shouldHideSilentStatusBarIcons(Context context)323     public boolean shouldHideSilentStatusBarIcons(Context context) {
324         try {
325             return sINM.shouldHideSilentStatusIcons(context.getPackageName());
326         } catch (Exception e) {
327             Log.w(TAG, "Error calling NoMan", e);
328             return false;
329         }
330     }
331 
setHideSilentStatusIcons(boolean hide)332     public void setHideSilentStatusIcons(boolean hide) {
333         try {
334             sINM.setHideSilentStatusIcons(hide);
335         } catch (Exception e) {
336             Log.w(TAG, "Error calling NoMan", e);
337         }
338     }
339 
allowAssistantAdjustment(String capability, boolean allowed)340     public void allowAssistantAdjustment(String capability, boolean allowed) {
341         try {
342             if (allowed) {
343                 sINM.allowAssistantAdjustment(capability);
344             } else {
345                 sINM.disallowAssistantAdjustment(capability);
346             }
347         } catch (Exception e) {
348             Log.w(TAG, "Error calling NoMan", e);
349         }
350     }
351 
getAssistantAdjustments(String pkg)352     public List<String> getAssistantAdjustments(String pkg) {
353         try {
354             return sINM.getAllowedAssistantAdjustments(pkg);
355         } catch (Exception e) {
356             Log.w(TAG, "Error calling NoMan", e);
357         }
358         return new ArrayList<>();
359     }
360 
showSilentInStatusBar(String pkg)361     public boolean showSilentInStatusBar(String pkg) {
362         try {
363             return !sINM.shouldHideSilentStatusIcons(pkg);
364         } catch (Exception e) {
365             Log.w(TAG, "Error calling NoMan", e);
366         }
367         return false;
368     }
369 
recordAggregatedUsageEvents(Context context, AppRow appRow)370     protected void recordAggregatedUsageEvents(Context context, AppRow appRow) {
371         long now = System.currentTimeMillis();
372         long startTime = now - (DateUtils.DAY_IN_MILLIS * DAYS_TO_CHECK);
373         UsageEvents events = null;
374         try {
375             events = sUsageStatsManager.queryEventsForPackageForUser(
376                     startTime, now, appRow.userId, appRow.pkg, context.getPackageName());
377         } catch (RemoteException e) {
378             e.printStackTrace();
379         }
380         recordAggregatedUsageEvents(events, appRow);
381     }
382 
recordAggregatedUsageEvents(UsageEvents events, AppRow appRow)383     protected void recordAggregatedUsageEvents(UsageEvents events, AppRow appRow) {
384         appRow.sentByChannel = new HashMap<>();
385         appRow.sentByApp = new NotificationsSentState();
386         if (events != null) {
387             UsageEvents.Event event = new UsageEvents.Event();
388             while (events.hasNextEvent()) {
389                 events.getNextEvent(event);
390 
391                 if (event.getEventType() == UsageEvents.Event.NOTIFICATION_INTERRUPTION) {
392                     String channelId = event.mNotificationChannelId;
393                     if (channelId != null) {
394                         NotificationsSentState stats = appRow.sentByChannel.get(channelId);
395                         if (stats == null) {
396                             stats = new NotificationsSentState();
397                             appRow.sentByChannel.put(channelId, stats);
398                         }
399                         if (event.getTimeStamp() > stats.lastSent) {
400                             stats.lastSent = event.getTimeStamp();
401                             appRow.sentByApp.lastSent = event.getTimeStamp();
402                         }
403                         stats.sentCount++;
404                         appRow.sentByApp.sentCount++;
405                         calculateAvgSentCounts(stats);
406                     }
407                 }
408 
409             }
410             calculateAvgSentCounts(appRow.sentByApp);
411         }
412     }
413 
getSentSummary(Context context, NotificationsSentState state, boolean sortByRecency)414     public static CharSequence getSentSummary(Context context, NotificationsSentState state,
415             boolean sortByRecency) {
416         if (state == null) {
417             return null;
418         }
419         if (sortByRecency) {
420             if (state.lastSent == 0) {
421                 return context.getString(R.string.notifications_sent_never);
422             }
423             return StringUtil.formatRelativeTime(
424                     context, System.currentTimeMillis() - state.lastSent, true);
425         } else {
426             if (state.avgSentDaily > 0) {
427                 return context.getResources().getQuantityString(R.plurals.notifications_sent_daily,
428                         state.avgSentDaily, state.avgSentDaily);
429             }
430             return context.getResources().getQuantityString(R.plurals.notifications_sent_weekly,
431                     state.avgSentWeekly, state.avgSentWeekly);
432         }
433     }
434 
calculateAvgSentCounts(NotificationsSentState stats)435     private void calculateAvgSentCounts(NotificationsSentState stats) {
436         if (stats != null) {
437             stats.avgSentDaily = Math.round((float) stats.sentCount / DAYS_TO_CHECK);
438             if (stats.sentCount < DAYS_TO_CHECK) {
439                 stats.avgSentWeekly = stats.sentCount;
440             }
441         }
442     }
443 
getAllowedNotificationAssistant()444     public ComponentName getAllowedNotificationAssistant() {
445         try {
446             return sINM.getAllowedNotificationAssistant();
447         } catch (Exception e) {
448             Log.w(TAG, "Error calling NoMan", e);
449             return null;
450         }
451     }
452 
setNotificationAssistantGranted(ComponentName cn)453     public boolean setNotificationAssistantGranted(ComponentName cn) {
454         try {
455             sINM.setNotificationAssistantAccessGranted(cn, true);
456             if (cn == null) {
457                 return sINM.getAllowedNotificationAssistant() == null;
458             } else {
459                 return cn.equals(sINM.getAllowedNotificationAssistant());
460             }
461         } catch (Exception e) {
462             Log.w(TAG, "Error calling NoMan", e);
463             return false;
464         }
465     }
466 
467     /**
468      * NotificationsSentState contains how often an app sends notifications and how recently it sent
469      * one.
470      */
471     public static class NotificationsSentState {
472         public int avgSentDaily = 0;
473         public int avgSentWeekly = 0;
474         public long lastSent = 0;
475         public int sentCount = 0;
476     }
477 
478     static class Row {
479         public String section;
480     }
481 
482     public static class AppRow extends Row {
483         public String pkg;
484         public int uid;
485         public Drawable icon;
486         public CharSequence label;
487         public Intent settingsIntent;
488         public boolean banned;
489         public boolean first;  // first app in section
490         public boolean systemApp;
491         public boolean lockedImportance;
492         public boolean showBadge;
493         public boolean allowBubbles;
494         public int userId;
495         public int blockedChannelCount;
496         public int channelCount;
497         public Map<String, NotificationsSentState> sentByChannel;
498         public NotificationsSentState sentByApp;
499     }
500 }
501