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