* Requires root, disable-verity, reboot, root before use. Test: manual through UI and sequencing Change-Id: I68965334776e130b8220a5814b2525109cf96800
421 lines
14 KiB
Java
421 lines
14 KiB
Java
/*
|
|
* 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) {
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
}
|