Hook up the content provider pushFile

- Use appops to write to legacy location.
- Disable logging of Tokenizer, it's too verbose
- PushFile to CP which write them to sdcard instead.

Bug: 123529934
Test: unit tests
./cts-tradefed run cts-dev -m CtsBionic (replacing path with sdcard)
Bug: 123527583
Bug: 121325876

Change-Id: I29f69aaa14cacf0a2f73f40082a267873a00a359
Merged-In: I29f69aaa14cacf0a2f73f40082a267873a00a359
diff --git a/src/com/android/tradefed/device/NativeDevice.java b/src/com/android/tradefed/device/NativeDevice.java
index 12c3a6a..5ee7a17 100644
--- a/src/com/android/tradefed/device/NativeDevice.java
+++ b/src/com/android/tradefed/device/NativeDevice.java
@@ -35,6 +35,7 @@
 import com.android.tradefed.build.IBuildInfo;
 import com.android.tradefed.command.remote.DeviceDescriptor;
 import com.android.tradefed.config.GlobalConfiguration;
+import com.android.tradefed.device.contentprovider.ContentProviderHandler;
 import com.android.tradefed.host.IHostOptions;
 import com.android.tradefed.log.ITestLogger;
 import com.android.tradefed.log.LogUtil.CLog;
@@ -96,6 +97,7 @@
  */
 public class NativeDevice implements IManagedTestDevice {
 
+    private static final String SD_CARD = "/sdcard/";
     /**
      * Allow pauses of up to 2 minutes while receiving bugreport.
      * <p/>
@@ -205,6 +207,9 @@
     private String mLastConnectedWifiPsk = null;
     private boolean mNetworkMonitorEnabled = false;
 
+    private ContentProviderHandler mContentProvider = null;
+    private boolean mShouldSkipContentProviderSetup = false;
+
     /**
      * Interface for a generic device communication attempt.
      */
@@ -1018,6 +1023,14 @@
     @Override
     public boolean pushFile(final File localFile, final String remoteFilePath)
             throws DeviceNotAvailableException {
+        if (remoteFilePath.startsWith(SD_CARD)) {
+            ContentProviderHandler handler = getContentProvider();
+            if (handler != null) {
+                mShouldSkipContentProviderSetup = true;
+                return handler.pushFile(localFile, remoteFilePath);
+            }
+        }
+
         DeviceAction pushAction =
                 new DeviceAction() {
                     @Override
@@ -1794,7 +1807,11 @@
     /** Builds the OS command for the given adb shell command session and args */
     private String[] buildAdbShellCommand(String command) {
         // TODO: implement the shell v2 support in ddmlib itself.
-        String[] commandArgs = QuotationAwareTokenizer.tokenizeLine(command);
+        String[] commandArgs =
+                QuotationAwareTokenizer.tokenizeLine(
+                        command,
+                        /** No logging */
+                        false);
         return ArrayUtil.buildArray(
                 new String[] {"adb", "-s", getSerialNumber(), "shell"}, commandArgs);
     }
@@ -3869,7 +3886,18 @@
      */
     @Override
     public void postInvocationTearDown() {
-        // Default implementation empty on purpose
+        // Default implementation
+        if (getIDevice() instanceof StubDevice) {
+            return;
+        }
+        try {
+            ContentProviderHandler handler = getContentProvider();
+            if (handler != null) {
+                handler.tearDown();
+            }
+        } catch (DeviceNotAvailableException e) {
+            CLog.e(e);
+        }
     }
 
     /**
@@ -4103,4 +4131,19 @@
     IHostOptions getHostOptions() {
         return GlobalConfiguration.getInstance().getHostOptions();
     }
+
+    /** Returns the {@link ContentProviderHandler} or null if not available. */
+    @VisibleForTesting
+    ContentProviderHandler getContentProvider() throws DeviceNotAvailableException {
+        if (mContentProvider == null) {
+            mContentProvider = new ContentProviderHandler(this);
+        }
+        if (!mShouldSkipContentProviderSetup) {
+            boolean res = mContentProvider.setUp();
+            if (!res) {
+                return null;
+            }
+        }
+        return mContentProvider;
+    }
 }
diff --git a/src/com/android/tradefed/device/contentprovider/ContentProviderHandler.java b/src/com/android/tradefed/device/contentprovider/ContentProviderHandler.java
index a245dd8..2647364 100644
--- a/src/com/android/tradefed/device/contentprovider/ContentProviderHandler.java
+++ b/src/com/android/tradefed/device/contentprovider/ContentProviderHandler.java
@@ -56,22 +56,33 @@
      *
      * @return True if ready to be used, False otherwise.
      */
-    public boolean setUp() throws DeviceNotAvailableException, IOException {
+    public boolean setUp() throws DeviceNotAvailableException {
         Set<String> packageNames = mDevice.getInstalledPackageNames();
         if (packageNames.contains(PACKAGE_NAME)) {
             return true;
         }
         if (mContentProviderApk == null) {
-            mContentProviderApk = extractResourceApk();
+            try {
+                mContentProviderApk = extractResourceApk();
+            } catch (IOException e) {
+                CLog.e(e);
+                return false;
+            }
         }
         // Install package for all users
-        String output = mDevice.installPackage(mContentProviderApk, true, true);
-        if (output == null) {
-            return true;
+        String output =
+                mDevice.installPackage(
+                        mContentProviderApk,
+                        /** reinstall */
+                        true,
+                        /** grant permission */
+                        true);
+        if (output != null) {
+            CLog.e("Something went wrong while installing the content provider apk: %s", output);
+            FileUtil.deleteFile(mContentProviderApk);
+            return false;
         }
-        CLog.e("Something went wrong while installing the content provider apk: %s", output);
-        FileUtil.deleteFile(mContentProviderApk);
-        return false;
+        return true;
     }
 
     /** Clean the device from the content provider helper. */
diff --git a/src/com/android/tradefed/util/QuotationAwareTokenizer.java b/src/com/android/tradefed/util/QuotationAwareTokenizer.java
index e72afa7..eaf7377 100644
--- a/src/com/android/tradefed/util/QuotationAwareTokenizer.java
+++ b/src/com/android/tradefed/util/QuotationAwareTokenizer.java
@@ -25,34 +25,40 @@
     private static final String LOG_TAG = "TOKEN";
 
     /**
-     * Tokenizes the string, splitting on specified delimiter.  Does not split between consecutive,
+     * Tokenizes the string, splitting on specified delimiter. Does not split between consecutive,
      * unquoted double-quote marks.
-     * <p/>
-     * How the tokenizer works:
+     *
+     * <p>How the tokenizer works:
+     *
      * <ol>
-     *     <li> Split the string into "characters" where each "character" is either an escaped
-     *          character like \" (that is, "\\\"") or a single real character like f (just "f").
-     *     <li> For each "character"
-     *     <ol>
+     *   <li> Split the string into "characters" where each "character" is either an escaped
+     *       character like \" (that is, "\\\"") or a single real character like f (just "f").
+     *   <li> For each "character"
+     *       <ol>
      *         <li> If it's a space, finish a token unless we're being quoted
      *         <li> If it's a quotation mark, flip the "we're being quoted" bit
      *         <li> Otherwise, add it to the token being built
-     *     </ol>
-     *     <li> At EOL, we typically haven't added the final token to the (tokens) {@link ArrayList}
-     *     <ol>
+     *       </ol>
+     *
+     *   <li> At EOL, we typically haven't added the final token to the (tokens) {@link ArrayList}
+     *       <ol>
      *         <li> If the last "character" is an escape character, throw an exception; that's not
-     *              valid
+     *             valid
      *         <li> If we're in the middle of a quotation, throw an exception; that's not valid
      *         <li> Otherwise, add the final token to (tokens)
-     *     </ol>
-     *     <li> Return a String[] version of (tokens)
+     *       </ol>
+     *
+     *   <li> Return a String[] version of (tokens)
      * </ol>
      *
      * @param line A {@link String} to be tokenized
+     * @param delim the delimiter to split on
+     * @param logging whether or not to log operations
      * @return A tokenized version of the string
      * @throws IllegalArgumentException if the line cannot be parsed
      */
-    public static String[] tokenizeLine(String line, String delim) throws IllegalArgumentException {
+    public static String[] tokenizeLine(String line, String delim, boolean logging)
+            throws IllegalArgumentException {
         if (line == null) {
             throw new IllegalArgumentException("line is null");
         }
@@ -65,7 +71,7 @@
         String aChar = "";
         boolean quotation = false;
 
-        Log.d(LOG_TAG, String.format("Trying to tokenize the line '%s'", line));
+        log(String.format("Trying to tokenize the line '%s'", line), logging);
         while (charMatcher.find()) {
             aChar = charMatcher.group();
 
@@ -77,7 +83,7 @@
                     if (token.length() > 0) {
                         // this is the end of a non-empty token; dump it in our list of tokens,
                         // clear our temp storage, and keep rolling
-                        Log.d(LOG_TAG, String.format("Finished token '%s'", token.toString()));
+                        log(String.format("Finished token '%s'", token.toString()), logging);
                         tokens.add(token.toString());
                         token.delete(0, token.length());
                     }
@@ -85,7 +91,7 @@
                 }
             } else if ("\"".equals(aChar)) {
                 // unescaped quotation mark; flip quotation state
-                Log.v(LOG_TAG, "Flipped quotation state");
+                log("Flipped quotation state", logging);
                 quotation ^= true;
             } else {
                 // default case: add the character to the token being built
@@ -101,7 +107,7 @@
 
         // Add the final token to the tokens array.
         if (token.length() > 0) {
-            Log.v(LOG_TAG, String.format("Finished final token '%s'", token.toString()));
+            log(String.format("Finished final token '%s'", token.toString()), logging);
             tokens.add(token.toString());
             token.delete(0, token.length());
         }
@@ -117,7 +123,22 @@
      * See also {@link #tokenizeLine(String, String)}
      */
     public static String[] tokenizeLine(String line) throws IllegalArgumentException {
-        return tokenizeLine(line, " ");
+        return tokenizeLine(line, " ", true);
+    }
+
+    public static String[] tokenizeLine(String line, String delim) throws IllegalArgumentException {
+        return tokenizeLine(line, delim, true);
+    }
+
+    /**
+     * Tokenizes the string, splitting on spaces. Does not split between consecutive, unquoted
+     * double-quote marks.
+     *
+     * <p>See also {@link #tokenizeLine(String, String)}
+     */
+    public static String[] tokenizeLine(String line, boolean logging)
+            throws IllegalArgumentException {
+        return tokenizeLine(line, " ", logging);
     }
 
     /**
@@ -147,4 +168,10 @@
         }
         return sb.toString();
     }
+
+    private static void log(String message, boolean display) {
+        if (display) {
+            Log.v(LOG_TAG, message);
+        }
+    }
 }
diff --git a/tests/src/com/android/tradefed/device/TestDeviceTest.java b/tests/src/com/android/tradefed/device/TestDeviceTest.java
index 9ca9df7..ecb4c42 100644
--- a/tests/src/com/android/tradefed/device/TestDeviceTest.java
+++ b/tests/src/com/android/tradefed/device/TestDeviceTest.java
@@ -29,6 +29,7 @@
 import com.android.tradefed.config.OptionSetter;
 import com.android.tradefed.device.ITestDevice.MountPointInfo;
 import com.android.tradefed.device.ITestDevice.RecoveryMode;
+import com.android.tradefed.device.contentprovider.ContentProviderHandler;
 import com.android.tradefed.host.HostOptions;
 import com.android.tradefed.host.IHostOptions;
 import com.android.tradefed.log.LogUtil.CLog;
@@ -3756,6 +3757,11 @@
                             throws DeviceNotAvailableException {
                         return mMockWifi;
                     }
+
+                    @Override
+                    ContentProviderHandler getContentProvider() throws DeviceNotAvailableException {
+                        return null;
+                    }
                 };
         mMockIDevice.executeShellCommand(
                 EasyMock.eq("dumpsys package com.android.tradefed.utils.wifi"),
diff --git a/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java b/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java
index 2c1395f..84ec7bc 100644
--- a/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java
+++ b/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java
@@ -55,13 +55,13 @@
         mProvider.tearDown();
     }
 
+    /** Test the install flow. */
     @Test
     public void testSetUp_install() throws Exception {
         Set<String> set = new HashSet<>();
         doReturn(set).when(mMockDevice).getInstalledPackageNames();
         doReturn(1).when(mMockDevice).getCurrentUser();
         doReturn(null).when(mMockDevice).installPackage(any(), eq(true), eq(true));
-
         assertTrue(mProvider.setUp());
     }