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