diff --git a/quickstep/src/com/android/launcher3/QuickstepAccessibilityDelegate.java b/quickstep/src/com/android/launcher3/QuickstepAccessibilityDelegate.java new file mode 100644 index 0000000000..96559cbb8a --- /dev/null +++ b/quickstep/src/com/android/launcher3/QuickstepAccessibilityDelegate.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2021 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; + +import android.view.KeyEvent; +import android.view.View; + +import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; +import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.uioverrides.PredictedAppIcon; +import com.android.launcher3.uioverrides.QuickstepLauncher; + +import java.util.List; + +public class QuickstepAccessibilityDelegate extends LauncherAccessibilityDelegate { + + public QuickstepAccessibilityDelegate(QuickstepLauncher launcher) { + super(launcher); + mActions.put(PIN_PREDICTION, new LauncherAction( + PIN_PREDICTION, R.string.pin_prediction, KeyEvent.KEYCODE_P)); + } + + @Override + protected void getSupportedActions(View host, ItemInfo item, List out) { + if (host instanceof PredictedAppIcon && !((PredictedAppIcon) host).isPinned()) { + out.add(new LauncherAction(PIN_PREDICTION, R.string.pin_prediction, + KeyEvent.KEYCODE_P)); + } + super.getSupportedActions(host, item, out); + } + + @Override + protected boolean performAction(View host, ItemInfo item, int action, boolean fromKeyboard) { + QuickstepLauncher launcher = (QuickstepLauncher) mLauncher; + if (action == PIN_PREDICTION) { + if (launcher.getHotseatPredictionController() == null) { + return false; + } + launcher.getHotseatPredictionController().pinPrediction(item); + return true; + } + return super.performAction(host, item, action, fromKeyboard); + } +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java index 2d501257c3..98551fb17a 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java +++ b/quickstep/src/com/android/launcher3/uioverrides/PredictedAppIcon.java @@ -15,7 +15,6 @@ */ package com.android.launcher3.uioverrides; -import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.PIN_PREDICTION; import static com.android.launcher3.graphics.IconShape.getShape; import android.content.Context; @@ -29,7 +28,6 @@ import android.os.Process; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.ViewGroup; -import android.view.accessibility.AccessibilityNodeInfo; import androidx.core.graphics.ColorUtils; @@ -37,9 +35,7 @@ import com.android.launcher3.CellLayout; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.R; -import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; import com.android.launcher3.graphics.IconPalette; -import com.android.launcher3.hybridhotseat.HotseatPredictionController; import com.android.launcher3.icons.IconNormalizer; import com.android.launcher3.icons.LauncherIcons; import com.android.launcher3.model.data.ItemInfo; @@ -53,8 +49,7 @@ import com.android.launcher3.views.DoubleShadowBubbleTextView; /** * A BubbleTextView with a ring around it's drawable */ -public class PredictedAppIcon extends DoubleShadowBubbleTextView implements - LauncherAccessibilityDelegate.AccessibilityActionHandler { +public class PredictedAppIcon extends DoubleShadowBubbleTextView { private static final int RING_SHADOW_COLOR = 0x99000000; private static final float RING_EFFECT_RATIO = 0.095f; @@ -147,29 +142,6 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView implements verifyHighRes(); } - @Override - public void addSupportedAccessibilityActions(AccessibilityNodeInfo accessibilityNodeInfo) { - if (!mIsPinned) { - accessibilityNodeInfo.addAction( - new AccessibilityNodeInfo.AccessibilityAction(PIN_PREDICTION, - getContext().getText(R.string.pin_prediction))); - } - } - - @Override - public boolean performAccessibilityAction(int action, ItemInfo info) { - QuickstepLauncher launcher = Launcher.cast(Launcher.getLauncher(getContext())); - if (action == PIN_PREDICTION) { - if (launcher == null) { - return false; - } - HotseatPredictionController controller = launcher.getHotseatPredictionController(); - controller.pinPrediction(info); - return true; - } - return false; - } - @Override public void getIconBounds(Rect outBounds) { super.getIconBounds(outBounds); @@ -179,6 +151,10 @@ public class PredictedAppIcon extends DoubleShadowBubbleTextView implements } } + public boolean isPinned() { + return mIsPinned; + } + private int getOutlineOffsetX() { return (getMeasuredWidth() / 2) - mNormalizedIconRadius; } diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java index 2e018f357f..0461e96871 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java @@ -44,7 +44,9 @@ import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.LauncherState; +import com.android.launcher3.QuickstepAccessibilityDelegate; import com.android.launcher3.Workspace; +import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.appprediction.PredictionRowView; import com.android.launcher3.hybridhotseat.HotseatPredictionController; @@ -138,6 +140,11 @@ public class QuickstepLauncher extends BaseQuickstepLauncher { mHotseatPredictionController.logLaunchedAppRankingInfo(info, instanceId); } + @Override + protected LauncherAccessibilityDelegate createAccessibilityDelegate() { + return new QuickstepAccessibilityDelegate(this); + } + /** * Returns Prediction controller for hybrid hotseat */ diff --git a/res/drawable/ic_widget_height_decrease.xml b/res/drawable/ic_widget_height_decrease.xml new file mode 100644 index 0000000000..df704bacbe --- /dev/null +++ b/res/drawable/ic_widget_height_decrease.xml @@ -0,0 +1,25 @@ + + + + diff --git a/res/drawable/ic_widget_height_increase.xml b/res/drawable/ic_widget_height_increase.xml new file mode 100644 index 0000000000..c263a4b43c --- /dev/null +++ b/res/drawable/ic_widget_height_increase.xml @@ -0,0 +1,25 @@ + + + + diff --git a/res/drawable/ic_widget_width_decrease.xml b/res/drawable/ic_widget_width_decrease.xml new file mode 100644 index 0000000000..2a2fad777c --- /dev/null +++ b/res/drawable/ic_widget_width_decrease.xml @@ -0,0 +1,22 @@ + + diff --git a/res/drawable/ic_widget_width_increase.xml b/res/drawable/ic_widget_width_increase.xml new file mode 100644 index 0000000000..89b9f40673 --- /dev/null +++ b/res/drawable/ic_widget_width_increase.xml @@ -0,0 +1,22 @@ + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 447c9ac568..dd1cc7cbf9 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -37,8 +37,6 @@ Shortcut isn\'t available Home - - Custom actions diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java index 6037c96f82..95cdbdd147 100644 --- a/src/com/android/launcher3/AbstractFloatingView.java +++ b/src/com/android/launcher3/AbstractFloatingView.java @@ -59,7 +59,7 @@ public abstract class AbstractFloatingView extends LinearLayout implements Touch TYPE_SNACKBAR, TYPE_LISTENER, TYPE_ALL_APPS_EDU, - + TYPE_DRAG_DROP_POPUP, TYPE_TASK_MENU, TYPE_OPTIONS_POPUP, TYPE_ICON_SURFACE diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java index 452207db3b..d750c6ccc1 100644 --- a/src/com/android/launcher3/CellLayout.java +++ b/src/com/android/launcher3/CellLayout.java @@ -305,6 +305,8 @@ public class CellLayout extends ViewGroup { setImportantForAccessibility(accessibilityFlag); getShortcutsAndWidgets().setImportantForAccessibility(accessibilityFlag); + // ExploreByTouchHelper sets focusability. Clear it when the delegate is cleared. + setFocusable(delegate != null); // Invalidate the accessibility hierarchy if (getParent() != null) { getParent().notifySubtreeAccessibilityStateChanged( diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index 2df9cbed54..30dac64fc9 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -37,6 +37,7 @@ import static com.android.launcher3.LauncherState.NO_SCALE; import static com.android.launcher3.LauncherState.OVERVIEW; import static com.android.launcher3.LauncherState.SPRING_LOADED; import static com.android.launcher3.Utilities.postAsyncCallback; +import static com.android.launcher3.accessibility.LauncherAccessibilityDelegate.getSupportedActions; import static com.android.launcher3.dragndrop.DragLayer.ALPHA_INDEX_LAUNCHER_LOAD; import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_BACKGROUND; import static com.android.launcher3.logging.StatsLogManager.LAUNCHER_STATE_HOME; @@ -104,6 +105,7 @@ import androidx.annotation.VisibleForTesting; import com.android.launcher3.DropTarget.DragObject; import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; +import com.android.launcher3.accessibility.LauncherAccessibilityDelegate.LauncherAction; import com.android.launcher3.allapps.AllAppsContainerView; import com.android.launcher3.allapps.AllAppsStore; import com.android.launcher3.allapps.AllAppsTransitionController; @@ -120,7 +122,6 @@ import com.android.launcher3.folder.FolderGridOrganizer; import com.android.launcher3.folder.FolderIcon; import com.android.launcher3.icons.BitmapRenderer; import com.android.launcher3.icons.IconCache; -import com.android.launcher3.keyboard.CustomActionsPopup; import com.android.launcher3.keyboard.ViewGroupFocusHelper; import com.android.launcher3.logger.LauncherAtom; import com.android.launcher3.logging.FileLog; @@ -163,7 +164,6 @@ import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.util.PendingRequestArgs; import com.android.launcher3.util.SafeCloseable; -import com.android.launcher3.util.ShortcutUtil; import com.android.launcher3.util.SystemUiController; import com.android.launcher3.util.Themes; import com.android.launcher3.util.Thunk; @@ -383,7 +383,7 @@ public class Launcher extends StatefulActivity implements Launche idp.addOnChangeListener(this); mSharedPrefs = Utilities.getPrefs(this); mIconCache = app.getIconCache(); - mAccessibilityDelegate = new LauncherAccessibilityDelegate(this); + mAccessibilityDelegate = createAccessibilityDelegate(); mDragController = new DragController(this); mAllAppsController = new AllAppsTransitionController(this); @@ -2633,19 +2633,9 @@ public class Launcher extends StatefulActivity implements Launche shortcutInfos.add(new KeyboardShortcutInfo(getString(R.string.widget_button_text), KeyEvent.KEYCODE_W, KeyEvent.META_CTRL_ON)); } - final View currentFocus = getCurrentFocus(); - if (currentFocus != null) { - if (new CustomActionsPopup(this, currentFocus).canShow()) { - shortcutInfos.add(new KeyboardShortcutInfo(getString(R.string.custom_actions), - KeyEvent.KEYCODE_O, KeyEvent.META_CTRL_ON)); - } - if (currentFocus.getTag() instanceof ItemInfo - && ShortcutUtil.supportsShortcuts((ItemInfo) currentFocus.getTag())) { + getSupportedActions(this, getCurrentFocus()).forEach(la -> shortcutInfos.add(new KeyboardShortcutInfo( - getString(R.string.shortcuts_menu_with_notifications_description), - KeyEvent.KEYCODE_S, KeyEvent.META_CTRL_ON)); - } - } + la.accessibilityAction.getLabel(), la.keyCode, KeyEvent.META_CTRL_ON))); if (!shortcutInfos.isEmpty()) { data.add(new KeyboardShortcutGroup(getString(R.string.home_screen), shortcutInfos)); } @@ -2663,30 +2653,18 @@ public class Launcher extends StatefulActivity implements Launche return true; } break; - case KeyEvent.KEYCODE_S: { - View focusedView = getCurrentFocus(); - if (focusedView instanceof BubbleTextView - && focusedView.getTag() instanceof ItemInfo - && mAccessibilityDelegate.performAction(focusedView, - (ItemInfo) focusedView.getTag(), - LauncherAccessibilityDelegate.DEEP_SHORTCUTS, - true)) { - PopupContainerWithArrow.getOpen(this).requestFocus(); - return true; - } - break; - } - case KeyEvent.KEYCODE_O: - if (new CustomActionsPopup(this, getCurrentFocus()).show()) { - return true; - } - break; case KeyEvent.KEYCODE_W: if (isInState(NORMAL)) { OptionsPopupView.openWidgets(this); return true; } break; + default: + for (LauncherAction la : getSupportedActions(this, getCurrentFocus())) { + if (la.keyCode == keyCode) { + return la.invokeFromKeyboard(getCurrentFocus()); + } + } } } return super.onKeyShortcut(keyCode, event); @@ -2744,6 +2722,9 @@ public class Launcher extends StatefulActivity implements Launche return Stream.of(APP_INFO, WIDGETS, INSTALL); } + protected LauncherAccessibilityDelegate createAccessibilityDelegate() { + return new LauncherAccessibilityDelegate(this); + } /** * @see LauncherState#getOverviewScaleAndOffset(Launcher) diff --git a/src/com/android/launcher3/SecondaryDropTarget.java b/src/com/android/launcher3/SecondaryDropTarget.java index 92b88e6186..7276887f76 100644 --- a/src/com/android/launcher3/SecondaryDropTarget.java +++ b/src/com/android/launcher3/SecondaryDropTarget.java @@ -37,6 +37,8 @@ import com.android.launcher3.Launcher.OnResumeCallback; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.dragndrop.DragOptions; import com.android.launcher3.logging.FileLog; +import com.android.launcher3.logging.InstanceId; +import com.android.launcher3.logging.InstanceIdSequence; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.logging.StatsLogManager.StatsLogger; import com.android.launcher3.model.data.ItemInfo; @@ -203,9 +205,13 @@ public class SecondaryDropTarget extends ButtonDropTarget implements OnAlarmList d.dragSource = new DeferredOnComplete(d.dragSource, getContext()); super.onDrop(d, options); - StatsLogger logger = mStatsLogManager.logger().withInstanceId(d.logInstanceId); - if (d.originalDragInfo != null) { - logger.withItemInfo(d.originalDragInfo); + doLog(d.logInstanceId, d.originalDragInfo); + } + + private void doLog(InstanceId logInstanceId, ItemInfo itemInfo) { + StatsLogger logger = mStatsLogManager.logger().withInstanceId(logInstanceId); + if (itemInfo != null) { + logger.withItemInfo(itemInfo); } if (mCurrentAccessibilityAction == UNINSTALL) { logger.log(LAUNCHER_ITEM_DROPPED_ON_UNINSTALL); @@ -296,6 +302,7 @@ public class SecondaryDropTarget extends ButtonDropTarget implements OnAlarmList @Override public void onAccessibilityDrop(View view, ItemInfo item) { + doLog(new InstanceIdSequence().newInstanceId(), item); performDropAction(view, item); } diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java index 6fac79aa7e..cd4616a967 100644 --- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java +++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java @@ -3,17 +3,18 @@ package com.android.launcher3.accessibility; import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK; import static com.android.launcher3.LauncherState.NORMAL; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.IGNORE; -import android.app.AlertDialog; import android.appwidget.AppWidgetProviderInfo; -import android.content.DialogInterface; import android.graphics.Point; import android.graphics.Rect; +import android.graphics.RectF; import android.os.Bundle; import android.os.Handler; import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; +import android.view.KeyEvent; import android.view.View; import android.view.View.AccessibilityDelegate; import android.view.accessibility.AccessibilityNodeInfo; @@ -25,7 +26,6 @@ import com.android.launcher3.ButtonDropTarget; import com.android.launcher3.CellLayout; import com.android.launcher3.DropTarget.DragObject; import com.android.launcher3.Launcher; -import com.android.launcher3.LauncherSettings; import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.PendingAddItemInfo; import com.android.launcher3.R; @@ -33,7 +33,6 @@ import com.android.launcher3.Workspace; import com.android.launcher3.dragndrop.DragController.DragListener; import com.android.launcher3.dragndrop.DragOptions; import com.android.launcher3.folder.Folder; -import com.android.launcher3.keyboard.CustomActionsPopup; import com.android.launcher3.keyboard.KeyboardDragAndDropView; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.FolderInfo; @@ -41,14 +40,19 @@ import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.notification.NotificationListener; +import com.android.launcher3.popup.ArrowPopup; import com.android.launcher3.popup.PopupContainerWithArrow; import com.android.launcher3.touch.ItemLongClickListener; import com.android.launcher3.util.IntArray; import com.android.launcher3.util.ShortcutUtil; import com.android.launcher3.util.Thunk; +import com.android.launcher3.views.OptionsPopupView; +import com.android.launcher3.views.OptionsPopupView.OptionItem; import com.android.launcher3.widget.LauncherAppWidgetHostView; import java.util.ArrayList; +import java.util.Collections; +import java.util.List; public class LauncherAccessibilityDelegate extends AccessibilityDelegate implements DragListener { @@ -78,89 +82,105 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme public View item; } - protected final SparseArray mActions = new SparseArray<>(); - @Thunk final Launcher mLauncher; + protected final SparseArray mActions = new SparseArray<>(); + protected final Launcher mLauncher; private DragInfo mDragInfo = null; public LauncherAccessibilityDelegate(Launcher launcher) { mLauncher = launcher; - mActions.put(REMOVE, new AccessibilityAction(REMOVE, - launcher.getText(R.string.remove_drop_target_label))); - mActions.put(UNINSTALL, new AccessibilityAction(UNINSTALL, - launcher.getText(R.string.uninstall_drop_target_label))); - mActions.put(DISMISS_PREDICTION, new AccessibilityAction(DISMISS_PREDICTION, - launcher.getText(R.string.dismiss_prediction_label))); - mActions.put(RECONFIGURE, new AccessibilityAction(RECONFIGURE, - launcher.getText(R.string.gadget_setup_text))); - mActions.put(ADD_TO_WORKSPACE, new AccessibilityAction(ADD_TO_WORKSPACE, - launcher.getText(R.string.action_add_to_workspace))); - mActions.put(MOVE, new AccessibilityAction(MOVE, - launcher.getText(R.string.action_move))); - mActions.put(MOVE_TO_WORKSPACE, new AccessibilityAction(MOVE_TO_WORKSPACE, - launcher.getText(R.string.action_move_to_workspace))); - mActions.put(RESIZE, new AccessibilityAction(RESIZE, - launcher.getText(R.string.action_resize))); - mActions.put(DEEP_SHORTCUTS, new AccessibilityAction(DEEP_SHORTCUTS, - launcher.getText(R.string.action_deep_shortcut))); - mActions.put(SHORTCUTS_AND_NOTIFICATIONS, new AccessibilityAction(DEEP_SHORTCUTS, - launcher.getText(R.string.shortcuts_menu_with_notifications_description))); + mActions.put(REMOVE, new LauncherAction( + REMOVE, R.string.remove_drop_target_label, KeyEvent.KEYCODE_X)); + mActions.put(UNINSTALL, new LauncherAction( + UNINSTALL, R.string.uninstall_drop_target_label, KeyEvent.KEYCODE_U)); + mActions.put(DISMISS_PREDICTION, new LauncherAction(DISMISS_PREDICTION, + R.string.dismiss_prediction_label, KeyEvent.KEYCODE_X)); + mActions.put(RECONFIGURE, new LauncherAction( + RECONFIGURE, R.string.gadget_setup_text, KeyEvent.KEYCODE_E)); + mActions.put(ADD_TO_WORKSPACE, new LauncherAction( + ADD_TO_WORKSPACE, R.string.action_add_to_workspace, KeyEvent.KEYCODE_P)); + mActions.put(MOVE, new LauncherAction( + MOVE, R.string.action_move, KeyEvent.KEYCODE_M)); + mActions.put(MOVE_TO_WORKSPACE, new LauncherAction(MOVE_TO_WORKSPACE, + R.string.action_move_to_workspace, KeyEvent.KEYCODE_P)); + mActions.put(RESIZE, new LauncherAction( + RESIZE, R.string.action_resize, KeyEvent.KEYCODE_R)); + mActions.put(DEEP_SHORTCUTS, new LauncherAction(DEEP_SHORTCUTS, + R.string.action_deep_shortcut, KeyEvent.KEYCODE_S)); + mActions.put(SHORTCUTS_AND_NOTIFICATIONS, new LauncherAction(DEEP_SHORTCUTS, + R.string.shortcuts_menu_with_notifications_description, KeyEvent.KEYCODE_S)); } @Override public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(host, info); - addSupportedActions(host, info, false); + if (host.getTag() instanceof ItemInfo) { + ItemInfo item = (ItemInfo) host.getTag(); + + List actions = new ArrayList<>(); + getSupportedActions(host, item, actions); + actions.forEach(la -> info.addAction(la.accessibilityAction)); + + if (!itemSupportsLongClick(host, item)) { + info.setLongClickable(false); + info.removeAction(AccessibilityAction.ACTION_LONG_CLICK); + } + } } - public void addSupportedActions(View host, AccessibilityNodeInfo info, boolean fromKeyboard) { - if (!(host.getTag() instanceof ItemInfo)) return; - ItemInfo item = (ItemInfo) host.getTag(); - - if (host instanceof AccessibilityActionHandler) { - ((AccessibilityActionHandler) host).addSupportedAccessibilityActions(info); - } - + /** + * Adds all the accessibility actions that can be handled. + */ + protected void getSupportedActions(View host, ItemInfo item, List out) { // If the request came from keyboard, do not add custom shortcuts as that is already // exposed as a direct shortcut - if (!fromKeyboard && ShortcutUtil.supportsShortcuts(item)) { - info.addAction(mActions.get(NotificationListener.getInstanceIfConnected() != null + if (ShortcutUtil.supportsShortcuts(item)) { + out.add(mActions.get(NotificationListener.getInstanceIfConnected() != null ? SHORTCUTS_AND_NOTIFICATIONS : DEEP_SHORTCUTS)); } for (ButtonDropTarget target : mLauncher.getDropTargetBar().getDropTargets()) { if (target.supportsAccessibilityDrop(item, host)) { - info.addAction(mActions.get(target.getAccessibilityAction())); + out.add(mActions.get(target.getAccessibilityAction())); } } // Do not add move actions for keyboard request as this uses virtual nodes. if (itemSupportsAccessibleDrag(item)) { - info.addAction(mActions.get(MOVE)); + out.add(mActions.get(MOVE)); if (item.container >= 0) { - info.addAction(mActions.get(MOVE_TO_WORKSPACE)); + out.add(mActions.get(MOVE_TO_WORKSPACE)); } else if (item instanceof LauncherAppWidgetInfo) { if (!getSupportedResizeActions(host, (LauncherAppWidgetInfo) item).isEmpty()) { - info.addAction(mActions.get(RESIZE)); + out.add(mActions.get(RESIZE)); } } } - if (!fromKeyboard && !itemSupportsLongClick(host, item)) { - info.setLongClickable(false); - info.removeAction(AccessibilityAction.ACTION_LONG_CLICK); - } - if ((item instanceof AppInfo) || (item instanceof PendingAddItemInfo)) { - info.addAction(mActions.get(ADD_TO_WORKSPACE)); + out.add(mActions.get(ADD_TO_WORKSPACE)); } } + /** + * Returns all the accessibility actions that can be handled by the host. + */ + public static List getSupportedActions(Launcher launcher, View host) { + if (host == null || !(host.getTag() instanceof ItemInfo)) { + return Collections.emptyList(); + } + PopupContainerWithArrow container = PopupContainerWithArrow.getOpen(launcher); + LauncherAccessibilityDelegate delegate = container != null + ? container.getAccessibilityDelegate() : launcher.getAccessibilityDelegate(); + List result = new ArrayList<>(); + delegate.getSupportedActions(host, (ItemInfo) host.getTag(), result); + return result; + } + private boolean itemSupportsLongClick(View host, ItemInfo info) { - return PopupContainerWithArrow.canShow(host, info) - || new CustomActionsPopup(mLauncher, host).canShow(); + return PopupContainerWithArrow.canShow(host, info); } private boolean itemSupportsAccessibleDrag(ItemInfo item) { @@ -184,7 +204,7 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme /** * Performs the provided action on the host */ - public boolean performAction(final View host, final ItemInfo item, int action, + protected boolean performAction(final View host, final ItemInfo item, int action, boolean fromKeyboard) { if (action == ACTION_LONG_CLICK) { if (PopupContainerWithArrow.canShow(host, item)) { @@ -193,19 +213,8 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme // standard long press path does. PopupContainerWithArrow.showForIcon((BubbleTextView) host); return true; - } else { - CustomActionsPopup popup = new CustomActionsPopup(mLauncher, host); - if (popup.canShow()) { - popup.show(); - return true; - } } - } - if (host instanceof AccessibilityActionHandler - && ((AccessibilityActionHandler) host).performAccessibilityAction(action, item)) { - return true; - } - if (action == MOVE) { + } else if (action == MOVE) { return beginAccessibleDrag(host, item, fromKeyboard); } else if (action == ADD_TO_WORKSPACE) { final int[] coordinates = new int[2]; @@ -220,9 +229,7 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme Favorites.CONTAINER_DESKTOP, screenId, coordinates[0], coordinates[1]); - ArrayList itemList = new ArrayList<>(); - itemList.add(info); - mLauncher.bindItems(itemList, true); + mLauncher.bindItems(Collections.singletonList(info), true); announceConfirmation(R.string.item_added_to_workspace); } else if (item instanceof PendingAddItemInfo) { PendingAddItemInfo info = (PendingAddItemInfo) item; @@ -243,47 +250,31 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme final int[] coordinates = new int[2]; final int screenId = findSpaceOnWorkspace(item, coordinates); mLauncher.getModelWriter().moveItemInDatabase(info, - LauncherSettings.Favorites.CONTAINER_DESKTOP, + Favorites.CONTAINER_DESKTOP, screenId, coordinates[0], coordinates[1]); // Bind the item in next frame so that if a new workspace page was created, // it will get laid out. - new Handler().post(new Runnable() { - - @Override - public void run() { - ArrayList itemList = new ArrayList<>(); - itemList.add(item); - mLauncher.bindItems(itemList, true); - announceConfirmation(R.string.item_moved); - } + new Handler().post(() -> { + mLauncher.bindItems(Collections.singletonList(item), true); + announceConfirmation(R.string.item_moved); }); + return true; } else if (action == RESIZE) { final LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) item; - final IntArray actions = getSupportedResizeActions(host, info); - CharSequence[] labels = new CharSequence[actions.size()]; - for (int i = 0; i < actions.size(); i++) { - labels[i] = mLauncher.getText(actions.get(i)); - } - - new AlertDialog.Builder(mLauncher) - .setTitle(R.string.action_resize) - .setItems(labels, new DialogInterface.OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - performResizeAction(actions.get(which), host, info); - dialog.dismiss(); - } - }) - .show(); + List actions = getSupportedResizeActions(host, info); + Rect pos = new Rect(); + mLauncher.getDragLayer().getDescendantRectRelativeToSelf(host, pos); + ArrowPopup popup = OptionsPopupView.show(mLauncher, new RectF(pos), actions); + popup.requestFocus(); + popup.setOnCloseCallback(host::requestFocus); return true; - } else if (action == DEEP_SHORTCUTS) { + } else if (action == DEEP_SHORTCUTS || action == SHORTCUTS_AND_NOTIFICATIONS) { return PopupContainerWithArrow.showForIcon((BubbleTextView) host) != null; } else { for (ButtonDropTarget dropTarget : mLauncher.getDropTargetBar().getDropTargets()) { - if (dropTarget.supportsAccessibilityDrop(item, host) && - action == dropTarget.getAccessibilityAction()) { + if (dropTarget.supportsAccessibilityDrop(item, host) + && action == dropTarget.getAccessibilityAction()) { dropTarget.onAccessibilityDrop(host, item); return true; } @@ -292,9 +283,8 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme return false; } - private IntArray getSupportedResizeActions(View host, LauncherAppWidgetInfo info) { - IntArray actions = new IntArray(); - + private List getSupportedResizeActions(View host, LauncherAppWidgetInfo info) { + List actions = new ArrayList<>(); AppWidgetProviderInfo providerInfo = ((LauncherAppWidgetHostView) host).getAppWidgetInfo(); if (providerInfo == null) { return actions; @@ -304,28 +294,40 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme if ((providerInfo.resizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0) { if (layout.isRegionVacant(info.cellX + info.spanX, info.cellY, 1, info.spanY) || layout.isRegionVacant(info.cellX - 1, info.cellY, 1, info.spanY)) { - actions.add(R.string.action_increase_width); + actions.add(new OptionItem( + R.string.action_increase_width, R.drawable.ic_widget_width_increase, + IGNORE, + v -> performResizeAction(R.string.action_increase_width, host, info))); } if (info.spanX > info.minSpanX && info.spanX > 1) { - actions.add(R.string.action_decrease_width); + actions.add(new OptionItem( + R.string.action_decrease_width, R.drawable.ic_widget_width_decrease, + IGNORE, + v -> performResizeAction(R.string.action_decrease_width, host, info))); } } if ((providerInfo.resizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0) { if (layout.isRegionVacant(info.cellX, info.cellY + info.spanY, info.spanX, 1) || layout.isRegionVacant(info.cellX, info.cellY - 1, info.spanX, 1)) { - actions.add(R.string.action_increase_height); + actions.add(new OptionItem( + R.string.action_increase_height, R.drawable.ic_widget_height_increase, + IGNORE, + v -> performResizeAction(R.string.action_increase_height, host, info))); } if (info.spanY > info.minSpanY && info.spanY > 1) { - actions.add(R.string.action_decrease_height); + actions.add(new OptionItem( + R.string.action_decrease_height, R.drawable.ic_widget_height_decrease, + IGNORE, + v -> performResizeAction(R.string.action_decrease_height, host, info))); } } return actions; } - @Thunk void performResizeAction(int action, View host, LauncherAppWidgetInfo info) { + private boolean performResizeAction(int action, View host, LauncherAppWidgetInfo info) { CellLayout.LayoutParams lp = (CellLayout.LayoutParams) host.getLayoutParams(); CellLayout layout = (CellLayout) host.getParent().getParent(); layout.markCellsAsUnoccupiedForView(host); @@ -362,6 +364,7 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme host.requestLayout(); mLauncher.getModelWriter().updateItemInDatabase(info); announceConfirmation(mLauncher.getString(R.string.widget_resized, info.spanX, info.spanY)); + return true; } @Thunk void announceConfirmation(int resId) { @@ -489,19 +492,28 @@ public class LauncherAccessibilityDelegate extends AccessibilityDelegate impleme return screenId; } - /** - * An interface allowing views to handle their own action. - */ - public interface AccessibilityActionHandler { + public class LauncherAction { + public final int keyCode; + public final AccessibilityAction accessibilityAction; + + private final LauncherAccessibilityDelegate mDelegate; + + public LauncherAction(int id, int labelRes, int keyCode) { + this.keyCode = keyCode; + accessibilityAction = new AccessibilityAction(id, mLauncher.getString(labelRes)); + mDelegate = LauncherAccessibilityDelegate.this; + } /** - * performs accessibility action and returns true on success + * Invokes the action for the provided host */ - boolean performAccessibilityAction(int action, ItemInfo itemInfo); - - /** - * adds all the accessibility actions that can be handled. - */ - void addSupportedAccessibilityActions(AccessibilityNodeInfo accessibilityNodeInfo); + public boolean invokeFromKeyboard(View host) { + if (host != null && host.getTag() instanceof ItemInfo) { + return mDelegate.performAction( + host, (ItemInfo) host.getTag(), accessibilityAction.getId(), true); + } else { + return false; + } + } } } diff --git a/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java index aaaff98e18..1733e5dd41 100644 --- a/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java +++ b/src/com/android/launcher3/accessibility/ShortcutMenuAccessibilityDelegate.java @@ -18,9 +18,8 @@ package com.android.launcher3.accessibility; import static com.android.launcher3.LauncherState.NORMAL; +import android.view.KeyEvent; import android.view.View; -import android.view.accessibility.AccessibilityNodeInfo; -import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.Launcher; @@ -31,7 +30,8 @@ import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.notification.NotificationMainView; import com.android.launcher3.shortcuts.DeepShortcutView; -import java.util.ArrayList; +import java.util.Collections; +import java.util.List; /** * Extension of {@link LauncherAccessibilityDelegate} with actions specific to shortcuts in @@ -43,23 +43,23 @@ public class ShortcutMenuAccessibilityDelegate extends LauncherAccessibilityDele public ShortcutMenuAccessibilityDelegate(Launcher launcher) { super(launcher); - mActions.put(DISMISS_NOTIFICATION, new AccessibilityAction(DISMISS_NOTIFICATION, - launcher.getText(R.string.action_dismiss_notification))); + mActions.put(DISMISS_NOTIFICATION, new LauncherAction(DISMISS_NOTIFICATION, + R.string.action_dismiss_notification, KeyEvent.KEYCODE_X)); } @Override - public void addSupportedActions(View host, AccessibilityNodeInfo info, boolean fromKeyboard) { + protected void getSupportedActions(View host, ItemInfo item, List out) { if ((host.getParent() instanceof DeepShortcutView)) { - info.addAction(mActions.get(ADD_TO_WORKSPACE)); + out.add(mActions.get(ADD_TO_WORKSPACE)); } else if (host instanceof NotificationMainView) { if (((NotificationMainView) host).canChildBeDismissed()) { - info.addAction(mActions.get(DISMISS_NOTIFICATION)); + out.add(mActions.get(DISMISS_NOTIFICATION)); } } } @Override - public boolean performAction(View host, ItemInfo item, int action, boolean fromKeyboard) { + protected boolean performAction(View host, ItemInfo item, int action, boolean fromKeyboard) { if (action == ADD_TO_WORKSPACE) { if (!(host.getParent() instanceof DeepShortcutView)) { return false; @@ -73,9 +73,7 @@ public class ShortcutMenuAccessibilityDelegate extends LauncherAccessibilityDele mLauncher.getModelWriter().addItemToDatabase(info, LauncherSettings.Favorites.CONTAINER_DESKTOP, screenId, coordinates[0], coordinates[1]); - ArrayList itemList = new ArrayList<>(); - itemList.add(info); - mLauncher.bindItems(itemList, true); + mLauncher.bindItems(Collections.singletonList(info), true); AbstractFloatingView.closeAllOpenViews(mLauncher); announceConfirmation(R.string.item_added_to_workspace); } diff --git a/src/com/android/launcher3/keyboard/CustomActionsPopup.java b/src/com/android/launcher3/keyboard/CustomActionsPopup.java deleted file mode 100644 index 77ce4a8c88..0000000000 --- a/src/com/android/launcher3/keyboard/CustomActionsPopup.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (C) 2016 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.keyboard; - -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.accessibility.AccessibilityNodeInfo; -import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; -import android.widget.PopupMenu; -import android.widget.PopupMenu.OnMenuItemClickListener; - -import com.android.launcher3.Launcher; -import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; -import com.android.launcher3.model.data.ItemInfo; -import com.android.launcher3.popup.PopupContainerWithArrow; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * Handles showing a popup menu with available custom actions for a launcher icon. - * This allows exposing various custom actions using keyboard shortcuts. - */ -public class CustomActionsPopup implements OnMenuItemClickListener { - - private final Launcher mLauncher; - private final LauncherAccessibilityDelegate mDelegate; - private final View mIcon; - - public CustomActionsPopup(Launcher launcher, View icon) { - mLauncher = launcher; - mIcon = icon; - PopupContainerWithArrow container = PopupContainerWithArrow.getOpen(launcher); - if (container != null) { - mDelegate = container.getAccessibilityDelegate(); - } else { - mDelegate = launcher.getAccessibilityDelegate(); - } - } - - private List getActionList() { - if (mIcon == null || !(mIcon.getTag() instanceof ItemInfo)) { - return Collections.EMPTY_LIST; - } - - AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(); - mDelegate.addSupportedActions(mIcon, info, true); - List result = new ArrayList<>(info.getActionList()); - info.recycle(); - return result; - } - - public boolean canShow() { - return !getActionList().isEmpty(); - } - - public boolean show() { - List actions = getActionList(); - if (actions.isEmpty()) { - return false; - } - - PopupMenu popup = new PopupMenu(mLauncher, mIcon); - popup.setOnMenuItemClickListener(this); - Menu menu = popup.getMenu(); - for (AccessibilityAction action : actions) { - menu.add(Menu.NONE, action.getId(), Menu.NONE, action.getLabel()); - } - popup.show(); - return true; - } - - @Override - public boolean onMenuItemClick(MenuItem menuItem) { - return mDelegate.performAction(mIcon, (ItemInfo) mIcon.getTag(), menuItem.getItemId(), - true); - } -} diff --git a/src/com/android/launcher3/popup/ArrowPopup.java b/src/com/android/launcher3/popup/ArrowPopup.java index 90285c470b..56438d0815 100644 --- a/src/com/android/launcher3/popup/ArrowPopup.java +++ b/src/com/android/launcher3/popup/ArrowPopup.java @@ -40,6 +40,8 @@ import android.view.ViewGroup; import android.view.ViewOutlineProvider; import android.widget.FrameLayout; +import androidx.annotation.NonNull; + import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.BaseDraggingActivity; import com.android.launcher3.InsettableFrameLayout; @@ -82,6 +84,8 @@ public abstract class ArrowPopup extends Abstrac private final Rect mStartRect = new Rect(); private final Rect mEndRect = new Rect(); + private Runnable mOnCloseCallback = () -> { }; + public ArrowPopup(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mInflater = LayoutInflater.from(context); @@ -555,6 +559,14 @@ public abstract class ArrowPopup extends Abstrac mDeferContainerRemoval = false; getPopupContainer().removeView(this); getPopupContainer().removeView(mArrow); + mOnCloseCallback.run(); + } + + /** + * Callback to be called when the popup is closed + */ + public void setOnCloseCallback(@NonNull Runnable callback) { + mOnCloseCallback = callback; } protected BaseDragLayer getPopupContainer() { diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java index 59930ff850..a1ba74703c 100644 --- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java +++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java @@ -206,6 +206,7 @@ public class PopupContainerWithArrow extends Arr .filter(Objects::nonNull) .collect(Collectors.toList())); launcher.refreshAndBindWidgetsForPackageUser(PackageUserKey.fromItemInfo(item)); + container.requestFocus(); return container; } diff --git a/src/com/android/launcher3/views/OptionsPopupView.java b/src/com/android/launcher3/views/OptionsPopupView.java index addaf9cb82..0cb8c1ec77 100644 --- a/src/com/android/launcher3/views/OptionsPopupView.java +++ b/src/com/android/launcher3/views/OptionsPopupView.java @@ -118,7 +118,8 @@ public class OptionsPopupView extends ArrowPopup mTargetRect.roundOut(outPos); } - public static void show(Launcher launcher, RectF targetRect, List items) { + public static OptionsPopupView show( + Launcher launcher, RectF targetRect, List items) { OptionsPopupView popup = (OptionsPopupView) launcher.getLayoutInflater() .inflate(R.layout.longpress_options_menu, launcher.getDragLayer(), false); popup.mTargetRect = targetRect; @@ -134,6 +135,7 @@ public class OptionsPopupView extends ArrowPopup popup.mItemMap.put(view, item); } popup.show(); + return popup; } @VisibleForTesting