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