1 /* 2 * Copyright (C) 2010 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.systemui.statusbar; 18 19 import android.app.ActivityManager; 20 import android.app.ActivityManagerNative; 21 import android.app.Notification; 22 import android.app.PendingIntent; 23 import android.app.TaskStackBuilder; 24 import android.content.BroadcastReceiver; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.IntentFilter; 28 import android.content.pm.ApplicationInfo; 29 import android.content.pm.PackageManager.NameNotFoundException; 30 import android.content.res.Configuration; 31 import android.database.ContentObserver; 32 import android.graphics.Rect; 33 import android.net.Uri; 34 import android.os.Build; 35 import android.os.Handler; 36 import android.os.IBinder; 37 import android.os.Message; 38 import android.os.PowerManager; 39 import android.os.RemoteException; 40 import android.os.ServiceManager; 41 import android.os.UserHandle; 42 import android.provider.Settings; 43 import android.service.dreams.DreamService; 44 import android.service.dreams.IDreamManager; 45 import android.service.notification.StatusBarNotification; 46 import android.text.TextUtils; 47 import android.util.Log; 48 import android.view.Display; 49 import android.view.IWindowManager; 50 import android.view.LayoutInflater; 51 import android.view.MenuItem; 52 import android.view.MotionEvent; 53 import android.view.View; 54 import android.view.ViewGroup; 55 import android.view.ViewGroup.LayoutParams; 56 import android.view.WindowManager; 57 import android.view.WindowManagerGlobal; 58 import android.widget.ImageView; 59 import android.widget.LinearLayout; 60 import android.widget.PopupMenu; 61 import android.widget.RemoteViews; 62 import android.widget.TextView; 63 64 import com.android.internal.statusbar.IStatusBarService; 65 import com.android.internal.statusbar.StatusBarIcon; 66 import com.android.internal.statusbar.StatusBarIconList; 67 import com.android.internal.widget.SizeAdaptiveLayout; 68 import com.android.systemui.R; 69 import com.android.systemui.RecentsComponent; 70 import com.android.systemui.SearchPanelView; 71 import com.android.systemui.SystemUI; 72 import com.android.systemui.statusbar.phone.KeyguardTouchDelegate; 73 import com.android.systemui.statusbar.policy.NotificationRowLayout; 74 75 import java.util.ArrayList; 76 import java.util.Locale; 77 78 public abstract class BaseStatusBar extends SystemUI implements 79 CommandQueue.Callbacks { 80 public static final String TAG = "StatusBar"; 81 public static final boolean DEBUG = false; 82 public static final boolean MULTIUSER_DEBUG = false; 83 84 protected static final int MSG_TOGGLE_RECENTS_PANEL = 1020; 85 protected static final int MSG_CLOSE_RECENTS_PANEL = 1021; 86 protected static final int MSG_PRELOAD_RECENT_APPS = 1022; 87 protected static final int MSG_CANCEL_PRELOAD_RECENT_APPS = 1023; 88 protected static final int MSG_OPEN_SEARCH_PANEL = 1024; 89 protected static final int MSG_CLOSE_SEARCH_PANEL = 1025; 90 protected static final int MSG_SHOW_HEADS_UP = 1026; 91 protected static final int MSG_HIDE_HEADS_UP = 1027; 92 protected static final int MSG_ESCALATE_HEADS_UP = 1028; 93 94 protected static final boolean ENABLE_HEADS_UP = true; 95 // scores above this threshold should be displayed in heads up mode. 96 protected static final int INTERRUPTION_THRESHOLD = 11; 97 protected static final String SETTING_HEADS_UP = "heads_up_enabled"; 98 99 // Should match the value in PhoneWindowManager 100 public static final String SYSTEM_DIALOG_REASON_RECENT_APPS = "recentapps"; 101 102 public static final int EXPANDED_LEAVE_ALONE = -10000; 103 public static final int EXPANDED_FULL_OPEN = -10001; 104 105 protected CommandQueue mCommandQueue; 106 protected IStatusBarService mBarService; 107 protected H mHandler = createHandler(); 108 109 // all notifications 110 protected NotificationData mNotificationData = new NotificationData(); 111 protected NotificationRowLayout mPile; 112 113 protected NotificationData.Entry mInterruptingNotificationEntry; 114 protected long mInterruptingNotificationTime; 115 116 // used to notify status bar for suppressing notification LED 117 protected boolean mPanelSlightlyVisible; 118 119 // Search panel 120 protected SearchPanelView mSearchPanelView; 121 122 protected PopupMenu mNotificationBlamePopup; 123 124 protected int mCurrentUserId = 0; 125 126 protected int mLayoutDirection = -1; // invalid 127 private Locale mLocale; 128 protected boolean mUseHeadsUp = false; 129 130 protected IDreamManager mDreamManager; 131 PowerManager mPowerManager; 132 protected int mRowHeight; 133 134 // UI-specific methods 135 136 /** 137 * Create all windows necessary for the status bar (including navigation, overlay panels, etc) 138 * and add them to the window manager. 139 */ 140 protected abstract void createAndAddWindows(); 141 142 protected WindowManager mWindowManager; 143 protected IWindowManager mWindowManagerService; 144 protected abstract void refreshLayout(int layoutDirection); 145 146 protected Display mDisplay; 147 148 private boolean mDeviceProvisioned = false; 149 150 private RecentsComponent mRecents; 151 152 public IStatusBarService getStatusBarService() { 153 return mBarService; 154 } 155 156 public boolean isDeviceProvisioned() { 157 return mDeviceProvisioned; 158 } 159 160 private ContentObserver mProvisioningObserver = new ContentObserver(new Handler()) { 161 @Override 162 public void onChange(boolean selfChange) { 163 final boolean provisioned = 0 != Settings.Global.getInt( 164 mContext.getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 0); 165 if (provisioned != mDeviceProvisioned) { 166 mDeviceProvisioned = provisioned; 167 updateNotificationIcons(); 168 } 169 } 170 }; 171 172 private RemoteViews.OnClickHandler mOnClickHandler = new RemoteViews.OnClickHandler() { 173 @Override 174 public boolean onClickHandler(View view, PendingIntent pendingIntent, Intent fillInIntent) { 175 if (DEBUG) { 176 Log.v(TAG, "Notification click handler invoked for intent: " + pendingIntent); 177 } 178 final boolean isActivity = pendingIntent.isActivity(); 179 if (isActivity) { 180 try { 181 // The intent we are sending is for the application, which 182 // won't have permission to immediately start an activity after 183 // the user switches to home. We know it is safe to do at this 184 // point, so make sure new activity switches are now allowed. 185 ActivityManagerNative.getDefault().resumeAppSwitches(); 186 // Also, notifications can be launched from the lock screen, 187 // so dismiss the lock screen when the activity starts. 188 ActivityManagerNative.getDefault().dismissKeyguardOnNextActivity(); 189 } catch (RemoteException e) { 190 } 191 } 192 193 boolean handled = super.onClickHandler(view, pendingIntent, fillInIntent); 194 195 if (isActivity && handled) { 196 // close the shade if it was open 197 animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE); 198 visibilityChanged(false); 199 } 200 return handled; 201 } 202 }; 203 204 private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { 205 @Override 206 public void onReceive(Context context, Intent intent) { 207 String action = intent.getAction(); 208 if (Intent.ACTION_USER_SWITCHED.equals(action)) { 209 mCurrentUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); 210 if (true) Log.v(TAG, "userId " + mCurrentUserId + " is in the house"); 211 userSwitched(mCurrentUserId); 212 } 213 } 214 }; 215 216 public void start() { 217 mWindowManager = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE); 218 mWindowManagerService = WindowManagerGlobal.getWindowManagerService(); 219 mDisplay = mWindowManager.getDefaultDisplay(); 220 221 mDreamManager = IDreamManager.Stub.asInterface( 222 ServiceManager.checkService(DreamService.DREAM_SERVICE)); 223 mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); 224 225 mProvisioningObserver.onChange(false); // set up 226 mContext.getContentResolver().registerContentObserver( 227 Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), true, 228 mProvisioningObserver); 229 230 mBarService = IStatusBarService.Stub.asInterface( 231 ServiceManager.getService(Context.STATUS_BAR_SERVICE)); 232 233 mRecents = getComponent(RecentsComponent.class); 234 235 mLocale = mContext.getResources().getConfiguration().locale; 236 mLayoutDirection = TextUtils.getLayoutDirectionFromLocale(mLocale); 237 238 // Connect in to the status bar manager service 239 StatusBarIconList iconList = new StatusBarIconList(); 240 ArrayList<IBinder> notificationKeys = new ArrayList<IBinder>(); 241 ArrayList<StatusBarNotification> notifications = new ArrayList<StatusBarNotification>(); 242 mCommandQueue = new CommandQueue(this, iconList); 243 244 int[] switches = new int[7]; 245 ArrayList<IBinder> binders = new ArrayList<IBinder>(); 246 try { 247 mBarService.registerStatusBar(mCommandQueue, iconList, notificationKeys, notifications, 248 switches, binders); 249 } catch (RemoteException ex) { 250 // If the system process isn't there we're doomed anyway. 251 } 252 253 createAndAddWindows(); 254 255 disable(switches[0]); 256 setSystemUiVisibility(switches[1], 0xffffffff); 257 topAppWindowChanged(switches[2] != 0); 258 // StatusBarManagerService has a back up of IME token and it's restored here. 259 setImeWindowStatus(binders.get(0), switches[3], switches[4]); 260 setHardKeyboardStatus(switches[5] != 0, switches[6] != 0); 261 262 // Set up the initial icon state 263 int N = iconList.size(); 264 int viewIndex = 0; 265 for (int i=0; i<N; i++) { 266 StatusBarIcon icon = iconList.getIcon(i); 267 if (icon != null) { 268 addIcon(iconList.getSlot(i), i, viewIndex, icon); 269 viewIndex++; 270 } 271 } 272 273 // Set up the initial notification state 274 N = notificationKeys.size(); 275 if (N == notifications.size()) { 276 for (int i=0; i<N; i++) { 277 addNotification(notificationKeys.get(i), notifications.get(i)); 278 } 279 } else { 280 Log.wtf(TAG, "Notification list length mismatch: keys=" + N 281 + " notifications=" + notifications.size()); 282 } 283 284 if (DEBUG) { 285 Log.d(TAG, String.format( 286 "init: icons=%d disabled=0x%08x lights=0x%08x menu=0x%08x imeButton=0x%08x", 287 iconList.size(), 288 switches[0], 289 switches[1], 290 switches[2], 291 switches[3] 292 )); 293 } 294 295 mCurrentUserId = ActivityManager.getCurrentUser(); 296 297 IntentFilter filter = new IntentFilter(); 298 filter.addAction(Intent.ACTION_USER_SWITCHED); 299 mContext.registerReceiver(mBroadcastReceiver, filter); 300 } 301 302 public void userSwitched(int newUserId) { 303 // should be overridden 304 } 305 306 public boolean notificationIsForCurrentUser(StatusBarNotification n) { 307 final int thisUserId = mCurrentUserId; 308 final int notificationUserId = n.getUserId(); 309 if (DEBUG && MULTIUSER_DEBUG) { 310 Log.v(TAG, String.format("%s: current userid: %d, notification userid: %d", 311 n, thisUserId, notificationUserId)); 312 } 313 return notificationUserId == UserHandle.USER_ALL 314 || thisUserId == notificationUserId; 315 } 316 317 @Override 318 protected void onConfigurationChanged(Configuration newConfig) { 319 final Locale locale = mContext.getResources().getConfiguration().locale; 320 final int ld = TextUtils.getLayoutDirectionFromLocale(locale); 321 if (! locale.equals(mLocale) || ld != mLayoutDirection) { 322 if (DEBUG) { 323 Log.v(TAG, String.format( 324 "config changed locale/LD: %s (%d) -> %s (%d)", mLocale, mLayoutDirection, 325 locale, ld)); 326 } 327 mLocale = locale; 328 mLayoutDirection = ld; 329 refreshLayout(ld); 330 } 331 } 332 333 protected View updateNotificationVetoButton(View row, StatusBarNotification n) { 334 View vetoButton = row.findViewById(R.id.veto); 335 if (n.isClearable() || (mInterruptingNotificationEntry != null 336 && mInterruptingNotificationEntry.row == row)) { 337 final String _pkg = n.getPackageName(); 338 final String _tag = n.getTag(); 339 final int _id = n.getId(); 340 vetoButton.setOnClickListener(new View.OnClickListener() { 341 public void onClick(View v) { 342 // Accessibility feedback 343 v.announceForAccessibility( 344 mContext.getString(R.string.accessibility_notification_dismissed)); 345 try { 346 mBarService.onNotificationClear(_pkg, _tag, _id); 347 348 } catch (RemoteException ex) { 349 // system process is dead if we're here. 350 } 351 } 352 }); 353 vetoButton.setVisibility(View.VISIBLE); 354 } else { 355 vetoButton.setVisibility(View.GONE); 356 } 357 vetoButton.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); 358 return vetoButton; 359 } 360 361 362 protected void applyLegacyRowBackground(StatusBarNotification sbn, View content) { 363 if (sbn.getNotification().contentView.getLayoutId() != 364 com.android.internal.R.layout.notification_template_base) { 365 int version = 0; 366 try { 367 ApplicationInfo info = mContext.getPackageManager().getApplicationInfo(sbn.getPackageName(), 0); 368 version = info.targetSdkVersion; 369 } catch (NameNotFoundException ex) { 370 Log.e(TAG, "Failed looking up ApplicationInfo for " + sbn.getPackageName(), ex); 371 } 372 if (version > 0 && version < Build.VERSION_CODES.GINGERBREAD) { 373 content.setBackgroundResource(R.drawable.notification_row_legacy_bg); 374 } else { 375 content.setBackgroundResource(com.android.internal.R.drawable.notification_bg); 376 } 377 } 378 } 379 380 private void startApplicationDetailsActivity(String packageName) { 381 Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, 382 Uri.fromParts("package", packageName, null)); 383 intent.setComponent(intent.resolveActivity(mContext.getPackageManager())); 384 TaskStackBuilder.create(mContext).addNextIntentWithParentStack(intent).startActivities( 385 null, UserHandle.CURRENT); 386 } 387 388 protected View.OnLongClickListener getNotificationLongClicker() { 389 return new View.OnLongClickListener() { 390 @Override 391 public boolean onLongClick(View v) { 392 final String packageNameF = (String) v.getTag(); 393 if (packageNameF == null) return false; 394 if (v.getWindowToken() == null) return false; 395 mNotificationBlamePopup = new PopupMenu(mContext, v); 396 mNotificationBlamePopup.getMenuInflater().inflate( 397 R.menu.notification_popup_menu, 398 mNotificationBlamePopup.getMenu()); 399 mNotificationBlamePopup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { 400 public boolean onMenuItemClick(MenuItem item) { 401 if (item.getItemId() == R.id.notification_inspect_item) { 402 startApplicationDetailsActivity(packageNameF); 403 animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE); 404 } else { 405 return false; 406 } 407 return true; 408 } 409 }); 410 mNotificationBlamePopup.show(); 411 412 return true; 413 } 414 }; 415 } 416 417 public void dismissPopups() { 418 if (mNotificationBlamePopup != null) { 419 mNotificationBlamePopup.dismiss(); 420 mNotificationBlamePopup = null; 421 } 422 } 423 424 public void onHeadsUpDismissed() { 425 } 426 427 @Override 428 public void toggleRecentApps() { 429 int msg = MSG_TOGGLE_RECENTS_PANEL; 430 mHandler.removeMessages(msg); 431 mHandler.sendEmptyMessage(msg); 432 } 433 434 @Override 435 public void preloadRecentApps() { 436 int msg = MSG_PRELOAD_RECENT_APPS; 437 mHandler.removeMessages(msg); 438 mHandler.sendEmptyMessage(msg); 439 } 440 441 @Override 442 public void cancelPreloadRecentApps() { 443 int msg = MSG_CANCEL_PRELOAD_RECENT_APPS; 444 mHandler.removeMessages(msg); 445 mHandler.sendEmptyMessage(msg); 446 } 447 448 @Override 449 public void showSearchPanel() { 450 int msg = MSG_OPEN_SEARCH_PANEL; 451 mHandler.removeMessages(msg); 452 mHandler.sendEmptyMessage(msg); 453 } 454 455 @Override 456 public void hideSearchPanel() { 457 int msg = MSG_CLOSE_SEARCH_PANEL; 458 mHandler.removeMessages(msg); 459 mHandler.sendEmptyMessage(msg); 460 } 461 462 protected abstract WindowManager.LayoutParams getSearchLayoutParams( 463 LayoutParams layoutParams); 464 465 protected void updateSearchPanel() { 466 // Search Panel 467 boolean visible = false; 468 if (mSearchPanelView != null) { 469 visible = mSearchPanelView.isShowing(); 470 mWindowManager.removeView(mSearchPanelView); 471 } 472 473 // Provide SearchPanel with a temporary parent to allow layout params to work. 474 LinearLayout tmpRoot = new LinearLayout(mContext); 475 mSearchPanelView = (SearchPanelView) LayoutInflater.from(mContext).inflate( 476 R.layout.status_bar_search_panel, tmpRoot, false); 477 mSearchPanelView.setOnTouchListener( 478 new TouchOutsideListener(MSG_CLOSE_SEARCH_PANEL, mSearchPanelView)); 479 mSearchPanelView.setVisibility(View.GONE); 480 481 WindowManager.LayoutParams lp = getSearchLayoutParams(mSearchPanelView.getLayoutParams()); 482 483 mWindowManager.addView(mSearchPanelView, lp); 484 mSearchPanelView.setBar(this); 485 if (visible) { 486 mSearchPanelView.show(true, false); 487 } 488 } 489 490 protected H createHandler() { 491 return new H(); 492 } 493 494 static void sendCloseSystemWindows(Context context, String reason) { 495 if (ActivityManagerNative.isSystemReady()) { 496 try { 497 ActivityManagerNative.getDefault().closeSystemDialogs(reason); 498 } catch (RemoteException e) { 499 } 500 } 501 } 502 503 protected abstract View getStatusBarView(); 504 505 protected View.OnTouchListener mRecentsPreloadOnTouchListener = new View.OnTouchListener() { 506 // additional optimization when we have software system buttons - start loading the recent 507 // tasks on touch down 508 @Override 509 public boolean onTouch(View v, MotionEvent event) { 510 int action = event.getAction() & MotionEvent.ACTION_MASK; 511 if (action == MotionEvent.ACTION_DOWN) { 512 preloadRecentTasksList(); 513 } else if (action == MotionEvent.ACTION_CANCEL) { 514 cancelPreloadingRecentTasksList(); 515 } else if (action == MotionEvent.ACTION_UP) { 516 if (!v.isPressed()) { 517 cancelPreloadingRecentTasksList(); 518 } 519 520 } 521 return false; 522 } 523 }; 524 525 protected void toggleRecentsActivity() { 526 if (mRecents != null) { 527 mRecents.toggleRecents(mDisplay, mLayoutDirection, getStatusBarView()); 528 } 529 } 530 531 protected void preloadRecentTasksList() { 532 if (mRecents != null) { 533 mRecents.preloadRecentTasksList(); 534 } 535 } 536 537 protected void cancelPreloadingRecentTasksList() { 538 if (mRecents != null) { 539 mRecents.cancelPreloadingRecentTasksList(); 540 } 541 } 542 543 protected void closeRecents() { 544 if (mRecents != null) { 545 mRecents.closeRecents(); 546 } 547 } 548 549 public abstract void resetHeadsUpDecayTimer(); 550 551 protected class H extends Handler { 552 public void handleMessage(Message m) { 553 Intent intent; 554 switch (m.what) { 555 case MSG_TOGGLE_RECENTS_PANEL: 556 toggleRecentsActivity(); 557 break; 558 case MSG_CLOSE_RECENTS_PANEL: 559 closeRecents(); 560 break; 561 case MSG_PRELOAD_RECENT_APPS: 562 preloadRecentTasksList(); 563 break; 564 case MSG_CANCEL_PRELOAD_RECENT_APPS: 565 cancelPreloadingRecentTasksList(); 566 break; 567 case MSG_OPEN_SEARCH_PANEL: 568 if (DEBUG) Log.d(TAG, "opening search panel"); 569 if (mSearchPanelView != null && mSearchPanelView.isAssistantAvailable()) { 570 mSearchPanelView.show(true, true); 571 onShowSearchPanel(); 572 } 573 break; 574 case MSG_CLOSE_SEARCH_PANEL: 575 if (DEBUG) Log.d(TAG, "closing search panel"); 576 if (mSearchPanelView != null && mSearchPanelView.isShowing()) { 577 mSearchPanelView.show(false, true); 578 onHideSearchPanel(); 579 } 580 break; 581 } 582 } 583 } 584 585 public class TouchOutsideListener implements View.OnTouchListener { 586 private int mMsg; 587 private StatusBarPanel mPanel; 588 589 public TouchOutsideListener(int msg, StatusBarPanel panel) { 590 mMsg = msg; 591 mPanel = panel; 592 } 593 594 public boolean onTouch(View v, MotionEvent ev) { 595 final int action = ev.getAction(); 596 if (action == MotionEvent.ACTION_OUTSIDE 597 || (action == MotionEvent.ACTION_DOWN 598 && !mPanel.isInContentArea((int)ev.getX(), (int)ev.getY()))) { 599 mHandler.removeMessages(mMsg); 600 mHandler.sendEmptyMessage(mMsg); 601 return true; 602 } 603 return false; 604 } 605 } 606 607 protected void workAroundBadLayerDrawableOpacity(View v) { 608 } 609 610 protected void onHideSearchPanel() { 611 } 612 613 protected void onShowSearchPanel() { 614 } 615 616 public boolean inflateViews(NotificationData.Entry entry, ViewGroup parent) { 617 int minHeight = 618 mContext.getResources().getDimensionPixelSize(R.dimen.notification_min_height); 619 int maxHeight = 620 mContext.getResources().getDimensionPixelSize(R.dimen.notification_max_height); 621 StatusBarNotification sbn = entry.notification; 622 RemoteViews contentView = sbn.getNotification().contentView; 623 RemoteViews bigContentView = sbn.getNotification().bigContentView; 624 if (contentView == null) { 625 return false; 626 } 627 628 // create the row view 629 LayoutInflater inflater = (LayoutInflater)mContext.getSystemService( 630 Context.LAYOUT_INFLATER_SERVICE); 631 ExpandableNotificationRow row = (ExpandableNotificationRow) inflater.inflate( 632 R.layout.status_bar_notification_row, parent, false); 633 634 // for blaming (see SwipeHelper.setLongPressListener) 635 row.setTag(sbn.getPackageName()); 636 637 workAroundBadLayerDrawableOpacity(row); 638 View vetoButton = updateNotificationVetoButton(row, sbn); 639 vetoButton.setContentDescription(mContext.getString( 640 R.string.accessibility_remove_notification)); 641 642 // NB: the large icon is now handled entirely by the template 643 644 // bind the click event to the content area 645 ViewGroup content = (ViewGroup)row.findViewById(R.id.content); 646 ViewGroup adaptive = (ViewGroup)row.findViewById(R.id.adaptive); 647 648 content.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); 649 650 PendingIntent contentIntent = sbn.getNotification().contentIntent; 651 if (contentIntent != null) { 652 final View.OnClickListener listener = new NotificationClicker(contentIntent, 653 sbn.getPackageName(), sbn.getTag(), sbn.getId()); 654 content.setOnClickListener(listener); 655 } else { 656 content.setOnClickListener(null); 657 } 658 659 View contentViewLocal = null; 660 View bigContentViewLocal = null; 661 try { 662 contentViewLocal = contentView.apply(mContext, adaptive, mOnClickHandler); 663 if (bigContentView != null) { 664 bigContentViewLocal = bigContentView.apply(mContext, adaptive, mOnClickHandler); 665 } 666 } 667 catch (RuntimeException e) { 668 final String ident = sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()); 669 Log.e(TAG, "couldn't inflate view for notification " + ident, e); 670 return false; 671 } 672 673 if (contentViewLocal != null) { 674 SizeAdaptiveLayout.LayoutParams params = 675 new SizeAdaptiveLayout.LayoutParams(contentViewLocal.getLayoutParams()); 676 params.minHeight = minHeight; 677 params.maxHeight = minHeight; 678 adaptive.addView(contentViewLocal, params); 679 } 680 if (bigContentViewLocal != null) { 681 SizeAdaptiveLayout.LayoutParams params = 682 new SizeAdaptiveLayout.LayoutParams(bigContentViewLocal.getLayoutParams()); 683 params.minHeight = minHeight+1; 684 params.maxHeight = maxHeight; 685 adaptive.addView(bigContentViewLocal, params); 686 } 687 row.setDrawingCacheEnabled(true); 688 689 applyLegacyRowBackground(sbn, content); 690 691 if (MULTIUSER_DEBUG) { 692 TextView debug = (TextView) row.findViewById(R.id.debug_info); 693 if (debug != null) { 694 debug.setVisibility(View.VISIBLE); 695 debug.setText("U " + entry.notification.getUserId()); 696 } 697 } 698 entry.row = row; 699 entry.row.setRowHeight(mRowHeight); 700 entry.content = content; 701 entry.expanded = contentViewLocal; 702 entry.setBigContentView(bigContentViewLocal); 703 704 return true; 705 } 706 707 public NotificationClicker makeClicker(PendingIntent intent, String pkg, String tag, int id) { 708 return new NotificationClicker(intent, pkg, tag, id); 709 } 710 711 protected class NotificationClicker implements View.OnClickListener { 712 private PendingIntent mIntent; 713 private String mPkg; 714 private String mTag; 715 private int mId; 716 717 public NotificationClicker(PendingIntent intent, String pkg, String tag, int id) { 718 mIntent = intent; 719 mPkg = pkg; 720 mTag = tag; 721 mId = id; 722 } 723 724 public void onClick(View v) { 725 try { 726 // The intent we are sending is for the application, which 727 // won't have permission to immediately start an activity after 728 // the user switches to home. We know it is safe to do at this 729 // point, so make sure new activity switches are now allowed. 730 ActivityManagerNative.getDefault().resumeAppSwitches(); 731 // Also, notifications can be launched from the lock screen, 732 // so dismiss the lock screen when the activity starts. 733 ActivityManagerNative.getDefault().dismissKeyguardOnNextActivity(); 734 } catch (RemoteException e) { 735 } 736 737 if (mIntent != null) { 738 int[] pos = new int[2]; 739 v.getLocationOnScreen(pos); 740 Intent overlay = new Intent(); 741 overlay.setSourceBounds( 742 new Rect(pos[0], pos[1], pos[0]+v.getWidth(), pos[1]+v.getHeight())); 743 try { 744 mIntent.send(mContext, 0, overlay); 745 } catch (PendingIntent.CanceledException e) { 746 // the stack trace isn't very helpful here. Just log the exception message. 747 Log.w(TAG, "Sending contentIntent failed: " + e); 748 } 749 750 KeyguardTouchDelegate.getInstance(mContext).dismiss(); 751 } 752 753 try { 754 mBarService.onNotificationClick(mPkg, mTag, mId); 755 } catch (RemoteException ex) { 756 // system process is dead if we're here. 757 } 758 759 // close the shade if it was open 760 animateCollapsePanels(CommandQueue.FLAG_EXCLUDE_NONE); 761 visibilityChanged(false); 762 } 763 } 764 /** 765 * The LEDs are turned o)ff when the notification panel is shown, even just a little bit. 766 * This was added last-minute and is inconsistent with the way the rest of the notifications 767 * are handled, because the notification isn't really cancelled. The lights are just 768 * turned off. If any other notifications happen, the lights will turn back on. Steve says 769 * this is what he wants. (see bug 1131461) 770 */ 771 protected void visibilityChanged(boolean visible) { 772 if (mPanelSlightlyVisible != visible) { 773 mPanelSlightlyVisible = visible; 774 try { 775 mBarService.onPanelRevealed(); 776 } catch (RemoteException ex) { 777 // Won't fail unless the world has ended. 778 } 779 } 780 } 781 782 /** 783 * Cancel this notification and tell the StatusBarManagerService / NotificationManagerService 784 * about the failure. 785 * 786 * WARNING: this will call back into us. Don't hold any locks. 787 */ 788 void handleNotificationError(IBinder key, StatusBarNotification n, String message) { 789 removeNotification(key); 790 try { 791 mBarService.onNotificationError(n.getPackageName(), n.getTag(), n.getId(), n.getUid(), n.getInitialPid(), message); 792 } catch (RemoteException ex) { 793 // The end is nigh. 794 } 795 } 796 797 protected StatusBarNotification removeNotificationViews(IBinder key) { 798 NotificationData.Entry entry = mNotificationData.remove(key); 799 if (entry == null) { 800 Log.w(TAG, "removeNotification for unknown key: " + key); 801 return null; 802 } 803 // Remove the expanded view. 804 ViewGroup rowParent = (ViewGroup)entry.row.getParent(); 805 if (rowParent != null) rowParent.removeView(entry.row); 806 updateExpansionStates(); 807 updateNotificationIcons(); 808 809 return entry.notification; 810 } 811 812 protected NotificationData.Entry createNotificationViews(IBinder key, 813 StatusBarNotification notification) { 814 if (DEBUG) { 815 Log.d(TAG, "createNotificationViews(key=" + key + ", notification=" + notification); 816 } 817 // Construct the icon. 818 final StatusBarIconView iconView = new StatusBarIconView(mContext, 819 notification.getPackageName() + "/0x" + Integer.toHexString(notification.getId()), 820 notification.getNotification()); 821 iconView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); 822 823 final StatusBarIcon ic = new StatusBarIcon(notification.getPackageName(), 824 notification.getUser(), 825 notification.getNotification().icon, 826 notification.getNotification().iconLevel, 827 notification.getNotification().number, 828 notification.getNotification().tickerText); 829 if (!iconView.set(ic)) { 830 handleNotificationError(key, notification, "Couldn't create icon: " + ic); 831 return null; 832 } 833 // Construct the expanded view. 834 NotificationData.Entry entry = new NotificationData.Entry(key, notification, iconView); 835 if (!inflateViews(entry, mPile)) { 836 handleNotificationError(key, notification, "Couldn't expand RemoteViews for: " 837 + notification); 838 return null; 839 } 840 return entry; 841 } 842 843 protected void addNotificationViews(NotificationData.Entry entry) { 844 // Add the expanded view and icon. 845 int pos = mNotificationData.add(entry); 846 if (DEBUG) { 847 Log.d(TAG, "addNotificationViews: added at " + pos); 848 } 849 updateExpansionStates(); 850 updateNotificationIcons(); 851 } 852 853 private void addNotificationViews(IBinder key, StatusBarNotification notification) { 854 addNotificationViews(createNotificationViews(key, notification)); 855 } 856 857 protected void updateExpansionStates() { 858 int N = mNotificationData.size(); 859 for (int i = 0; i < N; i++) { 860 NotificationData.Entry entry = mNotificationData.get(i); 861 if (!entry.row.isUserLocked()) { 862 if (i == (N-1)) { 863 if (DEBUG) Log.d(TAG, "expanding top notification at " + i); 864 entry.row.setExpanded(true); 865 } else { 866 if (!entry.row.isUserExpanded()) { 867 if (DEBUG) Log.d(TAG, "collapsing notification at " + i); 868 entry.row.setExpanded(false); 869 } else { 870 if (DEBUG) Log.d(TAG, "ignoring user-modified notification at " + i); 871 } 872 } 873 } else { 874 if (DEBUG) Log.d(TAG, "ignoring notification being held by user at " + i); 875 } 876 } 877 } 878 879 protected abstract void haltTicker(); 880 protected abstract void setAreThereNotifications(); 881 protected abstract void updateNotificationIcons(); 882 protected abstract void tick(IBinder key, StatusBarNotification n, boolean firstTime); 883 protected abstract void updateExpandedViewPos(int expandedPosition); 884 protected abstract int getExpandedViewMaxHeight(); 885 protected abstract boolean shouldDisableNavbarGestures(); 886 887 protected boolean isTopNotification(ViewGroup parent, NotificationData.Entry entry) { 888 return parent != null && parent.indexOfChild(entry.row) == 0; 889 } 890 891 public void updateNotification(IBinder key, StatusBarNotification notification) { 892 if (DEBUG) Log.d(TAG, "updateNotification(" + key + " -> " + notification + ")"); 893 894 final NotificationData.Entry oldEntry = mNotificationData.findByKey(key); 895 if (oldEntry == null) { 896 Log.w(TAG, "updateNotification for unknown key: " + key); 897 return; 898 } 899 900 final StatusBarNotification oldNotification = oldEntry.notification; 901 902 // XXX: modify when we do something more intelligent with the two content views 903 final RemoteViews oldContentView = oldNotification.getNotification().contentView; 904 final RemoteViews contentView = notification.getNotification().contentView; 905 final RemoteViews oldBigContentView = oldNotification.getNotification().bigContentView; 906 final RemoteViews bigContentView = notification.getNotification().bigContentView; 907 908 if (DEBUG) { 909 Log.d(TAG, "old notification: when=" + oldNotification.getNotification().when 910 + " ongoing=" + oldNotification.isOngoing() 911 + " expanded=" + oldEntry.expanded 912 + " contentView=" + oldContentView 913 + " bigContentView=" + oldBigContentView 914 + " rowParent=" + oldEntry.row.getParent()); 915 Log.d(TAG, "new notification: when=" + notification.getNotification().when 916 + " ongoing=" + oldNotification.isOngoing() 917 + " contentView=" + contentView 918 + " bigContentView=" + bigContentView); 919 } 920 921 // Can we just reapply the RemoteViews in place? If when didn't change, the order 922 // didn't change. 923 924 // 1U is never null 925 boolean contentsUnchanged = oldEntry.expanded != null 926 && contentView.getPackage() != null 927 && oldContentView.getPackage() != null 928 && oldContentView.getPackage().equals(contentView.getPackage()) 929 && oldContentView.getLayoutId() == contentView.getLayoutId(); 930 // large view may be null 931 boolean bigContentsUnchanged = 932 (oldEntry.getBigContentView() == null && bigContentView == null) 933 || ((oldEntry.getBigContentView() != null && bigContentView != null) 934 && bigContentView.getPackage() != null 935 && oldBigContentView.getPackage() != null 936 && oldBigContentView.getPackage().equals(bigContentView.getPackage()) 937 && oldBigContentView.getLayoutId() == bigContentView.getLayoutId()); 938 ViewGroup rowParent = (ViewGroup) oldEntry.row.getParent(); 939 boolean orderUnchanged = notification.getNotification().when== oldNotification.getNotification().when 940 && notification.getScore() == oldNotification.getScore(); 941 // score now encompasses/supersedes isOngoing() 942 943 boolean updateTicker = notification.getNotification().tickerText != null 944 && !TextUtils.equals(notification.getNotification().tickerText, 945 oldEntry.notification.getNotification().tickerText); 946 boolean isTopAnyway = isTopNotification(rowParent, oldEntry); 947 if (contentsUnchanged && bigContentsUnchanged && (orderUnchanged || isTopAnyway)) { 948 if (DEBUG) Log.d(TAG, "reusing notification for key: " + key); 949 oldEntry.notification = notification; 950 try { 951 updateNotificationViews(oldEntry, notification); 952 953 if (ENABLE_HEADS_UP && mInterruptingNotificationEntry != null 954 && oldNotification == mInterruptingNotificationEntry.notification) { 955 if (!shouldInterrupt(notification)) { 956 if (DEBUG) Log.d(TAG, "no longer interrupts!"); 957 mHandler.sendEmptyMessage(MSG_HIDE_HEADS_UP); 958 } else { 959 if (DEBUG) Log.d(TAG, "updating the current heads up:" + notification); 960 mInterruptingNotificationEntry.notification = notification; 961 updateNotificationViews(mInterruptingNotificationEntry, notification); 962 } 963 } 964 965 // Update the icon. 966 final StatusBarIcon ic = new StatusBarIcon(notification.getPackageName(), 967 notification.getUser(), 968 notification.getNotification().icon, notification.getNotification().iconLevel, 969 notification.getNotification().number, 970 notification.getNotification().tickerText); 971 if (!oldEntry.icon.set(ic)) { 972 handleNotificationError(key, notification, "Couldn't update icon: " + ic); 973 return; 974 } 975 updateExpansionStates(); 976 } 977 catch (RuntimeException e) { 978 // It failed to add cleanly. Log, and remove the view from the panel. 979 Log.w(TAG, "Couldn't reapply views for package " + contentView.getPackage(), e); 980 removeNotificationViews(key); 981 addNotificationViews(key, notification); 982 } 983 } else { 984 if (DEBUG) Log.d(TAG, "not reusing notification for key: " + key); 985 if (DEBUG) Log.d(TAG, "contents was " + (contentsUnchanged ? "unchanged" : "changed")); 986 if (DEBUG) Log.d(TAG, "order was " + (orderUnchanged ? "unchanged" : "changed")); 987 if (DEBUG) Log.d(TAG, "notification is " + (isTopAnyway ? "top" : "not top")); 988 final boolean wasExpanded = oldEntry.row.isUserExpanded(); 989 removeNotificationViews(key); 990 addNotificationViews(key, notification); // will also replace the heads up 991 if (wasExpanded) { 992 final NotificationData.Entry newEntry = mNotificationData.findByKey(key); 993 newEntry.row.setExpanded(true); 994 newEntry.row.setUserExpanded(true); 995 } 996 } 997 998 // Update the veto button accordingly (and as a result, whether this row is 999 // swipe-dismissable) 1000 updateNotificationVetoButton(oldEntry.row, notification); 1001 1002 // Is this for you? 1003 boolean isForCurrentUser = notificationIsForCurrentUser(notification); 1004 if (DEBUG) Log.d(TAG, "notification is " + (isForCurrentUser ? "" : "not ") + "for you"); 1005 1006 // Restart the ticker if it's still running 1007 if (updateTicker && isForCurrentUser) { 1008 haltTicker(); 1009 tick(key, notification, false); 1010 } 1011 1012 // Recalculate the position of the sliding windows and the titles. 1013 setAreThereNotifications(); 1014 updateExpandedViewPos(EXPANDED_LEAVE_ALONE); 1015 } 1016 1017 private void updateNotificationViews(NotificationData.Entry entry, 1018 StatusBarNotification notification) { 1019 final RemoteViews contentView = notification.getNotification().contentView; 1020 final RemoteViews bigContentView = notification.getNotification().bigContentView; 1021 // Reapply the RemoteViews 1022 contentView.reapply(mContext, entry.expanded, mOnClickHandler); 1023 if (bigContentView != null && entry.getBigContentView() != null) { 1024 bigContentView.reapply(mContext, entry.getBigContentView(), mOnClickHandler); 1025 } 1026 // update the contentIntent 1027 final PendingIntent contentIntent = notification.getNotification().contentIntent; 1028 if (contentIntent != null) { 1029 final View.OnClickListener listener = makeClicker(contentIntent, 1030 notification.getPackageName(), notification.getTag(), notification.getId()); 1031 entry.content.setOnClickListener(listener); 1032 } else { 1033 entry.content.setOnClickListener(null); 1034 } 1035 } 1036 1037 protected void notifyHeadsUpScreenOn(boolean screenOn) { 1038 if (!screenOn && mInterruptingNotificationEntry != null) { 1039 mHandler.sendEmptyMessage(MSG_ESCALATE_HEADS_UP); 1040 } 1041 } 1042 1043 protected boolean shouldInterrupt(StatusBarNotification sbn) { 1044 Notification notification = sbn.getNotification(); 1045 // some predicates to make the boolean logic legible 1046 boolean isNoisy = (notification.defaults & Notification.DEFAULT_SOUND) != 0 1047 || (notification.defaults & Notification.DEFAULT_VIBRATE) != 0 1048 || notification.sound != null 1049 || notification.vibrate != null; 1050 boolean isHighPriority = sbn.getScore() >= INTERRUPTION_THRESHOLD; 1051 boolean isFullscreen = notification.fullScreenIntent != null; 1052 boolean isAllowed = notification.extras.getInt(Notification.EXTRA_AS_HEADS_UP, 1053 Notification.HEADS_UP_ALLOWED) != Notification.HEADS_UP_NEVER; 1054 1055 final KeyguardTouchDelegate keyguard = KeyguardTouchDelegate.getInstance(mContext); 1056 boolean interrupt = (isFullscreen || (isHighPriority && isNoisy)) 1057 && isAllowed 1058 && mPowerManager.isScreenOn() 1059 && !keyguard.isShowingAndNotHidden() 1060 && !keyguard.isInputRestricted(); 1061 try { 1062 interrupt = interrupt && !mDreamManager.isDreaming(); 1063 } catch (RemoteException e) { 1064 Log.d(TAG, "failed to query dream manager", e); 1065 } 1066 if (DEBUG) Log.d(TAG, "interrupt: " + interrupt); 1067 return interrupt; 1068 } 1069 1070 // Q: What kinds of notifications should show during setup? 1071 // A: Almost none! Only things coming from the system (package is "android") that also 1072 // have special "kind" tags marking them as relevant for setup (see below). 1073 protected boolean showNotificationEvenIfUnprovisioned(StatusBarNotification sbn) { 1074 if ("android".equals(sbn.getPackageName())) { 1075 if (sbn.getNotification().kind != null) { 1076 for (String aKind : sbn.getNotification().kind) { 1077 // IME switcher, created by InputMethodManagerService 1078 if ("android.system.imeswitcher".equals(aKind)) return true; 1079 // OTA availability & errors, created by SystemUpdateService 1080 if ("android.system.update".equals(aKind)) return true; 1081 } 1082 } 1083 } 1084 return false; 1085 } 1086 1087 public boolean inKeyguardRestrictedInputMode() { 1088 return KeyguardTouchDelegate.getInstance(mContext).isInputRestricted(); 1089 } 1090 1091 public void setInteracting(int barWindow, boolean interacting) { 1092 // hook for subclasses 1093 } 1094 1095 public void destroy() { 1096 if (mSearchPanelView != null) { 1097 mWindowManager.removeViewImmediate(mSearchPanelView); 1098 } 1099 mContext.unregisterReceiver(mBroadcastReceiver); 1100 } 1101 } 1102