1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.wm.shell.pip.phone; 17 18 import static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_NONE; 19 20 import android.annotation.NonNull; 21 import android.content.Context; 22 import android.graphics.Rect; 23 import android.graphics.Region; 24 import android.os.Bundle; 25 import android.os.RemoteException; 26 import android.view.MagnificationSpec; 27 import android.view.accessibility.AccessibilityManager; 28 import android.view.accessibility.AccessibilityNodeInfo; 29 import android.view.accessibility.AccessibilityWindowInfo; 30 import android.view.accessibility.IAccessibilityInteractionConnection; 31 import android.view.accessibility.IAccessibilityInteractionConnectionCallback; 32 33 import androidx.annotation.BinderThread; 34 35 import com.android.wm.shell.R; 36 import com.android.wm.shell.common.ShellExecutor; 37 import com.android.wm.shell.pip.PipBoundsState; 38 import com.android.wm.shell.pip.PipSnapAlgorithm; 39 import com.android.wm.shell.pip.PipTaskOrganizer; 40 41 import java.util.ArrayList; 42 import java.util.List; 43 44 /** 45 * Expose the touch actions to accessibility as if this object were a window with a single view. 46 * That pseudo-view exposes all of the actions this object can perform. 47 */ 48 public class PipAccessibilityInteractionConnection { 49 50 public interface AccessibilityCallbacks { onAccessibilityShowMenu()51 void onAccessibilityShowMenu(); 52 } 53 54 private static final long ACCESSIBILITY_NODE_ID = 1; 55 private List<AccessibilityNodeInfo> mAccessibilityNodeInfoList; 56 57 private final Context mContext; 58 private final ShellExecutor mMainExcutor; 59 private final @NonNull PipBoundsState mPipBoundsState; 60 private final PipMotionHelper mMotionHelper; 61 private final PipTaskOrganizer mTaskOrganizer; 62 private final PipSnapAlgorithm mSnapAlgorithm; 63 private final Runnable mUpdateMovementBoundCallback; 64 private final Runnable mUnstashCallback; 65 private final AccessibilityCallbacks mCallbacks; 66 private final IAccessibilityInteractionConnection mConnectionImpl; 67 68 private final Rect mNormalBounds = new Rect(); 69 private final Rect mExpandedBounds = new Rect(); 70 private final Rect mNormalMovementBounds = new Rect(); 71 private final Rect mExpandedMovementBounds = new Rect(); 72 private Rect mTmpBounds = new Rect(); 73 PipAccessibilityInteractionConnection(Context context, @NonNull PipBoundsState pipBoundsState, PipMotionHelper motionHelper, PipTaskOrganizer taskOrganizer, PipSnapAlgorithm snapAlgorithm, AccessibilityCallbacks callbacks, Runnable updateMovementBoundCallback, Runnable unstashCallback, ShellExecutor mainExcutor)74 public PipAccessibilityInteractionConnection(Context context, 75 @NonNull PipBoundsState pipBoundsState, PipMotionHelper motionHelper, 76 PipTaskOrganizer taskOrganizer, PipSnapAlgorithm snapAlgorithm, 77 AccessibilityCallbacks callbacks, Runnable updateMovementBoundCallback, 78 Runnable unstashCallback, ShellExecutor mainExcutor) { 79 mContext = context; 80 mMainExcutor = mainExcutor; 81 mPipBoundsState = pipBoundsState; 82 mMotionHelper = motionHelper; 83 mTaskOrganizer = taskOrganizer; 84 mSnapAlgorithm = snapAlgorithm; 85 mUpdateMovementBoundCallback = updateMovementBoundCallback; 86 mUnstashCallback = unstashCallback; 87 mCallbacks = callbacks; 88 mConnectionImpl = new PipAccessibilityInteractionConnectionImpl(); 89 } 90 register(AccessibilityManager am)91 public void register(AccessibilityManager am) { 92 am.setPictureInPictureActionReplacingConnection(mConnectionImpl); 93 } 94 findAccessibilityNodeInfoByAccessibilityId(long accessibilityNodeId, Region interactiveRegion, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, long interrogatingTid, MagnificationSpec spec, Bundle args)95 private void findAccessibilityNodeInfoByAccessibilityId(long accessibilityNodeId, 96 Region interactiveRegion, int interactionId, 97 IAccessibilityInteractionConnectionCallback callback, int flags, 98 int interrogatingPid, long interrogatingTid, MagnificationSpec spec, Bundle args) { 99 try { 100 callback.setFindAccessibilityNodeInfosResult( 101 (accessibilityNodeId == AccessibilityNodeInfo.ROOT_NODE_ID) 102 ? getNodeList() : null, interactionId); 103 } catch (RemoteException re) { 104 /* best effort - ignore */ 105 } 106 } 107 performAccessibilityAction(long accessibilityNodeId, int action, Bundle arguments, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, long interrogatingTid)108 private void performAccessibilityAction(long accessibilityNodeId, int action, 109 Bundle arguments, int interactionId, 110 IAccessibilityInteractionConnectionCallback callback, int flags, 111 int interrogatingPid, long interrogatingTid) { 112 // We only support one view. A request for anything else is invalid 113 boolean result = false; 114 if (accessibilityNodeId == AccessibilityNodeInfo.ROOT_NODE_ID) { 115 116 // R constants are not final so this cannot be put in the switch-case. 117 if (action == R.id.action_pip_resize) { 118 if (mPipBoundsState.getBounds().width() == mNormalBounds.width() 119 && mPipBoundsState.getBounds().height() == mNormalBounds.height()) { 120 setToExpandedBounds(); 121 } else { 122 setToNormalBounds(); 123 } 124 result = true; 125 } else if (action == R.id.action_pip_stash) { 126 mMotionHelper.animateToStashedClosestEdge(); 127 result = true; 128 } else if (action == R.id.action_pip_unstash) { 129 mUnstashCallback.run(); 130 mPipBoundsState.setStashed(STASH_TYPE_NONE); 131 result = true; 132 } else { 133 switch (action) { 134 case AccessibilityNodeInfo.ACTION_CLICK: 135 mCallbacks.onAccessibilityShowMenu(); 136 result = true; 137 break; 138 case AccessibilityNodeInfo.ACTION_DISMISS: 139 mMotionHelper.dismissPip(); 140 result = true; 141 break; 142 case com.android.internal.R.id.accessibilityActionMoveWindow: 143 int newX = arguments.getInt( 144 AccessibilityNodeInfo.ACTION_ARGUMENT_MOVE_WINDOW_X); 145 int newY = arguments.getInt( 146 AccessibilityNodeInfo.ACTION_ARGUMENT_MOVE_WINDOW_Y); 147 Rect pipBounds = new Rect(); 148 pipBounds.set(mPipBoundsState.getBounds()); 149 mTmpBounds.offsetTo(newX, newY); 150 mMotionHelper.movePip(mTmpBounds); 151 result = true; 152 break; 153 case AccessibilityNodeInfo.ACTION_EXPAND: 154 mMotionHelper.expandLeavePip(false /* skipAnimation */); 155 result = true; 156 break; 157 default: 158 // Leave result as false 159 } 160 } 161 } 162 try { 163 callback.setPerformAccessibilityActionResult(result, interactionId); 164 } catch (RemoteException re) { 165 /* best effort - ignore */ 166 } 167 } 168 setToExpandedBounds()169 private void setToExpandedBounds() { 170 float savedSnapFraction = mSnapAlgorithm.getSnapFraction( 171 mPipBoundsState.getBounds(), mNormalMovementBounds); 172 mSnapAlgorithm.applySnapFraction(mExpandedBounds, mExpandedMovementBounds, 173 savedSnapFraction); 174 mTaskOrganizer.scheduleFinishResizePip(mExpandedBounds, (Rect bounds) -> { 175 mMotionHelper.synchronizePinnedStackBounds(); 176 mUpdateMovementBoundCallback.run(); 177 }); 178 } 179 setToNormalBounds()180 private void setToNormalBounds() { 181 float savedSnapFraction = mSnapAlgorithm.getSnapFraction( 182 mPipBoundsState.getBounds(), mExpandedMovementBounds); 183 mSnapAlgorithm.applySnapFraction(mNormalBounds, mNormalMovementBounds, savedSnapFraction); 184 mTaskOrganizer.scheduleFinishResizePip(mNormalBounds, (Rect bounds) -> { 185 mMotionHelper.synchronizePinnedStackBounds(); 186 mUpdateMovementBoundCallback.run(); 187 }); 188 } 189 findAccessibilityNodeInfosByViewId(long accessibilityNodeId, String viewId, Region interactiveRegion, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, long interrogatingTid, MagnificationSpec spec)190 private void findAccessibilityNodeInfosByViewId(long accessibilityNodeId, 191 String viewId, Region interactiveRegion, int interactionId, 192 IAccessibilityInteractionConnectionCallback callback, int flags, 193 int interrogatingPid, long interrogatingTid, MagnificationSpec spec) { 194 // We have no view with a proper ID 195 try { 196 callback.setFindAccessibilityNodeInfoResult(null, interactionId); 197 } catch (RemoteException re) { 198 /* best effort - ignore */ 199 } 200 } 201 findAccessibilityNodeInfosByText(long accessibilityNodeId, String text, Region interactiveRegion, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, long interrogatingTid, MagnificationSpec spec)202 private void findAccessibilityNodeInfosByText(long accessibilityNodeId, String text, 203 Region interactiveRegion, int interactionId, 204 IAccessibilityInteractionConnectionCallback callback, int flags, 205 int interrogatingPid, long interrogatingTid, MagnificationSpec spec) { 206 // We have no view with text 207 try { 208 callback.setFindAccessibilityNodeInfoResult(null, interactionId); 209 } catch (RemoteException re) { 210 /* best effort - ignore */ 211 } 212 } 213 findFocus(long accessibilityNodeId, int focusType, Region interactiveRegion, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, long interrogatingTid, MagnificationSpec spec)214 private void findFocus(long accessibilityNodeId, int focusType, Region interactiveRegion, 215 int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, 216 int interrogatingPid, long interrogatingTid, MagnificationSpec spec) { 217 // We have no view that can take focus 218 try { 219 callback.setFindAccessibilityNodeInfoResult(null, interactionId); 220 } catch (RemoteException re) { 221 /* best effort - ignore */ 222 } 223 } 224 focusSearch(long accessibilityNodeId, int direction, Region interactiveRegion, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, long interrogatingTid, MagnificationSpec spec)225 private void focusSearch(long accessibilityNodeId, int direction, Region interactiveRegion, 226 int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, 227 int interrogatingPid, long interrogatingTid, MagnificationSpec spec) { 228 // We have no view that can take focus 229 try { 230 callback.setFindAccessibilityNodeInfoResult(null, interactionId); 231 } catch (RemoteException re) { 232 /* best effort - ignore */ 233 } 234 } 235 236 /** 237 * Update the normal and expanded bounds so they can be used for Resize. 238 */ onMovementBoundsChanged(Rect normalBounds, Rect expandedBounds, Rect normalMovementBounds, Rect expandedMovementBounds)239 void onMovementBoundsChanged(Rect normalBounds, Rect expandedBounds, Rect normalMovementBounds, 240 Rect expandedMovementBounds) { 241 mNormalBounds.set(normalBounds); 242 mExpandedBounds.set(expandedBounds); 243 mNormalMovementBounds.set(normalMovementBounds); 244 mExpandedMovementBounds.set(expandedMovementBounds); 245 } 246 247 /** 248 * Update the Root node with PIP Accessibility action items. 249 */ obtainRootAccessibilityNodeInfo(Context context)250 public static AccessibilityNodeInfo obtainRootAccessibilityNodeInfo(Context context) { 251 AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(); 252 info.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID, 253 AccessibilityWindowInfo.PICTURE_IN_PICTURE_ACTION_REPLACER_WINDOW_ID); 254 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK); 255 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_DISMISS); 256 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_MOVE_WINDOW); 257 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_EXPAND); 258 info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_pip_resize, 259 context.getString(R.string.accessibility_action_pip_resize))); 260 info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_pip_stash, 261 context.getString(R.string.accessibility_action_pip_stash))); 262 info.addAction(new AccessibilityNodeInfo.AccessibilityAction(R.id.action_pip_unstash, 263 context.getString(R.string.accessibility_action_pip_unstash))); 264 info.setImportantForAccessibility(true); 265 info.setClickable(true); 266 info.setVisibleToUser(true); 267 return info; 268 } 269 getNodeList()270 private List<AccessibilityNodeInfo> getNodeList() { 271 if (mAccessibilityNodeInfoList == null) { 272 mAccessibilityNodeInfoList = new ArrayList<>(1); 273 } 274 AccessibilityNodeInfo info = obtainRootAccessibilityNodeInfo(mContext); 275 mAccessibilityNodeInfoList.clear(); 276 mAccessibilityNodeInfoList.add(info); 277 return mAccessibilityNodeInfoList; 278 } 279 280 @BinderThread 281 private class PipAccessibilityInteractionConnectionImpl 282 extends IAccessibilityInteractionConnection.Stub { 283 @Override findAccessibilityNodeInfoByAccessibilityId(long accessibilityNodeId, Region bounds, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, long interrogatingTid, MagnificationSpec spec, Bundle arguments)284 public void findAccessibilityNodeInfoByAccessibilityId(long accessibilityNodeId, 285 Region bounds, int interactionId, 286 IAccessibilityInteractionConnectionCallback callback, int flags, 287 int interrogatingPid, long interrogatingTid, MagnificationSpec spec, 288 Bundle arguments) throws RemoteException { 289 mMainExcutor.execute(() -> { 290 PipAccessibilityInteractionConnection.this 291 .findAccessibilityNodeInfoByAccessibilityId(accessibilityNodeId, bounds, 292 interactionId, callback, flags, interrogatingPid, interrogatingTid, 293 spec, arguments); 294 }); 295 } 296 297 @Override findAccessibilityNodeInfosByViewId(long accessibilityNodeId, String viewId, Region bounds, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, long interrogatingTid, MagnificationSpec spec)298 public void findAccessibilityNodeInfosByViewId(long accessibilityNodeId, String viewId, 299 Region bounds, int interactionId, 300 IAccessibilityInteractionConnectionCallback callback, int flags, 301 int interrogatingPid, long interrogatingTid, MagnificationSpec spec) 302 throws RemoteException { 303 mMainExcutor.execute(() -> { 304 PipAccessibilityInteractionConnection.this.findAccessibilityNodeInfosByViewId( 305 accessibilityNodeId, viewId, bounds, interactionId, callback, flags, 306 interrogatingPid, interrogatingTid, spec); 307 }); 308 } 309 310 @Override findAccessibilityNodeInfosByText(long accessibilityNodeId, String text, Region bounds, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, long interrogatingTid, MagnificationSpec spec)311 public void findAccessibilityNodeInfosByText(long accessibilityNodeId, String text, 312 Region bounds, int interactionId, 313 IAccessibilityInteractionConnectionCallback callback, int flags, 314 int interrogatingPid, long interrogatingTid, MagnificationSpec spec) 315 throws RemoteException { 316 mMainExcutor.execute(() -> { 317 PipAccessibilityInteractionConnection.this.findAccessibilityNodeInfosByText( 318 accessibilityNodeId, text, bounds, interactionId, callback, flags, 319 interrogatingPid, interrogatingTid, spec); 320 }); 321 } 322 323 @Override findFocus(long accessibilityNodeId, int focusType, Region bounds, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, long interrogatingTid, MagnificationSpec spec)324 public void findFocus(long accessibilityNodeId, int focusType, Region bounds, 325 int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, 326 int interrogatingPid, long interrogatingTid, MagnificationSpec spec) 327 throws RemoteException { 328 mMainExcutor.execute(() -> { 329 PipAccessibilityInteractionConnection.this.findFocus(accessibilityNodeId, focusType, 330 bounds, interactionId, callback, flags, interrogatingPid, interrogatingTid, 331 spec); 332 }); 333 } 334 335 @Override focusSearch(long accessibilityNodeId, int direction, Region bounds, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, long interrogatingTid, MagnificationSpec spec)336 public void focusSearch(long accessibilityNodeId, int direction, Region bounds, 337 int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, 338 int interrogatingPid, long interrogatingTid, MagnificationSpec spec) 339 throws RemoteException { 340 mMainExcutor.execute(() -> { 341 PipAccessibilityInteractionConnection.this.focusSearch(accessibilityNodeId, 342 direction, 343 bounds, interactionId, callback, flags, interrogatingPid, interrogatingTid, 344 spec); 345 }); 346 } 347 348 @Override performAccessibilityAction(long accessibilityNodeId, int action, Bundle arguments, int interactionId, IAccessibilityInteractionConnectionCallback callback, int flags, int interrogatingPid, long interrogatingTid)349 public void performAccessibilityAction(long accessibilityNodeId, int action, 350 Bundle arguments, int interactionId, 351 IAccessibilityInteractionConnectionCallback callback, int flags, 352 int interrogatingPid, long interrogatingTid) throws RemoteException { 353 mMainExcutor.execute(() -> { 354 PipAccessibilityInteractionConnection.this.performAccessibilityAction( 355 accessibilityNodeId, action, arguments, interactionId, callback, flags, 356 interrogatingPid, interrogatingTid); 357 }); 358 } 359 360 @Override clearAccessibilityFocus()361 public void clearAccessibilityFocus() throws RemoteException { 362 // Do nothing 363 } 364 365 @Override notifyOutsideTouch()366 public void notifyOutsideTouch() throws RemoteException { 367 // Do nothing 368 } 369 } 370 } 371