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 String extra = ""; 134 File file = getFileForUri(uri); 135 if (!file.exists()) { 136 Log.e(TAG, String.format("Insert - File from uri: '%s' does not exists.", uri)); 137 return null; 138 } 139 if (mFileTracker.get(uri) != null) { 140 Log.e( 141 TAG, 142 String.format("Insert - File from uri: '%s' already exists, ignoring.", uri)); 143 return null; 144 } 145 mFileTracker.put(uri, contentValues); 146 return uri; 147 } 148 149 @Override delete(Uri uri, String selection, String[] selectionArgs)150 public int delete(Uri uri, String selection, String[] selectionArgs) { 151 // Stop Tracking the File of directory if it was tracked and delete it from the disk 152 mFileTracker.remove(uri); 153 File file = getFileForUri(uri); 154 int num = recursiveDelete(file); 155 return num; 156 } 157 158 @Override update(Uri uri, ContentValues values, String selection, String[] selectionArgs)159 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 160 File file = getFileForUri(uri); 161 if (!file.exists()) { 162 Log.e(TAG, String.format("Update - File from uri: '%s' does not exists.", uri)); 163 return 0; 164 } 165 if (mFileTracker.get(uri) == null) { 166 Log.e( 167 TAG, 168 String.format( 169 "Update - File from uri: '%s' is not tracked yet, use insert.", uri)); 170 return 0; 171 } 172 mFileTracker.put(uri, values); 173 return 1; 174 } 175 176 @Override openFile(Uri uri, String mode)177 public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { 178 final File file = getFileForUri(uri); 179 final int fileMode = modeToMode(mode); 180 181 if ((fileMode & ParcelFileDescriptor.MODE_CREATE) == ParcelFileDescriptor.MODE_CREATE) { 182 // If the file is being created, create all its parent directories that don't already 183 // exist. 184 file.getParentFile().mkdirs(); 185 if (!mFileTracker.containsKey(uri)) { 186 // Track the file, if not already tracked. 187 mFileTracker.put(uri, new ContentValues()); 188 } 189 } 190 return ParcelFileDescriptor.open(file, fileMode); 191 } 192 getRow(String[] columns, File file, String metadata)193 private Object[] getRow(String[] columns, File file, String metadata) { 194 Object[] values = new Object[columns.length]; 195 for (int i = 0; i < columns.length; i++) { 196 values[i] = getColumnValue(columns[i], file, metadata); 197 } 198 return values; 199 } 200 getColumnValue(String columnName, File file, String metadata)201 private Object getColumnValue(String columnName, File file, String metadata) { 202 Object value = null; 203 if (COLUMN_NAME.equals(columnName)) { 204 value = file.getName(); 205 } else if (COLUMN_ABSOLUTE_PATH.equals(columnName)) { 206 value = file.getAbsolutePath(); 207 } else if (COLUMN_DIRECTORY.equals(columnName)) { 208 value = file.isDirectory(); 209 } else if (COLUMN_METADATA.equals(columnName)) { 210 value = metadata; 211 } else if (COLUMN_MIME_TYPE.equals(columnName)) { 212 value = file.isDirectory() ? null : getType(file); 213 } 214 return value; 215 } 216 getType(File file)217 private String getType(File file) { 218 final int lastDot = file.getName().lastIndexOf('.'); 219 if (lastDot >= 0) { 220 final String extension = file.getName().substring(lastDot + 1); 221 final String mime = sMimeMap.getMimeTypeFromExtension(extension); 222 if (mime != null) { 223 return mime; 224 } 225 } 226 227 return "application/octet-stream"; 228 } 229 230 @SuppressLint("SdCardPath") getFileForUri(Uri uri)231 private File getFileForUri(Uri uri) { 232 // TODO: apply the /sdcard resolution to query() too. 233 String uriPath = uri.getPath(); 234 try { 235 uriPath = URLDecoder.decode(uriPath, "UTF-8"); 236 } catch (UnsupportedEncodingException e) { 237 throw new RuntimeException(e); 238 } 239 if (uriPath.startsWith("/sdcard/")) { 240 uriPath = 241 uriPath.replaceAll( 242 "/sdcard", Environment.getExternalStorageDirectory().getAbsolutePath()); 243 } 244 return new File(uriPath); 245 } 246 247 /** Copied from FileProvider.java. */ modeToMode(String mode)248 private static int modeToMode(String mode) { 249 int modeBits; 250 if ("r".equals(mode)) { 251 modeBits = ParcelFileDescriptor.MODE_READ_ONLY; 252 } else if ("w".equals(mode) || "wt".equals(mode)) { 253 modeBits = 254 ParcelFileDescriptor.MODE_WRITE_ONLY 255 | ParcelFileDescriptor.MODE_CREATE 256 | ParcelFileDescriptor.MODE_TRUNCATE; 257 } else if ("wa".equals(mode)) { 258 modeBits = 259 ParcelFileDescriptor.MODE_WRITE_ONLY 260 | ParcelFileDescriptor.MODE_CREATE 261 | ParcelFileDescriptor.MODE_APPEND; 262 } else if ("rw".equals(mode)) { 263 modeBits = ParcelFileDescriptor.MODE_READ_WRITE | ParcelFileDescriptor.MODE_CREATE; 264 } else if ("rwt".equals(mode)) { 265 modeBits = 266 ParcelFileDescriptor.MODE_READ_WRITE 267 | ParcelFileDescriptor.MODE_CREATE 268 | ParcelFileDescriptor.MODE_TRUNCATE; 269 } else { 270 throw new IllegalArgumentException("Invalid mode: " + mode); 271 } 272 return modeBits; 273 } 274 275 /** 276 * Recursively delete given file or directory and all its contents. 277 * 278 * @param rootDir the directory or file to be deleted; can be null 279 * @return The number of deleted files. 280 */ recursiveDelete(File rootDir)281 private int recursiveDelete(File rootDir) { 282 int count = 0; 283 if (rootDir != null) { 284 if (rootDir.isDirectory()) { 285 File[] childFiles = rootDir.listFiles(); 286 if (childFiles != null) { 287 for (File child : childFiles) { 288 count += recursiveDelete(child); 289 } 290 } 291 } 292 rootDir.delete(); 293 count++; 294 } 295 return count; 296 } 297 sortFilesByAbsolutePath(File[] files)298 private void sortFilesByAbsolutePath(File[] files) { 299 Arrays.sort( 300 files, 301 new Comparator<File>() { 302 @Override 303 public int compare(File f1, File f2) { 304 return f1.getAbsolutePath().compareTo(f2.getAbsolutePath()); 305 } 306 }); 307 } 308 } 309