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