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