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