Initial host-side handler for the content provider

Drop the first version of the ContentProvider and a very
basic host side handler to start it out.

Next follow up will be to start using it in device interaction.

Bug: 123529934
Test: unit tests
Bug: 121327547
Bug: 121326701
Change-Id: I701e801659c074dbc0fe00a14f940987e4bb53e0
Merged-In: I701e801659c074dbc0fe00a14f940987e4bb53e0
diff --git a/res/apks/contentprovider/MODULE_LICENSE_APL b/res/apks/contentprovider/MODULE_LICENSE_APL
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/res/apks/contentprovider/MODULE_LICENSE_APL
diff --git a/res/apks/contentprovider/PREBUILT b/res/apks/contentprovider/PREBUILT
new file mode 100644
index 0000000..84ae85c
--- /dev/null
+++ b/res/apks/contentprovider/PREBUILT
@@ -0,0 +1,4 @@
+This apk can be rebuilt from
+        platform/tools/tradefederation/core/util-apps/ContentProvider
+
+By running `m TradefedContentProvider` on revision b2b676a778864281572ec0c252623562be9c3c2b
diff --git a/res/apks/contentprovider/TradefedContentProvider.apk b/res/apks/contentprovider/TradefedContentProvider.apk
new file mode 100644
index 0000000..49472b2
--- /dev/null
+++ b/res/apks/contentprovider/TradefedContentProvider.apk
Binary files differ
diff --git a/src/com/android/tradefed/device/contentprovider/ContentProviderHandler.java b/src/com/android/tradefed/device/contentprovider/ContentProviderHandler.java
new file mode 100644
index 0000000..bf8978f
--- /dev/null
+++ b/src/com/android/tradefed/device/contentprovider/ContentProviderHandler.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2019 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.device.contentprovider;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.WifiHelper;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.FileUtil;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Set;
+
+/**
+ * Handler that abstract the content provider interactions and allow to use the device side content
+ * provider for different operations.
+ *
+ * <p>All implementation in this class should be mindful of the user currently running on the
+ * device.
+ */
+public class ContentProviderHandler {
+
+    public static final String PACKAGE_NAME = "android.tradefed.contentprovider";
+    public static final String CONTENT_PROVIDER_URI = "content://android.tradefed.contentprovider";
+    private static final String APK_NAME = "TradefedContentProvider.apk";
+    private static final String CONTENT_PROVIDER_APK_RES = "/apks/contentprovider/" + APK_NAME;
+
+    private ITestDevice mDevice;
+    private File mContentProviderApk = null;
+
+    /** Constructor. */
+    public ContentProviderHandler(ITestDevice device) {
+        mDevice = device;
+    }
+
+    /**
+     * Ensure the content provider helper apk is installed and ready to be used.
+     *
+     * @return True if ready to be used, False otherwise.
+     */
+    public boolean setUp() throws DeviceNotAvailableException, IOException {
+        Set<String> packageNames = mDevice.getInstalledPackageNames();
+        if (packageNames.contains(PACKAGE_NAME)) {
+            return true;
+        }
+        if (mContentProviderApk == null) {
+            mContentProviderApk = extractResourceApk();
+        }
+        // Install package for all users
+        String output = mDevice.installPackage(mContentProviderApk, true, true);
+        if (output == null) {
+            return true;
+        }
+        CLog.e("Something went wrong while installing the content provider apk: %s", output);
+        FileUtil.deleteFile(mContentProviderApk);
+        return false;
+    }
+
+    /** Clean the device from the content provider helper. */
+    public void tearDown() throws Exception {
+        FileUtil.deleteFile(mContentProviderApk);
+        mDevice.uninstallPackage(PACKAGE_NAME);
+    }
+
+    /**
+     * Content provider callback that delete a file at the URI location. File will be deleted from
+     * the disk.
+     *
+     * @param deviceFilePath The path on the device of the file to delete.
+     * @return True if successful, False otherwise
+     * @throws DeviceNotAvailableException
+     */
+    public boolean deleteFile(String deviceFilePath) throws DeviceNotAvailableException {
+        String contentUri = String.format("%s/%s", CONTENT_PROVIDER_URI, deviceFilePath);
+        String deleteCommand =
+                String.format(
+                        "content delete --user %d --uri %s", mDevice.getCurrentUser(), contentUri);
+        CommandResult deleteResult = mDevice.executeShellV2Command(deleteCommand);
+
+        if (CommandStatus.SUCCESS.equals(deleteResult.getStatus())) {
+            return true;
+        }
+        CLog.e(
+                "Failed to remove a file at %s using content provider. Error: '%s'",
+                deviceFilePath, deleteResult.getStderr());
+        return false;
+    }
+
+    /** Helper method to extract the content provider apk. */
+    private File extractResourceApk() throws IOException {
+        File apkTempFile = FileUtil.createTempFile(APK_NAME, ".apk");
+        InputStream apkStream = WifiHelper.class.getResourceAsStream(CONTENT_PROVIDER_APK_RES);
+        FileUtil.writeToFile(apkStream, apkTempFile);
+        return apkTempFile;
+    }
+}
diff --git a/tests/src/com/android/tradefed/UnitTests.java b/tests/src/com/android/tradefed/UnitTests.java
index 51aa8d8..2429be3 100644
--- a/tests/src/com/android/tradefed/UnitTests.java
+++ b/tests/src/com/android/tradefed/UnitTests.java
@@ -67,6 +67,7 @@
 import com.android.tradefed.device.TopHelperTest;
 import com.android.tradefed.device.WaitDeviceRecoveryTest;
 import com.android.tradefed.device.WifiHelperTest;
