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 }