1/*
2 * Copyright (C) 2013 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
17package android.app;
18
19import android.accessibilityservice.AccessibilityServiceInfo;
20import android.accessibilityservice.IAccessibilityServiceClient;
21import android.content.Context;
22import android.content.pm.IPackageManager;
23import android.graphics.Bitmap;
24import android.graphics.Rect;
25import android.hardware.input.InputManager;
26import android.os.Binder;
27import android.os.IBinder;
28import android.os.ParcelFileDescriptor;
29import android.os.Process;
30import android.os.RemoteException;
31import android.os.ServiceManager;
32import android.os.UserHandle;
33import android.view.IWindowManager;
34import android.view.InputEvent;
35import android.view.SurfaceControl;
36import android.view.WindowAnimationFrameStats;
37import android.view.WindowContentFrameStats;
38import android.view.accessibility.AccessibilityEvent;
39import android.view.accessibility.IAccessibilityManager;
40import android.util.Log;
41
42import libcore.io.IoUtils;
43
44import java.io.FileInputStream;
45import java.io.FileOutputStream;
46import java.io.IOException;
47import java.io.InputStream;
48import java.io.OutputStream;
49
50/**
51 * This is a remote object that is passed from the shell to an instrumentation
52 * for enabling access to privileged operations which the shell can do and the
53 * instrumentation cannot. These privileged operations are needed for implementing
54 * a {@link UiAutomation} that enables across application testing by simulating
55 * user actions and performing screen introspection.
56 *
57 * @hide
58 */
59public final class UiAutomationConnection extends IUiAutomationConnection.Stub {
60
61    private static final String TAG = "UiAutomationConnection";
62
63    private static final int INITIAL_FROZEN_ROTATION_UNSPECIFIED = -1;
64
65    private final IWindowManager mWindowManager = IWindowManager.Stub.asInterface(
66            ServiceManager.getService(Service.WINDOW_SERVICE));
67
68    private final IAccessibilityManager mAccessibilityManager = IAccessibilityManager.Stub
69            .asInterface(ServiceManager.getService(Service.ACCESSIBILITY_SERVICE));
70
71    private final IPackageManager mPackageManager = IPackageManager.Stub
72            .asInterface(ServiceManager.getService("package"));
73
74    private final Object mLock = new Object();
75
76    private final Binder mToken = new Binder();
77
78    private int mInitialFrozenRotation = INITIAL_FROZEN_ROTATION_UNSPECIFIED;
79
80    private IAccessibilityServiceClient mClient;
81
82    private boolean mIsShutdown;
83
84    private int mOwningUid;
85
86    @Override
87    public void connect(IAccessibilityServiceClient client, int flags) {
88        if (client == null) {
89            throw new IllegalArgumentException("Client cannot be null!");
90        }
91        synchronized (mLock) {
92            throwIfShutdownLocked();
93            if (isConnectedLocked()) {
94                throw new IllegalStateException("Already connected.");
95            }
96            mOwningUid = Binder.getCallingUid();
97            registerUiTestAutomationServiceLocked(client, flags);
98            storeRotationStateLocked();
99        }
100    }
101
102    @Override
103    public void disconnect() {
104        synchronized (mLock) {
105            throwIfCalledByNotTrustedUidLocked();
106            throwIfShutdownLocked();
107            if (!isConnectedLocked()) {
108                throw new IllegalStateException("Already disconnected.");
109            }
110            mOwningUid = -1;
111            unregisterUiTestAutomationServiceLocked();
112            restoreRotationStateLocked();
113        }
114    }
115
116    @Override
117    public boolean injectInputEvent(InputEvent event, boolean sync) {
118        synchronized (mLock) {
119            throwIfCalledByNotTrustedUidLocked();
120            throwIfShutdownLocked();
121            throwIfNotConnectedLocked();
122        }
123        final int mode = (sync) ? InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH
124                : InputManager.INJECT_INPUT_EVENT_MODE_ASYNC;
125        final long identity = Binder.clearCallingIdentity();
126        try {
127            return InputManager.getInstance().injectInputEvent(event, mode);
128        } finally {
129            Binder.restoreCallingIdentity(identity);
130        }
131    }
132
133    @Override
134    public boolean setRotation(int rotation) {
135        synchronized (mLock) {
136            throwIfCalledByNotTrustedUidLocked();
137            throwIfShutdownLocked();
138            throwIfNotConnectedLocked();
139        }
140        final long identity = Binder.clearCallingIdentity();
141        try {
142            if (rotation == UiAutomation.ROTATION_UNFREEZE) {
143                mWindowManager.thawRotation();
144            } else {
145                mWindowManager.freezeRotation(rotation);
146            }
147            return true;
148        } catch (RemoteException re) {
149            /* ignore */
150        } finally {
151            Binder.restoreCallingIdentity(identity);
152        }
153        return false;
154    }
155
156    @Override
157    public Bitmap takeScreenshot(Rect crop, int rotation) {
158        synchronized (mLock) {
159            throwIfCalledByNotTrustedUidLocked();
160            throwIfShutdownLocked();
161            throwIfNotConnectedLocked();
162        }
163        final long identity = Binder.clearCallingIdentity();
164        try {
165            int width = crop.width();
166            int height = crop.height();
167            return SurfaceControl.screenshot(crop, width, height, rotation);
168        } finally {
169            Binder.restoreCallingIdentity(identity);
170        }
171    }
172
173    @Override
174    public boolean clearWindowContentFrameStats(int windowId) throws RemoteException {
175        synchronized (mLock) {
176            throwIfCalledByNotTrustedUidLocked();
177            throwIfShutdownLocked();
178            throwIfNotConnectedLocked();
179        }
180        int callingUserId = UserHandle.getCallingUserId();
181        final long identity = Binder.clearCallingIdentity();
182        try {
183            IBinder token = mAccessibilityManager.getWindowToken(windowId, callingUserId);
184            if (token == null) {
185                return false;
186            }
187            return mWindowManager.clearWindowContentFrameStats(token);
188        } finally {
189            Binder.restoreCallingIdentity(identity);
190        }
191    }
192
193    @Override
194    public WindowContentFrameStats getWindowContentFrameStats(int windowId) throws RemoteException {
195        synchronized (mLock) {
196            throwIfCalledByNotTrustedUidLocked();
197            throwIfShutdownLocked();
198            throwIfNotConnectedLocked();
199        }
200        int callingUserId = UserHandle.getCallingUserId();
201        final long identity = Binder.clearCallingIdentity();
202        try {
203            IBinder token = mAccessibilityManager.getWindowToken(windowId, callingUserId);
204            if (token == null) {
205                return null;
206            }
207            return mWindowManager.getWindowContentFrameStats(token);
208        } finally {
209            Binder.restoreCallingIdentity(identity);
210        }
211    }
212
213    @Override
214    public void clearWindowAnimationFrameStats() {
215        synchronized (mLock) {
216            throwIfCalledByNotTrustedUidLocked();
217            throwIfShutdownLocked();
218            throwIfNotConnectedLocked();
219        }
220        final long identity = Binder.clearCallingIdentity();
221        try {
222            SurfaceControl.clearAnimationFrameStats();
223        } finally {
224            Binder.restoreCallingIdentity(identity);
225        }
226    }
227
228    @Override
229    public WindowAnimationFrameStats getWindowAnimationFrameStats() {
230        synchronized (mLock) {
231            throwIfCalledByNotTrustedUidLocked();
232            throwIfShutdownLocked();
233            throwIfNotConnectedLocked();
234        }
235        final long identity = Binder.clearCallingIdentity();
236        try {
237            WindowAnimationFrameStats stats = new WindowAnimationFrameStats();
238            SurfaceControl.getAnimationFrameStats(stats);
239            return stats;
240        } finally {
241            Binder.restoreCallingIdentity(identity);
242        }
243    }
244
245    @Override
246    public void grantRuntimePermission(String packageName, String permission, int userId)
247            throws RemoteException {
248        synchronized (mLock) {
249            throwIfCalledByNotTrustedUidLocked();
250            throwIfShutdownLocked();
251            throwIfNotConnectedLocked();
252        }
253        final long identity = Binder.clearCallingIdentity();
254        try {
255            mPackageManager.grantRuntimePermission(packageName, permission, userId);
256        } finally {
257            Binder.restoreCallingIdentity(identity);
258        }
259    }
260
261    @Override
262    public void revokeRuntimePermission(String packageName, String permission, int userId)
263            throws RemoteException {
264        synchronized (mLock) {
265            throwIfCalledByNotTrustedUidLocked();
266            throwIfShutdownLocked();
267            throwIfNotConnectedLocked();
268        }
269        final long identity = Binder.clearCallingIdentity();
270        try {
271            mPackageManager.revokeRuntimePermission(packageName, permission, userId);
272        } finally {
273            Binder.restoreCallingIdentity(identity);
274        }
275    }
276
277    public class Repeater implements Runnable {
278        // Continuously read readFrom and write back to writeTo until EOF is encountered
279        private final InputStream readFrom;
280        private final OutputStream writeTo;
281        public Repeater (InputStream readFrom, OutputStream writeTo) {
282            this.readFrom = readFrom;
283            this.writeTo = writeTo;
284        }
285        @Override
286        public void run() {
287            try {
288                final byte[] buffer = new byte[8192];
289                int readByteCount;
290                while (true) {
291                    readByteCount = readFrom.read(buffer);
292                    if (readByteCount < 0) {
293                        break;
294                    }
295                    writeTo.write(buffer, 0, readByteCount);
296                    writeTo.flush();
297                }
298            } catch (IOException ioe) {
299                throw new RuntimeException("Error while reading/writing ", ioe);
300            } finally {
301                IoUtils.closeQuietly(readFrom);
302                IoUtils.closeQuietly(writeTo);
303            }
304        }
305    }
306
307    @Override
308    public void executeShellCommand(final String command, final ParcelFileDescriptor sink,
309            final ParcelFileDescriptor source) throws RemoteException {
310        synchronized (mLock) {
311            throwIfCalledByNotTrustedUidLocked();
312            throwIfShutdownLocked();
313            throwIfNotConnectedLocked();
314        }
315        final java.lang.Process process;
316
317        try {
318            process = Runtime.getRuntime().exec(command);
319        } catch (IOException exc) {
320            throw new RuntimeException("Error running shell command '" + command + "'", exc);
321        }
322
323        // Read from process and write to pipe
324        final Thread readFromProcess;
325        if (sink != null) {
326            InputStream sink_in = process.getInputStream();;
327            OutputStream sink_out = new FileOutputStream(sink.getFileDescriptor());
328
329            readFromProcess = new Thread(new Repeater(sink_in, sink_out));
330            readFromProcess.start();
331        } else {
332            readFromProcess = null;
333        }
334
335        // Read from pipe and write to process
336        final Thread writeToProcess;
337        if (source != null) {
338            OutputStream source_out = process.getOutputStream();
339            InputStream source_in = new FileInputStream(source.getFileDescriptor());
340
341            writeToProcess = new Thread(new Repeater(source_in, source_out));
342            writeToProcess.start();
343        } else {
344            writeToProcess = null;
345        }
346
347        Thread cleanup = new Thread(new Runnable() {
348            @Override
349            public void run() {
350                try {
351                    if (writeToProcess != null) {
352                        writeToProcess.join();
353                    }
354                    if (readFromProcess != null) {
355                        readFromProcess.join();
356                    }
357                } catch (InterruptedException exc) {
358                    Log.e(TAG, "At least one of the threads was interrupted");
359                }
360                IoUtils.closeQuietly(sink);
361                IoUtils.closeQuietly(source);
362                process.destroy();
363                }
364            });
365        cleanup.start();
366    }
367
368    @Override
369    public void shutdown() {
370        synchronized (mLock) {
371            if (isConnectedLocked()) {
372                throwIfCalledByNotTrustedUidLocked();
373            }
374            throwIfShutdownLocked();
375            mIsShutdown = true;
376            if (isConnectedLocked()) {
377                disconnect();
378            }
379        }
380    }
381
382    private void registerUiTestAutomationServiceLocked(IAccessibilityServiceClient client,
383            int flags) {
384        IAccessibilityManager manager = IAccessibilityManager.Stub.asInterface(
385                ServiceManager.getService(Context.ACCESSIBILITY_SERVICE));
386        final AccessibilityServiceInfo info = new AccessibilityServiceInfo();
387        info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;
388        info.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC;
389        info.flags |= AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
390                | AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS
391                | AccessibilityServiceInfo.FLAG_FORCE_DIRECT_BOOT_AWARE;
392        info.setCapabilities(AccessibilityServiceInfo.CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT
393                | AccessibilityServiceInfo.CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION
394                | AccessibilityServiceInfo.CAPABILITY_CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY
395                | AccessibilityServiceInfo.CAPABILITY_CAN_REQUEST_FILTER_KEY_EVENTS);
396        try {
397            // Calling out with a lock held is fine since if the system
398            // process is gone the client calling in will be killed.
399            manager.registerUiTestAutomationService(mToken, client, info, flags);
400            mClient = client;
401        } catch (RemoteException re) {
402            throw new IllegalStateException("Error while registering UiTestAutomationService.", re);
403        }
404    }
405
406    private void unregisterUiTestAutomationServiceLocked() {
407        IAccessibilityManager manager = IAccessibilityManager.Stub.asInterface(
408              ServiceManager.getService(Context.ACCESSIBILITY_SERVICE));
409        try {
410            // Calling out with a lock held is fine since if the system
411            // process is gone the client calling in will be killed.
412            manager.unregisterUiTestAutomationService(mClient);
413            mClient = null;
414        } catch (RemoteException re) {
415            throw new IllegalStateException("Error while unregistering UiTestAutomationService",
416                    re);
417        }
418    }
419
420    private void storeRotationStateLocked() {
421        try {
422            if (mWindowManager.isRotationFrozen()) {
423                // Calling out with a lock held is fine since if the system
424                // process is gone the client calling in will be killed.
425                mInitialFrozenRotation = mWindowManager.getDefaultDisplayRotation();
426            }
427        } catch (RemoteException re) {
428            /* ignore */
429        }
430    }
431
432    private void restoreRotationStateLocked() {
433        try {
434            if (mInitialFrozenRotation != INITIAL_FROZEN_ROTATION_UNSPECIFIED) {
435                // Calling out with a lock held is fine since if the system
436                // process is gone the client calling in will be killed.
437                mWindowManager.freezeRotation(mInitialFrozenRotation);
438            } else {
439                // Calling out with a lock held is fine since if the system
440                // process is gone the client calling in will be killed.
441                mWindowManager.thawRotation();
442            }
443        } catch (RemoteException re) {
444            /* ignore */
445        }
446    }
447
448    private boolean isConnectedLocked() {
449        return mClient != null;
450    }
451
452    private void throwIfShutdownLocked() {
453        if (mIsShutdown) {
454            throw new IllegalStateException("Connection shutdown!");
455        }
456    }
457
458    private void throwIfNotConnectedLocked() {
459        if (!isConnectedLocked()) {
460            throw new IllegalStateException("Not connected!");
461        }
462    }
463
464    private void throwIfCalledByNotTrustedUidLocked() {
465        final int callingUid = Binder.getCallingUid();
466        if (callingUid != mOwningUid && mOwningUid != Process.SYSTEM_UID
467                && callingUid != 0 /*root*/) {
468            throw new SecurityException("Calling from not trusted UID!");
469        }
470    }
471}
472