/* * 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 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) { } } } }