content: /*
   * Copyright (C) 2007 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.commands.am;
  
  import static android.app.ActivityManager.INSTR_FLAG_DISABLE_HIDDEN_API_CHECKS;
  import static android.app.ActivityManager.INSTR_FLAG_MOUNT_EXTERNAL_STORAGE_FULL;
  
  import android.app.IActivityManager;
  import android.app.IInstrumentationWatcher;
  import android.app.Instrumentation;
  import android.app.UiAutomationConnection;
  import android.content.ComponentName;
  import android.content.pm.IPackageManager;
  import android.content.pm.InstrumentationInfo;
  import android.os.Build;
  import android.os.Bundle;
  import android.os.Environment;
  import android.os.ServiceManager;
  import android.os.UserHandle;
  import android.util.AndroidException;
  import android.util.proto.ProtoOutputStream;
  import android.view.IWindowManager;
  
  import java.io.File;
  import java.io.FileOutputStream;
  import java.io.IOException;
  import java.io.InputStreamReader;
  import java.io.OutputStream;
  import java.text.SimpleDateFormat;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.Date;
  import java.util.List;
  import java.util.Locale;
  
  
  /**
   * Runs the am instrument command
   *
   * Test Result Code:
   * 1 - Test running
   * 0 - Test passed
   * -2 - assertion failure
   * -1 - other exceptions
   *
   * Session Result Code:
   * -1: Success
   * other: Failure
   */
  public class Instrument {
      private static final String TAG = "am";
  
      public static final String DEFAULT_LOG_DIR = "instrument-logs";
  
      private static final int STATUS_TEST_PASSED = 0;
      private static final int STATUS_TEST_STARTED = 1;
      private static final int STATUS_TEST_FAILED_ASSERTION = -1;
      private static final int STATUS_TEST_FAILED_OTHER = -2;
  
      private final IActivityManager mAm;
      private final IPackageManager mPm;
      private final IWindowManager mWm;
  
      // Command line arguments
      public String profileFile = null;
      public boolean wait = false;
      public boolean rawMode = false;
      boolean protoStd = false;  // write proto to stdout
      boolean protoFile = false;  // write proto to a file
      String logPath = null;
      public boolean noWindowAnimation = false;
      public boolean disableHiddenApiChecks = false;
      public boolean disableIsolatedStorage = false;
      public String abi = null;
      public int userId = UserHandle.USER_CURRENT;
      public Bundle args = new Bundle();
      // Required
      public String componentNameArg;
  
      /**
       * Construct the instrument command runner.
       */
      public Instrument(IActivityManager am, IPackageManager pm) {
          mAm = am;
          mPm = pm;
          mWm = IWindowManager.Stub.asInterface(ServiceManager.getService("window"));
      }
  
      /**
       * Base class for status reporting.
       *
       * All the methods on this interface are called within the synchronized block
       * of the InstrumentationWatcher, so calls are in order.  However, that means
       * you must be careful not to do blocking operations because you don't know
       * exactly the locking dependencies.
       */
      private interface StatusReporter {
          /**
           * Status update for tests.
           */
          public void onInstrumentationStatusLocked(ComponentName name, int resultCode,
                  Bundle results);
  
          /**
           * The tests finished.
           */
          public void onInstrumentationFinishedLocked(ComponentName name, int resultCode,
                  Bundle results);
  
          /**
           * @param errorText a description of the error
           * @param commandError True if the error is related to the commandline, as opposed
           *      to a test failing.
           */
          public void onError(String errorText, boolean commandError);
      }
  
      private static Collection<String> sorted(Collection<String> list) {
          final ArrayList<String> copy = new ArrayList<>(list);
          Collections.sort(copy);
          return copy;
      }
  
      /**
       * Printer for the 'classic' text based status reporting.
       */
      private class TextStatusReporter implements StatusReporter {
          private boolean mRawMode;
  
          /**
           * Human-ish readable output.
           *
           * @param rawMode   In "raw mode" (true), all bundles are dumped.
           *                  In "pretty mode" (false), if a bundle includes
           *                  Instrumentation.REPORT_KEY_STREAMRESULT, just print that.
           */
          public TextStatusReporter(boolean rawMode) {
              mRawMode = rawMode;
          }
  
          @Override
          public void onInstrumentationStatusLocked(ComponentName name, int resultCode,
                  Bundle results) {
              // pretty printer mode?
              String pretty = null;
              if (!mRawMode && results != null) {
                  pretty = results.getString(Instrumentation.REPORT_KEY_STREAMRESULT);
              }
              if (pretty != null) {
                  System.out.print(pretty);
              } else {
                  if (results != null) {
                      for (String key : sorted(results.keySet())) {
                          System.out.println(
                                  "INSTRUMENTATION_STATUS: " + key + "=" + results.get(key));
                      }
                  }
                  System.out.println("INSTRUMENTATION_STATUS_CODE: " + resultCode);
              }
          }
  
          @Override
          public void onInstrumentationFinishedLocked(ComponentName name, int resultCode,
                  Bundle results) {
              // pretty printer mode?
              String pretty = null;
              if (!mRawMode && results != null) {
                  pretty = results.getString(Instrumentation.REPORT_KEY_STREAMRESULT);
              }
              if (pretty != null) {
                  System.out.println(pretty);
              } else {
                  if (results != null) {
                      for (String key : sorted(results.keySet())) {
                          System.out.println(
                                  "INSTRUMENTATION_RESULT: " + key + "=" + results.get(key));
                      }
                  }
                  System.out.println("INSTRUMENTATION_CODE: " + resultCode);
              }
          }
  
          @Override
          public void onError(String errorText, boolean commandError) {
              if (mRawMode) {
                  System.out.println("onError: commandError=" + commandError + " message="
                          + errorText);
              }
              // The regular BaseCommand error printing will print the commandErrors.
              if (!commandError) {
                  System.out.println(errorText);
              }
          }
      }
  
      /**
       * Printer for the protobuf based status reporting.
       */
      private class ProtoStatusReporter implements StatusReporter {
  
          private File mLog;
  
          private long mTestStartMs;
  
          ProtoStatusReporter() {
              if (protoFile) {
                  if (logPath == null) {
                      File logDir = new File(Environment.getLegacyExternalStorageDirectory(),
                              DEFAULT_LOG_DIR);
                      if (!logDir.exists() && !logDir.mkdirs()) {
                          System.err.format("Unable to create log directory: %s\n",
                                  logDir.getAbsolutePath());
                          protoFile = false;
                          return;
                      }
                      SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd-hhmmss-SSS", Locale.US);
                      String fileName = String.format("log-%s.instrumentation_data_proto",
                              format.format(new Date()));
                      mLog = new File(logDir, fileName);
                  } else {
                      mLog = new File(Environment.getLegacyExternalStorageDirectory(), logPath);
                      File logDir = mLog.getParentFile();
                      if (!logDir.exists() && !logDir.mkdirs()) {
                          System.err.format("Unable to create log directory: %s\n",
                                  logDir.getAbsolutePath());
                          protoFile = false;
                          return;
                      }
                  }
                  if (mLog.exists()) mLog.delete();
              }
          }
  
          @Override
          public void onInstrumentationStatusLocked(ComponentName name, int resultCode,
                  Bundle results) {
              final ProtoOutputStream proto = new ProtoOutputStream();
  
              final long testStatusToken = proto.start(InstrumentationData.Session.TEST_STATUS);
  
              proto.write(InstrumentationData.TestStatus.RESULT_CODE, resultCode);
              writeBundle(proto, InstrumentationData.TestStatus.RESULTS, results);
  
              if (resultCode == STATUS_TEST_STARTED) {
                  // Logcat -T takes wall clock time (!?)
                  mTestStartMs = System.currentTimeMillis();
              } else {
                  if (mTestStartMs > 0) {
                      proto.write(InstrumentationData.TestStatus.LOGCAT, readLogcat(mTestStartMs));
                  }
                  mTestStartMs = 0;
              }
  
              proto.end(testStatusToken);
  
              outputProto(proto);
          }
  
          @Override
          public void onInstrumentationFinishedLocked(ComponentName name, int resultCode,
                  Bundle results) {
              final ProtoOutputStream proto = new ProtoOutputStream();
  
              final long sessionStatusToken = proto.start(InstrumentationData.Session.SESSION_STATUS);
              proto.write(InstrumentationData.SessionStatus.STATUS_CODE,
                      InstrumentationData.SESSION_FINISHED);
              proto.write(InstrumentationData.SessionStatus.RESULT_CODE, resultCode);
              writeBundle(proto, InstrumentationData.SessionStatus.RESULTS, results);
              proto.end(sessionStatusToken);
  
              outputProto(proto);
          }
  
          @Override
          public void onError(String errorText, boolean commandError) {
              final ProtoOutputStream proto = new ProtoOutputStream();
  
              final long sessionStatusToken = proto.start(InstrumentationData.Session.SESSION_STATUS);
              proto.write(InstrumentationData.SessionStatus.STATUS_CODE,
                      InstrumentationData.SESSION_ABORTED);
              proto.write(InstrumentationData.SessionStatus.ERROR_TEXT, errorText);
              proto.end(sessionStatusToken);
  
              outputProto(proto);
          }
  
          private void writeBundle(ProtoOutputStream proto, long fieldId, Bundle bundle) {
              final long bundleToken = proto.start(fieldId);
  
              for (final String key: sorted(bundle.keySet())) {
                  final long entryToken = proto.startRepeatedObject(
                          InstrumentationData.ResultsBundle.ENTRIES);
  
                  proto.write(InstrumentationData.ResultsBundleEntry.KEY, key);
  
                  final Object val = bundle.get(key);
                  if (val instanceof String) {
                      proto.write(InstrumentationData.ResultsBundleEntry.VALUE_STRING,
                              (String)val);
                  } else if (val instanceof Byte) {
                      proto.write(InstrumentationData.ResultsBundleEntry.VALUE_INT,
                              ((Byte)val).intValue());
                  } else if (val instanceof Double) {
                      proto.write(InstrumentationData.ResultsBundleEntry.VALUE_DOUBLE, (double)val);
                  } else if (val instanceof Float) {
                      proto.write(InstrumentationData.ResultsBundleEntry.VALUE_FLOAT, (float)val);
                  } else if (val instanceof Integer) {
                      proto.write(InstrumentationData.ResultsBundleEntry.VALUE_INT, (int)val);
                  } else if (val instanceof Long) {
                      proto.write(InstrumentationData.ResultsBundleEntry.VALUE_LONG, (long)val);
                  } else if (val instanceof Short) {
                      proto.write(InstrumentationData.ResultsBundleEntry.VALUE_INT, (short)val);
                  } else if (val instanceof Bundle) {
                      writeBundle(proto, InstrumentationData.ResultsBundleEntry.VALUE_BUNDLE,
                              (Bundle)val);
                  } else if (val instanceof byte[]) {
                      proto.write(InstrumentationData.ResultsBundleEntry.VALUE_BYTES, (byte[])val);
                  }
  
                  proto.end(entryToken);
              }
  
              proto.end(bundleToken);
          }
  
          private void outputProto(ProtoOutputStream proto) {
              byte[] out = proto.getBytes();
              if (protoStd) {
                  try {
                      System.out.write(out);
                      System.out.flush();
                  } catch (IOException ex) {
                      System.err.println("Error writing finished response: ");
                      ex.printStackTrace(System.err);
                  }
              }
              if (protoFile) {
                  try (OutputStream os = new FileOutputStream(mLog, true)) {
                      os.write(proto.getBytes());
                      os.flush();
                  } catch (IOException ex) {
                      System.err.format("Cannot write to %s:\n", mLog.getAbsolutePath());
                      ex.printStackTrace();
                  }
              }
          }
      }
  
  
      /**
       * Callbacks from the remote instrumentation instance.
       */
      private class InstrumentationWatcher extends IInstrumentationWatcher.Stub {
          private final StatusReporter mReporter;
  
          private boolean mFinished = false;
  
          public InstrumentationWatcher(StatusReporter reporter) {
              mReporter = reporter;
          }
  
          @Override
          public void instrumentationStatus(ComponentName name, int resultCode, Bundle results) {
              synchronized (this) {
                  mReporter.onInstrumentationStatusLocked(name, resultCode, results);
                  notifyAll();
              }
          }
  
          @Override
          public void instrumentationFinished(ComponentName name, int resultCode, Bundle results) {
              synchronized (this) {
                  mReporter.onInstrumentationFinishedLocked(name, resultCode, results);
                  mFinished = true;
                  notifyAll();
              }
          }
  
          public boolean waitForFinish() {
              synchronized (this) {
                  while (!mFinished) {
                      try {
                          if (!mAm.asBinder().pingBinder()) {
                              return false;
                          }
                          wait(1000);
                      } catch (InterruptedException e) {
                          throw new IllegalStateException(e);
                      }
                  }
              }
              return true;
          }
      }
  
      /**
       * Figure out which component they really meant.
       */
      private ComponentName parseComponentName(String cnArg) throws Exception {
          if (cnArg.contains("/")) {
              ComponentName cn = ComponentName.unflattenFromString(cnArg);
              if (cn == null) throw new IllegalArgumentException("Bad component name: " + cnArg);
              return cn;
          } else {
              List<InstrumentationInfo> infos = mPm.queryInstrumentation(null, 0).getList();
  
              final int numInfos = infos == null ? 0: infos.size();
              ArrayList<ComponentName> cns = new ArrayList<>();
              for (int i = 0; i < numInfos; i++) {
                  InstrumentationInfo info = infos.get(i);
  
                  ComponentName c = new ComponentName(info.packageName, info.name);
                  if (cnArg.equals(info.packageName)) {
                      cns.add(c);
                  }
              }
  
              if (cns.size() == 0) {
                  throw new IllegalArgumentException("No instrumentation found for: " + cnArg);
              } else if (cns.size() == 1) {
                  return cns.get(0);
              } else {
                  StringBuilder cnsStr = new StringBuilder();
                  final int numCns = cns.size();
                  for (int i = 0; i < numCns; i++) {
                      cnsStr.append(cns.get(i).flattenToString());
                      cnsStr.append(", ");
                  }
  
                  // Remove last ", "
                  cnsStr.setLength(cnsStr.length() - 2);
  
                  throw new IllegalArgumentException("Found multiple instrumentations: "
                          + cnsStr.toString());
              }
          }
      }
  
      /**
       * Run the instrumentation.
       */
      public void run() throws Exception {
          StatusReporter reporter = null;
          float[] oldAnims = null;
  
          try {
              // Choose which output we will do.
              if (protoFile || protoStd) {
                  reporter = new ProtoStatusReporter();
              } else if (wait) {
                  reporter = new TextStatusReporter(rawMode);
              }
  
              // Choose whether we have to wait for the results.
              InstrumentationWatcher watcher = null;
              UiAutomationConnection connection = null;
              if (reporter != null) {
                  watcher = new InstrumentationWatcher(reporter);
                  connection = new UiAutomationConnection();
              }
  
              // Set the window animation if necessary
              if (noWindowAnimation) {
                  oldAnims = mWm.getAnimationScales();
                  mWm.setAnimationScale(0, 0.0f);
                  mWm.setAnimationScale(1, 0.0f);
                  mWm.setAnimationScale(2, 0.0f);
              }
  
              // Figure out which component we are trying to do.
              final ComponentName cn = parseComponentName(componentNameArg);
  
              // Choose an ABI if necessary
              if (abi != null) {
                  final String[] supportedAbis = Build.SUPPORTED_ABIS;
                  boolean matched = false;
                  for (String supportedAbi : supportedAbis) {
                      if (supportedAbi.equals(abi)) {
                          matched = true;
                          break;
                      }
                  }
                  if (!matched) {
                      throw new AndroidException(
                              "INSTRUMENTATION_FAILED: Unsupported instruction set " + abi);
                  }
              }
  
              // Start the instrumentation
              int flags = 0;
              if (disableHiddenApiChecks) {
                  flags |= INSTR_FLAG_DISABLE_HIDDEN_API_CHECKS;
              }
              if (disableIsolatedStorage) {
                  flags |= INSTR_FLAG_MOUNT_EXTERNAL_STORAGE_FULL;
              }
              if (!mAm.startInstrumentation(cn, profileFile, flags, args, watcher, connection, userId,
                          abi)) {
                  throw new AndroidException("INSTRUMENTATION_FAILED: " + cn.flattenToString());
              }
  
              // If we have been requested to wait, do so until the instrumentation is finished.
              if (watcher != null) {
                  if (!watcher.waitForFinish()) {
                      reporter.onError("INSTRUMENTATION_ABORTED: System has crashed.", false);
                      return;
                  }
              }
          } catch (Exception ex) {
              // Report failures
              if (reporter != null) {
                  reporter.onError(ex.getMessage(), true);
              }
  
              // And re-throw the exception
              throw ex;
          } finally {
              // Clean up
              if (oldAnims != null) {
                  mWm.setAnimationScales(oldAnims);
              }
          }
      }
  
      private static String readLogcat(long startTimeMs) {
          try {
              // Figure out the timestamp arg for logcat.
              final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
              final String timestamp = format.format(new Date(startTimeMs));
  
              // Start the process
              final Process process = new ProcessBuilder()
                      .command("logcat", "-d", "-v threadtime,uid", "-T", timestamp)
                      .start();
  
              // Nothing to write. Don't let the command accidentally block.
              process.getOutputStream().close();
  
              // Read the output
              final StringBuilder str = new StringBuilder();
              final InputStreamReader reader = new InputStreamReader(process.getInputStream());
              char[] buffer = new char[4096];
              int amt;
              while ((amt = reader.read(buffer, 0, buffer.length)) >= 0) {
                  if (amt > 0) {
                      str.append(buffer, 0, amt);
                  }
              }
  
              try {
                  process.waitFor();
              } catch (InterruptedException ex) {
                  // We already have the text, drop the exception.
              }
  
              return str.toString();
  
          } catch (IOException ex) {
              return "Error reading logcat command:\n" + ex.toString();
          }
      }
  }
  
  