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