ExternalStorageProvider.java revision 92d7e697a864a3e18bef4ef256bb3eb339a66b4e
1/* 2 * Copyright (C) 2013 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 17package com.android.externalstorage; 18 19import android.content.ContentProvider; 20import android.content.ContentResolver; 21import android.content.ContentValues; 22import android.content.UriMatcher; 23import android.database.Cursor; 24import android.database.MatrixCursor; 25import android.net.Uri; 26import android.os.Environment; 27import android.os.ParcelFileDescriptor; 28import android.provider.BaseColumns; 29import android.provider.DocumentsContract; 30import android.provider.DocumentsContract.DocumentColumns; 31import android.provider.DocumentsContract.RootColumns; 32import android.util.Log; 33import android.webkit.MimeTypeMap; 34 35import com.google.android.collect.Maps; 36 37import java.io.File; 38import java.io.FileNotFoundException; 39import java.io.IOException; 40import java.util.HashMap; 41import java.util.LinkedList; 42 43public class ExternalStorageProvider extends ContentProvider { 44 private static final String TAG = "ExternalStorage"; 45 46 private static final String AUTHORITY = "com.android.externalstorage"; 47 48 // TODO: support multiple storage devices 49 50 private static final UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH); 51 52 private static final int URI_ROOTS = 1; 53 private static final int URI_ROOTS_ID = 2; 54 private static final int URI_DOCS_ID = 3; 55 private static final int URI_DOCS_ID_CONTENTS = 4; 56 private static final int URI_DOCS_ID_SEARCH = 5; 57 58 private HashMap<String, Root> mRoots = Maps.newHashMap(); 59 60 private static class Root { 61 public int rootType; 62 public String name; 63 public int icon = 0; 64 public String title = null; 65 public String summary = null; 66 public File path; 67 } 68 69 static { 70 sMatcher.addURI(AUTHORITY, "roots", URI_ROOTS); 71 sMatcher.addURI(AUTHORITY, "roots/*", URI_ROOTS_ID); 72 sMatcher.addURI(AUTHORITY, "roots/*/docs/*", URI_DOCS_ID); 73 sMatcher.addURI(AUTHORITY, "roots/*/docs/*/contents", URI_DOCS_ID_CONTENTS); 74 sMatcher.addURI(AUTHORITY, "roots/*/docs/*/search", URI_DOCS_ID_SEARCH); 75 } 76 77 @Override 78 public boolean onCreate() { 79 mRoots.clear(); 80 81 final Root root = new Root(); 82 root.rootType = DocumentsContract.ROOT_TYPE_DEVICE_ADVANCED; 83 root.name = "primary"; 84 root.title = getContext().getString(R.string.root_internal_storage); 85 root.path = Environment.getExternalStorageDirectory(); 86 mRoots.put(root.name, root); 87 88 return true; 89 } 90 91 @Override 92 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 93 String sortOrder) { 94 95 // TODO: support custom projections 96 final String[] rootsProjection = new String[] { 97 BaseColumns._ID, RootColumns.ROOT_ID, RootColumns.ROOT_TYPE, RootColumns.ICON, 98 RootColumns.TITLE, RootColumns.SUMMARY, RootColumns.AVAILABLE_BYTES }; 99 final String[] docsProjection = new String[] { 100 BaseColumns._ID, DocumentColumns.DISPLAY_NAME, DocumentColumns.SIZE, 101 DocumentColumns.DOC_ID, DocumentColumns.MIME_TYPE, DocumentColumns.LAST_MODIFIED, 102 DocumentColumns.FLAGS }; 103 104 switch (sMatcher.match(uri)) { 105 case URI_ROOTS: { 106 final MatrixCursor cursor = new MatrixCursor(rootsProjection); 107 for (Root root : mRoots.values()) { 108 includeRoot(cursor, root); 109 } 110 return cursor; 111 } 112 case URI_ROOTS_ID: { 113 final String root = uri.getPathSegments().get(1); 114 115 final MatrixCursor cursor = new MatrixCursor(rootsProjection); 116 includeRoot(cursor, mRoots.get(root)); 117 return cursor; 118 } 119 case URI_DOCS_ID: { 120 final Root root = mRoots.get(uri.getPathSegments().get(1)); 121 final String docId = uri.getPathSegments().get(3); 122 123 final MatrixCursor cursor = new MatrixCursor(docsProjection); 124 final File file = docIdToFile(root, docId); 125 includeFile(cursor, root, file); 126 return cursor; 127 } 128 case URI_DOCS_ID_CONTENTS: { 129 final Root root = mRoots.get(uri.getPathSegments().get(1)); 130 final String docId = uri.getPathSegments().get(3); 131 132 final MatrixCursor cursor = new MatrixCursor(docsProjection); 133 final File parent = docIdToFile(root, docId); 134 for (File file : parent.listFiles()) { 135 includeFile(cursor, root, file); 136 } 137 return cursor; 138 } 139 case URI_DOCS_ID_SEARCH: { 140 final Root root = mRoots.get(uri.getPathSegments().get(1)); 141 final String docId = uri.getPathSegments().get(3); 142 final String query = uri.getQueryParameter(DocumentsContract.PARAM_QUERY).toLowerCase(); 143 144 final MatrixCursor cursor = new MatrixCursor(docsProjection); 145 final File parent = docIdToFile(root, docId); 146 147 final LinkedList<File> pending = new LinkedList<File>(); 148 pending.add(parent); 149 while (!pending.isEmpty() && cursor.getCount() < 20) { 150 final File file = pending.removeFirst(); 151 if (file.isDirectory()) { 152 for (File child : file.listFiles()) { 153 pending.add(child); 154 } 155 } else { 156 if (file.getName().toLowerCase().contains(query)) { 157 includeFile(cursor, root, file); 158 } 159 } 160 } 161 return cursor; 162 } 163 default: { 164 throw new UnsupportedOperationException("Unsupported Uri " + uri); 165 } 166 } 167 } 168 169 private String fileToDocId(Root root, File file) { 170 String rootPath = root.path.getAbsolutePath(); 171 final String path = file.getAbsolutePath(); 172 if (path.equals(rootPath)) { 173 return DocumentsContract.ROOT_DOC_ID; 174 } 175 176 if (!rootPath.endsWith("/")) { 177 rootPath += "/"; 178 } 179 if (!path.startsWith(rootPath)) { 180 throw new IllegalArgumentException("File " + path + " outside root " + root.path); 181 } else { 182 return path.substring(rootPath.length()); 183 } 184 } 185 186 private File docIdToFile(Root root, String docId) { 187 if (DocumentsContract.ROOT_DOC_ID.equals(docId)) { 188 return root.path; 189 } else { 190 return new File(root.path, docId); 191 } 192 } 193 194 private void includeRoot(MatrixCursor cursor, Root root) { 195 cursor.addRow(new Object[] { 196 root.name.hashCode(), root.name, root.rootType, root.icon, root.title, root.summary, 197 root.path.getFreeSpace() }); 198 } 199 200 private void includeFile(MatrixCursor cursor, Root root, File file) { 201 int flags = 0; 202 203 if (file.isDirectory()) { 204 flags |= DocumentsContract.FLAG_SUPPORTS_SEARCH; 205 } 206 if (file.isDirectory() && file.canWrite()) { 207 flags |= DocumentsContract.FLAG_SUPPORTS_CREATE; 208 } 209 if (file.canWrite()) { 210 flags |= DocumentsContract.FLAG_SUPPORTS_RENAME; 211 flags |= DocumentsContract.FLAG_SUPPORTS_DELETE; 212 } 213 214 final String mimeType = getTypeForFile(file); 215 if (mimeType.startsWith("image/")) { 216 flags |= DocumentsContract.FLAG_SUPPORTS_THUMBNAIL; 217 } 218 219 final String docId = fileToDocId(root, file); 220 final long id = docId.hashCode(); 221 cursor.addRow(new Object[] { 222 id, file.getName(), file.length(), docId, mimeType, file.lastModified(), flags }); 223 } 224 225 @Override 226 public String getType(Uri uri) { 227 switch (sMatcher.match(uri)) { 228 case URI_DOCS_ID: { 229 final Root root = mRoots.get(uri.getPathSegments().get(1)); 230 final String docId = uri.getPathSegments().get(3); 231 return getTypeForFile(docIdToFile(root, docId)); 232 } 233 default: { 234 throw new UnsupportedOperationException("Unsupported Uri " + uri); 235 } 236 } 237 } 238 239 private String getTypeForFile(File file) { 240 if (file.isDirectory()) { 241 return DocumentsContract.MIME_TYPE_DIRECTORY; 242 } else { 243 return getTypeForName(file.getName()); 244 } 245 } 246 247 private String getTypeForName(String name) { 248 final int lastDot = name.lastIndexOf('.'); 249 if (lastDot >= 0) { 250 final String extension = name.substring(lastDot + 1); 251 final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); 252 if (mime != null) { 253 return mime; 254 } 255 } 256 257 return "application/octet-stream"; 258 } 259 260 @Override 261 public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { 262 switch (sMatcher.match(uri)) { 263 case URI_DOCS_ID: { 264 final Root root = mRoots.get(uri.getPathSegments().get(1)); 265 final String docId = uri.getPathSegments().get(3); 266 267 // TODO: offer as thumbnail 268 final File file = docIdToFile(root, docId); 269 return ParcelFileDescriptor.open(file, ContentResolver.modeToMode(uri, mode)); 270 } 271 default: { 272 throw new UnsupportedOperationException("Unsupported Uri " + uri); 273 } 274 } 275 } 276 277 @Override 278 public Uri insert(Uri uri, ContentValues values) { 279 switch (sMatcher.match(uri)) { 280 case URI_DOCS_ID: { 281 final Root root = mRoots.get(uri.getPathSegments().get(1)); 282 final String docId = uri.getPathSegments().get(3); 283 284 final File parent = docIdToFile(root, docId); 285 286 final String mimeType = values.getAsString(DocumentColumns.MIME_TYPE); 287 final String name = validateDisplayName( 288 values.getAsString(DocumentColumns.DISPLAY_NAME), mimeType); 289 290 final File file = new File(parent, name); 291 if (DocumentsContract.MIME_TYPE_DIRECTORY.equals(mimeType)) { 292 if (!file.mkdir()) { 293 return null; 294 } 295 296 } else { 297 try { 298 if (!file.createNewFile()) { 299 return null; 300 } 301 } catch (IOException e) { 302 Log.w(TAG, "Failed to create file", e); 303 return null; 304 } 305 } 306 307 final String newDocId = fileToDocId(root, file); 308 return DocumentsContract.buildDocumentUri(AUTHORITY, root.name, newDocId); 309 } 310 default: { 311 throw new UnsupportedOperationException("Unsupported Uri " + uri); 312 } 313 } 314 } 315 316 @Override 317 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 318 switch (sMatcher.match(uri)) { 319 case URI_DOCS_ID: { 320 final Root root = mRoots.get(uri.getPathSegments().get(1)); 321 final String docId = uri.getPathSegments().get(3); 322 323 final File file = docIdToFile(root, docId); 324 final File newFile = new File( 325 file.getParentFile(), values.getAsString(DocumentColumns.DISPLAY_NAME)); 326 return file.renameTo(newFile) ? 1 : 0; 327 } 328 default: { 329 throw new UnsupportedOperationException("Unsupported Uri " + uri); 330 } 331 } 332 } 333 334 @Override 335 public int delete(Uri uri, String selection, String[] selectionArgs) { 336 switch (sMatcher.match(uri)) { 337 case URI_DOCS_ID: { 338 final Root root = mRoots.get(uri.getPathSegments().get(1)); 339 final String docId = uri.getPathSegments().get(3); 340 341 final File file = docIdToFile(root, docId); 342 return file.delete() ? 1 : 0; 343 } 344 default: { 345 throw new UnsupportedOperationException("Unsupported Uri " + uri); 346 } 347 } 348 } 349 350 private String validateDisplayName(String displayName, String mimeType) { 351 if (DocumentsContract.MIME_TYPE_DIRECTORY.equals(mimeType)) { 352 return displayName; 353 } else { 354 // Try appending meaningful extension if needed 355 if (!mimeType.equals(getTypeForName(displayName))) { 356 final String extension = MimeTypeMap.getSingleton() 357 .getExtensionFromMimeType(mimeType); 358 if (extension != null) { 359 displayName += "." + extension; 360 } 361 } 362 363 return displayName; 364 } 365 } 366} 367