1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.preload;
18
19import com.android.ddmlib.AdbCommandRejectedException;
20import com.android.ddmlib.AndroidDebugBridge;
21import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
22import com.android.preload.classdataretrieval.hprof.Hprof;
23import com.android.ddmlib.DdmPreferences;
24import com.android.ddmlib.IDevice;
25import com.android.ddmlib.IShellOutputReceiver;
26import com.android.ddmlib.SyncException;
27import com.android.ddmlib.TimeoutException;
28
29import java.io.File;
30import java.io.IOException;
31import java.util.Date;
32import java.util.concurrent.Future;
33import java.util.concurrent.TimeUnit;
34
35/**
36 * Helper class for some device routines.
37 */
38public class DeviceUtils {
39
40  // Locations
41  private static final String PRELOADED_CLASSES_FILE = "/etc/preloaded-classes";
42  // Shell commands
43  private static final String CREATE_EMPTY_PRELOADED_CMD = "touch " + PRELOADED_CLASSES_FILE;
44  private static final String DELETE_CACHE_CMD = "rm /data/dalvik-cache/*/*boot.art";
45  private static final String DELETE_PRELOADED_CMD = "rm " + PRELOADED_CLASSES_FILE;
46  private static final String READ_PRELOADED_CMD = "cat " + PRELOADED_CLASSES_FILE;
47  private static final String START_SHELL_CMD = "start";
48  private static final String STOP_SHELL_CMD = "stop";
49  private static final String REMOUNT_SYSTEM_CMD = "mount -o rw,remount /system";
50  private static final String UNSET_BOOTCOMPLETE_CMD = "setprop dev.bootcomplete \"0\"";
51
52  public static void init(int debugPort) {
53    DdmPreferences.setSelectedDebugPort(debugPort);
54
55    Hprof.init();
56
57    AndroidDebugBridge.init(true);
58
59    AndroidDebugBridge.createBridge();
60  }
61
62  /**
63   * Run a command in the shell on the device.
64   */
65  public static void doShell(IDevice device, String cmdline, long timeout, TimeUnit unit) {
66    doShell(device, cmdline, new NullShellOutputReceiver(), timeout, unit);
67  }
68
69  /**
70   * Run a command in the shell on the device. Collects and returns the console output.
71   */
72  public static String doShellReturnString(IDevice device, String cmdline, long timeout,
73      TimeUnit unit) {
74    CollectStringShellOutputReceiver rec = new CollectStringShellOutputReceiver();
75    doShell(device, cmdline, rec, timeout, unit);
76    return rec.toString();
77  }
78
79  /**
80   * Run a command in the shell on the device, directing all output to the given receiver.
81   */
82  public static void doShell(IDevice device, String cmdline, IShellOutputReceiver receiver,
83      long timeout, TimeUnit unit) {
84    try {
85      device.executeShellCommand(cmdline, receiver, timeout, unit);
86    } catch (Exception e) {
87      e.printStackTrace();
88    }
89  }
90
91  /**
92   * Run am start on the device.
93   */
94  public static void doAMStart(IDevice device, String name, String activity) {
95    doShell(device, "am start -n " + name + " /." + activity, 30, TimeUnit.SECONDS);
96  }
97
98  /**
99   * Find the device with the given serial. Give up after the given timeout (in milliseconds).
100   */
101  public static IDevice findDevice(String serial, int timeout) {
102    WaitForDevice wfd = new WaitForDevice(serial, timeout);
103    return wfd.get();
104  }
105
106  /**
107   * Get all devices ddms knows about. Wait at most for the given timeout.
108   */
109  public static IDevice[] findDevices(int timeout) {
110    WaitForDevice wfd = new WaitForDevice(null, timeout);
111    wfd.get();
112    return AndroidDebugBridge.getBridge().getDevices();
113  }
114
115  /**
116   * Return the build type of the given device. This is the value of the "ro.build.type"
117   * system property.
118   */
119  public static String getBuildType(IDevice device) {
120    try {
121      Future<String> buildType = device.getSystemProperty("ro.build.type");
122      return buildType.get(500, TimeUnit.MILLISECONDS);
123    } catch (Exception e) {
124    }
125    return null;
126  }
127
128  /**
129   * Check whether the given device has a pre-optimized boot image. More precisely, checks
130   * whether /system/framework/ * /boot.art exists.
131   */
132  public static boolean hasPrebuiltBootImage(IDevice device) {
133    String ret =
134        doShellReturnString(device, "ls /system/framework/*/boot.art", 500, TimeUnit.MILLISECONDS);
135
136    return !ret.contains("No such file or directory");
137  }
138
139    /**
140     * Write over the preloaded-classes file with an empty or existing file and regenerate the boot
141     * image as necessary.
142     *
143     * @param device
144     * @param pcFile
145     * @param bootTimeout
146     * @throws AdbCommandRejectedException
147     * @throws IOException
148     * @throws TimeoutException
149     * @throws SyncException
150     * @return true if successfully overwritten, false otherwise
151     */
152    public static boolean overwritePreloaded(IDevice device, File pcFile, long bootTimeout)
153            throws AdbCommandRejectedException, IOException, TimeoutException, SyncException {
154        boolean writeEmpty = (pcFile == null);
155        if (writeEmpty) {
156            // Check if the preloaded-classes file is already empty.
157            String oldContent =
158                    doShellReturnString(device, READ_PRELOADED_CMD, 1, TimeUnit.SECONDS);
159            if (oldContent.trim().equals("")) {
160                System.out.println("Preloaded-classes already empty.");
161                return true;
162            }
163        }
164
165        // Stop the system server etc.
166        doShell(device, STOP_SHELL_CMD, 1, TimeUnit.SECONDS);
167        // Remount the read-only system partition
168        doShell(device, REMOUNT_SYSTEM_CMD, 1, TimeUnit.SECONDS);
169        // Delete the preloaded-classes file
170        doShell(device, DELETE_PRELOADED_CMD, 1, TimeUnit.SECONDS);
171        // Delete the dalvik cache files
172        doShell(device, DELETE_CACHE_CMD, 1, TimeUnit.SECONDS);
173        if (writeEmpty) {
174            // Write an empty preloaded-classes file
175            doShell(device, CREATE_EMPTY_PRELOADED_CMD, 500, TimeUnit.MILLISECONDS);
176        } else {
177            // Push the new preloaded-classes file
178            device.pushFile(pcFile.getAbsolutePath(), PRELOADED_CLASSES_FILE);
179        }
180        // Manually reset the boot complete flag
181        doShell(device, UNSET_BOOTCOMPLETE_CMD, 1, TimeUnit.SECONDS);
182        // Restart system server on the device
183        doShell(device, START_SHELL_CMD, 1, TimeUnit.SECONDS);
184        // Wait for the boot complete flag and return the outcome.
185        return waitForBootComplete(device, bootTimeout);
186  }
187
188  private static boolean waitForBootComplete(IDevice device, long timeout) {
189    // Do a loop checking each second whether bootcomplete. Wait for at most the given
190    // threshold.
191    Date startDate = new Date();
192    for (;;) {
193      try {
194        Thread.sleep(1000);
195      } catch (InterruptedException e) {
196        // Ignore spurious wakeup.
197      }
198      // Check whether bootcomplete.
199      String ret =
200          doShellReturnString(device, "getprop dev.bootcomplete", 500, TimeUnit.MILLISECONDS);
201      if (ret.trim().equals("1")) {
202        break;
203      }
204      System.out.println("Still not booted: " + ret);
205
206      // Check whether we timed out. This is a simplistic check that doesn't take into account
207      // things like switches in time.
208      Date endDate = new Date();
209      long seconds =
210          TimeUnit.SECONDS.convert(endDate.getTime() - startDate.getTime(), TimeUnit.MILLISECONDS);
211      if (seconds > timeout) {
212        return false;
213      }
214    }
215
216    return true;
217  }
218
219  /**
220   * Enable method-tracing on device. The system should be restarted after this.
221   */
222  public static void enableTracing(IDevice device) {
223    // Disable selinux.
224    doShell(device, "setenforce 0", 100, TimeUnit.MILLISECONDS);
225
226    // Make the profile directory world-writable.
227    doShell(device, "chmod 777 /data/dalvik-cache/profiles", 100, TimeUnit.MILLISECONDS);
228
229    // Enable streaming method tracing with a small 1K buffer.
230    doShell(device, "setprop dalvik.vm.method-trace true", 100, TimeUnit.MILLISECONDS);
231    doShell(device, "setprop dalvik.vm.method-trace-file "
232                    + "/data/dalvik-cache/profiles/zygote.trace.bin", 100, TimeUnit.MILLISECONDS);
233    doShell(device, "setprop dalvik.vm.method-trace-file-siz 1024", 100, TimeUnit.MILLISECONDS);
234    doShell(device, "setprop dalvik.vm.method-trace-stream true", 100, TimeUnit.MILLISECONDS);
235  }
236
237  private static class NullShellOutputReceiver implements IShellOutputReceiver {
238    @Override
239    public boolean isCancelled() {
240      return false;
241    }
242
243    @Override
244    public void flush() {}
245
246    @Override
247    public void addOutput(byte[] arg0, int arg1, int arg2) {}
248  }
249
250  private static class CollectStringShellOutputReceiver implements IShellOutputReceiver {
251
252    private StringBuilder builder = new StringBuilder();
253
254    @Override
255    public String toString() {
256      String ret = builder.toString();
257      // Strip trailing newlines. They are especially ugly because adb uses DOS line endings.
258      while (ret.endsWith("\r") || ret.endsWith("\n")) {
259        ret = ret.substring(0, ret.length() - 1);
260      }
261      return ret;
262    }
263
264    @Override
265    public void addOutput(byte[] arg0, int arg1, int arg2) {
266      builder.append(new String(arg0, arg1, arg2));
267    }
268
269    @Override
270    public void flush() {}
271
272    @Override
273    public boolean isCancelled() {
274      return false;
275    }
276  }
277
278  private static class WaitForDevice {
279
280    private String serial;
281    private long timeout;
282    private IDevice device;
283
284    public WaitForDevice(String serial, long timeout) {
285      this.serial = serial;
286      this.timeout = timeout;
287      device = null;
288    }
289
290    public IDevice get() {
291      if (device == null) {
292          WaitForDeviceListener wfdl = new WaitForDeviceListener(serial);
293          synchronized (wfdl) {
294              AndroidDebugBridge.addDeviceChangeListener(wfdl);
295
296              // Check whether we already know about this device.
297              IDevice[] devices = AndroidDebugBridge.getBridge().getDevices();
298              if (serial != null) {
299                  for (IDevice d : devices) {
300                      if (serial.equals(d.getSerialNumber())) {
301                          // Only accept if there are clients already. Else wait for the callback informing
302                          // us that we now have clients.
303                          if (d.hasClients()) {
304                              device = d;
305                          }
306
307                          break;
308                      }
309                  }
310              } else {
311                  if (devices.length > 0) {
312                      device = devices[0];
313                  }
314              }
315
316              if (device == null) {
317                  try {
318                      wfdl.wait(timeout);
319                  } catch (InterruptedException e) {
320                      // Ignore spurious wakeups.
321                  }
322                  device = wfdl.getDevice();
323              }
324
325              AndroidDebugBridge.removeDeviceChangeListener(wfdl);
326          }
327      }
328
329      if (device != null) {
330          // Wait for clients.
331          WaitForClientsListener wfcl = new WaitForClientsListener(device);
332          synchronized (wfcl) {
333              AndroidDebugBridge.addDeviceChangeListener(wfcl);
334
335              if (!device.hasClients()) {
336                  try {
337                      wfcl.wait(timeout);
338                  } catch (InterruptedException e) {
339                      // Ignore spurious wakeups.
340                  }
341              }
342
343              AndroidDebugBridge.removeDeviceChangeListener(wfcl);
344          }
345      }
346
347      return device;
348    }
349
350    private static class WaitForDeviceListener implements IDeviceChangeListener {
351
352        private String serial;
353        private IDevice device;
354
355        public WaitForDeviceListener(String serial) {
356            this.serial = serial;
357        }
358
359        public IDevice getDevice() {
360            return device;
361        }
362
363        @Override
364        public void deviceChanged(IDevice arg0, int arg1) {
365            // We may get a device changed instead of connected. Handle like a connection.
366            deviceConnected(arg0);
367        }
368
369        @Override
370        public void deviceConnected(IDevice arg0) {
371            if (device != null) {
372                // Ignore updates.
373                return;
374            }
375
376            if (serial == null || serial.equals(arg0.getSerialNumber())) {
377                device = arg0;
378                synchronized (this) {
379                    notifyAll();
380                }
381            }
382        }
383
384        @Override
385        public void deviceDisconnected(IDevice arg0) {
386            // Ignore disconnects.
387        }
388
389    }
390
391    private static class WaitForClientsListener implements IDeviceChangeListener {
392
393        private IDevice myDevice;
394
395        public WaitForClientsListener(IDevice myDevice) {
396            this.myDevice = myDevice;
397        }
398
399        @Override
400        public void deviceChanged(IDevice arg0, int arg1) {
401            if (arg0 == myDevice && (arg1 & IDevice.CHANGE_CLIENT_LIST) != 0) {
402                // Got a client list, done here.
403                synchronized (this) {
404                    notifyAll();
405                }
406            }
407        }
408
409        @Override
410        public void deviceConnected(IDevice arg0) {
411        }
412
413        @Override
414        public void deviceDisconnected(IDevice arg0) {
415        }
416
417    }
418  }
419
420}
421