/*
* 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.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 java.util.Date;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
/**
* Helper class for some device routines.
*/
public class DeviceUtils {
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");
}
/**
* Remove files involved in a standard build that interfere with collecting data. This will
* remove /etc/preloaded-classes, which determines which classes are allocated already in the
* boot image. It also deletes any compiled boot image on the device. Then it restarts the
* device.
*
* This is a potentially long-running operation, as the boot after the deletion may take a while.
* The method will abort after the given timeout.
*/
public static boolean removePreloaded(IDevice device, long preloadedWaitTimeInSeconds) {
String oldContent =
DeviceUtils.doShellReturnString(device, "cat /etc/preloaded-classes", 1, TimeUnit.SECONDS);
if (oldContent.trim().equals("")) {
System.out.println("Preloaded-classes already empty.");
return true;
}
// Stop the system server etc.
doShell(device, "stop", 100, TimeUnit.MILLISECONDS);
// Remount /system, delete /etc/preloaded-classes. It would be nice to use "adb remount,"
// but AndroidDebugBridge doesn't expose it.
doShell(device, "mount -o remount,rw /system", 500, TimeUnit.MILLISECONDS);
doShell(device, "rm /etc/preloaded-classes", 100, TimeUnit.MILLISECONDS);
// We do need an empty file.
doShell(device, "touch /etc/preloaded-classes", 100, TimeUnit.MILLISECONDS);
// Delete the files in the dalvik cache.
doShell(device, "rm /data/dalvik-cache/*/*boot.art", 500, TimeUnit.MILLISECONDS);
// We'll try to use dev.bootcomplete to know when the system server is back up. But stop
// doesn't reset it, so do it manually.
doShell(device, "setprop dev.bootcomplete \"0\"", 500, TimeUnit.MILLISECONDS);
// Start the system server.
doShell(device, "start", 100, TimeUnit.MILLISECONDS);
// 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 > preloadedWaitTimeInSeconds) {
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) {
}
}
}
}