Merge "Fix docs: replace </> with &lt;/&gt; or (/)"
diff --git a/src/com/android/tradefed/device/WifiHelper.java b/src/com/android/tradefed/device/WifiHelper.java
index 06c0fdf..50c2f45 100644
--- a/src/com/android/tradefed/device/WifiHelper.java
+++ b/src/com/android/tradefed/device/WifiHelper.java
@@ -36,7 +36,7 @@
 
     private static final String NULL_IP_ADDR = "0.0.0.0";
     private static final String INSTRUMENTATION_CLASS = ".WifiUtil";
-    static final String INSTRUMENTATION_PKG = "com.android.tradefed.utils.wifi";
+    public static final String INSTRUMENTATION_PKG = "com.android.tradefed.utils.wifi";
     static final String FULL_INSTRUMENTATION_NAME =
             String.format("%s/%s", INSTRUMENTATION_PKG, INSTRUMENTATION_CLASS);
 
@@ -95,7 +95,7 @@
     /**
      * Helper method to extract the wifi util apk from the classpath
      */
-    static File extractWifiUtilApk() throws IOException {
+    public static File extractWifiUtilApk() throws IOException {
         File apkTempFile;
         apkTempFile = FileUtil.createTempFile(WIFIUTIL_APK_NAME, ".apk");
         InputStream apkStream = WifiHelper.class.getResourceAsStream(
diff --git a/src/com/android/tradefed/targetprep/AppSetup.java b/src/com/android/tradefed/targetprep/AppSetup.java
index 8137dd4..176f0a6 100644
--- a/src/com/android/tradefed/targetprep/AppSetup.java
+++ b/src/com/android/tradefed/targetprep/AppSetup.java
@@ -23,12 +23,16 @@
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.AaptParser;
 
+import java.io.File;
 import java.util.HashSet;
 import java.util.Set;
 
 /**
  * A {@link ITargetPreparer} that installs an apk and its tests.
+ * <p/>
+ * Requires 'aapt' on PATH when --uninstall is set
  */
 @OptionClass(alias="app-setup")
 public class AppSetup implements ITargetPreparer, ITargetCleaner {
@@ -39,13 +43,21 @@
     @Option(name = "install", description = "install all apks in build.")
     private boolean mInstall = true;
 
-    @Option(name = "uninstall", description = "uninstall all apks after test completes.")
+    @Option(name = "uninstall", description =
+            "uninstall only apks in build after test completes.")
     private boolean mUninstall = true;
 
+    @Option(name = "uninstall-all", description =
+            "uninstall all unnstallable apks found on device after test completes.")
+    private boolean mUninstallAll = false;
+
     @Option(name = "skip-uninstall-pkg", description =
-            "force retention of this package when --uninstall is set.")
+            "force retention of this package when --uninstall-all is set.")
     private Set<String> mSkipUninstallPkgs = new HashSet<String>();
 
+    /** contains package names of installed apps. Used for uninstall */
+    private Set<String> mInstalledPkgs = new HashSet<String>();
+
     /**
      * {@inheritDoc}
      */
@@ -59,7 +71,7 @@
         CLog.i("Performing setup on %s", device.getSerialNumber());
 
         // double check that device is clean, in case it has unexpected cruft on it
-        if (mUninstall && !uninstallApps(device)) {
+        if (mUninstallAll && !uninstallAllApps(device)) {
             // cannot cleanup device! Bad things may happen in future tests. Take device out
             // of service
             // TODO: in future, consider doing more sophisticated recovery operations
@@ -75,11 +87,27 @@
                             "Failed to install %s on %s. Reason: %s",
                             apkFile.getFile().getName(), device.getSerialNumber(), result));
                 }
+                if (mUninstall && !mUninstallAll) {
+                    addPackageNameToUninstall(apkFile.getFile());
+                }
             }
         }
 
     }
 
