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