The client implementation for remote selection bar

The DefaultSelectionToolbarRenderService renders the menu items and
callbacks to the application with the SurfacePackage and the relative
postition, the application will set the SurfacePackage to the
SurfaceView and put the PopupWindow on the correct position.

The MenuItemRepr is a copy from LocalFloatingToolbarPopup, no logic
change in this class. The above changes don't contain the solution
for transfering the touch focus on the non-toolbar part to the
application process. The part will be implemented on the follow up
change.

Bug: 190030331
Bug: 205823020
Test: manual. Enable the flag, try to selectin the text then the
toolbar can show successfully. Can open opverflow menu, scroll the
view, the selection toolbar also changes the position.

Merged-In: I3915e8165b4d371700cb5fb9e7eb23d46f39bd5c
Change-Id: I1f2add3e93cee9d7af20cba9ccd4e0b393afffcb
This commit is contained in:
Joanne Chung
2021-12-24 20:34:49 +08:00
parent db78cc2cd8
commit 6e56f6bac9
2 changed files with 497 additions and 21 deletions

View File

@@ -16,6 +16,7 @@
package com.android.internal.widget.floatingtoolbar;
import android.annotation.Nullable;
import android.content.Context;
import android.graphics.Rect;
import android.view.MenuItem;
@@ -84,7 +85,8 @@ public interface FloatingToolbarPopup {
* @see PopupWindow#setFocusable(boolean)
* @see PopupWindow.OnDismissListener
*/
boolean setOutsideTouchable(boolean outsideTouchable, PopupWindow.OnDismissListener onDismiss);
boolean setOutsideTouchable(boolean outsideTouchable,
@Nullable PopupWindow.OnDismissListener onDismiss);
/**
* Returns {@link RemoteFloatingToolbarPopup} implementation if the system selection toolbar

View File

@@ -16,13 +16,47 @@
package com.android.internal.widget.floatingtoolbar;
import static android.view.selectiontoolbar.SelectionToolbarManager.NO_TOOLBAR_ID;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UiThread;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.text.TextUtils;
import android.util.Log;
import android.view.Gravity;
import android.view.MenuItem;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.selectiontoolbar.ISelectionToolbarCallback;
import android.view.selectiontoolbar.SelectionToolbarManager;
import android.view.selectiontoolbar.ShowInfo;
import android.view.selectiontoolbar.ToolbarMenuItem;
import android.view.selectiontoolbar.WidgetInfo;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import com.android.internal.R;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
@@ -34,55 +68,495 @@ import java.util.Objects;
*/
public final class RemoteFloatingToolbarPopup implements FloatingToolbarPopup {
private final SelectionToolbarManager mSelectionToolbarManager;
// Parent for the popup window.
private final View mParent;
private static final boolean DEBUG =
Log.isLoggable(FloatingToolbar.FLOATING_TOOLBAR_TAG, Log.VERBOSE);
public RemoteFloatingToolbarPopup(Context context, View parent) {
// TODO: implement it
mParent = Objects.requireNonNull(parent);
mSelectionToolbarManager = context.getSystemService(SelectionToolbarManager.class);
private static final int TOOLBAR_STATE_SHOWN = 1;
private static final int TOOLBAR_STATE_HIDDEN = 2;
private static final int TOOLBAR_STATE_DISMISSED = 3;
@IntDef(prefix = {"TOOLBAR_STATE_"}, value = {
TOOLBAR_STATE_SHOWN,
TOOLBAR_STATE_HIDDEN,
TOOLBAR_STATE_DISMISSED
})
@Retention(RetentionPolicy.SOURCE)
public @interface ToolbarState {
}
@NonNull
private final SelectionToolbarManager mSelectionToolbarManager;
// Parent for the popup window.
@NonNull
private final View mParent;
// A popup window used for showing menu items rendered by the remote system process
@NonNull
private final PopupWindow mPopupWindow;
// The callback to handle remote rendered selection toolbar.
@NonNull
private final SelectionToolbarCallbackImpl mSelectionToolbarCallback;
// tracks this popup state.
private @ToolbarState int mState;
// The token of the current showing floating toolbar.
private long mFloatingToolbarToken;
private final Rect mPreviousContentRect = new Rect();
private List<MenuItem> mMenuItems;
private MenuItem.OnMenuItemClickListener mMenuItemClickListener;
private int mSuggestedWidth;
private final Rect mScreenViewPort = new Rect();
private boolean mWidthChanged = true;
private final int[] mCoordsOnScreen = new int[2];
private final int[] mCoordsOnWindow = new int[2];
public RemoteFloatingToolbarPopup(Context context, View parent) {
mParent = Objects.requireNonNull(parent);
mPopupWindow = createPopupWindow(context);
mSelectionToolbarManager = context.getSystemService(SelectionToolbarManager.class);
mSelectionToolbarCallback = new SelectionToolbarCallbackImpl(this);
mFloatingToolbarToken = NO_TOOLBAR_ID;
}
@UiThread
@Override
public void show(List<MenuItem> menuItems,
MenuItem.OnMenuItemClickListener menuItemClickListener, Rect contentRect) {
// TODO: implement it
Objects.requireNonNull(menuItems);
Objects.requireNonNull(menuItemClickListener);
if (isShowing() && Objects.equals(menuItems, mMenuItems)
&& Objects.equals(contentRect, mPreviousContentRect)) {
if (DEBUG) {
Log.v(FloatingToolbar.FLOATING_TOOLBAR_TAG,
"Ignore duplicate show() for the same content.");
}
return;
}
boolean isLayoutRequired = mMenuItems == null
|| !MenuItemRepr.reprEquals(menuItems, mMenuItems)
|| mWidthChanged;
if (isLayoutRequired) {
mSelectionToolbarManager.dismissToolbar(mFloatingToolbarToken);
doDismissPopupWindow();
}
mMenuItemClickListener = menuItemClickListener;
mMenuItems = menuItems;
mParent.getWindowVisibleDisplayFrame(mScreenViewPort);
final int suggestWidth = mSuggestedWidth > 0
? mSuggestedWidth
: mParent.getResources().getDimensionPixelSize(
R.dimen.floating_toolbar_preferred_width);
final ShowInfo showInfo = new ShowInfo(
mFloatingToolbarToken, isLayoutRequired,
getToolbarMenuItems(mMenuItems),
contentRect,
suggestWidth,
mScreenViewPort,
mParent.getViewRootImpl().getInputToken());
if (DEBUG) {
Log.v(FloatingToolbar.FLOATING_TOOLBAR_TAG,
"RemoteFloatingToolbarPopup.show() for " + showInfo);
}
mSelectionToolbarManager.showToolbar(showInfo, mSelectionToolbarCallback);
mPreviousContentRect.set(contentRect);
}
@UiThread
@Override
public void dismiss() {
if (mState == TOOLBAR_STATE_DISMISSED) {
Log.w(FloatingToolbar.FLOATING_TOOLBAR_TAG,
"The floating toolbar already dismissed.");
return;
}
if (DEBUG) {
Log.v(FloatingToolbar.FLOATING_TOOLBAR_TAG,
"RemoteFloatingToolbarPopup.dismiss().");
}
mSelectionToolbarManager.dismissToolbar(mFloatingToolbarToken);
doDismissPopupWindow();
}
@UiThread
@Override
public void hide() {
// TODO: implement it
if (mState == TOOLBAR_STATE_DISMISSED || mState == TOOLBAR_STATE_HIDDEN) {
if (DEBUG) {
Log.v(FloatingToolbar.FLOATING_TOOLBAR_TAG,
"The floating toolbar already dismissed or hidden.");
}
return;
}
if (DEBUG) {
Log.v(FloatingToolbar.FLOATING_TOOLBAR_TAG,
"RemoteFloatingToolbarPopup.hide().");
}
mSelectionToolbarManager.hideToolbar(mFloatingToolbarToken);
mState = TOOLBAR_STATE_HIDDEN;
mPopupWindow.dismiss();
}
@UiThread
@Override
public void setSuggestedWidth(int suggestedWidth) {
// TODO: implement it
int difference = Math.abs(suggestedWidth - mSuggestedWidth);
mWidthChanged = difference > (mSuggestedWidth * 0.2);
mSuggestedWidth = suggestedWidth;
}
@Override
public void setWidthChanged(boolean widthChanged) {
// no-op
}
@Override
public void dismiss() {
// TODO: implement it
mWidthChanged = widthChanged;
}
@UiThread
@Override
public boolean isHidden() {
return false;
return mState == TOOLBAR_STATE_HIDDEN;
}
@UiThread
@Override
public boolean isShowing() {
return false;
return mState == TOOLBAR_STATE_SHOWN;
}
@UiThread
@Override
public boolean setOutsideTouchable(boolean outsideTouchable,
PopupWindow.OnDismissListener onDismiss) {
return false;
@Nullable PopupWindow.OnDismissListener onDismiss) {
if (mState == TOOLBAR_STATE_DISMISSED) {
return false;
}
boolean ret = false;
if (mPopupWindow.isOutsideTouchable() ^ outsideTouchable) {
mPopupWindow.setOutsideTouchable(outsideTouchable);
mPopupWindow.setFocusable(!outsideTouchable);
mPopupWindow.update();
ret = true;
}
mPopupWindow.setOnDismissListener(onDismiss);
return ret;
}
private void updatePopupWindowContent(WidgetInfo widgetInfo) {
if (DEBUG) {
Log.v(FloatingToolbar.FLOATING_TOOLBAR_TAG, "updatePopupWindowContent.");
}
ViewGroup contentContainer = (ViewGroup) mPopupWindow.getContentView();
contentContainer.removeAllViews();
SurfaceView surfaceView = new SurfaceView(mParent.getContext());
surfaceView.setZOrderOnTop(true);
surfaceView.getHolder().setFormat(PixelFormat.TRANSPARENT);
surfaceView.setChildSurfacePackage(widgetInfo.getSurfacePackage());
contentContainer.addView(surfaceView);
}
private MenuItem getMenuItemByToolbarMenuItem(ToolbarMenuItem toolbarMenuItem) {
for (MenuItem item : mMenuItems) {
if (toolbarMenuItem.getItemId() == item.getItemId()) {
return item;
}
}
return null;
}
private Point getCoordinatesInWindow(int x, int y) {
// We later specify the location of PopupWindow relative to the attached window.
// The idea here is that 1) we can get the location of a View in both window coordinates
// and screen coordinates, where the offset between them should be equal to the window
// origin, and 2) we can use an arbitrary for this calculation while calculating the
// location of the rootview is supposed to be least expensive.
// TODO: Consider to use PopupWindow.setIsLaidOutInScreen(true) so that we can avoid
// the following calculation.
mParent.getRootView().getLocationOnScreen(mCoordsOnScreen);
mParent.getRootView().getLocationInWindow(mCoordsOnWindow);
int windowLeftOnScreen = mCoordsOnScreen[0] - mCoordsOnWindow[0];
int windowTopOnScreen = mCoordsOnScreen[1] - mCoordsOnWindow[1];
return new Point(Math.max(0, x - windowLeftOnScreen), Math.max(0, y - windowTopOnScreen));
}
private static List<ToolbarMenuItem> getToolbarMenuItems(List<MenuItem> menuItems) {
final List<ToolbarMenuItem> list = new ArrayList<>(menuItems.size());
for (MenuItem menuItem : menuItems) {
// TODO: use ToolbarMenuItem.Builder(MenuItem) instead
ToolbarMenuItem toolbarMenuItem = new ToolbarMenuItem.Builder(menuItem.getItemId(),
menuItem.getTitle(), menuItem.getContentDescription(), menuItem.getGroupId(),
convertDrawableToIcon(menuItem.getIcon()),
menuItem.getTooltipText(),
ToolbarMenuItem.getPriorityFromMenuItem(menuItem)).build();
list.add(toolbarMenuItem);
}
return list;
}
private static Icon convertDrawableToIcon(Drawable drawable) {
if (drawable == null) {
return null;
}
if (drawable instanceof BitmapDrawable) {
final BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
if (bitmapDrawable.getBitmap() != null) {
return Icon.createWithBitmap(bitmapDrawable.getBitmap());
}
}
final Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return Icon.createWithBitmap(bitmap);
}
private static PopupWindow createPopupWindow(Context content) {
ViewGroup popupContentHolder = new LinearLayout(content);
PopupWindow popupWindow = new PopupWindow(popupContentHolder);
popupWindow.setClippingEnabled(false);
popupWindow.setWindowLayoutType(
WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
popupWindow.setAnimationStyle(0);
popupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
return popupWindow;
}
private void doDismissPopupWindow() {
if (DEBUG) {
Log.v(FloatingToolbar.FLOATING_TOOLBAR_TAG, "RemoteFloatingToolbarPopup.doDismiss().");
}
mState = TOOLBAR_STATE_DISMISSED;
mMenuItems = null;
mMenuItemClickListener = null;
mFloatingToolbarToken = 0;
mSuggestedWidth = 0;
mWidthChanged = true;
resetCoords();
mPreviousContentRect.setEmpty();
mScreenViewPort.setEmpty();
mPopupWindow.dismiss();
}
private void resetCoords() {
mCoordsOnScreen[0] = 0;
mCoordsOnScreen[1] = 0;
mCoordsOnWindow[0] = 0;
mCoordsOnWindow[1] = 0;
}
private void runOnUiThread(Runnable runnable) {
mParent.post(runnable);
}
private void onShow(WidgetInfo info) {
runOnUiThread(() -> {
mFloatingToolbarToken = info.getWidgetToken();
mState = TOOLBAR_STATE_SHOWN;
updatePopupWindowContent(info);
Rect contentRect = info.getContentRect();
mPopupWindow.setWidth(contentRect.width());
mPopupWindow.setHeight(contentRect.height());
final Point coords = getCoordinatesInWindow(contentRect.left, contentRect.top);
mPopupWindow.showAtLocation(mParent, Gravity.NO_GRAVITY, coords.x, coords.y);
});
}
private void onWidgetUpdated(WidgetInfo info) {
runOnUiThread(() -> {
if (!isShowing()) {
Log.w(FloatingToolbar.FLOATING_TOOLBAR_TAG,
"onWidgetUpdated(): The widget isn't showing.");
return;
}
updatePopupWindowContent(info);
Rect contentRect = info.getContentRect();
Point coords = getCoordinatesInWindow(contentRect.left, contentRect.top);
if (DEBUG) {
Log.v(FloatingToolbar.FLOATING_TOOLBAR_TAG,
"PopupWindow x= " + coords.x + " y= " + coords.y + " w="
+ contentRect.width() + " h=" + contentRect.height());
}
mPopupWindow.update(coords.x, coords.y, contentRect.width(), contentRect.height());
});
}
private void onToolbarShowTimeout() {
runOnUiThread(() -> {
if (mState == TOOLBAR_STATE_DISMISSED) {
return;
}
doDismissPopupWindow();
});
}
private void onMenuItemClicked(ToolbarMenuItem toolbarMenuItem) {
runOnUiThread(() -> {
if (mMenuItems == null || mMenuItemClickListener == null) {
return;
}
MenuItem item = getMenuItemByToolbarMenuItem(toolbarMenuItem);
if (DEBUG) {
Log.v(FloatingToolbar.FLOATING_TOOLBAR_TAG,
"SelectionToolbarCallbackImpl onMenuItemClicked. toolbarMenuItem="
+ toolbarMenuItem + " item=" + item);
}
// TODO: handle the menu item like clipboard
if (item != null) {
mMenuItemClickListener.onMenuItemClick(item);
} else {
Log.e(FloatingToolbar.FLOATING_TOOLBAR_TAG,
"onMenuItemClicked: cannot find menu item.");
}
});
}
private static class SelectionToolbarCallbackImpl extends ISelectionToolbarCallback.Stub {
private final WeakReference<RemoteFloatingToolbarPopup> mRemotePopup;
SelectionToolbarCallbackImpl(RemoteFloatingToolbarPopup popup) {
mRemotePopup = new WeakReference<>(popup);
}
@Override
public void onShown(WidgetInfo info) {
if (DEBUG) {
Log.v(FloatingToolbar.FLOATING_TOOLBAR_TAG,
"SelectionToolbarCallbackImpl onShown: " + info);
}
final RemoteFloatingToolbarPopup remoteFloatingToolbarPopup = mRemotePopup.get();
if (remoteFloatingToolbarPopup != null) {
remoteFloatingToolbarPopup.onShow(info);
} else {
Log.w(FloatingToolbar.FLOATING_TOOLBAR_TAG,
"Lost remoteFloatingToolbarPopup reference for onShown.");
}
}
@Override
public void onWidgetUpdated(WidgetInfo info) {
if (DEBUG) {
Log.v(FloatingToolbar.FLOATING_TOOLBAR_TAG,
"SelectionToolbarCallbackImpl onWidgetUpdated: info = " + info);
}
final RemoteFloatingToolbarPopup remoteFloatingToolbarPopup = mRemotePopup.get();
if (remoteFloatingToolbarPopup != null) {
remoteFloatingToolbarPopup.onWidgetUpdated(info);
} else {
Log.w(FloatingToolbar.FLOATING_TOOLBAR_TAG,
"Lost remoteFloatingToolbarPopup reference for onWidgetUpdated.");
}
}
@Override
public void onToolbarShowTimeout() {
final RemoteFloatingToolbarPopup remoteFloatingToolbarPopup = mRemotePopup.get();
if (remoteFloatingToolbarPopup != null) {
remoteFloatingToolbarPopup.onToolbarShowTimeout();
} else {
Log.w(FloatingToolbar.FLOATING_TOOLBAR_TAG,
"Lost remoteFloatingToolbarPopup reference for onToolbarShowTimeout.");
}
}
@Override
public void onMenuItemClicked(ToolbarMenuItem toolbarMenuItem) {
final RemoteFloatingToolbarPopup remoteFloatingToolbarPopup = mRemotePopup.get();
if (remoteFloatingToolbarPopup != null) {
remoteFloatingToolbarPopup.onMenuItemClicked(toolbarMenuItem);
} else {
Log.w(FloatingToolbar.FLOATING_TOOLBAR_TAG,
"Lost remoteFloatingToolbarPopup reference for onMenuItemClicked.");
}
}
@Override
public void onError(int errorCode) {
if (DEBUG) {
Log.v(FloatingToolbar.FLOATING_TOOLBAR_TAG,
"SelectionToolbarCallbackImpl onError: " + errorCode);
}
}
}
/**
* Represents the identity of a MenuItem that is rendered in a FloatingToolbarPopup.
*/
static final class MenuItemRepr {
public final int mItemId;
public final int mGroupId;
@Nullable
public final String mTitle;
@Nullable private final Drawable mIcon;
private MenuItemRepr(
int itemId, int groupId, @Nullable CharSequence title,
@Nullable Drawable icon) {
mItemId = itemId;
mGroupId = groupId;
mTitle = (title == null) ? null : title.toString();
mIcon = icon;
}
/**
* Creates an instance of MenuItemRepr for the specified menu item.
*/
public static MenuItemRepr of(MenuItem menuItem) {
return new MenuItemRepr(
menuItem.getItemId(),
menuItem.getGroupId(),
menuItem.getTitle(),
menuItem.getIcon());
}
/**
* Returns this object's hashcode.
*/
@Override
public int hashCode() {
return Objects.hash(mItemId, mGroupId, mTitle, mIcon);
}
/**
* Returns true if this object is the same as the specified object.
*/
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (!(o instanceof LocalFloatingToolbarPopup.MenuItemRepr)) {
return false;
}
final MenuItemRepr other = (MenuItemRepr) o;
return mItemId == other.mItemId
&& mGroupId == other.mGroupId
&& TextUtils.equals(mTitle, other.mTitle)
// Many Drawables (icons) do not implement equals(). Using equals() here instead
// of reference comparisons in case a Drawable subclass implements equals().
&& Objects.equals(mIcon, other.mIcon);
}
/**
* Returns true if the two menu item collections are the same based on MenuItemRepr.
*/
public static boolean reprEquals(
Collection<MenuItem> menuItems1, Collection<MenuItem> menuItems2) {
if (menuItems1.size() != menuItems2.size()) {
return false;
}
final Iterator<MenuItem> menuItems2Iter = menuItems2.iterator();
for (MenuItem menuItem1 : menuItems1) {
final MenuItem menuItem2 = menuItems2Iter.next();
if (!MenuItemRepr.of(menuItem1).equals(
MenuItemRepr.of(menuItem2))) {
return false;
}
}
return true;
}
}
}