blob: 19d042147bf5140db8e3e7b687d3ee495aae5e02 [file] [log] [blame]
/*
* 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.accessibility;
import android.content.Context;
import android.graphics.Rect;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.SparseArray;
import android.view.View;
import android.view.accessibility.AccessibilityNodeInfo;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.DropTarget;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.dragndrop.DragController;
import com.android.launcher3.dragndrop.DragOptions;
import com.android.launcher3.model.data.FolderInfo;
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.util.Thunk;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.BubbleTextHolder;
import java.util.ArrayList;
import java.util.List;
public abstract class BaseAccessibilityDelegate<T extends Context & ActivityContext>
extends View.AccessibilityDelegate implements DragController.DragListener {
public enum DragType {
ICON,
FOLDER,
WIDGET
}
public static class DragInfo {
public DragType dragType;
public ItemInfo info;
public View item;
}
protected final SparseArray<LauncherAction> mActions = new SparseArray<>();
protected final T mContext;
protected DragInfo mDragInfo = null;
protected BaseAccessibilityDelegate(T context) {
mContext = context;
}
@Override
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(host, info);
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)) {
info.setLongClickable(false);
info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK);
}
}
}
/**
* Adds all the accessibility actions that can be handled.
*/
protected abstract void getSupportedActions(View host, ItemInfo item, List<LauncherAction> out);
private boolean itemSupportsLongClick(View host) {
if (host instanceof BubbleTextView) {
return ((BubbleTextView) host).canShowLongPressPopup();
} else if (host instanceof BubbleTextHolder) {
BubbleTextHolder holder = (BubbleTextHolder) host;
return holder.getBubbleText() != null && holder.getBubbleText().canShowLongPressPopup();
} else {
return false;
}
}
protected boolean itemSupportsAccessibleDrag(ItemInfo item) {
if (item instanceof WorkspaceItemInfo) {
// Support the action unless the item is in a context menu.
return item.screenId >= 0
&& item.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
}
return (item instanceof LauncherAppWidgetInfo)
|| (item instanceof FolderInfo);
}
@Override
public boolean performAccessibilityAction(View host, int action, Bundle args) {
if ((host.getTag() instanceof ItemInfo)
&& performAction(host, (ItemInfo) host.getTag(), action, false)) {
return true;
}
return super.performAccessibilityAction(host, action, args);
}
protected abstract boolean performAction(
View host, ItemInfo item, int action, boolean fromKeyboard);
@Thunk
protected void announceConfirmation(String confirmation) {
mContext.getDragLayer().announceForAccessibility(confirmation);
}
public boolean isInAccessibleDrag() {
return mDragInfo != null;
}
public DragInfo getDragInfo() {
return mDragInfo;
}
/**
* @param clickedTarget the actual view that was clicked
* @param dropLocation relative to {@param clickedTarget}. If provided, its center is used
* as the actual drop location otherwise the views center is used.
*/
public void handleAccessibleDrop(View clickedTarget, Rect dropLocation,
String confirmation) {
if (!isInAccessibleDrag()) return;
int[] loc = new int[2];
if (dropLocation == null) {
loc[0] = clickedTarget.getWidth() / 2;
loc[1] = clickedTarget.getHeight() / 2;
} else {
loc[0] = dropLocation.centerX();
loc[1] = dropLocation.centerY();
}
mContext.getDragLayer().getDescendantCoordRelativeToSelf(clickedTarget, loc);
mContext.getDragController().completeAccessibleDrag(loc);
if (!TextUtils.isEmpty(confirmation)) {
announceConfirmation(confirmation);
}
}
protected abstract boolean beginAccessibleDrag(View item, ItemInfo info, boolean fromKeyboard);
@Override
public void onDragEnd() {
mContext.getDragController().removeDragListener(this);
mDragInfo = null;
}
@Override
public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
// No-op
}
public class LauncherAction {
public final int keyCode;
public final AccessibilityNodeInfo.AccessibilityAction accessibilityAction;
private final BaseAccessibilityDelegate<T> mDelegate;
public LauncherAction(int id, int labelRes, int keyCode) {
this.keyCode = keyCode;
accessibilityAction = new AccessibilityNodeInfo.AccessibilityAction(
id, mContext.getString(labelRes));
mDelegate = BaseAccessibilityDelegate.this;
}
/**
* Invokes the action for the provided host
*/
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;
}
}
}
}