MtpDocumentsProvider.java revision f52ef008c76566f7118a80bf28f599ba48d7c578
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 static com.android.internal.util.Preconditions.checkArgument;
20
21import android.content.ContentResolver;
22import android.content.res.AssetFileDescriptor;
23import android.content.res.Resources;
24import android.database.Cursor;
25import android.graphics.Point;
26import android.media.MediaFile;
27import android.mtp.MtpConstants;
28import android.mtp.MtpObjectInfo;
29import android.os.CancellationSignal;
30import android.os.ParcelFileDescriptor;
31import android.os.storage.StorageManager;
32import android.provider.DocumentsContract.Document;
33import android.provider.DocumentsContract.Root;
34import android.provider.DocumentsContract;
35import android.provider.DocumentsProvider;
36import android.util.Log;
37
38import com.android.internal.annotations.GuardedBy;
39import com.android.internal.annotations.VisibleForTesting;
40import com.android.internal.util.Preconditions;
41
42import java.io.FileNotFoundException;
43import java.io.IOException;
44import java.util.HashMap;
45import java.util.Map;
46
47/**
48 * DocumentsProvider for MTP devices.
49 */
50public class MtpDocumentsProvider extends DocumentsProvider {
51    static final String AUTHORITY = "com.android.mtp.documents";
52    static final String TAG = "MtpDocumentsProvider";
53    static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
54            Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON,
55            Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID,
56            Root.COLUMN_AVAILABLE_BYTES,
57    };
58    static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
59            Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE,
60            Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED,
61            Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
62    };
63
64    private final Object mDeviceListLock = new Object();
65
66    private static MtpDocumentsProvider sSingleton;
67
68    private MtpManager mMtpManager;
69    private ContentResolver mResolver;
70    @GuardedBy("mDeviceListLock")
71    private Map<Integer, DeviceToolkit> mDeviceToolkits;
72    private RootScanner mRootScanner;
73    private Resources mResources;
74    private MtpDatabase mDatabase;
75    private AppFuse mAppFuse;
76
77    /**
78     * Provides singleton instance to MtpDocumentsService.
79     */
80    static MtpDocumentsProvider getInstance() {
81        return sSingleton;
82    }
83
84    @Override
85    public boolean onCreate() {
86        sSingleton = this;
87        mResources = getContext().getResources();
88        mMtpManager = new MtpManager(getContext());
89        mResolver = getContext().getContentResolver();
90        mDeviceToolkits = new HashMap<Integer, DeviceToolkit>();
91        mDatabase = new MtpDatabase(getContext(), MtpDatabaseConstants.FLAG_DATABASE_IN_FILE);
92        mRootScanner = new RootScanner(mResolver, mResources, mMtpManager, mDatabase);
93        mAppFuse = new AppFuse(TAG, new AppFuseCallback());
94        // TODO: Mount AppFuse on demands.
95        mAppFuse.mount(getContext().getSystemService(StorageManager.class));
96        resume();
97        return true;
98    }
99
100    @VisibleForTesting
101    void onCreateForTesting(
102            Resources resources,
103            MtpManager mtpManager,
104            ContentResolver resolver,
105            MtpDatabase database) {
106        mResources = resources;
107        mMtpManager = mtpManager;
108        mResolver = resolver;
109        mDeviceToolkits = new HashMap<Integer, DeviceToolkit>();
110        mDatabase = database;
111        mRootScanner = new RootScanner(mResolver, mResources, mMtpManager, mDatabase);
112        resume();
113    }
114
115    @Override
116    public Cursor queryRoots(String[] projection) throws FileNotFoundException {
117        if (projection == null) {
118            projection = MtpDocumentsProvider.DEFAULT_ROOT_PROJECTION;
119        }
120        final Cursor cursor = mDatabase.queryRoots(projection);
121        cursor.setNotificationUri(
122                mResolver, DocumentsContract.buildRootsUri(MtpDocumentsProvider.AUTHORITY));
123        return cursor;
124    }
125
126    @Override
127    public Cursor queryDocument(String documentId, String[] projection)
128            throws FileNotFoundException {
129        if (projection == null) {
130            projection = MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION;
131        }
132        return mDatabase.queryDocument(documentId, projection);
133    }
134
135    @Override
136    public Cursor queryChildDocuments(String parentDocumentId,
137            String[] projection, String sortOrder) throws FileNotFoundException {
138        if (projection == null) {
139            projection = MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION;
140        }
141        final Identifier parentIdentifier = mDatabase.createIdentifier(parentDocumentId);
142        try {
143            return getDocumentLoader(parentIdentifier).queryChildDocuments(
144                    projection, parentIdentifier);
145        } catch (IOException exception) {
146            throw new FileNotFoundException(exception.getMessage());
147        }
148    }
149
150    @Override
151    public ParcelFileDescriptor openDocument(
152            String documentId, String mode, CancellationSignal signal)
153                    throws FileNotFoundException {
154        final Identifier identifier = mDatabase.createIdentifier(documentId);
155        try {
156            switch (mode) {
157                case "r":
158                    final long fileSize = getFileSize(documentId);
159                    // MTP getPartialObject operation does not support files that are larger than 4GB.
160                    // Fallback to non-seekable file descriptor.
161                    // TODO: Use getPartialObject64 for MTP devices that support Android vendor
162                    // extension.
163                    if (fileSize <= 0xffffffff) {
164                        return mAppFuse.openFile(Integer.parseInt(documentId));
165                    } else {
166                        return getPipeManager(identifier).readDocument(mMtpManager, identifier);
167                    }
168                case "w":
169                    // TODO: Clear the parent document loader task (if exists) and call notify
170                    // when writing is completed.
171                    return getPipeManager(identifier).writeDocument(
172                            getContext(), mMtpManager, identifier);
173                case "rw":
174                    // TODO: Add support for "rw" mode.
175                    throw new UnsupportedOperationException(
176                            "The provider does not support 'rw' mode.");
177                default:
178                    throw new IllegalArgumentException("Unknown mode for openDocument: " + mode);
179            }
180        } catch (IOException error) {
181            throw new FileNotFoundException(error.getMessage());
182        }
183    }
184
185    @Override
186    public AssetFileDescriptor openDocumentThumbnail(
187            String documentId,
188            Point sizeHint,
189            CancellationSignal signal) throws FileNotFoundException {
190        final Identifier identifier = mDatabase.createIdentifier(documentId);
191        try {
192            return new AssetFileDescriptor(
193                    getPipeManager(identifier).readThumbnail(mMtpManager, identifier),
194                    0,  // Start offset.
195                    AssetFileDescriptor.UNKNOWN_LENGTH);
196        } catch (IOException error) {
197            throw new FileNotFoundException(error.getMessage());
198        }
199    }
200
201    @Override
202    public void deleteDocument(String documentId) throws FileNotFoundException {
203        try {
204            final Identifier identifier = mDatabase.createIdentifier(documentId);
205            final Identifier parentIdentifier =
206                    mDatabase.createIdentifier(mDatabase.getParentId(documentId));
207            mMtpManager.deleteDocument(identifier.mDeviceId, identifier.mObjectHandle);
208            mDatabase.deleteDocument(documentId);
209            getDocumentLoader(parentIdentifier).clearTask(parentIdentifier);
210            notifyChildDocumentsChange(parentIdentifier.mDocumentId);
211        } catch (IOException error) {
212            throw new FileNotFoundException(error.getMessage());
213        }
214    }
215
216    @Override
217    public void onTrimMemory(int level) {
218        synchronized (mDeviceListLock) {
219            for (final DeviceToolkit toolkit : mDeviceToolkits.values()) {
220                toolkit.mDocumentLoader.clearCompletedTasks();
221            }
222        }
223    }
224
225    @Override
226    public String createDocument(String parentDocumentId, String mimeType, String displayName)
227            throws FileNotFoundException {
228        try {
229            final Identifier parentId = mDatabase.createIdentifier(parentDocumentId);
230            final ParcelFileDescriptor pipe[] = ParcelFileDescriptor.createReliablePipe();
231            pipe[0].close();  // 0 bytes for a new document.
232            final int formatCode = Document.MIME_TYPE_DIR.equals(mimeType) ?
233                    MtpConstants.FORMAT_ASSOCIATION :
234                    MediaFile.getFormatCode(displayName, mimeType);
235            final MtpObjectInfo info = new MtpObjectInfo.Builder()
236                    .setStorageId(parentId.mStorageId)
237                    .setParent(parentId.mObjectHandle)
238                    .setFormat(formatCode)
239                    .setName(displayName)
240                    .build();
241            final int objectHandle = mMtpManager.createDocument(parentId.mDeviceId, info, pipe[1]);
242            final MtpObjectInfo infoWithHandle =
243                    new MtpObjectInfo.Builder(info).setObjectHandle(objectHandle).build();
244            final String documentId = mDatabase.putNewDocument(
245                    parentId.mDeviceId, parentDocumentId, infoWithHandle);
246            getDocumentLoader(parentId).clearTask(parentId);
247            notifyChildDocumentsChange(parentDocumentId);
248            return documentId;
249        } catch (IOException error) {
250            Log.e(TAG, error.getMessage());
251            throw new FileNotFoundException(error.getMessage());
252        }
253    }
254
255    void openDevice(int deviceId) throws IOException {
256        synchronized (mDeviceListLock) {
257            mMtpManager.openDevice(deviceId);
258            mDeviceToolkits.put(
259                    deviceId, new DeviceToolkit(mMtpManager, mResolver, mDatabase));
260        }
261        mRootScanner.resume();
262    }
263
264    void closeDevice(int deviceId) throws IOException, InterruptedException {
265        synchronized (mDeviceListLock) {
266            closeDeviceInternal(deviceId);
267        }
268        mRootScanner.resume();
269    }
270
271    int[] getOpenedDeviceIds() {
272        synchronized (mDeviceListLock) {
273            return mMtpManager.getOpenedDeviceIds();
274        }
275    }
276
277    String getDeviceName(int deviceId) throws IOException {
278        synchronized (mDeviceListLock) {
279            for (final MtpDeviceRecord device : mMtpManager.getDevices()) {
280                if (device.deviceId == deviceId) {
281                    return device.name;
282                }
283            }
284            throw new IOException("Not found the device: " + Integer.toString(deviceId));
285        }
286    }
287
288    /**
289     * Finalize the content provider for unit tests.
290     */
291    @Override
292    public void shutdown() {
293        synchronized (mDeviceListLock) {
294            try {
295                for (final int id : mMtpManager.getOpenedDeviceIds()) {
296                    closeDeviceInternal(id);
297                }
298            } catch (InterruptedException|IOException e) {
299                // It should fail unit tests by throwing runtime exception.
300                throw new RuntimeException(e);
301            } finally {
302                mDatabase.close();
303                super.shutdown();
304            }
305        }
306    }
307
308    private void notifyChildDocumentsChange(String parentDocumentId) {
309        mResolver.notifyChange(
310                DocumentsContract.buildChildDocumentsUri(AUTHORITY, parentDocumentId),
311                null,
312                false);
313    }
314
315    /**
316     * Clears MTP identifier in the database.
317     */
318    private void resume() {
319        synchronized (mDeviceListLock) {
320            mDatabase.getMapper().clearMapping();
321        }
322    }
323
324    private void closeDeviceInternal(int deviceId) throws IOException, InterruptedException {
325        // TODO: Flush the device before closing (if not closed externally).
326        getDeviceToolkit(deviceId).mDocumentLoader.clearTasks();
327        mDeviceToolkits.remove(deviceId);
328        mMtpManager.closeDevice(deviceId);
329        if (getOpenedDeviceIds().length == 0) {
330            mRootScanner.pause();
331        }
332    }
333
334    private DeviceToolkit getDeviceToolkit(int deviceId) throws FileNotFoundException {
335        synchronized (mDeviceListLock) {
336            final DeviceToolkit toolkit = mDeviceToolkits.get(deviceId);
337            if (toolkit == null) {
338                throw new FileNotFoundException();
339            }
340            return toolkit;
341        }
342    }
343
344    private PipeManager getPipeManager(Identifier identifier) throws FileNotFoundException {
345        return getDeviceToolkit(identifier.mDeviceId).mPipeManager;
346    }
347
348    private DocumentLoader getDocumentLoader(Identifier identifier) throws FileNotFoundException {
349        return getDeviceToolkit(identifier.mDeviceId).mDocumentLoader;
350    }
351
352    private long getFileSize(String documentId) throws FileNotFoundException {
353        final Cursor cursor = mDatabase.queryDocument(
354                documentId,
355                MtpDatabase.strings(Document.COLUMN_SIZE, Document.COLUMN_DISPLAY_NAME));
356        try {
357            if (cursor.moveToNext()) {
358                return cursor.getLong(0);
359            } else {
360                throw new FileNotFoundException();
361            }
362        } finally {
363            cursor.close();
364        }
365    }
366
367    private static class DeviceToolkit {
368        public final PipeManager mPipeManager;
369        public final DocumentLoader mDocumentLoader;
370
371        public DeviceToolkit(MtpManager manager, ContentResolver resolver, MtpDatabase database) {
372            mPipeManager = new PipeManager();
373            mDocumentLoader = new DocumentLoader(manager, resolver, database);
374        }
375    }
376
377    private class AppFuseCallback implements AppFuse.Callback {
378        final byte[] mBytes = new byte[AppFuse.MAX_READ];
379
380        @Override
381        public byte[] getObjectBytes(int inode, long offset, int size) throws IOException {
382            final Identifier identifier = mDatabase.createIdentifier(Integer.toString(inode));
383            mMtpManager.getPartialObject(
384                    identifier.mDeviceId, identifier.mObjectHandle, (int) offset, size, mBytes);
385            return mBytes;
386        }
387
388        @Override
389        public long getFileSize(int inode) throws FileNotFoundException {
390            return MtpDocumentsProvider.this.getFileSize(String.valueOf(inode));
391        }
392    }
393}
394