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