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.view.View; 28 import android.view.ViewGroup; 29 import android.widget.Switch; 30 31 import com.android.settings.R; 32 import com.android.settings.Utils; 33 import com.android.settings.notification.NotificationBackend; 34 import com.android.settingslib.applications.ApplicationsState; 35 import com.android.settingslib.applications.ApplicationsState.AppEntry; 36 import com.android.settingslib.applications.ApplicationsState.AppFilter; 37 import com.android.settingslib.utils.StringUtil; 38 39 import java.util.ArrayList; 40 import java.util.Comparator; 41 import java.util.List; 42 import java.util.Map; 43 44 /** 45 * Connects the info provided by ApplicationsState and UsageStatsManager. 46 * Also provides app filters that can use the notification data. 47 */ 48 public class AppStateNotificationBridge extends AppStateBaseBridge { 49 50 private final String TAG = "AppStateNotificationBridge"; 51 private final boolean DEBUG = false; 52 private final Context mContext; 53 private IUsageStatsManager mUsageStatsManager; 54 protected List<Integer> mUserIds; 55 private NotificationBackend mBackend; 56 private static final int DAYS_TO_CHECK = 7; 57 AppStateNotificationBridge(Context context, ApplicationsState appState, Callback callback, IUsageStatsManager usageStatsManager, UserManager userManager, NotificationBackend backend)58 public AppStateNotificationBridge(Context context, ApplicationsState appState, 59 Callback callback, IUsageStatsManager usageStatsManager, 60 UserManager userManager, NotificationBackend backend) { 61 super(appState, callback); 62 mContext = context; 63 mUsageStatsManager = usageStatsManager; 64 mBackend = backend; 65 mUserIds = new ArrayList<>(); 66 mUserIds.add(mContext.getUserId()); 67 int workUserId = Utils.getManagedProfileId(userManager, mContext.getUserId()); 68 if (workUserId != UserHandle.USER_NULL) { 69 mUserIds.add(workUserId); 70 } 71 } 72 73 @Override loadAllExtraInfo()74 protected void loadAllExtraInfo() { 75 ArrayList<AppEntry> apps = mAppSession.getAllApps(); 76 if (apps == null) { 77 if (DEBUG) { 78 Log.d(TAG, "No apps. No extra info loaded"); 79 } 80 return; 81 } 82 83 final Map<String, NotificationsSentState> map = getAggregatedUsageEvents(); 84 for (AppEntry entry : apps) { 85 NotificationsSentState stats = 86 map.get(getKey(UserHandle.getUserId(entry.info.uid), entry.info.packageName)); 87 calculateAvgSentCounts(stats); 88 addBlockStatus(entry, stats); 89 entry.extraInfo = stats; 90 } 91 } 92 93 @Override updateExtraInfo(AppEntry entry, String pkg, int uid)94 protected void updateExtraInfo(AppEntry entry, String pkg, int uid) { 95 NotificationsSentState stats = getAggregatedUsageEvents( 96 UserHandle.getUserId(entry.info.uid), entry.info.packageName); 97 calculateAvgSentCounts(stats); 98 addBlockStatus(entry, stats); 99 entry.extraInfo = stats; 100 } 101 getSummary(Context context, NotificationsSentState state, int sortOrder)102 public static CharSequence getSummary(Context context, NotificationsSentState state, 103 int sortOrder) { 104 if (sortOrder == R.id.sort_order_recent_notification) { 105 if (state.lastSent == 0) { 106 return context.getString(R.string.notifications_sent_never); 107 } 108 return StringUtil.formatRelativeTime( 109 context, System.currentTimeMillis() - state.lastSent, true); 110 } else if (sortOrder == R.id.sort_order_frequent_notification) { 111 if (state.avgSentDaily > 0) { 112 return context.getResources().getQuantityString( 113 R.plurals.notifications_sent_daily, state.avgSentDaily, state.avgSentDaily); 114 } 115 return context.getResources().getQuantityString(R.plurals.notifications_sent_weekly, 116 state.avgSentWeekly, state.avgSentWeekly); 117 } else { 118 return ""; 119 } 120 } 121 addBlockStatus(AppEntry entry, NotificationsSentState stats)122 private void addBlockStatus(AppEntry entry, NotificationsSentState stats) { 123 if (stats != null) { 124 stats.blocked = mBackend.getNotificationsBanned(entry.info.packageName, entry.info.uid); 125 stats.systemApp = mBackend.isSystemApp(mContext, entry.info); 126 stats.blockable = !stats.systemApp || (stats.systemApp && stats.blocked); 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 getSwitchOnClickListener(final AppEntry entry)222 public View.OnClickListener getSwitchOnClickListener(final AppEntry entry) { 223 if (entry != null) { 224 return v -> { 225 ViewGroup view = (ViewGroup) v; 226 Switch toggle = view.findViewById(R.id.switchWidget); 227 if (toggle != null) { 228 if (!toggle.isEnabled()) { 229 return; 230 } 231 toggle.toggle(); 232 mBackend.setNotificationsEnabledForPackage( 233 entry.info.packageName, entry.info.uid, toggle.isChecked()); 234 NotificationsSentState stats = getNotificationsSentState(entry); 235 if (stats != null) { 236 stats.blocked = !toggle.isChecked(); 237 } 238 } 239 }; 240 } 241 return null; 242 } 243 244 public static final AppFilter FILTER_APP_NOTIFICATION_RECENCY = new AppFilter() { 245 @Override 246 public void init() { 247 } 248 249 @Override 250 public boolean filterApp(AppEntry info) { 251 NotificationsSentState state = getNotificationsSentState(info); 252 if (state != null) { 253 return state.lastSent != 0; 254 } 255 return false; 256 } 257 }; 258 259 public static final AppFilter FILTER_APP_NOTIFICATION_FREQUENCY = new AppFilter() { 260 @Override 261 public void init() { 262 } 263 264 @Override 265 public boolean filterApp(AppEntry info) { 266 NotificationsSentState state = getNotificationsSentState(info); 267 if (state != null) { 268 return state.sentCount != 0; 269 } 270 return false; 271 } 272 }; 273 274 public static final AppFilter FILTER_APP_NOTIFICATION_BLOCKED = new AppFilter() { 275 @Override 276 public void init() { 277 } 278 279 @Override 280 public boolean filterApp(AppEntry info) { 281 NotificationsSentState state = getNotificationsSentState(info); 282 if (state != null) { 283 return state.blocked; 284 } 285 return false; 286 } 287 }; 288 289 public static final Comparator<AppEntry> RECENT_NOTIFICATION_COMPARATOR 290 = new Comparator<AppEntry>() { 291 @Override 292 public int compare(AppEntry object1, AppEntry object2) { 293 NotificationsSentState state1 = getNotificationsSentState(object1); 294 NotificationsSentState state2 = getNotificationsSentState(object2); 295 if (state1 == null && state2 != null) return -1; 296 if (state1 != null && state2 == null) return 1; 297 if (state1 != null && state2 != null) { 298 if (state1.lastSent < state2.lastSent) return 1; 299 if (state1.lastSent > state2.lastSent) return -1; 300 } 301 return ApplicationsState.ALPHA_COMPARATOR.compare(object1, object2); 302 } 303 }; 304 305 public static final Comparator<AppEntry> FREQUENCY_NOTIFICATION_COMPARATOR 306 = new Comparator<AppEntry>() { 307 @Override 308 public int compare(AppEntry object1, AppEntry object2) { 309 NotificationsSentState state1 = getNotificationsSentState(object1); 310 NotificationsSentState state2 = getNotificationsSentState(object2); 311 if (state1 == null && state2 != null) return -1; 312 if (state1 != null && state2 == null) return 1; 313 if (state1 != null && state2 != null) { 314 if (state1.sentCount < state2.sentCount) return 1; 315 if (state1.sentCount > state2.sentCount) return -1; 316 } 317 return ApplicationsState.ALPHA_COMPARATOR.compare(object1, object2); 318 } 319 }; 320 enableSwitch(AppEntry entry)321 public static final boolean enableSwitch(AppEntry entry) { 322 NotificationsSentState stats = getNotificationsSentState(entry); 323 if (stats == null) { 324 return false; 325 } 326 327 return stats.blockable; 328 } 329 checkSwitch(AppEntry entry)330 public static final boolean checkSwitch(AppEntry entry) { 331 NotificationsSentState stats = getNotificationsSentState(entry); 332 if (stats == null) { 333 return false; 334 } 335 336 return !stats.blocked; 337 } 338 339 /** 340 * NotificationsSentState contains how often an app sends notifications and how recently it sent 341 * one. 342 */ 343 public static class NotificationsSentState { 344 public int avgSentDaily = 0; 345 public int avgSentWeekly = 0; 346 public long lastSent = 0; 347 public int sentCount = 0; 348 public boolean blockable; 349 public boolean blocked; 350 public boolean systemApp; 351 } 352 } 353