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.applications;
17 
18 import android.app.usage.IUsageStatsManager;
19 import android.app.usage.UsageEvents;
20 import android.content.Context;
21 import android.os.RemoteException;
22 import android.os.UserHandle;
23 import android.os.UserManager;
24 import android.text.format.DateUtils;
25 import android.util.ArrayMap;
26 import android.util.Log;
27 import android.util.Slog;
28 import android.view.View;
29 import android.view.ViewGroup;
30 import android.widget.CompoundButton;
31 import android.widget.Switch;
32 
33 import com.android.settings.R;
34 import com.android.settings.Utils;
35 import com.android.settings.notification.NotificationBackend;
36 import com.android.settingslib.applications.ApplicationsState;
37 import com.android.settingslib.applications.ApplicationsState.AppEntry;
38 import com.android.settingslib.applications.ApplicationsState.AppFilter;
39 import com.android.settingslib.utils.StringUtil;
40 
41 import java.util.ArrayList;
42 import java.util.Comparator;
43 import java.util.List;
44 import java.util.Map;
45 
46 /**
47  * Connects the info provided by ApplicationsState and UsageStatsManager.
48  * Also provides app filters that can use the notification data.
49  */
50 public class AppStateNotificationBridge extends AppStateBaseBridge {
51 
52     private final String TAG = "AppStateNotificationBridge";
53     private final boolean DEBUG = false;
54     private final Context mContext;
55     private IUsageStatsManager mUsageStatsManager;
56     protected List<Integer> mUserIds;
57     private NotificationBackend mBackend;
58     private static final int DAYS_TO_CHECK = 7;
59 
AppStateNotificationBridge(Context context, ApplicationsState appState, Callback callback, IUsageStatsManager usageStatsManager, UserManager userManager, NotificationBackend backend)60     public AppStateNotificationBridge(Context context, ApplicationsState appState,
61             Callback callback, IUsageStatsManager usageStatsManager,
62             UserManager userManager, NotificationBackend backend) {
63         super(appState, callback);
64         mContext = context;
65         mUsageStatsManager = usageStatsManager;
66         mBackend = backend;
67         mUserIds = new ArrayList<>();
68         mUserIds.add(mContext.getUserId());
69         int workUserId = Utils.getManagedProfileId(userManager, mContext.getUserId());
70         if (workUserId != UserHandle.USER_NULL) {
71             mUserIds.add(workUserId);
72         }
73     }
74 
75     @Override
loadAllExtraInfo()76     protected void loadAllExtraInfo() {
77         ArrayList<AppEntry> apps = mAppSession.getAllApps();
78         if (apps == null) {
79             if (DEBUG) {
80                 Log.d(TAG, "No apps.  No extra info loaded");
81             }
82             return;
83         }
84 
85         final Map<String, NotificationsSentState> map = getAggregatedUsageEvents();
86         for (AppEntry entry : apps) {
87             NotificationsSentState stats =
88                     map.get(getKey(UserHandle.getUserId(entry.info.uid), entry.info.packageName));
89             if (stats == null) {
90                 stats = new NotificationsSentState();
91             }
92             calculateAvgSentCounts(stats);
93             addBlockStatus(entry, stats);
94             entry.extraInfo = stats;
95         }
96     }
97 
98     @Override
updateExtraInfo(AppEntry entry, String pkg, int uid)99     protected void updateExtraInfo(AppEntry entry, String pkg, int uid) {
100         NotificationsSentState stats = getAggregatedUsageEvents(
101                 UserHandle.getUserId(entry.info.uid), entry.info.packageName);
102         calculateAvgSentCounts(stats);
103         addBlockStatus(entry, stats);
104         entry.extraInfo = stats;
105     }
106 
getSummary(Context context, NotificationsSentState state, int sortOrder)107     public static CharSequence getSummary(Context context, NotificationsSentState state,
108             int sortOrder) {
109         if (sortOrder == R.id.sort_order_recent_notification) {
110             if (state.lastSent == 0) {
111                 return context.getString(R.string.notifications_sent_never);
112             }
113             return StringUtil.formatRelativeTime(
114                     context, System.currentTimeMillis() - state.lastSent, true);
115         } else if (sortOrder == R.id.sort_order_frequent_notification) {
116             if (state.avgSentDaily > 0) {
117                 return context.getResources().getQuantityString(
118                         R.plurals.notifications_sent_daily, state.avgSentDaily, state.avgSentDaily);
119             }
120             return context.getResources().getQuantityString(R.plurals.notifications_sent_weekly,
121                     state.avgSentWeekly, state.avgSentWeekly);
122         } else {
123             return "";
124         }
125     }
126 
addBlockStatus(AppEntry entry, NotificationsSentState stats)127     private void addBlockStatus(AppEntry entry, NotificationsSentState stats) {
128         if (stats != null) {
129             stats.blocked = mBackend.getNotificationsBanned(entry.info.packageName, entry.info.uid);
130             stats.systemApp = mBackend.isSystemApp(mContext, entry.info);
131             stats.blockable = !stats.systemApp || (stats.systemApp && stats.blocked);
132         }
133     }
134 
calculateAvgSentCounts(NotificationsSentState stats)135     private void calculateAvgSentCounts(NotificationsSentState stats) {
136         if (stats != null) {
137             stats.avgSentDaily = Math.round((float) stats.sentCount / DAYS_TO_CHECK);
138             if (stats.sentCount < DAYS_TO_CHECK) {
139                 stats.avgSentWeekly = stats.sentCount;
140             }
141         }
142     }
143 
getAggregatedUsageEvents()144     protected Map<String, NotificationsSentState> getAggregatedUsageEvents() {
145         ArrayMap<String, NotificationsSentState> aggregatedStats = new ArrayMap<>();
146 
147         long now = System.currentTimeMillis();
148         long startTime = now - (DateUtils.DAY_IN_MILLIS * DAYS_TO_CHECK);
149         for (int userId : mUserIds) {
150             UsageEvents events = null;
151             try {
152                 events = mUsageStatsManager.queryEventsForUser(
153                         startTime, now, userId, mContext.getPackageName());
154             } catch (RemoteException e) {
155                 e.printStackTrace();
156             }
157             if (events != null) {
158                 UsageEvents.Event event = new UsageEvents.Event();
159                 while (events.hasNextEvent()) {
160                     events.getNextEvent(event);
161                     NotificationsSentState stats =
162                             aggregatedStats.get(getKey(userId, event.getPackageName()));
163                     if (stats == null) {
164                         stats = new NotificationsSentState();
165                         aggregatedStats.put(getKey(userId, event.getPackageName()), stats);
166                     }
167 
168                     if (event.getEventType() == UsageEvents.Event.NOTIFICATION_INTERRUPTION) {
169                         if (event.getTimeStamp() > stats.lastSent) {
170                             stats.lastSent = event.getTimeStamp();
171                         }
172                         stats.sentCount++;
173                     }
174 
175                 }
176             }
177         }
178         return aggregatedStats;
179     }
180 
getAggregatedUsageEvents(int userId, String pkg)181     protected NotificationsSentState getAggregatedUsageEvents(int userId, String pkg) {
182         NotificationsSentState stats = null;
183 
184         long now = System.currentTimeMillis();
185         long startTime = now - (DateUtils.DAY_IN_MILLIS * DAYS_TO_CHECK);
186         UsageEvents events = null;
187         try {
188             events = mUsageStatsManager.queryEventsForPackageForUser(
189                     startTime, now, userId, pkg, mContext.getPackageName());
190         } catch (RemoteException e) {
191             e.printStackTrace();
192         }
193         if (events != null) {
194             UsageEvents.Event event = new UsageEvents.Event();
195             while (events.hasNextEvent()) {
196                 events.getNextEvent(event);
197 
198                 if (event.getEventType() == UsageEvents.Event.NOTIFICATION_INTERRUPTION) {
199                     if (stats == null) {
200                         stats = new NotificationsSentState();
201                     }
202                     if (event.getTimeStamp() > stats.lastSent) {
203                         stats.lastSent = event.getTimeStamp();
204                     }
205                     stats.sentCount++;
206                 }
207 
208             }
209         }
210         return stats;
211     }
212 
getNotificationsSentState(AppEntry entry)213     private static NotificationsSentState getNotificationsSentState(AppEntry entry) {
214         if (entry == null || entry.extraInfo == null) {
215             return null;
216         }
217         if (entry.extraInfo instanceof NotificationsSentState) {
218             return (NotificationsSentState) entry.extraInfo;
219         }
220         return null;
221     }
222 
getKey(int userId, String pkg)223     protected static String getKey(int userId, String pkg) {
224         return userId + "|" + pkg;
225     }
226 
getSwitchOnCheckedListener(final AppEntry entry)227     public CompoundButton.OnCheckedChangeListener getSwitchOnCheckedListener(final AppEntry entry) {
228         if (entry == null) {
229             return null;
230         }
231         return (buttonView, isChecked) -> {
232             mBackend.setNotificationsEnabledForPackage(
233                     entry.info.packageName, entry.info.uid, isChecked);
234             NotificationsSentState stats = getNotificationsSentState(entry);
235             if (stats != null) {
236                 stats.blocked = !isChecked;
237             }
238         };
239     }
240 
241     public static final AppFilter FILTER_APP_NOTIFICATION_RECENCY = new AppFilter() {
242         @Override
243         public void init() {
244         }
245 
246         @Override
247         public boolean filterApp(AppEntry info) {
248             NotificationsSentState state = getNotificationsSentState(info);
249             if (state != null) {
250                 return state.lastSent != 0;
251             }
252             return false;
253         }
254     };
255 
256     public static final AppFilter FILTER_APP_NOTIFICATION_FREQUENCY = new AppFilter() {
257         @Override
258         public void init() {
259         }
260 
261         @Override
262         public boolean filterApp(AppEntry info) {
263             NotificationsSentState state = getNotificationsSentState(info);
264             if (state != null) {
265                 return state.sentCount != 0;
266             }
267             return false;
268         }
269     };
270 
271     public static final AppFilter FILTER_APP_NOTIFICATION_BLOCKED = new AppFilter() {
272         @Override
273         public void init() {
274         }
275 
276         @Override
277         public boolean filterApp(AppEntry info) {
278             NotificationsSentState state = getNotificationsSentState(info);
279             if (state != null) {
280                 return state.blocked;
281             }
282             return false;
283         }
284     };
285 
286     public static final Comparator<AppEntry> RECENT_NOTIFICATION_COMPARATOR
287             = new Comparator<AppEntry>() {
288         @Override
289         public int compare(AppEntry object1, AppEntry object2) {
290             NotificationsSentState state1 = getNotificationsSentState(object1);
291             NotificationsSentState state2 = getNotificationsSentState(object2);
292             if (state1 == null && state2 != null) return -1;
293             if (state1 != null && state2 == null) return 1;
294             if (state1 != null && state2 != null) {
295                 if (state1.lastSent < state2.lastSent) return 1;
296                 if (state1.lastSent > state2.lastSent) return -1;
297             }
298             return ApplicationsState.ALPHA_COMPARATOR.compare(object1, object2);
299         }
300     };
301 
302     public static final Comparator<AppEntry> FREQUENCY_NOTIFICATION_COMPARATOR
303             = new Comparator<AppEntry>() {
304         @Override
305         public int compare(AppEntry object1, AppEntry object2) {
306             NotificationsSentState state1 = getNotificationsSentState(object1);
307             NotificationsSentState state2 = getNotificationsSentState(object2);
308             if (state1 == null && state2 != null) return -1;
309             if (state1 != null && state2 == null) return 1;
310             if (state1 != null && state2 != null) {
311                 if (state1.sentCount < state2.sentCount) return 1;
312                 if (state1.sentCount > state2.sentCount) return -1;
313             }
314             return ApplicationsState.ALPHA_COMPARATOR.compare(object1, object2);
315         }
316     };
317 
enableSwitch(AppEntry entry)318     public static final boolean enableSwitch(AppEntry entry) {
319         NotificationsSentState stats = getNotificationsSentState(entry);
320         if (stats == null) {
321             return false;
322         }
323 
324         return stats.blockable;
325     }
326 
checkSwitch(AppEntry entry)327     public static final boolean checkSwitch(AppEntry entry) {
328         NotificationsSentState stats = getNotificationsSentState(entry);
329         if (stats == null) {
330             return false;
331         }
332 
333         return !stats.blocked;
334     }
335 
336     /**
337      * NotificationsSentState contains how often an app sends notifications and how recently it sent
338      * one.
339      */
340     public static class NotificationsSentState {
341         public int avgSentDaily = 0;
342         public int avgSentWeekly = 0;
343         public long lastSent = 0;
344         public int sentCount = 0;
345         public boolean blockable;
346         public boolean blocked;
347         public boolean systemApp;
348     }
349 }
350