1 /* 2 * Copyright (C) 2018 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.bubbles; 18 19 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; 20 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; 21 import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT; 22 import static android.graphics.PixelFormat.TRANSPARENT; 23 import static android.view.Display.INVALID_DISPLAY; 24 import static android.view.InsetsState.ITYPE_IME; 25 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; 26 import static android.view.ViewRootImpl.NEW_INSETS_MODE_FULL; 27 import static android.view.ViewRootImpl.sNewInsetsMode; 28 import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; 29 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 30 import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; 31 import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL; 32 33 import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_EXPANDED_VIEW; 34 import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES; 35 import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; 36 37 import android.annotation.SuppressLint; 38 import android.app.ActivityManager; 39 import android.app.ActivityOptions; 40 import android.app.ActivityTaskManager; 41 import android.app.ActivityView; 42 import android.app.PendingIntent; 43 import android.content.ComponentName; 44 import android.content.Context; 45 import android.content.Intent; 46 import android.content.res.Configuration; 47 import android.content.res.Resources; 48 import android.content.res.TypedArray; 49 import android.graphics.Color; 50 import android.graphics.Insets; 51 import android.graphics.Outline; 52 import android.graphics.Point; 53 import android.graphics.Rect; 54 import android.graphics.drawable.ShapeDrawable; 55 import android.hardware.display.VirtualDisplay; 56 import android.os.Binder; 57 import android.os.RemoteException; 58 import android.util.AttributeSet; 59 import android.util.Log; 60 import android.view.Gravity; 61 import android.view.SurfaceControl; 62 import android.view.SurfaceView; 63 import android.view.View; 64 import android.view.ViewGroup; 65 import android.view.ViewOutlineProvider; 66 import android.view.WindowInsets; 67 import android.view.WindowManager; 68 import android.view.accessibility.AccessibilityNodeInfo; 69 import android.widget.FrameLayout; 70 import android.widget.LinearLayout; 71 72 import androidx.annotation.Nullable; 73 74 import com.android.internal.policy.ScreenDecorationsUtils; 75 import com.android.systemui.Dependency; 76 import com.android.systemui.R; 77 import com.android.systemui.recents.TriangleShape; 78 import com.android.systemui.statusbar.AlphaOptimizedButton; 79 80 /** 81 * Container for the expanded bubble view, handles rendering the caret and settings icon. 82 */ 83 public class BubbleExpandedView extends LinearLayout { 84 private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleExpandedView" : TAG_BUBBLES; 85 private static final String WINDOW_TITLE = "ImeInsetsWindowWithoutContent"; 86 87 private enum ActivityViewStatus { 88 // ActivityView is being initialized, cannot start an activity yet. 89 INITIALIZING, 90 // ActivityView is initialized, and ready to start an activity. 91 INITIALIZED, 92 // Activity runs in the ActivityView. 93 ACTIVITY_STARTED, 94 // ActivityView is released, so activity launching will no longer be permitted. 95 RELEASED, 96 } 97 98 // The triangle pointing to the expanded view 99 private View mPointerView; 100 private int mPointerMargin; 101 @Nullable private int[] mExpandedViewContainerLocation; 102 103 private AlphaOptimizedButton mSettingsIcon; 104 105 // Views for expanded state 106 private ActivityView mActivityView; 107 108 private ActivityViewStatus mActivityViewStatus = ActivityViewStatus.INITIALIZING; 109 private int mTaskId = -1; 110 111 private PendingIntent mPendingIntent; 112 113 private boolean mKeyboardVisible; 114 private boolean mNeedsNewHeight; 115 116 private Point mDisplaySize; 117 private int mMinHeight; 118 private int mOverflowHeight; 119 private int mSettingsIconHeight; 120 private int mPointerWidth; 121 private int mPointerHeight; 122 private ShapeDrawable mPointerDrawable; 123 private int mExpandedViewPadding; 124 125 126 @Nullable private Bubble mBubble; 127 128 private boolean mIsOverflow; 129 130 private BubbleController mBubbleController = Dependency.get(BubbleController.class); 131 private WindowManager mWindowManager; 132 private ActivityManager mActivityManager; 133 134 private BubbleStackView mStackView; 135 private View mVirtualImeView; 136 private WindowManager mVirtualDisplayWindowManager; 137 private boolean mImeShowing = false; 138 private float mCornerRadius = 0f; 139 140 /** 141 * Container for the ActivityView that has a solid, round-rect background that shows if the 142 * ActivityView hasn't loaded. 143 */ 144 private FrameLayout mActivityViewContainer = new FrameLayout(getContext()); 145 146 /** The SurfaceView that the ActivityView draws to. */ 147 @Nullable private SurfaceView mActivitySurface; 148 149 private ActivityView.StateCallback mStateCallback = new ActivityView.StateCallback() { 150 @Override 151 public void onActivityViewReady(ActivityView view) { 152 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 153 Log.d(TAG, "onActivityViewReady: mActivityViewStatus=" + mActivityViewStatus 154 + " bubble=" + getBubbleKey()); 155 } 156 switch (mActivityViewStatus) { 157 case INITIALIZING: 158 case INITIALIZED: 159 // Custom options so there is no activity transition animation 160 ActivityOptions options = ActivityOptions.makeCustomAnimation(getContext(), 161 0 /* enterResId */, 0 /* exitResId */); 162 options.setTaskAlwaysOnTop(true); 163 options.setLaunchWindowingMode(WINDOWING_MODE_MULTI_WINDOW); 164 // Post to keep the lifecycle normal 165 post(() -> { 166 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 167 Log.d(TAG, "onActivityViewReady: calling startActivity, " 168 + "bubble=" + getBubbleKey()); 169 } 170 if (mActivityView == null) { 171 mBubbleController.removeBubble(getBubbleKey(), 172 BubbleController.DISMISS_INVALID_INTENT); 173 return; 174 } 175 try { 176 if (!mIsOverflow && mBubble.hasMetadataShortcutId() 177 && mBubble.getShortcutInfo() != null) { 178 options.setApplyActivityFlagsForBubbles(true); 179 mActivityView.startShortcutActivity(mBubble.getShortcutInfo(), 180 options, null /* sourceBounds */); 181 } else { 182 Intent fillInIntent = new Intent(); 183 // Apply flags to make behaviour match documentLaunchMode=always. 184 fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT); 185 fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); 186 if (mBubble != null) { 187 mBubble.setIntentActive(); 188 } 189 mActivityView.startActivity(mPendingIntent, fillInIntent, options); 190 } 191 } catch (RuntimeException e) { 192 // If there's a runtime exception here then there's something 193 // wrong with the intent, we can't really recover / try to populate 194 // the bubble again so we'll just remove it. 195 Log.w(TAG, "Exception while displaying bubble: " + getBubbleKey() 196 + ", " + e.getMessage() + "; removing bubble"); 197 mBubbleController.removeBubble(getBubbleKey(), 198 BubbleController.DISMISS_INVALID_INTENT); 199 } 200 }); 201 mActivityViewStatus = ActivityViewStatus.ACTIVITY_STARTED; 202 break; 203 case ACTIVITY_STARTED: 204 post(() -> mActivityManager.moveTaskToFront(mTaskId, 0)); 205 break; 206 } 207 } 208 209 @Override 210 public void onActivityViewDestroyed(ActivityView view) { 211 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 212 Log.d(TAG, "onActivityViewDestroyed: mActivityViewStatus=" + mActivityViewStatus 213 + " bubble=" + getBubbleKey()); 214 } 215 mActivityViewStatus = ActivityViewStatus.RELEASED; 216 } 217 218 @Override 219 public void onTaskCreated(int taskId, ComponentName componentName) { 220 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 221 Log.d(TAG, "onTaskCreated: taskId=" + taskId 222 + " bubble=" + getBubbleKey()); 223 } 224 // Since Bubble ActivityView applies singleTaskDisplay this is 225 // guaranteed to only be called once per ActivityView. The taskId is 226 // saved to use for removeTask, preventing appearance in recent tasks. 227 mTaskId = taskId; 228 } 229 230 /** 231 * This is only called for tasks on this ActivityView, which is also set to 232 * single-task mode -- meaning never more than one task on this display. If a task 233 * is being removed, it's the top Activity finishing and this bubble should 234 * be removed or collapsed. 235 */ 236 @Override 237 public void onTaskRemovalStarted(int taskId) { 238 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 239 Log.d(TAG, "onTaskRemovalStarted: taskId=" + taskId 240 + " mActivityViewStatus=" + mActivityViewStatus 241 + " bubble=" + getBubbleKey()); 242 } 243 if (mBubble != null) { 244 // Must post because this is called from a binder thread. 245 post(() -> mBubbleController.removeBubble(mBubble.getKey(), 246 BubbleController.DISMISS_TASK_FINISHED)); 247 } 248 } 249 }; 250 BubbleExpandedView(Context context)251 public BubbleExpandedView(Context context) { 252 this(context, null); 253 } 254 BubbleExpandedView(Context context, AttributeSet attrs)255 public BubbleExpandedView(Context context, AttributeSet attrs) { 256 this(context, attrs, 0); 257 } 258 BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr)259 public BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr) { 260 this(context, attrs, defStyleAttr, 0); 261 } 262 BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)263 public BubbleExpandedView(Context context, AttributeSet attrs, int defStyleAttr, 264 int defStyleRes) { 265 super(context, attrs, defStyleAttr, defStyleRes); 266 updateDimensions(); 267 mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); 268 } 269 updateDimensions()270 void updateDimensions() { 271 mDisplaySize = new Point(); 272 mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); 273 // Get the real size -- this includes screen decorations (notches, statusbar, navbar). 274 mWindowManager.getDefaultDisplay().getRealSize(mDisplaySize); 275 Resources res = getResources(); 276 mMinHeight = res.getDimensionPixelSize(R.dimen.bubble_expanded_default_height); 277 mOverflowHeight = res.getDimensionPixelSize(R.dimen.bubble_overflow_height); 278 mPointerMargin = res.getDimensionPixelSize(R.dimen.bubble_pointer_margin); 279 } 280 281 @SuppressLint("ClickableViewAccessibility") 282 @Override onFinishInflate()283 protected void onFinishInflate() { 284 super.onFinishInflate(); 285 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 286 Log.d(TAG, "onFinishInflate: bubble=" + getBubbleKey()); 287 } 288 289 Resources res = getResources(); 290 mPointerView = findViewById(R.id.pointer_view); 291 mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width); 292 mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height); 293 294 mPointerDrawable = new ShapeDrawable(TriangleShape.create( 295 mPointerWidth, mPointerHeight, true /* pointUp */)); 296 mPointerView.setVisibility(INVISIBLE); 297 298 mSettingsIconHeight = getContext().getResources().getDimensionPixelSize( 299 R.dimen.bubble_manage_button_height); 300 mSettingsIcon = findViewById(R.id.settings_button); 301 302 mActivityView = new ActivityView(mContext, null /* attrs */, 0 /* defStyle */, 303 true /* singleTaskInstance */, false /* usePublicVirtualDisplay*/, 304 true /* disableSurfaceViewBackgroundLayer */); 305 306 // Set ActivityView's alpha value as zero, since there is no view content to be shown. 307 setContentVisibility(false); 308 309 mActivityViewContainer.setBackgroundColor(Color.WHITE); 310 mActivityViewContainer.setOutlineProvider(new ViewOutlineProvider() { 311 @Override 312 public void getOutline(View view, Outline outline) { 313 outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCornerRadius); 314 } 315 }); 316 mActivityViewContainer.setClipToOutline(true); 317 mActivityViewContainer.addView(mActivityView); 318 mActivityViewContainer.setLayoutParams( 319 new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); 320 addView(mActivityViewContainer); 321 322 if (mActivityView != null 323 && mActivityView.getChildCount() > 0 324 && mActivityView.getChildAt(0) instanceof SurfaceView) { 325 // Retrieve the surface from the ActivityView so we can screenshot it and change its 326 // z-ordering. This should always be possible, since ActivityView's constructor adds the 327 // SurfaceView as its first child. 328 mActivitySurface = (SurfaceView) mActivityView.getChildAt(0); 329 } 330 331 // Expanded stack layout, top to bottom: 332 // Expanded view container 333 // ==> bubble row 334 // ==> expanded view 335 // ==> activity view 336 // ==> manage button 337 bringChildToFront(mActivityView); 338 bringChildToFront(mSettingsIcon); 339 340 applyThemeAttrs(); 341 342 setOnApplyWindowInsetsListener((View view, WindowInsets insets) -> { 343 // Keep track of IME displaying because we should not make any adjustments that might 344 // cause a config change while the IME is displayed otherwise it'll loose focus. 345 final int keyboardHeight = insets.getSystemWindowInsetBottom() 346 - insets.getStableInsetBottom(); 347 mKeyboardVisible = keyboardHeight != 0; 348 if (!mKeyboardVisible && mNeedsNewHeight) { 349 updateHeight(); 350 } 351 return view.onApplyWindowInsets(insets); 352 }); 353 354 mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding); 355 setPadding(mExpandedViewPadding, mExpandedViewPadding, mExpandedViewPadding, 356 mExpandedViewPadding); 357 setOnTouchListener((view, motionEvent) -> { 358 if (!usingActivityView()) { 359 return false; 360 } 361 362 final Rect avBounds = new Rect(); 363 mActivityView.getBoundsOnScreen(avBounds); 364 365 // Consume and ignore events on the expanded view padding that are within the 366 // ActivityView's vertical bounds. These events are part of a back gesture, and so they 367 // should not collapse the stack (which all other touches on areas around the AV would 368 // do). 369 if (motionEvent.getRawY() >= avBounds.top 370 && motionEvent.getRawY() <= avBounds.bottom 371 && (motionEvent.getRawX() < avBounds.left 372 || motionEvent.getRawX() > avBounds.right)) { 373 return true; 374 } 375 376 return false; 377 }); 378 379 // BubbleStackView is forced LTR, but we want to respect the locale for expanded view layout 380 // so the Manage button appears on the right. 381 setLayoutDirection(LAYOUT_DIRECTION_LOCALE); 382 } 383 getBubbleKey()384 private String getBubbleKey() { 385 return mBubble != null ? mBubble.getKey() : "null"; 386 } 387 388 /** 389 * Asks the ActivityView's surface to draw on top of all other views in the window. This is 390 * useful for ordering surfaces during animations, but should otherwise be set to false so that 391 * bubbles and menus can draw over the ActivityView. 392 */ setSurfaceZOrderedOnTop(boolean onTop)393 void setSurfaceZOrderedOnTop(boolean onTop) { 394 if (mActivitySurface == null) { 395 return; 396 } 397 398 mActivitySurface.setZOrderedOnTop(onTop, true); 399 } 400 401 /** Return a GraphicBuffer with the contents of the ActivityView's underlying surface. */ snapshotActivitySurface()402 @Nullable SurfaceControl.ScreenshotGraphicBuffer snapshotActivitySurface() { 403 if (mActivitySurface == null) { 404 return null; 405 } 406 407 return SurfaceControl.captureLayers( 408 mActivitySurface.getSurfaceControl(), 409 new Rect(0, 0, mActivityView.getWidth(), mActivityView.getHeight()), 410 1 /* scale */); 411 } 412 getActivityViewLocationOnScreen()413 int[] getActivityViewLocationOnScreen() { 414 if (mActivityView != null) { 415 return mActivityView.getLocationOnScreen(); 416 } else { 417 return new int[]{0, 0}; 418 } 419 } 420 setManageClickListener(OnClickListener manageClickListener)421 void setManageClickListener(OnClickListener manageClickListener) { 422 findViewById(R.id.settings_button).setOnClickListener(manageClickListener); 423 } 424 425 /** 426 * Updates the ActivityView's obscured touchable region. This calls onLocationChanged, which 427 * results in a call to {@link BubbleStackView#subtractObscuredTouchableRegion}. This is useful 428 * if a view has been added or removed from on top of the ActivityView, such as the manage menu. 429 */ updateObscuredTouchableRegion()430 void updateObscuredTouchableRegion() { 431 if (mActivityView != null) { 432 mActivityView.onLocationChanged(); 433 } 434 } 435 applyThemeAttrs()436 void applyThemeAttrs() { 437 final TypedArray ta = mContext.obtainStyledAttributes( 438 new int[] {android.R.attr.dialogCornerRadius}); 439 mCornerRadius = ta.getDimensionPixelSize(0, 0); 440 ta.recycle(); 441 442 if (mActivityView != null && ScreenDecorationsUtils.supportsRoundedCornersOnWindows( 443 mContext.getResources())) { 444 mActivityView.setCornerRadius(mCornerRadius); 445 } 446 447 final int mode = 448 getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; 449 switch (mode) { 450 case Configuration.UI_MODE_NIGHT_NO: 451 mPointerDrawable.setTint(getResources().getColor(R.color.bubbles_light)); 452 break; 453 case Configuration.UI_MODE_NIGHT_YES: 454 mPointerDrawable.setTint(getResources().getColor(R.color.bubbles_dark)); 455 break; 456 } 457 mPointerView.setBackground(mPointerDrawable); 458 } 459 460 /** 461 * Hides the IME if it's showing. This is currently done by dispatching a back press to the AV. 462 */ hideImeIfVisible()463 void hideImeIfVisible() { 464 if (mKeyboardVisible) { 465 performBackPressIfNeeded(); 466 } 467 } 468 469 @Override onDetachedFromWindow()470 protected void onDetachedFromWindow() { 471 super.onDetachedFromWindow(); 472 mKeyboardVisible = false; 473 mNeedsNewHeight = false; 474 if (mActivityView != null) { 475 if (sNewInsetsMode == NEW_INSETS_MODE_FULL) { 476 setImeWindowToDisplay(0, 0); 477 } else { 478 mActivityView.setForwardedInsets(Insets.of(0, 0, 0, 0)); 479 } 480 } 481 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 482 Log.d(TAG, "onDetachedFromWindow: bubble=" + getBubbleKey()); 483 } 484 } 485 486 /** 487 * Set visibility of contents in the expanded state. 488 * 489 * @param visibility {@code true} if the contents should be visible on the screen. 490 * 491 * Note that this contents visibility doesn't affect visibility at {@link android.view.View}, 492 * and setting {@code false} actually means rendering the contents in transparent. 493 */ setContentVisibility(boolean visibility)494 void setContentVisibility(boolean visibility) { 495 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 496 Log.d(TAG, "setContentVisibility: visibility=" + visibility 497 + " bubble=" + getBubbleKey()); 498 } 499 final float alpha = visibility ? 1f : 0f; 500 501 mPointerView.setAlpha(alpha); 502 503 if (mActivityView != null && alpha != mActivityView.getAlpha()) { 504 mActivityView.setAlpha(alpha); 505 mActivityView.bringToFront(); 506 } 507 } 508 getActivityView()509 @Nullable ActivityView getActivityView() { 510 return mActivityView; 511 } 512 getTaskId()513 int getTaskId() { 514 return mTaskId; 515 } 516 517 /** 518 * Called by {@link BubbleStackView} when the insets for the expanded state should be updated. 519 * This should be done post-move and post-animation. 520 */ updateInsets(WindowInsets insets)521 void updateInsets(WindowInsets insets) { 522 if (usingActivityView()) { 523 int[] screenLoc = mActivityView.getLocationOnScreen(); 524 final int activityViewBottom = screenLoc[1] + mActivityView.getHeight(); 525 final int keyboardTop = mDisplaySize.y - Math.max(insets.getSystemWindowInsetBottom(), 526 insets.getDisplayCutout() != null 527 ? insets.getDisplayCutout().getSafeInsetBottom() 528 : 0); 529 final int insetsBottom = Math.max(activityViewBottom - keyboardTop, 0); 530 531 if (sNewInsetsMode == NEW_INSETS_MODE_FULL) { 532 setImeWindowToDisplay(getWidth(), insetsBottom); 533 } else { 534 mActivityView.setForwardedInsets(Insets.of(0, 0, 0, insetsBottom)); 535 } 536 } 537 } 538 setImeWindowToDisplay(int w, int h)539 private void setImeWindowToDisplay(int w, int h) { 540 if (getVirtualDisplayId() == INVALID_DISPLAY) { 541 return; 542 } 543 if (h == 0 || w == 0) { 544 if (mImeShowing) { 545 mVirtualImeView.setVisibility(GONE); 546 mImeShowing = false; 547 } 548 return; 549 } 550 final Context virtualDisplayContext = mContext.createDisplayContext( 551 getVirtualDisplay().getDisplay()); 552 553 if (mVirtualDisplayWindowManager == null) { 554 mVirtualDisplayWindowManager = 555 (WindowManager) virtualDisplayContext.getSystemService(Context.WINDOW_SERVICE); 556 } 557 if (mVirtualImeView == null) { 558 mVirtualImeView = new View(virtualDisplayContext); 559 mVirtualImeView.setVisibility(VISIBLE); 560 mVirtualDisplayWindowManager.addView(mVirtualImeView, 561 getVirtualImeViewAttrs(w, h)); 562 } else { 563 mVirtualDisplayWindowManager.updateViewLayout(mVirtualImeView, 564 getVirtualImeViewAttrs(w, h)); 565 mVirtualImeView.setVisibility(VISIBLE); 566 } 567 568 mImeShowing = true; 569 } 570 getVirtualImeViewAttrs(int w, int h)571 private WindowManager.LayoutParams getVirtualImeViewAttrs(int w, int h) { 572 // To use TYPE_NAVIGATION_BAR_PANEL instead of TYPE_IME_BAR to bypass the IME window type 573 // token check when adding the window. 574 final WindowManager.LayoutParams attrs = 575 new WindowManager.LayoutParams(w, h, TYPE_NAVIGATION_BAR_PANEL, 576 FLAG_LAYOUT_NO_LIMITS | FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE, 577 TRANSPARENT); 578 attrs.gravity = Gravity.BOTTOM; 579 attrs.setTitle(WINDOW_TITLE); 580 attrs.token = new Binder(); 581 attrs.providesInsetsTypes = new int[]{ITYPE_IME}; 582 attrs.alpha = 0.0f; 583 return attrs; 584 } 585 setStackView(BubbleStackView stackView)586 void setStackView(BubbleStackView stackView) { 587 mStackView = stackView; 588 } 589 setOverflow(boolean overflow)590 public void setOverflow(boolean overflow) { 591 mIsOverflow = overflow; 592 593 Intent target = new Intent(mContext, BubbleOverflowActivity.class); 594 mPendingIntent = PendingIntent.getActivity(mContext, /* requestCode */ 0, 595 target, PendingIntent.FLAG_UPDATE_CURRENT); 596 mSettingsIcon.setVisibility(GONE); 597 } 598 599 /** 600 * Sets the bubble used to populate this view. 601 */ update(Bubble bubble)602 void update(Bubble bubble) { 603 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 604 Log.d(TAG, "update: bubble=" + (bubble != null ? bubble.getKey() : "null")); 605 } 606 boolean isNew = mBubble == null || didBackingContentChange(bubble); 607 if (isNew || bubble != null && bubble.getKey().equals(mBubble.getKey())) { 608 mBubble = bubble; 609 mSettingsIcon.setContentDescription(getResources().getString( 610 R.string.bubbles_settings_button_description, bubble.getAppName())); 611 612 mSettingsIcon.setAccessibilityDelegate( 613 new AccessibilityDelegate() { 614 @Override 615 public void onInitializeAccessibilityNodeInfo(View host, 616 AccessibilityNodeInfo info) { 617 super.onInitializeAccessibilityNodeInfo(host, info); 618 // On focus, have TalkBack say 619 // "Actions available. Use swipe up then right to view." 620 // in addition to the default "double tap to activate". 621 mStackView.setupLocalMenu(info); 622 } 623 }); 624 625 if (isNew) { 626 mPendingIntent = mBubble.getBubbleIntent(); 627 if (mPendingIntent != null || mBubble.hasMetadataShortcutId()) { 628 setContentVisibility(false); 629 mActivityView.setVisibility(VISIBLE); 630 } 631 } 632 applyThemeAttrs(); 633 } else { 634 Log.w(TAG, "Trying to update entry with different key, new bubble: " 635 + bubble.getKey() + " old bubble: " + bubble.getKey()); 636 } 637 } 638 didBackingContentChange(Bubble newBubble)639 private boolean didBackingContentChange(Bubble newBubble) { 640 boolean prevWasIntentBased = mBubble != null && mPendingIntent != null; 641 boolean newIsIntentBased = newBubble.getBubbleIntent() != null; 642 return prevWasIntentBased != newIsIntentBased; 643 } 644 645 /** 646 * Lets activity view know it should be shown / populated with activity content. 647 */ populateExpandedView()648 void populateExpandedView() { 649 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 650 Log.d(TAG, "populateExpandedView: " 651 + "bubble=" + getBubbleKey()); 652 } 653 654 if (usingActivityView()) { 655 mActivityView.setCallback(mStateCallback); 656 } else { 657 Log.e(TAG, "Cannot populate expanded view."); 658 } 659 } 660 performBackPressIfNeeded()661 boolean performBackPressIfNeeded() { 662 if (!usingActivityView()) { 663 return false; 664 } 665 mActivityView.performBackPress(); 666 return true; 667 } 668 updateHeight()669 void updateHeight() { 670 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 671 Log.d(TAG, "updateHeight: bubble=" + getBubbleKey()); 672 } 673 674 if (mExpandedViewContainerLocation == null) { 675 return; 676 } 677 678 if (usingActivityView()) { 679 float desiredHeight = mOverflowHeight; 680 if (!mIsOverflow) { 681 desiredHeight = Math.max(mBubble.getDesiredHeight(mContext), mMinHeight); 682 } 683 float height = Math.min(desiredHeight, getMaxExpandedHeight()); 684 height = Math.max(height, mMinHeight); 685 ViewGroup.LayoutParams lp = mActivityView.getLayoutParams(); 686 mNeedsNewHeight = lp.height != height; 687 if (!mKeyboardVisible) { 688 // If the keyboard is visible... don't adjust the height because that will cause 689 // a configuration change and the keyboard will be lost. 690 lp.height = (int) height; 691 mActivityView.setLayoutParams(lp); 692 mNeedsNewHeight = false; 693 } 694 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 695 Log.d(TAG, "updateHeight: bubble=" + getBubbleKey() 696 + " height=" + height 697 + " mNeedsNewHeight=" + mNeedsNewHeight); 698 } 699 } 700 } 701 getMaxExpandedHeight()702 private int getMaxExpandedHeight() { 703 mWindowManager.getDefaultDisplay().getRealSize(mDisplaySize); 704 int bottomInset = getRootWindowInsets() != null 705 ? getRootWindowInsets().getStableInsetBottom() 706 : 0; 707 708 return mDisplaySize.y 709 - mExpandedViewContainerLocation[1] 710 - getPaddingTop() 711 - getPaddingBottom() 712 - mSettingsIconHeight 713 - mPointerHeight 714 - mPointerMargin - bottomInset; 715 } 716 717 /** 718 * Update appearance of the expanded view being displayed. 719 * 720 * @param containerLocationOnScreen The location on-screen of the container the expanded view is 721 * added to. This allows us to calculate max height without 722 * waiting for layout. 723 */ updateView(int[] containerLocationOnScreen)724 public void updateView(int[] containerLocationOnScreen) { 725 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 726 Log.d(TAG, "updateView: bubble=" 727 + getBubbleKey()); 728 } 729 730 mExpandedViewContainerLocation = containerLocationOnScreen; 731 732 if (usingActivityView() 733 && mActivityView.getVisibility() == VISIBLE 734 && mActivityView.isAttachedToWindow()) { 735 mActivityView.onLocationChanged(); 736 updateHeight(); 737 } 738 } 739 740 /** 741 * Set the x position that the tip of the triangle should point to. 742 */ setPointerPosition(float x)743 public void setPointerPosition(float x) { 744 float halfPointerWidth = mPointerWidth / 2f; 745 float pointerLeft = x - halfPointerWidth - mExpandedViewPadding; 746 mPointerView.setTranslationX(pointerLeft); 747 mPointerView.setVisibility(VISIBLE); 748 } 749 750 /** 751 * Position of the manage button displayed in the expanded view. Used for placing user 752 * education about the manage button. 753 */ getManageButtonBoundsOnScreen(Rect rect)754 public void getManageButtonBoundsOnScreen(Rect rect) { 755 mSettingsIcon.getBoundsOnScreen(rect); 756 } 757 758 /** 759 * Removes and releases an ActivityView if one was previously created for this bubble. 760 */ cleanUpExpandedState()761 public void cleanUpExpandedState() { 762 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 763 Log.d(TAG, "cleanUpExpandedState: mActivityViewStatus=" + mActivityViewStatus 764 + ", bubble=" + getBubbleKey()); 765 } 766 if (mActivityView == null) { 767 return; 768 } 769 mActivityView.release(); 770 if (mTaskId != -1) { 771 try { 772 ActivityTaskManager.getService().removeTask(mTaskId); 773 } catch (RemoteException e) { 774 Log.w(TAG, "Failed to remove taskId " + mTaskId); 775 } 776 mTaskId = -1; 777 } 778 removeView(mActivityView); 779 780 mActivityView = null; 781 } 782 783 /** 784 * Called when the last task is removed from a {@link android.hardware.display.VirtualDisplay} 785 * which {@link ActivityView} uses. 786 */ notifyDisplayEmpty()787 void notifyDisplayEmpty() { 788 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 789 Log.d(TAG, "notifyDisplayEmpty: bubble=" 790 + getBubbleKey() 791 + " mActivityViewStatus=" + mActivityViewStatus); 792 } 793 if (mActivityViewStatus == ActivityViewStatus.ACTIVITY_STARTED) { 794 mActivityViewStatus = ActivityViewStatus.INITIALIZED; 795 } 796 } 797 usingActivityView()798 private boolean usingActivityView() { 799 return (mPendingIntent != null || mBubble.hasMetadataShortcutId()) 800 && mActivityView != null; 801 } 802 803 /** 804 * @return the display id of the virtual display. 805 */ getVirtualDisplayId()806 public int getVirtualDisplayId() { 807 if (usingActivityView()) { 808 return mActivityView.getVirtualDisplayId(); 809 } 810 return INVALID_DISPLAY; 811 } 812 getVirtualDisplay()813 private VirtualDisplay getVirtualDisplay() { 814 if (usingActivityView()) { 815 return mActivityView.getVirtualDisplay(); 816 } 817 return null; 818 } 819 } 820