| /* |
| * 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.io.UnsupportedEncodingException; |
| import java.net.URLDecoder; |
| import java.util.Arrays; |
| import java.util.Comparator; |
| import java.util.HashMap; |
| 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_NAME = "name"; |
| public static final String COLUMN_ABSOLUTE_PATH = "absolute_path"; |
| public static final String COLUMN_DIRECTORY = "is_directory"; |
| 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_NAME, |
| COLUMN_ABSOLUTE_PATH, |
| COLUMN_DIRECTORY, |
| 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; |
| } |
| |
| /** |
| * Use a content URI with absolute device path embedded to get information about a file or a |
| * directory on the device. |
| * |
| * @param uri A content uri that contains the path to the desired file/directory. |
| * @param projection - not supported. |
| * @param selection - not supported. |
| * @param selectionArgs - not supported. |
| * @param sortOrder - not supported. |
| * @return A {@link Cursor} containing the results of the query. Cursor contains a single row |
| * for files and for directories it returns one row for each {@link File} returned by {@link |
| * File#listFiles()}. |
| */ |
| @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(getRow(COLUMNS, getFileForUri(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 (!file.isDirectory()) { |
| // Just return the information about the file itself. |
| final MatrixCursor cursor = new MatrixCursor(COLUMNS, 1); |
| cursor.addRow(getRow(COLUMNS, file, /* metadata= */ null)); |
| return cursor; |
| } |
| |
| // Otherwise return the content of the directory - similar to doing ls command. |
| File[] files = file.listFiles(); |
| sortFilesByAbsolutePath(files); |
| final MatrixCursor cursor = new MatrixCursor(COLUMNS, files.length + 1); |
| for (File child : files) { |
| cursor.addRow(getRow(COLUMNS, child, /* metadata= */ null)); |
| } |
| return cursor; |
| } |
| |
| @Nullable |
| @Override |
| public String getType(@NonNull Uri uri) { |
| return getType(getFileForUri(uri)); |
| } |
| |
| @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) { |
| // Stop Tracking the File of directory if it was tracked and delete it from the disk |
| mFileTracker.remove(uri); |
| File file = getFileForUri(uri); |
| int num = recursiveDelete(file); |
| return num; |
| } |
| |
| @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 { |
| 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(); |
| if (!mFileTracker.containsKey(uri)) { |
| // Track the file, if not already tracked. |
| mFileTracker.put(uri, new ContentValues()); |
| } |
| } |
| return ParcelFileDescriptor.open(file, fileMode); |
| } |
| |
| private Object[] getRow(String[] columns, File file, String metadata) { |
| Object[] values = new Object[columns.length]; |
| for (int i = 0; i < columns.length; i++) { |
| values[i] = getColumnValue(columns[i], file, metadata); |
| } |
| return values; |
| } |
| |
| private Object getColumnValue(String columnName, File file, String metadata) { |
| Object value = null; |
| if (COLUMN_NAME.equals(columnName)) { |
| value = file.getName(); |
| } else if (COLUMN_ABSOLUTE_PATH.equals(columnName)) { |
| value = file.getAbsolutePath(); |
| } else if (COLUMN_DIRECTORY.equals(columnName)) { |
| value = file.isDirectory(); |
| } else if (COLUMN_METADATA.equals(columnName)) { |
| value = metadata; |
| } else if (COLUMN_MIME_TYPE.equals(columnName)) { |
| value = file.isDirectory() ? null : getType(file); |
| } |
| return value; |
| } |
| |
| private String getType(@NonNull File file) { |
| 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"; |
| } |
| |
| private File getFileForUri(@NonNull Uri uri) { |
| // TODO: apply the /sdcard resolution to query() too. |
| String uriPath = uri.getPath(); |
| try { |
| uriPath = URLDecoder.decode(uriPath, "UTF-8"); |
| } catch (UnsupportedEncodingException e) { |
| throw new RuntimeException(e); |
| } |
| if (uriPath.startsWith("/sdcard/")) { |
| uriPath = |
| uriPath.replaceAll( |
| "/sdcard", Environment.getExternalStorageDirectory().getAbsolutePath()); |
| } |
| return new File(uriPath); |
| } |
| |
| /** 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; |
| } |
| |
| private void sortFilesByAbsolutePath(File[] files) { |
| Arrays.sort( |
| files, |
| new Comparator<File>() { |
| @Override |
| public int compare(File f1, File f2) { |
| return f1.getAbsolutePath().compareTo(f2.getAbsolutePath()); |
| } |
| }); |
| } |
| } |