blob: ce1d43d10c343e2251347f417bedaf51366ea3d7 [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.app;
import static android.view.Display.DEFAULT_DISPLAY;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.accessibilityservice.IAccessibilityServiceClient;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.graphics.Rect;
import android.hardware.input.InputManager;
import android.hardware.input.InputManagerGlobal;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.permission.IPermissionManager;
import android.util.Log;
import android.view.IWindowManager;
import android.view.InputDevice;
import android.view.InputEvent;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.SurfaceControl;
import android.view.WindowAnimationFrameStats;
import android.view.WindowContentFrameStats;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.IAccessibilityManager;
import android.window.ScreenCapture;
import android.window.ScreenCapture.CaptureArgs;
import libcore.io.IoUtils;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
/**
* This is a remote object that is passed from the shell to an instrumentation
* for enabling access to privileged operations which the shell can do and the
* instrumentation cannot. These privileged operations are needed for implementing
* a {@link UiAutomation} that enables across application testing by simulating
* user actions and performing screen introspection.
*
* @hide
*/
public final class UiAutomationConnection extends IUiAutomationConnection.Stub {
private static final String TAG = "UiAutomationConnection";
private static final int INITIAL_FROZEN_ROTATION_UNSPECIFIED = -1;
private final IWindowManager mWindowManager = IWindowManager.Stub.asInterface(
ServiceManager.getService(Service.WINDOW_SERVICE));
private final IAccessibilityManager mAccessibilityManager = IAccessibilityManager.Stub
.asInterface(ServiceManager.getService(Service.ACCESSIBILITY_SERVICE));
private final IPermissionManager mPermissionManager = IPermissionManager.Stub
.asInterface(ServiceManager.getService("permissionmgr"));
private final IActivityManager mActivityManager = IActivityManager.Stub
.asInterface(ServiceManager.getService("activity"));
private final Object mLock = new Object();
private final Binder mToken = new Binder();
private int mInitialFrozenRotation = INITIAL_FROZEN_ROTATION_UNSPECIFIED;
private IAccessibilityServiceClient mClient;
private boolean mIsShutdown;
private int mOwningUid;
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public UiAutomationConnection() {
Log.d(TAG, "Created on user " + Process.myUserHandle());
}
@Override
public void connect(IAccessibilityServiceClient client, int flags) {
if (client == null) {
throw new IllegalArgumentException("Client cannot be null!");
}
synchronized (mLock) {
throwIfShutdownLocked();
if (isConnectedLocked()) {
throw new IllegalStateException("Already connected.");
}
mOwningUid = Binder.getCallingUid();
registerUiTestAutomationServiceLocked(client,
Binder.getCallingUserHandle().getIdentifier(), flags);
storeRotationStateLocked();
}
}
@Override
public void disconnect() {
synchronized (mLock) {
throwIfCalledByNotTrustedUidLocked();
throwIfShutdownLocked();
if (!isConnectedLocked()) {
throw new IllegalStateException("Already disconnected.");
}
mOwningUid = -1;
unregisterUiTestAutomationServiceLocked();
restoreRotationStateLocked();
}
}
@Override
public boolean injectInputEvent(InputEvent event, boolean sync, boolean waitForAnimations) {
synchronized (mLock) {
throwIfCalledByNotTrustedUidLocked();
throwIfShutdownLocked();
throwIfNotConnectedLocked();
}
final boolean syncTransactionsBefore;
final boolean syncTransactionsAfter;
if (event instanceof KeyEvent) {
KeyEvent keyEvent = (KeyEvent) event;
syncTransactionsBefore = keyEvent.getAction() == KeyEvent.ACTION_DOWN;
syncTransactionsAfter = keyEvent.getAction() == KeyEvent.ACTION_UP;
} else {
MotionEvent motionEvent = (MotionEvent) event;
syncTransactionsBefore = motionEvent.getAction() == MotionEvent.ACTION_DOWN
|| motionEvent.isFromSource(InputDevice.SOURCE_MOUSE);
syncTransactionsAfter = motionEvent.getAction() == MotionEvent.ACTION_UP;
}
final long identity = Binder.clearCallingIdentity();
try {
if (syncTransactionsBefore) {
mWindowManager.syncInputTransactions(waitForAnimations);
}
final boolean result = InputManagerGlobal.getInstance().injectInputEvent(event,
sync ? InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH
: InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
if (syncTransactionsAfter) {
mWindowManager.syncInputTransactions(waitForAnimations);
}
return result;
} catch (RemoteException e) {
e.rethrowFromSystemServer();
} finally {
Binder.restoreCallingIdentity(identity);
}
return false;
}
@Override
public void injectInputEventToInputFilter(InputEvent event) throws RemoteException {
synchronized (mLock) {
throwIfCalledByNotTrustedUidLocked();
throwIfShutdownLocked();
throwIfNotConnectedLocked();
}
mAccessibilityManager.injectInputEventToInputFilter(event);
}
@Override
public void syncInputTransactions(boolean waitForAnimations) {
synchronized (mLock) {
throwIfCalledByNotTrustedUidLocked();
throwIfShutdownLocked();
throwIfNotConnectedLocked();
}
try {
mWindowManager.syncInputTransactions(waitForAnimations);
} catch (RemoteException e) {
}
}
@Override
public boolean setRotation(int rotation) {
synchronized (mLock) {
throwIfCalledByNotTrustedUidLocked();
throwIfShutdownLocked();
throwIfNotConnectedLocked();
}
final long identity = Binder.clearCallingIdentity();
try {
if (rotation == UiAutomation.ROTATION_UNFREEZE) {
mWindowManager.thawRotation(/* caller= */ "UiAutomationConnection#setRotation");
} else {
mWindowManager.freezeRotation(rotation,
/* caller= */ "UiAutomationConnection#setRotation");
}
return true;
} catch (RemoteException re) {
/* ignore */
} finally {
Binder.restoreCallingIdentity(identity);
}
return false;
}
@Override
public boolean takeScreenshot(Rect crop, ScreenCapture.ScreenCaptureListener listener) {
synchronized (mLock) {
throwIfCalledByNotTrustedUidLocked();
throwIfShutdownLocked();
throwIfNotConnectedLocked();
}
final long identity = Binder.clearCallingIdentity();
try {
final CaptureArgs captureArgs = new CaptureArgs.Builder<>()
.setSourceCrop(crop)
.build();
mWindowManager.captureDisplay(DEFAULT_DISPLAY, captureArgs, listener);
} catch (RemoteException re) {
re.rethrowAsRuntimeException();
} finally {
Binder.restoreCallingIdentity(identity);
}
return true;
}
@Nullable
@Override
public boolean takeSurfaceControlScreenshot(@NonNull SurfaceControl surfaceControl,
ScreenCapture.ScreenCaptureListener listener) {
synchronized (mLock) {
throwIfCalledByNotTrustedUidLocked();
throwIfShutdownLocked();
throwIfNotConnectedLocked();
}
final long identity = Binder.clearCallingIdentity();
try {
ScreenCapture.LayerCaptureArgs args =
new ScreenCapture.LayerCaptureArgs.Builder(surfaceControl)
.setChildrenOnly(false)
.build();
int status = ScreenCapture.captureLayers(args, listener);
if (status != 0) {
return false;
}
} finally {
Binder.restoreCallingIdentity(identity);
}
return true;
}
@Override
public boolean clearWindowContentFrameStats(int windowId) throws RemoteException {
synchronized (mLock) {
throwIfCalledByNotTrustedUidLocked();
throwIfShutdownLocked();
throwIfNotConnectedLocked();
}
int callingUserId = UserHandle.getCallingUserId();
final long identity = Binder.clearCallingIdentity();
try {
IBinder token = mAccessibilityManager.getWindowToken(windowId, callingUserId);
if (token == null) {
return false;
}
return mWindowManager.clearWindowContentFrameStats(token);
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public WindowContentFrameStats getWindowContentFrameStats(int windowId) throws RemoteException {
synchronized (mLock) {
throwIfCalledByNotTrustedUidLocked();
throwIfShutdownLocked();
throwIfNotConnectedLocked();
}
int callingUserId = UserHandle.getCallingUserId();
final long identity = Binder.clearCallingIdentity();
try {
IBinder token = mAccessibilityManager.getWindowToken(windowId, callingUserId);
if (token == null) {
return null;
}
return mWindowManager.getWindowContentFrameStats(token);
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void clearWindowAnimationFrameStats() {
synchronized (mLock) {
throwIfCalledByNotTrustedUidLocked();
throwIfShutdownLocked();
throwIfNotConnectedLocked();
}
final long identity = Binder.clearCallingIdentity();
try {
SurfaceControl.clearAnimationFrameStats();
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public WindowAnimationFrameStats getWindowAnimationFrameStats() {
synchronized (mLock) {
throwIfCalledByNotTrustedUidLocked();
throwIfShutdownLocked();
throwIfNotConnectedLocked();
}
final long identity = Binder.clearCallingIdentity();
try {
WindowAnimationFrameStats stats = new WindowAnimationFrameStats();
SurfaceControl.getAnimationFrameStats(stats);
return stats;
} finally {
Binder.restoreCallingIdentity(identity);
}
}
/**
* Grants permission for the {@link Context#DEVICE_ID_DEFAULT default device}
*/
@Override
public void grantRuntimePermission(String packageName, String permission, int userId)
throws RemoteException {
synchronized (mLock) {
throwIfCalledByNotTrustedUidLocked();
throwIfShutdownLocked();
throwIfNotConnectedLocked();
}
final long identity = Binder.clearCallingIdentity();
try {
mPermissionManager.grantRuntimePermission(packageName, permission,
Context.DEVICE_ID_DEFAULT, userId);
} finally {
Binder.restoreCallingIdentity(identity);
}
}
/**
* Revokes permission for the {@link Context#DEVICE_ID_DEFAULT default device}
*/
@Override
public void revokeRuntimePermission(String packageName, String permission, int userId)
throws RemoteException {
synchronized (mLock) {
throwIfCalledByNotTrustedUidLocked();
throwIfShutdownLocked();
throwIfNotConnectedLocked();
}
final long identity = Binder.clearCallingIdentity();
try {
mPermissionManager.revokeRuntimePermission(packageName, permission,
Context.DEVICE_ID_DEFAULT, userId, null);
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void adoptShellPermissionIdentity(int uid, @Nullable String[] permissions)
throws RemoteException {
synchronized (mLock) {
throwIfCalledByNotTrustedUidLocked();
throwIfShutdownLocked();
throwIfNotConnectedLocked();
}
final long identity = Binder.clearCallingIdentity();
try {
mActivityManager.startDelegateShellPermissionIdentity(uid, permissions);
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void dropShellPermissionIdentity() throws RemoteException {
synchronized (mLock) {
throwIfCalledByNotTrustedUidLocked();
throwIfShutdownLocked();
throwIfNotConnectedLocked();
}
final long identity = Binder.clearCallingIdentity();
try {
mActivityManager.stopDelegateShellPermissionIdentity();
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
@Nullable
public List<String> getAdoptedShellPermissions() throws RemoteException {
synchronized (mLock) {
throwIfCalledByNotTrustedUidLocked();
throwIfShutdownLocked();
throwIfNotConnectedLocked();
}
final long identity = Binder.clearCallingIdentity();
try {
return mActivityManager.getDelegatedShellPermissions();
} finally {
Binder.restoreCallingIdentity(identity);
}
}
public class Repeater implements Runnable {
// Continuously read readFrom and write back to writeTo until EOF is encountered
private final InputStream readFrom;
private final OutputStream writeTo;
public Repeater (InputStream readFrom, OutputStream writeTo) {
this.readFrom = readFrom;
this.writeTo = writeTo;
}
@Override
public void run() {
try {
final byte[] buffer = new byte[8192];
int readByteCount;
while (true) {
readByteCount = readFrom.read(buffer);
if (readByteCount < 0) {
break;
}
writeTo.write(buffer, 0, readByteCount);
writeTo.flush();
}
} catch (IOException ignored) {
} finally {
IoUtils.closeQuietly(readFrom);
IoUtils.closeQuietly(writeTo);
}
}
}
@Override
public void executeShellCommand(final String command, final ParcelFileDescriptor sink,
final ParcelFileDescriptor source) throws RemoteException {
executeShellCommandWithStderr(command, sink, source, null /* stderrSink */);
}
@Override
public void executeShellCommandWithStderr(final String command, final ParcelFileDescriptor sink,
final ParcelFileDescriptor source, final ParcelFileDescriptor stderrSink)
throws RemoteException {
synchronized (mLock) {
throwIfCalledByNotTrustedUidLocked();
throwIfShutdownLocked();
throwIfNotConnectedLocked();
}
final java.lang.Process process;
try {
process = Runtime.getRuntime().exec(command);
} catch (IOException exc) {
throw new RuntimeException("Error running shell command '" + command + "'", exc);
}
// Read from process and write to pipe
final Thread readFromProcess;
if (sink != null) {
InputStream sink_in = process.getInputStream();;
OutputStream sink_out = new FileOutputStream(sink.getFileDescriptor());
readFromProcess = new Thread(new Repeater(sink_in, sink_out));
readFromProcess.start();
} else {
readFromProcess = null;
}
// Read from pipe and write to process
final Thread writeToProcess;
if (source != null) {
OutputStream source_out = process.getOutputStream();
InputStream source_in = new FileInputStream(source.getFileDescriptor());
writeToProcess = new Thread(new Repeater(source_in, source_out));
writeToProcess.start();
} else {
writeToProcess = null;
}
// Read from process stderr and write to pipe
final Thread readStderrFromProcess;
if (stderrSink != null) {
InputStream sink_in = process.getErrorStream();
OutputStream sink_out = new FileOutputStream(stderrSink.getFileDescriptor());
readStderrFromProcess = new Thread(new Repeater(sink_in, sink_out));
readStderrFromProcess.start();
} else {
readStderrFromProcess = null;
}
Thread cleanup = new Thread(new Runnable() {
@Override
public void run() {
try {
if (writeToProcess != null) {
writeToProcess.join();
}
if (readFromProcess != null) {
readFromProcess.join();
}
if (readStderrFromProcess != null) {
readStderrFromProcess.join();
}
} catch (InterruptedException exc) {
Log.e(TAG, "At least one of the threads was interrupted");
}
IoUtils.closeQuietly(sink);
IoUtils.closeQuietly(source);
IoUtils.closeQuietly(stderrSink);
process.destroy();
}
});
cleanup.start();
}
@Override
public void shutdown() {
synchronized (mLock) {
if (isConnectedLocked()) {
throwIfCalledByNotTrustedUidLocked();
}
throwIfShutdownLocked();
mIsShutdown = true;
if (isConnectedLocked()) {
disconnect();
}
}
}
private void registerUiTestAutomationServiceLocked(IAccessibilityServiceClient client,
@UserIdInt int userId, int flags) {
IAccessibilityManager manager = IAccessibilityManager.Stub.asInterface(
ServiceManager.getService(Context.ACCESSIBILITY_SERVICE));
final AccessibilityServiceInfo info = new AccessibilityServiceInfo();
info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;
info.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC;
info.flags |= AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
| AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS
| AccessibilityServiceInfo.FLAG_FORCE_DIRECT_BOOT_AWARE;
info.setCapabilities(AccessibilityServiceInfo.CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT
| AccessibilityServiceInfo.CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION
| AccessibilityServiceInfo.CAPABILITY_CAN_REQUEST_FILTER_KEY_EVENTS);
if ((flags & UiAutomation.FLAG_NOT_ACCESSIBILITY_TOOL) == 0) {
info.setAccessibilityTool(true);
}
try {
// Calling out with a lock held is fine since if the system
// process is gone the client calling in will be killed.
manager.registerUiTestAutomationService(mToken, client, info, userId, flags);
mClient = client;
} catch (RemoteException re) {
throw new IllegalStateException("Error while registering UiTestAutomationService for "
+ "user " + userId + ".", re);
}
}
private void unregisterUiTestAutomationServiceLocked() {
IAccessibilityManager manager = IAccessibilityManager.Stub.asInterface(
ServiceManager.getService(Context.ACCESSIBILITY_SERVICE));
try {
// Calling out with a lock held is fine since if the system
// process is gone the client calling in will be killed.
manager.unregisterUiTestAutomationService(mClient);
mClient = null;
} catch (RemoteException re) {
throw new IllegalStateException("Error while unregistering UiTestAutomationService",
re);
}
}
private void storeRotationStateLocked() {
try {
if (mWindowManager.isRotationFrozen()) {
// Calling out with a lock held is fine since if the system
// process is gone the client calling in will be killed.
mInitialFrozenRotation = mWindowManager.getDefaultDisplayRotation();
}
} catch (RemoteException re) {
/* ignore */
}
}
private void restoreRotationStateLocked() {
try {
if (mInitialFrozenRotation != INITIAL_FROZEN_ROTATION_UNSPECIFIED) {
// Calling out with a lock held is fine since if the system
// process is gone the client calling in will be killed.
mWindowManager.freezeRotation(mInitialFrozenRotation,
/* caller= */ "UiAutomationConnection#restoreRotationStateLocked");
} else {
// Calling out with a lock held is fine since if the system
// process is gone the client calling in will be killed.
mWindowManager.thawRotation(
/* caller= */ "UiAutomationConnection#restoreRotationStateLocked");
}
} catch (RemoteException re) {
/* ignore */
}
}
private boolean isConnectedLocked() {
return mClient != null;
}
private void throwIfShutdownLocked() {
if (mIsShutdown) {
throw new IllegalStateException("Connection shutdown!");
}
}
private void throwIfNotConnectedLocked() {
if (!isConnectedLocked()) {
throw new IllegalStateException("Not connected!");
}
}
private void throwIfCalledByNotTrustedUidLocked() {
final int callingUid = Binder.getCallingUid();
if (callingUid != mOwningUid && mOwningUid != Process.SYSTEM_UID
&& callingUid != 0 /*root*/) {
throw new SecurityException("Calling from not trusted UID!");
}
}
}