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