• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
RecentNotifyingAppsPreferenceController(Context context, NotificationBackend backend, Application app, Fragment host)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)
RecentNotifyingAppsPreferenceController(Context context, NotificationBackend backend, ApplicationsState appState, Fragment host)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
isAvailable()113     public boolean isAvailable() {
114         return true;
115     }
116 
117     @Override
getPreferenceKey()118     public String getPreferenceKey() {
119         return KEY_PREF_CATEGORY;
120     }
121 
122     @Override
updateNonIndexableKeys(List<String> keys)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
displayPreference(PreferenceScreen screen)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
updateState(Preference preference)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
refreshUi(Context prefContext)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
reloadData()158     void reloadData() {
159         mApps = mNotificationBackend.getRecentApps();
160     }
161 
displayOnlyAllAppsLink()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 
displayRecentApps(Context prefContext, List<NotifyingApp> recentApps)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 
getDisplayableRecentAppList()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      */
shouldIncludePkgInRecents(String pkgName)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