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