1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package android.tradefed.contentprovider; 17 18 import android.content.ContentProvider; 19 import android.content.ContentValues; 20 import android.database.Cursor; 21 import android.database.MatrixCursor; 22 import android.net.Uri; 23 import android.os.Environment; 24 import android.os.ParcelFileDescriptor; 25 import androidx.annotation.NonNull; 26 import androidx.annotation.Nullable; 27 import android.util.Log; 28 import android.webkit.MimeTypeMap; 29 30 import java.io.File; 31 import java.io.FileNotFoundException; 32 import java.io.UnsupportedEncodingException; 33 import java.net.URLDecoder; 34 import java.util.Arrays; 35 import java.util.Comparator; 36 import java.util.HashMap; 37 import java.util.Map; 38 39 /** 40 * Content Provider implementation to hide sd card details away from host/device interactions, and 41 * that allows to abstract the host/device interactions more by allowing device and host to 42 * communicate files through the provider. 43 * 44 * <p>This implementation aims to be standard and work in all situations. 45 */ 46 public class ManagedFileContentProvider extends ContentProvider { 47 public static final String COLUMN_NAME = "name"; 48 public static final String COLUMN_ABSOLUTE_PATH = "absolute_path"; 49 public static final String COLUMN_DIRECTORY = "is_directory"; 50 public static final String COLUMN_MIME_TYPE = "mime_type"; 51 public static final String COLUMN_METADATA = "metadata"; 52 53 // TODO: Complete the list of columns 54 public static final String[] COLUMNS = 55 new String[] { 56 COLUMN_NAME, 57 COLUMN_ABSOLUTE_PATH, 58 COLUMN_DIRECTORY, 59 COLUMN_MIME_TYPE, 60 COLUMN_METADATA 61 }; 62 63 private static final String TAG = "ManagedFileContentProvider"; 64 private static MimeTypeMap sMimeMap = MimeTypeMap.getSingleton(); 65 66 private Map<Uri, ContentValues> mFileTracker = new HashMap<>(); 67 68 @Override onCreate()69 public boolean onCreate() { 70 mFileTracker = new HashMap<>(); 71 return true; 72 } 73 74 /** 75 * Use a content URI with absolute device path embedded to get information about a file or a 76 * directory on the device. 77 * 78 * @param uri A content uri that contains the path to the desired file/directory. 79 * @param projection - not supported. 80 * @param selection - not supported. 81 * @param selectionArgs - not supported. 82 * @param sortOrder - not supported. 83 * @return A {@link Cursor} containing the results of the query. Cursor contains a single row 84 * for files and for directories it returns one row for each {@link File} returned by {@link 85 * File#listFiles()}. 86 */ 87 @Nullable 88 @Override query( @onNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder)89 public Cursor query( 90 @NonNull Uri uri, 91 @Nullable String[] projection, 92 @Nullable String selection, 93 @Nullable String[] selectionArgs, 94 @Nullable String sortOrder) { 95 File file = getFileForUri(uri); 96 if ("/".equals(file.getAbsolutePath())) { 97 // Querying the root will list all the known file (inserted) 98 final MatrixCursor cursor = new MatrixCursor(COLUMNS, mFileTracker.size()); 99 for (Map.Entry<Uri, ContentValues> path : mFileTracker.entrySet()) { 100 String metadata = path.getValue().getAsString(COLUMN_METADATA); 101 cursor.addRow(getRow(COLUMNS, getFileForUri(path.getKey()), metadata)); 102 } 103 return cursor; 104 } 105 106 if (!file.exists()) { 107 Log.e(TAG, String.format("Query - File from uri: '%s' does not exists.", uri)); 108 return null; 109 } 110 111 if (!file.isDirectory()) { 112 // Just return the information about the file itself. 113 final MatrixCursor cursor = new MatrixCursor(COLUMNS, 1); 114 cursor.addRow(getRow(COLUMNS, file, /* metadata= */ null)); 115 return cursor; 116 } 117 118 // Otherwise return the content of the directory - similar to doing ls command. 119 File[] files = file.listFiles(); 120 sortFilesByAbsolutePath(files); 121 final MatrixCursor cursor = new MatrixCursor(COLUMNS, files.length + 1); 122 for (File child : files) { 123 cursor.addRow(getRow(COLUMNS, child, /* metadata= */ null)); 124 } 125 return cursor; 126 } 127 128 @Nullable 129 @Override getType(@onNull Uri uri)130 public String getType(@NonNull Uri uri) { 131 return getType(getFileForUri(uri)); 132 } 133 134 @Nullable 135 @Override insert(@onNull Uri uri, @Nullable ContentValues contentValues)136 public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) { 137 String extra = ""; 138 File file = getFileForUri(uri); 139 if (!file.exists()) { 140 Log.e(TAG, String.format("Insert - File from uri: '%s' does not exists.", uri)); 141 return null; 142 } 143 if (mFileTracker.get(uri) != null) { 144 Log.e( 145 TAG, 146 String.format("Insert - File from uri: '%s' already exists, ignoring.", uri)); 147 return null; 148 } 149 mFileTracker.put(uri, contentValues); 150 return uri; 151 } 152 153 @Override delete( @onNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs)154 public int delete( 155 @NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) { 156 // Stop Tracking the File of directory if it was tracked and delete it from the disk 157 mFileTracker.remove(uri); 158 File file = getFileForUri(uri); 159 int num = recursiveDelete(file); 160 return num; 161 } 162 163 @Override update( @onNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs)164 public int update( 165 @NonNull Uri uri, 166 @Nullable ContentValues values, 167 @Nullable String selection, 168 @Nullable String[] selectionArgs) { 169 File file = getFileForUri(uri); 170 if (!file.exists()) { 171 Log.e(TAG, String.format("Update - File from uri: '%s' does not exists.", uri)); 172 return 0; 173 } 174 if (mFileTracker.get(uri) == null) { 175 Log.e( 176 TAG, 177 String.format( 178 "Update - File from uri: '%s' is not tracked yet, use insert.", uri)); 179 return 0; 180 } 181 mFileTracker.put(uri, values); 182 return 1; 183 } 184 185 @Override openFile(@onNull Uri uri, @NonNull String mode)186 public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) 187 throws FileNotFoundException { 188 final File file = getFileForUri(uri); 189 final int fileMode = modeToMode(mode); 190 191 if ((fileMode & ParcelFileDescriptor.MODE_CREATE) == ParcelFileDescriptor.MODE_CREATE) { 192 // If the file is being created, create all its parent directories that don't already 193 // exist. 194 file.getParentFile().mkdirs(); 195 if (!mFileTracker.containsKey(uri)) { 196 // Track the file, if not already tracked. 197 mFileTracker.put(uri, new ContentValues()); 198 } 199 } 200 return ParcelFileDescriptor.open(file, fileMode); 201 } 202 getRow(String[] columns, File file, String metadata)203 private Object[] getRow(String[] columns, File file, String metadata) { 204 Object[] values = new Object[columns.length]; 205 for (int i = 0; i < columns.length; i++) { 206 values[i] = getColumnValue(columns[i], file, metadata); 207 } 208 return values; 209 } 210 getColumnValue(String columnName, File file, String metadata)211 private Object getColumnValue(String columnName, File file, String metadata) { 212 Object value = null; 213 if (COLUMN_NAME.equals(columnName)) { 214 value = file.getName(); 215 } else if (COLUMN_ABSOLUTE_PATH.equals(columnName)) { 216 value = file.getAbsolutePath(); 217 } else if (COLUMN_DIRECTORY.equals(columnName)) { 218 value = file.isDirectory(); 219 } else if (COLUMN_METADATA.equals(columnName)) { 220 value = metadata; 221 } else if (COLUMN_MIME_TYPE.equals(columnName)) { 222 value = file.isDirectory() ? null : getType(file); 223 } 224 return value; 225 } 226 getType(@onNull File file)227 private String getType(@NonNull File file) { 228 final int lastDot = file.getName().lastIndexOf('.'); 229 if (lastDot >= 0) { 230 final String extension = file.getName().substring(lastDot + 1); 231 final String mime = sMimeMap.getMimeTypeFromExtension(extension); 232 if (mime != null) { 233 return mime; 234 } 235 } 236 237 return "application/octet-stream"; 238 } 239 getFileForUri(@onNull Uri uri)240 private File getFileForUri(@NonNull Uri uri) { 241 // TODO: apply the /sdcard resolution to query() too. 242 String uriPath = uri.getPath(); 243 try { 244 uriPath = URLDecoder.decode(uriPath, "UTF-8"); 245 } catch (UnsupportedEncodingException e) { 246 throw new RuntimeException(e); 247 } 248 if (uriPath.startsWith("/sdcard/")) { 249 uriPath = 250 uriPath.replaceAll( 251 "/sdcard", Environment.getExternalStorageDirectory().getAbsolutePath()); 252 } 253 return new File(uriPath); 254 } 255 256 /** Copied from FileProvider.java. */ modeToMode(String mode)257 private static int modeToMode(String mode) { 258 int modeBits; 259 if ("r".equals(mode)) { 260 modeBits = ParcelFileDescriptor.MODE_READ_ONLY; 261 } else if ("w".equals(mode) || "wt".equals(mode)) { 262 modeBits = 263 ParcelFileDescriptor.MODE_WRITE_ONLY 264 | ParcelFileDescriptor.MODE_CREATE 265 | ParcelFileDescriptor.MODE_TRUNCATE; 266 } else if ("wa".equals(mode)) { 267 modeBits = 268 ParcelFileDescriptor.MODE_WRITE_ONLY 269 | ParcelFileDescriptor.MODE_CREATE 270 | ParcelFileDescriptor.MODE_APPEND; 271 } else if ("rw".equals(mode)) { 272 modeBits = ParcelFileDescriptor.MODE_READ_WRITE | ParcelFileDescriptor.MODE_CREATE; 273 } else if ("rwt".equals(mode)) { 274 modeBits = 275 ParcelFileDescriptor.MODE_READ_WRITE 276 | ParcelFileDescriptor.MODE_CREATE 277 | ParcelFileDescriptor.MODE_TRUNCATE; 278 } else { 279 throw new IllegalArgumentException("Invalid mode: " + mode); 280 } 281 return modeBits; 282 } 283 284 /** 285 * Recursively delete given file or directory and all its contents. 286 * 287 * @param rootDir the directory or file to be deleted; can be null 288 * @return The number of deleted files. 289 */ recursiveDelete(File rootDir)290 private int recursiveDelete(File rootDir) { 291 int count = 0; 292 if (rootDir != null) { 293 if (rootDir.isDirectory()) { 294 File[] childFiles = rootDir.listFiles(); 295 if (childFiles != null) { 296 for (File child : childFiles) { 297 count += recursiveDelete(child); 298 } 299 } 300 } 301 rootDir.delete(); 302 count++; 303 } 304 return count; 305 } 306 sortFilesByAbsolutePath(File[] files)307 private void sortFilesByAbsolutePath(File[] files) { 308 Arrays.sort( 309 files, 310 new Comparator<File>() { 311 @Override 312 public int compare(File f1, File f2) { 313 return f1.getAbsolutePath().compareTo(f2.getAbsolutePath()); 314 } 315 }); 316 } 317 } 318