1 /* 2 * Copyright (C) 2018 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 17 package com.android.settings.notification; 18 19 import android.app.Application; 20 import android.app.settings.SettingsEnums; 21 import android.app.usage.IUsageStatsManager; 22 import android.app.usage.UsageEvents; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.pm.PackageManager; 26 import android.os.Bundle; 27 import android.os.RemoteException; 28 import android.os.UserHandle; 29 import android.os.UserManager; 30 import android.service.notification.NotifyingApp; 31 import android.text.TextUtils; 32 import android.util.ArrayMap; 33 import android.util.ArraySet; 34 import android.util.IconDrawableFactory; 35 import android.util.Log; 36 37 import com.android.settings.R; 38 import com.android.settings.Utils; 39 import com.android.settings.applications.AppInfoBase; 40 import com.android.settings.core.PreferenceControllerMixin; 41 import com.android.settings.core.SubSettingLauncher; 42 import com.android.settingslib.TwoTargetPreference; 43 import com.android.settingslib.applications.AppUtils; 44 import com.android.settingslib.applications.ApplicationsState; 45 import com.android.settingslib.core.AbstractPreferenceController; 46 import com.android.settingslib.utils.StringUtil; 47 48 import java.util.ArrayList; 49 import java.util.Calendar; 50 import java.util.Collections; 51 import java.util.List; 52 import java.util.Map; 53 import java.util.Set; 54 55 import androidx.annotation.VisibleForTesting; 56 import androidx.fragment.app.Fragment; 57 import androidx.preference.Preference; 58 import androidx.preference.PreferenceCategory; 59 import androidx.preference.PreferenceScreen; 60 61 /** 62 * This controller displays a list of recently used apps and a "See all" button. If there is 63 * no recently used app, "See all" will be displayed as "Notifications". 64 */ 65 public class RecentNotifyingAppsPreferenceController extends AbstractPreferenceController 66 implements PreferenceControllerMixin { 67 68 private static final String TAG = "RecentNotisCtrl"; 69 private static final String KEY_PREF_CATEGORY = "recent_notifications_category"; 70 @VisibleForTesting 71 static final String KEY_DIVIDER = "all_notifications_divider"; 72 @VisibleForTesting 73 static final String KEY_SEE_ALL = "all_notifications"; 74 private static final int SHOW_RECENT_APP_COUNT = 3; 75 private static final int DAYS = 3; 76 private static final Set<String> SKIP_SYSTEM_PACKAGES = new ArraySet<>(); 77 78 private final Fragment mHost; 79 private final PackageManager mPm; 80 private final NotificationBackend mNotificationBackend; 81 private IUsageStatsManager mUsageStatsManager; 82 private final IconDrawableFactory mIconDrawableFactory; 83 84 private Calendar mCal; 85 List<NotifyingApp> mApps; 86 private final ApplicationsState mApplicationsState; 87 88 private PreferenceCategory mCategory; 89 private Preference mSeeAllPref; 90 private Preference mDivider; 91 protected List<Integer> mUserIds; 92 RecentNotifyingAppsPreferenceController(Context context, NotificationBackend backend, IUsageStatsManager usageStatsManager, UserManager userManager, Application app, Fragment host)93 public RecentNotifyingAppsPreferenceController(Context context, NotificationBackend backend, 94 IUsageStatsManager usageStatsManager, UserManager userManager, 95 Application app, Fragment host) { 96 this(context, backend, usageStatsManager, userManager, 97 app == null ? null : ApplicationsState.getInstance(app), host); 98 } 99 100 @VisibleForTesting(otherwise = VisibleForTesting.NONE) RecentNotifyingAppsPreferenceController(Context context, NotificationBackend backend, IUsageStatsManager usageStatsManager, UserManager userManager, ApplicationsState appState, Fragment host)101 RecentNotifyingAppsPreferenceController(Context context, NotificationBackend backend, 102 IUsageStatsManager usageStatsManager, UserManager userManager, 103 ApplicationsState appState, Fragment host) { 104 super(context); 105 mIconDrawableFactory = IconDrawableFactory.newInstance(context); 106 mPm = context.getPackageManager(); 107 mHost = host; 108 mApplicationsState = appState; 109 mNotificationBackend = backend; 110 mUsageStatsManager = usageStatsManager; 111 mUserIds = new ArrayList<>(); 112 mUserIds.add(mContext.getUserId()); 113 int workUserId = Utils.getManagedProfileId(userManager, mContext.getUserId()); 114 if (workUserId != UserHandle.USER_NULL) { 115 mUserIds.add(workUserId); 116 } 117 } 118 119 @Override isAvailable()120 public boolean isAvailable() { 121 return true; 122 } 123 124 @Override getPreferenceKey()125 public String getPreferenceKey() { 126 return KEY_PREF_CATEGORY; 127 } 128 129 @Override updateNonIndexableKeys(List<String> keys)130 public void updateNonIndexableKeys(List<String> keys) { 131 PreferenceControllerMixin.super.updateNonIndexableKeys(keys); 132 // Don't index category name into search. It's not actionable. 133 keys.add(KEY_PREF_CATEGORY); 134 keys.add(KEY_DIVIDER); 135 } 136 137 @Override displayPreference(PreferenceScreen screen)138 public void displayPreference(PreferenceScreen screen) { 139 mCategory = screen.findPreference(getPreferenceKey()); 140 mSeeAllPref = screen.findPreference(KEY_SEE_ALL); 141 mDivider = screen.findPreference(KEY_DIVIDER); 142 super.displayPreference(screen); 143 refreshUi(mCategory.getContext()); 144 } 145 146 @Override updateState(Preference preference)147 public void updateState(Preference preference) { 148 super.updateState(preference); 149 refreshUi(mCategory.getContext()); 150 mSeeAllPref.setTitle(mContext.getString(R.string.recent_notifications_see_all_title)); 151 } 152 153 @VisibleForTesting refreshUi(Context prefContext)154 void refreshUi(Context prefContext) { 155 reloadData(); 156 final List<NotifyingApp> recentApps = getDisplayableRecentAppList(); 157 if (recentApps != null && !recentApps.isEmpty()) { 158 displayRecentApps(prefContext, recentApps); 159 } else { 160 displayOnlyAllAppsLink(); 161 } 162 } 163 164 @VisibleForTesting reloadData()165 void reloadData() { 166 mApps = new ArrayList<>(); 167 mCal = Calendar.getInstance(); 168 mCal.add(Calendar.DAY_OF_YEAR, -DAYS); 169 for (int userId : mUserIds) { 170 UsageEvents events = null; 171 try { 172 events = mUsageStatsManager.queryEventsForUser(mCal.getTimeInMillis(), 173 System.currentTimeMillis(), userId, mContext.getPackageName()); 174 } catch (RemoteException e) { 175 e.printStackTrace(); 176 } 177 if (events != null) { 178 ArrayMap<String, NotifyingApp> aggregatedStats = new ArrayMap<>(); 179 180 UsageEvents.Event event = new UsageEvents.Event(); 181 while (events.hasNextEvent()) { 182 events.getNextEvent(event); 183 184 if (event.getEventType() == UsageEvents.Event.NOTIFICATION_INTERRUPTION) { 185 NotifyingApp app = 186 aggregatedStats.get(getKey(userId, event.getPackageName())); 187 if (app == null) { 188 app = new NotifyingApp(); 189 aggregatedStats.put(getKey(userId, event.getPackageName()), app); 190 app.setPackage(event.getPackageName()); 191 app.setUserId(userId); 192 } 193 if (event.getTimeStamp() > app.getLastNotified()) { 194 app.setLastNotified(event.getTimeStamp()); 195 } 196 } 197 198 } 199 200 mApps.addAll(aggregatedStats.values()); 201 } 202 } 203 } 204 205 @VisibleForTesting getKey(int userId, String pkg)206 static String getKey(int userId, String pkg) { 207 return userId + "|" + pkg; 208 } 209 displayOnlyAllAppsLink()210 private void displayOnlyAllAppsLink() { 211 mCategory.setTitle(null); 212 mDivider.setVisible(false); 213 mSeeAllPref.setTitle(R.string.notifications_title); 214 mSeeAllPref.setIcon(null); 215 int prefCount = mCategory.getPreferenceCount(); 216 for (int i = prefCount - 1; i >= 0; i--) { 217 final Preference pref = mCategory.getPreference(i); 218 if (!TextUtils.equals(pref.getKey(), KEY_SEE_ALL)) { 219 mCategory.removePreference(pref); 220 } 221 } 222 } 223 displayRecentApps(Context prefContext, List<NotifyingApp> recentApps)224 private void displayRecentApps(Context prefContext, List<NotifyingApp> recentApps) { 225 mCategory.setTitle(R.string.recent_notifications); 226 mDivider.setVisible(true); 227 mSeeAllPref.setSummary(null); 228 mSeeAllPref.setIcon(R.drawable.ic_chevron_right_24dp); 229 230 // Rebind prefs/avoid adding new prefs if possible. Adding/removing prefs causes jank. 231 // Build a cached preference pool 232 final Map<String, NotificationAppPreference> appPreferences = new ArrayMap<>(); 233 int prefCount = mCategory.getPreferenceCount(); 234 for (int i = 0; i < prefCount; i++) { 235 final Preference pref = mCategory.getPreference(i); 236 final String key = pref.getKey(); 237 if (!TextUtils.equals(key, KEY_SEE_ALL)) { 238 appPreferences.put(key, (NotificationAppPreference) pref); 239 } 240 } 241 final int recentAppsCount = recentApps.size(); 242 for (int i = 0; i < recentAppsCount; i++) { 243 final NotifyingApp app = recentApps.get(i); 244 // Bind recent apps to existing prefs if possible, or create a new pref. 245 final String pkgName = app.getPackage(); 246 final ApplicationsState.AppEntry appEntry = 247 mApplicationsState.getEntry(app.getPackage(), app.getUserId()); 248 if (appEntry == null) { 249 continue; 250 } 251 252 boolean rebindPref = true; 253 NotificationAppPreference pref = appPreferences.remove(getKey(app.getUserId(), 254 pkgName)); 255 if (pref == null) { 256 pref = new NotificationAppPreference(prefContext); 257 rebindPref = false; 258 } 259 pref.setKey(getKey(app.getUserId(), pkgName)); 260 pref.setTitle(appEntry.label); 261 pref.setIcon(mIconDrawableFactory.getBadgedIcon(appEntry.info)); 262 pref.setIconSize(TwoTargetPreference.ICON_SIZE_SMALL); 263 pref.setSummary(StringUtil.formatRelativeTime(mContext, 264 System.currentTimeMillis() - app.getLastNotified(), true)); 265 pref.setOrder(i); 266 Bundle args = new Bundle(); 267 args.putString(AppInfoBase.ARG_PACKAGE_NAME, pkgName); 268 args.putInt(AppInfoBase.ARG_PACKAGE_UID, appEntry.info.uid); 269 pref.setIntent(new SubSettingLauncher(mHost.getActivity()) 270 .setDestination(AppNotificationSettings.class.getName()) 271 .setTitleRes(R.string.notifications_title) 272 .setArguments(args) 273 .setUserHandle(new UserHandle(UserHandle.getUserId(appEntry.info.uid))) 274 .setSourceMetricsCategory( 275 SettingsEnums.MANAGE_APPLICATIONS_NOTIFICATIONS) 276 .toIntent()); 277 pref.setSwitchEnabled(mNotificationBackend.isBlockable(mContext, appEntry.info)); 278 pref.setOnPreferenceChangeListener((preference, newValue) -> { 279 boolean blocked = !(Boolean) newValue; 280 mNotificationBackend.setNotificationsEnabledForPackage( 281 pkgName, appEntry.info.uid, !blocked); 282 return true; 283 }); 284 pref.setChecked( 285 !mNotificationBackend.getNotificationsBanned(pkgName, appEntry.info.uid)); 286 287 if (!rebindPref) { 288 mCategory.addPreference(pref); 289 } 290 } 291 // Remove unused prefs from pref cache pool 292 for (Preference unusedPrefs : appPreferences.values()) { 293 mCategory.removePreference(unusedPrefs); 294 } 295 } 296 getDisplayableRecentAppList()297 private List<NotifyingApp> getDisplayableRecentAppList() { 298 Collections.sort(mApps); 299 List<NotifyingApp> displayableApps = new ArrayList<>(SHOW_RECENT_APP_COUNT); 300 int count = 0; 301 for (NotifyingApp app : mApps) { 302 final ApplicationsState.AppEntry appEntry = mApplicationsState.getEntry( 303 app.getPackage(), app.getUserId()); 304 if (appEntry == null) { 305 continue; 306 } 307 if (!shouldIncludePkgInRecents(app.getPackage(), app.getUserId())) { 308 continue; 309 } 310 displayableApps.add(app); 311 count++; 312 if (count >= SHOW_RECENT_APP_COUNT) { 313 break; 314 } 315 } 316 return displayableApps; 317 } 318 319 320 /** 321 * Whether or not the app should be included in recent list. 322 */ shouldIncludePkgInRecents(String pkgName, int userId)323 private boolean shouldIncludePkgInRecents(String pkgName, int userId) { 324 final Intent launchIntent = new Intent().addCategory(Intent.CATEGORY_LAUNCHER) 325 .setPackage(pkgName); 326 327 if (mPm.resolveActivity(launchIntent, 0) == null) { 328 // Not visible on launcher -> likely not a user visible app, skip if non-instant. 329 final ApplicationsState.AppEntry appEntry = 330 mApplicationsState.getEntry(pkgName, userId); 331 if (appEntry == null || appEntry.info == null || !AppUtils.isInstant(appEntry.info)) { 332 Log.d(TAG, "Not a user visible or instant app, skipping " + pkgName); 333 return false; 334 } 335 } 336 return true; 337 } 338 } 339