MtpDocumentsProvider.java revision 46d537716e00878073a740101f1e1e088f728817
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.Bundle; 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.mtp.exceptions.BusyDeviceException; 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 static final boolean DEBUG = false; 65 66 private final Object mDeviceListLock = new Object(); 67 68 private static MtpDocumentsProvider sSingleton; 69 70 private MtpManager mMtpManager; 71 private ContentResolver mResolver; 72 @GuardedBy("mDeviceListLock") 73 private Map<Integer, DeviceToolkit> mDeviceToolkits; 74 private RootScanner mRootScanner; 75 private Resources mResources; 76 private MtpDatabase mDatabase; 77 private AppFuse mAppFuse; 78 private ServiceIntentSender mIntentSender; 79 80 /** 81 * Provides singleton instance to MtpDocumentsService. 82 */ 83 static MtpDocumentsProvider getInstance() { 84 return sSingleton; 85 } 86 87 @Override 88 public boolean onCreate() { 89 sSingleton = this; 90 mResources = getContext().getResources(); 91 mMtpManager = new MtpManager(getContext()); 92 mResolver = getContext().getContentResolver(); 93 mDeviceToolkits = new HashMap<Integer, DeviceToolkit>(); 94 mDatabase = new MtpDatabase(getContext(), MtpDatabaseConstants.FLAG_DATABASE_IN_FILE); 95 mRootScanner = new RootScanner(mResolver, mMtpManager, mDatabase); 96 mAppFuse = new AppFuse(TAG, new AppFuseCallback()); 97 mIntentSender = new ServiceIntentSender(getContext()); 98 // TODO: Mount AppFuse on demands. 99 try { 100 mAppFuse.mount(getContext().getSystemService(StorageManager.class)); 101 } catch (IOException e) { 102 Log.e(TAG, "Failed to start app fuse.", e); 103 return false; 104 } 105 resume(); 106 return true; 107 } 108 109 @VisibleForTesting 110 boolean onCreateForTesting( 111 Resources resources, 112 MtpManager mtpManager, 113 ContentResolver resolver, 114 MtpDatabase database, 115 StorageManager storageManager, 116 ServiceIntentSender intentSender) { 117 mResources = resources; 118 mMtpManager = mtpManager; 119 mResolver = resolver; 120 mDeviceToolkits = new HashMap<Integer, DeviceToolkit>(); 121 mDatabase = database; 122 mRootScanner = new RootScanner(mResolver, mMtpManager, mDatabase); 123 mAppFuse = new AppFuse(TAG, new AppFuseCallback()); 124 mIntentSender = intentSender; 125 // TODO: Mount AppFuse on demands. 126 try { 127 mAppFuse.mount(storageManager); 128 } catch (IOException e) { 129 Log.e(TAG, "Failed to start app fuse.", e); 130 return false; 131 } 132 resume(); 133 return true; 134 } 135 136 @Override 137 public Cursor queryRoots(String[] projection) throws FileNotFoundException { 138 if (projection == null) { 139 projection = MtpDocumentsProvider.DEFAULT_ROOT_PROJECTION; 140 } 141 final Cursor cursor = mDatabase.queryRoots(mResources, projection); 142 cursor.setNotificationUri( 143 mResolver, DocumentsContract.buildRootsUri(MtpDocumentsProvider.AUTHORITY)); 144 return cursor; 145 } 146 147 @Override 148 public Cursor queryDocument(String documentId, String[] projection) 149 throws FileNotFoundException { 150 if (projection == null) { 151 projection = MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION; 152 } 153 return mDatabase.queryDocument(documentId, projection); 154 } 155 156 @Override 157 public Cursor queryChildDocuments(String parentDocumentId, 158 String[] projection, String sortOrder) throws FileNotFoundException { 159 if (DEBUG) { 160 Log.d(TAG, "queryChildDocuments: " + parentDocumentId); 161 } 162 if (projection == null) { 163 projection = MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION; 164 } 165 Identifier parentIdentifier = mDatabase.createIdentifier(parentDocumentId); 166 try { 167 openDevice(parentIdentifier.mDeviceId); 168 if (parentIdentifier.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE) { 169 final Identifier singleStorageIdentifier = 170 mDatabase.getSingleStorageIdentifier(parentDocumentId); 171 if (singleStorageIdentifier == null) { 172 // Returns storage list from database. 173 return mDatabase.queryChildDocuments(projection, parentDocumentId); 174 } 175 parentIdentifier = singleStorageIdentifier; 176 } 177 // Returns object list from document loader. 178 return getDocumentLoader(parentIdentifier).queryChildDocuments( 179 projection, parentIdentifier); 180 } catch (BusyDeviceException exception) { 181 final Bundle bundle = new Bundle(); 182 bundle.putString( 183 DocumentsContract.EXTRA_ERROR, 184 mResources.getString(R.string.error_busy_device)); 185 final Cursor cursor = new MatrixCursor(projection); 186 cursor.setExtras(bundle); 187 return cursor; 188 } catch (IOException exception) { 189 Log.e(MtpDocumentsProvider.TAG, "queryChildDocuments", exception); 190 throw new FileNotFoundException(exception.getMessage()); 191 } 192 } 193 194 @Override 195 public ParcelFileDescriptor openDocument( 196 String documentId, String mode, CancellationSignal signal) 197 throws FileNotFoundException { 198 if (DEBUG) { 199 Log.d(TAG, "openDocument: " + documentId); 200 } 201 final Identifier identifier = mDatabase.createIdentifier(documentId); 202 try { 203 openDevice(identifier.mDeviceId); 204 switch (mode) { 205 case "r": 206 final long fileSize = getFileSize(documentId); 207 // MTP getPartialObject operation does not support files that are larger than 208 // 4GB. Fallback to non-seekable file descriptor. 209 // TODO: Use getPartialObject64 for MTP devices that support Android vendor 210 // extension. 211 if (fileSize <= 0xffffffffl) { 212 return mAppFuse.openFile(Integer.parseInt(documentId)); 213 } else { 214 return getPipeManager(identifier).readDocument(mMtpManager, identifier); 215 } 216 case "w": 217 // TODO: Clear the parent document loader task (if exists) and call notify 218 // when writing is completed. 219 return getPipeManager(identifier).writeDocument( 220 getContext(), mMtpManager, identifier); 221 case "rw": 222 // TODO: Add support for "rw" mode. 223 throw new UnsupportedOperationException( 224 "The provider does not support 'rw' mode."); 225 default: 226 throw new IllegalArgumentException("Unknown mode for openDocument: " + mode); 227 } 228 } catch (IOException error) { 229 Log.e(MtpDocumentsProvider.TAG, "openDocument", error); 230 throw new FileNotFoundException(error.getMessage()); 231 } 232 } 233 234 @Override 235 public AssetFileDescriptor openDocumentThumbnail( 236 String documentId, 237 Point sizeHint, 238 CancellationSignal signal) throws FileNotFoundException { 239 final Identifier identifier = mDatabase.createIdentifier(documentId); 240 try { 241 openDevice(identifier.mDeviceId); 242 return new AssetFileDescriptor( 243 getPipeManager(identifier).readThumbnail(mMtpManager, identifier), 244 0, // Start offset. 245 AssetFileDescriptor.UNKNOWN_LENGTH); 246 } catch (IOException error) { 247 Log.e(MtpDocumentsProvider.TAG, "openDocumentThumbnail", error); 248 throw new FileNotFoundException(error.getMessage()); 249 } 250 } 251 252 @Override 253 public void deleteDocument(String documentId) throws FileNotFoundException { 254 try { 255 final Identifier identifier = mDatabase.createIdentifier(documentId); 256 openDevice(identifier.mDeviceId); 257 final Identifier parentIdentifier = mDatabase.getParentIdentifier(documentId); 258 mMtpManager.deleteDocument(identifier.mDeviceId, identifier.mObjectHandle); 259 mDatabase.deleteDocument(documentId); 260 getDocumentLoader(parentIdentifier).clearTask(parentIdentifier); 261 notifyChildDocumentsChange(parentIdentifier.mDocumentId); 262 if (parentIdentifier.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE) { 263 // If the parent is storage, the object might be appeared as child of device because 264 // we skip storage when the device has only one storage. 265 final Identifier deviceIdentifier = mDatabase.getParentIdentifier( 266 parentIdentifier.mDocumentId); 267 notifyChildDocumentsChange(deviceIdentifier.mDocumentId); 268 } 269 } catch (IOException error) { 270 Log.e(MtpDocumentsProvider.TAG, "deleteDocument", error); 271 throw new FileNotFoundException(error.getMessage()); 272 } 273 } 274 275 @Override 276 public void onTrimMemory(int level) { 277 synchronized (mDeviceListLock) { 278 for (final DeviceToolkit toolkit : mDeviceToolkits.values()) { 279 toolkit.mDocumentLoader.clearCompletedTasks(); 280 } 281 } 282 } 283 284 @Override 285 public String createDocument(String parentDocumentId, String mimeType, String displayName) 286 throws FileNotFoundException { 287 if (DEBUG) { 288 Log.d(TAG, "createDocument: " + displayName); 289 } 290 try { 291 final Identifier parentId = mDatabase.createIdentifier(parentDocumentId); 292 openDevice(parentId.mDeviceId); 293 final ParcelFileDescriptor pipe[] = ParcelFileDescriptor.createReliablePipe(); 294 pipe[0].close(); // 0 bytes for a new document. 295 final int formatCode = Document.MIME_TYPE_DIR.equals(mimeType) ? 296 MtpConstants.FORMAT_ASSOCIATION : 297 MediaFile.getFormatCode(displayName, mimeType); 298 final MtpObjectInfo info = new MtpObjectInfo.Builder() 299 .setStorageId(parentId.mStorageId) 300 .setParent(parentId.mObjectHandle) 301 .setFormat(formatCode) 302 .setName(displayName) 303 .build(); 304 final int objectHandle = mMtpManager.createDocument(parentId.mDeviceId, info, pipe[1]); 305 final MtpObjectInfo infoWithHandle = 306 new MtpObjectInfo.Builder(info).setObjectHandle(objectHandle).build(); 307 final String documentId = mDatabase.putNewDocument( 308 parentId.mDeviceId, parentDocumentId, infoWithHandle); 309 getDocumentLoader(parentId).clearTask(parentId); 310 notifyChildDocumentsChange(parentDocumentId); 311 return documentId; 312 } catch (IOException error) { 313 Log.e(TAG, "createDocument", error); 314 throw new FileNotFoundException(error.getMessage()); 315 } 316 } 317 318 void openDevice(int deviceId) throws IOException { 319 synchronized (mDeviceListLock) { 320 if (mDeviceToolkits.containsKey(deviceId)) { 321 return; 322 } 323 if (DEBUG) { 324 Log.d(TAG, "Open device " + deviceId); 325 } 326 mMtpManager.openDevice(deviceId); 327 mDeviceToolkits.put( 328 deviceId, new DeviceToolkit(mMtpManager, mResolver, mDatabase)); 329 mIntentSender.sendUpdateNotificationIntent(); 330 try { 331 mRootScanner.resume().await(); 332 } catch (InterruptedException error) { 333 Log.e(TAG, "openDevice", error); 334 } 335 } 336 } 337 338 void closeDevice(int deviceId) throws IOException, InterruptedException { 339 synchronized (mDeviceListLock) { 340 closeDeviceInternal(deviceId); 341 } 342 mRootScanner.resume(); 343 mIntentSender.sendUpdateNotificationIntent(); 344 } 345 346 int[] getOpenedDeviceIds() { 347 synchronized (mDeviceListLock) { 348 return mMtpManager.getOpenedDeviceIds(); 349 } 350 } 351 352 String getDeviceName(int deviceId) throws IOException { 353 synchronized (mDeviceListLock) { 354 for (final MtpDeviceRecord device : mMtpManager.getDevices()) { 355 if (device.deviceId == deviceId) { 356 return device.name; 357 } 358 } 359 throw new IOException("Not found the device: " + Integer.toString(deviceId)); 360 } 361 } 362 363 /** 364 * Obtains document ID for the given device ID. 365 * @param deviceId 366 * @return document ID 367 * @throws FileNotFoundException device ID has not been build. 368 */ 369 public String getDeviceDocumentId(int deviceId) throws FileNotFoundException { 370 return mDatabase.getDeviceDocumentId(deviceId); 371 } 372 373 /** 374 * Resumes root scanner to handle the update of device list. 375 */ 376 void resumeRootScanner() { 377 mRootScanner.resume(); 378 } 379 380 /** 381 * Finalize the content provider for unit tests. 382 */ 383 @Override 384 public void shutdown() { 385 synchronized (mDeviceListLock) { 386 try { 387 for (final int id : mMtpManager.getOpenedDeviceIds()) { 388 closeDeviceInternal(id); 389 } 390 } catch (InterruptedException|IOException e) { 391 // It should fail unit tests by throwing runtime exception. 392 throw new RuntimeException(e); 393 } finally { 394 mDatabase.close(); 395 mAppFuse.close(); 396 super.shutdown(); 397 } 398 } 399 } 400 401 private void notifyChildDocumentsChange(String parentDocumentId) { 402 mResolver.notifyChange( 403 DocumentsContract.buildChildDocumentsUri(AUTHORITY, parentDocumentId), 404 null, 405 false); 406 } 407 408 /** 409 * Clears MTP identifier in the database. 410 */ 411 private void resume() { 412 synchronized (mDeviceListLock) { 413 mDatabase.getMapper().clearMapping(); 414 } 415 } 416 417 private void closeDeviceInternal(int deviceId) throws IOException, InterruptedException { 418 // TODO: Flush the device before closing (if not closed externally). 419 if (!mDeviceToolkits.containsKey(deviceId)) { 420 return; 421 } 422 if (DEBUG) { 423 Log.d(TAG, "Close device " + deviceId); 424 } 425 getDeviceToolkit(deviceId).mDocumentLoader.clearTasks(); 426 mDeviceToolkits.remove(deviceId); 427 mMtpManager.closeDevice(deviceId); 428 if (getOpenedDeviceIds().length == 0) { 429 mRootScanner.pause(); 430 } 431 } 432 433 private DeviceToolkit getDeviceToolkit(int deviceId) throws FileNotFoundException { 434 synchronized (mDeviceListLock) { 435 final DeviceToolkit toolkit = mDeviceToolkits.get(deviceId); 436 if (toolkit == null) { 437 throw new FileNotFoundException(); 438 } 439 return toolkit; 440 } 441 } 442 443 private PipeManager getPipeManager(Identifier identifier) throws FileNotFoundException { 444 return getDeviceToolkit(identifier.mDeviceId).mPipeManager; 445 } 446 447 private DocumentLoader getDocumentLoader(Identifier identifier) throws FileNotFoundException { 448 return getDeviceToolkit(identifier.mDeviceId).mDocumentLoader; 449 } 450 451 private long getFileSize(String documentId) throws FileNotFoundException { 452 final Cursor cursor = mDatabase.queryDocument( 453 documentId, 454 MtpDatabase.strings(Document.COLUMN_SIZE, Document.COLUMN_DISPLAY_NAME)); 455 try { 456 if (cursor.moveToNext()) { 457 return cursor.getLong(0); 458 } else { 459 throw new FileNotFoundException(); 460 } 461 } finally { 462 cursor.close(); 463 } 464 } 465 466 private static class DeviceToolkit { 467 public final PipeManager mPipeManager; 468 public final DocumentLoader mDocumentLoader; 469 470 public DeviceToolkit(MtpManager manager, ContentResolver resolver, MtpDatabase database) { 471 mPipeManager = new PipeManager(); 472 mDocumentLoader = new DocumentLoader(manager, resolver, database); 473 } 474 } 475 476 private class AppFuseCallback implements AppFuse.Callback { 477 @Override 478 public long readObjectBytes( 479 int inode, long offset, long size, byte[] buffer) throws IOException { 480 final Identifier identifier = mDatabase.createIdentifier(Integer.toString(inode)); 481 return mMtpManager.getPartialObject( 482 identifier.mDeviceId, identifier.mObjectHandle, offset, size, buffer); 483 } 484 485 @Override 486 public long getFileSize(int inode) throws FileNotFoundException { 487 return MtpDocumentsProvider.this.getFileSize(String.valueOf(inode)); 488 } 489 } 490} 491