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.AndroidDebugBridge;
20import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
21import com.android.preload.classdataretrieval.hprof.Hprof;
22import com.android.ddmlib.DdmPreferences;
23import com.android.ddmlib.IDevice;
24import com.android.ddmlib.IShellOutputReceiver;
25
26import java.util.Date;
27import java.util.concurrent.Future;
28import java.util.concurrent.TimeUnit;
29
30/**
31 * Helper class for some device routines.
32 */
33public class DeviceUtils {
34
35  public static void init(int debugPort) {
36    DdmPreferences.setSelectedDebugPort(debugPort);
37
38    Hprof.init();
39
40    AndroidDebugBridge.init(true);
41
42    AndroidDebugBridge.createBridge();
43  }
44
45  /**
46   * Run a command in the shell on the device.
47   */
48  public static void doShell(IDevice device, String cmdline, long timeout, TimeUnit unit) {
49    doShell(device, cmdline, new NullShellOutputReceiver(), timeout, unit);
50  }
51
52  /**
53   * Run a command in the shell on the device. Collects and returns the console output.
54   */
55  public static String doShellReturnString(IDevice device, String cmdline, long timeout,
56      TimeUnit unit) {
57    CollectStringShellOutputReceiver rec = new CollectStringShellOutputReceiver();
58    doShell(device, cmdline, rec, timeout, unit);
59    return rec.toString();
60  }
61
62  /**
63   * Run a command in the shell on the device, directing all output to the given receiver.
64   */
65  public static void doShell(IDevice device, String cmdline, IShellOutputReceiver receiver,
66      long timeout, TimeUnit unit) {
67    try {
68      device.executeShellCommand(cmdline, receiver, timeout, unit);
69    } catch (Exception e) {
70      e.printStackTrace();
71    }
72  }
73
74  /**
75   * Run am start on the device.
76   */
77  public static void doAMStart(IDevice device, String name, String activity) {
78    doShell(device, "am start -n " + name + " /." + activity, 30, TimeUnit.SECONDS);
79  }
80
81  /**
82   * Find the device with the given serial. Give up after the given timeout (in milliseconds).
83   */
84  public static IDevice findDevice(String serial, int timeout) {
85    WaitForDevice wfd = new WaitForDevice(serial, timeout);
86    return wfd.get();
87  }
88
89  /**
90   * Get all devices ddms knows about. Wait at most for the given timeout.
91   */
92  public static IDevice[] findDevices(int timeout) {
93    WaitForDevice wfd = new WaitForDevice(null, timeout);
94    wfd.get();
95    return AndroidDebugBridge.getBridge().getDevices();
96  }
97
98  /**
99   * Return the build type of the given device. This is the value of the "ro.build.type"
100   * system property.
101   */
102  public static String getBuildType(IDevice device) {
103    try {
104      Future<String> buildType = device.getSystemProperty("ro.build.type");
105      return buildType.get(500, TimeUnit.MILLISECONDS);
106    } catch (Exception e) {
107    }
108    return null;
109  }
110
111  /**
112   * Check whether the given device has a pre-optimized boot image. More precisely, checks
113   * whether /system/framework/ * /boot.art exists.
114   */
115  public static boolean hasPrebuiltBootImage(IDevice device) {
116    String ret =
117        doShellReturnString(device, "ls /system/framework/*/boot.art", 500, TimeUnit.MILLISECONDS);
118
119    return !ret.contains("No such file or directory");
120  }
121
122  /**
123   * Remove files involved in a standard build that interfere with collecting data. This will
124   * remove /etc/preloaded-classes, which determines which classes are allocated already in the
125   * boot image. It also deletes any compiled boot image on the device. Then it restarts the
126   * device.
127   *
128   * This is a potentially long-running operation, as the boot after the deletion may take a while.
129   * The method will abort after the given timeout.
130   */
131  public static boolean removePreloaded(IDevice device, long preloadedWaitTimeInSeconds) {
132    String oldContent =
133        DeviceUtils.doShellReturnString(device, "cat /etc/preloaded-classes", 1, TimeUnit.SECONDS);
134    if (oldContent.trim().equals("")) {
135      System.out.println("Preloaded-classes already empty.");
136      return true;
137    }
138
139    // Stop the system server etc.
140    doShell(device, "stop", 100, TimeUnit.MILLISECONDS);
141
142    // Remount /system, delete /etc/preloaded-classes. It would be nice to use "adb remount,"
143    // but AndroidDebugBridge doesn't expose it.
144    doShell(device, "mount -o remount,rw /system", 500, TimeUnit.MILLISECONDS);
145    doShell(device, "rm /etc/preloaded-classes", 100, TimeUnit.MILLISECONDS);
146    // We do need an empty file.
147    doShell(device, "touch /etc/preloaded-classes", 100, TimeUnit.MILLISECONDS);
148
149    // Delete the files in the dalvik cache.
150    doShell(device, "rm /data/dalvik-cache/*/*boot.art", 500, TimeUnit.MILLISECONDS);
151
152    // We'll try to use dev.bootcomplete to know when the system server is back up. But stop
153    // doesn't reset it, so do it manually.
154    doShell(device, "setprop dev.bootcomplete \"0\"", 500, TimeUnit.MILLISECONDS);
155
156    // Start the system server.
157    doShell(device, "start", 100, TimeUnit.MILLISECONDS);
158
159    // Do a loop checking each second whether bootcomplete. Wait for at most the given
160    // threshold.
161    Date startDate = new Date();
162    for (;;) {
163      try {
164        Thread.sleep(1000);
165      } catch (InterruptedException e) {
166        // Ignore spurious wakeup.
167      }
168      // Check whether bootcomplete.
169      String ret =
170          doShellReturnString(device, "getprop dev.bootcomplete", 500, TimeUnit.MILLISECONDS);
171      if (ret.trim().equals("1")) {
172        break;
173      }
174      System.out.println("Still not booted: " + ret);
175
176      // Check whether we timed out. This is a simplistic check that doesn't take into account
177      // things like switches in time.
178      Date endDate = new Date();
179      long seconds =
180          TimeUnit.SECONDS.convert(endDate.getTime() - startDate.getTime(), TimeUnit.MILLISECONDS);
181      if (seconds > preloadedWaitTimeInSeconds) {
182        return false;
183      }
184    }
185
186    return true;
187  }
188
189  /**
190   * Enable method-tracing on device. The system should be restarted after this.
191   */
192  public static void enableTracing(IDevice device) {
193    // Disable selinux.
194    doShell(device, "setenforce 0", 100, TimeUnit.MILLISECONDS);
195
196    // Make the profile directory world-writable.
197    doShell(device, "chmod 777 /data/dalvik-cache/profiles", 100, TimeUnit.MILLISECONDS);
198
199    // Enable streaming method tracing with a small 1K buffer.
200    doShell(device, "setprop dalvik.vm.method-trace true", 100, TimeUnit.MILLISECONDS);
201    doShell(device, "setprop dalvik.vm.method-trace-file "
202                    + "/data/dalvik-cache/profiles/zygote.trace.bin", 100, TimeUnit.MILLISECONDS);
203    doShell(device, "setprop dalvik.vm.method-trace-file-siz 1024", 100, TimeUnit.MILLISECONDS);
204    doShell(device, "setprop dalvik.vm.method-trace-stream true", 100, TimeUnit.MILLISECONDS);
205  }
206
207  private static class NullShellOutputReceiver implements IShellOutputReceiver {
208    @Override
209    public boolean isCancelled() {
210      return false;
211    }
212
213    @Override
214    public void flush() {}
215
216    @Override
217    public void addOutput(byte[] arg0, int arg1, int arg2) {}
218  }
219
220  private static class CollectStringShellOutputReceiver implements IShellOutputReceiver {
221
222    private StringBuilder builder = new StringBuilder();
223
224    @Override
225    public String toString() {
226      String ret = builder.toString();
227      // Strip trailing newlines. They are especially ugly because adb uses DOS line endings.
228      while (ret.endsWith("\r") || ret.endsWith("\n")) {
229        ret = ret.substring(0, ret.length() - 1);
230      }
231      return ret;
232    }
233
234    @Override
235    public void addOutput(byte[] arg0, int arg1, int arg2) {
236      builder.append(new String(arg0, arg1, arg2));
237    }
238
239    @Override
240    public void flush() {}
241
242    @Override
243    public boolean isCancelled() {
244      return false;
245    }
246  }
247
248  private static class WaitForDevice {
249
250    private String serial;
251    private long timeout;
252    private IDevice device;
253
254    public WaitForDevice(String serial, long timeout) {
255      this.serial = serial;
256      this.timeout = timeout;
257      device = null;
258    }
259
260    public IDevice get() {
261      if (device == null) {
262          WaitForDeviceListener wfdl = new WaitForDeviceListener(serial);
263          synchronized (wfdl) {
264              AndroidDebugBridge.addDeviceChangeListener(wfdl);
265
266              // Check whether we already know about this device.
267              IDevice[] devices = AndroidDebugBridge.getBridge().getDevices();
268              if (serial != null) {
269                  for (IDevice d : devices) {
270                      if (serial.equals(d.getSerialNumber())) {
271                          // Only accept if there are clients already. Else wait for the callback informing
272                          // us that we now have clients.
273                          if (d.hasClients()) {
274                              device = d;
275                          }
276
277                          break;
278                      }
279                  }
280              } else {
281                  if (devices.length > 0) {
282                      device = devices[0];
283                  }
284              }
285
286              if (device == null) {
287                  try {
288                      wait(timeout);
289                  } catch (InterruptedException e) {
290                      // Ignore spurious wakeups.
291                  }
292                  device = wfdl.getDevice();
293              }
294
295              AndroidDebugBridge.removeDeviceChangeListener(wfdl);
296          }
297      }
298
299      if (device != null) {
300          // Wait for clients.
301          WaitForClientsListener wfcl = new WaitForClientsListener(device);
302          synchronized (wfcl) {
303              AndroidDebugBridge.addDeviceChangeListener(wfcl);
304
305              if (!device.hasClients()) {
306                  try {
307                      wait(timeout);
308                  } catch (InterruptedException e) {
309                      // Ignore spurious wakeups.
310                  }
311              }
312
313              AndroidDebugBridge.removeDeviceChangeListener(wfcl);
314          }
315      }
316
317      return device;
318    }
319
320    private static class WaitForDeviceListener implements IDeviceChangeListener {
321
322        private String serial;
323        private IDevice device;
324
325        public WaitForDeviceListener(String serial) {
326            this.serial = serial;
327        }
328
329        public IDevice getDevice() {
330            return device;
331        }
332
333        @Override
334        public void deviceChanged(IDevice arg0, int arg1) {
335            // We may get a device changed instead of connected. Handle like a connection.
336            deviceConnected(arg0);
337        }
338
339        @Override
340        public void deviceConnected(IDevice arg0) {
341            if (device != null) {
342                // Ignore updates.
343                return;
344            }
345
346            if (serial == null || serial.equals(arg0.getSerialNumber())) {
347                device = arg0;
348                synchronized (this) {
349                    notifyAll();
350                }
351            }
352        }
353
354        @Override
355        public void deviceDisconnected(IDevice arg0) {
356            // Ignore disconnects.
357        }
358
359    }
360
361    private static class WaitForClientsListener implements IDeviceChangeListener {
362
363        private IDevice myDevice;
364
365        public WaitForClientsListener(IDevice myDevice) {
366            this.myDevice = myDevice;
367        }
368
369        @Override
370        public void deviceChanged(IDevice arg0, int arg1) {
371            if (arg0 == myDevice && (arg1 & IDevice.CHANGE_CLIENT_LIST) != 0) {
372                // Got a client list, done here.
373                synchronized (this) {
374                    notifyAll();
375                }
376            }
377        }
378
379        @Override
380        public void deviceConnected(IDevice arg0) {
381        }
382
383        @Override
384        public void deviceDisconnected(IDevice arg0) {
385        }
386
387    }
388  }
389
390}
391