/* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.launcher3.taskbar.bubbles; import static android.os.Process.THREAD_PRIORITY_BACKGROUND; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_VISIBLE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED; import android.annotation.BinderThread; import android.annotation.Nullable; import android.content.Context; import android.graphics.Point; import android.os.Bundle; import android.os.SystemProperties; import android.util.ArrayMap; import android.util.Log; import androidx.annotation.NonNull; import com.android.launcher3.taskbar.TaskbarSharedState; import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController; import com.android.launcher3.util.Executors.SimpleThreadFactory; import com.android.quickstep.SystemUiProxy; import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags; import com.android.wm.shell.Flags; import com.android.wm.shell.bubbles.IBubblesListener; import com.android.wm.shell.shared.bubbles.BubbleBarLocation; import com.android.wm.shell.shared.bubbles.BubbleBarUpdate; import com.android.wm.shell.shared.bubbles.BubbleInfo; import com.android.wm.shell.shared.bubbles.RemovedBubble; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.concurrent.Executor; import java.util.concurrent.Executors; /** * This registers a listener with SysUIProxy to get information about changes to the bubble * stack state from WMShell (SysUI). The controller is also responsible for loading the necessary * information to render each of the bubbles & dispatches changes to * {@link BubbleBarViewController} which will then update {@link BubbleBarView} as needed. * *

For details around the behavior of the bubble bar, see {@link BubbleBarView}. */ public class BubbleBarController extends IBubblesListener.Stub { private static final String TAG = "BubbleBarController"; private static final boolean DEBUG = false; /** * Determines whether bubbles can be shown in the bubble bar. This value updates when the * taskbar is recreated. * * @see #onTaskbarRecreated() */ private static boolean sBubbleBarEnabled = Flags.enableBubbleBar() || SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false); /** Whether showing bubbles in the launcher bubble bar is enabled. */ public static boolean isBubbleBarEnabled() { return sBubbleBarEnabled; } /** Re-reads the value of the flag from SystemProperties when taskbar is recreated. */ public static void onTaskbarRecreated() { sBubbleBarEnabled = Flags.enableBubbleBar() || SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false); } private static final long MASK_HIDE_BUBBLE_BAR = SYSUI_STATE_BOUNCER_SHOWING | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED | SYSUI_STATE_IME_VISIBLE | SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED | SYSUI_STATE_QUICK_SETTINGS_EXPANDED; private static final long MASK_HIDE_HANDLE_VIEW = SYSUI_STATE_BOUNCER_SHOWING | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED; private static final long MASK_SYSUI_LOCKED = SYSUI_STATE_BOUNCER_SHOWING | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED; private final Context mContext; private final BubbleBarView mBarView; private final ArrayMap mBubbles = new ArrayMap<>(); private final ArrayMap mSuppressedBubbles = new ArrayMap<>(); private static final Executor BUBBLE_STATE_EXECUTOR = Executors.newSingleThreadExecutor( new SimpleThreadFactory("BubbleStateUpdates-", THREAD_PRIORITY_BACKGROUND)); private final SystemUiProxy mSystemUiProxy; private BubbleBarItem mSelectedBubble; private TaskbarSharedState mSharedState; private BubbleBarViewController mBubbleBarViewController; private BubbleStashController mBubbleStashController; private Optional mBubbleStashedHandleViewController; private BubblePinController mBubblePinController; private BubbleCreator mBubbleCreator; private BubbleBarLocationListener mBubbleBarLocationListener; // Cache last sent top coordinate to avoid sending duplicate updates to shell private int mLastSentBubbleBarTop; private boolean mIsImeVisible = false; /** * Similar to {@link BubbleBarUpdate} but rather than {@link BubbleInfo}s it uses * {@link BubbleBarBubble}s so that it can be used to update the views. */ private static class BubbleBarViewUpdate { final boolean initialState; boolean expandedChanged; boolean expanded; boolean shouldShowEducation; String selectedBubbleKey; String suppressedBubbleKey; String unsuppressedBubbleKey; BubbleBarLocation bubbleBarLocation; List removedBubbles; List bubbleKeysInOrder; Point expandedViewDropTargetSize; boolean showOverflow; boolean showOverflowChanged; // These need to be loaded in the background BubbleBarBubble addedBubble; BubbleBarBubble updatedBubble; List currentBubbles; BubbleBarViewUpdate(BubbleBarUpdate update) { initialState = update.initialState; expandedChanged = update.expandedChanged; expanded = update.expanded; shouldShowEducation = update.shouldShowEducation; selectedBubbleKey = update.selectedBubbleKey; suppressedBubbleKey = update.suppressedBubbleKey; unsuppressedBubbleKey = update.unsupressedBubbleKey; bubbleBarLocation = update.bubbleBarLocation; removedBubbles = update.removedBubbles; bubbleKeysInOrder = update.bubbleKeysInOrder; expandedViewDropTargetSize = update.expandedViewDropTargetSize; showOverflow = update.showOverflow; showOverflowChanged = update.showOverflowChanged; } } public BubbleBarController(Context context, BubbleBarView bubbleView) { mContext = context; mBarView = bubbleView; // Need the view for inflating bubble views. mSystemUiProxy = SystemUiProxy.INSTANCE.get(context); } public void onDestroy() { mSystemUiProxy.setBubblesListener(null); // Saves bubble bar state BubbleInfo[] bubbleInfoItems = new BubbleInfo[mBubbles.size()]; mBubbles.values().forEach(bubbleBarBubble -> { int index = mBubbleBarViewController.bubbleViewIndex(bubbleBarBubble.getView()); if (index < 0 || index >= bubbleInfoItems.length) { Log.e(TAG, "Found improper index: " + index + " for " + bubbleBarBubble); } else { bubbleInfoItems[index] = bubbleBarBubble.getInfo(); } }); mSharedState.bubbleInfoItems = Arrays.asList(bubbleInfoItems); mSharedState.suppressedBubbleInfoItems = new ArrayList<>(mSuppressedBubbles.size()); for (int i = 0; i < mSuppressedBubbles.size(); i++) { mSharedState.suppressedBubbleInfoItems.add(mSuppressedBubbles.valueAt(i).getInfo()); } } /** Initializes controllers. */ public void init(BubbleControllers bubbleControllers, BubbleBarLocationListener bubbleBarLocationListener, TaskbarSharedState sharedState) { mSharedState = sharedState; mBubbleBarViewController = bubbleControllers.bubbleBarViewController; mBubbleStashController = bubbleControllers.bubbleStashController; mBubbleStashedHandleViewController = bubbleControllers.bubbleStashedHandleViewController; mBubblePinController = bubbleControllers.bubblePinController; mBubbleCreator = bubbleControllers.bubbleCreator; mBubbleBarLocationListener = bubbleBarLocationListener; bubbleControllers.runAfterInit(() -> { restoreSavedState(sharedState); mBubbleBarViewController.setHiddenForBubbles( !sBubbleBarEnabled || mBubbles.isEmpty()); mBubbleStashedHandleViewController.ifPresent( controller -> controller.setHiddenForBubbles( !sBubbleBarEnabled || mBubbles.isEmpty())); mBubbleBarViewController.setUpdateSelectedBubbleAfterCollapse( key -> setSelectedBubbleInternal(mBubbles.get(key))); mBubbleBarViewController.setBoundsChangeListener(this::onBubbleBarBoundsChanged); mBubbleBarLocationListener.onBubbleBarLocationUpdated( mBubbleBarViewController.getBubbleBarLocation()); if (sBubbleBarEnabled) { mSystemUiProxy.setBubblesListener(this); } }); } /** * Updates the bubble bar, handle bar, and stash controllers based on sysui state flags. */ public void updateStateForSysuiFlags(@SystemUiStateFlags long flags) { boolean hideBubbleBar = (flags & MASK_HIDE_BUBBLE_BAR) != 0; mBubbleBarViewController.setHiddenForSysui(hideBubbleBar); boolean hideHandleView = (flags & MASK_HIDE_HANDLE_VIEW) != 0; mBubbleStashedHandleViewController.ifPresent( controller -> controller.setHiddenForSysui(hideHandleView)); boolean sysuiLocked = (flags & MASK_SYSUI_LOCKED) != 0; mBubbleStashController.setSysuiLocked(sysuiLocked); mIsImeVisible = (flags & SYSUI_STATE_IME_VISIBLE) != 0; if (mIsImeVisible) { mBubbleBarViewController.onImeVisible(); } } // // Bubble data changes // @BinderThread @Override public void onBubbleStateChange(Bundle bundle) { bundle.setClassLoader(BubbleBarUpdate.class.getClassLoader()); BubbleBarUpdate update = bundle.getParcelable("update", BubbleBarUpdate.class); BubbleBarViewUpdate viewUpdate = new BubbleBarViewUpdate(update); if (update.addedBubble != null || update.updatedBubble != null || !update.currentBubbleList.isEmpty()) { // We have bubbles to load BUBBLE_STATE_EXECUTOR.execute(() -> { if (update.addedBubble != null) { viewUpdate.addedBubble = mBubbleCreator.populateBubble(mContext, update.addedBubble, mBarView, null /* existingBubble */); } if (update.updatedBubble != null) { BubbleBarBubble existingBubble = mBubbles.get(update.updatedBubble.getKey()); viewUpdate.updatedBubble = mBubbleCreator.populateBubble(mContext, update.updatedBubble, mBarView, existingBubble); } if (update.currentBubbleList != null && !update.currentBubbleList.isEmpty()) { List currentBubbles = new ArrayList<>(); for (int i = 0; i < update.currentBubbleList.size(); i++) { BubbleBarBubble b = mBubbleCreator.populateBubble(mContext, update.currentBubbleList.get(i), mBarView, null /* existingBubble */); currentBubbles.add(b); } viewUpdate.currentBubbles = currentBubbles; } MAIN_EXECUTOR.execute(() -> applyViewChanges(viewUpdate)); }); } else { // No bubbles to load, immediately apply the changes. BUBBLE_STATE_EXECUTOR.execute( () -> MAIN_EXECUTOR.execute(() -> applyViewChanges(viewUpdate))); } } private void restoreSavedState(TaskbarSharedState sharedState) { if (sharedState.bubbleBarLocation != null) { updateBubbleBarLocationInternal(sharedState.bubbleBarLocation); } restoreSavedBubbles(sharedState.bubbleInfoItems); restoreSuppressed(sharedState.suppressedBubbleInfoItems); } private void restoreSavedBubbles(List bubbleInfos) { if (bubbleInfos == null || bubbleInfos.isEmpty()) return; // Iterate in reverse because new bubbles are added in front and the list is in order. for (int i = bubbleInfos.size() - 1; i >= 0; i--) { BubbleBarBubble bubble = mBubbleCreator.populateBubble(mContext, bubbleInfos.get(i), mBarView, /* existingBubble = */ null); if (bubble == null) { Log.e(TAG, "Could not instantiate BubbleBarBubble for " + bubbleInfos.get(i)); continue; } addBubbleInternally(bubble, /* isExpanding= */ false, /* suppressAnimation= */ true); } } private void restoreSuppressed(List bubbleInfos) { if (bubbleInfos == null || bubbleInfos.isEmpty()) return; for (BubbleInfo bubbleInfo : bubbleInfos.reversed()) { BubbleBarBubble bb = mBubbleCreator.populateBubble(mContext, bubbleInfo, mBarView, /* existingBubble= */ null); if (bb != null) { mSuppressedBubbles.put(bb.getKey(), bb); } } } private void applyViewChanges(BubbleBarViewUpdate update) { final boolean isCollapsed = (update.expandedChanged && !update.expanded) || (!update.expandedChanged && !mBubbleBarViewController.isExpanded()); final boolean isExpanding = update.expandedChanged && update.expanded; // don't animate bubbles if this is the initial state because we may be unfolding or // enabling gesture nav. also suppress animation if the bubble bar is hidden for sysui e.g. // the shade is open, or we're locked. final boolean suppressAnimation = update.initialState || mBubbleBarViewController.isHiddenForSysui() || mIsImeVisible; if (update.initialState && mSharedState.hasSavedBubbles()) { // clear restored state mBubbleBarViewController.removeAllBubbles(); mBubbles.clear(); mBubbleBarViewController.showOverflow(update.showOverflow); } if (update.addedBubble != null) { mBubbles.put(update.addedBubble.getKey(), update.addedBubble); } BubbleBarBubble bubbleToSelect = null; if (update.selectedBubbleKey != null) { if (mSelectedBubble == null || !update.selectedBubbleKey.equals(mSelectedBubble.getKey())) { BubbleBarBubble newlySelected = mBubbles.get(update.selectedBubbleKey); if (newlySelected != null) { bubbleToSelect = newlySelected; } else { Log.w(TAG, "trying to select bubble that doesn't exist:" + update.selectedBubbleKey); } } } if (Flags.enableOptionalBubbleOverflow() && update.showOverflowChanged && !update.showOverflow && update.addedBubble != null && update.removedBubbles.isEmpty() && !mBubbles.isEmpty()) { // A bubble was added from the overflow (& now it's empty / not showing) mBubbleBarViewController.removeOverflowAndAddBubble(update.addedBubble, bubbleToSelect); } else if (update.addedBubble != null && update.removedBubbles.size() == 1) { // we're adding and removing a bubble at the same time. handle this as a single update. RemovedBubble removedBubble = update.removedBubbles.get(0); BubbleBarBubble bubbleToRemove = mBubbles.remove(removedBubble.getKey()); boolean showOverflow = update.showOverflowChanged && update.showOverflow; if (bubbleToRemove != null) { mBubbleBarViewController.addBubbleAndRemoveBubble(update.addedBubble, bubbleToRemove, bubbleToSelect, isExpanding, suppressAnimation, showOverflow); } else { mBubbleBarViewController.addBubble(update.addedBubble, isExpanding, suppressAnimation, bubbleToSelect); Log.w(TAG, "trying to remove bubble that doesn't exist: " + removedBubble.getKey()); } } else { boolean overflowNeedsToBeAdded = Flags.enableOptionalBubbleOverflow() && update.showOverflowChanged && update.showOverflow; if (!update.removedBubbles.isEmpty()) { for (int i = 0; i < update.removedBubbles.size(); i++) { RemovedBubble removedBubble = update.removedBubbles.get(i); BubbleBarBubble bubble = mBubbles.remove(removedBubble.getKey()); if (bubble != null && overflowNeedsToBeAdded) { // First removal, show the overflow overflowNeedsToBeAdded = false; mBubbleBarViewController.addOverflowAndRemoveBubble(bubble, bubbleToSelect); } else if (bubble != null) { mBubbleBarViewController.removeBubble(bubble); } else { Log.w(TAG, "trying to remove bubble that doesn't exist: " + removedBubble.getKey()); } } } if (update.addedBubble != null) { mBubbleBarViewController.addBubble(update.addedBubble, isExpanding, suppressAnimation, bubbleToSelect); } if (Flags.enableOptionalBubbleOverflow() && update.showOverflowChanged && update.showOverflow != mBubbleBarViewController.isOverflowAdded()) { mBubbleBarViewController.showOverflow(update.showOverflow); } } // if a bubble was updated upstream, but removed before the update was received, add it back if (update.updatedBubble != null && !mBubbles.containsKey(update.updatedBubble.getKey())) { addBubbleInternally(update.updatedBubble, isExpanding, suppressAnimation); } if (update.addedBubble != null && isCollapsed && bubbleToSelect == null) { // If we're collapsed, the most recently added bubble will be selected. bubbleToSelect = update.addedBubble; } if (update.currentBubbles != null && !update.currentBubbles.isEmpty()) { // Iterate in reverse because new bubbles are added in front and the list is in order. for (int i = update.currentBubbles.size() - 1; i >= 0; i--) { BubbleBarBubble bubble = update.currentBubbles.get(i); if (bubble != null) { addBubbleInternally(bubble, isExpanding, suppressAnimation); if (isCollapsed && bubbleToSelect == null) { // If we're collapsed, the most recently added bubble will be selected. bubbleToSelect = bubble; } } else { Log.w(TAG, "trying to add bubble but null after loading! " + update.addedBubble.getKey()); } } } if (Flags.enableOptionalBubbleOverflow() && update.initialState && update.showOverflow) { mBubbleBarViewController.showOverflow(true); } if (update.suppressedBubbleKey != null) { BubbleBarBubble bb = mBubbles.remove(update.suppressedBubbleKey); if (bb != null) { mSuppressedBubbles.put(update.suppressedBubbleKey, bb); mBubbleBarViewController.removeBubble(bb); } } if (update.unsuppressedBubbleKey != null) { BubbleBarBubble bb = mSuppressedBubbles.remove(update.unsuppressedBubbleKey); if (bb != null) { // Unsuppressing an existing bubble should not cause the bar to expand or animate addBubbleInternally(bb, /* isExpanding= */ false, /* suppressAnimation= */ true); if (mBubbleBarViewController.isHiddenForNoBubbles()) { mBubbleBarViewController.setHiddenForBubbles(false); } } } // Update the visibility if this is the initial state, if there are no bubbles, or if the // animation is suppressed. // If this is the initial bubble, the bubble bar will become visible as part of the // animation. if (update.initialState || mBubbles.isEmpty() || suppressAnimation) { mBubbleBarViewController.setHiddenForBubbles(mBubbles.isEmpty()); } mBubbleStashedHandleViewController.ifPresent( controller -> controller.setHiddenForBubbles(mBubbles.isEmpty())); if (mBubbles.isEmpty()) { // all bubbles were removed. clear the selected bubble mSelectedBubble = null; } if (update.updatedBubble != null) { // Updates mean the dot state may have changed; any other changes were updated in // the populateBubble step. BubbleBarBubble bb = mBubbles.get(update.updatedBubble.getKey()); if (suppressAnimation) { // since we're not animating this update, we should update the dot visibility here. bb.getView().updateDotVisibility(/* animate= */ false); } else { mBubbleBarViewController.animateBubbleNotification( bb, /* isExpanding= */ false, /* isUpdate= */ true); } } if (update.bubbleKeysInOrder != null && !update.bubbleKeysInOrder.isEmpty()) { // Create the new list List newOrder = update.bubbleKeysInOrder.stream() .map(mBubbles::get).filter(Objects::nonNull).toList(); if (!newOrder.isEmpty()) { mBubbleBarViewController.reorderBubbles(newOrder); } } if (bubbleToSelect != null) { setSelectedBubbleInternal(bubbleToSelect); } if (update.shouldShowEducation) { mBubbleBarViewController.prepareToShowEducation(); } if (update.expandedChanged) { if (update.expanded != mBubbleBarViewController.isExpanded()) { mBubbleBarViewController.setExpandedFromSysui(update.expanded); } else { Log.w(TAG, "expansion was changed but is the same"); } } if (update.bubbleBarLocation != null) { mSharedState.bubbleBarLocation = update.bubbleBarLocation; if (update.bubbleBarLocation != mBubbleBarViewController.getBubbleBarLocation()) { updateBubbleBarLocationInternal(update.bubbleBarLocation); } } if (update.expandedViewDropTargetSize != null) { mBubblePinController.setDropTargetSize(update.expandedViewDropTargetSize); } } /** * Removes the given bubble from the backing list of bubbles after it was dismissed by the user. */ public void onBubbleDismissed(BubbleView bubble) { mBubbles.remove(bubble.getBubble().getKey()); } /** Tells WMShell to show the currently selected bubble. */ public void showSelectedBubble() { if (getSelectedBubbleKey() != null) { mLastSentBubbleBarTop = mBarView.getRestingTopPositionOnScreen(); mSystemUiProxy.showBubble(getSelectedBubbleKey(), mLastSentBubbleBarTop); } else { Log.w(TAG, "Trying to show the selected bubble but it's null"); } } /** Updates the currently selected bubble for launcher views and tells WMShell to show it. */ public void showAndSelectBubble(BubbleBarItem b) { if (DEBUG) Log.w(TAG, "showingSelectedBubble: " + b.getKey()); setSelectedBubbleInternal(b); showSelectedBubble(); } /** * Sets the bubble that should be selected. This notifies the views, it does not notify * WMShell that the selection has changed, that should go through either * {@link #showSelectedBubble()} or {@link #showAndSelectBubble(BubbleBarItem)}. */ private void setSelectedBubbleInternal(BubbleBarItem b) { if (!Objects.equals(b, mSelectedBubble)) { if (DEBUG) Log.w(TAG, "selectingBubble: " + b.getKey()); mSelectedBubble = b; mBubbleBarViewController.updateSelectedBubble(mSelectedBubble); } } /** * Returns the selected bubble or null if no bubble is selected. */ @Nullable public String getSelectedBubbleKey() { if (mSelectedBubble != null) { return mSelectedBubble.getKey(); } return null; } /** * Set a new bubble bar location. *

