MtpDocumentsProvider.java revision 124d060bc980c7555616ff9d07a4dc3b8f3cd341
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.os.CancellationSignal; 25import android.os.ParcelFileDescriptor; 26import android.provider.DocumentsContract; 27import android.provider.DocumentsContract.Document; 28import android.provider.DocumentsContract.Root; 29import android.provider.DocumentsProvider; 30import android.util.Log; 31 32import com.android.internal.annotations.VisibleForTesting; 33 34import java.io.FileNotFoundException; 35import java.io.IOException; 36 37/** 38 * DocumentsProvider for MTP devices. 39 */ 40public class MtpDocumentsProvider extends DocumentsProvider { 41 static final String AUTHORITY = "com.android.mtp.documents"; 42 static final String TAG = "MtpDocumentsProvider"; 43 private static final String[] DEFAULT_ROOT_PROJECTION = new String[] { 44 Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, 45 Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID, 46 Root.COLUMN_AVAILABLE_BYTES, 47 }; 48 private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] { 49 Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, 50 Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED, 51 Document.COLUMN_FLAGS, Document.COLUMN_SIZE, 52 }; 53 54 private static MtpDocumentsProvider sSingleton; 55 56 private MtpManager mMtpManager; 57 private ContentResolver mResolver; 58 private PipeManager mPipeManager; 59 60 /** 61 * Provides singleton instance to MtpDocumentsService. 62 */ 63 static MtpDocumentsProvider getInstance() { 64 return sSingleton; 65 } 66 67 @Override 68 public boolean onCreate() { 69 sSingleton = this; 70 mMtpManager = new MtpManager(getContext()); 71 mResolver = getContext().getContentResolver(); 72 mPipeManager = new PipeManager(); 73 74 return true; 75 } 76 77 @VisibleForTesting 78 void onCreateForTesting(MtpManager mtpManager, ContentResolver resolver) { 79 this.mMtpManager = mtpManager; 80 this.mResolver = resolver; 81 } 82 83 @Override 84 public Cursor queryRoots(String[] projection) throws FileNotFoundException { 85 if (projection == null) { 86 projection = MtpDocumentsProvider.DEFAULT_ROOT_PROJECTION; 87 } 88 final MatrixCursor cursor = new MatrixCursor(projection); 89 for (final int deviceId : mMtpManager.getOpenedDeviceIds()) { 90 try { 91 final MtpRoot[] roots = mMtpManager.getRoots(deviceId); 92 // TODO: Add retry logic here. 93 94 for (final MtpRoot root : roots) { 95 final Identifier rootIdentifier = new Identifier(deviceId, root.mStorageId); 96 final MatrixCursor.RowBuilder builder = cursor.newRow(); 97 builder.add(Root.COLUMN_ROOT_ID, rootIdentifier.toRootId()); 98 builder.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_IS_CHILD); 99 builder.add(Root.COLUMN_TITLE, root.mDescription); 100 builder.add( 101 Root.COLUMN_DOCUMENT_ID, 102 rootIdentifier.toDocumentId()); 103 builder.add(Root.COLUMN_AVAILABLE_BYTES , root.mFreeSpace); 104 } 105 } catch (IOException error) { 106 Log.d(TAG, error.getMessage()); 107 } 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 MtpDocument document = null; 123 if (identifier.mObjectHandle != MtpDocument.DUMMY_HANDLE_FOR_ROOT) { 124 try { 125 document = mMtpManager.getDocument(identifier.mDeviceId, identifier.mObjectHandle); 126 } catch (IOException e) { 127 throw new FileNotFoundException(e.getMessage()); 128 } 129 } else { 130 MtpRoot[] roots; 131 try { 132 roots = mMtpManager.getRoots(identifier.mDeviceId); 133 if (roots != null) { 134 for (final MtpRoot root : roots) { 135 if (identifier.mStorageId == root.mStorageId) { 136 document = new MtpDocument(root); 137 break; 138 } 139 } 140 } 141 if (document == null) { 142 throw new FileNotFoundException(); 143 } 144 } catch (IOException e) { 145 throw new FileNotFoundException(e.getMessage()); 146 } 147 } 148 149 final MatrixCursor cursor = new MatrixCursor(projection); 150 document.addToCursor( 151 new Identifier(identifier.mDeviceId, identifier.mStorageId), cursor.newRow()); 152 return cursor; 153 } 154 155 // TODO: Support background loading for large number of files. 156 @Override 157 public Cursor queryChildDocuments(String parentDocumentId, 158 String[] projection, String sortOrder) throws FileNotFoundException { 159 if (projection == null) { 160 projection = MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION; 161 } 162 final Identifier parentIdentifier = Identifier.createFromDocumentId(parentDocumentId); 163 int parentHandle = parentIdentifier.mObjectHandle; 164 // Need to pass the special value MtpManager.OBJECT_HANDLE_ROOT_CHILDREN to 165 // getObjectHandles if we would like to obtain children under the root. 166 if (parentHandle == MtpDocument.DUMMY_HANDLE_FOR_ROOT) { 167 parentHandle = MtpManager.OBJECT_HANDLE_ROOT_CHILDREN; 168 } 169 try { 170 final MatrixCursor cursor = new MatrixCursor(projection); 171 final Identifier rootIdentifier = new Identifier( 172 parentIdentifier.mDeviceId, parentIdentifier.mStorageId); 173 final int[] objectHandles = mMtpManager.getObjectHandles( 174 parentIdentifier.mDeviceId, parentIdentifier.mStorageId, parentHandle); 175 for (int i = 0; i < objectHandles.length; i++) { 176 try { 177 final MtpDocument document = mMtpManager.getDocument( 178 parentIdentifier.mDeviceId, objectHandles[i]); 179 document.addToCursor(rootIdentifier, cursor.newRow()); 180 } catch (IOException error) { 181 cursor.close(); 182 throw new FileNotFoundException(error.getMessage()); 183 } 184 } 185 return cursor; 186 } catch (IOException exception) { 187 throw new FileNotFoundException(exception.getMessage()); 188 } 189 } 190 191 @Override 192 public ParcelFileDescriptor openDocument( 193 String documentId, String mode, CancellationSignal signal) 194 throws FileNotFoundException { 195 if (!"r".equals(mode) && !"w".equals(mode)) { 196 // TODO: Support seekable file. 197 throw new UnsupportedOperationException("The provider does not support seekable file."); 198 } 199 final Identifier identifier = Identifier.createFromDocumentId(documentId); 200 try { 201 return mPipeManager.readDocument(mMtpManager, identifier); 202 } catch (IOException error) { 203 throw new FileNotFoundException(error.getMessage()); 204 } 205 } 206 207 @Override 208 public AssetFileDescriptor openDocumentThumbnail( 209 String documentId, 210 Point sizeHint, 211 CancellationSignal signal) throws FileNotFoundException { 212 final Identifier identifier = Identifier.createFromDocumentId(documentId); 213 try { 214 return new AssetFileDescriptor( 215 mPipeManager.readThumbnail(mMtpManager, identifier), 216 0, 217 AssetFileDescriptor.UNKNOWN_LENGTH); 218 } catch (IOException error) { 219 throw new FileNotFoundException(error.getMessage()); 220 } 221 } 222 223 @Override 224 public void deleteDocument(String documentId) throws FileNotFoundException { 225 try { 226 final Identifier identifier = Identifier.createFromDocumentId(documentId); 227 final int parentHandle = 228 mMtpManager.getParent(identifier.mDeviceId, identifier.mObjectHandle); 229 mMtpManager.deleteDocument(identifier.mDeviceId, identifier.mObjectHandle); 230 notifyChildDocumentsChange(new Identifier( 231 identifier.mDeviceId, identifier.mStorageId, parentHandle).toDocumentId()); 232 } catch (IOException error) { 233 throw new FileNotFoundException(error.getMessage()); 234 } 235 } 236 237 void openDevice(int deviceId) throws IOException { 238 mMtpManager.openDevice(deviceId); 239 notifyRootsChange(); 240 } 241 242 void closeDevice(int deviceId) throws IOException { 243 mMtpManager.closeDevice(deviceId); 244 notifyRootsChange(); 245 } 246 247 void closeAllDevices() { 248 boolean closed = false; 249 for (int deviceId : mMtpManager.getOpenedDeviceIds()) { 250 try { 251 mMtpManager.closeDevice(deviceId); 252 closed = true; 253 } catch (IOException d) { 254 Log.d(TAG, "Failed to close the MTP device: " + deviceId); 255 } 256 } 257 if (closed) { 258 notifyRootsChange(); 259 } 260 } 261 262 boolean hasOpenedDevices() { 263 return mMtpManager.getOpenedDeviceIds().length != 0; 264 } 265 266 private void notifyRootsChange() { 267 mResolver.notifyChange( 268 DocumentsContract.buildRootsUri(MtpDocumentsProvider.AUTHORITY), null, false); 269 } 270 271 private void notifyChildDocumentsChange(String parentDocumentId) { 272 mResolver.notifyChange( 273 DocumentsContract.buildChildDocumentsUri(AUTHORITY, parentDocumentId), 274 null, 275 false); 276 } 277} 278