• 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.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