* Updates the value locally in Launcher and in WMShell. */ public void updateBubbleBarLocation(BubbleBarLocation location, @BubbleBarLocation.UpdateSource int source) { updateBubbleBarLocationInternal(location); mSystemUiProxy.setBubbleBarLocation(location, source); } private void updateBubbleBarLocationInternal(BubbleBarLocation location) { mBubbleBarViewController.setBubbleBarLocation(location); mBubbleStashController.setBubbleBarLocation(location); mBubbleBarLocationListener.onBubbleBarLocationUpdated(location); } @Override public void animateBubbleBarLocation(BubbleBarLocation bubbleBarLocation) { MAIN_EXECUTOR.execute( () -> { mBubbleBarViewController.animateBubbleBarLocation(bubbleBarLocation); mBubbleBarLocationListener.onBubbleBarLocationAnimated(bubbleBarLocation); }); } @Override public void onDragItemOverBubbleBarDragZone(@NonNull BubbleBarLocation bubbleBarLocation) { MAIN_EXECUTOR.execute(() -> { mBubbleBarViewController.onDragItemOverBubbleBarDragZone(bubbleBarLocation); if (mBubbleBarViewController.isLocationUpdatedForDropTarget()) { mBubbleBarLocationListener.onBubbleBarLocationAnimated(bubbleBarLocation); } }); } @Override public void onItemDraggedOutsideBubbleBarDropZone() { MAIN_EXECUTOR.execute(() -> { if (mBubbleBarViewController.isLocationUpdatedForDropTarget()) { BubbleBarLocation original = mBubbleBarViewController.getBubbleBarLocation(); mBubbleBarLocationListener.onBubbleBarLocationAnimated(original); } mBubbleBarViewController.onItemDraggedOutsideBubbleBarDropZone(); }); } /** Notifies WMShell to show the expanded view. */ void showExpandedView() { mSystemUiProxy.showExpandedView(); } // // Loading data for the bubbles // private void onBubbleBarBoundsChanged() { int newTop = mBarView.getRestingTopPositionOnScreen(); if (newTop != mLastSentBubbleBarTop) { mLastSentBubbleBarTop = newTop; mSystemUiProxy.updateBubbleBarTopOnScreen(newTop); } } private void addBubbleInternally(BubbleBarBubble bubble, boolean isExpanding, boolean suppressAnimation) { mBubbles.put(bubble.getKey(), bubble); mBubbleBarViewController.addBubble(bubble, isExpanding, suppressAnimation, /* bubbleToSelect = */ null); } /** Listener of {@link BubbleBarLocation} updates. */ public interface BubbleBarLocationListener { /** Called when {@link BubbleBarLocation} is animated, but change is not yet final. */ void onBubbleBarLocationAnimated(BubbleBarLocation location); /** Called when {@link BubbleBarLocation} is updated permanently. */ void onBubbleBarLocationUpdated(BubbleBarLocation location); } }