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