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.LauncherActivityInfo; 30import android.content.pm.LauncherApps; 31import android.content.pm.PackageManager; 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 private Spinner mSpinner; 98 99 private PackageManager mPM; 100 private UserManager mUM; 101 private LauncherApps mLauncherApps; 102 103 @Override 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 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 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 135 public void onActivityCreated(Bundle savedInstanceState) { 136 super.onActivityCreated(savedInstanceState); 137 repositionScrollbar(); 138 getListView().setAdapter(mAdapter); 139 } 140 141 @Override 142 public void onPause() { 143 super.onPause(); 144 if (DEBUG) Log.d(TAG, "Saving listView state"); 145 mListViewState = getListView().onSaveInstanceState(); 146 } 147 148 @Override 149 public void onDestroyView() { 150 super.onDestroyView(); 151 mListViewState = null; // you're dead to me 152 } 153 154 @Override 155 public void onResume() { 156 super.onResume(); 157 loadAppsList(); 158 } 159 160 @Override 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 175 public void onNothingSelected(AdapterView<?> parent) { 176 } 177 178 public void setBackend(Backend backend) { 179 mBackend = backend; 180 } 181 182 private void loadAppsList() { 183 AsyncTask.execute(mCollectAppsRunnable); 184 } 185 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 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 { 216 public NotificationAppAdapter(Context context) { 217 super(context, 0, 0); 218 } 219 220 @Override 221 public boolean hasStableIds() { 222 return true; 223 } 224 225 @Override 226 public long getItemId(int position) { 227 return position; 228 } 229 230 @Override 231 public int getViewTypeCount() { 232 return 2; 233 } 234 235 @Override 236 public int getItemViewType(int position) { 237 Row r = getItem(position); 238 return r instanceof AppRow ? 1 : 0; 239 } 240 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 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 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 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 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 327 public Object[] getSections() { 328 return mSections.toArray(new Object[mSections.size()]); 329 } 330 331 @Override 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 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 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 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 } 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 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 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 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 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 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 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 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 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