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.LauncherActivityInfo;
30import android.content.pm.LauncherApps;
31import android.content.pm.PackageManager;
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    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