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 17 package com.android.providers.downloads; 18 19 import android.app.DownloadManager; 20 import android.app.DownloadManager.Query; 21 import android.content.ContentResolver; 22 import android.content.Context; 23 import android.content.res.AssetFileDescriptor; 24 import android.database.Cursor; 25 import android.database.MatrixCursor; 26 import android.database.MatrixCursor.RowBuilder; 27 import android.graphics.Point; 28 import android.net.Uri; 29 import android.os.Binder; 30 import android.os.CancellationSignal; 31 import android.os.Environment; 32 import android.os.FileUtils; 33 import android.os.ParcelFileDescriptor; 34 import android.provider.DocumentsContract; 35 import android.provider.DocumentsContract.Document; 36 import android.provider.DocumentsContract.Root; 37 import android.provider.DocumentsProvider; 38 import android.support.provider.DocumentArchiveHelper; 39 import android.text.TextUtils; 40 import android.webkit.MimeTypeMap; 41 42 import libcore.io.IoUtils; 43 44 import java.io.File; 45 import java.io.FileNotFoundException; 46 import java.io.IOException; 47 import java.text.NumberFormat; 48 49 /** 50 * Presents a {@link DocumentsContract} view of {@link DownloadManager} 51 * contents. 52 */ 53 public class DownloadStorageProvider extends DocumentsProvider { 54 private static final String AUTHORITY = Constants.STORAGE_AUTHORITY; 55 private static final String DOC_ID_ROOT = Constants.STORAGE_ROOT_ID; 56 57 private static final String[] DEFAULT_ROOT_PROJECTION = new String[] { 58 Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, 59 Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID, 60 }; 61 62 private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] { 63 Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME, 64 Document.COLUMN_SUMMARY, Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, 65 Document.COLUMN_SIZE, 66 }; 67 68 private DownloadManager mDm; 69 private DocumentArchiveHelper mArchiveHelper; 70 71 @Override onCreate()72 public boolean onCreate() { 73 mDm = (DownloadManager) getContext().getSystemService(Context.DOWNLOAD_SERVICE); 74 mDm.setAccessAllDownloads(true); 75 mDm.setAccessFilename(true); 76 mArchiveHelper = new DocumentArchiveHelper(this, ':'); 77 return true; 78 } 79 resolveRootProjection(String[] projection)80 private static String[] resolveRootProjection(String[] projection) { 81 return projection != null ? projection : DEFAULT_ROOT_PROJECTION; 82 } 83 resolveDocumentProjection(String[] projection)84 private static String[] resolveDocumentProjection(String[] projection) { 85 return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION; 86 } 87 copyNotificationUri(MatrixCursor result, Cursor cursor)88 private void copyNotificationUri(MatrixCursor result, Cursor cursor) { 89 result.setNotificationUri(getContext().getContentResolver(), cursor.getNotificationUri()); 90 } 91 onDownloadProviderDelete(Context context, long id)92 static void onDownloadProviderDelete(Context context, long id) { 93 final Uri uri = DocumentsContract.buildDocumentUri(AUTHORITY, Long.toString(id)); 94 context.revokeUriPermission(uri, ~0); 95 } 96 97 @Override queryRoots(String[] projection)98 public Cursor queryRoots(String[] projection) throws FileNotFoundException { 99 final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection)); 100 final RowBuilder row = result.newRow(); 101 row.add(Root.COLUMN_ROOT_ID, DOC_ID_ROOT); 102 row.add(Root.COLUMN_FLAGS, 103 Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_RECENTS | Root.FLAG_SUPPORTS_CREATE); 104 row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher_download); 105 row.add(Root.COLUMN_TITLE, getContext().getString(R.string.root_downloads)); 106 row.add(Root.COLUMN_DOCUMENT_ID, DOC_ID_ROOT); 107 return result; 108 } 109 110 @Override createDocument(String docId, String mimeType, String displayName)111 public String createDocument(String docId, String mimeType, String displayName) 112 throws FileNotFoundException { 113 displayName = FileUtils.buildValidFatFilename(displayName); 114 115 if (Document.MIME_TYPE_DIR.equals(mimeType)) { 116 throw new FileNotFoundException("Directory creation not supported"); 117 } 118 119 final File parent = Environment.getExternalStoragePublicDirectory( 120 Environment.DIRECTORY_DOWNLOADS); 121 parent.mkdirs(); 122 123 // Delegate to real provider 124 final long token = Binder.clearCallingIdentity(); 125 try { 126 final File file = FileUtils.buildUniqueFile(parent, mimeType, displayName); 127 128 try { 129 if (!file.createNewFile()) { 130 throw new IllegalStateException("Failed to touch " + file); 131 } 132 } catch (IOException e) { 133 throw new IllegalStateException("Failed to touch " + file + ": " + e); 134 } 135 136 return Long.toString(mDm.addCompletedDownload( 137 file.getName(), file.getName(), true, mimeType, file.getAbsolutePath(), 0L, 138 false, true)); 139 } finally { 140 Binder.restoreCallingIdentity(token); 141 } 142 } 143 144 @Override deleteDocument(String docId)145 public void deleteDocument(String docId) throws FileNotFoundException { 146 // Delegate to real provider 147 final long token = Binder.clearCallingIdentity(); 148 try { 149 if (mDm.remove(Long.parseLong(docId)) != 1) { 150 throw new IllegalStateException("Failed to delete " + docId); 151 } 152 } finally { 153 Binder.restoreCallingIdentity(token); 154 } 155 } 156 157 @Override renameDocument(String documentId, String displayName)158 public String renameDocument(String documentId, String displayName) 159 throws FileNotFoundException { 160 displayName = FileUtils.buildValidFatFilename(displayName); 161 162 final long token = Binder.clearCallingIdentity(); 163 try { 164 final long id = Long.parseLong(documentId); 165 166 if (!mDm.rename(getContext(), id, displayName)) { 167 throw new IllegalStateException( 168 "Failed to rename to " + displayName + " in downloadsManager"); 169 } 170 } finally { 171 Binder.restoreCallingIdentity(token); 172 } 173 return null; 174 } 175 176 @Override queryDocument(String docId, String[] projection)177 public Cursor queryDocument(String docId, String[] projection) throws FileNotFoundException { 178 if (mArchiveHelper.isArchivedDocument(docId)) { 179 return mArchiveHelper.queryDocument(docId, projection); 180 } 181 182 final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); 183 184 if (DOC_ID_ROOT.equals(docId)) { 185 includeDefaultDocument(result); 186 } else { 187 // Delegate to real provider 188 final long token = Binder.clearCallingIdentity(); 189 Cursor cursor = null; 190 try { 191 cursor = mDm.query(new Query().setFilterById(Long.parseLong(docId))); 192 copyNotificationUri(result, cursor); 193 if (cursor.moveToFirst()) { 194 // We don't know if this queryDocument() call is from Downloads (manage) 195 // or Files. Safely assume it's Files. 196 includeDownloadFromCursor(result, cursor); 197 } 198 } finally { 199 IoUtils.closeQuietly(cursor); 200 Binder.restoreCallingIdentity(token); 201 } 202 } 203 return result; 204 } 205 206 @Override queryChildDocuments(String docId, String[] projection, String sortOrder)207 public Cursor queryChildDocuments(String docId, String[] projection, String sortOrder) 208 throws FileNotFoundException { 209 if (mArchiveHelper.isArchivedDocument(docId) || 210 mArchiveHelper.isSupportedArchiveType(getDocumentType(docId))) { 211 return mArchiveHelper.queryChildDocuments(docId, projection, sortOrder); 212 } 213 214 final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); 215 216 // Delegate to real provider 217 final long token = Binder.clearCallingIdentity(); 218 Cursor cursor = null; 219 try { 220 cursor = mDm.query(new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true) 221 .setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL)); 222 copyNotificationUri(result, cursor); 223 while (cursor.moveToNext()) { 224 includeDownloadFromCursor(result, cursor); 225 } 226 } finally { 227 IoUtils.closeQuietly(cursor); 228 Binder.restoreCallingIdentity(token); 229 } 230 return result; 231 } 232 233 @Override queryChildDocumentsForManage( String parentDocumentId, String[] projection, String sortOrder)234 public Cursor queryChildDocumentsForManage( 235 String parentDocumentId, String[] projection, String sortOrder) 236 throws FileNotFoundException { 237 if (mArchiveHelper.isArchivedDocument(parentDocumentId)) { 238 return mArchiveHelper.queryDocument(parentDocumentId, projection); 239 } 240 241 final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); 242 243 // Delegate to real provider 244 final long token = Binder.clearCallingIdentity(); 245 Cursor cursor = null; 246 try { 247 cursor = mDm.query( 248 new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true)); 249 copyNotificationUri(result, cursor); 250 while (cursor.moveToNext()) { 251 includeDownloadFromCursor(result, cursor); 252 } 253 } finally { 254 IoUtils.closeQuietly(cursor); 255 Binder.restoreCallingIdentity(token); 256 } 257 return result; 258 } 259 260 @Override queryRecentDocuments(String rootId, String[] projection)261 public Cursor queryRecentDocuments(String rootId, String[] projection) 262 throws FileNotFoundException { 263 final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection)); 264 265 // Delegate to real provider 266 final long token = Binder.clearCallingIdentity(); 267 Cursor cursor = null; 268 try { 269 cursor = mDm.query(new DownloadManager.Query().setOnlyIncludeVisibleInDownloadsUi(true) 270 .setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL)); 271 copyNotificationUri(result, cursor); 272 while (cursor.moveToNext() && result.getCount() < 12) { 273 final String mimeType = cursor.getString( 274 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIA_TYPE)); 275 final String uri = cursor.getString( 276 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIAPROVIDER_URI)); 277 278 // Skip images that have been inserted into the MediaStore so we 279 // don't duplicate them in the recents list. 280 if (mimeType == null 281 || (mimeType.startsWith("image/") && !TextUtils.isEmpty(uri))) { 282 continue; 283 } 284 285 includeDownloadFromCursor(result, cursor); 286 } 287 } finally { 288 IoUtils.closeQuietly(cursor); 289 Binder.restoreCallingIdentity(token); 290 } 291 return result; 292 } 293 294 @Override openDocument(String docId, String mode, CancellationSignal signal)295 public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal) 296 throws FileNotFoundException { 297 if (mArchiveHelper.isArchivedDocument(docId)) { 298 return mArchiveHelper.openDocument(docId, mode, signal); 299 } 300 301 // Delegate to real provider 302 final long token = Binder.clearCallingIdentity(); 303 try { 304 final long id = Long.parseLong(docId); 305 final ContentResolver resolver = getContext().getContentResolver(); 306 return resolver.openFileDescriptor(mDm.getDownloadUri(id), mode, signal); 307 } finally { 308 Binder.restoreCallingIdentity(token); 309 } 310 } 311 312 @Override openDocumentThumbnail( String docId, Point sizeHint, CancellationSignal signal)313 public AssetFileDescriptor openDocumentThumbnail( 314 String docId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException { 315 // TODO: extend ExifInterface to support fds 316 final ParcelFileDescriptor pfd = openDocument(docId, "r", signal); 317 return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH); 318 } 319 includeDefaultDocument(MatrixCursor result)320 private void includeDefaultDocument(MatrixCursor result) { 321 final RowBuilder row = result.newRow(); 322 row.add(Document.COLUMN_DOCUMENT_ID, DOC_ID_ROOT); 323 row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR); 324 row.add(Document.COLUMN_FLAGS, 325 Document.FLAG_DIR_PREFERS_LAST_MODIFIED | Document.FLAG_DIR_SUPPORTS_CREATE); 326 } 327 includeDownloadFromCursor(MatrixCursor result, Cursor cursor)328 private void includeDownloadFromCursor(MatrixCursor result, Cursor cursor) { 329 final long id = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_ID)); 330 final String docId = String.valueOf(id); 331 332 final String displayName = cursor.getString( 333 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TITLE)); 334 String summary = cursor.getString( 335 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_DESCRIPTION)); 336 String mimeType = cursor.getString( 337 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_MEDIA_TYPE)); 338 if (mimeType == null) { 339 // Provide fake MIME type so it's openable 340 mimeType = "vnd.android.document/file"; 341 } 342 Long size = cursor.getLong( 343 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)); 344 if (size == -1) { 345 size = null; 346 } 347 348 int extraFlags = Document.FLAG_PARTIAL; 349 final int status = cursor.getInt( 350 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)); 351 switch (status) { 352 case DownloadManager.STATUS_SUCCESSFUL: 353 extraFlags = Document.FLAG_SUPPORTS_RENAME; // only successful is non-partial 354 break; 355 case DownloadManager.STATUS_PAUSED: 356 summary = getContext().getString(R.string.download_queued); 357 break; 358 case DownloadManager.STATUS_PENDING: 359 summary = getContext().getString(R.string.download_queued); 360 break; 361 case DownloadManager.STATUS_RUNNING: 362 final long progress = cursor.getLong(cursor.getColumnIndexOrThrow( 363 DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)); 364 if (size != null) { 365 String percent = 366 NumberFormat.getPercentInstance().format((double) progress / size); 367 summary = getContext().getString(R.string.download_running_percent, percent); 368 } else { 369 summary = getContext().getString(R.string.download_running); 370 } 371 break; 372 case DownloadManager.STATUS_FAILED: 373 default: 374 summary = getContext().getString(R.string.download_error); 375 break; 376 } 377 378 int flags = Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_WRITE | extraFlags; 379 if (mimeType.startsWith("image/")) { 380 flags |= Document.FLAG_SUPPORTS_THUMBNAIL; 381 } 382 383 if (mArchiveHelper.isSupportedArchiveType(mimeType)) { 384 flags |= Document.FLAG_ARCHIVE; 385 } 386 387 final long lastModified = cursor.getLong( 388 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP)); 389 390 final RowBuilder row = result.newRow(); 391 row.add(Document.COLUMN_DOCUMENT_ID, docId); 392 row.add(Document.COLUMN_DISPLAY_NAME, displayName); 393 row.add(Document.COLUMN_SUMMARY, summary); 394 row.add(Document.COLUMN_SIZE, size); 395 row.add(Document.COLUMN_MIME_TYPE, mimeType); 396 row.add(Document.COLUMN_FLAGS, flags); 397 // Incomplete downloads get a null timestamp. This prevents thrashy UI when a bunch of 398 // active downloads get sorted by mod time. 399 if (status != DownloadManager.STATUS_RUNNING) { 400 row.add(Document.COLUMN_LAST_MODIFIED, lastModified); 401 } 402 403 final String localFilePath = cursor.getString( 404 cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_FILENAME)); 405 if (localFilePath != null) { 406 row.add(DocumentArchiveHelper.COLUMN_LOCAL_FILE_PATH, localFilePath); 407 } 408 } 409 410 /** 411 * Remove file extension from name, but only if exact MIME type mapping 412 * exists. This means we can reapply the extension later. 413 */ removeExtension(String mimeType, String name)414 private static String removeExtension(String mimeType, String name) { 415 final int lastDot = name.lastIndexOf('.'); 416 if (lastDot >= 0) { 417 final String extension = name.substring(lastDot + 1); 418 final String nameMime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); 419 if (mimeType.equals(nameMime)) { 420 return name.substring(0, lastDot); 421 } 422 } 423 return name; 424 } 425 426 /** 427 * Add file extension to name, but only if exact MIME type mapping exists. 428 */ addExtension(String mimeType, String name)429 private static String addExtension(String mimeType, String name) { 430 final String extension = MimeTypeMap.getSingleton() 431 .getExtensionFromMimeType(mimeType); 432 if (extension != null) { 433 return name + "." + extension; 434 } 435 return name; 436 } 437 } 438