NotificationAppList.java revision b58b5127040b3c843fe42544a89a1085cf7e74f6
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.util.ArrayMap; 45import android.util.Log; 46import android.util.TypedValue; 47import android.view.LayoutInflater; 48import android.view.View; 49import android.view.View.OnClickListener; 50import android.view.ViewGroup; 51import android.widget.AdapterView; 52import android.widget.AdapterView.OnItemSelectedListener; 53import android.widget.ArrayAdapter; 54import android.widget.ImageView; 55import android.widget.SectionIndexer; 56import android.widget.Spinner; 57import android.widget.TextView; 58 59import com.android.settings.PinnedHeaderListFragment; 60import com.android.settings.R; 61import com.android.settings.Settings.NotificationAppListActivity; 62import com.android.settings.UserSpinnerAdapter; 63import com.android.settings.Utils; 64 65import java.text.Collator; 66import java.util.ArrayList; 67import java.util.Collections; 68import java.util.Comparator; 69import java.util.List; 70 71/** Just a sectioned list of installed applications, nothing else to index **/ 72public class NotificationAppList extends PinnedHeaderListFragment 73 implements OnItemSelectedListener { 74 private static final String TAG = "NotificationAppList"; 75 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 76 77 private static final String EMPTY_SUBTITLE = ""; 78 private static final String SECTION_BEFORE_A = "*"; 79 private static final String SECTION_AFTER_Z = "**"; 80 private static final Intent APP_NOTIFICATION_PREFS_CATEGORY_INTENT 81 = new Intent(Intent.ACTION_MAIN) 82 .addCategory(Notification.INTENT_CATEGORY_NOTIFICATION_PREFERENCES); 83 84 private final Handler mHandler = new Handler(); 85 private final ArrayMap<String, AppRow> mRows = new ArrayMap<String, AppRow>(); 86 private final ArrayList<AppRow> mSortedRows = new ArrayList<AppRow>(); 87 private final ArrayList<String> mSections = new ArrayList<String>(); 88 89 private Context mContext; 90 private LayoutInflater mInflater; 91 private NotificationAppAdapter mAdapter; 92 private Signature[] mSystemSignature; 93 private Parcelable mListViewState; 94 private Backend mBackend = new Backend(); 95 private UserSpinnerAdapter mProfileSpinnerAdapter; 96 97 @Override 98 public void onCreate(Bundle savedInstanceState) { 99 super.onCreate(savedInstanceState); 100 mContext = getActivity(); 101 mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 102 mAdapter = new NotificationAppAdapter(mContext); 103 getActivity().setTitle(R.string.app_notifications_title); 104 } 105 106 @Override 107 public View onCreateView(LayoutInflater inflater, ViewGroup container, 108 Bundle savedInstanceState) { 109 return inflater.inflate(R.layout.notification_app_list, container, false); 110 } 111 112 @Override 113 public void onViewCreated(View view, Bundle savedInstanceState) { 114 super.onViewCreated(view, savedInstanceState); 115 final UserManager um = (UserManager) getActivity().getSystemService(Context.USER_SERVICE); 116 mProfileSpinnerAdapter = Utils.createUserSpinnerAdapter(um, mContext); 117 if (mProfileSpinnerAdapter != null) { 118 Spinner spinner = (Spinner) getActivity().getLayoutInflater().inflate( 119 R.layout.spinner_view, null); 120 spinner.setAdapter(mProfileSpinnerAdapter); 121 spinner.setOnItemSelectedListener(this); 122 setPinnedHeaderView(spinner); 123 } 124 } 125 126 @Override 127 public void onActivityCreated(Bundle savedInstanceState) { 128 super.onActivityCreated(savedInstanceState); 129 repositionScrollbar(); 130 getListView().setAdapter(mAdapter); 131 } 132 133 @Override 134 public void onPause() { 135 super.onPause(); 136 if (DEBUG) Log.d(TAG, "Saving listView state"); 137 mListViewState = getListView().onSaveInstanceState(); 138 } 139 140 @Override 141 public void onDestroyView() { 142 super.onDestroyView(); 143 mListViewState = null; // you're dead to me 144 } 145 146 @Override 147 public void onResume() { 148 super.onResume(); 149 loadAppsList(); 150 } 151 152 @Override 153 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { 154 UserHandle selectedUser = mProfileSpinnerAdapter.getUserHandle(position); 155 if (selectedUser.getIdentifier() != UserHandle.myUserId()) { 156 Intent intent = new Intent(getActivity(), NotificationAppListActivity.class); 157 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 158 mContext.startActivityAsUser(intent, selectedUser); 159 getActivity().finish(); 160 } 161 } 162 163 @Override 164 public void onNothingSelected(AdapterView<?> parent) { 165 } 166 167 public void setBackend(Backend backend) { 168 mBackend = backend; 169 } 170 171 private void loadAppsList() { 172 AsyncTask.execute(mCollectAppsRunnable); 173 } 174 175 private String getSection(CharSequence label) { 176 if (label == null || label.length() == 0) return SECTION_BEFORE_A; 177 final char c = Character.toUpperCase(label.charAt(0)); 178 if (c < 'A') return SECTION_BEFORE_A; 179 if (c > 'Z') return SECTION_AFTER_Z; 180 return Character.toString(c); 181 } 182 183 private void repositionScrollbar() { 184 final int sbWidthPx = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 185 getListView().getScrollBarSize(), 186 getResources().getDisplayMetrics()); 187 final View parent = (View)getView().getParent(); 188 final int eat = Math.min(sbWidthPx, parent.getPaddingEnd()); 189 if (eat <= 0) return; 190 if (DEBUG) Log.d(TAG, String.format("Eating %dpx into %dpx padding for %dpx scroll, ld=%d", 191 eat, parent.getPaddingEnd(), sbWidthPx, getListView().getLayoutDirection())); 192 parent.setPaddingRelative(parent.getPaddingStart(), parent.getPaddingTop(), 193 parent.getPaddingEnd() - eat, parent.getPaddingBottom()); 194 } 195 196 private static class ViewHolder { 197 ViewGroup row; 198 ImageView icon; 199 TextView title; 200 TextView subtitle; 201 View rowDivider; 202 } 203 204 private class NotificationAppAdapter extends ArrayAdapter<Row> implements SectionIndexer { 205 public NotificationAppAdapter(Context context) { 206 super(context, 0, 0); 207 } 208 209 @Override 210 public boolean hasStableIds() { 211 return true; 212 } 213 214 @Override 215 public long getItemId(int position) { 216 return position; 217 } 218 219 @Override 220 public int getViewTypeCount() { 221 return 2; 222 } 223 224 @Override 225 public int getItemViewType(int position) { 226 Row r = getItem(position); 227 return r instanceof AppRow ? 1 : 0; 228 } 229 230 public View getView(int position, View convertView, ViewGroup parent) { 231 Row r = getItem(position); 232 View v; 233 if (convertView == null) { 234 v = newView(parent, r); 235 } else { 236 v = convertView; 237 } 238 bindView(v, r, false /*animate*/); 239 return v; 240 } 241 242 public View newView(ViewGroup parent, Row r) { 243 if (!(r instanceof AppRow)) { 244 return mInflater.inflate(R.layout.notification_app_section, parent, false); 245 } 246 final View v = mInflater.inflate(R.layout.notification_app, parent, false); 247 final ViewHolder vh = new ViewHolder(); 248 vh.row = (ViewGroup) v; 249 vh.row.setLayoutTransition(new LayoutTransition()); 250 vh.row.setLayoutTransition(new LayoutTransition()); 251 vh.icon = (ImageView) v.findViewById(android.R.id.icon); 252 vh.title = (TextView) v.findViewById(android.R.id.title); 253 vh.subtitle = (TextView) v.findViewById(android.R.id.text1); 254 vh.rowDivider = v.findViewById(R.id.row_divider); 255 v.setTag(vh); 256 return v; 257 } 258 259 private void enableLayoutTransitions(ViewGroup vg, boolean enabled) { 260 if (enabled) { 261 vg.getLayoutTransition().enableTransitionType(LayoutTransition.APPEARING); 262 vg.getLayoutTransition().enableTransitionType(LayoutTransition.DISAPPEARING); 263 } else { 264 vg.getLayoutTransition().disableTransitionType(LayoutTransition.APPEARING); 265 vg.getLayoutTransition().disableTransitionType(LayoutTransition.DISAPPEARING); 266 } 267 } 268 269 public void bindView(final View view, Row r, boolean animate) { 270 if (!(r instanceof AppRow)) { 271 // it's a section row 272 final TextView tv = (TextView)view.findViewById(android.R.id.title); 273 tv.setText(r.section); 274 return; 275 } 276 277 final AppRow row = (AppRow)r; 278 final ViewHolder vh = (ViewHolder) view.getTag(); 279 enableLayoutTransitions(vh.row, animate); 280 vh.rowDivider.setVisibility(row.first ? View.GONE : View.VISIBLE); 281 vh.row.setOnClickListener(new OnClickListener() { 282 @Override 283 public void onClick(View v) { 284 mContext.startActivity(new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) 285 .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) 286 .putExtra(Settings.EXTRA_APP_PACKAGE, row.pkg) 287 .putExtra(Settings.EXTRA_APP_UID, row.uid) 288 .putExtra(EXTRA_HAS_SETTINGS_INTENT, row.settingsIntent != null) 289 .putExtra(EXTRA_SETTINGS_INTENT, row.settingsIntent)); 290 } 291 }); 292 enableLayoutTransitions(vh.row, animate); 293 vh.icon.setImageDrawable(row.icon); 294 vh.title.setText(row.label); 295 final String sub = getSubtitle(row); 296 vh.subtitle.setText(sub); 297 vh.subtitle.setVisibility(!sub.isEmpty() ? View.VISIBLE : View.GONE); 298 } 299 300 private String getSubtitle(AppRow row) { 301 if (row.banned) { 302 return mContext.getString(R.string.app_notification_row_banned); 303 } 304 if (!row.priority && !row.sensitive) { 305 return EMPTY_SUBTITLE; 306 } 307 final String priString = mContext.getString(R.string.app_notification_row_priority); 308 final String senString = mContext.getString(R.string.app_notification_row_sensitive); 309 if (row.priority != row.sensitive) { 310 return row.priority ? priString : senString; 311 } 312 return priString + mContext.getString(R.string.summary_divider_text) + senString; 313 } 314 315 @Override 316 public Object[] getSections() { 317 return mSections.toArray(new Object[mSections.size()]); 318 } 319 320 @Override 321 public int getPositionForSection(int sectionIndex) { 322 final String section = mSections.get(sectionIndex); 323 final int n = getCount(); 324 for (int i = 0; i < n; i++) { 325 final Row r = getItem(i); 326 if (r.section.equals(section)) { 327 return i; 328 } 329 } 330 return 0; 331 } 332 333 @Override 334 public int getSectionForPosition(int position) { 335 Row row = getItem(position); 336 return mSections.indexOf(row.section); 337 } 338 } 339 340 private static class Row { 341 public String section; 342 } 343 344 public static class AppRow extends Row { 345 public String pkg; 346 public int uid; 347 public Drawable icon; 348 public CharSequence label; 349 public Intent settingsIntent; 350 public boolean banned; 351 public boolean priority; 352 public boolean sensitive; 353 public boolean first; // first app in section 354 public boolean isSystem; 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 row.isSystem = Utils.isSystemPackage(pm, pkg); 381 return row; 382 } 383 384 public static List<ResolveInfo> queryNotificationConfigActivities(PackageManager pm) { 385 if (DEBUG) Log.d(TAG, "APP_NOTIFICATION_PREFS_CATEGORY_INTENT is " 386 + APP_NOTIFICATION_PREFS_CATEGORY_INTENT); 387 final List<ResolveInfo> resolveInfos = pm.queryIntentActivities( 388 APP_NOTIFICATION_PREFS_CATEGORY_INTENT, 389 0 //PackageManager.MATCH_DEFAULT_ONLY 390 ); 391 return resolveInfos; 392 } 393 public static void collectConfigActivities(PackageManager pm, ArrayMap<String, AppRow> rows) { 394 final List<ResolveInfo> resolveInfos = queryNotificationConfigActivities(pm); 395 applyConfigActivities(pm, rows, resolveInfos); 396 } 397 398 public static void applyConfigActivities(PackageManager pm, ArrayMap<String, AppRow> rows, 399 List<ResolveInfo> resolveInfos) { 400 if (DEBUG) Log.d(TAG, "Found " + resolveInfos.size() + " preference activities" 401 + (resolveInfos.size() == 0 ? " ;_;" : "")); 402 for (ResolveInfo ri : resolveInfos) { 403 final ActivityInfo activityInfo = ri.activityInfo; 404 final ApplicationInfo appInfo = activityInfo.applicationInfo; 405 final AppRow row = rows.get(appInfo.packageName); 406 if (row == null) { 407 Log.v(TAG, "Ignoring notification preference activity (" 408 + activityInfo.name + ") for unknown package " 409 + activityInfo.packageName); 410 continue; 411 } 412 if (row.settingsIntent != null) { 413 Log.v(TAG, "Ignoring duplicate notification preference activity (" 414 + activityInfo.name + ") for package " 415 + activityInfo.packageName); 416 continue; 417 } 418 row.settingsIntent = new Intent(APP_NOTIFICATION_PREFS_CATEGORY_INTENT) 419 .setClassName(activityInfo.packageName, activityInfo.name); 420 } 421 } 422 423 private final Runnable mCollectAppsRunnable = new Runnable() { 424 @Override 425 public void run() { 426 synchronized (mRows) { 427 final long start = SystemClock.uptimeMillis(); 428 if (DEBUG) Log.d(TAG, "Collecting apps..."); 429 mRows.clear(); 430 mSortedRows.clear(); 431 432 // collect all launchable apps, plus any packages that have notification settings 433 final PackageManager pm = mContext.getPackageManager(); 434 final List<ResolveInfo> resolvedApps = pm.queryIntentActivities( 435 new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER), 436 PackageManager.MATCH_DEFAULT_ONLY 437 ); 438 final List<ResolveInfo> resolvedConfigActivities 439 = queryNotificationConfigActivities(pm); 440 resolvedApps.addAll(resolvedConfigActivities); 441 442 for (ResolveInfo info : resolvedApps) { 443 String pkgName = info.activityInfo.packageName; 444 if (mRows.containsKey(pkgName)) { 445 // we already have this app, thanks 446 continue; 447 } 448 449 PackageInfo pkg = null; 450 try { 451 pkg = pm.getPackageInfo(pkgName, 452 PackageManager.GET_SIGNATURES); 453 } catch (NameNotFoundException e) { 454 if (DEBUG) Log.d(TAG, "Skipping (NNFE): " + pkg.packageName); 455 continue; 456 } 457 if (info.activityInfo.applicationInfo == null) { 458 if (DEBUG) Log.d(TAG, "Skipping (no applicationInfo): " + pkg.packageName); 459 continue; 460 } 461 final AppRow row = loadAppRow(pm, pkg, mBackend); 462 mRows.put(pkgName, row); 463 } 464 465 // add config activities to the list 466 applyConfigActivities(pm, mRows, resolvedConfigActivities); 467 // sort rows 468 mSortedRows.addAll(mRows.values()); 469 Collections.sort(mSortedRows, mRowComparator); 470 // compute sections 471 mSections.clear(); 472 String section = null; 473 for (AppRow r : mSortedRows) { 474 r.section = getSection(r.label); 475 if (!r.section.equals(section)) { 476 section = r.section; 477 mSections.add(section); 478 } 479 } 480 mHandler.post(mRefreshAppsListRunnable); 481 final long elapsed = SystemClock.uptimeMillis() - start; 482 if (DEBUG) Log.d(TAG, "Collected " + mRows.size() + " apps in " + elapsed + "ms"); 483 } 484 } 485 }; 486 487 private void refreshDisplayedItems() { 488 if (DEBUG) Log.d(TAG, "Refreshing apps..."); 489 mAdapter.clear(); 490 synchronized (mSortedRows) { 491 String section = null; 492 final int N = mSortedRows.size(); 493 boolean first = true; 494 for (int i = 0; i < N; i++) { 495 final AppRow row = mSortedRows.get(i); 496 if (!row.section.equals(section)) { 497 section = row.section; 498 Row r = new Row(); 499 r.section = section; 500 mAdapter.add(r); 501 first = true; 502 } 503 row.first = first; 504 mAdapter.add(row); 505 first = false; 506 } 507 } 508 if (mListViewState != null) { 509 if (DEBUG) Log.d(TAG, "Restoring listView state"); 510 getListView().onRestoreInstanceState(mListViewState); 511 mListViewState = null; 512 } 513 if (DEBUG) Log.d(TAG, "Refreshed " + mSortedRows.size() + " displayed items"); 514 } 515 516 private final Runnable mRefreshAppsListRunnable = new Runnable() { 517 @Override 518 public void run() { 519 refreshDisplayedItems(); 520 } 521 }; 522 523 public static class Backend { 524 public boolean setNotificationsBanned(String pkg, int uid, boolean banned) { 525 INotificationManager nm = INotificationManager.Stub.asInterface( 526 ServiceManager.getService(Context.NOTIFICATION_SERVICE)); 527 try { 528 nm.setNotificationsEnabledForPackage(pkg, uid, !banned); 529 return true; 530 } catch (Exception e) { 531 Log.w(TAG, "Error calling NoMan", e); 532 return false; 533 } 534 } 535 536 public boolean getNotificationsBanned(String pkg, int uid) { 537 INotificationManager nm = INotificationManager.Stub.asInterface( 538 ServiceManager.getService(Context.NOTIFICATION_SERVICE)); 539 try { 540 final boolean enabled = nm.areNotificationsEnabledForPackage(pkg, uid); 541 return !enabled; 542 } catch (Exception e) { 543 Log.w(TAG, "Error calling NoMan", e); 544 return false; 545 } 546 } 547 548 public boolean getHighPriority(String pkg, int uid) { 549 INotificationManager nm = INotificationManager.Stub.asInterface( 550 ServiceManager.getService(Context.NOTIFICATION_SERVICE)); 551 try { 552 return nm.getPackagePriority(pkg, uid) == Notification.PRIORITY_MAX; 553 } catch (Exception e) { 554 Log.w(TAG, "Error calling NoMan", e); 555 return false; 556 } 557 } 558 559 public boolean setHighPriority(String pkg, int uid, boolean highPriority) { 560 INotificationManager nm = INotificationManager.Stub.asInterface( 561 ServiceManager.getService(Context.NOTIFICATION_SERVICE)); 562 try { 563 nm.setPackagePriority(pkg, uid, 564 highPriority ? Notification.PRIORITY_MAX : Notification.PRIORITY_DEFAULT); 565 return true; 566 } catch (Exception e) { 567 Log.w(TAG, "Error calling NoMan", e); 568 return false; 569 } 570 } 571 572 public boolean getSensitive(String pkg, int uid) { 573 // TODO get visibility state from NoMan 574 return false; 575 } 576 577 public boolean setSensitive(String pkg, int uid, boolean sensitive) { 578 // TODO save visibility state to NoMan 579 return true; 580 } 581 } 582} 583