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