MtpDocumentsProvider.java revision 124d060bc980c7555616ff9d07a4dc3b8f3cd341
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.os.CancellationSignal;
25import android.os.ParcelFileDescriptor;
26import android.provider.DocumentsContract;
27import android.provider.DocumentsContract.Document;
28import android.provider.DocumentsContract.Root;
29import android.provider.DocumentsProvider;
30import android.util.Log;
31
32import com.android.internal.annotations.VisibleForTesting;
33
34import java.io.FileNotFoundException;
35import java.io.IOException;
36
37/**
38 * DocumentsProvider for MTP devices.
39 */
40public class MtpDocumentsProvider extends DocumentsProvider {
41    static final String AUTHORITY = "com.android.mtp.documents";
42    static final String TAG = "MtpDocumentsProvider";
43    private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
44            Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON,
45            Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID,
46            Root.COLUMN_AVAILABLE_BYTES,
47    };
48    private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
49            Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE,
50            Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED,
51            Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
52    };
53
54    private static MtpDocumentsProvider sSingleton;
55
56    private MtpManager mMtpManager;
57    private ContentResolver mResolver;
58    private PipeManager mPipeManager;
59
60    /**
61     * Provides singleton instance to MtpDocumentsService.
62     */
63    static MtpDocumentsProvider getInstance() {
64        return sSingleton;
65    }
66
67    @Override
68    public boolean onCreate() {
69        sSingleton = this;
70        mMtpManager = new MtpManager(getContext());
71        mResolver = getContext().getContentResolver();
72        mPipeManager = new PipeManager();
73
74        return true;
75    }
76
77    @VisibleForTesting
78    void onCreateForTesting(MtpManager mtpManager, ContentResolver resolver) {
79        this.mMtpManager = mtpManager;
80        this.mResolver = resolver;
81    }
82
83    @Override
84    public Cursor queryRoots(String[] projection) throws FileNotFoundException {
85        if (projection == null) {
86            projection = MtpDocumentsProvider.DEFAULT_ROOT_PROJECTION;
87        }
88        final MatrixCursor cursor = new MatrixCursor(projection);
89        for (final int deviceId : mMtpManager.getOpenedDeviceIds()) {
90            try {
91                final MtpRoot[] roots = mMtpManager.getRoots(deviceId);
92                // TODO: Add retry logic here.
93
94                for (final MtpRoot root : roots) {
95                    final Identifier rootIdentifier = new Identifier(deviceId, root.mStorageId);
96                    final MatrixCursor.RowBuilder builder = cursor.newRow();
97                    builder.add(Root.COLUMN_ROOT_ID, rootIdentifier.toRootId());
98                    builder.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_IS_CHILD);
99                    builder.add(Root.COLUMN_TITLE, root.mDescription);
100                    builder.add(
101                            Root.COLUMN_DOCUMENT_ID,
102                            rootIdentifier.toDocumentId());
103                    builder.add(Root.COLUMN_AVAILABLE_BYTES , root.mFreeSpace);
104                }
105            } catch (IOException error) {
106                Log.d(TAG, error.getMessage());
107            }
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        MtpDocument document = null;
123        if (identifier.mObjectHandle != MtpDocument.DUMMY_HANDLE_FOR_ROOT) {
124            try {
125                document = mMtpManager.getDocument(identifier.mDeviceId, identifier.mObjectHandle);
126            } catch (IOException e) {
127                throw new FileNotFoundException(e.getMessage());
128            }
129        } else {
130            MtpRoot[] roots;
131            try {
132                roots = mMtpManager.getRoots(identifier.mDeviceId);
133                if (roots != null) {
134                    for (final MtpRoot root : roots) {
135                        if (identifier.mStorageId == root.mStorageId) {
136                            document = new MtpDocument(root);
137                            break;
138                        }
139                    }
140                }
141                if (document == null) {
142                    throw new FileNotFoundException();
143                }
144            } catch (IOException e) {
145                throw new FileNotFoundException(e.getMessage());
146            }
147        }
148
149        final MatrixCursor cursor = new MatrixCursor(projection);
150        document.addToCursor(
151                new Identifier(identifier.mDeviceId, identifier.mStorageId), cursor.newRow());
152        return cursor;
153    }
154
155    // TODO: Support background loading for large number of files.
156    @Override
157    public Cursor queryChildDocuments(String parentDocumentId,
158            String[] projection, String sortOrder) throws FileNotFoundException {
159        if (projection == null) {
160            projection = MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION;
161        }
162        final Identifier parentIdentifier = Identifier.createFromDocumentId(parentDocumentId);
163        int parentHandle = parentIdentifier.mObjectHandle;
164        // Need to pass the special value MtpManager.OBJECT_HANDLE_ROOT_CHILDREN to
165        // getObjectHandles if we would like to obtain children under the root.
166        if (parentHandle == MtpDocument.DUMMY_HANDLE_FOR_ROOT) {
167            parentHandle = MtpManager.OBJECT_HANDLE_ROOT_CHILDREN;
168        }
169        try {
170            final MatrixCursor cursor = new MatrixCursor(projection);
171            final Identifier rootIdentifier = new Identifier(
172                    parentIdentifier.mDeviceId, parentIdentifier.mStorageId);
173            final int[] objectHandles = mMtpManager.getObjectHandles(
174                    parentIdentifier.mDeviceId, parentIdentifier.mStorageId, parentHandle);
175            for (int i = 0; i < objectHandles.length; i++) {
176                try {
177                    final MtpDocument document = mMtpManager.getDocument(
178                            parentIdentifier.mDeviceId,  objectHandles[i]);
179                    document.addToCursor(rootIdentifier, cursor.newRow());
180                } catch (IOException error) {
181                    cursor.close();
182                    throw new FileNotFoundException(error.getMessage());
183                }
184            }
185            return cursor;
186        } catch (IOException exception) {
187            throw new FileNotFoundException(exception.getMessage());
188        }
189    }
190
191    @Override
192    public ParcelFileDescriptor openDocument(
193            String documentId, String mode, CancellationSignal signal)
194                    throws FileNotFoundException {
195        if (!"r".equals(mode) && !"w".equals(mode)) {
196            // TODO: Support seekable file.
197            throw new UnsupportedOperationException("The provider does not support seekable file.");
198        }
199        final Identifier identifier = Identifier.createFromDocumentId(documentId);
200        try {
201            return mPipeManager.readDocument(mMtpManager, identifier);
202        } catch (IOException error) {
203            throw new FileNotFoundException(error.getMessage());
204        }
205    }
206
207    @Override
208    public AssetFileDescriptor openDocumentThumbnail(
209            String documentId,
210            Point sizeHint,
211            CancellationSignal signal) throws FileNotFoundException {
212        final Identifier identifier = Identifier.createFromDocumentId(documentId);
213        try {
214            return new AssetFileDescriptor(
215                    mPipeManager.readThumbnail(mMtpManager, identifier),
216                    0,
217                    AssetFileDescriptor.UNKNOWN_LENGTH);
218        } catch (IOException error) {
219            throw new FileNotFoundException(error.getMessage());
220        }
221    }
222
223    @Override
224    public void deleteDocument(String documentId) throws FileNotFoundException {
225        try {
226            final Identifier identifier = Identifier.createFromDocumentId(documentId);
227            final int parentHandle =
228                    mMtpManager.getParent(identifier.mDeviceId, identifier.mObjectHandle);
229            mMtpManager.deleteDocument(identifier.mDeviceId, identifier.mObjectHandle);
230            notifyChildDocumentsChange(new Identifier(
231                    identifier.mDeviceId, identifier.mStorageId, parentHandle).toDocumentId());
232        } catch (IOException error) {
233            throw new FileNotFoundException(error.getMessage());
234        }
235    }
236
237    void openDevice(int deviceId) throws IOException {
238        mMtpManager.openDevice(deviceId);
239        notifyRootsChange();
240    }
241
242    void closeDevice(int deviceId) throws IOException {
243        mMtpManager.closeDevice(deviceId);
244        notifyRootsChange();
245    }
246
247    void closeAllDevices() {
248        boolean closed = false;
249        for (int deviceId : mMtpManager.getOpenedDeviceIds()) {
250            try {
251                mMtpManager.closeDevice(deviceId);
252                closed = true;
253            } catch (IOException d) {
254                Log.d(TAG, "Failed to close the MTP device: " + deviceId);
255            }
256        }
257        if (closed) {
258            notifyRootsChange();
259        }
260    }
261
262    boolean hasOpenedDevices() {
263        return mMtpManager.getOpenedDeviceIds().length != 0;
264    }
265
266    private void notifyRootsChange() {
267        mResolver.notifyChange(
268                DocumentsContract.buildRootsUri(MtpDocumentsProvider.AUTHORITY), null, false);
269    }
270
271    private void notifyChildDocumentsChange(String parentDocumentId) {
272        mResolver.notifyChange(
273                DocumentsContract.buildChildDocumentsUri(AUTHORITY, parentDocumentId),
274                null,
275                false);
276    }
277}
278