2 * Copyright (C) 2014 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.settings.notification;
19 import static com.android.settings.notification.AppNotificationSettings.EXTRA_HAS_SETTINGS_INTENT;
20 import static com.android.settings.notification.AppNotificationSettings.EXTRA_SETTINGS_INTENT;
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;
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;
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;
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);
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);
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>();
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;
99 private PackageManager mPM;
100 private UserManager mUM;
101 private LauncherApps mLauncherApps;
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);
116 public View onCreateView(LayoutInflater inflater, ViewGroup container,
117 Bundle savedInstanceState) {
118 return inflater.inflate(R.layout.notification_app_list, container, false);
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);
135 public void onActivityCreated(Bundle savedInstanceState) {
136 super.onActivityCreated(savedInstanceState);
137 repositionScrollbar();
138 getListView().setAdapter(mAdapter);
142 public void onPause() {
144 if (DEBUG) Log.d(TAG, "Saving listView state");
145 mListViewState = getListView().onSaveInstanceState();
149 public void onDestroyView() {
150 super.onDestroyView();
151 mListViewState = null; // you're dead to me
155 public void onResume() {
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);
175 public void onNothingSelected(AdapterView<?> parent) {
178 public void setBackend(Backend backend) {
182 private void loadAppsList() {
183 AsyncTask.execute(mCollectAppsRunnable);
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);
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());
207 private static class ViewHolder {
215 private class NotificationAppAdapter extends ArrayAdapter<Row> implements SectionIndexer {
216 public NotificationAppAdapter(Context context) {
217 super(context, 0, 0);
221 public boolean hasStableIds() {
226 public long getItemId(int position) {
231 public int getViewTypeCount() {
236 public int getItemViewType(int position) {
237 Row r = getItem(position);
238 return r instanceof AppRow ? 1 : 0;
241 public View getView(int position, View convertView, ViewGroup parent) {
242 Row r = getItem(position);
244 if (convertView == null) {
245 v = newView(parent, r);
249 bindView(v, r, false /*animate*/);
253 public View newView(ViewGroup parent, Row r) {
254 if (!(r instanceof AppRow)) {
255 return mInflater.inflate(R.layout.notification_app_section, parent, false);
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);
270 private void enableLayoutTransitions(ViewGroup vg, boolean enabled) {
272 vg.getLayoutTransition().enableTransitionType(LayoutTransition.APPEARING);
273 vg.getLayoutTransition().enableTransitionType(LayoutTransition.DISAPPEARING);
275 vg.getLayoutTransition().disableTransitionType(LayoutTransition.APPEARING);
276 vg.getLayoutTransition().disableTransitionType(LayoutTransition.DISAPPEARING);
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);
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() {
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));
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);
311 private String getSubtitle(AppRow row) {
313 return mContext.getString(R.string.app_notification_row_banned);
315 if (!row.priority && !row.sensitive) {
316 return EMPTY_SUBTITLE;
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;
323 return priString + mContext.getString(R.string.summary_divider_text) + senString;
327 public Object[] getSections() {
328 return mSections.toArray(new Object[mSections.size()]);
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)) {
345 public int getSectionForPosition(int position) {
346 Row row = getItem(position);
347 return mSections.indexOf(row.section);
351 private static class Row {
352 public String section;
355 public static class AppRow extends Row {
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
367 private static final Comparator<AppRow> mRowComparator = new Comparator<AppRow>() {
368 private final Collator sCollator = Collator.getInstance();
370 public int compare(AppRow lhs, AppRow rhs) {
371 return sCollator.compare(lhs.label, rhs.label);
376 public static AppRow loadAppRow(PackageManager pm, ApplicationInfo app,
378 final AppRow row = new AppRow();
379 row.pkg = app.packageName;
382 row.label = app.loadLabel(pm);
383 } catch (Throwable t) {
384 Log.e(TAG, "Error loading application label for " + row.pkg, t);
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);
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
403 public static void collectConfigActivities(PackageManager pm, ArrayMap<String, AppRow> rows) {
404 final List<ResolveInfo> resolveInfos = queryNotificationConfigActivities(pm);
405 applyConfigActivities(pm, rows, 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);
417 Log.v(TAG, "Ignoring notification preference activity ("
418 + activityInfo.name + ") for unknown package "
419 + activityInfo.packageName);
422 if (row.settingsIntent != null) {
423 Log.v(TAG, "Ignoring duplicate notification preference activity ("
424 + activityInfo.name + ") for package "
425 + activityInfo.packageName);
428 row.settingsIntent = new Intent(APP_NOTIFICATION_PREFS_CATEGORY_INTENT)
429 .setClassName(activityInfo.packageName, activityInfo.name);
433 private final Runnable mCollectAppsRunnable = new Runnable() {
436 synchronized (mRows) {
437 final long start = SystemClock.uptimeMillis();
438 if (DEBUG) Log.d(TAG, "Collecting apps...");
442 // collect all launchable apps, plus any packages that have notification settings
443 final List<ApplicationInfo> appInfos = new ArrayList<ApplicationInfo>();
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());
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);
463 for (ApplicationInfo info : appInfos) {
464 final String key = info.packageName;
465 if (mRows.containsKey(key)) {
466 // we already have this app, thanks
470 final AppRow row = loadAppRow(mPM, info, mBackend);
474 // add config activities to the list
475 applyConfigActivities(mPM, mRows, resolvedConfigActivities);
478 mSortedRows.addAll(mRows.values());
479 Collections.sort(mSortedRows, mRowComparator);
482 String section = null;
483 for (AppRow r : mSortedRows) {
484 r.section = getSection(r.label);
485 if (!r.section.equals(section)) {
487 mSections.add(section);
490 mHandler.post(mRefreshAppsListRunnable);
491 final long elapsed = SystemClock.uptimeMillis() - start;
492 if (DEBUG) Log.d(TAG, "Collected " + mRows.size() + " apps in " + elapsed + "ms");
497 private void refreshDisplayedItems() {
498 if (DEBUG) Log.d(TAG, "Refreshing apps...");
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;
518 if (mListViewState != null) {
519 if (DEBUG) Log.d(TAG, "Restoring listView state");
520 getListView().onRestoreInstanceState(mListViewState);
521 mListViewState = null;
523 if (DEBUG) Log.d(TAG, "Refreshed " + mSortedRows.size() + " displayed items");
526 private final Runnable mRefreshAppsListRunnable = new Runnable() {
529 refreshDisplayedItems();
533 public static class Backend {
534 static INotificationManager sINM = INotificationManager.Stub.asInterface(
535 ServiceManager.getService(Context.NOTIFICATION_SERVICE));
537 public boolean setNotificationsBanned(String pkg, int uid, boolean banned) {
539 sINM.setNotificationsEnabledForPackage(pkg, uid, !banned);
541 } catch (Exception e) {
542 Log.w(TAG, "Error calling NoMan", e);
547 public boolean getNotificationsBanned(String pkg, int uid) {
549 final boolean enabled = sINM.areNotificationsEnabledForPackage(pkg, uid);
551 } catch (Exception e) {
552 Log.w(TAG, "Error calling NoMan", e);
557 public boolean getHighPriority(String pkg, int uid) {
559 return sINM.getPackagePriority(pkg, uid) == Notification.PRIORITY_MAX;
560 } catch (Exception e) {
561 Log.w(TAG, "Error calling NoMan", e);
566 public boolean setHighPriority(String pkg, int uid, boolean highPriority) {
568 sINM.setPackagePriority(pkg, uid,
569 highPriority ? Notification.PRIORITY_MAX : Notification.PRIORITY_DEFAULT);
571 } catch (Exception e) {
572 Log.w(TAG, "Error calling NoMan", e);
577 public boolean getSensitive(String pkg, int uid) {
579 return sINM.getPackageVisibilityOverride(pkg, uid) == Notification.VISIBILITY_PRIVATE;
580 } catch (Exception e) {
581 Log.w(TAG, "Error calling NoMan", e);
586 public boolean setSensitive(String pkg, int uid, boolean sensitive) {
588 sINM.setPackageVisibilityOverride(pkg, uid,
589 sensitive ? Notification.VISIBILITY_PRIVATE
590 : NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE);
592 } catch (Exception e) {
593 Log.w(TAG, "Error calling NoMan", e);