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.Fragment; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.pm.PackageManager; 24 import android.os.Bundle; 25 import android.os.UserHandle; 26 import android.service.notification.NotifyingApp; 27 import android.support.annotation.VisibleForTesting; 28 import android.support.v7.preference.Preference; 29 import android.support.v7.preference.PreferenceCategory; 30 import android.support.v7.preference.PreferenceScreen; 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.internal.logging.nano.MetricsProto; 38 import com.android.settings.R; 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.Arrays; 50 import java.util.Collections; 51 import java.util.List; 52 import java.util.Map; 53 import java.util.Set; 54 55 /** 56 * This controller displays a list of recently used apps and a "See all" button. If there is 57 * no recently used app, "See all" will be displayed as "Notifications". 58 */ 59 public class RecentNotifyingAppsPreferenceController extends AbstractPreferenceController 60 implements PreferenceControllerMixin { 61 62 private static final String TAG = "RecentNotisCtrl"; 63 private static final String KEY_PREF_CATEGORY = "recent_notifications_category"; 64 @VisibleForTesting 65 static final String KEY_DIVIDER = "all_notifications_divider"; 66 @VisibleForTesting 67 static final String KEY_SEE_ALL = "all_notifications"; 68 private static final int SHOW_RECENT_APP_COUNT = 5; 69 private static final Set<String> SKIP_SYSTEM_PACKAGES = new ArraySet<>(); 70 71 private final Fragment mHost; 72 private final PackageManager mPm; 73 private final NotificationBackend mNotificationBackend; 74 private final int mUserId; 75 private final IconDrawableFactory mIconDrawableFactory; 76 77 private List<NotifyingApp> mApps; 78 private final ApplicationsState mApplicationsState; 79 80 private PreferenceCategory mCategory; 81 private Preference mSeeAllPref; 82 private Preference mDivider; 83 84 static { 85 SKIP_SYSTEM_PACKAGES.addAll(Arrays.asList( 86 "android", 87 "com.android.phone", 88 "com.android.settings", 89 "com.android.systemui", 90 "com.android.providers.calendar", 91 "com.android.providers.media" 92 )); 93 } 94 95 public RecentNotifyingAppsPreferenceController(Context context, NotificationBackend backend, 96 Application app, Fragment host) { 97 this(context, backend, app == null ? null : ApplicationsState.getInstance(app), host); 98 } 99 100 @VisibleForTesting(otherwise = VisibleForTesting.NONE) 101 RecentNotifyingAppsPreferenceController(Context context, NotificationBackend backend, 102 ApplicationsState appState, Fragment host) { 103 super(context); 104 mIconDrawableFactory = IconDrawableFactory.newInstance(context); 105 mUserId = UserHandle.myUserId(); 106 mPm = context.getPackageManager(); 107 mHost = host; 108 mApplicationsState = appState; 109 mNotificationBackend = backend; 110 } 111 112 @Override 113 public boolean isAvailable() { 114 return true; 115 } 116 117 @Override 118 public String getPreferenceKey() { 119 return KEY_PREF_CATEGORY; 120 } 121 122 @Override 123 public void updateNonIndexableKeys(List<String> keys) { 124 PreferenceControllerMixin.super.updateNonIndexableKeys(keys); 125 // Don't index category name into search. It's not actionable. 126 keys.add(KEY_PREF_CATEGORY); 127 keys.add(KEY_DIVIDER); 128 } 129 130 @Override 131 public void displayPreference(PreferenceScreen screen) { 132 mCategory = (PreferenceCategory) screen.findPreference(getPreferenceKey()); 133 mSeeAllPref = screen.findPreference(KEY_SEE_ALL); 134 mDivider = screen.findPreference(KEY_DIVIDER); 135 super.displayPreference(screen); 136 refreshUi(mCategory.getContext()); 137 } 138 139 @Override 140 public void updateState(Preference preference) { 141 super.updateState(preference); 142 refreshUi(mCategory.getContext()); 143 mSeeAllPref.setTitle(mContext.getString(R.string.recent_notifications_see_all_title)); 144 } 145 146 @VisibleForTesting 147 void refreshUi(Context prefContext) { 148 reloadData(); 149 final List<NotifyingApp> recentApps = getDisplayableRecentAppList(); 150 if (recentApps != null && !recentApps.isEmpty()) { 151 displayRecentApps(prefContext, recentApps); 152 } else { 153 displayOnlyAllAppsLink(); 154 } 155 } 156 157 @VisibleForTesting 158 void reloadData() { 159 mApps = mNotificationBackend.getRecentApps(); 160 } 161 162 private void displayOnlyAllAppsLink() { 163 mCategory.setTitle(null); 164 mDivider.setVisible(false); 165 mSeeAllPref.setTitle(R.string.notifications_title); 166 mSeeAllPref.setIcon(null); 167 int prefCount = mCategory.getPreferenceCount(); 168 for (int i = prefCount - 1; i >= 0; i--) { 169 final Preference pref = mCategory.getPreference(i); 170 if (!TextUtils.equals(pref.getKey(), KEY_SEE_ALL)) { 171 mCategory.removePreference(pref); 172 } 173 } 174 } 175 176 private void displayRecentApps(Context prefContext, List<NotifyingApp> recentApps) { 177 mCategory.setTitle(R.string.recent_notifications); 178 mDivider.setVisible(true); 179 mSeeAllPref.setSummary(null); 180 mSeeAllPref.setIcon(R.drawable.ic_chevron_right_24dp); 181 182 // Rebind prefs/avoid adding new prefs if possible. Adding/removing prefs causes jank. 183 // Build a cached preference pool 184 final Map<String, NotificationAppPreference> appPreferences = new ArrayMap<>(); 185 int prefCount = mCategory.getPreferenceCount(); 186 for (int i = 0; i < prefCount; i++) { 187 final Preference pref = mCategory.getPreference(i); 188 final String key = pref.getKey(); 189 if (!TextUtils.equals(key, KEY_SEE_ALL)) { 190 appPreferences.put(key, (NotificationAppPreference) pref); 191 } 192 } 193 final int recentAppsCount = recentApps.size(); 194 for (int i = 0; i < recentAppsCount; i++) { 195 final NotifyingApp app = recentApps.get(i); 196 // Bind recent apps to existing prefs if possible, or create a new pref. 197 final String pkgName = app.getPackage(); 198 final ApplicationsState.AppEntry appEntry = 199 mApplicationsState.getEntry(app.getPackage(), mUserId); 200 if (appEntry == null) { 201 continue; 202 } 203 204 boolean rebindPref = true; 205 NotificationAppPreference pref = appPreferences.remove(pkgName); 206 if (pref == null) { 207 pref = new NotificationAppPreference(prefContext); 208 rebindPref = false; 209 } 210 pref.setKey(pkgName); 211 pref.setTitle(appEntry.label); 212 pref.setIcon(mIconDrawableFactory.getBadgedIcon(appEntry.info)); 213 pref.setIconSize(TwoTargetPreference.ICON_SIZE_SMALL); 214 pref.setSummary(StringUtil.formatRelativeTime(mContext, 215 System.currentTimeMillis() - app.getLastNotified(), true)); 216 pref.setOrder(i); 217 Bundle args = new Bundle(); 218 args.putString(AppInfoBase.ARG_PACKAGE_NAME, pkgName); 219 args.putInt(AppInfoBase.ARG_PACKAGE_UID, appEntry.info.uid); 220 221 pref.setIntent(new SubSettingLauncher(mHost.getActivity()) 222 .setDestination(AppNotificationSettings.class.getName()) 223 .setTitle(R.string.notifications_title) 224 .setArguments(args) 225 .setSourceMetricsCategory( 226 MetricsProto.MetricsEvent.MANAGE_APPLICATIONS_NOTIFICATIONS) 227 .toIntent()); 228 pref.setOnPreferenceChangeListener((preference, newValue) -> { 229 boolean blocked = !(Boolean) newValue; 230 mNotificationBackend.setNotificationsEnabledForPackage( 231 pkgName, appEntry.info.uid, !blocked); 232 return true; 233 }); 234 pref.setChecked( 235 !mNotificationBackend.getNotificationsBanned(pkgName, appEntry.info.uid)); 236 237 if (!rebindPref) { 238 mCategory.addPreference(pref); 239 } 240 } 241 // Remove unused prefs from pref cache pool 242 for (Preference unusedPrefs : appPreferences.values()) { 243 mCategory.removePreference(unusedPrefs); 244 } 245 } 246 247 private List<NotifyingApp> getDisplayableRecentAppList() { 248 Collections.sort(mApps); 249 List<NotifyingApp> displayableApps = new ArrayList<>(SHOW_RECENT_APP_COUNT); 250 int count = 0; 251 for (NotifyingApp app : mApps) { 252 final ApplicationsState.AppEntry appEntry = mApplicationsState.getEntry( 253 app.getPackage(), mUserId); 254 if (appEntry == null) { 255 continue; 256 } 257 if (!shouldIncludePkgInRecents(app.getPackage())) { 258 continue; 259 } 260 displayableApps.add(app); 261 count++; 262 if (count >= SHOW_RECENT_APP_COUNT) { 263 break; 264 } 265 } 266 return displayableApps; 267 } 268 269 270 /** 271 * Whether or not the app should be included in recent list. 272 */ 273 private boolean shouldIncludePkgInRecents(String pkgName) { 274 if (SKIP_SYSTEM_PACKAGES.contains(pkgName)) { 275 Log.d(TAG, "System package, skipping " + pkgName); 276 return false; 277 } 278 final Intent launchIntent = new Intent().addCategory(Intent.CATEGORY_LAUNCHER) 279 .setPackage(pkgName); 280 281 if (mPm.resolveActivity(launchIntent, 0) == null) { 282 // Not visible on launcher -> likely not a user visible app, skip if non-instant. 283 final ApplicationsState.AppEntry appEntry = 284 mApplicationsState.getEntry(pkgName, mUserId); 285 if (appEntry == null || appEntry.info == null || !AppUtils.isInstant(appEntry.info)) { 286 Log.d(TAG, "Not a user visible or instant app, skipping " + pkgName); 287 return false; 288 } 289 } 290 return true; 291 } 292 } 293