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.pm.PackageManager; 25 import android.os.Bundle; 26 import android.os.RemoteException; 27 import android.os.UserHandle; 28 import android.os.UserManager; 29 import android.service.notification.NotifyingApp; 30 import android.text.TextUtils; 31 import android.util.ArrayMap; 32 import android.util.ArraySet; 33 import android.util.IconDrawableFactory; 34 import android.util.Slog; 35 36 import com.android.settings.R; 37 import com.android.settings.Utils; 38 import com.android.settings.applications.AppInfoBase; 39 import com.android.settings.core.PreferenceControllerMixin; 40 import com.android.settings.core.SubSettingLauncher; 41 import com.android.settings.notification.app.AppNotificationSettings; 42 import com.android.settings.widget.MasterSwitchPreference; 43 import com.android.settingslib.TwoTargetPreference; 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 71 @VisibleForTesting 72 static final String KEY_SEE_ALL = "all_notifications"; 73 private static final int SHOW_RECENT_APP_COUNT = 3; 74 private static final int DAYS = 3; 75 private static final Set<String> SKIP_SYSTEM_PACKAGES = new ArraySet<>(); 76 77 private final Fragment mHost; 78 private final PackageManager mPm; 79 private final NotificationBackend mNotificationBackend; 80 private IUsageStatsManager mUsageStatsManager; 81 private final IconDrawableFactory mIconDrawableFactory; 82 83 private Calendar mCal; 84 List<NotifyingApp> mApps; 85 private final ApplicationsState mApplicationsState; 86 87 private PreferenceCategory mCategory; 88 private Preference mSeeAllPref; 89 protected List<Integer> mUserIds; 90 RecentNotifyingAppsPreferenceController(Context context, NotificationBackend backend, IUsageStatsManager usageStatsManager, UserManager userManager, Application app, Fragment host)91 public RecentNotifyingAppsPreferenceController(Context context, NotificationBackend backend, 92 IUsageStatsManager usageStatsManager, UserManager userManager, 93 Application app, Fragment host) { 94 this(context, backend, usageStatsManager, userManager, 95 app == null ? null : ApplicationsState.getInstance(app), host); 96 } 97 98 @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) RecentNotifyingAppsPreferenceController(Context context, NotificationBackend backend, IUsageStatsManager usageStatsManager, UserManager userManager, ApplicationsState appState, Fragment host)99 RecentNotifyingAppsPreferenceController(Context context, NotificationBackend backend, 100 IUsageStatsManager usageStatsManager, UserManager userManager, 101 ApplicationsState appState, Fragment host) { 102 super(context); 103 mIconDrawableFactory = IconDrawableFactory.newInstance(context); 104 mPm = context.getPackageManager(); 105 mHost = host; 106 mApplicationsState = appState; 107 mNotificationBackend = backend; 108 mUsageStatsManager = usageStatsManager; 109 mUserIds = new ArrayList<>(); 110 mUserIds.add(mContext.getUserId()); 111 int workUserId = Utils.getManagedProfileId(userManager, mContext.getUserId()); 112 if (workUserId != UserHandle.USER_NULL) { 113 mUserIds.add(workUserId); 114 } 115 } 116 117 @Override isAvailable()118 public boolean isAvailable() { 119 return mApplicationsState != null; 120 } 121 122 @Override getPreferenceKey()123 public String getPreferenceKey() { 124 return KEY_PREF_CATEGORY; 125 } 126 127 @Override updateNonIndexableKeys(List<String> keys)128 public void updateNonIndexableKeys(List<String> keys) { 129 PreferenceControllerMixin.super.updateNonIndexableKeys(keys); 130 // Don't index category name into search. It's not actionable. 131 keys.add(KEY_PREF_CATEGORY); 132 } 133 134 @Override displayPreference(PreferenceScreen screen)135 public void displayPreference(PreferenceScreen screen) { 136 mCategory = screen.findPreference(getPreferenceKey()); 137 mSeeAllPref = screen.findPreference(KEY_SEE_ALL); 138 super.displayPreference(screen); 139 refreshUi(mCategory.getContext()); 140 } 141 142 @Override updateState(Preference preference)143 public void updateState(Preference preference) { 144 super.updateState(preference); 145 refreshUi(mCategory.getContext()); 146 mSeeAllPref.setTitle(mContext.getString(R.string.recent_notifications_see_all_title)); 147 } 148 149 @VisibleForTesting refreshUi(Context prefContext)150 void refreshUi(Context prefContext) { 151 reloadData(); 152 final List<NotifyingApp> recentApps = getDisplayableRecentAppList(); 153 if (recentApps != null && !recentApps.isEmpty()) { 154 displayRecentApps(prefContext, recentApps); 155 } else { 156 displayOnlyAllAppsLink(); 157 } 158 } 159 160 @VisibleForTesting reloadData()161 void reloadData() { 162 mApps = new ArrayList<>(); 163 mCal = Calendar.getInstance(); 164 mCal.add(Calendar.DAY_OF_YEAR, -DAYS); 165 for (int userId : mUserIds) { 166 UsageEvents events = null; 167 try { 168 events = mUsageStatsManager.queryEventsForUser(mCal.getTimeInMillis(), 169 System.currentTimeMillis(), userId, mContext.getPackageName()); 170 } catch (RemoteException e) { 171 e.printStackTrace(); 172 } 173 if (events != null) { 174 ArrayMap<String, NotifyingApp> aggregatedStats = new ArrayMap<>(); 175 176 UsageEvents.Event event = new UsageEvents.Event(); 177 while (events.hasNextEvent()) { 178 events.getNextEvent(event); 179 180 if (event.getEventType() == UsageEvents.Event.NOTIFICATION_INTERRUPTION) { 181 NotifyingApp app = 182 aggregatedStats.get(getKey(userId, event.getPackageName())); 183 if (app == null) { 184 app = new NotifyingApp(); 185 aggregatedStats.put(getKey(userId, event.getPackageName()), app); 186 app.setPackage(event.getPackageName()); 187 app.setUserId(userId); 188 } 189 if (event.getTimeStamp() > app.getLastNotified()) { 190 app.setLastNotified(event.getTimeStamp()); 191 } 192 } 193 194 } 195 196 mApps.addAll(aggregatedStats.values()); 197 } 198 } 199 } 200 201 @VisibleForTesting getKey(int userId, String pkg)202 static String getKey(int userId, String pkg) { 203 return userId + "|" + pkg; 204 } 205 displayOnlyAllAppsLink()206 private void displayOnlyAllAppsLink() { 207 mCategory.setTitle(null); 208 mSeeAllPref.setTitle(R.string.notifications_title); 209 mSeeAllPref.setIcon(null); 210 int prefCount = mCategory.getPreferenceCount(); 211 for (int i = prefCount - 1; i >= 0; i--) { 212 final Preference pref = mCategory.getPreference(i); 213 if (!TextUtils.equals(pref.getKey(), KEY_SEE_ALL)) { 214 mCategory.removePreference(pref); 215 } 216 } 217 } 218 displayRecentApps(Context prefContext, List<NotifyingApp> recentApps)219 private void displayRecentApps(Context prefContext, List<NotifyingApp> recentApps) { 220 mCategory.setTitle(R.string.recent_notifications); 221 mSeeAllPref.setSummary(null); 222 mSeeAllPref.setIcon(R.drawable.ic_chevron_right_24dp); 223 224 // Rebind prefs/avoid adding new prefs if possible. Adding/removing prefs causes jank. 225 // Build a cached preference pool 226 final Map<String, MasterSwitchPreference> appPreferences = new ArrayMap<>(); 227 int prefCount = mCategory.getPreferenceCount(); 228 for (int i = 0; i < prefCount; i++) { 229 final Preference pref = mCategory.getPreference(i); 230 final String key = pref.getKey(); 231 if (!TextUtils.equals(key, KEY_SEE_ALL)) { 232 appPreferences.put(key, (MasterSwitchPreference) pref); 233 } 234 } 235 final int recentAppsCount = recentApps.size(); 236 for (int i = 0; i < recentAppsCount; i++) { 237 final NotifyingApp app = recentApps.get(i); 238 // Bind recent apps to existing prefs if possible, or create a new pref. 239 final String pkgName = app.getPackage(); 240 final ApplicationsState.AppEntry appEntry = 241 mApplicationsState.getEntry(app.getPackage(), app.getUserId()); 242 if (appEntry == null) { 243 continue; 244 } 245 246 boolean rebindPref = true; 247 MasterSwitchPreference pref = appPreferences.remove(getKey(app.getUserId(), 248 pkgName)); 249 if (pref == null) { 250 pref = new MasterSwitchPreference(prefContext); 251 rebindPref = false; 252 } 253 pref.setKey(getKey(app.getUserId(), pkgName)); 254 pref.setTitle(appEntry.label); 255 pref.setIcon(mIconDrawableFactory.getBadgedIcon(appEntry.info)); 256 pref.setIconSize(TwoTargetPreference.ICON_SIZE_SMALL); 257 pref.setSummary(StringUtil.formatRelativeTime(mContext, 258 System.currentTimeMillis() - app.getLastNotified(), true)); 259 pref.setOrder(i); 260 Bundle args = new Bundle(); 261 args.putString(AppInfoBase.ARG_PACKAGE_NAME, pkgName); 262 args.putInt(AppInfoBase.ARG_PACKAGE_UID, appEntry.info.uid); 263 pref.setOnPreferenceClickListener(preference -> { 264 new SubSettingLauncher(mHost.getActivity()) 265 .setDestination(AppNotificationSettings.class.getName()) 266 .setTitleRes(R.string.notifications_title) 267 .setArguments(args) 268 .setUserHandle(new UserHandle(UserHandle.getUserId(appEntry.info.uid))) 269 .setSourceMetricsCategory( 270 SettingsEnums.MANAGE_APPLICATIONS_NOTIFICATIONS) 271 .launch(); 272 return true; 273 }); 274 pref.setSwitchEnabled(mNotificationBackend.isBlockable(mContext, appEntry.info)); 275 pref.setOnPreferenceChangeListener((preference, newValue) -> { 276 mNotificationBackend.setNotificationsEnabledForPackage( 277 pkgName, appEntry.info.uid, (Boolean) newValue); 278 return true; 279 }); 280 pref.setChecked( 281 !mNotificationBackend.getNotificationsBanned(pkgName, appEntry.info.uid)); 282 283 if (!rebindPref) { 284 mCategory.addPreference(pref); 285 } 286 } 287 // Remove unused prefs from pref cache pool 288 for (Preference unusedPrefs : appPreferences.values()) { 289 mCategory.removePreference(unusedPrefs); 290 } 291 } 292 getDisplayableRecentAppList()293 private List<NotifyingApp> getDisplayableRecentAppList() { 294 Collections.sort(mApps); 295 List<NotifyingApp> displayableApps = new ArrayList<>(SHOW_RECENT_APP_COUNT); 296 int count = 0; 297 for (NotifyingApp app : mApps) { 298 try { 299 final ApplicationsState.AppEntry appEntry = mApplicationsState.getEntry( 300 app.getPackage(), app.getUserId()); 301 if (appEntry == null) { 302 continue; 303 } 304 displayableApps.add(app); 305 count++; 306 if (count >= SHOW_RECENT_APP_COUNT) { 307 break; 308 } 309 } catch (Exception e) { 310 Slog.e(TAG, "Failed to find app " + app.getPackage() + "/" + app.getUserId(), e); 311 } 312 } 313 return displayableApps; 314 } 315 } 316