blob: 18cab7bee12d6f80ae3741cdbee57cbecf48cb96 [file] [log] [blame]
/*
* Copyright (C) 2015 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 com.android.preload;
import com.android.ddmlib.AdbCommandRejectedException;
import com.android.ddmlib.AndroidDebugBridge;
import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
import com.android.preload.classdataretrieval.hprof.Hprof;
import com.android.ddmlib.DdmPreferences;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.IShellOutputReceiver;
import com.android.ddmlib.SyncException;
import com.android.ddmlib.TimeoutException;
import java.io.File;
import java.io.IOException;
import java.util.Date;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
/**
* Helper class for some device routines.
*/
public class DeviceUtils {
// Locations
private static final String PRELOADED_CLASSES_FILE = "/etc/preloaded-classes";
// Shell commands
private static final String CREATE_EMPTY_PRELOADED_CMD = "touch " + PRELOADED_CLASSES_FILE;
private static final String DELETE_CACHE_CMD = "rm /data/dalvik-cache/*/*boot.art";
private static final String DELETE_PRELOADED_CMD = "rm " + PRELOADED_CLASSES_FILE;
private static final String READ_PRELOADED_CMD = "cat " + PRELOADED_CLASSES_FILE;
private static final String START_SHELL_CMD = "start";
private static final String STOP_SHELL_CMD = "stop";
private static final String REMOUNT_SYSTEM_CMD = "mount -o rw,remount /system";
private static final String UNSET_BOOTCOMPLETE_CMD = "setprop dev.bootcomplete \"0\"";
public static void init(int debugPort) {
DdmPreferences.setSelectedDebugPort(debugPort);
Hprof.init();
AndroidDebugBridge.init(true);
AndroidDebugBridge.createBridge();
}
/**
* Run a command in the shell on the device.
*/
public static void doShell(IDevice device, String cmdline, long timeout, TimeUnit unit) {
doShell(device, cmdline, new NullShellOutputReceiver(), timeout, unit);
}
/**
* Run a command in the shell on the device. Collects and returns the console output.
*/
public static String doShellReturnString(IDevice device, String cmdline, long timeout,
TimeUnit unit) {
CollectStringShellOutputReceiver rec = new CollectStringShellOutputReceiver();
doShell(device, cmdline, rec, timeout, unit);
return rec.toString();
}
/**
* Run a command in the shell on the device, directing all output to the given receiver.
*/
public static void doShell(IDevice device, String cmdline, IShellOutputReceiver receiver,
long timeout, TimeUnit unit) {
try {
device.executeShellCommand(cmdline, receiver, timeout, unit);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Run am start on the device.
*/
public static void doAMStart(IDevice device, String name, String activity) {
doShell(device, "am start -n " + name + " /." + activity, 30, TimeUnit.SECONDS);
}
/**
* Find the device with the given serial. Give up after the given timeout (in milliseconds).
*/
public static IDevice findDevice(String serial, int timeout) {
WaitForDevice wfd = new WaitForDevice(serial, timeout);
return wfd.get();
}
/**
* Get all devices ddms knows about. Wait at most for the given timeout.
*/
public static IDevice[] findDevices(int timeout) {
WaitForDevice wfd = new WaitForDevice(null, timeout);
wfd.get();
return AndroidDebugBridge.getBridge().getDevices();
}
/**
* Return the build type of the given device. This is the value of the "ro.build.type"
* system property.
*/
public static String getBuildType(IDevice device) {
try {
Future<String> buildType = device.getSystemProperty("ro.build.type");
return buildType.get(500, TimeUnit.MILLISECONDS);
} catch (Exception e) {
}
return null;
}
/**
* Check whether the given device has a pre-optimized boot image. More precisely, checks
* whether /system/framework/ * /boot.art exists.
*/
public static boolean hasPrebuiltBootImage(IDevice device) {
String ret =
doShellReturnString(device, "ls /system/framework/*/boot.art", 500, TimeUnit.MILLISECONDS);
return !ret.contains("No such file or directory");
}
/**
* Write over the preloaded-classes file with an empty or existing file and regenerate the boot
* image as necessary.
*
* @param device
* @param pcFile
* @param bootTimeout
* @throws AdbCommandRejectedException
* @throws IOException
* @throws TimeoutException
* @throws SyncException
* @return true if successfully overwritten, false otherwise
*/
public static boolean overwritePreloaded(IDevice device, File pcFile, long bootTimeout)
throws AdbCommandRejectedException, IOException, TimeoutException, SyncException {
boolean writeEmpty = (pcFile == null);
if (writeEmpty) {
// Check if the preloaded-classes file is already empty.
String oldContent =
doShellReturnString(device, READ_PRELOADED_CMD, 1, TimeUnit.SECONDS);
if (oldContent.trim().equals("")) {
System.out.println("Preloaded-classes already empty.");
return true;
}
}
// Stop the system server etc.
doShell(device, STOP_SHELL_CMD, 1, TimeUnit.SECONDS);
// Remount the read-only system partition
doShell(device, REMOUNT_SYSTEM_CMD, 1, TimeUnit.SECONDS);
// Delete the preloaded-classes file
doShell(device, DELETE_PRELOADED_CMD, 1, TimeUnit.SECONDS);
// Delete the dalvik cache files
doShell(device, DELETE_CACHE_CMD, 1, TimeUnit.SECONDS);
if (writeEmpty) {
// Write an empty preloaded-classes file
doShell(device, CREATE_EMPTY_PRELOADED_CMD, 500, TimeUnit.MILLISECONDS);
} else {
// Push the new preloaded-classes file
device.pushFile(pcFile.getAbsolutePath(), PRELOADED_CLASSES_FILE);
}
// Manually reset the boot complete flag
doShell(device, UNSET_BOOTCOMPLETE_CMD, 1, TimeUnit.SECONDS);
// Restart system server on the device
doShell(device, START_SHELL_CMD, 1, TimeUnit.SECONDS);
// Wait for the boot complete flag and return the outcome.
return waitForBootComplete(device, bootTimeout);
}
private static boolean waitForBootComplete(IDevice device, long timeout) {
// Do a loop checking each second whether bootcomplete. Wait for at most the given
// threshold.
Date startDate = new Date();
for (;;) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// Ignore spurious wakeup.
}
// Check whether bootcomplete.
String ret =
doShellReturnString(device, "getprop dev.bootcomplete", 500, TimeUnit.MILLISECONDS);
if (ret.trim().equals("1")) {
break;
}
System.out.println("Still not booted: " + ret);
// Check whether we timed out. This is a simplistic check that doesn't take into account
// things like switches in time.
Date endDate = new Date();
long seconds =
TimeUnit.SECONDS.convert(endDate.getTime() - startDate.getTime(), TimeUnit.MILLISECONDS);
if (seconds > timeout) {
return false;
}
}
return true;
}
/**
* Enable method-tracing on device. The system should be restarted after this.
*/
public static void enableTracing(IDevice device) {
// Disable selinux.
doShell(device, "setenforce 0", 100, TimeUnit.MILLISECONDS);
// Make the profile directory world-writable.
doShell(device, "chmod 777 /data/dalvik-cache/profiles", 100, TimeUnit.MILLISECONDS);
// Enable streaming method tracing with a small 1K buffer.
doShell(device, "setprop dalvik.vm.method-trace true", 100, TimeUnit.MILLISECONDS);
doShell(device, "setprop dalvik.vm.method-trace-file "
+ "/data/dalvik-cache/profiles/zygote.trace.bin", 100, TimeUnit.MILLISECONDS);
doShell(device, "setprop dalvik.vm.method-trace-file-siz 1024", 100, TimeUnit.MILLISECONDS);
doShell(device, "setprop dalvik.vm.method-trace-stream true", 100, TimeUnit.MILLISECONDS);
}
private static class NullShellOutputReceiver implements IShellOutputReceiver {
@Override
public boolean isCancelled() {
return false;
}
@Override
public void flush() {}
@Override
public void addOutput(byte[] arg0, int arg1, int arg2) {}
}
private static class CollectStringShellOutputReceiver implements IShellOutputReceiver {
private StringBuilder builder = new StringBuilder();
@Override
public String toString() {
String ret = builder.toString();
// Strip trailing newlines. They are especially ugly because adb uses DOS line endings.
while (ret.endsWith("\r") || ret.endsWith("\n")) {
ret = ret.substring(0, ret.length() - 1);
}
return ret;
}
@Override
public void addOutput(byte[] arg0, int arg1, int arg2) {
builder.append(new String(arg0, arg1, arg2));
}
@Override
public void flush() {}
@Override
public boolean isCancelled() {
return false;
}
}
private static class WaitForDevice {
private String serial;
private long timeout;
private IDevice device;
public WaitForDevice(String serial, long timeout) {
this.serial = serial;
this.timeout = timeout;
device = null;
}
public IDevice get() {
if (device == null) {
WaitForDeviceListener wfdl = new WaitForDeviceListener(serial);
synchronized (wfdl) {
AndroidDebugBridge.addDeviceChangeListener(wfdl);
// Check whether we already know about this device.
IDevice[] devices = AndroidDebugBridge.getBridge().getDevices();
if (serial != null) {
for (IDevice d : devices) {
if (serial.equals(d.getSerialNumber())) {
// Only accept if there are clients already. Else wait for the callback informing
// us that we now have clients.
if (d.hasClients()) {
device = d;
}
break;
}
}
} else {
if (devices.length > 0) {
device = devices[0];
}
}
if (device == null) {
try {
wfdl.wait(timeout);
} catch (InterruptedException e) {
// Ignore spurious wakeups.
}
device = wfdl.getDevice();
}
AndroidDebugBridge.removeDeviceChangeListener(wfdl);
}
}
if (device != null) {
// Wait for clients.
WaitForClientsListener wfcl = new WaitForClientsListener(device);
synchronized (wfcl) {
AndroidDebugBridge.addDeviceChangeListener(wfcl);
if (!device.hasClients()) {
try {
wfcl.wait(timeout);
} catch (InterruptedException e) {
// Ignore spurious wakeups.
}
}
AndroidDebugBridge.removeDeviceChangeListener(wfcl);
}
}
return device;
}
private static class WaitForDeviceListener implements IDeviceChangeListener {
private String serial;
private IDevice device;
public WaitForDeviceListener(String serial) {
this.serial = serial;
}
public IDevice getDevice() {
return device;
}
@Override
public void deviceChanged(IDevice arg0, int arg1) {
// We may get a device changed instead of connected. Handle like a connection.
deviceConnected(arg0);
}
@Override
public void deviceConnected(IDevice arg0) {
if (device != null) {
// Ignore updates.
return;
}
if (serial == null || serial.equals(arg0.getSerialNumber())) {
device = arg0;
synchronized (this) {
notifyAll();
}
}
}
@Override
public void deviceDisconnected(IDevice arg0) {
// Ignore disconnects.
}
}
private static class WaitForClientsListener implements IDeviceChangeListener {
private IDevice myDevice;
public WaitForClientsListener(IDevice myDevice) {
this.myDevice = myDevice;
}
@Override
public void deviceChanged(IDevice arg0, int arg1) {
if (arg0 == myDevice && (arg1 & IDevice.CHANGE_CLIENT_LIST) != 0) {
// Got a client list, done here.
synchronized (this) {
notifyAll();
}
}
}
@Override
public void deviceConnected(IDevice arg0) {
}
@Override
public void deviceDisconnected(IDevice arg0) {
}
}
}
}