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