• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
onCreate(Bundle savedInstanceState)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
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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
onViewCreated(View view, Bundle savedInstanceState)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
onActivityCreated(Bundle savedInstanceState)134     public void onActivityCreated(Bundle savedInstanceState) {
135         super.onActivityCreated(savedInstanceState);
136         repositionScrollbar();
137         getListView().setAdapter(mAdapter);
138     }
139 
140     @Override
onPause()141     public void onPause() {
142         super.onPause();
143         if (DEBUG) Log.d(TAG, "Saving listView state");
144         mListViewState = getListView().onSaveInstanceState();
145     }
146 
147     @Override
onDestroyView()148     public void onDestroyView() {
149         super.onDestroyView();
150         mListViewState = null;  // you're dead to me
151     }
152 
153     @Override
onResume()154     public void onResume() {
155         super.onResume();
156         loadAppsList();
157     }
158 
159     @Override
onItemSelected(AdapterView<?> parent, View view, int position, long id)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
onNothingSelected(AdapterView<?> parent)171     public void onNothingSelected(AdapterView<?> parent) {
172     }
173 
setBackend(Backend backend)174     public void setBackend(Backend backend) {
175         mBackend = backend;
176     }
177 
loadAppsList()178     private void loadAppsList() {
179         AsyncTask.execute(mCollectAppsRunnable);
180     }
181 
getSection(CharSequence label)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 
repositionScrollbar()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 {
NotificationAppAdapter(Context context)212         public NotificationAppAdapter(Context context) {
213             super(context, 0, 0);
214         }
215 
216         @Override
hasStableIds()217         public boolean hasStableIds() {
218             return true;
219         }
220 
221         @Override
getItemId(int position)222         public long getItemId(int position) {
223             return position;
224         }
225 
226         @Override
getViewTypeCount()227         public int getViewTypeCount() {
228             return 2;
229         }
230 
231         @Override
getItemViewType(int position)232         public int getItemViewType(int position) {
233             Row r = getItem(position);
234             return r instanceof AppRow ? 1 : 0;
235         }
236 
getView(int position, View convertView, ViewGroup parent)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 
newView(ViewGroup parent, Row r)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 
enableLayoutTransitions(ViewGroup vg, boolean enabled)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 
bindView(final View view, Row r, boolean animate)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 
getSubtitle(AppRow row)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
getSections()323         public Object[] getSections() {
324             return mSections.toArray(new Object[mSections.size()]);
325         }
326 
327         @Override
getPositionForSection(int sectionIndex)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
getSectionForPosition(int position)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 
loadAppRow(PackageManager pm, ApplicationInfo app, Backend backend)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 
queryNotificationConfigActivities(PackageManager pm)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     }
collectConfigActivities(PackageManager pm, ArrayMap<String, AppRow> rows)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 
applyConfigActivities(PackageManager pm, ArrayMap<String, AppRow> rows, List<ResolveInfo> resolveInfos)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 
refreshDisplayedItems()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 
setNotificationsBanned(String pkg, int uid, boolean banned)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 
getNotificationsBanned(String pkg, int uid)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 
getHighPriority(String pkg, int uid)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 
setHighPriority(String pkg, int uid, boolean highPriority)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 
getSensitive(String pkg, int uid)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 
setSensitive(String pkg, int uid, boolean sensitive)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