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 */, true /* useTrustedDisplay */); 305 306 // Set ActivityView's alpha value as zero, since there is no view content to be shown. 307 setContentVisibility(false); 308 309 mActivityViewContainer.setOutlineProvider(new ViewOutlineProvider() { 310 @Override 311 public void getOutline(View view, Outline outline) { 312 outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCornerRadius); 313 } 314 }); 315 mActivityViewContainer.setClipToOutline(true); 316 mActivityViewContainer.addView(mActivityView); 317 mActivityViewContainer.setLayoutParams( 318 new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); 319 addView(mActivityViewContainer); 320 321 if (mActivityView != null 322 && mActivityView.getChildCount() > 0 323 && mActivityView.getChildAt(0) instanceof SurfaceView) { 324 // Retrieve the surface from the ActivityView so we can screenshot it and change its 325 // z-ordering. This should always be possible, since ActivityView's constructor adds the 326 // SurfaceView as its first child. 327 mActivitySurface = (SurfaceView) mActivityView.getChildAt(0); 328 } 329 330 // Expanded stack layout, top to bottom: 331 // Expanded view container 332 // ==> bubble row 333 // ==> expanded view 334 // ==> activity view 335 // ==> manage button 336 bringChildToFront(mActivityView); 337 bringChildToFront(mSettingsIcon); 338 339 applyThemeAttrs(); 340 341 setOnApplyWindowInsetsListener((View view, WindowInsets insets) -> { 342 // Keep track of IME displaying because we should not make any adjustments that might 343 // cause a config change while the IME is displayed otherwise it'll loose focus. 344 final int keyboardHeight = insets.getSystemWindowInsetBottom() 345 - insets.getStableInsetBottom(); 346 mKeyboardVisible = keyboardHeight != 0; 347 if (!mKeyboardVisible && mNeedsNewHeight) { 348 updateHeight(); 349 } 350 return view.onApplyWindowInsets(insets); 351 }); 352 353 mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding); 354 setPadding(mExpandedViewPadding, mExpandedViewPadding, mExpandedViewPadding, 355 mExpandedViewPadding); 356 setOnTouchListener((view, motionEvent) -> { 357 if (!usingActivityView()) { 358 return false; 359 } 360 361 final Rect avBounds = new Rect(); 362 mActivityView.getBoundsOnScreen(avBounds); 363 364 // Consume and ignore events on the expanded view padding that are within the 365 // ActivityView's vertical bounds. These events are part of a back gesture, and so they 366 // should not collapse the stack (which all other touches on areas around the AV would 367 // do). 368 if (motionEvent.getRawY() >= avBounds.top 369 && motionEvent.getRawY() <= avBounds.bottom 370 && (motionEvent.getRawX() < avBounds.left 371 || motionEvent.getRawX() > avBounds.right)) { 372 return true; 373 } 374 375 return false; 376 }); 377 378 // BubbleStackView is forced LTR, but we want to respect the locale for expanded view layout 379 // so the Manage button appears on the right. 380 setLayoutDirection(LAYOUT_DIRECTION_LOCALE); 381 } 382 getBubbleKey()383 private String getBubbleKey() { 384 return mBubble != null ? mBubble.getKey() : "null"; 385 } 386 387 /** 388 * Asks the ActivityView's surface to draw on top of all other views in the window. This is 389 * useful for ordering surfaces during animations, but should otherwise be set to false so that 390 * bubbles and menus can draw over the ActivityView. 391 */ setSurfaceZOrderedOnTop(boolean onTop)392 void setSurfaceZOrderedOnTop(boolean onTop) { 393 if (mActivitySurface == null) { 394 return; 395 } 396 397 mActivitySurface.setZOrderedOnTop(onTop, true); 398 } 399 400 /** Return a GraphicBuffer with the contents of the ActivityView's underlying surface. */ snapshotActivitySurface()401 @Nullable SurfaceControl.ScreenshotGraphicBuffer snapshotActivitySurface() { 402 if (mActivitySurface == null) { 403 return null; 404 } 405 406 return SurfaceControl.captureLayers( 407 mActivitySurface.getSurfaceControl(), 408 new Rect(0, 0, mActivityView.getWidth(), mActivityView.getHeight()), 409 1 /* scale */); 410 } 411 getActivityViewLocationOnScreen()412 int[] getActivityViewLocationOnScreen() { 413 if (mActivityView != null) { 414 return mActivityView.getLocationOnScreen(); 415 } else { 416 return new int[]{0, 0}; 417 } 418 } 419 setManageClickListener(OnClickListener manageClickListener)420 void setManageClickListener(OnClickListener manageClickListener) { 421 findViewById(R.id.settings_button).setOnClickListener(manageClickListener); 422 } 423 424 /** 425 * Updates the ActivityView's obscured touchable region. This calls onLocationChanged, which 426 * results in a call to {@link BubbleStackView#subtractObscuredTouchableRegion}. This is useful 427 * if a view has been added or removed from on top of the ActivityView, such as the manage menu. 428 */ updateObscuredTouchableRegion()429 void updateObscuredTouchableRegion() { 430 if (mActivityView != null) { 431 mActivityView.onLocationChanged(); 432 } 433 } 434 applyThemeAttrs()435 void applyThemeAttrs() { 436 final TypedArray ta = mContext.obtainStyledAttributes(new int[] { 437 android.R.attr.dialogCornerRadius, 438 android.R.attr.colorBackgroundFloating}); 439 mCornerRadius = ta.getDimensionPixelSize(0, 0); 440 mActivityViewContainer.setBackgroundColor(ta.getColor(1, Color.WHITE)); 441 ta.recycle(); 442 443 if (mActivityView != null && ScreenDecorationsUtils.supportsRoundedCornersOnWindows( 444 mContext.getResources())) { 445 mActivityView.setCornerRadius(mCornerRadius); 446 } 447 448 final int mode = 449 getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; 450 switch (mode) { 451 case Configuration.UI_MODE_NIGHT_NO: 452 mPointerDrawable.setTint(getResources().getColor(R.color.bubbles_light)); 453 break; 454 case Configuration.UI_MODE_NIGHT_YES: 455 mPointerDrawable.setTint(getResources().getColor(R.color.bubbles_dark)); 456 break; 457 } 458 mPointerView.setBackground(mPointerDrawable); 459 } 460 461 /** 462 * Hides the IME if it's showing. This is currently done by dispatching a back press to the AV. 463 */ hideImeIfVisible()464 void hideImeIfVisible() { 465 if (mKeyboardVisible) { 466 performBackPressIfNeeded(); 467 } 468 } 469 470 @Override onDetachedFromWindow()471 protected void onDetachedFromWindow() { 472 super.onDetachedFromWindow(); 473 mKeyboardVisible = false; 474 mNeedsNewHeight = false; 475 if (mActivityView != null) { 476 if (sNewInsetsMode == NEW_INSETS_MODE_FULL) { 477 setImeWindowToDisplay(0, 0); 478 } else { 479 mActivityView.setForwardedInsets(Insets.of(0, 0, 0, 0)); 480 } 481 } 482 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 483 Log.d(TAG, "onDetachedFromWindow: bubble=" + getBubbleKey()); 484 } 485 } 486 487 /** 488 * Set visibility of contents in the expanded state. 489 * 490 * @param visibility {@code true} if the contents should be visible on the screen. 491 * 492 * Note that this contents visibility doesn't affect visibility at {@link android.view.View}, 493 * and setting {@code false} actually means rendering the contents in transparent. 494 */ setContentVisibility(boolean visibility)495 void setContentVisibility(boolean visibility) { 496 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 497 Log.d(TAG, "setContentVisibility: visibility=" + visibility 498 + " bubble=" + getBubbleKey()); 499 } 500 final float alpha = visibility ? 1f : 0f; 501 502 mPointerView.setAlpha(alpha); 503 504 if (mActivityView != null && alpha != mActivityView.getAlpha()) { 505 mActivityView.setAlpha(alpha); 506 mActivityView.bringToFront(); 507 } 508 } 509 getActivityView()510 @Nullable ActivityView getActivityView() { 511 return mActivityView; 512 } 513 getTaskId()514 int getTaskId() { 515 return mTaskId; 516 } 517 518 /** 519 * Called by {@link BubbleStackView} when the insets for the expanded state should be updated. 520 * This should be done post-move and post-animation. 521 */ updateInsets(WindowInsets insets)522 void updateInsets(WindowInsets insets) { 523 if (usingActivityView()) { 524 int[] screenLoc = mActivityView.getLocationOnScreen(); 525 final int activityViewBottom = screenLoc[1] + mActivityView.getHeight(); 526 final int keyboardTop = mDisplaySize.y - Math.max(insets.getSystemWindowInsetBottom(), 527 insets.getDisplayCutout() != null 528 ? insets.getDisplayCutout().getSafeInsetBottom() 529 : 0); 530 final int insetsBottom = Math.max(activityViewBottom - keyboardTop, 0); 531 532 if (sNewInsetsMode == NEW_INSETS_MODE_FULL) { 533 setImeWindowToDisplay(getWidth(), insetsBottom); 534 } else { 535 mActivityView.setForwardedInsets(Insets.of(0, 0, 0, insetsBottom)); 536 } 537 } 538 } 539 setImeWindowToDisplay(int w, int h)540 private void setImeWindowToDisplay(int w, int h) { 541 if (getVirtualDisplayId() == INVALID_DISPLAY) { 542 return; 543 } 544 if (h == 0 || w == 0) { 545 if (mImeShowing) { 546 mVirtualImeView.setVisibility(GONE); 547 mImeShowing = false; 548 } 549 return; 550 } 551 final Context virtualDisplayContext = mContext.createDisplayContext( 552 getVirtualDisplay().getDisplay()); 553 554 if (mVirtualDisplayWindowManager == null) { 555 mVirtualDisplayWindowManager = 556 (WindowManager) virtualDisplayContext.getSystemService(Context.WINDOW_SERVICE); 557 } 558 if (mVirtualImeView == null) { 559 mVirtualImeView = new View(virtualDisplayContext); 560 mVirtualImeView.setVisibility(VISIBLE); 561 mVirtualDisplayWindowManager.addView(mVirtualImeView, 562 getVirtualImeViewAttrs(w, h)); 563 } else { 564 mVirtualDisplayWindowManager.updateViewLayout(mVirtualImeView, 565 getVirtualImeViewAttrs(w, h)); 566 mVirtualImeView.setVisibility(VISIBLE); 567 } 568 569 mImeShowing = true; 570 } 571 getVirtualImeViewAttrs(int w, int h)572 private WindowManager.LayoutParams getVirtualImeViewAttrs(int w, int h) { 573 // To use TYPE_NAVIGATION_BAR_PANEL instead of TYPE_IME_BAR to bypass the IME window type 574 // token check when adding the window. 575 final WindowManager.LayoutParams attrs = 576 new WindowManager.LayoutParams(w, h, TYPE_NAVIGATION_BAR_PANEL, 577 FLAG_LAYOUT_NO_LIMITS | FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE, 578 TRANSPARENT); 579 attrs.gravity = Gravity.BOTTOM; 580 attrs.setTitle(WINDOW_TITLE); 581 attrs.token = new Binder(); 582 attrs.providesInsetsTypes = new int[]{ITYPE_IME}; 583 attrs.alpha = 0.0f; 584 return attrs; 585 } 586 setStackView(BubbleStackView stackView)587 void setStackView(BubbleStackView stackView) { 588 mStackView = stackView; 589 } 590 setOverflow(boolean overflow)591 public void setOverflow(boolean overflow) { 592 mIsOverflow = overflow; 593 594 Intent target = new Intent(mContext, BubbleOverflowActivity.class); 595 mPendingIntent = PendingIntent.getActivity(mContext, /* requestCode */ 0, 596 target, PendingIntent.FLAG_UPDATE_CURRENT); 597 mSettingsIcon.setVisibility(GONE); 598 } 599 600 /** 601 * Sets the bubble used to populate this view. 602 */ update(Bubble bubble)603 void update(Bubble bubble) { 604 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 605 Log.d(TAG, "update: bubble=" + (bubble != null ? bubble.getKey() : "null")); 606 } 607 boolean isNew = mBubble == null || didBackingContentChange(bubble); 608 if (isNew || bubble != null && bubble.getKey().equals(mBubble.getKey())) { 609 mBubble = bubble; 610 mSettingsIcon.setContentDescription(getResources().getString( 611 R.string.bubbles_settings_button_description, bubble.getAppName())); 612 613 mSettingsIcon.setAccessibilityDelegate( 614 new AccessibilityDelegate() { 615 @Override 616 public void onInitializeAccessibilityNodeInfo(View host, 617 AccessibilityNodeInfo info) { 618 super.onInitializeAccessibilityNodeInfo(host, info); 619 // On focus, have TalkBack say 620 // "Actions available. Use swipe up then right to view." 621 // in addition to the default "double tap to activate". 622 mStackView.setupLocalMenu(info); 623 } 624 }); 625 626 if (isNew) { 627 mPendingIntent = mBubble.getBubbleIntent(); 628 if (mPendingIntent != null || mBubble.hasMetadataShortcutId()) { 629 setContentVisibility(false); 630 mActivityView.setVisibility(VISIBLE); 631 } 632 } 633 applyThemeAttrs(); 634 } else { 635 Log.w(TAG, "Trying to update entry with different key, new bubble: " 636 + bubble.getKey() + " old bubble: " + bubble.getKey()); 637 } 638 } 639 didBackingContentChange(Bubble newBubble)640 private boolean didBackingContentChange(Bubble newBubble) { 641 boolean prevWasIntentBased = mBubble != null && mPendingIntent != null; 642 boolean newIsIntentBased = newBubble.getBubbleIntent() != null; 643 return prevWasIntentBased != newIsIntentBased; 644 } 645 646 /** 647 * Lets activity view know it should be shown / populated with activity content. 648 */ populateExpandedView()649 void populateExpandedView() { 650 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 651 Log.d(TAG, "populateExpandedView: " 652 + "bubble=" + getBubbleKey()); 653 } 654 655 if (usingActivityView()) { 656 mActivityView.setCallback(mStateCallback); 657 } else { 658 Log.e(TAG, "Cannot populate expanded view."); 659 } 660 } 661 performBackPressIfNeeded()662 boolean performBackPressIfNeeded() { 663 if (!usingActivityView()) { 664 return false; 665 } 666 mActivityView.performBackPress(); 667 return true; 668 } 669 updateHeight()670 void updateHeight() { 671 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 672 Log.d(TAG, "updateHeight: bubble=" + getBubbleKey()); 673 } 674 675 if (mExpandedViewContainerLocation == null) { 676 return; 677 } 678 679 if (usingActivityView()) { 680 float desiredHeight = mOverflowHeight; 681 if (!mIsOverflow) { 682 desiredHeight = Math.max(mBubble.getDesiredHeight(mContext), mMinHeight); 683 } 684 float height = Math.min(desiredHeight, getMaxExpandedHeight()); 685 height = Math.max(height, mMinHeight); 686 ViewGroup.LayoutParams lp = mActivityView.getLayoutParams(); 687 mNeedsNewHeight = lp.height != height; 688 if (!mKeyboardVisible) { 689 // If the keyboard is visible... don't adjust the height because that will cause 690 // a configuration change and the keyboard will be lost. 691 lp.height = (int) height; 692 mActivityView.setLayoutParams(lp); 693 mNeedsNewHeight = false; 694 } 695 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 696 Log.d(TAG, "updateHeight: bubble=" + getBubbleKey() 697 + " height=" + height 698 + " mNeedsNewHeight=" + mNeedsNewHeight); 699 } 700 } 701 } 702 getMaxExpandedHeight()703 private int getMaxExpandedHeight() { 704 mWindowManager.getDefaultDisplay().getRealSize(mDisplaySize); 705 int bottomInset = getRootWindowInsets() != null 706 ? getRootWindowInsets().getStableInsetBottom() 707 : 0; 708 709 return mDisplaySize.y 710 - mExpandedViewContainerLocation[1] 711 - getPaddingTop() 712 - getPaddingBottom() 713 - mSettingsIconHeight 714 - mPointerHeight 715 - mPointerMargin - bottomInset; 716 } 717 718 /** 719 * Update appearance of the expanded view being displayed. 720 * 721 * @param containerLocationOnScreen The location on-screen of the container the expanded view is 722 * added to. This allows us to calculate max height without 723 * waiting for layout. 724 */ updateView(int[] containerLocationOnScreen)725 public void updateView(int[] containerLocationOnScreen) { 726 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 727 Log.d(TAG, "updateView: bubble=" 728 + getBubbleKey()); 729 } 730 731 mExpandedViewContainerLocation = containerLocationOnScreen; 732 733 if (usingActivityView() 734 && mActivityView.getVisibility() == VISIBLE 735 && mActivityView.isAttachedToWindow()) { 736 mActivityView.onLocationChanged(); 737 updateHeight(); 738 } 739 } 740 741 /** 742 * Set the x position that the tip of the triangle should point to. 743 */ setPointerPosition(float x)744 public void setPointerPosition(float x) { 745 float halfPointerWidth = mPointerWidth / 2f; 746 float pointerLeft = x - halfPointerWidth - mExpandedViewPadding; 747 mPointerView.setTranslationX(pointerLeft); 748 mPointerView.setVisibility(VISIBLE); 749 } 750 751 /** 752 * Position of the manage button displayed in the expanded view. Used for placing user 753 * education about the manage button. 754 */ getManageButtonBoundsOnScreen(Rect rect)755 public void getManageButtonBoundsOnScreen(Rect rect) { 756 mSettingsIcon.getBoundsOnScreen(rect); 757 } 758 759 /** 760 * Removes and releases an ActivityView if one was previously created for this bubble. 761 */ cleanUpExpandedState()762 public void cleanUpExpandedState() { 763 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 764 Log.d(TAG, "cleanUpExpandedState: mActivityViewStatus=" + mActivityViewStatus 765 + ", bubble=" + getBubbleKey()); 766 } 767 if (mActivityView == null) { 768 return; 769 } 770 mActivityView.release(); 771 if (mTaskId != -1) { 772 try { 773 ActivityTaskManager.getService().removeTask(mTaskId); 774 } catch (RemoteException e) { 775 Log.w(TAG, "Failed to remove taskId " + mTaskId); 776 } 777 mTaskId = -1; 778 } 779 removeView(mActivityView); 780 781 mActivityView = null; 782 } 783 784 /** 785 * Called when the last task is removed from a {@link android.hardware.display.VirtualDisplay} 786 * which {@link ActivityView} uses. 787 */ notifyDisplayEmpty()788 void notifyDisplayEmpty() { 789 if (DEBUG_BUBBLE_EXPANDED_VIEW) { 790 Log.d(TAG, "notifyDisplayEmpty: bubble=" 791 + getBubbleKey() 792 + " mActivityViewStatus=" + mActivityViewStatus); 793 } 794 if (mActivityViewStatus == ActivityViewStatus.ACTIVITY_STARTED) { 795 mActivityViewStatus = ActivityViewStatus.INITIALIZED; 796 } 797 } 798 usingActivityView()799 private boolean usingActivityView() { 800 return (mPendingIntent != null || mBubble.hasMetadataShortcutId()) 801 && mActivityView != null; 802 } 803 804 /** 805 * @return the display id of the virtual display. 806 */ getVirtualDisplayId()807 public int getVirtualDisplayId() { 808 if (usingActivityView()) { 809 return mActivityView.getVirtualDisplayId(); 810 } 811 return INVALID_DISPLAY; 812 } 813 getVirtualDisplay()814 private VirtualDisplay getVirtualDisplay() { 815 if (usingActivityView()) { 816 return mActivityView.getVirtualDisplay(); 817 } 818 return null; 819 } 820 } 821