+import com.android.tradefed.device.contentprovider.ContentProviderHandlerTest;
 import com.android.tradefed.device.metric.AtraceCollectorTest;
 import com.android.tradefed.device.metric.AtraceRunMetricCollectorTest;
 import com.android.tradefed.device.metric.BaseDeviceMetricCollectorTest;
@@ -367,6 +368,9 @@
     WaitDeviceRecoveryTest.class,
     WifiHelperTest.class,
 
+    // device.contentprovider
+    ContentProviderHandlerTest.class,
+
     // device.metric
     AtraceCollectorTest.class,
     AtraceRunMetricCollectorTest.class,
diff --git a/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java b/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java
new file mode 100644
index 0000000..2183140
--- /dev/null
+++ b/tests/src/com/android/tradefed/device/contentprovider/ContentProviderHandlerTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2019 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.device.contentprovider;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mockito;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/** Run unit tests for {@link ContentProviderHandler}. */
+@RunWith(JUnit4.class)
+public class ContentProviderHandlerTest {
+
+    private ContentProviderHandler mProvider;
+    private ITestDevice mMockDevice;
+
+    @Before
+    public void setUp() {
+        mMockDevice = Mockito.mock(ITestDevice.class);
+        mProvider = new ContentProviderHandler(mMockDevice);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mProvider.tearDown();
+    }
+
+    @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());
+    }
+
+    @Test
+    public void testSetUp_alreadyInstalled() throws Exception {
+        Set<String> set = new HashSet<>();
+        set.add(ContentProviderHandler.PACKAGE_NAME);
+        doReturn(set).when(mMockDevice).getInstalledPackageNames();
+
+        assertTrue(mProvider.setUp());
+    }
+
+    @Test
+    public void testSetUp_installFail() throws Exception {
+        Set<String> set = new HashSet<>();
+        doReturn(set).when(mMockDevice).getInstalledPackageNames();
+        doReturn(1).when(mMockDevice).getCurrentUser();
+        doReturn("fail").when(mMockDevice).installPackage(any(), eq(true), eq(true));
+
+        assertFalse(mProvider.setUp());
+    }
+
+    /** Test {@link ContentProviderHandler#deleteFile(String)}. */
+    @Test
+    public void testDeleteFile() throws Exception {
+        String devicePath = "path/somewhere/file.txt";
+        CommandResult result = new CommandResult(CommandStatus.SUCCESS);
+        doReturn(99).when(mMockDevice).getCurrentUser();
+        doReturn(result)
+                .when(mMockDevice)
+                .executeShellV2Command(
+                        eq(
+                                "content delete --user 99 --uri "
+                                        + ContentProviderHandler.CONTENT_PROVIDER_URI
+                                        + "/"
+                                        + devicePath));
+        assertTrue(mProvider.deleteFile(devicePath));
+    }
+
+    /** Test {@link ContentProviderHandler#deleteFile(String)}. */
+    @Test
+    public void testDeleteFile_fail() throws Exception {
+        String devicePath = "path/somewhere/file.txt";
+        CommandResult result = new CommandResult(CommandStatus.FAILED);
+        result.setStderr("couldn't find the file");
+        doReturn(99).when(mMockDevice).getCurrentUser();
+        doReturn(result)
+                .when(mMockDevice)
+                .executeShellV2Command(
+                        eq(
+                                "content delete --user 99 --uri "
+                                        + ContentProviderHandler.CONTENT_PROVIDER_URI
+                                        + "/"
+                                        + devicePath));
+        assertFalse(mProvider.deleteFile(devicePath));
+    }
+}