Merge "Removing menu and dialog for custom actions hanlding. These do not work well with gesture-nav and can potentially block the Launcher UI." into sc-dev

This commit is contained in:
Sunny Goyal 2021-02-17 21:40:26 +00:00 committed by Android (Google) Code Review
commit 3c5e748b36
18 changed files with 342 additions and 289 deletions

View File

@ -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<LauncherAction> 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);
}
}

View File

@ -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;
}

View File

@ -43,7 +43,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;
@ -134,6 +136,11 @@ public class QuickstepLauncher extends BaseQuickstepLauncher {
mHotseatPredictionController.logLaunchedAppRankingInfo(info, instanceId);
}
@Override
protected LauncherAccessibilityDelegate createAccessibilityDelegate() {
return new QuickstepAccessibilityDelegate(this);
}
/**
* Returns Prediction controller for hybrid hotseat
*/

View File

@ -0,0 +1,25 @@
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?android:attr/textColorPrimary">
<path
android:fillColor="#FFFFFF"
android:pathData="M8,19h3v3h2v-3h3l-4,-4 -4,4zM16,4h-3L13,1h-2v3L8,4l4,4 4,-4zM4,9v2h16L20,9L4,9zM4,12h16v2H4z"/>
</vector>

View File

@ -0,0 +1,25 @@
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?android:attr/textColorPrimary">
<path
android:fillColor="#FFFFFF"
android:pathData="M4,20h16v2L4,22zM4,2h16v2L4,4zM13,9h3l-4,-4 -4,4h3v6L8,15l4,4 4,-4h-3z"/>
</vector>

View File

@ -0,0 +1,22 @@
<!--
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.
-->
<rotate
xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ic_widget_height_decrease"
android:pivotX="50%"
android:pivotY="50%"
android:fromDegrees="90"
android:toDegrees="90" />

View File

@ -0,0 +1,22 @@
<!--
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.
-->
<rotate
xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ic_widget_height_increase"
android:pivotX="50%"
android:pivotY="50%"
android:fromDegrees="90"
android:toDegrees="90" />

View File

@ -37,8 +37,6 @@
<string name="shortcut_not_available">Shortcut isn\'t available</string>
<!-- User visible name for the launcher/home screen. [CHAR_LIMIT=30] -->
<string name="home_screen">Home</string>
<!-- Label for showing custom action list of a shortcut or widget. [CHAR_LIMIT=30] -->
<string name="custom_actions">Custom actions</string>
<!-- Widgets -->
<!-- Message to tell the user to press and hold on a widget to add it [CHAR_LIMIT=50] -->

View File

@ -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

View File

@ -309,6 +309,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(

View File

@ -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;
@ -107,6 +108,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;
@ -123,7 +125,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;
@ -168,7 +169,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;
@ -395,7 +395,7 @@ public class Launcher extends StatefulActivity<LauncherState> 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);
@ -2694,19 +2694,9 @@ public class Launcher extends StatefulActivity<LauncherState> 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));
}
@ -2724,30 +2714,18 @@ public class Launcher extends StatefulActivity<LauncherState> 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);
@ -2805,6 +2783,9 @@ public class Launcher extends StatefulActivity<LauncherState> implements Launche
return Stream.of(APP_INFO, WIDGETS, INSTALL);
}
protected LauncherAccessibilityDelegate createAccessibilityDelegate() {
return new LauncherAccessibilityDelegate(this);
}
/**
* @see LauncherState#getOverviewScaleAndOffset(Launcher)

View File

@ -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);
}

View File

@ -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<AccessibilityAction> mActions = new SparseArray<>();
@Thunk final Launcher mLauncher;
protected final SparseArray<LauncherAction> 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<LauncherAction> 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<LauncherAction> 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<LauncherAction> 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<LauncherAction> 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<ItemInfo> 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<ItemInfo> 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<OptionItem> 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<OptionItem> getSupportedResizeActions(View host, LauncherAppWidgetInfo info) {
List<OptionItem> 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;
}
}
}
}

View File

@ -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<LauncherAction> 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<ItemInfo> 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);
}

View File

@ -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<AccessibilityAction> getActionList() {
if (mIcon == null || !(mIcon.getTag() instanceof ItemInfo)) {
return Collections.EMPTY_LIST;
}
AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
mDelegate.addSupportedActions(mIcon, info, true);
List<AccessibilityAction> result = new ArrayList<>(info.getActionList());
info.recycle();
return result;
}
public boolean canShow() {
return !getActionList().isEmpty();
}
public boolean show() {
List<AccessibilityAction> 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);
}
}

View File

@ -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<T extends BaseDraggingActivity> 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<T extends BaseDraggingActivity> 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() {

View File

@ -206,6 +206,7 @@ public class PopupContainerWithArrow<T extends BaseDraggingActivity> extends Arr
.filter(Objects::nonNull)
.collect(Collectors.toList()));
launcher.refreshAndBindWidgetsForPackageUser(PackageUserKey.fromItemInfo(item));
container.requestFocus();
return container;
}

View File

@ -118,7 +118,8 @@ public class OptionsPopupView extends ArrowPopup
mTargetRect.roundOut(outPos);
}
public static void show(Launcher launcher, RectF targetRect, List<OptionItem> items) {
public static OptionsPopupView show(
Launcher launcher, RectF targetRect, List<OptionItem> 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