NotificationAppList.java revision 3d903185c792623a1e122f047adc0725db7ea01e
1/* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.settings.notification; 18 19import static com.android.settings.notification.AppNotificationSettings.EXTRA_HAS_SETTINGS_INTENT; 20import static com.android.settings.notification.AppNotificationSettings.EXTRA_SETTINGS_INTENT; 21 22import android.animation.LayoutTransition; 23import android.app.INotificationManager; 24import android.app.Notification; 25import android.content.Context; 26import android.content.Intent; 27import android.content.pm.ActivityInfo; 28import android.content.pm.ApplicationInfo; 29import android.content.pm.PackageInfo; 30import android.content.pm.PackageManager; 31import android.content.pm.PackageManager.NameNotFoundException; 32import android.content.pm.ResolveInfo; 33import android.content.pm.Signature; 34import android.graphics.drawable.Drawable; 35import android.os.AsyncTask; 36import android.os.Bundle; 37import android.os.Handler; 38import android.os.Parcelable; 39import android.os.ServiceManager; 40import android.os.SystemClock; 41import android.os.UserHandle; 42import android.os.UserManager; 43import android.provider.Settings; 44import android.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 98 @Override 99 public void onCreate(Bundle savedInstanceState) { 100 super.onCreate(savedInstanceState); 101 mContext = getActivity(); 102 mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 103 mAdapter = new NotificationAppAdapter(mContext); 104 getActivity().setTitle(R.string.app_notifications_title); 105 } 106 107 @Override 108 public View onCreateView(LayoutInflater inflater, ViewGroup container, 109 Bundle savedInstanceState) { 110 return inflater.inflate(R.layout.notification_app_list, container, false); 111 } 112 113 @Override 114 public void onViewCreated(View view, Bundle savedInstanceState) { 115 super.onViewCreated(view, savedInstanceState); 116 final UserManager um = (UserManager) getActivity().getSystemService(Context.USER_SERVICE); 117 mProfileSpinnerAdapter = Utils.createUserSpinnerAdapter(um, mContext); 118 if (mProfileSpinnerAdapter != null) { 119 Spinner spinner = (Spinner) getActivity().getLayoutInflater().inflate( 120 R.layout.spinner_view, null); 121 spinner.setAdapter(mProfileSpinnerAdapter); 122 spinner.setOnItemSelectedListener(this); 123 setPinnedHeaderView(spinner); 124 } 125 } 126 127 @Override 128 public void onActivityCreated(Bundle savedInstanceState) { 129 super.onActivityCreated(savedInstanceState); 130 repositionScrollbar(); 131 getListView().setAdapter(mAdapter); 132 } 133 134 @Override 135 public void onPause() { 136 super.onPause(); 137 if (DEBUG) Log.d(TAG, "Saving listView state"); 138 mListViewState = getListView().onSaveInstanceState(); 139 } 140 141 @Override 142 public void onDestroyView() { 143 super.onDestroyView(); 144 mListViewState = null; // you're dead to me 145 } 146 147 @Override 148 public void onResume() { 149 super.onResume(); 150 loadAppsList(); 151 } 152 153 @Override 154 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { 155 UserHandle selectedUser = mProfileSpinnerAdapter.getUserHandle(position); 156 if (selectedUser.getIdentifier() != UserHandle.myUserId()) { 157 Intent intent = new Intent(getActivity(), NotificationAppListActivity.class); 158 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 159 mContext.startActivityAsUser(intent, selectedUser); 160 getActivity().finish(); 161 } 162 } 163 164 @Override 165 public void onNothingSelected(AdapterView<?> parent) { 166 } 167 168 public void setBackend(Backend backend) { 169 mBackend = backend; 170 } 171 172 private void loadAppsList() { 173 AsyncTask.execute(mCollectAppsRunnable); 174 } 175 176 private String getSection(CharSequence label) { 177 if (label == null || label.length() == 0) return SECTION_BEFORE_A; 178 final char c = Character.toUpperCase(label.charAt(0)); 179 if (c < 'A') return SECTION_BEFORE_A; 180 if (c > 'Z') return SECTION_AFTER_Z; 181 return Character.toString(c); 182 } 183 184 private void repositionScrollbar() { 185 final int sbWidthPx = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 186 getListView().getScrollBarSize(), 187 getResources().getDisplayMetrics()); 188 final View parent = (View)getView().getParent(); 189 final int eat = Math.min(sbWidthPx, parent.getPaddingEnd()); 190 if (eat <= 0) return; 191 if (DEBUG) Log.d(TAG, String.format("Eating %dpx into %dpx padding for %dpx scroll, ld=%d", 192 eat, parent.getPaddingEnd(), sbWidthPx, getListView().getLayoutDirection())); 193 parent.setPaddingRelative(parent.getPaddingStart(), parent.getPaddingTop(), 194 parent.getPaddingEnd() - eat, parent.getPaddingBottom()); 195 } 196 197 private static class ViewHolder { 198 ViewGroup row; 199 ImageView icon; 200 TextView title; 201 TextView subtitle; 202 View rowDivider; 203 } 204 205 private class NotificationAppAdapter extends ArrayAdapter<Row> implements SectionIndexer { 206 public NotificationAppAdapter(Context context) { 207 super(context, 0, 0); 208 } 209 210 @Override 211 public boolean hasStableIds() { 212 return true; 213 } 214 215 @Override 216 public long getItemId(int position) { 217 return position; 218 } 219 220 @Override 221 public int getViewTypeCount() { 222 return 2; 223 } 224 225 @Override 226 public int getItemViewType(int position) { 227 Row r = getItem(position); 228 return r instanceof AppRow ? 1 : 0; 229 } 230 231 public View getView(int position, View convertView, ViewGroup parent) { 232 Row r = getItem(position); 233 View v; 234 if (convertView == null) { 235 v = newView(parent, r); 236 } else { 237 v = convertView; 238 } 239 bindView(v, r, false /*animate*/); 240 return v; 241 } 242 243 public View newView(ViewGroup parent, Row r) { 244 if (!(r instanceof AppRow)) { 245 return mInflater.inflate(R.layout.notification_app_section, parent, false); 246 } 247 final View v = mInflater.inflate(R.layout.notification_app, parent, false); 248 final ViewHolder vh = new ViewHolder(); 249 vh.row = (ViewGroup) v; 250 vh.row.setLayoutTransition(new LayoutTransition()); 251 vh.row.setLayoutTransition(new LayoutTransition()); 252 vh.icon = (ImageView) v.findViewById(android.R.id.icon); 253 vh.title = (TextView) v.findViewById(android.R.id.title); 254 vh.subtitle = (TextView) v.findViewById(android.R.id.text1); 255 vh.rowDivider = v.findViewById(R.id.row_divider); 256 v.setTag(vh); 257 return v; 258 } 259 260 private void enableLayoutTransitions(ViewGroup vg, boolean enabled) { 261 if (enabled) { 262 vg.getLayoutTransition().enableTransitionType(LayoutTransition.APPEARING); 263 vg.getLayoutTransition().enableTransitionType(LayoutTransition.DISAPPEARING); 264 } else { 265 vg.getLayoutTransition().disableTransitionType(LayoutTransition.APPEARING); 266 vg.getLayoutTransition().disableTransitionType(LayoutTransition.DISAPPEARING); 267 } 268 } 269 270 public void bindView(final View view, Row r, boolean animate) { 271 if (!(r instanceof AppRow)) { 272 // it's a section row 273 final TextView tv = (TextView)view.findViewById(android.R.id.title); 274 tv.setText(r.section); 275 return; 276 } 277 278 final AppRow row = (AppRow)r; 279 final ViewHolder vh = (ViewHolder) view.getTag(); 280 enableLayoutTransitions(vh.row, animate); 281 vh.rowDivider.setVisibility(row.first ? View.GONE : View.VISIBLE); 282 vh.row.setOnClickListener(new OnClickListener() { 283 @Override 284 public void onClick(View v) { 285 mContext.startActivity(new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) 286 .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) 287 .putExtra(Settings.EXTRA_APP_PACKAGE, row.pkg) 288 .putExtra(Settings.EXTRA_APP_UID, row.uid) 289 .putExtra(EXTRA_HAS_SETTINGS_INTENT, row.settingsIntent != null) 290 .putExtra(EXTRA_SETTINGS_INTENT, row.settingsIntent)); 291 } 292 }); 293 enableLayoutTransitions(vh.row, animate); 294 vh.icon.setImageDrawable(row.icon); 295 vh.title.setText(row.label); 296 final String sub = getSubtitle(row); 297 vh.subtitle.setText(sub); 298 vh.subtitle.setVisibility(!sub.isEmpty() ? View.VISIBLE : View.GONE); 299 } 300 301 private String getSubtitle(AppRow row) { 302 if (row.banned) { 303 return mContext.getString(R.string.app_notification_row_banned); 304 } 305 if (!row.priority && !row.sensitive) { 306 return EMPTY_SUBTITLE; 307 } 308 final String priString = mContext.getString(R.string.app_notification_row_priority); 309 final String senString = mContext.getString(R.string.app_notification_row_sensitive); 310 if (row.priority != row.sensitive) { 311 return row.priority ? priString : senString; 312 } 313 return priString + mContext.getString(R.string.summary_divider_text) + senString; 314 } 315 316 @Override 317 public Object[] getSections() { 318 return mSections.toArray(new Object[mSections.size()]); 319 } 320 321 @Override 322 public int getPositionForSection(int sectionIndex) { 323 final String section = mSections.get(sectionIndex); 324 final int n = getCount(); 325 for (int i = 0; i < n; i++) { 326 final Row r = getItem(i); 327 if (r.section.equals(section)) { 328 return i; 329 } 330 } 331 return 0; 332 } 333 334 @Override 335 public int getSectionForPosition(int position) { 336 Row row = getItem(position); 337 return mSections.indexOf(row.section); 338 } 339 } 340 341 private static class Row { 342 public String section; 343 } 344 345 public static class AppRow extends Row { 346 public String pkg; 347 public int uid; 348 public Drawable icon; 349 public CharSequence label; 350 public Intent settingsIntent; 351 public boolean banned; 352 public boolean priority; 353 public boolean sensitive; 354 public boolean first; // first app in section 355 } 356 357 private static final Comparator<AppRow> mRowComparator = new Comparator<AppRow>() { 358 private final Collator sCollator = Collator.getInstance(); 359 @Override 360 public int compare(AppRow lhs, AppRow rhs) { 361 return sCollator.compare(lhs.label, rhs.label); 362 } 363 }; 364 365 366 public static AppRow loadAppRow(PackageManager pm, PackageInfo pkg, Backend backend) { 367 final AppRow row = new AppRow(); 368 row.pkg = pkg.packageName; 369 row.uid = pkg.applicationInfo.uid; 370 try { 371 row.label = pkg.applicationInfo.loadLabel(pm); 372 } catch (Throwable t) { 373 Log.e(TAG, "Error loading application label for " + row.pkg, t); 374 row.label = row.pkg; 375 } 376 row.icon = pkg.applicationInfo.loadIcon(pm); 377 row.banned = backend.getNotificationsBanned(row.pkg, row.uid); 378 row.priority = backend.getHighPriority(row.pkg, row.uid); 379 row.sensitive = backend.getSensitive(row.pkg, row.uid); 380 return row; 381 } 382 383 public static List<ResolveInfo> queryNotificationConfigActivities(PackageManager pm) { 384 if (DEBUG) Log.d(TAG, "APP_NOTIFICATION_PREFS_CATEGORY_INTENT is " 385 + APP_NOTIFICATION_PREFS_CATEGORY_INTENT); 386 final List<ResolveInfo> resolveInfos = pm.queryIntentActivities( 387 APP_NOTIFICATION_PREFS_CATEGORY_INTENT, 388 0 //PackageManager.MATCH_DEFAULT_ONLY 389 ); 390 return resolveInfos; 391 } 392 public static void collectConfigActivities(PackageManager pm, ArrayMap<String, AppRow> rows) { 393 final List<ResolveInfo> resolveInfos = queryNotificationConfigActivities(pm); 394 applyConfigActivities(pm, rows, resolveInfos); 395 } 396 397 public static void applyConfigActivities(PackageManager pm, ArrayMap<String, AppRow> rows, 398 List<ResolveInfo> resolveInfos) { 399 if (DEBUG) Log.d(TAG, "Found " + resolveInfos.size() + " preference activities" 400 + (resolveInfos.size() == 0 ? " ;_;" : "")); 401 for (ResolveInfo ri : resolveInfos) { 402 final ActivityInfo activityInfo = ri.activityInfo; 403 final ApplicationInfo appInfo = activityInfo.applicationInfo; 404 final AppRow row = rows.get(appInfo.packageName); 405 if (row == null) { 406 Log.v(TAG, "Ignoring notification preference activity (" 407 + activityInfo.name + ") for unknown package " 408 + activityInfo.packageName); 409 continue; 410 } 411 if (row.settingsIntent != null) { 412 Log.v(TAG, "Ignoring duplicate notification preference activity (" 413 + activityInfo.name + ") for package " 414 + activityInfo.packageName); 415 continue; 416 } 417 row.settingsIntent = new Intent(APP_NOTIFICATION_PREFS_CATEGORY_INTENT) 418 .setClassName(activityInfo.packageName, activityInfo.name); 419 } 420 } 421 422 private final Runnable mCollectAppsRunnable = new Runnable() { 423 @Override 424 public void run() { 425 synchronized (mRows) { 426 final long start = SystemClock.uptimeMillis(); 427 if (DEBUG) Log.d(TAG, "Collecting apps..."); 428 mRows.clear(); 429 mSortedRows.clear(); 430 431 // collect all launchable apps, plus any packages that have notification settings 432 final PackageManager pm = mContext.getPackageManager(); 433 final List<ResolveInfo> resolvedApps = pm.queryIntentActivities( 434 new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER), 435 PackageManager.MATCH_DEFAULT_ONLY 436 ); 437 final List<ResolveInfo> resolvedConfigActivities 438 = queryNotificationConfigActivities(pm); 439 resolvedApps.addAll(resolvedConfigActivities); 440 441 for (ResolveInfo info : resolvedApps) { 442 String pkgName = info.activityInfo.packageName; 443 if (mRows.containsKey(pkgName)) { 444 // we already have this app, thanks 445 continue; 446 } 447 448 PackageInfo pkg = null; 449 try { 450 pkg = pm.getPackageInfo(pkgName, 451 PackageManager.GET_SIGNATURES); 452 } catch (NameNotFoundException e) { 453 if (DEBUG) Log.d(TAG, "Skipping (NNFE): " + pkg.packageName); 454 continue; 455 } 456 if (info.activityInfo.applicationInfo == null) { 457 if (DEBUG) Log.d(TAG, "Skipping (no applicationInfo): " + pkg.packageName); 458 continue; 459 } 460 final AppRow row = loadAppRow(pm, pkg, mBackend); 461 mRows.put(pkgName, row); 462 } 463 464 // add config activities to the list 465 applyConfigActivities(pm, mRows, resolvedConfigActivities); 466 // sort rows 467 mSortedRows.addAll(mRows.values()); 468 Collections.sort(mSortedRows, mRowComparator); 469 // compute sections 470 mSections.clear(); 471 String section = null; 472 for (AppRow r : mSortedRows) { 473 r.section = getSection(r.label); 474 if (!r.section.equals(section)) { 475 section = r.section; 476 mSections.add(section); 477 } 478 } 479 mHandler.post(mRefreshAppsListRunnable); 480 final long elapsed = SystemClock.uptimeMillis() - start; 481 if (DEBUG) Log.d(TAG, "Collected " + mRows.size() + " apps in " + elapsed + "ms"); 482 } 483 } 484 }; 485 486 private void refreshDisplayedItems() { 487 if (DEBUG) Log.d(TAG, "Refreshing apps..."); 488 mAdapter.clear(); 489 synchronized (mSortedRows) { 490 String section = null; 491 final int N = mSortedRows.size(); 492 boolean first = true; 493 for (int i = 0; i < N; i++) { 494 final AppRow row = mSortedRows.get(i); 495 if (!row.section.equals(section)) { 496 section = row.section; 497 Row r = new Row(); 498 r.section = section; 499 mAdapter.add(r); 500 first = true; 501 } 502 row.first = first; 503 mAdapter.add(row); 504 first = false; 505 } 506 } 507 if (mListViewState != null) { 508 if (DEBUG) Log.d(TAG, "Restoring listView state"); 509 getListView().onRestoreInstanceState(mListViewState); 510 mListViewState = null; 511 } 512 if (DEBUG) Log.d(TAG, "Refreshed " + mSortedRows.size() + " displayed items"); 513 } 514 515 private final Runnable mRefreshAppsListRunnable = new Runnable() { 516 @Override 517 public void run() { 518 refreshDisplayedItems(); 519 } 520 }; 521 522 public static class Backend { 523 public boolean setNotificationsBanned(String pkg, int uid, boolean banned) { 524 INotificationManager nm = INotificationManager.Stub.asInterface( 525 ServiceManager.getService(Context.NOTIFICATION_SERVICE)); 526 try { 527 nm.setNotificationsEnabledForPackage(pkg, uid, !banned); 528 return true; 529 } catch (Exception e) { 530 Log.w(TAG, "Error calling NoMan", e); 531 return false; 532 } 533 } 534 535 public boolean getNotificationsBanned(String pkg, int uid) { 536 INotificationManager nm = INotificationManager.Stub.asInterface( 537 ServiceManager.getService(Context.NOTIFICATION_SERVICE)); 538 try { 539 final boolean enabled = nm.areNotificationsEnabledForPackage(pkg, uid); 540 return !enabled; 541 } catch (Exception e) { 542 Log.w(TAG, "Error calling NoMan", e); 543 return false; 544 } 545 } 546 547 public boolean getHighPriority(String pkg, int uid) { 548 INotificationManager nm = INotificationManager.Stub.asInterface( 549 ServiceManager.getService(Context.NOTIFICATION_SERVICE)); 550 try { 551 return nm.getPackagePriority(pkg, uid) == Notification.PRIORITY_MAX; 552 } catch (Exception e) { 553 Log.w(TAG, "Error calling NoMan", e); 554 return false; 555 } 556 } 557 558 public boolean setHighPriority(String pkg, int uid, boolean highPriority) { 559 INotificationManager nm = INotificationManager.Stub.asInterface( 560 ServiceManager.getService(Context.NOTIFICATION_SERVICE)); 561 try { 562 nm.setPackagePriority(pkg, uid, 563 highPriority ? Notification.PRIORITY_MAX : Notification.PRIORITY_DEFAULT); 564 return true; 565 } catch (Exception e) { 566 Log.w(TAG, "Error calling NoMan", e); 567 return false; 568 } 569 } 570 571 public boolean getSensitive(String pkg, int uid) { 572 INotificationManager nm = INotificationManager.Stub.asInterface( 573 ServiceManager.getService(Context.NOTIFICATION_SERVICE)); 574 try { 575 return nm.getPackageVisibilityOverride(pkg, uid) == Notification.VISIBILITY_PRIVATE; 576 } catch (Exception e) { 577 Log.w(TAG, "Error calling NoMan", e); 578 return false; 579 } 580 } 581 582 public boolean setSensitive(String pkg, int uid, boolean sensitive) { 583 INotificationManager nm = INotificationManager.Stub.asInterface( 584 ServiceManager.getService(Context.NOTIFICATION_SERVICE)); 585 try { 586 nm.setPackageVisibilityOverride(pkg, uid, 587 sensitive ? Notification.VISIBILITY_PRIVATE 588 : NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE); 589 return true; 590 } catch (Exception e) { 591 Log.w(TAG, "Error calling NoMan", e); 592 return false; 593 } 594 } 595 } 596} 597