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