| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.mtp; |
| |
| import android.annotation.Nullable; |
| import android.content.ContentResolver; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.content.UriPermission; |
| import android.content.res.AssetFileDescriptor; |
| import android.content.res.Resources; |
| import android.database.Cursor; |
| import android.database.DatabaseUtils; |
| import android.database.MatrixCursor; |
| import android.database.sqlite.SQLiteDiskIOException; |
| import android.graphics.Point; |
| import android.media.MediaFile; |
| import android.mtp.MtpConstants; |
| import android.mtp.MtpObjectInfo; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.os.CancellationSignal; |
| import android.os.FileUtils; |
| import android.os.ParcelFileDescriptor; |
| import android.os.ProxyFileDescriptorCallback; |
| import android.os.storage.StorageManager; |
| import android.provider.DocumentsContract; |
| import android.provider.DocumentsContract.Document; |
| import android.provider.DocumentsContract.Path; |
| import android.provider.DocumentsContract.Root; |
| import android.provider.DocumentsProvider; |
| import android.provider.MetadataReader; |
| import android.provider.Settings; |
| import android.system.ErrnoException; |
| import android.system.OsConstants; |
| import android.util.Log; |
| |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.annotations.VisibleForTesting; |
| |
| import libcore.io.IoUtils; |
| |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.HashMap; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.concurrent.TimeoutException; |
| |
| /** |
| * DocumentsProvider for MTP devices. |
| */ |
| public class MtpDocumentsProvider extends DocumentsProvider { |
| static final String AUTHORITY = "com.android.mtp.documents"; |
| static final String TAG = "MtpDocumentsProvider"; |
| static final String[] DEFAULT_ROOT_PROJECTION = new String[] { |
| Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, |
| Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID, |
| Root.COLUMN_AVAILABLE_BYTES, |
| }; |
| static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] { |
| Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, |
| Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED, |
| Document.COLUMN_FLAGS, Document.COLUMN_SIZE, |
| }; |
| |
| static final boolean DEBUG = false; |
| |
| private final Object mDeviceListLock = new Object(); |
| |
| private static MtpDocumentsProvider sSingleton; |
| |
| private MtpManager mMtpManager; |
| private ContentResolver mResolver; |
| @GuardedBy("mDeviceListLock") |
| private Map<Integer, DeviceToolkit> mDeviceToolkits; |
| private RootScanner mRootScanner; |
| private Resources mResources; |
| private MtpDatabase mDatabase; |
| private ServiceIntentSender mIntentSender; |
| private Context mContext; |
| private StorageManager mStorageManager; |
| |
| /** |
| * Provides singleton instance to MtpDocumentsService. |
| */ |
| static MtpDocumentsProvider getInstance() { |
| return sSingleton; |
| } |
| |
| @Override |
| public boolean onCreate() { |
| sSingleton = this; |
| mContext = getContext(); |
| mResources = getContext().getResources(); |
| mMtpManager = new MtpManager(getContext()); |
| mResolver = getContext().getContentResolver(); |
| mDeviceToolkits = new HashMap<>(); |
| mDatabase = new MtpDatabase(getContext(), MtpDatabaseConstants.FLAG_DATABASE_IN_FILE); |
| mRootScanner = new RootScanner(mResolver, mMtpManager, mDatabase); |
| mIntentSender = new ServiceIntentSender(getContext()); |
| mStorageManager = getContext().getSystemService(StorageManager.class); |
| |
| // Check boot count and cleans database if it's first time to launch MtpDocumentsProvider |
| // after booting. |
| try { |
| final int bootCount = Settings.Global.getInt(mResolver, Settings.Global.BOOT_COUNT, -1); |
| final int lastBootCount = mDatabase.getLastBootCount(); |
| if (bootCount != -1 && bootCount != lastBootCount) { |
| mDatabase.setLastBootCount(bootCount); |
| final List<UriPermission> permissions = |
| mResolver.getOutgoingPersistedUriPermissions(); |
| final Uri[] uris = new Uri[permissions.size()]; |
| for (int i = 0; i < permissions.size(); i++) { |
| uris[i] = permissions.get(i).getUri(); |
| } |
| mDatabase.cleanDatabase(uris); |
| } |
| } catch (SQLiteDiskIOException error) { |
| // It can happen due to disk shortage. |
| Log.e(TAG, "Failed to clean database.", error); |
| return false; |
| } |
| |
| resume(); |
| return true; |
| } |
| |
| @VisibleForTesting |
| boolean onCreateForTesting( |
| Context context, |
| Resources resources, |
| MtpManager mtpManager, |
| ContentResolver resolver, |
| MtpDatabase database, |
| StorageManager storageManager, |
| ServiceIntentSender intentSender) { |
| mContext = context; |
| mResources = resources; |
| mMtpManager = mtpManager; |
| mResolver = resolver; |
| mDeviceToolkits = new HashMap<>(); |
| mDatabase = database; |
| mRootScanner = new RootScanner(mResolver, mMtpManager, mDatabase); |
| mIntentSender = intentSender; |
| mStorageManager = storageManager; |
| |
| resume(); |
| return true; |
| } |
| |
| @Override |
| public Cursor queryRoots(String[] projection) throws FileNotFoundException { |
| if (projection == null) { |
| projection = MtpDocumentsProvider.DEFAULT_ROOT_PROJECTION; |
| } |
| final Cursor cursor = mDatabase.queryRoots(mResources, projection); |
| cursor.setNotificationUri( |
| mResolver, DocumentsContract.buildRootsUri(MtpDocumentsProvider.AUTHORITY)); |
| return cursor; |
| } |
| |
| @Override |
| public Cursor queryDocument(String documentId, String[] projection) |
| throws FileNotFoundException { |
| if (projection == null) { |
| projection = MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION; |
| } |
| final Cursor cursor = mDatabase.queryDocument(documentId, projection); |
| final int cursorCount = cursor.getCount(); |
| if (cursorCount == 0) { |
| cursor.close(); |
| throw new FileNotFoundException(); |
| } else if (cursorCount != 1) { |
| cursor.close(); |
| Log.wtf(TAG, "Unexpected cursor size: " + cursorCount); |
| return null; |
| } |
| |
| final Identifier identifier = mDatabase.createIdentifier(documentId); |
| if (identifier.mDocumentType != MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE) { |
| return cursor; |
| } |
| final String[] storageDocIds = mDatabase.getStorageDocumentIds(documentId); |
| if (storageDocIds.length != 1) { |
| return mDatabase.queryDocument(documentId, projection); |
| } |
| |
| // If the documentId specifies a device having exact one storage, we repalce some device |
| // attributes with the storage attributes. |
| try { |
| final String storageName; |
| final int storageFlags; |
| try (final Cursor storageCursor = mDatabase.queryDocument( |
| storageDocIds[0], |
| MtpDatabase.strings(Document.COLUMN_DISPLAY_NAME, Document.COLUMN_FLAGS))) { |
| if (!storageCursor.moveToNext()) { |
| throw new FileNotFoundException(); |
| } |
| storageName = storageCursor.getString(0); |
| storageFlags = storageCursor.getInt(1); |
| } |
| |
| cursor.moveToNext(); |
| final ContentValues values = new ContentValues(); |
| DatabaseUtils.cursorRowToContentValues(cursor, values); |
| if (values.containsKey(Document.COLUMN_DISPLAY_NAME)) { |
| values.put(Document.COLUMN_DISPLAY_NAME, mResources.getString( |
| R.string.root_name, |
| values.getAsString(Document.COLUMN_DISPLAY_NAME), |
| storageName)); |
| } |
| values.put(Document.COLUMN_FLAGS, storageFlags); |
| final MatrixCursor output = new MatrixCursor(projection, 1); |
| MtpDatabase.putValuesToCursor(values, output); |
| return output; |
| } finally { |
| cursor.close(); |
| } |
| } |
| |
| @Override |
| public Cursor queryChildDocuments(String parentDocumentId, |
| String[] projection, String sortOrder) throws FileNotFoundException { |
| if (DEBUG) { |
| Log.d(TAG, "queryChildDocuments: " + parentDocumentId); |
| } |
| if (projection == null) { |
| projection = MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION; |
| } |
| Identifier parentIdentifier = mDatabase.createIdentifier(parentDocumentId); |
| try { |
| openDevice(parentIdentifier.mDeviceId); |
| if (parentIdentifier.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE) { |
| final String[] storageDocIds = mDatabase.getStorageDocumentIds(parentDocumentId); |
| if (storageDocIds.length == 0) { |
| // Remote device does not provide storages. Maybe it is locked. |
| return createErrorCursor(projection, R.string.error_locked_device); |
| } else if (storageDocIds.length > 1) { |
| // Returns storage list from database. |
| return mDatabase.queryChildDocuments(projection, parentDocumentId); |
| } |
| |
| // Exact one storage is found. Skip storage and returns object in the single |
| // storage. |
| parentIdentifier = mDatabase.createIdentifier(storageDocIds[0]); |
| } |
| |
| // Returns object list from document loader. |
| return getDocumentLoader(parentIdentifier).queryChildDocuments( |
| projection, parentIdentifier); |
| } catch (BusyDeviceException exception) { |
| return createErrorCursor(projection, R.string.error_busy_device); |
| } catch (IOException exception) { |
| Log.e(MtpDocumentsProvider.TAG, "queryChildDocuments", exception); |
| throw new FileNotFoundException(exception.getMessage()); |
| } |
| } |
| |
| @Override |
| public ParcelFileDescriptor openDocument( |
| String documentId, String mode, CancellationSignal signal) |
| throws FileNotFoundException { |
| if (DEBUG) { |
| Log.d(TAG, "openDocument: " + documentId); |
| } |
| final Identifier identifier = mDatabase.createIdentifier(documentId); |
| try { |
| openDevice(identifier.mDeviceId); |
| final MtpDeviceRecord device = getDeviceToolkit(identifier.mDeviceId).mDeviceRecord; |
| // Turn off MODE_CREATE because openDocument does not allow to create new files. |
| final int modeFlag = |
| ParcelFileDescriptor.parseMode(mode) & ~ParcelFileDescriptor.MODE_CREATE; |
| if ((modeFlag & ParcelFileDescriptor.MODE_READ_ONLY) != 0) { |
| long fileSize; |
| try { |
| fileSize = getFileSize(documentId); |
| } catch (UnsupportedOperationException exception) { |
| fileSize = -1; |
| } |
| if (MtpDeviceRecord.isPartialReadSupported( |
| device.operationsSupported, fileSize)) { |
| |
| return mStorageManager.openProxyFileDescriptor( |
| modeFlag, |
| new MtpProxyFileDescriptorCallback(Integer.parseInt(documentId))); |
| } else { |
| // If getPartialObject{|64} are not supported for the device, returns |
| // non-seekable pipe FD instead. |
| return getPipeManager(identifier).readDocument(mMtpManager, identifier); |
| } |
| } else if ((modeFlag & ParcelFileDescriptor.MODE_WRITE_ONLY) != 0) { |
| // TODO: Clear the parent document loader task (if exists) and call notify |
| // when writing is completed. |
| if (MtpDeviceRecord.isWritingSupported(device.operationsSupported)) { |
| return mStorageManager.openProxyFileDescriptor( |
| modeFlag, |
| new MtpProxyFileDescriptorCallback(Integer.parseInt(documentId))); |
| } else { |
| throw new UnsupportedOperationException( |
| "The device does not support writing operation."); |
| } |
| } else { |
| // TODO: Add support for "rw" mode. |
| throw new UnsupportedOperationException("The provider does not support 'rw' mode."); |
| } |
| } catch (FileNotFoundException | RuntimeException error) { |
| Log.e(MtpDocumentsProvider.TAG, "openDocument", error); |
| throw error; |
| } catch (IOException error) { |
| Log.e(MtpDocumentsProvider.TAG, "openDocument", error); |
| throw new IllegalStateException(error); |
| } |
| } |
| |
| @Override |
| public AssetFileDescriptor openDocumentThumbnail( |
| String documentId, |
| Point sizeHint, |
| CancellationSignal signal) throws FileNotFoundException { |
| final Identifier identifier = mDatabase.createIdentifier(documentId); |
| try { |
| openDevice(identifier.mDeviceId); |
| return new AssetFileDescriptor( |
| getPipeManager(identifier).readThumbnail(mMtpManager, identifier), |
| 0, // Start offset. |
| AssetFileDescriptor.UNKNOWN_LENGTH); |
| } catch (IOException error) { |
| Log.e(MtpDocumentsProvider.TAG, "openDocumentThumbnail", error); |
| throw new FileNotFoundException(error.getMessage()); |
| } |
| } |
| |
| @Override |
| public void deleteDocument(String documentId) throws FileNotFoundException { |
| try { |
| final Identifier identifier = mDatabase.createIdentifier(documentId); |
| openDevice(identifier.mDeviceId); |
| final Identifier parentIdentifier = mDatabase.getParentIdentifier(documentId); |
| mMtpManager.deleteDocument(identifier.mDeviceId, identifier.mObjectHandle); |
| mDatabase.deleteDocument(documentId); |
| getDocumentLoader(parentIdentifier).cancelTask(parentIdentifier); |
| notifyChildDocumentsChange(parentIdentifier.mDocumentId); |
| if (parentIdentifier.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE) { |
| // If the parent is storage, the object might be appeared as child of device because |
| // we skip storage when the device has only one storage. |
| final Identifier deviceIdentifier = mDatabase.getParentIdentifier( |
| parentIdentifier.mDocumentId); |
| notifyChildDocumentsChange(deviceIdentifier.mDocumentId); |
| } |
| } catch (IOException error) { |
| Log.e(MtpDocumentsProvider.TAG, "deleteDocument", error); |
| throw new FileNotFoundException(error.getMessage()); |
| } |
| } |
| |
| @Override |
| public void onTrimMemory(int level) { |
| synchronized (mDeviceListLock) { |
| for (final DeviceToolkit toolkit : mDeviceToolkits.values()) { |
| toolkit.mDocumentLoader.clearCompletedTasks(); |
| } |
| } |
| } |
| |
| @Override |
| public String createDocument(String parentDocumentId, String mimeType, String displayName) |
| throws FileNotFoundException { |
| if (DEBUG) { |
| Log.d(TAG, "createDocument: " + displayName); |
| } |
| final Identifier parentId; |
| final MtpDeviceRecord record; |
| final ParcelFileDescriptor[] pipe; |
| try { |
| parentId = mDatabase.createIdentifier(parentDocumentId); |
| openDevice(parentId.mDeviceId); |
| record = getDeviceToolkit(parentId.mDeviceId).mDeviceRecord; |
| if (!MtpDeviceRecord.isWritingSupported(record.operationsSupported)) { |
| throw new UnsupportedOperationException( |
| "Writing operation is not supported by the device."); |
| } |
| |
| final int parentObjectHandle; |
| final int storageId; |
| switch (parentId.mDocumentType) { |
| case MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE: |
| final String[] storageDocumentIds = |
| mDatabase.getStorageDocumentIds(parentId.mDocumentId); |
| if (storageDocumentIds.length == 1) { |
| final String newDocumentId = |
| createDocument(storageDocumentIds[0], mimeType, displayName); |
| notifyChildDocumentsChange(parentDocumentId); |
| return newDocumentId; |
| } else { |
| throw new UnsupportedOperationException( |
| "Cannot create a file under the device."); |
| } |
| case MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE: |
| storageId = parentId.mStorageId; |
| parentObjectHandle = -1; |
| break; |
| case MtpDatabaseConstants.DOCUMENT_TYPE_OBJECT: |
| storageId = parentId.mStorageId; |
| parentObjectHandle = parentId.mObjectHandle; |
| break; |
| default: |
| throw new IllegalArgumentException("Unexpected document type."); |
| } |
| |
| pipe = ParcelFileDescriptor.createReliablePipe(); |
| int objectHandle = -1; |
| MtpObjectInfo info = null; |
| try { |
| pipe[0].close(); // 0 bytes for a new document. |
| |
| final int formatCode = Document.MIME_TYPE_DIR.equals(mimeType) ? |
| MtpConstants.FORMAT_ASSOCIATION : |
| MediaFile.getFormatCode(displayName, mimeType); |
| info = new MtpObjectInfo.Builder() |
| .setStorageId(storageId) |
| .setParent(parentObjectHandle) |
| .setFormat(formatCode) |
| .setName(displayName) |
| .build(); |
| |
| final String[] parts = FileUtils.splitFileName(mimeType, displayName); |
| final String baseName = parts[0]; |
| final String extension = parts[1]; |
| for (int i = 0; i <= 32; i++) { |
| final MtpObjectInfo infoUniqueName; |
| if (i == 0) { |
| infoUniqueName = info; |
| } else { |
| String suffixedName = baseName + " (" + i + " )"; |
| if (!extension.isEmpty()) { |
| suffixedName += "." + extension; |
| } |
| infoUniqueName = |
| new MtpObjectInfo.Builder(info).setName(suffixedName).build(); |
| } |
| try { |
| objectHandle = mMtpManager.createDocument( |
| parentId.mDeviceId, infoUniqueName, pipe[1]); |
| break; |
| } catch (SendObjectInfoFailure exp) { |
| // This can be caused when we have an existing file with the same name. |
| continue; |
| } |
| } |
| } finally { |
| pipe[1].close(); |
| } |
| if (objectHandle == -1) { |
| throw new IllegalArgumentException( |
| "The file name \"" + displayName + "\" is conflicted with existing files " + |
| "and the provider failed to find unique name."); |
| } |
| final MtpObjectInfo infoWithHandle = |
| new MtpObjectInfo.Builder(info).setObjectHandle(objectHandle).build(); |
| final String documentId = mDatabase.putNewDocument( |
| parentId.mDeviceId, parentDocumentId, record.operationsSupported, |
| infoWithHandle, 0l); |
| getDocumentLoader(parentId).cancelTask(parentId); |
| notifyChildDocumentsChange(parentDocumentId); |
| return documentId; |
| } catch (FileNotFoundException | RuntimeException error) { |
| Log.e(TAG, "createDocument", error); |
| throw error; |
| } catch (IOException error) { |
| Log.e(TAG, "createDocument", error); |
| throw new IllegalStateException(error); |
| } |
| } |
| |
| @Override |
| public Path findDocumentPath(String parentDocumentId, String childDocumentId) |
| throws FileNotFoundException { |
| final LinkedList<String> ids = new LinkedList<>(); |
| final Identifier childIdentifier = mDatabase.createIdentifier(childDocumentId); |
| |
| Identifier i = childIdentifier; |
| outer: while (true) { |
| if (i.mDocumentId.equals(parentDocumentId)) { |
| ids.addFirst(i.mDocumentId); |
| break; |
| } |
| switch (i.mDocumentType) { |
| case MtpDatabaseConstants.DOCUMENT_TYPE_OBJECT: |
| ids.addFirst(i.mDocumentId); |
| i = mDatabase.getParentIdentifier(i.mDocumentId); |
| break; |
| case MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE: { |
| // Check if there is the multiple storage. |
| final Identifier deviceIdentifier = |
| mDatabase.getParentIdentifier(i.mDocumentId); |
| final String[] storageIds = |
| mDatabase.getStorageDocumentIds(deviceIdentifier.mDocumentId); |
| // Add storage's document ID to the path only when the device has multiple |
| // storages. |
| if (storageIds.length > 1) { |
| ids.addFirst(i.mDocumentId); |
| break outer; |
| } |
| i = deviceIdentifier; |
| break; |
| } |
| case MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE: |
| ids.addFirst(i.mDocumentId); |
| break outer; |
| } |
| } |
| |
| if (parentDocumentId != null) { |
| return new Path(null, ids); |
| } else { |
| return new Path(/* Should be same with root ID */ i.mDocumentId, ids); |
| } |
| } |
| |
| @Override |
| public boolean isChildDocument(String parentDocumentId, String documentId) { |
| try { |
| Identifier identifier = mDatabase.createIdentifier(documentId); |
| while (true) { |
| if (parentDocumentId.equals(identifier.mDocumentId)) { |
| return true; |
| } |
| if (identifier.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE) { |
| return false; |
| } |
| identifier = mDatabase.getParentIdentifier(identifier.mDocumentId); |
| } |
| } catch (FileNotFoundException error) { |
| return false; |
| } |
| } |
| |
| @Override |
| public @Nullable Bundle getDocumentMetadata(String docId) throws FileNotFoundException { |
| String mimeType = getDocumentType(docId); |
| |
| if (!MetadataReader.isSupportedMimeType(mimeType)) { |
| return null; |
| } |
| |
| InputStream stream = null; |
| try { |
| stream = new ParcelFileDescriptor.AutoCloseInputStream( |
| openDocument(docId, "r", null)); |
| Bundle metadata = new Bundle(); |
| MetadataReader.getMetadata(metadata, stream, mimeType, null); |
| return metadata; |
| } catch (IOException e) { |
| Log.e(TAG, "An error occurred retrieving the metadata", e); |
| return null; |
| } finally { |
| IoUtils.closeQuietly(stream); |
| } |
| } |
| |
| void openDevice(int deviceId) throws IOException { |
| synchronized (mDeviceListLock) { |
| if (mDeviceToolkits.containsKey(deviceId)) { |
| return; |
| } |
| if (DEBUG) { |
| Log.d(TAG, "Open device " + deviceId); |
| } |
| final MtpDeviceRecord device = mMtpManager.openDevice(deviceId); |
| final DeviceToolkit toolkit = |
| new DeviceToolkit(mMtpManager, mResolver, mDatabase, device); |
| mDeviceToolkits.put(deviceId, toolkit); |
| mIntentSender.sendUpdateNotificationIntent(getOpenedDeviceRecordsCache()); |
| try { |
| mRootScanner.resume().await(); |
| } catch (InterruptedException error) { |
| Log.e(TAG, "openDevice", error); |
| } |
| // Resume document loader to remap disconnected document ID. Must be invoked after the |
| // root scanner resumes. |
| toolkit.mDocumentLoader.resume(); |
| } |
| } |
| |
| void closeDevice(int deviceId) throws IOException, InterruptedException { |
| synchronized (mDeviceListLock) { |
| closeDeviceInternal(deviceId); |
| mIntentSender.sendUpdateNotificationIntent(getOpenedDeviceRecordsCache()); |
| } |
| mRootScanner.resume(); |
| } |
| |
| MtpDeviceRecord[] getOpenedDeviceRecordsCache() { |
| synchronized (mDeviceListLock) { |
| final MtpDeviceRecord[] records = new MtpDeviceRecord[mDeviceToolkits.size()]; |
| int i = 0; |
| for (final DeviceToolkit toolkit : mDeviceToolkits.values()) { |
| records[i] = toolkit.mDeviceRecord; |
| i++; |
| } |
| return records; |
| } |
| } |
| |
| /** |
| * Obtains document ID for the given device ID. |
| * @param deviceId |
| * @return document ID |
| * @throws FileNotFoundException device ID has not been build. |
| */ |
| public String getDeviceDocumentId(int deviceId) throws FileNotFoundException { |
| return mDatabase.getDeviceDocumentId(deviceId); |
| } |
| |
| /** |
| * Resumes root scanner to handle the update of device list. |
| */ |
| void resumeRootScanner() { |
| if (DEBUG) { |
| Log.d(MtpDocumentsProvider.TAG, "resumeRootScanner"); |
| } |
| mRootScanner.resume(); |
| } |
| |
| /** |
| * Finalize the content provider for unit tests. |
| */ |
| @Override |
| public void shutdown() { |
| synchronized (mDeviceListLock) { |
| try { |
| // Copy the opened key set because it will be modified when closing devices. |
| final Integer[] keySet = |
| mDeviceToolkits.keySet().toArray(new Integer[mDeviceToolkits.size()]); |
| for (final int id : keySet) { |
| closeDeviceInternal(id); |
| } |
| mRootScanner.pause(); |
| } catch (InterruptedException | IOException | TimeoutException e) { |
| // It should fail unit tests by throwing runtime exception. |
| throw new RuntimeException(e); |
| } finally { |
| mDatabase.close(); |
| super.shutdown(); |
| } |
| } |
| } |
| |
| private void notifyChildDocumentsChange(String parentDocumentId) { |
| mResolver.notifyChange( |
| DocumentsContract.buildChildDocumentsUri(AUTHORITY, parentDocumentId), |
| null, |
| false); |
| } |
| |
| /** |
| * Clears MTP identifier in the database. |
| */ |
| private void resume() { |
| synchronized (mDeviceListLock) { |
| mDatabase.getMapper().clearMapping(); |
| } |
| } |
| |
| private void closeDeviceInternal(int deviceId) throws IOException, InterruptedException { |
| // TODO: Flush the device before closing (if not closed externally). |
| if (!mDeviceToolkits.containsKey(deviceId)) { |
| return; |
| } |
| if (DEBUG) { |
| Log.d(TAG, "Close device " + deviceId); |
| } |
| getDeviceToolkit(deviceId).close(); |
| mDeviceToolkits.remove(deviceId); |
| mMtpManager.closeDevice(deviceId); |
| } |
| |
| private DeviceToolkit getDeviceToolkit(int deviceId) throws FileNotFoundException { |
| synchronized (mDeviceListLock) { |
| final DeviceToolkit toolkit = mDeviceToolkits.get(deviceId); |
| if (toolkit == null) { |
| throw new FileNotFoundException(); |
| } |
| return toolkit; |
| } |
| } |
| |
| private PipeManager getPipeManager(Identifier identifier) throws FileNotFoundException { |
| return getDeviceToolkit(identifier.mDeviceId).mPipeManager; |
| } |
| |
| private DocumentLoader getDocumentLoader(Identifier identifier) throws FileNotFoundException { |
| return getDeviceToolkit(identifier.mDeviceId).mDocumentLoader; |
| } |
| |
| private long getFileSize(String documentId) throws FileNotFoundException { |
| final Cursor cursor = mDatabase.queryDocument( |
| documentId, |
| MtpDatabase.strings(Document.COLUMN_SIZE, Document.COLUMN_DISPLAY_NAME)); |
| try { |
| if (cursor.moveToNext()) { |
| if (cursor.isNull(0)) { |
| throw new UnsupportedOperationException(); |
| } |
| return cursor.getLong(0); |
| } else { |
| throw new FileNotFoundException(); |
| } |
| } finally { |
| cursor.close(); |
| } |
| } |
| |
| /** |
| * Creates empty cursor with specific error message. |
| * |
| * @param projection Column names. |
| * @param stringResId String resource ID of error message. |
| * @return Empty cursor with error message. |
| */ |
| private Cursor createErrorCursor(String[] projection, int stringResId) { |
| final Bundle bundle = new Bundle(); |
| bundle.putString(DocumentsContract.EXTRA_ERROR, mResources.getString(stringResId)); |
| final Cursor cursor = new MatrixCursor(projection); |
| cursor.setExtras(bundle); |
| return cursor; |
| } |
| |
| private static class DeviceToolkit implements AutoCloseable { |
| public final PipeManager mPipeManager; |
| public final DocumentLoader mDocumentLoader; |
| public final MtpDeviceRecord mDeviceRecord; |
| |
| public DeviceToolkit(MtpManager manager, |
| ContentResolver resolver, |
| MtpDatabase database, |
| MtpDeviceRecord record) { |
| mPipeManager = new PipeManager(database); |
| mDocumentLoader = new DocumentLoader(record, manager, resolver, database); |
| mDeviceRecord = record; |
| } |
| |
| @Override |
| public void close() throws InterruptedException { |
| mPipeManager.close(); |
| mDocumentLoader.close(); |
| } |
| } |
| |
| private class MtpProxyFileDescriptorCallback extends ProxyFileDescriptorCallback { |
| private final int mInode; |
| private MtpFileWriter mWriter; |
| |
| MtpProxyFileDescriptorCallback(int inode) { |
| mInode = inode; |
| } |
| |
| @Override |
| public long onGetSize() throws ErrnoException { |
| try { |
| return getFileSize(String.valueOf(mInode)); |
| } catch (FileNotFoundException e) { |
| Log.e(TAG, e.getMessage(), e); |
| throw new ErrnoException("onGetSize", OsConstants.ENOENT); |
| } |
| } |
| |
| @Override |
| public int onRead(long offset, int size, byte[] data) throws ErrnoException { |
| try { |
| final Identifier identifier = mDatabase.createIdentifier(Integer.toString(mInode)); |
| final MtpDeviceRecord record = getDeviceToolkit(identifier.mDeviceId).mDeviceRecord; |
| if (MtpDeviceRecord.isSupported( |
| record.operationsSupported, MtpConstants.OPERATION_GET_PARTIAL_OBJECT_64)) { |
| |
| return (int) mMtpManager.getPartialObject64( |
| identifier.mDeviceId, identifier.mObjectHandle, offset, size, data); |
| |
| } |
| if (0 <= offset && offset <= 0xffffffffL && MtpDeviceRecord.isSupported( |
| record.operationsSupported, MtpConstants.OPERATION_GET_PARTIAL_OBJECT)) { |
| return (int) mMtpManager.getPartialObject( |
| identifier.mDeviceId, identifier.mObjectHandle, offset, size, data); |
| } |
| throw new ErrnoException("onRead", OsConstants.ENOTSUP); |
| } catch (IOException e) { |
| Log.e(TAG, e.getMessage(), e); |
| throw new ErrnoException("onRead", OsConstants.EIO); |
| } |
| } |
| |
| @Override |
| public int onWrite(long offset, int size, byte[] data) throws ErrnoException { |
| try { |
| if (mWriter == null) { |
| mWriter = new MtpFileWriter(mContext, String.valueOf(mInode)); |
| } |
| return mWriter.write(offset, size, data); |
| } catch (IOException e) { |
| Log.e(TAG, e.getMessage(), e); |
| throw new ErrnoException("onWrite", OsConstants.EIO); |
| } |
| } |
| |
| @Override |
| public void onFsync() throws ErrnoException { |
| tryFsync(); |
| } |
| |
| @Override |
| public void onRelease() { |
| try { |
| tryFsync(); |
| } catch (ErrnoException error) { |
| // Cannot recover from the error at onRelease. Client app should use fsync to |
| // ensure the provider writes data correctly. |
| Log.e(TAG, "Cannot recover from the error at onRelease.", error); |
| } finally { |
| if (mWriter != null) { |
| IoUtils.closeQuietly(mWriter); |
| } |
| } |
| } |
| |
| private void tryFsync() throws ErrnoException { |
| try { |
| if (mWriter != null) { |
| final MtpDeviceRecord device = |
| getDeviceToolkit(mDatabase.createIdentifier( |
| mWriter.getDocumentId()).mDeviceId).mDeviceRecord; |
| mWriter.flush(mMtpManager, mDatabase, device.operationsSupported); |
| } |
| } catch (IOException e) { |
| Log.e(TAG, e.getMessage(), e); |
| throw new ErrnoException("onWrite", OsConstants.EIO); |
| } |
| } |
| } |
| } |