Start a content provider for file interactions

Create a content provider for file interactions with
the device.

Bug: 123529934
Test: atest TradefedContentProviderTest
Bug: 121327553
Change-Id: I9d007409c3799edf556d5be34bff0af503fe0b3e
Merged-In: I9d007409c3799edf556d5be34bff0af503fe0b3e
diff --git a/util-apps/ContentProvider/Android.mk b/util-apps/ContentProvider/Android.mk
new file mode 100644
index 0000000..29a3d6b
--- /dev/null
+++ b/util-apps/ContentProvider/Android.mk
@@ -0,0 +1,20 @@
+# Copyright (C) 2012 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.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+# Build the test APKs using their own makefiles
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/util-apps/ContentProvider/androidTest/Android.mk b/util-apps/ContentProvider/androidTest/Android.mk
new file mode 100644
index 0000000..7373c0d
--- /dev/null
+++ b/util-apps/ContentProvider/androidTest/Android.mk
@@ -0,0 +1,31 @@
+
+# Copyright (C) 2012 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.
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := TradefedContentProviderTest
+LOCAL_INSTRUMENTATION_FOR := TradefedContentProvider
+LOCAL_MODULE_TAGS := tests optional
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_SDK_VERSION := 24
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    junit \
+    androidx.test.runner
+
+LOCAL_COMPATIBILITY_SUITE := general-tests
+
+include $(BUILD_PACKAGE)
diff --git a/util-apps/ContentProvider/androidTest/AndroidManifest.xml b/util-apps/ContentProvider/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..18e1b11
--- /dev/null
+++ b/util-apps/ContentProvider/androidTest/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2018 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.tradefed.contentprovider.test">
+    <uses-sdk android:minSdkVersion="24"
+          android:targetSdkVersion="26"/>
+
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="android.tradefed.contentprovider"
+                     android:label="Unit tests for Tradefed Content provider">
+    </instrumentation>
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+</manifest>
diff --git a/util-apps/ContentProvider/androidTest/AndroidTest.xml b/util-apps/ContentProvider/androidTest/AndroidTest.xml
new file mode 100644
index 0000000..325e7f9
--- /dev/null
+++ b/util-apps/ContentProvider/androidTest/AndroidTest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 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.
+-->
+<configuration description="Configuration for Tradefed Content Provider Tests">
+    <option name="test-suite-tag" value="tradefed_content_provider" />
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="TradefedContentProviderTest.apk" />
+    </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="TradefedContentProvider.apk" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <option name="package" value="android.tradefed.contentprovider.test" />
+    </test>
+</configuration>
diff --git a/util-apps/ContentProvider/androidTest/src/android/tradefed/contentprovider/ManagedFileContentProviderTest.java b/util-apps/ContentProvider/androidTest/src/android/tradefed/contentprovider/ManagedFileContentProviderTest.java
new file mode 100644
index 0000000..cb13496
--- /dev/null
+++ b/util-apps/ContentProvider/androidTest/src/android/tradefed/contentprovider/ManagedFileContentProviderTest.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2018 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 android.tradefed.contentprovider;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Environment;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests for {@link android.tradefed.contentprovider.ManagedFileContentProvider}. TODO: Complete the
+ * tests when automatic test setup is made.
+ */
+@RunWith(AndroidJUnit4.class)
+public class ManagedFileContentProviderTest {
+
+    public static final String CONTENT_PROVIDER =
+            String.format("%s://android.tradefed.contentprovider", ContentResolver.SCHEME_CONTENT);
+    private static final String TEST_FILE = "ManagedFileContentProviderTest.txt";
+
+    private File mTestFile = null;
+    private Context mAppContext;
+    private List<Uri> mShouldBeCleaned = new ArrayList<>();
+    private ContentValues mCv;
+    private Uri mTestUri;
+
+    @Before
+    public void setUp() throws Exception {
+        mCv = new ContentValues();
+        mTestFile = new File(Environment.getExternalStorageDirectory(), TEST_FILE);
+        if (mTestFile.exists()) {
+            mTestFile.delete();
+        }
+        mTestFile.createNewFile();
+        // Context of the app under test.
+        mAppContext = InstrumentationRegistry.getTargetContext();
+        assertEquals("android.tradefed.contentprovider", mAppContext.getPackageName());
+
+        String fullUriPath = String.format("%s%s", CONTENT_PROVIDER, mTestFile.getAbsolutePath());
+        mTestUri = Uri.parse(fullUriPath);
+    }
+
+    @After
+    public void tearDown() {
+        if (mTestFile != null) {
+            mTestFile.delete();
+        }
+        for (Uri uri : mShouldBeCleaned) {
+            mAppContext
+                    .getContentResolver()
+                    .delete(
+                            uri,
+                            /* selection */
+                            null,
+                            /* selectionArgs */
+                            null);
+        }
+    }
+
+    /** Test that we can delete a file from the content provider. */
+    @Test
+    public void testDelete() throws Exception {
+        ContentResolver resolver = mAppContext.getContentResolver();
+        Uri uriResult = resolver.insert(mTestUri, mCv);
+        mShouldBeCleaned.add(mTestUri);
+        // Insert is successful
+        assertEquals(mTestUri, uriResult);
+        // Trying to insert again is inop
+        Uri reInsert = resolver.insert(mTestUri, mCv);
+        assertNull(reInsert);
+        // Now delete
+        int affected =
+                resolver.delete(
+                        mTestUri,
+                        /* selection */
+                        null,
+                        /* selectionArgs */
+                        null);
+        assertEquals(1, affected);
+        // File should have been deleted.
+        assertFalse(mTestFile.exists());
+        // We can now insert again
+        mTestFile.createNewFile();
+        uriResult = resolver.insert(mTestUri, mCv);
+        assertEquals(mTestUri, uriResult);
+    }
+
+    /** Test that querying the content provider is working. */
+    @Test
+    public void testQuery() throws Exception {
+        ContentResolver resolver = mAppContext.getContentResolver();
+        Uri uriResult = resolver.insert(mTestUri, mCv);
+        mShouldBeCleaned.add(mTestUri);
+        // Insert is successful
+        assertEquals(mTestUri, uriResult);
+
+        Cursor cursor =
+                resolver.query(
+                        mTestUri,
+                        /* projection */
+                        null,
+                        /* selection */
+                        null,
+                        /* selectionArgs */
+                        null,
+                        /* sortOrder */
+                        null);
+        try {
+            assertEquals(1, cursor.getCount());
+            String[] columns = cursor.getColumnNames();
+            assertEquals(ManagedFileContentProvider.COLUMNS, columns);
+            assertTrue(cursor.moveToNext());
+            // Absolute path
+            assertEquals(
+                    Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + TEST_FILE,
+                    cursor.getString(0));
+            // Uri
+            assertEquals(mTestUri.toString(), cursor.getString(1));
+            // Type
+            assertEquals("text/plain", cursor.getString(2));
+            // Metadata
+            assertNull(cursor.getString(3));
+        } finally {
+            cursor.close();
+        }
+    }
+
+    /** Test that querying the content provider is working when abstracting the sdcard */
+    @Test
+    public void testQuery_sdcard() throws Exception {
+        ContentResolver resolver = mAppContext.getContentResolver();
+        Uri uriResult = resolver.insert(mTestUri, mCv);
+        mShouldBeCleaned.add(mTestUri);
+        // Insert is successful
+        assertEquals(mTestUri, uriResult);
+
+        String sdcardUriPath = String.format("%s/sdcard/%s", CONTENT_PROVIDER, mTestFile.getName());
+        Uri sdcardUri = Uri.parse(sdcardUriPath);
+
+        Cursor cursor =
+                resolver.query(
+                        sdcardUri,
+                        /* projection */
+                        null,
+                        /* selection */
+                        null,
+                        /* selectionArgs */
+                        null,
+                        /* sortOrder */
+                        null);
+        try {
+            assertEquals(1, cursor.getCount());
+            String[] columns = cursor.getColumnNames();
+            assertEquals(ManagedFileContentProvider.COLUMNS, columns);
+            assertTrue(cursor.moveToNext());
+            // Absolute path
+            assertEquals(
+                    Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + TEST_FILE,
+                    cursor.getString(0));
+            // Uri
+            assertEquals(sdcardUri.toString(), cursor.getString(1));
+            // Type
+            assertEquals("text/plain", cursor.getString(2));
+            // Metadata
+            assertNull(cursor.getString(3));
+        } finally {
+            cursor.close();
+        }
+    }
+}
diff --git a/util-apps/ContentProvider/main/Android.mk b/util-apps/ContentProvider/main/Android.mk
new file mode 100644
index 0000000..4add64f
--- /dev/null
+++ b/util-apps/ContentProvider/main/Android.mk
@@ -0,0 +1,26 @@
+
+# Copyright (C) 2012 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.
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+LOCAL_SRC_FILES := $(call all-java-files-under, java)
+LOCAL_PACKAGE_NAME := TradefedContentProvider
+LOCAL_SDK_VERSION := 24
+LOCAL_COMPATIBILITY_SUITE := general-tests
+LOCAL_STATIC_JAVA_LIBRARIES := androidx.annotation_annotation
+
+include $(BUILD_PACKAGE)
diff --git a/util-apps/ContentProvider/main/AndroidManifest.xml b/util-apps/ContentProvider/main/AndroidManifest.xml
new file mode 100644
index 0000000..ac37e68
--- /dev/null
+++ b/util-apps/ContentProvider/main/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- Copyright (C) 2018 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.tradefed.contentprovider">
+
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+
+    <application>
+        <provider
+            android:name="android.tradefed.contentprovider.ManagedFileContentProvider"
+            android:authorities="android.tradefed.contentprovider"
+            android:grantUriPermissions="true"
+            android:exported="true"
+            android:enabled="true"
+            />
+    </application>
+</manifest>
diff --git a/util-apps/ContentProvider/main/java/android/tradefed/contentprovider/ManagedFileContentProvider.java b/util-apps/ContentProvider/main/java/android/tradefed/contentprovider/ManagedFileContentProvider.java
new file mode 100644
index 0000000..57639f0
--- /dev/null
+++ b/util-apps/ContentProvider/main/java/android/tradefed/contentprovider/ManagedFileContentProvider.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright (C) 2018 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 android.tradefed.contentprovider;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.ParcelFileDescriptor;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Content Provider implementation to hide sd card details away from host/device interactions, and
+ * that allows to abstract the host/device interactions more by allowing device and host to
+ * communicate files through the provider.
+ *
+ * <p>This implementation aims to be standard and work in all situations.
+ */
+public class ManagedFileContentProvider extends ContentProvider {
+
+    public static final String COLUMN_ABSOLUTE_PATH = "absolute_path";
+    public static final String COLUMN_URI = "uri";
+    public static final String COLUMN_MIME_TYPE = "mime_type";
+    public static final String COLUMN_METADATA = "metadata";
+    // TODO: Complete the list of columns
+    public static final String[] COLUMNS =
+            new String[] {COLUMN_ABSOLUTE_PATH, COLUMN_URI, COLUMN_MIME_TYPE, COLUMN_METADATA};
+
+    private static String TAG = "ManagedFileContentProvider";
+    private static MimeTypeMap sMimeMap = MimeTypeMap.getSingleton();
+
+    private Map<Uri, ContentValues> mFileTracker = new HashMap<>();
+
+    @Override
+    public boolean onCreate() {
+        mFileTracker = new HashMap<>();
+        return true;
+    }
+
+    @Nullable
+    @Override
+    public Cursor query(
+            @NonNull Uri uri,
+            @Nullable String[] projection,
+            @Nullable String selection,
+            @Nullable String[] selectionArgs,
+            @Nullable String sortOrder) {
+        File file = getFileForUri(uri);
+        if ("/".equals(file.getAbsolutePath())) {
+            // Querying the root will list all the known file (inserted)
+            final MatrixCursor cursor = new MatrixCursor(COLUMNS, mFileTracker.size());
+            for (Map.Entry<Uri, ContentValues> path : mFileTracker.entrySet()) {
+                String metadata = path.getValue().getAsString(COLUMN_METADATA);
+                cursor.addRow(
+                        new String[] {
+                            getFileForUri(path.getKey()).getAbsolutePath(),
+                            uri.toString(),
+                            getType(path.getKey()),
+                            metadata
+                        });
+            }
+            return cursor;
+        }
+
+        if (!file.exists()) {
+            Log.e(TAG, String.format("Query - File from uri: '%s' does not exists.", uri));
+            return null;
+        }
+
+        // If a particular file is requested, find it and return it.
+        List<String> filePaths = new ArrayList<>();
+        if (file.isDirectory()) {
+            readDirectory(filePaths, file);
+        } else {
+            // If not a directory, return a single row - the name of the file.
+            filePaths.add(file.getAbsolutePath());
+        }
+
+        // Add all the paths to the cursor.
+        final MatrixCursor cursor = new MatrixCursor(COLUMNS, filePaths.size());
+        for (String path : filePaths) {
+            // TODO: Return a properly formed uri for each filepath
+            cursor.addRow(
+                    new String[] {
+                        path,
+                        uri.toString(),
+                        getType(uri),
+                        /* metadata */
+                        null
+                    });
+        }
+
+        return cursor;
+    }
+
+    @Nullable
+    @Override
+    public String getType(@NonNull Uri uri) {
+        final File file = getFileForUri(uri);
+
+        final int lastDot = file.getName().lastIndexOf('.');
+        if (lastDot >= 0) {
+            final String extension = file.getName().substring(lastDot + 1);
+            final String mime = sMimeMap.getMimeTypeFromExtension(extension);
+            if (mime != null) {
+                return mime;
+            }
+        }
+
+        return "application/octet-stream";
+    }
+
+    @Nullable
+    @Override
+    public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {
+        String extra = "";
+        File file = getFileForUri(uri);
+        if (!file.exists()) {
+            Log.e(TAG, String.format("Insert - File from uri: '%s' does not exists.", uri));
+            return null;
+        }
+        if (mFileTracker.get(uri) != null) {
+            Log.e(
+                    TAG,
+                    String.format("Insert - File from uri: '%s' already exists, ignoring.", uri));
+            return null;
+        }
+        mFileTracker.put(uri, contentValues);
+        return uri;
+    }
+
+    @Override
+    public int delete(
+            @NonNull Uri uri,
+            @Nullable String selection,
+            @Nullable String[] selectionArgs) {
+        // TODO: Support deleting a file created via content write
+        ContentValues values = mFileTracker.remove(uri);
+        if (values == null) {
+            return 0;
+        }
+        File file = getFileForUri(uri);
+        int num = recursiveDelete(file);
+        return 1;
+    }
+
+    @Override
+    public int update(
+            @NonNull Uri uri,
+            @Nullable ContentValues values,
+            @Nullable String selection,
+            @Nullable String[] selectionArgs) {
+        File file = getFileForUri(uri);
+        if (!file.exists()) {
+            Log.e(TAG, String.format("Update - File from uri: '%s' does not exists.", uri));
+            return 0;
+        }
+        if (mFileTracker.get(uri) == null) {
+            Log.e(
+                    TAG,
+                    String.format(
+                            "Update - File from uri: '%s' is not tracked yet, use insert.", uri));
+            return 0;
+        }
+        mFileTracker.put(uri, values);
+        return 1;
+    }
+
+    @Override
+    public ParcelFileDescriptor openFile(
+            @NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
+        // TODO: Track the file created via this callback (content write)
+        final File file = getFileForUri(uri);
+        final int fileMode = modeToMode(mode);
+
+        if ((fileMode & ParcelFileDescriptor.MODE_CREATE) == ParcelFileDescriptor.MODE_CREATE) {
+            // If the file is being created, create all its parent directories that don't already
+            // exist.
+            file.getParentFile().mkdirs();
+        }
+        return ParcelFileDescriptor.open(file, fileMode);
+    }
+
+    private File getFileForUri(@NonNull Uri uri) {
+        // TODO: apply the /sdcard resolution to query() too.
+        String uriPath = uri.getPath();
+        if (uriPath.startsWith("/sdcard/")) {
+            uriPath =
+                    uriPath.replaceAll(
+                            "/sdcard", Environment.getExternalStorageDirectory().getAbsolutePath());
+        }
+        return new File(uriPath);
+    }
+
+    private void readDirectory(List<String> files, File dir) {
+        for (File f : dir.listFiles()) {
+            if (f.isDirectory()) {
+                readDirectory(files, f);
+            } else {
+                files.add(f.getAbsolutePath());
+            }
+        }
+    }
+
+    /** Copied from FileProvider.java. */
+    private static int modeToMode(String mode) {
+        int modeBits;
+        if ("r".equals(mode)) {
+            modeBits = ParcelFileDescriptor.MODE_READ_ONLY;
+        } else if ("w".equals(mode) || "wt".equals(mode)) {
+            modeBits =
+                    ParcelFileDescriptor.MODE_WRITE_ONLY
+                            | ParcelFileDescriptor.MODE_CREATE
+                            | ParcelFileDescriptor.MODE_TRUNCATE;
+        } else if ("wa".equals(mode)) {
+            modeBits =
+                    ParcelFileDescriptor.MODE_WRITE_ONLY
+                            | ParcelFileDescriptor.MODE_CREATE
+                            | ParcelFileDescriptor.MODE_APPEND;
+        } else if ("rw".equals(mode)) {
+            modeBits = ParcelFileDescriptor.MODE_READ_WRITE | ParcelFileDescriptor.MODE_CREATE;
+        } else if ("rwt".equals(mode)) {
+            modeBits =
+                    ParcelFileDescriptor.MODE_READ_WRITE
+                            | ParcelFileDescriptor.MODE_CREATE
+                            | ParcelFileDescriptor.MODE_TRUNCATE;
+        } else {
+            throw new IllegalArgumentException("Invalid mode: " + mode);
+        }
+        return modeBits;
+    }
+
+    /**
+     * Recursively delete given file or directory and all its contents.
+     *
+     * @param rootDir the directory or file to be deleted; can be null
+     * @return The number of deleted files.
+     */
+    private int recursiveDelete(File rootDir) {
+        int count = 0;
+        if (rootDir != null) {
+            if (rootDir.isDirectory()) {
+                File[] childFiles = rootDir.listFiles();
+                if (childFiles != null) {
+                    for (File child : childFiles) {
+                        count += recursiveDelete(child);
+                    }
+                }
+            }
+            rootDir.delete();
+            count++;
+        }
+        return count;
+    }
+}