| /* |
| * Copyright (C) 2020 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.wm.shell.pip.phone; |
| |
| import static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_NONE; |
| |
| import android.annotation.NonNull; |
| import android.content.Context; |
| import android.graphics.Rect; |
| import android.graphics.Region; |
| import android.os.Bundle; |
| import android.os.RemoteException; |
| import android.view.MagnificationSpec; |
| import android.view.accessibility.AccessibilityManager; |
| import android.view.accessibility.AccessibilityNodeInfo; |
| import android.view.accessibility.AccessibilityWindowInfo; |
| import android.view.accessibility.IAccessibilityInteractionConnection; |
| import android.view.accessibility.IAccessibilityInteractionConnectionCallback; |
| |
| import androidx.annotation.BinderThread; |
| |
| import com.android.wm.shell.R; |
| import com.android.wm.shell.common.ShellExecutor; |
| import com.android.wm.shell.pip.PipBoundsState; |
| import com.android.wm.shell.pip.PipSnapAlgorithm; |
| import com.android.wm.shell.pip.PipTaskOrganizer; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * Expose the touch actions to accessibility as if this object were a window with a single view. |
| * That pseudo-view exposes all of the actions this object can perform. |
| */ |
| public class PipAccessibilityInteractionConnection { |
| |
| public interface AccessibilityCallbacks { |
| void onAccessibilityShowMenu(); |
| } |
| |
| private static final long ACCESSIBILITY_NODE_ID = 1; |
| private List<AccessibilityNodeInfo> mAccessibilityNodeInfoList; |
| |
| private final Context mContext; |
| private final ShellExecutor mMainExcutor; |
| private final @NonNull PipBoundsState mPipBoundsState; |
| private final PipMotionHelper mMotionHelper; |
| private final PipTaskOrganizer mTaskOrganizer; |
| private final PipSnapAlgorithm mSnapAlgorithm; |
| private final Runnable mUpdateMovementBoundCallback; |
| private final Runnable mUnstashCallback; |
| private final AccessibilityCallbacks mCallbacks; |
| private final IAccessibilityInteractionConnection mConnectionImpl; |
| |
| private final Rect mNormalBounds = new Rect(); |
| private final Rect mExpandedBounds = new Rect(); |
| private final Rect mNormalMovementBounds = new Rect(); |
| private final Rect mExpandedMovementBounds = new Rect(); |
| private Rect mTmpBounds = new Rect(); |
| |
| public PipAccessibilityInteractionConnection(Context context, |
| @NonNull PipBoundsState pipBoundsState, PipMotionHelper motionHelper, |
| PipTaskOrganizer taskOrganizer, PipSnapAlgorithm snapAlgorithm, |
| AccessibilityCallbacks callbacks, Runnable updateMovementBoundCallback, |
| Runnable unstashCallback, ShellExecutor mainExcutor) { |
| mContext = context; |
| mMainExcutor = mainExcutor; |
| mPipBoundsState = pipBoundsState; |
| mMotionHelper = motionHelper; |
| mTaskOrganizer = taskOrganizer; |
| mSnapAlgorithm = snapAlgorithm; |
| mUpdateMovementBoundCallback = updateMovementBoundCallback; |
| mUnstashCallback = unstashCallback; |
| mCallbacks = callbacks; |
| mConnectionImpl = new PipAccessibilityInteractionConnectionImpl(); |
| } |
| |
| public void register(AccessibilityManager am) { |
| am.setPictureInPictureActionReplacingConnection(mConnectionImpl); |
| } |
| |
| private void findAccessibilityNodeInfoByAccessibilityId(long accessibilityNodeId, |
| Region interactiveRegion, int interactionId, |
| IAccessibilityInteractionConnectionCallback callback, int flags, |
| int interrogatingPid, long interrogatingTid, MagnificationSpec spec, Bundle args) { |
| try { |
| callback.setFindAccessibilityNodeInfosResult( |
| (accessibilityNodeId == AccessibilityNodeInfo.ROOT_NODE_ID) |
| ? getNodeList() : null, interactionId); |
| } catch (RemoteException re) { |
| /* best effort - ignore */ |
| } |
| } |
| |
| private void performAccessibilityAction(long accessibilityNodeId, int action, |
| Bundle arguments, int interactionId, |
| IAccessibilityInteractionConnectionCallback callback, int flags, |
| int interrogatingPid, long interrogatingTid) { |
| // We only support one view. A request for anything else is invalid |
| boolean result = false; |
| if (accessibilityNodeId == AccessibilityNodeInfo.ROOT_NODE_ID) { |
| |
| // R constants are not final so this cannot be put in the switch-case. |
| if (action == R.id.action_pip_resize) { |
| if (mPipBoundsState.getBounds().width() == mNormalBounds.width() |
| && mPipBoundsState.getBounds().height() == mNormalBounds.height()) { |
| setToExpandedBounds(); |
| } else { |
| setToNormalBounds(); |
| } |
| result = true; |
| } else if (action == R.id.action_pip_stash) { |
| mMotionHelper.animateToStashedClosestEdge(); |
| result = true; |
| } else if (action == R.id.action_pip_unstash) { |
| mUnstashCallback.run(); |
| mPipBoundsState.setStashed(STASH_TYPE_NONE); |
| result = true; |
| } else { |
| switch (action) { |
| case AccessibilityNodeInfo.ACTION_CLICK: |
| mCallbacks.onAccessibilityShowMenu(); |
| result = true; |
| break; |
| case AccessibilityNodeInfo.ACTION_DISMISS: |
| mMotionHelper.dismissPip(); |
| result = true; |
| break; |
| case com.android.internal.R.id.accessibilityActionMoveWindow: |
| int newX = arguments.getInt( |
| AccessibilityNodeInfo.ACTION_ARGUMENT_MOVE_WINDOW_X); |
| int newY = arguments.getInt( |
| AccessibilityNodeInfo.ACTION_ARGUMENT_MOVE_WINDOW_Y); |
| Rect pipBounds = new Rect(); |
| pipBounds.set(mPipBoundsState.getBounds()); |
| mTmpBounds.offsetTo(newX, newY); |
| mMotionHelper.movePip(mTmpBounds); |
| result = true; |
| break; |
| case AccessibilityNodeInfo.ACTION_EXPAND: |
| mMotionHelper.expandLeavePip(); |
| result = true; |
| break; |
| default: |
| // Leave result as false |
| } |
| } |
| } |
| try { |
| callback.setPerformAccessibilityActionResult(result, interactionId); |
| } catch (RemoteException re) { |
| /* best effort - ignore */ |
| } |
| } |
| |
| private void setToExpandedBounds() { |
| float savedSnapFraction = mSnapAlgorithm.getSnapFraction( |
| mPipBoundsState.getBounds(), mNormalMovementBounds); |
| mSnapAlgorithm.applySnapFraction(mExpandedBounds, mExpandedMovementBounds, |
| savedSnapFraction); |
| mTaskOrganizer.scheduleFinishResizePip(mExpandedBounds, (Rect bounds) -> { |
| mMotionHelper.synchronizePinnedStackBounds(); |
| mUpdateMovementBoundCallback.run(); |
| }); |
| } |
| |
| private void setToNormalBounds() { |
| float savedSnapFraction = mSnapAlgorithm.getSnapFraction( |
| mPipBoundsState.getBounds(), mExpandedMovementBounds); |
| mSnapAlgorithm.applySnapFraction(mNormalBounds, mNormalMovementBounds, savedSnapFraction); |
| mTaskOrganizer.scheduleFinishResizePip(mNormalBounds, (Rect bounds) -> { |
| mMotionHelper.synchronizePinnedStackBounds(); |
| mUpdateMovementBoundCallback.run(); |
| }); |
| } |
| |
| private void findAccessibilityNodeInfosByViewId(long accessibilityNodeId, |
| String viewId, Region interactiveRegion, int interactionId, |
| IAccessibilityInteractionConnectionCallback callback, int flags, |
| int interrogatingPid, long interrogatingTid, MagnificationSpec spec) { |
| // We have no view with a proper ID |
| try { |
| callback.setFindAccessibilityNodeInfoResult(null, interactionId); |
| } catch (RemoteException re) { |
| /* best effort - ignore */ |
| } |
| } |
| |
| private void findAccessibilityNodeInfosByText(long accessibilityNodeId, String text, |
| Region interactiveRegion, int interactionId, |
| IAccessibilityInteractionConnectionCallback callback, int flags, |
| int interrogatingPid, long interrogatingTid, MagnificationSpec spec) { |
| // We have no view with text |
| try { |
| callback.setFindAccessibilityNodeInfoResult(null, interactionId); |
| } catch (RemoteException re) { |
| /* best effort - ignore */ |
| } |
| } |
| |
| private void findFocus(long accessibilityNodeId, int focusType, Region interactiveRegion, |
| int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, |
| int interrogatingPid, long interrogatingTid, MagnificationSpec spec) { |
| // We have no view that can take focus |
| try { |
| callback.setFindAccessibilityNodeInfoResult(null, interactionId); |
| } catch (RemoteException re) { |
| /* best effort - ignore */ |
| } |
| } |
| |
| private void focusSearch(long accessibilityNodeId, int direction, Region interactiveRegion, |
| int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, |
| int interrogatingPid, long interrogatingTid, MagnificationSpec spec) { |
| // We have no view that can take focus |
| try { |
| callback.setFindAccessibilityNodeInfoResult(null, interactionId); |
| } catch (RemoteException re) { |
| /* best effort - ignore */ |
| } |
| } |
| |
| /** |
| * Update the normal and expanded bounds so they can be used for Resize. |
| */ |
| void onMovementBoundsChanged(Rect normalBounds, Rect expandedBounds, Rect normalMovementBounds, |
| Rect expandedMovementBounds) { |
| mNormalBounds.set(normalBounds); |
| mExpandedBounds.set(expandedBounds); |
| mNormalMovementBounds.set(normalMovementBounds); |
| mExpandedMovementBounds.set(expandedMovementBounds); |
| } |
| |
| /** |
| * Update the Root node with PIP Accessibility action items. |
| */ |
| public static AccessibilityNodeInfo obtainRootAccessibilityNodeInfo(Context context) { |
| AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(); |
| info.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID, |
| AccessibilityWindowInfo.PICTURE_IN_PICTURE_ACTION_REPLACER_WINDOW_ID); |
| info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK); |
| info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_DISMISS); |
| info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_MOVE_WINDOW); |
| info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND); |
| info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_pip_resize, |
| context.getString(R.string.accessibility_action_pip_resize))); |
| info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_pip_stash, |
| context.getString(R.string.accessibility_action_pip_stash))); |
| info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_pip_unstash, |
| context.getString(R.string.accessibility_action_pip_unstash))); |
| info.setImportantForAccessibility(true); |
| info.setClickable(true); |
| info.setVisibleToUser(true); |
| return info; |
| } |
| |
| private List<AccessibilityNodeInfo> getNodeList() { |
| if (mAccessibilityNodeInfoList == null) { |
| mAccessibilityNodeInfoList = new ArrayList<>(1); |
| } |
| AccessibilityNodeInfo info = obtainRootAccessibilityNodeInfo(mContext); |
| mAccessibilityNodeInfoList.clear(); |
| mAccessibilityNodeInfoList.add(info); |
| return mAccessibilityNodeInfoList; |
| } |
| |
| @BinderThread |
| private class PipAccessibilityInteractionConnectionImpl |
| extends IAccessibilityInteractionConnection.Stub { |
| @Override |
| public void findAccessibilityNodeInfoByAccessibilityId(long accessibilityNodeId, |
| Region bounds, int interactionId, |
| IAccessibilityInteractionConnectionCallback callback, int flags, |
| int interrogatingPid, long interrogatingTid, MagnificationSpec spec, |
| Bundle arguments) throws RemoteException { |
| mMainExcutor.execute(() -> { |
| PipAccessibilityInteractionConnection.this |
| .findAccessibilityNodeInfoByAccessibilityId(accessibilityNodeId, bounds, |
| interactionId, callback, flags, interrogatingPid, interrogatingTid, |
| spec, arguments); |
| }); |
| } |
| |
| @Override |
| public void findAccessibilityNodeInfosByViewId(long accessibilityNodeId, String viewId, |
| Region bounds, int interactionId, |
| IAccessibilityInteractionConnectionCallback callback, int flags, |
| int interrogatingPid, long interrogatingTid, MagnificationSpec spec) |
| throws RemoteException { |
| mMainExcutor.execute(() -> { |
| PipAccessibilityInteractionConnection.this.findAccessibilityNodeInfosByViewId( |
| accessibilityNodeId, viewId, bounds, interactionId, callback, flags, |
| interrogatingPid, interrogatingTid, spec); |
| }); |
| } |
| |
| @Override |
| public void findAccessibilityNodeInfosByText(long accessibilityNodeId, String text, |
| Region bounds, int interactionId, |
| IAccessibilityInteractionConnectionCallback callback, int flags, |
| int interrogatingPid, long interrogatingTid, MagnificationSpec spec) |
| throws RemoteException { |
| mMainExcutor.execute(() -> { |
| PipAccessibilityInteractionConnection.this.findAccessibilityNodeInfosByText( |
| accessibilityNodeId, text, bounds, interactionId, callback, flags, |
| interrogatingPid, interrogatingTid, spec); |
| }); |
| } |
| |
| @Override |
| public void findFocus(long accessibilityNodeId, int focusType, Region bounds, |
| int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, |
| int interrogatingPid, long interrogatingTid, MagnificationSpec spec) |
| throws RemoteException { |
| mMainExcutor.execute(() -> { |
| PipAccessibilityInteractionConnection.this.findFocus(accessibilityNodeId, focusType, |
| bounds, interactionId, callback, flags, interrogatingPid, interrogatingTid, |
| spec); |
| }); |
| } |
| |
| @Override |
| public void focusSearch(long accessibilityNodeId, int direction, Region bounds, |
| int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, |
| int interrogatingPid, long interrogatingTid, MagnificationSpec spec) |
| throws RemoteException { |
| mMainExcutor.execute(() -> { |
| PipAccessibilityInteractionConnection.this.focusSearch(accessibilityNodeId, |
| direction, |
| bounds, interactionId, callback, flags, interrogatingPid, interrogatingTid, |
| spec); |
| }); |
| } |
| |
| @Override |
| public void performAccessibilityAction(long accessibilityNodeId, int action, |
| Bundle arguments, int interactionId, |
| IAccessibilityInteractionConnectionCallback callback, int flags, |
| int interrogatingPid, long interrogatingTid) throws RemoteException { |
| mMainExcutor.execute(() -> { |
| PipAccessibilityInteractionConnection.this.performAccessibilityAction( |
| accessibilityNodeId, action, arguments, interactionId, callback, flags, |
| interrogatingPid, interrogatingTid); |
| }); |
| } |
| |
| @Override |
| public void clearAccessibilityFocus() throws RemoteException { |
| // Do nothing |
| } |
| |
| @Override |
| public void notifyOutsideTouch() throws RemoteException { |
| // Do nothing |
| } |
| } |
| } |