Home | History | Annotate | Download | only in notification
      1 /*
      2  * Copyright (C) 2014 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 static com.android.settings.notification.AppNotificationSettings.EXTRA_HAS_SETTINGS_INTENT;
     20 import static com.android.settings.notification.AppNotificationSettings.EXTRA_SETTINGS_INTENT;
     21 
     22 import android.animation.LayoutTransition;
     23 import android.app.INotificationManager;
     24 import android.app.Notification;
     25 import android.content.Context;
     26 import android.content.Intent;
     27 import android.content.pm.ActivityInfo;
     28 import android.content.pm.ApplicationInfo;
     29 import android.content.pm.LauncherActivityInfo;
     30 import android.content.pm.LauncherApps;
     31 import android.content.pm.PackageManager;
     32 import android.content.pm.ResolveInfo;
     33 import android.content.pm.Signature;
     34 import android.graphics.drawable.Drawable;
     35 import android.os.AsyncTask;
     36 import android.os.Bundle;
     37 import android.os.Handler;
     38 import android.os.Parcelable;
     39 import android.os.ServiceManager;
     40 import android.os.SystemClock;
     41 import android.os.UserHandle;
     42 import android.os.UserManager;
     43 import android.provider.Settings;
     44 import android.service.notification.NotificationListenerService;
     45 import android.util.ArrayMap;
     46 import android.util.Log;
     47 import android.util.TypedValue;
     48 import android.view.LayoutInflater;
     49 import android.view.View;
     50 import android.view.View.OnClickListener;
     51 import android.view.ViewGroup;
     52 import android.widget.AdapterView;
     53 import android.widget.AdapterView.OnItemSelectedListener;
     54 import android.widget.ArrayAdapter;
     55 import android.widget.ImageView;
     56 import android.widget.SectionIndexer;
     57 import android.widget.Spinner;
     58 import android.widget.TextView;
     59 
     60 import com.android.settings.PinnedHeaderListFragment;
     61 import com.android.settings.R;
     62 import com.android.settings.Settings.NotificationAppListActivity;
     63 import com.android.settings.UserSpinnerAdapter;
     64 import com.android.settings.Utils;
     65 
     66 import java.text.Collator;
     67 import java.util.ArrayList;
     68 import java.util.Collections;
     69 import java.util.Comparator;
     70 import java.util.List;
     71 
     72 /** Just a sectioned list of installed applications, nothing else to index **/
     73 public class NotificationAppList extends PinnedHeaderListFragment
     74         implements OnItemSelectedListener {
     75     private static final String TAG = "NotificationAppList";
     76     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
     77 
     78     private static final String EMPTY_SUBTITLE = "";
     79     private static final String SECTION_BEFORE_A = "*";
     80     private static final String SECTION_AFTER_Z = "**";
     81     private static final Intent APP_NOTIFICATION_PREFS_CATEGORY_INTENT
     82             = new Intent(Intent.ACTION_MAIN)
     83                 .addCategory(Notification.INTENT_CATEGORY_NOTIFICATION_PREFERENCES);
     84 
     85     private final Handler mHandler = new Handler();
     86     private final ArrayMap<String, AppRow> mRows = new ArrayMap<String, AppRow>();
     87     private final ArrayList<AppRow> mSortedRows = new ArrayList<AppRow>();
     88     private final ArrayList<String> mSections = new ArrayList<String>();
     89 
     90     private Context mContext;
     91     private LayoutInflater mInflater;
     92     private NotificationAppAdapter mAdapter;
     93     private Signature[] mSystemSignature;
     94     private Parcelable mListViewState;
     95     private Backend mBackend = new Backend();
     96     private UserSpinnerAdapter mProfileSpinnerAdapter;
     97     private Spinner mSpinner;
     98 
     99     private PackageManager mPM;
    100     private UserManager mUM;
    101     private LauncherApps mLauncherApps;
    102 
    103     @Override
    104     public void onCreate(Bundle savedInstanceState) {
    105         super.onCreate(savedInstanceState);
    106         mContext = getActivity();
    107         mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    108         mAdapter = new NotificationAppAdapter(mContext);
    109         mUM = UserManager.get(mContext);
    110         mPM = mContext.getPackageManager();
    111         mLauncherApps = (LauncherApps) mContext.getSystemService(Context.LAUNCHER_APPS_SERVICE);
    112         getActivity().setTitle(R.string.app_notifications_title);
    113     }
    114 
    115     @Override
    116     public View onCreateView(LayoutInflater inflater, ViewGroup container,
    117             Bundle savedInstanceState) {
    118         return inflater.inflate(R.layout.notification_app_list, container, false);
    119     }
    120 
    121     @Override
    122     public void onViewCreated(View view, Bundle savedInstanceState) {
    123         super.onViewCreated(view, savedInstanceState);
    124         mProfileSpinnerAdapter = Utils.createUserSpinnerAdapter(mUM, mContext);
    125         if (mProfileSpinnerAdapter != null) {
    126             mSpinner = (Spinner) getActivity().getLayoutInflater().inflate(
    127                     R.layout.spinner_view, null);
    128             mSpinner.setAdapter(mProfileSpinnerAdapter);
    129             mSpinner.setOnItemSelectedListener(this);
    130             setPinnedHeaderView(mSpinner);
    131         }
    132     }
    133 
    134     @Override
    135     public void onActivityCreated(Bundle savedInstanceState) {
    136         super.onActivityCreated(savedInstanceState);
    137         repositionScrollbar();
    138         getListView().setAdapter(mAdapter);
    139     }
    140 
    141     @Override
    142     public void onPause() {
    143         super.onPause();
    144         if (DEBUG) Log.d(TAG, "Saving listView state");
    145         mListViewState = getListView().onSaveInstanceState();
    146     }
    147 
    148     @Override
    149     public void onDestroyView() {
    150         super.onDestroyView();
    151         mListViewState = null;  // you're dead to me
    152     }
    153 
    154     @Override
    155     public void onResume() {
    156         super.onResume();
    157         loadAppsList();
    158     }
    159 
    160     @Override
    161     public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
    162         UserHandle selectedUser = mProfileSpinnerAdapter.getUserHandle(position);
    163         if (selectedUser.getIdentifier() != UserHandle.myUserId()) {
    164             Intent intent = new Intent(getActivity(), NotificationAppListActivity.class);
    165             intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    166             intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
    167             mContext.startActivityAsUser(intent, selectedUser);
    168             // Go back to default selection, which is the first one; this makes sure that pressing
    169             // the back button takes you into a consistent state
    170             mSpinner.setSelection(0);
    171         }
    172     }
    173 
    174     @Override
    175     public void onNothingSelected(AdapterView<?> parent) {
    176     }
    177 
    178     public void setBackend(Backend backend) {
    179         mBackend = backend;
    180     }
    181 
    182     private void loadAppsList() {
    183         AsyncTask.execute(mCollectAppsRunnable);
    184     }
    185 
    186     private String getSection(CharSequence label) {
    187         if (label == null || label.length() == 0) return SECTION_BEFORE_A;
    188         final char c = Character.toUpperCase(label.charAt(0));
    189         if (c < 'A') return SECTION_BEFORE_A;
    190         if (c > 'Z') return SECTION_AFTER_Z;
    191         return Character.toString(c);
    192     }
    193 
    194     private void repositionScrollbar() {
    195         final int sbWidthPx = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
    196                 getListView().getScrollBarSize(),
    197                 getResources().getDisplayMetrics());
    198         final View parent = (View)getView().getParent();
    199         final int eat = Math.min(sbWidthPx, parent.getPaddingEnd());
    200         if (eat <= 0) return;
    201         if (DEBUG) Log.d(TAG, String.format("Eating %dpx into %dpx padding for %dpx scroll, ld=%d",
    202                 eat, parent.getPaddingEnd(), sbWidthPx, getListView().getLayoutDirection()));
    203         parent.setPaddingRelative(parent.getPaddingStart(), parent.getPaddingTop(),
    204                 parent.getPaddingEnd() - eat, parent.getPaddingBottom());
    205     }
    206 
    207     private static class ViewHolder {
    208         ViewGroup row;
    209         ImageView icon;
    210         TextView title;
    211         TextView subtitle;
    212         View rowDivider;
    213     }
    214 
    215     private class NotificationAppAdapter extends ArrayAdapter<Row> implements SectionIndexer {
    216         public NotificationAppAdapter(Context context) {
    217             super(context, 0, 0);
    218         }
    219 
    220         @Override
    221         public boolean hasStableIds() {
    222             return true;
    223         }
    224 
    225         @Override
    226         public long getItemId(int position) {
    227             return position;
    228         }
    229 
    230         @Override
    231         public int getViewTypeCount() {
    232             return 2;
    233         }
    234 
    235         @Override
    236         public int getItemViewType(int position) {
    237             Row r = getItem(position);
    238             return r instanceof AppRow ? 1 : 0;
    239         }
    240 
    241         public View getView(int position, View convertView, ViewGroup parent) {
    242             Row r = getItem(position);
    243             View v;
    244             if (convertView == null) {
    245                 v = newView(parent, r);
    246             } else {
    247                 v = convertView;
    248             }
    249             bindView(v, r, false /*animate*/);
    250             return v;
    251         }
    252 
    253         public View newView(ViewGroup parent, Row r) {
    254             if (!(r instanceof AppRow)) {
    255                 return mInflater.inflate(R.layout.notification_app_section, parent, false);
    256             }
    257             final View v = mInflater.inflate(R.layout.notification_app, parent, false);
    258             final ViewHolder vh = new ViewHolder();
    259             vh.row = (ViewGroup) v;
    260             vh.row.setLayoutTransition(new LayoutTransition());
    261             vh.row.setLayoutTransition(new LayoutTransition());
    262             vh.icon = (ImageView) v.findViewById(android.R.id.icon);
    263             vh.title = (TextView) v.findViewById(android.R.id.title);
    264             vh.subtitle = (TextView) v.findViewById(android.R.id.text1);
    265             vh.rowDivider = v.findViewById(R.id.row_divider);
    266             v.setTag(vh);
    267             return v;
    268         }
    269 
    270         private void enableLayoutTransitions(ViewGroup vg, boolean enabled) {
    271             if (enabled) {
    272                 vg.getLayoutTransition().enableTransitionType(LayoutTransition.APPEARING);
    273                 vg.getLayoutTransition().enableTransitionType(LayoutTransition.DISAPPEARING);
    274             } else {
    275                 vg.getLayoutTransition().disableTransitionType(LayoutTransition.APPEARING);
    276                 vg.getLayoutTransition().disableTransitionType(LayoutTransition.DISAPPEARING);
    277             }
    278         }
    279 
    280         public void bindView(final View view, Row r, boolean animate) {
    281             if (!(r instanceof AppRow)) {
    282                 // it's a section row
    283                 final TextView tv = (TextView)view.findViewById(android.R.id.title);
    284                 tv.setText(r.section);
    285                 return;
    286             }
    287 
    288             final AppRow row = (AppRow)r;
    289             final ViewHolder vh = (ViewHolder) view.getTag();
    290             enableLayoutTransitions(vh.row, animate);
    291             vh.rowDivider.setVisibility(row.first ? View.GONE : View.VISIBLE);
    292             vh.row.setOnClickListener(new OnClickListener() {
    293                 @Override
    294                 public void onClick(View v) {
    295                     mContext.startActivity(new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
    296                             .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
    297                             .putExtra(Settings.EXTRA_APP_PACKAGE, row.pkg)
    298                             .putExtra(Settings.EXTRA_APP_UID, row.uid)
    299                             .putExtra(EXTRA_HAS_SETTINGS_INTENT, row.settingsIntent != null)
    300                             .putExtra(EXTRA_SETTINGS_INTENT, row.settingsIntent));
    301                 }
    302             });
    303             enableLayoutTransitions(vh.row, animate);
    304             vh.icon.setImageDrawable(row.icon);
    305             vh.title.setText(row.label);
    306             final String sub = getSubtitle(row);
    307             vh.subtitle.setText(sub);
    308             vh.subtitle.setVisibility(!sub.isEmpty() ? View.VISIBLE : View.GONE);
    309         }
    310 
    311         private String getSubtitle(AppRow row) {
    312             if (row.banned) {
    313                 return mContext.getString(R.string.app_notification_row_banned);
    314             }
    315             if (!row.priority && !row.sensitive) {
    316                 return EMPTY_SUBTITLE;
    317             }
    318             final String priString = mContext.getString(R.string.app_notification_row_priority);
    319             final String senString = mContext.getString(R.string.app_notification_row_sensitive);
    320             if (row.priority != row.sensitive) {
    321                 return row.priority ? priString : senString;
    322             }
    323             return priString + mContext.getString(R.string.summary_divider_text) + senString;
    324         }
    325 
    326         @Override
    327         public Object[] getSections() {
    328             return mSections.toArray(new Object[mSections.size()]);
    329         }
    330 
    331         @Override
    332         public int getPositionForSection(int sectionIndex) {
    333             final String section = mSections.get(sectionIndex);
    334             final int n = getCount();
    335             for (int i = 0; i < n; i++) {
    336                 final Row r = getItem(i);
    337                 if (r.section.equals(section)) {
    338                     return i;
    339                 }
    340             }
    341             return 0;
    342         }
    343 
    344         @Override
    345         public int getSectionForPosition(int position) {
    346             Row row = getItem(position);
    347             return mSections.indexOf(row.section);
    348         }
    349     }
    350 
    351     private static class Row {
    352         public String section;
    353     }
    354 
    355     public static class AppRow extends Row {
    356         public String pkg;
    357         public int uid;
    358         public Drawable icon;
    359         public CharSequence label;
    360         public Intent settingsIntent;
    361         public boolean banned;
    362         public boolean priority;
    363         public boolean sensitive;
    364         public boolean first;  // first app in section
    365     }
    366 
    367     private static final Comparator<AppRow> mRowComparator = new Comparator<AppRow>() {
    368         private final Collator sCollator = Collator.getInstance();
    369         @Override
    370         public int compare(AppRow lhs, AppRow rhs) {
    371             return sCollator.compare(lhs.label, rhs.label);
    372         }
    373     };
    374 
    375 
    376     public static AppRow loadAppRow(PackageManager pm, ApplicationInfo app,
    377             Backend backend) {
    378         final AppRow row = new AppRow();
    379         row.pkg = app.packageName;
    380         row.uid = app.uid;
    381         try {
    382             row.label = app.loadLabel(pm);
    383         } catch (Throwable t) {
    384             Log.e(TAG, "Error loading application label for " + row.pkg, t);
    385             row.label = row.pkg;
    386         }
    387         row.icon = app.loadIcon(pm);
    388         row.banned = backend.getNotificationsBanned(row.pkg, row.uid);
    389         row.priority = backend.getHighPriority(row.pkg, row.uid);
    390         row.sensitive = backend.getSensitive(row.pkg, row.uid);
    391         return row;
    392     }
    393 
    394     public static List<ResolveInfo> queryNotificationConfigActivities(PackageManager pm) {
    395         if (DEBUG) Log.d(TAG, "APP_NOTIFICATION_PREFS_CATEGORY_INTENT is "
    396                 + APP_NOTIFICATION_PREFS_CATEGORY_INTENT);
    397         final List<ResolveInfo> resolveInfos = pm.queryIntentActivities(
    398                 APP_NOTIFICATION_PREFS_CATEGORY_INTENT,
    399                 0 //PackageManager.MATCH_DEFAULT_ONLY
    400         );
    401         return resolveInfos;
    402     }
    403     public static void collectConfigActivities(PackageManager pm, ArrayMap<String, AppRow> rows) {
    404         final List<ResolveInfo> resolveInfos = queryNotificationConfigActivities(pm);
    405         applyConfigActivities(pm, rows, resolveInfos);
    406     }
    407 
    408     public static void applyConfigActivities(PackageManager pm, ArrayMap<String, AppRow> rows,
    409             List<ResolveInfo> resolveInfos) {
    410         if (DEBUG) Log.d(TAG, "Found " + resolveInfos.size() + " preference activities"
    411                 + (resolveInfos.size() == 0 ? " ;_;" : ""));
    412         for (ResolveInfo ri : resolveInfos) {
    413             final ActivityInfo activityInfo = ri.activityInfo;
    414             final ApplicationInfo appInfo = activityInfo.applicationInfo;
    415             final AppRow row = rows.get(appInfo.packageName);
    416             if (row == null) {
    417                 Log.v(TAG, "Ignoring notification preference activity ("
    418                         + activityInfo.name + ") for unknown package "
    419                         + activityInfo.packageName);
    420                 continue;
    421             }
    422             if (row.settingsIntent != null) {
    423                 Log.v(TAG, "Ignoring duplicate notification preference activity ("
    424                         + activityInfo.name + ") for package "
    425                         + activityInfo.packageName);
    426                 continue;
    427             }
    428             row.settingsIntent = new Intent(APP_NOTIFICATION_PREFS_CATEGORY_INTENT)
    429                     .setClassName(activityInfo.packageName, activityInfo.name);
    430         }
    431     }
    432 
    433     private final Runnable mCollectAppsRunnable = new Runnable() {
    434         @Override
    435         public void run() {
    436             synchronized (mRows) {
    437                 final long start = SystemClock.uptimeMillis();
    438                 if (DEBUG) Log.d(TAG, "Collecting apps...");
    439                 mRows.clear();
    440                 mSortedRows.clear();
    441 
    442                 // collect all launchable apps, plus any packages that have notification settings
    443                 final List<ApplicationInfo> appInfos = new ArrayList<ApplicationInfo>();
    444 
    445                 final List<LauncherActivityInfo> lais
    446                         = mLauncherApps.getActivityList(null /* all */,
    447                             UserHandle.getCallingUserHandle());
    448                 if (DEBUG) Log.d(TAG, "  launchable activities:");
    449                 for (LauncherActivityInfo lai : lais) {
    450                     if (DEBUG) Log.d(TAG, "    " + lai.getComponentName().toString());
    451                     appInfos.add(lai.getApplicationInfo());
    452                 }
    453 
    454                 final List<ResolveInfo> resolvedConfigActivities
    455                         = queryNotificationConfigActivities(mPM);
    456                 if (DEBUG) Log.d(TAG, "  config activities:");
    457                 for (ResolveInfo ri : resolvedConfigActivities) {
    458                     if (DEBUG) Log.d(TAG, "    "
    459                             + ri.activityInfo.packageName + "/" + ri.activityInfo.name);
    460                     appInfos.add(ri.activityInfo.applicationInfo);
    461                 }
    462 
    463                 for (ApplicationInfo info : appInfos) {
    464                     final String key = info.packageName;
    465                     if (mRows.containsKey(key)) {
    466                         // we already have this app, thanks
    467                         continue;
    468                     }
    469 
    470                     final AppRow row = loadAppRow(mPM, info, mBackend);
    471                     mRows.put(key, row);
    472                 }
    473 
    474                 // add config activities to the list
    475                 applyConfigActivities(mPM, mRows, resolvedConfigActivities);
    476 
    477                 // sort rows
    478                 mSortedRows.addAll(mRows.values());
    479                 Collections.sort(mSortedRows, mRowComparator);
    480                 // compute sections
    481                 mSections.clear();
    482                 String section = null;
    483                 for (AppRow r : mSortedRows) {
    484                     r.section = getSection(r.label);
    485                     if (!r.section.equals(section)) {
    486                         section = r.section;
    487                         mSections.add(section);
    488                     }
    489                 }
    490                 mHandler.post(mRefreshAppsListRunnable);
    491                 final long elapsed = SystemClock.uptimeMillis() - start;
    492                 if (DEBUG) Log.d(TAG, "Collected " + mRows.size() + " apps in " + elapsed + "ms");
    493             }
    494         }
    495     };
    496 
    497     private void refreshDisplayedItems() {
    498         if (DEBUG) Log.d(TAG, "Refreshing apps...");
    499         mAdapter.clear();
    500         synchronized (mSortedRows) {
    501             String section = null;
    502             final int N = mSortedRows.size();
    503             boolean first = true;
    504             for (int i = 0; i < N; i++) {
    505                 final AppRow row = mSortedRows.get(i);
    506                 if (!row.section.equals(section)) {
    507                     section = row.section;
    508                     Row r = new Row();
    509                     r.section = section;
    510                     mAdapter.add(r);
    511                     first = true;
    512                 }
    513                 row.first = first;
    514                 mAdapter.add(row);
    515                 first = false;
    516             }
    517         }
    518         if (mListViewState != null) {
    519             if (DEBUG) Log.d(TAG, "Restoring listView state");
    520             getListView().onRestoreInstanceState(mListViewState);
    521             mListViewState = null;
    522         }
    523         if (DEBUG) Log.d(TAG, "Refreshed " + mSortedRows.size() + " displayed items");
    524     }
    525 
    526     private final Runnable mRefreshAppsListRunnable = new Runnable() {
    527         @Override
    528         public void run() {
    529             refreshDisplayedItems();
    530         }
    531     };
    532 
    533     public static class Backend {
    534         static INotificationManager sINM = INotificationManager.Stub.asInterface(
    535                 ServiceManager.getService(Context.NOTIFICATION_SERVICE));
    536 
    537         public boolean setNotificationsBanned(String pkg, int uid, boolean banned) {
    538             try {
    539                 sINM.setNotificationsEnabledForPackage(pkg, uid, !banned);
    540                 return true;
    541             } catch (Exception e) {
    542                Log.w(TAG, "Error calling NoMan", e);
    543                return false;
    544             }
    545         }
    546 
    547         public boolean getNotificationsBanned(String pkg, int uid) {
    548             try {
    549                 final boolean enabled = sINM.areNotificationsEnabledForPackage(pkg, uid);
    550                 return !enabled;
    551             } catch (Exception e) {
    552                 Log.w(TAG, "Error calling NoMan", e);
    553                 return false;
    554             }
    555         }
    556 
    557         public boolean getHighPriority(String pkg, int uid) {
    558             try {
    559                 return sINM.getPackagePriority(pkg, uid) == Notification.PRIORITY_MAX;
    560             } catch (Exception e) {
    561                 Log.w(TAG, "Error calling NoMan", e);
    562                 return false;
    563             }
    564         }
    565 
    566         public boolean setHighPriority(String pkg, int uid, boolean highPriority) {
    567             try {
    568                 sINM.setPackagePriority(pkg, uid,
    569                         highPriority ? Notification.PRIORITY_MAX : Notification.PRIORITY_DEFAULT);
    570                 return true;
    571             } catch (Exception e) {
    572                 Log.w(TAG, "Error calling NoMan", e);
    573                 return false;
    574             }
    575         }
    576 
    577         public boolean getSensitive(String pkg, int uid) {
    578             try {
    579                 return sINM.getPackageVisibilityOverride(pkg, uid) == Notification.VISIBILITY_PRIVATE;
    580             } catch (Exception e) {
    581                 Log.w(TAG, "Error calling NoMan", e);
    582                 return false;
    583             }
    584         }
    585 
    586         public boolean setSensitive(String pkg, int uid, boolean sensitive) {
    587             try {
    588                 sINM.setPackageVisibilityOverride(pkg, uid,
    589                         sensitive ? Notification.VISIBILITY_PRIVATE
    590                                 : NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE);
    591                 return true;
    592             } catch (Exception e) {
    593                 Log.w(TAG, "Error calling NoMan", e);
    594                 return false;
    595             }
    596         }
    597     }
    598 }
    599