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.providers.downloads;
18
19import android.app.DownloadManager;
20import android.app.DownloadManager.Query;
21import android.content.ContentResolver;
22import android.content.Context;
23import android.content.res.AssetFileDescriptor;
24import android.database.Cursor;
25import android.database.MatrixCursor;
26import android.database.MatrixCursor.RowBuilder;
27import android.graphics.Point;
28import android.net.Uri;
29import android.os.Binder;
30import android.os.CancellationSignal;
31import android.os.Environment;
32import android.os.ParcelFileDescriptor;
33import android.provider.DocumentsContract;
34import android.provider.DocumentsContract.Document;
35import android.provider.DocumentsContract.Root;
36import android.provider.DocumentsProvider;
37import android.text.TextUtils;
38import android.webkit.MimeTypeMap;
39
40import libcore.io.IoUtils;
41
42import java.io.File;
43import java.io.FileNotFoundException;
44import java.io.IOException;
45
46/**
47 * Presents a {@link DocumentsContract} view of {@link DownloadManager}
48 * contents.
49 */
50public 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(), false, 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