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