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