Home | History | Annotate | Download | only in notification
      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