+    private void addPackageNameToUninstall(File apkFile) throws TargetSetupError {
+        AaptParser aaptParser = AaptParser.parse(apkFile);
+        if (aaptParser == null) {
+            throw new TargetSetupError(String.format("Failed to extract info from '%s' using aapt",
+                    apkFile.getAbsolutePath()));
+        }
+        if (aaptParser.getPackageName() == null) {
+            throw new TargetSetupError(String.format(
+                    "Failed to find package name for '%s' using aapt", apkFile.getAbsolutePath()));
+        }
+        mInstalledPkgs.add(aaptParser.getPackageName());
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -90,7 +118,16 @@
         if (mReboot) {
             device.reboot();
         }
-        if (mUninstall && !uninstallApps(device)) {
+        if (mUninstall && !mUninstallAll) {
+            for (String pkgName : mInstalledPkgs) {
+                String result = device.uninstallPackage(pkgName);
+                if (result != null) {
+                    CLog.e("Failed to uninstall %s: %s", pkgName, result);
+                    // TODO: consider throwing here
+                }
+            }
+        }
+        if (mUninstallAll && !uninstallAllApps(device)) {
             // cannot cleanup device! Bad things may happen in future tests. Take device out
             // of service
             // TODO: in future, consider doing more sophisticated recovery operations
@@ -99,12 +136,12 @@
         }
     }
 
-    private boolean uninstallApps(ITestDevice device) throws DeviceNotAvailableException {
+    private boolean uninstallAllApps(ITestDevice device) throws DeviceNotAvailableException {
         // make multiple attempts to uninstall apps, aborting if failed
         // TODO: consider moving this to ITestDevice, so more sophisticated recovery attempts
         // can be performed
         for (int i = 0; i < 3; i++) {
-            Set<String> pkgs = getAppsToUninstall(device);
+            Set<String> pkgs = getAllAppsToUninstall(device);
             if (pkgs.isEmpty()) {
                 return true;
             }
@@ -118,10 +155,10 @@
         }
         // check getAppsToUninstall one more time, cause last attempt through loop might have been
         // successful
-        return getAppsToUninstall(device).isEmpty();
+        return getAllAppsToUninstall(device).isEmpty();
     }
 
-    private Set<String> getAppsToUninstall(ITestDevice device) throws DeviceNotAvailableException {
+    private Set<String> getAllAppsToUninstall(ITestDevice device) throws DeviceNotAvailableException {
         Set<String> pkgs = device.getUninstallablePackageNames();
         pkgs.removeAll(mSkipUninstallPkgs);
         return pkgs;
diff --git a/src/com/android/tradefed/testtype/InstalledInstrumentationsTest.java b/src/com/android/tradefed/testtype/InstalledInstrumentationsTest.java
index 0c91d8c..f0155be 100644
--- a/src/com/android/tradefed/testtype/InstalledInstrumentationsTest.java
+++ b/src/com/android/tradefed/testtype/InstalledInstrumentationsTest.java
@@ -85,6 +85,10 @@
             "collector, so use the EACH setting with due caution.")
     private BugreportCollector.Freq mBugreportFrequency = null;
 
+    @Option(name = "class",
+            description = "Only run tests in specified class")
+    private String mTestClass = null;
+
     private List<InstrumentationTest> mTests = null;
 
     /**
@@ -168,6 +172,7 @@
                 sendCoverage(test.getPackageName(), test.getCoverageTarget(), listener);
             }
             test.setDevice(getDevice());
+            test.setClassName(mTestClass);
             test.run(listener);
             // test completed, remove from list
             mTests.remove(0);
diff --git a/src/com/android/tradefed/util/AaptParser.java b/src/com/android/tradefed/util/AaptParser.java
index 2477eab..9bef466 100644
--- a/src/com/android/tradefed/util/AaptParser.java
+++ b/src/com/android/tradefed/util/AaptParser.java
@@ -22,7 +22,7 @@
 import java.util.regex.Pattern;
 
 /**
- * class that extracts info from apk by parsing output of 'aapt dump badging'.
+ * Class that extracts info from apk by parsing output of 'aapt dump badging'.
  * <p/>
  * aapt must be on PATH
  */
@@ -44,13 +44,18 @@
         }
     }
 
+    /**
+     * Parse info from the apk.
+     *
+     * @param apkFile the apk file
+     * @return the {@link AaptParser} or <code>null</code> if failed to extract the information
+     */
     public static AaptParser parse(File apkFile) {
         CommandResult result = RunUtil.getDefault().runTimedCmd(5000, "aapt", "dump", "badging",
                 apkFile.getAbsolutePath());
         if (result.getStatus() == CommandStatus.SUCCESS) {
             AaptParser p = new AaptParser();
             p.parse(result.getStdout());
-
             return p;
         }
         CLog.e("Failed to run aapt on %s", apkFile.getAbsoluteFile());
@@ -60,4 +65,5 @@
     public String getPackageName() {
         return mPackageName;
     }
+
 }
diff --git a/tests/src/com/android/tradefed/targetprep/AppSetupFuncTest.java b/tests/src/com/android/tradefed/targetprep/AppSetupFuncTest.java
new file mode 100644
index 0000000..3bd02a7
--- /dev/null
+++ b/tests/src/com/android/tradefed/targetprep/AppSetupFuncTest.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2013 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.tradefed.targetprep;
+
+import com.android.tradefed.build.AppBuildInfo;
+import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.device.WifiHelper;
+import com.android.tradefed.testtype.DeviceTestCase;
+import com.android.tradefed.util.FileUtil;
+
+import java.io.File;
+
+/**
+ * A functional test for {@link AppSetup}.
+ * <p/>
+ * Relies on WifiUtil.apk in tradefed.jar.
+ * <p/>
+ * 'aapt' must be in PATH.
+ */
+public class AppSetupFuncTest extends DeviceTestCase {
+
+    /**
+     * Test end to end normal case for {@link AppSetup}.
+     */
+    public void testSetupTeardown() throws Exception {
+        // use wifiutil as a test apk since it already exists
+        getDevice().uninstallPackage(WifiHelper.INSTRUMENTATION_PKG);
+        File wifiapk = WifiHelper.extractWifiUtilApk();
+        try {
+            AppBuildInfo appBuild = new AppBuildInfo("0", "stub", "stub");
+            appBuild.addAppPackageFile(wifiapk, "0");
+            AppSetup appSetup = new AppSetup();
+            // turn off reboot to reduce test execution time
+            OptionSetter optionSetter = new OptionSetter(appSetup);
+            optionSetter.setOptionValue("reboot", "false");
+            appSetup.setUp(getDevice(), appBuild);
+            assertTrue(getDevice().getUninstallablePackageNames().contains(
+                    WifiHelper.INSTRUMENTATION_PKG));
+            appSetup.tearDown(getDevice(), appBuild, null);
+            assertFalse(getDevice().getUninstallablePackageNames().contains(
+                    WifiHelper.INSTRUMENTATION_PKG));
+        } finally {
+            FileUtil.deleteFile(wifiapk);
+        }
+    }
+}