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