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