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;
+ }
+}