MtpDocumentsProvider.java revision 4c1d3dde05308cb10187269dd9824c9bfdbb27de
1/*
2 * Copyright (C) 2015 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.mtp;
18
19import android.content.ContentResolver;
20import android.content.res.AssetFileDescriptor;
21import android.database.Cursor;
22import android.database.MatrixCursor;
23import android.graphics.Point;
24import android.mtp.MtpObjectInfo;
25import android.os.CancellationSignal;
26import android.os.ParcelFileDescriptor;
27import android.provider.DocumentsContract.Document;
28import android.provider.DocumentsContract.Root;
29import android.provider.DocumentsContract;
30import android.provider.DocumentsProvider;
31import android.util.Log;
32
33import com.android.internal.annotations.VisibleForTesting;
34
35import java.io.FileNotFoundException;
36import java.io.IOException;
37import java.util.HashMap;
38import java.util.Map;
39
40/**
41 * DocumentsProvider for MTP devices.
42 */
43public class MtpDocumentsProvider extends DocumentsProvider {
44    static final String AUTHORITY = "com.android.mtp.documents";
45    static final String TAG = "MtpDocumentsProvider";
46    static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
47            Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON,
48            Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID,
49            Root.COLUMN_AVAILABLE_BYTES,
50    };
51    static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
52            Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE,
53            Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED,
54            Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
55    };
56
57    private static MtpDocumentsProvider sSingleton;
58
59    private MtpManager mMtpManager;
60    private ContentResolver mResolver;
61    private Map<Integer, DeviceToolkit> mDeviceToolkits;
62    private DocumentLoader mDocumentLoaders;
63    private RootScanner mRootScanner;
64
65    /**
66     * Provides singleton instance to MtpDocumentsService.
67     */
68    static MtpDocumentsProvider getInstance() {
69        return sSingleton;
70    }
71
72    @Override
73    public boolean onCreate() {
74        sSingleton = this;
75        mMtpManager = new MtpManager(getContext());
76        mResolver = getContext().getContentResolver();
77        mDeviceToolkits = new HashMap<Integer, DeviceToolkit>();
78        mRootScanner = new RootScanner(mResolver, mMtpManager);
79        return true;
80    }
81
82    @VisibleForTesting
83    void onCreateForTesting(MtpManager mtpManager, ContentResolver resolver) {
84        mMtpManager = mtpManager;
85        mResolver = resolver;
86        mDeviceToolkits = new HashMap<Integer, DeviceToolkit>();
87        mRootScanner = new RootScanner(mResolver, mMtpManager);
88    }
89
90    @Override
91    public Cursor queryRoots(String[] projection) throws FileNotFoundException {
92        if (projection == null) {
93            projection = MtpDocumentsProvider.DEFAULT_ROOT_PROJECTION;
94        }
95        final MatrixCursor cursor = new MatrixCursor(projection);
96        final MtpRoot[] roots = mRootScanner.getRoots();
97        for (final MtpRoot root : roots) {
98            final Identifier rootIdentifier = new Identifier(root.mDeviceId, root.mStorageId);
99            final MatrixCursor.RowBuilder builder = cursor.newRow();
100            builder.add(Root.COLUMN_ROOT_ID, rootIdentifier.toRootId());
101            builder.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_IS_CHILD | Root.FLAG_SUPPORTS_CREATE);
102            builder.add(Root.COLUMN_TITLE, root.mDescription);
103            builder.add(
104                    Root.COLUMN_DOCUMENT_ID,
105                    rootIdentifier.toDocumentId());
106            builder.add(Root.COLUMN_AVAILABLE_BYTES , root.mFreeSpace);
107        }
108        cursor.setNotificationUri(
109                mResolver, DocumentsContract.buildRootsUri(MtpDocumentsProvider.AUTHORITY));
110        return cursor;
111    }
112
113    @Override
114    public Cursor queryDocument(String documentId, String[] projection)
115            throws FileNotFoundException {
116        if (projection == null) {
117            projection = MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION;
118        }
119        final Identifier identifier = Identifier.createFromDocumentId(documentId);
120
121        if (identifier.mObjectHandle != CursorHelper.DUMMY_HANDLE_FOR_ROOT) {
122            MtpObjectInfo objectInfo;
123            try {
124                objectInfo = mMtpManager.getObjectInfo(
125                        identifier.mDeviceId, identifier.mObjectHandle);
126            } catch (IOException e) {
127                throw new FileNotFoundException(e.getMessage());
128            }
129            final MatrixCursor cursor = new MatrixCursor(projection);
130            CursorHelper.addToCursor(
131                    objectInfo,
132                    new Identifier(identifier.mDeviceId, identifier.mStorageId),
133                    cursor.newRow());
134            return cursor;
135        } else {
136            MtpRoot[] roots;
137            try {
138                roots = mMtpManager.getRoots(identifier.mDeviceId);
139            } catch (IOException e) {
140                throw new FileNotFoundException(e.getMessage());
141            }
142            for (final MtpRoot root : roots) {
143                if (identifier.mStorageId != root.mStorageId)
144                    continue;
145                final MatrixCursor cursor = new MatrixCursor(projection);
146                CursorHelper.addToCursor(root, cursor.newRow());
147                return cursor;
148            }
149        }
150
151        throw new FileNotFoundException();
152    }
153
154    @Override
155    public Cursor queryChildDocuments(String parentDocumentId,
156            String[] projection, String sortOrder) throws FileNotFoundException {
157        if (projection == null) {
158            projection = MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION;
159        }
160        final Identifier parentIdentifier = Identifier.createFromDocumentId(parentDocumentId);
161        try {
162            return getDocumentLoader(parentIdentifier).queryChildDocuments(
163                    projection, parentIdentifier);
164        } catch (IOException exception) {
165            throw new FileNotFoundException(exception.getMessage());
166        }
167    }
168
169    @Override
170    public ParcelFileDescriptor openDocument(
171            String documentId, String mode, CancellationSignal signal)
172                    throws FileNotFoundException {
173        final Identifier identifier = Identifier.createFromDocumentId(documentId);
174        try {
175            switch (mode) {
176                case "r":
177                    return getPipeManager(identifier).readDocument(mMtpManager, identifier);
178                case "w":
179                    // TODO: Clear the parent document loader task (if exists) and call notify
180                    // when writing is completed.
181                    return getPipeManager(identifier).writeDocument(
182                            getContext(), mMtpManager, identifier);
183                default:
184                    // TODO: Add support for seekable files.
185                    throw new UnsupportedOperationException(
186                            "The provider does not support seekable file.");
187            }
188        } catch (IOException error) {
189            throw new FileNotFoundException(error.getMessage());
190        }
191    }
192
193    @Override
194    public AssetFileDescriptor openDocumentThumbnail(
195            String documentId,
196            Point sizeHint,
197            CancellationSignal signal) throws FileNotFoundException {
198        final Identifier identifier = Identifier.createFromDocumentId(documentId);
199        try {
200            return new AssetFileDescriptor(
201                    getPipeManager(identifier).readThumbnail(mMtpManager, identifier),
202                    0,  // Start offset.
203                    AssetFileDescriptor.UNKNOWN_LENGTH);
204        } catch (IOException error) {
205            throw new FileNotFoundException(error.getMessage());
206        }
207    }
208
209    @Override
210    public void deleteDocument(String documentId) throws FileNotFoundException {
211        try {
212            final Identifier identifier = Identifier.createFromDocumentId(documentId);
213            final int parentHandle =
214                    mMtpManager.getParent(identifier.mDeviceId, identifier.mObjectHandle);
215            mMtpManager.deleteDocument(identifier.mDeviceId, identifier.mObjectHandle);
216            final Identifier parentIdentifier = new Identifier(
217                    identifier.mDeviceId, identifier.mStorageId, parentHandle);
218            getDocumentLoader(parentIdentifier).clearTask(parentIdentifier);
219            notifyChildDocumentsChange(parentIdentifier.toDocumentId());
220        } catch (IOException error) {
221            throw new FileNotFoundException(error.getMessage());
222        }
223    }
224
225    @Override
226    public void onTrimMemory(int level) {
227      for (final DeviceToolkit toolkit : mDeviceToolkits.values()) {
228          toolkit.mDocumentLoader.clearCompletedTasks();
229      }
230    }
231
232    @Override
233    public String createDocument(String parentDocumentId, String mimeType, String displayName)
234            throws FileNotFoundException {
235        try {
236            final Identifier parentId = Identifier.createFromDocumentId(parentDocumentId);
237            final ParcelFileDescriptor pipe[] = ParcelFileDescriptor.createReliablePipe();
238            pipe[0].close();  // 0 bytes for a new document.
239            final int objectHandle = mMtpManager.createDocument(
240                    parentId.mDeviceId,
241                    new MtpObjectInfo.Builder()
242                            .setStorageId(parentId.mStorageId)
243                            .setParent(parentId.mObjectHandle)
244                            .setFormat(CursorHelper.mimeTypeToFormatType(mimeType))
245                            .setName(displayName)
246                            .build(), pipe[1]);
247            final String documentId = new Identifier(parentId.mDeviceId, parentId.mStorageId,
248                   objectHandle).toDocumentId();
249            getDocumentLoader(parentId).clearTask(parentId);
250            notifyChildDocumentsChange(parentDocumentId);
251            return documentId;
252        } catch (IOException error) {
253            Log.e(TAG, error.getMessage());
254            throw new FileNotFoundException(error.getMessage());
255        }
256    }
257
258    void openDevice(int deviceId) throws IOException {
259        mMtpManager.openDevice(deviceId);
260        mDeviceToolkits.put(deviceId, new DeviceToolkit(mMtpManager, mResolver));
261        mRootScanner.scanNow();
262    }
263
264    void closeDevice(int deviceId) throws IOException {
265        // TODO: Flush the device before closing (if not closed externally).
266        getDeviceToolkit(deviceId).mDocumentLoader.clearTasks();
267        mDeviceToolkits.remove(deviceId);
268        mMtpManager.closeDevice(deviceId);
269        mRootScanner.scanNow();
270    }
271
272    void closeAllDevices() {
273        boolean closed = false;
274        for (int deviceId : mMtpManager.getOpenedDeviceIds()) {
275            try {
276                mMtpManager.closeDevice(deviceId);
277                getDeviceToolkit(deviceId).mDocumentLoader.clearTasks();
278                closed = true;
279            } catch (IOException d) {
280                Log.d(TAG, "Failed to close the MTP device: " + deviceId);
281            }
282        }
283        if (closed) {
284            mRootScanner.scanNow();
285        }
286    }
287
288    boolean hasOpenedDevices() {
289        return mMtpManager.getOpenedDeviceIds().length != 0;
290    }
291
292    private void notifyChildDocumentsChange(String parentDocumentId) {
293        mResolver.notifyChange(
294                DocumentsContract.buildChildDocumentsUri(AUTHORITY, parentDocumentId),
295                null,
296                false);
297    }
298
299    private DeviceToolkit getDeviceToolkit(int deviceId) throws FileNotFoundException {
300        final DeviceToolkit toolkit = mDeviceToolkits.get(deviceId);
301        if (toolkit == null) {
302            throw new FileNotFoundException();
303        }
304        return toolkit;
305    }
306
307    private PipeManager getPipeManager(Identifier identifier) throws FileNotFoundException {
308        return getDeviceToolkit(identifier.mDeviceId).mPipeManager;
309    }
310
311    private DocumentLoader getDocumentLoader(Identifier identifier) throws FileNotFoundException {
312        return getDeviceToolkit(identifier.mDeviceId).mDocumentLoader;
313    }
314
315    private static class DeviceToolkit {
316        public final PipeManager mPipeManager;
317        public final DocumentLoader mDocumentLoader;
318
319        public DeviceToolkit(MtpManager manager, ContentResolver resolver) {
320            mPipeManager = new PipeManager();
321            mDocumentLoader = new DocumentLoader(manager, resolver);
322        }
323    }
324}
325