MtpDocumentsProvider.java revision 4c1d3dde05308cb10187269dd9824c9bfdbb27de
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.database.Cursor; 22import android.database.MatrixCursor; 23import android.graphics.Point; 24import android.mtp.MtpObjectInfo; 25import android.os.CancellationSignal; 26import android.os.ParcelFileDescriptor; 27import android.provider.DocumentsContract.Document; 28import android.provider.DocumentsContract.Root; 29import android.provider.DocumentsContract; 30import android.provider.DocumentsProvider; 31import android.util.Log; 32 33import com.android.internal.annotations.VisibleForTesting; 34 35import java.io.FileNotFoundException; 36import java.io.IOException; 37import java.util.HashMap; 38import java.util.Map; 39 40/** 41 * DocumentsProvider for MTP devices. 42 */ 43public class MtpDocumentsProvider extends DocumentsProvider { 44 static final String AUTHORITY = "com.android.mtp.documents"; 45 static final String TAG = "MtpDocumentsProvider"; 46 static final String[] DEFAULT_ROOT_PROJECTION = new String[] { 47 Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, 48 Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID, 49 Root.COLUMN_AVAILABLE_BYTES, 50 }; 51 static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] { 52 Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, 53 Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED, 54 Document.COLUMN_FLAGS, Document.COLUMN_SIZE, 55 }; 56 57 private static MtpDocumentsProvider sSingleton; 58 59 private MtpManager mMtpManager; 60 private ContentResolver mResolver; 61 private Map<Integer, DeviceToolkit> mDeviceToolkits; 62 private DocumentLoader mDocumentLoaders; 63 private RootScanner mRootScanner; 64 65 /** 66 * Provides singleton instance to MtpDocumentsService. 67 */ 68 static MtpDocumentsProvider getInstance() { 69 return sSingleton; 70 } 71 72 @Override 73 public boolean onCreate() { 74 sSingleton = this; 75 mMtpManager = new MtpManager(getContext()); 76 mResolver = getContext().getContentResolver(); 77 mDeviceToolkits = new HashMap<Integer, DeviceToolkit>(); 78 mRootScanner = new RootScanner(mResolver, mMtpManager); 79 return true; 80 } 81 82 @VisibleForTesting 83 void onCreateForTesting(MtpManager mtpManager, ContentResolver resolver) { 84 mMtpManager = mtpManager; 85 mResolver = resolver; 86 mDeviceToolkits = new HashMap<Integer, DeviceToolkit>(); 87 mRootScanner = new RootScanner(mResolver, mMtpManager); 88 } 89 90 @Override 91 public Cursor queryRoots(String[] projection) throws FileNotFoundException { 92 if (projection == null) { 93 projection = MtpDocumentsProvider.DEFAULT_ROOT_PROJECTION; 94 } 95 final MatrixCursor cursor = new MatrixCursor(projection); 96 final MtpRoot[] roots = mRootScanner.getRoots(); 97 for (final MtpRoot root : roots) { 98 final Identifier rootIdentifier = new Identifier(root.mDeviceId, root.mStorageId); 99 final MatrixCursor.RowBuilder builder = cursor.newRow(); 100 builder.add(Root.COLUMN_ROOT_ID, rootIdentifier.toRootId()); 101 builder.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_IS_CHILD | Root.FLAG_SUPPORTS_CREATE); 102 builder.add(Root.COLUMN_TITLE, root.mDescription); 103 builder.add( 104 Root.COLUMN_DOCUMENT_ID, 105 rootIdentifier.toDocumentId()); 106 builder.add(Root.COLUMN_AVAILABLE_BYTES , root.mFreeSpace); 107 } 108 cursor.setNotificationUri( 109 mResolver, DocumentsContract.buildRootsUri(MtpDocumentsProvider.AUTHORITY)); 110 return cursor; 111 } 112 113 @Override 114 public Cursor queryDocument(String documentId, String[] projection) 115 throws FileNotFoundException { 116 if (projection == null) { 117 projection = MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION; 118 } 119 final Identifier identifier = Identifier.createFromDocumentId(documentId); 120 121 if (identifier.mObjectHandle != CursorHelper.DUMMY_HANDLE_FOR_ROOT) { 122 MtpObjectInfo objectInfo; 123 try { 124 objectInfo = mMtpManager.getObjectInfo( 125 identifier.mDeviceId, identifier.mObjectHandle); 126 } catch (IOException e) { 127 throw new FileNotFoundException(e.getMessage()); 128 } 129 final MatrixCursor cursor = new MatrixCursor(projection); 130 CursorHelper.addToCursor( 131 objectInfo, 132 new Identifier(identifier.mDeviceId, identifier.mStorageId), 133 cursor.newRow()); 134 return cursor; 135 } else { 136 MtpRoot[] roots; 137 try { 138 roots = mMtpManager.getRoots(identifier.mDeviceId); 139 } catch (IOException e) { 140 throw new FileNotFoundException(e.getMessage()); 141 } 142 for (final MtpRoot root : roots) { 143 if (identifier.mStorageId != root.mStorageId) 144 continue; 145 final MatrixCursor cursor = new MatrixCursor(projection); 146 CursorHelper.addToCursor(root, cursor.newRow()); 147 return cursor; 148 } 149 } 150 151 throw new FileNotFoundException(); 152 } 153 154 @Override 155 public Cursor queryChildDocuments(String parentDocumentId, 156 String[] projection, String sortOrder) throws FileNotFoundException { 157 if (projection == null) { 158 projection = MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION; 159 } 160 final Identifier parentIdentifier = Identifier.createFromDocumentId(parentDocumentId); 161 try { 162 return getDocumentLoader(parentIdentifier).queryChildDocuments( 163 projection, parentIdentifier); 164 } catch (IOException exception) { 165 throw new FileNotFoundException(exception.getMessage()); 166 } 167 } 168 169 @Override 170 public ParcelFileDescriptor openDocument( 171 String documentId, String mode, CancellationSignal signal) 172 throws FileNotFoundException { 173 final Identifier identifier = Identifier.createFromDocumentId(documentId); 174 try { 175 switch (mode) { 176 case "r": 177 return getPipeManager(identifier).readDocument(mMtpManager, identifier); 178 case "w": 179 // TODO: Clear the parent document loader task (if exists) and call notify 180 // when writing is completed. 181 return getPipeManager(identifier).writeDocument( 182 getContext(), mMtpManager, identifier); 183 default: 184 // TODO: Add support for seekable files. 185 throw new UnsupportedOperationException( 186 "The provider does not support seekable file."); 187 } 188 } catch (IOException error) { 189 throw new FileNotFoundException(error.getMessage()); 190 } 191 } 192 193 @Override 194 public AssetFileDescriptor openDocumentThumbnail( 195 String documentId, 196 Point sizeHint, 197 CancellationSignal signal) throws FileNotFoundException { 198 final Identifier identifier = Identifier.createFromDocumentId(documentId); 199 try { 200 return new AssetFileDescriptor( 201 getPipeManager(identifier).readThumbnail(mMtpManager, identifier), 202 0, // Start offset. 203 AssetFileDescriptor.UNKNOWN_LENGTH); 204 } catch (IOException error) { 205 throw new FileNotFoundException(error.getMessage()); 206 } 207 } 208 209 @Override 210 public void deleteDocument(String documentId) throws FileNotFoundException { 211 try { 212 final Identifier identifier = Identifier.createFromDocumentId(documentId); 213 final int parentHandle = 214 mMtpManager.getParent(identifier.mDeviceId, identifier.mObjectHandle); 215 mMtpManager.deleteDocument(identifier.mDeviceId, identifier.mObjectHandle); 216 final Identifier parentIdentifier = new Identifier( 217 identifier.mDeviceId, identifier.mStorageId, parentHandle); 218 getDocumentLoader(parentIdentifier).clearTask(parentIdentifier); 219 notifyChildDocumentsChange(parentIdentifier.toDocumentId()); 220 } catch (IOException error) { 221 throw new FileNotFoundException(error.getMessage()); 222 } 223 } 224 225 @Override 226 public void onTrimMemory(int level) { 227 for (final DeviceToolkit toolkit : mDeviceToolkits.values()) { 228 toolkit.mDocumentLoader.clearCompletedTasks(); 229 } 230 } 231 232 @Override 233 public String createDocument(String parentDocumentId, String mimeType, String displayName) 234 throws FileNotFoundException { 235 try { 236 final Identifier parentId = Identifier.createFromDocumentId(parentDocumentId); 237 final ParcelFileDescriptor pipe[] = ParcelFileDescriptor.createReliablePipe(); 238 pipe[0].close(); // 0 bytes for a new document. 239 final int objectHandle = mMtpManager.createDocument( 240 parentId.mDeviceId, 241 new MtpObjectInfo.Builder() 242 .setStorageId(parentId.mStorageId) 243 .setParent(parentId.mObjectHandle) 244 .setFormat(CursorHelper.mimeTypeToFormatType(mimeType)) 245 .setName(displayName) 246 .build(), pipe[1]); 247 final String documentId = new Identifier(parentId.mDeviceId, parentId.mStorageId, 248 objectHandle).toDocumentId(); 249 getDocumentLoader(parentId).clearTask(parentId); 250 notifyChildDocumentsChange(parentDocumentId); 251 return documentId; 252 } catch (IOException error) { 253 Log.e(TAG, error.getMessage()); 254 throw new FileNotFoundException(error.getMessage()); 255 } 256 } 257 258 void openDevice(int deviceId) throws IOException { 259 mMtpManager.openDevice(deviceId); 260 mDeviceToolkits.put(deviceId, new DeviceToolkit(mMtpManager, mResolver)); 261 mRootScanner.scanNow(); 262 } 263 264 void closeDevice(int deviceId) throws IOException { 265 // TODO: Flush the device before closing (if not closed externally). 266 getDeviceToolkit(deviceId).mDocumentLoader.clearTasks(); 267 mDeviceToolkits.remove(deviceId); 268 mMtpManager.closeDevice(deviceId); 269 mRootScanner.scanNow(); 270 } 271 272 void closeAllDevices() { 273 boolean closed = false; 274 for (int deviceId : mMtpManager.getOpenedDeviceIds()) { 275 try { 276 mMtpManager.closeDevice(deviceId); 277 getDeviceToolkit(deviceId).mDocumentLoader.clearTasks(); 278 closed = true; 279 } catch (IOException d) { 280 Log.d(TAG, "Failed to close the MTP device: " + deviceId); 281 } 282 } 283 if (closed) { 284 mRootScanner.scanNow(); 285 } 286 } 287 288 boolean hasOpenedDevices() { 289 return mMtpManager.getOpenedDeviceIds().length != 0; 290 } 291 292 private void notifyChildDocumentsChange(String parentDocumentId) { 293 mResolver.notifyChange( 294 DocumentsContract.buildChildDocumentsUri(AUTHORITY, parentDocumentId), 295 null, 296 false); 297 } 298 299 private DeviceToolkit getDeviceToolkit(int deviceId) throws FileNotFoundException { 300 final DeviceToolkit toolkit = mDeviceToolkits.get(deviceId); 301 if (toolkit == null) { 302 throw new FileNotFoundException(); 303 } 304 return toolkit; 305 } 306 307 private PipeManager getPipeManager(Identifier identifier) throws FileNotFoundException { 308 return getDeviceToolkit(identifier.mDeviceId).mPipeManager; 309 } 310 311 private DocumentLoader getDocumentLoader(Identifier identifier) throws FileNotFoundException { 312 return getDeviceToolkit(identifier.mDeviceId).mDocumentLoader; 313 } 314 315 private static class DeviceToolkit { 316 public final PipeManager mPipeManager; 317 public final DocumentLoader mDocumentLoader; 318 319 public DeviceToolkit(MtpManager manager, ContentResolver resolver) { 320 mPipeManager = new PipeManager(); 321 mDocumentLoader = new DocumentLoader(manager, resolver); 322 } 323 } 324} 325