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