1 /*
2  * Copyright (C) 2007 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 
17 package com.android.commands.am;
18 
19 import android.app.IActivityManager;
20 import android.app.IInstrumentationWatcher;
21 import android.app.Instrumentation;
22 import android.app.UiAutomationConnection;
23 import android.content.ComponentName;
24 import android.content.pm.IPackageManager;
25 import android.content.pm.InstrumentationInfo;
26 import android.os.Build;
27 import android.os.Bundle;
28 import android.os.Environment;
29 import android.os.ServiceManager;
30 import android.os.UserHandle;
31 import android.util.AndroidException;
32 import android.util.proto.ProtoOutputStream;
33 import android.view.IWindowManager;
34 
35 import java.io.File;
36 import java.io.FileOutputStream;
37 import java.io.IOException;
38 import java.io.OutputStream;
39 import java.text.SimpleDateFormat;
40 import java.util.ArrayList;
41 import java.util.Collection;
42 import java.util.Collections;
43 import java.util.Date;
44 import java.util.List;
45 import java.util.Locale;
46 
47 
48 /**
49  * Runs the am instrument command
50  *
51  * Test Result Code:
52  * 1 - Test running
53  * 0 - Test passed
54  * -2 - assertion failure
55  * -1 - other exceptions
56  *
57  * Session Result Code:
58  * -1: Success
59  * other: Failure
60  */
61 public class Instrument {
62     public static final String DEFAULT_LOG_DIR = "instrument-logs";
63 
64     private final IActivityManager mAm;
65     private final IPackageManager mPm;
66     private final IWindowManager mWm;
67 
68     // Command line arguments
69     public String profileFile = null;
70     public boolean wait = false;
71     public boolean rawMode = false;
72     boolean protoStd = false;  // write proto to stdout
73     boolean protoFile = false;  // write proto to a file
74     String logPath = null;
75     public boolean noWindowAnimation = false;
76     public boolean disableHiddenApiChecks = false;
77     public String abi = null;
78     public int userId = UserHandle.USER_CURRENT;
79     public Bundle args = new Bundle();
80     // Required
81     public String componentNameArg;
82 
83     // Disable hidden API checks for the newly started instrumentation.
84     // Must be kept in sync with ActivityManagerService.
85     private static final int INSTRUMENTATION_FLAG_DISABLE_HIDDEN_API_CHECKS = 1 << 0;
86 
87     /**
88      * Construct the instrument command runner.
89      */
Instrument(IActivityManager am, IPackageManager pm)90     public Instrument(IActivityManager am, IPackageManager pm) {
91         mAm = am;
92         mPm = pm;
93         mWm = IWindowManager.Stub.asInterface(ServiceManager.getService("window"));
94     }
95 
96     /**
97      * Base class for status reporting.
98      *
99      * All the methods on this interface are called within the synchronized block
100      * of the InstrumentationWatcher, so calls are in order.  However, that means
101      * you must be careful not to do blocking operations because you don't know
102      * exactly the locking dependencies.
103      */
104     private interface StatusReporter {
105         /**
106          * Status update for tests.
107          */
onInstrumentationStatusLocked(ComponentName name, int resultCode, Bundle results)108         public void onInstrumentationStatusLocked(ComponentName name, int resultCode,
109                 Bundle results);
110 
111         /**
112          * The tests finished.
113          */
onInstrumentationFinishedLocked(ComponentName name, int resultCode, Bundle results)114         public void onInstrumentationFinishedLocked(ComponentName name, int resultCode,
115                 Bundle results);
116 
117         /**
118          * @param errorText a description of the error
119          * @param commandError True if the error is related to the commandline, as opposed
120          *      to a test failing.
121          */
onError(String errorText, boolean commandError)122         public void onError(String errorText, boolean commandError);
123     }
124 
sorted(Collection<String> list)125     private static Collection<String> sorted(Collection<String> list) {
126         final ArrayList<String> copy = new ArrayList<>(list);
127         Collections.sort(copy);
128         return copy;
129     }
130 
131     /**
132      * Printer for the 'classic' text based status reporting.
133      */
134     private class TextStatusReporter implements StatusReporter {
135         private boolean mRawMode;
136 
137         /**
138          * Human-ish readable output.
139          *
140          * @param rawMode   In "raw mode" (true), all bundles are dumped.
141          *                  In "pretty mode" (false), if a bundle includes
142          *                  Instrumentation.REPORT_KEY_STREAMRESULT, just print that.
143          */
TextStatusReporter(boolean rawMode)144         public TextStatusReporter(boolean rawMode) {
145             mRawMode = rawMode;
146         }
147 
148         @Override
onInstrumentationStatusLocked(ComponentName name, int resultCode, Bundle results)149         public void onInstrumentationStatusLocked(ComponentName name, int resultCode,
150                 Bundle results) {
151             // pretty printer mode?
152             String pretty = null;
153             if (!mRawMode && results != null) {
154                 pretty = results.getString(Instrumentation.REPORT_KEY_STREAMRESULT);
155             }
156             if (pretty != null) {
157                 System.out.print(pretty);
158             } else {
159                 if (results != null) {
160                     for (String key : sorted(results.keySet())) {
161                         System.out.println(
162                                 "INSTRUMENTATION_STATUS: " + key + "=" + results.get(key));
163                     }
164                 }
165                 System.out.println("INSTRUMENTATION_STATUS_CODE: " + resultCode);
166             }
167         }
168 
169         @Override
onInstrumentationFinishedLocked(ComponentName name, int resultCode, Bundle results)170         public void onInstrumentationFinishedLocked(ComponentName name, int resultCode,
171                 Bundle results) {
172             // pretty printer mode?
173             String pretty = null;
174             if (!mRawMode && results != null) {
175                 pretty = results.getString(Instrumentation.REPORT_KEY_STREAMRESULT);
176             }
177             if (pretty != null) {
178                 System.out.println(pretty);
179             } else {
180                 if (results != null) {
181                     for (String key : sorted(results.keySet())) {
182                         System.out.println(
183                                 "INSTRUMENTATION_RESULT: " + key + "=" + results.get(key));
184                     }
185                 }
186                 System.out.println("INSTRUMENTATION_CODE: " + resultCode);
187             }
188         }
189 
190         @Override
onError(String errorText, boolean commandError)191         public void onError(String errorText, boolean commandError) {
192             if (mRawMode) {
193                 System.out.println("onError: commandError=" + commandError + " message="
194                         + errorText);
195             }
196             // The regular BaseCommand error printing will print the commandErrors.
197             if (!commandError) {
198                 System.out.println(errorText);
199             }
200         }
201     }
202 
203     /**
204      * Printer for the protobuf based status reporting.
205      */
206     private class ProtoStatusReporter implements StatusReporter {
207 
208         private File mLog;
209 
ProtoStatusReporter()210         ProtoStatusReporter() {
211             if (protoFile) {
212                 if (logPath == null) {
213                     File logDir = new File(Environment.getLegacyExternalStorageDirectory(),
214                             DEFAULT_LOG_DIR);
215                     if (!logDir.exists() && !logDir.mkdirs()) {
216                         System.err.format("Unable to create log directory: %s\n",
217                                 logDir.getAbsolutePath());
218                         protoFile = false;
219                         return;
220                     }
221                     SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd-hhmmss-SSS", Locale.US);
222                     String fileName = String.format("log-%s.instrumentation_data_proto",
223                             format.format(new Date()));
224                     mLog = new File(logDir, fileName);
225                 } else {
226                     mLog = new File(Environment.getLegacyExternalStorageDirectory(), logPath);
227                     File logDir = mLog.getParentFile();
228                     if (!logDir.exists() && !logDir.mkdirs()) {
229                         System.err.format("Unable to create log directory: %s\n",
230                                 logDir.getAbsolutePath());
231                         protoFile = false;
232                         return;
233                     }
234                 }
235                 if (mLog.exists()) mLog.delete();
236             }
237         }
238 
239         @Override
onInstrumentationStatusLocked(ComponentName name, int resultCode, Bundle results)240         public void onInstrumentationStatusLocked(ComponentName name, int resultCode,
241                 Bundle results) {
242             final ProtoOutputStream proto = new ProtoOutputStream();
243 
244             final long token = proto.start(InstrumentationData.Session.TEST_STATUS);
245             proto.write(InstrumentationData.TestStatus.RESULT_CODE, resultCode);
246             writeBundle(proto, InstrumentationData.TestStatus.RESULTS, results);
247             proto.end(token);
248 
249             outputProto(proto);
250         }
251 
252         @Override
onInstrumentationFinishedLocked(ComponentName name, int resultCode, Bundle results)253         public void onInstrumentationFinishedLocked(ComponentName name, int resultCode,
254                 Bundle results) {
255             final ProtoOutputStream proto = new ProtoOutputStream();
256 
257             final long token = proto.start(InstrumentationData.Session.SESSION_STATUS);
258             proto.write(InstrumentationData.SessionStatus.STATUS_CODE,
259                     InstrumentationData.SESSION_FINISHED);
260             proto.write(InstrumentationData.SessionStatus.RESULT_CODE, resultCode);
261             writeBundle(proto, InstrumentationData.SessionStatus.RESULTS, results);
262             proto.end(token);
263 
264             outputProto(proto);
265         }
266 
267         @Override
onError(String errorText, boolean commandError)268         public void onError(String errorText, boolean commandError) {
269             final ProtoOutputStream proto = new ProtoOutputStream();
270 
271             final long token = proto.start(InstrumentationData.Session.SESSION_STATUS);
272             proto.write(InstrumentationData.SessionStatus.STATUS_CODE,
273                     InstrumentationData.SESSION_ABORTED);
274             proto.write(InstrumentationData.SessionStatus.ERROR_TEXT, errorText);
275             proto.end(token);
276 
277             outputProto(proto);
278         }
279 
writeBundle(ProtoOutputStream proto, long fieldId, Bundle bundle)280         private void writeBundle(ProtoOutputStream proto, long fieldId, Bundle bundle) {
281             final long bundleToken = proto.start(fieldId);
282 
283             for (final String key: sorted(bundle.keySet())) {
284                 final long entryToken = proto.startRepeatedObject(
285                         InstrumentationData.ResultsBundle.ENTRIES);
286 
287                 proto.write(InstrumentationData.ResultsBundleEntry.KEY, key);
288 
289                 final Object val = bundle.get(key);
290                 if (val instanceof String) {
291                     proto.write(InstrumentationData.ResultsBundleEntry.VALUE_STRING,
292                             (String)val);
293                 } else if (val instanceof Byte) {
294                     proto.write(InstrumentationData.ResultsBundleEntry.VALUE_INT,
295                             ((Byte)val).intValue());
296                 } else if (val instanceof Double) {
297                     proto.write(InstrumentationData.ResultsBundleEntry.VALUE_DOUBLE, (double)val);
298                 } else if (val instanceof Float) {
299                     proto.write(InstrumentationData.ResultsBundleEntry.VALUE_FLOAT, (float)val);
300                 } else if (val instanceof Integer) {
301                     proto.write(InstrumentationData.ResultsBundleEntry.VALUE_INT, (int)val);
302                 } else if (val instanceof Long) {
303                     proto.write(InstrumentationData.ResultsBundleEntry.VALUE_LONG, (long)val);
304                 } else if (val instanceof Short) {
305                     proto.write(InstrumentationData.ResultsBundleEntry.VALUE_INT, (short)val);
306                 } else if (val instanceof Bundle) {
307                     writeBundle(proto, InstrumentationData.ResultsBundleEntry.VALUE_BUNDLE,
308                             (Bundle)val);
309                 } else if (val instanceof byte[]) {
310                     proto.write(InstrumentationData.ResultsBundleEntry.VALUE_BYTES, (byte[])val);
311                 }
312 
313                 proto.end(entryToken);
314             }
315 
316             proto.end(bundleToken);
317         }
318 
outputProto(ProtoOutputStream proto)319         private void outputProto(ProtoOutputStream proto) {
320             byte[] out = proto.getBytes();
321             if (protoStd) {
322                 try {
323                     System.out.write(out);
324                     System.out.flush();
325                 } catch (IOException ex) {
326                     System.err.println("Error writing finished response: ");
327                     ex.printStackTrace(System.err);
328                 }
329             }
330             if (protoFile) {
331                 try (OutputStream os = new FileOutputStream(mLog, true)) {
332                     os.write(proto.getBytes());
333                     os.flush();
334                 } catch (IOException ex) {
335                     System.err.format("Cannot write to %s:\n", mLog.getAbsolutePath());
336                     ex.printStackTrace();
337                 }
338             }
339         }
340     }
341 
342 
343     /**
344      * Callbacks from the remote instrumentation instance.
345      */
346     private class InstrumentationWatcher extends IInstrumentationWatcher.Stub {
347         private final StatusReporter mReporter;
348 
349         private boolean mFinished = false;
350 
InstrumentationWatcher(StatusReporter reporter)351         public InstrumentationWatcher(StatusReporter reporter) {
352             mReporter = reporter;
353         }
354 
355         @Override
instrumentationStatus(ComponentName name, int resultCode, Bundle results)356         public void instrumentationStatus(ComponentName name, int resultCode, Bundle results) {
357             synchronized (this) {
358                 mReporter.onInstrumentationStatusLocked(name, resultCode, results);
359                 notifyAll();
360             }
361         }
362 
363         @Override
instrumentationFinished(ComponentName name, int resultCode, Bundle results)364         public void instrumentationFinished(ComponentName name, int resultCode, Bundle results) {
365             synchronized (this) {
366                 mReporter.onInstrumentationFinishedLocked(name, resultCode, results);
367                 mFinished = true;
368                 notifyAll();
369             }
370         }
371 
waitForFinish()372         public boolean waitForFinish() {
373             synchronized (this) {
374                 while (!mFinished) {
375                     try {
376                         if (!mAm.asBinder().pingBinder()) {
377                             return false;
378                         }
379                         wait(1000);
380                     } catch (InterruptedException e) {
381                         throw new IllegalStateException(e);
382                     }
383                 }
384             }
385             return true;
386         }
387     }
388 
389     /**
390      * Figure out which component they really meant.
391      */
parseComponentName(String cnArg)392     private ComponentName parseComponentName(String cnArg) throws Exception {
393         if (cnArg.contains("/")) {
394             ComponentName cn = ComponentName.unflattenFromString(cnArg);
395             if (cn == null) throw new IllegalArgumentException("Bad component name: " + cnArg);
396             return cn;
397         } else {
398             List<InstrumentationInfo> infos = mPm.queryInstrumentation(null, 0).getList();
399 
400             final int numInfos = infos == null ? 0: infos.size();
401             ArrayList<ComponentName> cns = new ArrayList<>();
402             for (int i = 0; i < numInfos; i++) {
403                 InstrumentationInfo info = infos.get(i);
404 
405                 ComponentName c = new ComponentName(info.packageName, info.name);
406                 if (cnArg.equals(info.packageName)) {
407                     cns.add(c);
408                 }
409             }
410 
411             if (cns.size() == 0) {
412                 throw new IllegalArgumentException("No instrumentation found for: " + cnArg);
413             } else if (cns.size() == 1) {
414                 return cns.get(0);
415             } else {
416                 StringBuilder cnsStr = new StringBuilder();
417                 final int numCns = cns.size();
418                 for (int i = 0; i < numCns; i++) {
419                     cnsStr.append(cns.get(i).flattenToString());
420                     cnsStr.append(", ");
421                 }
422 
423                 // Remove last ", "
424                 cnsStr.setLength(cnsStr.length() - 2);
425 
426                 throw new IllegalArgumentException("Found multiple instrumentations: "
427                         + cnsStr.toString());
428             }
429         }
430     }
431 
432     /**
433      * Run the instrumentation.
434      */
run()435     public void run() throws Exception {
436         StatusReporter reporter = null;
437         float[] oldAnims = null;
438 
439         try {
440             // Choose which output we will do.
441             if (protoFile || protoStd) {
442                 reporter = new ProtoStatusReporter();
443             } else if (wait) {
444                 reporter = new TextStatusReporter(rawMode);
445             }
446 
447             // Choose whether we have to wait for the results.
448             InstrumentationWatcher watcher = null;
449             UiAutomationConnection connection = null;
450             if (reporter != null) {
451                 watcher = new InstrumentationWatcher(reporter);
452                 connection = new UiAutomationConnection();
453             }
454 
455             // Set the window animation if necessary
456             if (noWindowAnimation) {
457                 oldAnims = mWm.getAnimationScales();
458                 mWm.setAnimationScale(0, 0.0f);
459                 mWm.setAnimationScale(1, 0.0f);
460                 mWm.setAnimationScale(2, 0.0f);
461             }
462 
463             // Figure out which component we are trying to do.
464             final ComponentName cn = parseComponentName(componentNameArg);
465 
466             // Choose an ABI if necessary
467             if (abi != null) {
468                 final String[] supportedAbis = Build.SUPPORTED_ABIS;
469                 boolean matched = false;
470                 for (String supportedAbi : supportedAbis) {
471                     if (supportedAbi.equals(abi)) {
472                         matched = true;
473                         break;
474                     }
475                 }
476                 if (!matched) {
477                     throw new AndroidException(
478                             "INSTRUMENTATION_FAILED: Unsupported instruction set " + abi);
479                 }
480             }
481 
482             // Start the instrumentation
483             int flags = disableHiddenApiChecks ? INSTRUMENTATION_FLAG_DISABLE_HIDDEN_API_CHECKS : 0;
484             if (!mAm.startInstrumentation(cn, profileFile, flags, args, watcher, connection, userId,
485                         abi)) {
486                 throw new AndroidException("INSTRUMENTATION_FAILED: " + cn.flattenToString());
487             }
488 
489             // If we have been requested to wait, do so until the instrumentation is finished.
490             if (watcher != null) {
491                 if (!watcher.waitForFinish()) {
492                     reporter.onError("INSTRUMENTATION_ABORTED: System has crashed.", false);
493                     return;
494                 }
495             }
496         } catch (Exception ex) {
497             // Report failures
498             if (reporter != null) {
499                 reporter.onError(ex.getMessage(), true);
500             }
501 
502             // And re-throw the exception
503             throw ex;
504         } finally {
505             // Clean up
506             if (oldAnims != null) {
507                 mWm.setAnimationScales(oldAnims);
508             }
509         }
510     }
511 }
512 
513