blob: dc07df8d7a874efdbf445da37ce08f631abd75ae [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 android.accessibilityservice.AccessibilityServiceInfo;
import android.accessibilityservice.IAccessibilityServiceClient;
import android.annotation.Nullable;
import android.content.Context;
import android.content.pm.IPackageManager;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.hardware.input.InputManager;
import android.os.Binder;
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.util.Log;
import android.view.IWindowManager;
import android.view.InputEvent;
import android.view.SurfaceControl;
import android.view.WindowAnimationFrameStats;
import android.view.WindowContentFrameStats;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.IAccessibilityManager;
import libcore.io.IoUtils;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* 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 IPackageManager mPackageManager = IPackageManager.Stub
.asInterface(ServiceManager.getService("package"));
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;
@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, 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) {
synchronized (mLock) {
throwIfCalledByNotTrustedUidLocked();
throwIfShutdownLocked();
throwIfNotConnectedLocked();
}
final int mode = (sync) ? InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH
: InputManager.INJECT_INPUT_EVENT_MODE_ASYNC;
final long identity = Binder.clearCallingIdentity();
try {
return mWindowManager.injectInputAfterTransactionsApplied(event, mode);
} catch (RemoteException e) {
} finally {
Binder.restoreCallingIdentity(identity);
}
return false;
}
@Override
public void syncInputTransactions() {
synchronized (mLock) {
throwIfCalledByNotTrustedUidLocked();
throwIfShutdownLocked();
throwIfNotConnectedLocked();
}
try {
mWindowManager.syncInputTransactions();
} 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();
} else {
mWindowManager.freezeRotation(rotation);
}
return true;
} catch (RemoteException re) {
/* ignore */
} finally {
Binder.restoreCallingIdentity(identity);
}
return false;
}
@Override
public Bitmap takeScreenshot(Rect crop, int rotation) {
synchronized (mLock) {
throwIfCalledByNotTrustedUidLocked();
throwIfShutdownLocked();
throwIfNotConnectedLocked();
}
final long identity = Binder.clearCallingIdentity();
try {
int width = crop.width();
int height = crop.height();
return SurfaceControl.screenshot(crop, width, height, rotation);
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@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);
}
}
@Override
public void grantRuntimePermission(String packageName, String permission, int userId)
throws RemoteException {
synchronized (mLock) {
throwIfCalledByNotTrustedUidLocked();
throwIfShutdownLocked();
throwIfNotConnectedLocked();
}
final long identity = Binder.clearCallingIdentity();
try {
mPackageManager.grantRuntimePermission(packageName, permission, userId);
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void revokeRuntimePermission(String packageName, String permission, int userId)
throws RemoteException {
synchronized (mLock) {
throwIfCalledByNotTrustedUidLocked();
throwIfShutdownLocked();
throwIfNotConnectedLocked();
}
final long identity = Binder.clearCallingIdentity();
try {
mPackageManager.revokeRuntimePermission(packageName, permission, userId);
} 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);
}
}
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 ioe) {
Log.w(TAG, "Error while reading/writing to streams");
} finally {
IoUtils.closeQuietly(readFrom);
IoUtils.closeQuietly(writeTo);
}
}
}
@Override
public void executeShellCommand(final String command, final ParcelFileDescriptor sink,
final ParcelFileDescriptor source) 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;
}
Thread cleanup = new Thread(new Runnable() {
@Override
public void run() {
try {
if (writeToProcess != null) {
writeToProcess.join();
}
if (readFromProcess != null) {
readFromProcess.join();
}
} catch (InterruptedException exc) {
Log.e(TAG, "At least one of the threads was interrupted");
}
IoUtils.closeQuietly(sink);
IoUtils.closeQuietly(source);
process.destroy();
}
});
cleanup.start();
}
@Override
public void shutdown() {
synchronized (mLock) {
if (isConnectedLocked()) {
throwIfCalledByNotTrustedUidLocked();
}
throwIfShutdownLocked();
mIsShutdown = true;
if (isConnectedLocked()) {
disconnect();
}
}
}
private void registerUiTestAutomationServiceLocked(IAccessibilityServiceClient client,
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_ENHANCED_WEB_ACCESSIBILITY
| AccessibilityServiceInfo.CAPABILITY_CAN_REQUEST_FILTER_KEY_EVENTS);
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, flags);
mClient = client;
} catch (RemoteException re) {
throw new IllegalStateException("Error while registering UiTestAutomationService.", 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);
} 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();
}
} 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!");
}
}
}