blob: c031f34a37da22fd979c488dfe40aabafad94010 [file] [log] [blame]
Daichi Hironoc00d5d42015-05-28 11:17:41 -07001/*
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
Daichi Hironod5152422015-07-15 13:31:51 +090019import android.content.ContentResolver;
Daichi Hirono3faa43a2015-08-05 17:15:35 +090020import android.content.res.AssetFileDescriptor;
Daichi Hirono17c8d8b2015-10-12 11:28:46 -070021import android.content.res.Resources;
Daichi Hironoc00d5d42015-05-28 11:17:41 -070022import android.database.Cursor;
Daichi Hironoc18f8072016-02-10 14:59:52 -080023import android.database.MatrixCursor;
Daichi Hirono3faa43a2015-08-05 17:15:35 +090024import android.graphics.Point;
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +090025import android.media.MediaFile;
26import android.mtp.MtpConstants;
Tomasz Mikolajewskibb430fa2015-08-25 18:34:30 +090027import android.mtp.MtpObjectInfo;
Daichi Hironoc18f8072016-02-10 14:59:52 -080028import android.os.Bundle;
Daichi Hironoc00d5d42015-05-28 11:17:41 -070029import android.os.CancellationSignal;
30import android.os.ParcelFileDescriptor;
Daichi Hironof52ef002016-01-11 18:07:01 +090031import android.os.storage.StorageManager;
Daichi Hironoc00d5d42015-05-28 11:17:41 -070032import android.provider.DocumentsContract.Document;
33import android.provider.DocumentsContract.Root;
Tomasz Mikolajewskibb430fa2015-08-25 18:34:30 +090034import android.provider.DocumentsContract;
Daichi Hironoc00d5d42015-05-28 11:17:41 -070035import android.provider.DocumentsProvider;
Daichi Hironod5152422015-07-15 13:31:51 +090036import android.util.Log;
Daichi Hironoc00d5d42015-05-28 11:17:41 -070037
Daichi Hironoe1d57712015-11-17 10:55:45 +090038import com.android.internal.annotations.GuardedBy;
Daichi Hironod5152422015-07-15 13:31:51 +090039import com.android.internal.annotations.VisibleForTesting;
Daichi Hironoc18f8072016-02-10 14:59:52 -080040import com.android.mtp.exceptions.BusyDeviceException;
Daichi Hironod5152422015-07-15 13:31:51 +090041
42import java.io.FileNotFoundException;
43import java.io.IOException;
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +090044import java.util.HashMap;
45import java.util.Map;
Daichi Hironod5152422015-07-15 13:31:51 +090046
47/**
48 * DocumentsProvider for MTP devices.
49 */
Daichi Hironoc00d5d42015-05-28 11:17:41 -070050public class MtpDocumentsProvider extends DocumentsProvider {
Daichi Hirono2efe4ca2015-07-27 16:47:46 +090051 static final String AUTHORITY = "com.android.mtp.documents";
52 static final String TAG = "MtpDocumentsProvider";
Daichi Hirono6baa16e2015-08-12 13:51:59 +090053 static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
Daichi Hironoc00d5d42015-05-28 11:17:41 -070054 Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON,
55 Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID,
56 Root.COLUMN_AVAILABLE_BYTES,
57 };
Daichi Hirono6baa16e2015-08-12 13:51:59 +090058 static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
Daichi Hironoc00d5d42015-05-28 11:17:41 -070059 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
Daichi Hironof83ccbd2016-02-04 16:58:55 +090064 static final boolean DEBUG = false;
Daichi Hirono19aa9322016-02-04 14:19:52 +090065
Daichi Hironoe0282dd2015-11-26 15:20:08 +090066 private final Object mDeviceListLock = new Object();
67
Daichi Hirono2efe4ca2015-07-27 16:47:46 +090068 private static MtpDocumentsProvider sSingleton;
69
70 private MtpManager mMtpManager;
Daichi Hironod5152422015-07-15 13:31:51 +090071 private ContentResolver mResolver;
Daichi Hironoe0282dd2015-11-26 15:20:08 +090072 @GuardedBy("mDeviceListLock")
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +090073 private Map<Integer, DeviceToolkit> mDeviceToolkits;
Daichi Hirono8b9024f2015-08-12 12:59:09 +090074 private RootScanner mRootScanner;
Daichi Hirono17c8d8b2015-10-12 11:28:46 -070075 private Resources mResources;
Daichi Hironodc473442015-11-13 15:42:28 +090076 private MtpDatabase mDatabase;
Daichi Hironof52ef002016-01-11 18:07:01 +090077 private AppFuse mAppFuse;
Daichi Hironofda74742016-02-01 13:00:31 +090078 private ServiceIntentSender mIntentSender;
Daichi Hironod5152422015-07-15 13:31:51 +090079
Daichi Hirono2efe4ca2015-07-27 16:47:46 +090080 /**
81 * Provides singleton instance to MtpDocumentsService.
82 */
83 static MtpDocumentsProvider getInstance() {
84 return sSingleton;
85 }
86
Daichi Hironoc00d5d42015-05-28 11:17:41 -070087 @Override
88 public boolean onCreate() {
Daichi Hirono2efe4ca2015-07-27 16:47:46 +090089 sSingleton = this;
Daichi Hirono17c8d8b2015-10-12 11:28:46 -070090 mResources = getContext().getResources();
Daichi Hirono2efe4ca2015-07-27 16:47:46 +090091 mMtpManager = new MtpManager(getContext());
Daichi Hironod5152422015-07-15 13:31:51 +090092 mResolver = getContext().getContentResolver();
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +090093 mDeviceToolkits = new HashMap<Integer, DeviceToolkit>();
Daichi Hirono47eb1922015-11-16 13:01:31 +090094 mDatabase = new MtpDatabase(getContext(), MtpDatabaseConstants.FLAG_DATABASE_IN_FILE);
Daichi Hironof83ccbd2016-02-04 16:58:55 +090095 mRootScanner = new RootScanner(mResolver, mMtpManager, mDatabase);
Daichi Hironof52ef002016-01-11 18:07:01 +090096 mAppFuse = new AppFuse(TAG, new AppFuseCallback());
Daichi Hironofda74742016-02-01 13:00:31 +090097 mIntentSender = new ServiceIntentSender(getContext());
Daichi Hironof52ef002016-01-11 18:07:01 +090098 // TODO: Mount AppFuse on demands.
Daichi Hironoe6054c02016-01-20 15:36:04 +090099 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 }
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900105 resume();
Daichi Hironoc00d5d42015-05-28 11:17:41 -0700106 return true;
107 }
108
Daichi Hironod5152422015-07-15 13:31:51 +0900109 @VisibleForTesting
Daichi Hironob36b1552016-01-25 14:26:14 +0900110 boolean onCreateForTesting(
Daichi Hironodc473442015-11-13 15:42:28 +0900111 Resources resources,
112 MtpManager mtpManager,
113 ContentResolver resolver,
Daichi Hironob36b1552016-01-25 14:26:14 +0900114 MtpDatabase database,
Daichi Hironofda74742016-02-01 13:00:31 +0900115 StorageManager storageManager,
116 ServiceIntentSender intentSender) {
Daichi Hirono17c8d8b2015-10-12 11:28:46 -0700117 mResources = resources;
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900118 mMtpManager = mtpManager;
119 mResolver = resolver;
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900120 mDeviceToolkits = new HashMap<Integer, DeviceToolkit>();
Daichi Hironodc473442015-11-13 15:42:28 +0900121 mDatabase = database;
Daichi Hironof83ccbd2016-02-04 16:58:55 +0900122 mRootScanner = new RootScanner(mResolver, mMtpManager, mDatabase);
Daichi Hironob36b1552016-01-25 14:26:14 +0900123 mAppFuse = new AppFuse(TAG, new AppFuseCallback());
Daichi Hironofda74742016-02-01 13:00:31 +0900124 mIntentSender = intentSender;
Daichi Hironob36b1552016-01-25 14:26:14 +0900125 // 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 }
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900132 resume();
Daichi Hironob36b1552016-01-25 14:26:14 +0900133 return true;
Daichi Hironod5152422015-07-15 13:31:51 +0900134 }
135
Daichi Hironoc00d5d42015-05-28 11:17:41 -0700136 @Override
137 public Cursor queryRoots(String[] projection) throws FileNotFoundException {
Daichi Hirono50d17aa2015-07-28 15:49:01 +0900138 if (projection == null) {
139 projection = MtpDocumentsProvider.DEFAULT_ROOT_PROJECTION;
140 }
Daichi Hironof83ccbd2016-02-04 16:58:55 +0900141 final Cursor cursor = mDatabase.queryRoots(mResources, projection);
Daichi Hirono50d17aa2015-07-28 15:49:01 +0900142 cursor.setNotificationUri(
143 mResolver, DocumentsContract.buildRootsUri(MtpDocumentsProvider.AUTHORITY));
144 return cursor;
Daichi Hironoc00d5d42015-05-28 11:17:41 -0700145 }
146
147 @Override
148 public Cursor queryDocument(String documentId, String[] projection)
149 throws FileNotFoundException {
Daichi Hironoe5323b72015-07-29 16:10:47 +0900150 if (projection == null) {
151 projection = MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION;
152 }
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900153 return mDatabase.queryDocument(documentId, projection);
Daichi Hironoc00d5d42015-05-28 11:17:41 -0700154 }
155
156 @Override
Daichi Hirono124d0602015-08-11 17:08:35 +0900157 public Cursor queryChildDocuments(String parentDocumentId,
158 String[] projection, String sortOrder) throws FileNotFoundException {
Daichi Hirono19aa9322016-02-04 14:19:52 +0900159 if (DEBUG) {
160 Log.d(TAG, "queryChildDocuments: " + parentDocumentId);
161 }
Daichi Hirono124d0602015-08-11 17:08:35 +0900162 if (projection == null) {
163 projection = MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION;
164 }
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900165 Identifier parentIdentifier = mDatabase.createIdentifier(parentDocumentId);
Daichi Hirono124d0602015-08-11 17:08:35 +0900166 try {
Daichi Hironofda74742016-02-01 13:00:31 +0900167 openDevice(parentIdentifier.mDeviceId);
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900168 if (parentIdentifier.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE) {
Daichi Hirono29657762016-02-10 16:55:37 -0800169 final String[] storageDocIds = mDatabase.getStorageDocumentIds(parentDocumentId);
170 if (storageDocIds.length == 0) {
171 // Remote device does not provide storages. Maybe it is locked.
172 return createErrorCursor(projection, R.string.error_locked_device);
173 } else if (storageDocIds.length > 1) {
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900174 // Returns storage list from database.
175 return mDatabase.queryChildDocuments(projection, parentDocumentId);
176 }
Daichi Hirono29657762016-02-10 16:55:37 -0800177
178 // Exact one storage is found. Skip storage and returns object in the single
179 // storage.
180 parentIdentifier = mDatabase.createIdentifier(storageDocIds[0]);
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900181 }
Daichi Hirono29657762016-02-10 16:55:37 -0800182
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900183 // Returns object list from document loader.
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900184 return getDocumentLoader(parentIdentifier).queryChildDocuments(
185 projection, parentIdentifier);
Daichi Hironoc18f8072016-02-10 14:59:52 -0800186 } catch (BusyDeviceException exception) {
Daichi Hirono29657762016-02-10 16:55:37 -0800187 return createErrorCursor(projection, R.string.error_busy_device);
Daichi Hirono124d0602015-08-11 17:08:35 +0900188 } catch (IOException exception) {
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900189 Log.e(MtpDocumentsProvider.TAG, "queryChildDocuments", exception);
Daichi Hirono124d0602015-08-11 17:08:35 +0900190 throw new FileNotFoundException(exception.getMessage());
191 }
Daichi Hironoc00d5d42015-05-28 11:17:41 -0700192 }
193
194 @Override
Daichi Hirono8ba41912015-07-30 21:22:57 +0900195 public ParcelFileDescriptor openDocument(
196 String documentId, String mode, CancellationSignal signal)
197 throws FileNotFoundException {
Daichi Hirono6213cef2016-02-05 17:21:13 +0900198 if (DEBUG) {
199 Log.d(TAG, "openDocument: " + documentId);
200 }
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900201 final Identifier identifier = mDatabase.createIdentifier(documentId);
Daichi Hirono8ba41912015-07-30 21:22:57 +0900202 try {
Daichi Hironofda74742016-02-01 13:00:31 +0900203 openDevice(identifier.mDeviceId);
Daichi Hirono0f325372016-02-21 15:50:30 +0900204 final MtpDeviceRecord device = getDeviceToolkit(identifier.mDeviceId).mDeviceRecord;
Tomasz Mikolajewskib80a3cf2015-08-24 16:10:51 +0900205 switch (mode) {
206 case "r":
Daichi Hironof52ef002016-01-11 18:07:01 +0900207 final long fileSize = getFileSize(documentId);
Daichi Hironofda74742016-02-01 13:00:31 +0900208 // MTP getPartialObject operation does not support files that are larger than
209 // 4GB. Fallback to non-seekable file descriptor.
Daichi Hironof52ef002016-01-11 18:07:01 +0900210 // TODO: Use getPartialObject64 for MTP devices that support Android vendor
211 // extension.
Daichi Hirono0f325372016-02-21 15:50:30 +0900212 if (MtpDeviceRecord.isPartialReadSupported(
213 device.operationsSupported, fileSize)) {
Daichi Hironof52ef002016-01-11 18:07:01 +0900214 return mAppFuse.openFile(Integer.parseInt(documentId));
215 } else {
216 return getPipeManager(identifier).readDocument(mMtpManager, identifier);
217 }
Tomasz Mikolajewskib80a3cf2015-08-24 16:10:51 +0900218 case "w":
Tomasz Mikolajewski81d74742015-09-01 13:45:33 +0900219 // TODO: Clear the parent document loader task (if exists) and call notify
220 // when writing is completed.
Daichi Hirono0f325372016-02-21 15:50:30 +0900221 if (MtpDeviceRecord.isWritingSupported(device.operationsSupported)) {
222 return getPipeManager(identifier).writeDocument(
Daichi Hirono61ba9232016-02-26 12:58:39 +0900223 getContext(), mMtpManager, identifier, device.operationsSupported);
Daichi Hirono0f325372016-02-21 15:50:30 +0900224 } else {
225 throw new UnsupportedOperationException(
226 "The device does not support writing operation.");
227 }
Daichi Hironof52ef002016-01-11 18:07:01 +0900228 case "rw":
229 // TODO: Add support for "rw" mode.
Tomasz Mikolajewskib80a3cf2015-08-24 16:10:51 +0900230 throw new UnsupportedOperationException(
Daichi Hironof52ef002016-01-11 18:07:01 +0900231 "The provider does not support 'rw' mode.");
232 default:
233 throw new IllegalArgumentException("Unknown mode for openDocument: " + mode);
Tomasz Mikolajewskib80a3cf2015-08-24 16:10:51 +0900234 }
Daichi Hirono8ba41912015-07-30 21:22:57 +0900235 } catch (IOException error) {
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900236 Log.e(MtpDocumentsProvider.TAG, "openDocument", error);
Daichi Hirono8ba41912015-07-30 21:22:57 +0900237 throw new FileNotFoundException(error.getMessage());
238 }
Daichi Hironod5152422015-07-15 13:31:51 +0900239 }
240
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900241 @Override
242 public AssetFileDescriptor openDocumentThumbnail(
243 String documentId,
244 Point sizeHint,
245 CancellationSignal signal) throws FileNotFoundException {
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900246 final Identifier identifier = mDatabase.createIdentifier(documentId);
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900247 try {
Daichi Hironofda74742016-02-01 13:00:31 +0900248 openDevice(identifier.mDeviceId);
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900249 return new AssetFileDescriptor(
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900250 getPipeManager(identifier).readThumbnail(mMtpManager, identifier),
Daichi Hirono573c1fb2015-08-11 19:31:30 +0900251 0, // Start offset.
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900252 AssetFileDescriptor.UNKNOWN_LENGTH);
253 } catch (IOException error) {
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900254 Log.e(MtpDocumentsProvider.TAG, "openDocumentThumbnail", error);
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900255 throw new FileNotFoundException(error.getMessage());
256 }
257 }
258
259 @Override
260 public void deleteDocument(String documentId) throws FileNotFoundException {
261 try {
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900262 final Identifier identifier = mDatabase.createIdentifier(documentId);
Daichi Hironofda74742016-02-01 13:00:31 +0900263 openDevice(identifier.mDeviceId);
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900264 final Identifier parentIdentifier = mDatabase.getParentIdentifier(documentId);
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900265 mMtpManager.deleteDocument(identifier.mDeviceId, identifier.mObjectHandle);
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900266 mDatabase.deleteDocument(documentId);
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900267 getDocumentLoader(parentIdentifier).clearTask(parentIdentifier);
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900268 notifyChildDocumentsChange(parentIdentifier.mDocumentId);
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900269 if (parentIdentifier.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE) {
270 // If the parent is storage, the object might be appeared as child of device because
271 // we skip storage when the device has only one storage.
272 final Identifier deviceIdentifier = mDatabase.getParentIdentifier(
273 parentIdentifier.mDocumentId);
274 notifyChildDocumentsChange(deviceIdentifier.mDocumentId);
275 }
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900276 } catch (IOException error) {
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900277 Log.e(MtpDocumentsProvider.TAG, "deleteDocument", error);
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900278 throw new FileNotFoundException(error.getMessage());
279 }
280 }
281
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900282 @Override
283 public void onTrimMemory(int level) {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900284 synchronized (mDeviceListLock) {
Daichi Hironoe1d57712015-11-17 10:55:45 +0900285 for (final DeviceToolkit toolkit : mDeviceToolkits.values()) {
286 toolkit.mDocumentLoader.clearCompletedTasks();
287 }
288 }
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900289 }
290
Tomasz Mikolajewski87763e62015-08-10 10:10:22 +0900291 @Override
292 public String createDocument(String parentDocumentId, String mimeType, String displayName)
293 throws FileNotFoundException {
Daichi Hirono6213cef2016-02-05 17:21:13 +0900294 if (DEBUG) {
295 Log.d(TAG, "createDocument: " + displayName);
296 }
Tomasz Mikolajewski87763e62015-08-10 10:10:22 +0900297 try {
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900298 final Identifier parentId = mDatabase.createIdentifier(parentDocumentId);
Daichi Hironofda74742016-02-01 13:00:31 +0900299 openDevice(parentId.mDeviceId);
Daichi Hirono0f325372016-02-21 15:50:30 +0900300 final MtpDeviceRecord record = getDeviceToolkit(parentId.mDeviceId).mDeviceRecord;
301 if (!MtpDeviceRecord.isWritingSupported(record.operationsSupported)) {
302 throw new UnsupportedOperationException();
303 }
Tomasz Mikolajewskidf544172015-08-31 10:59:43 +0900304 final ParcelFileDescriptor pipe[] = ParcelFileDescriptor.createReliablePipe();
305 pipe[0].close(); // 0 bytes for a new document.
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900306 final int formatCode = Document.MIME_TYPE_DIR.equals(mimeType) ?
307 MtpConstants.FORMAT_ASSOCIATION :
308 MediaFile.getFormatCode(displayName, mimeType);
309 final MtpObjectInfo info = new MtpObjectInfo.Builder()
310 .setStorageId(parentId.mStorageId)
311 .setParent(parentId.mObjectHandle)
312 .setFormat(formatCode)
313 .setName(displayName)
314 .build();
315 final int objectHandle = mMtpManager.createDocument(parentId.mDeviceId, info, pipe[1]);
316 final MtpObjectInfo infoWithHandle =
317 new MtpObjectInfo.Builder(info).setObjectHandle(objectHandle).build();
318 final String documentId = mDatabase.putNewDocument(
Daichi Hirono61ba9232016-02-26 12:58:39 +0900319 parentId.mDeviceId, parentDocumentId, record.operationsSupported,
320 infoWithHandle);
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900321 getDocumentLoader(parentId).clearTask(parentId);
Tomasz Mikolajewski87763e62015-08-10 10:10:22 +0900322 notifyChildDocumentsChange(parentDocumentId);
323 return documentId;
324 } catch (IOException error) {
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900325 Log.e(TAG, "createDocument", error);
Tomasz Mikolajewski87763e62015-08-10 10:10:22 +0900326 throw new FileNotFoundException(error.getMessage());
327 }
328 }
329
Daichi Hirono2efe4ca2015-07-27 16:47:46 +0900330 void openDevice(int deviceId) throws IOException {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900331 synchronized (mDeviceListLock) {
Daichi Hironofda74742016-02-01 13:00:31 +0900332 if (mDeviceToolkits.containsKey(deviceId)) {
333 return;
334 }
Daichi Hirono19aa9322016-02-04 14:19:52 +0900335 if (DEBUG) {
336 Log.d(TAG, "Open device " + deviceId);
337 }
Daichi Hirono0f325372016-02-21 15:50:30 +0900338 final MtpDeviceRecord device = mMtpManager.openDevice(deviceId);
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900339 final DeviceToolkit toolkit =
Daichi Hirono61ba9232016-02-26 12:58:39 +0900340 new DeviceToolkit(mMtpManager, mResolver, mDatabase, device);
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900341 mDeviceToolkits.put(deviceId, toolkit);
Daichi Hironofda74742016-02-01 13:00:31 +0900342 mIntentSender.sendUpdateNotificationIntent();
343 try {
344 mRootScanner.resume().await();
345 } catch (InterruptedException error) {
346 Log.e(TAG, "openDevice", error);
347 }
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900348 // Resume document loader to remap disconnected document ID. Must be invoked after the
349 // root scanner resumes.
350 toolkit.mDocumentLoader.resume();
Daichi Hironoe1d57712015-11-17 10:55:45 +0900351 }
Daichi Hironod5152422015-07-15 13:31:51 +0900352 }
353
Daichi Hironoe1d57712015-11-17 10:55:45 +0900354 void closeDevice(int deviceId) throws IOException, InterruptedException {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900355 synchronized (mDeviceListLock) {
356 closeDeviceInternal(deviceId);
Daichi Hironoe1d57712015-11-17 10:55:45 +0900357 }
Daichi Hirono20754c52015-12-15 18:52:26 +0900358 mRootScanner.resume();
Daichi Hironofda74742016-02-01 13:00:31 +0900359 mIntentSender.sendUpdateNotificationIntent();
Daichi Hironod5152422015-07-15 13:31:51 +0900360 }
361
Daichi Hirono0f325372016-02-21 15:50:30 +0900362 MtpDeviceRecord[] getOpenedDeviceRecordsCache() {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900363 synchronized (mDeviceListLock) {
Daichi Hirono0f325372016-02-21 15:50:30 +0900364 final MtpDeviceRecord[] records = new MtpDeviceRecord[mDeviceToolkits.size()];
365 int i = 0;
366 for (final DeviceToolkit toolkit : mDeviceToolkits.values()) {
367 records[i] = toolkit.mDeviceRecord;
368 i++;
Daichi Hirono20754c52015-12-15 18:52:26 +0900369 }
Daichi Hirono0f325372016-02-21 15:50:30 +0900370 return records;
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900371 }
Daichi Hirono2efe4ca2015-07-27 16:47:46 +0900372 }
Daichi Hirono50d17aa2015-07-28 15:49:01 +0900373
Daichi Hironoe1d57712015-11-17 10:55:45 +0900374 /**
Daichi Hirono1e374442016-02-11 10:08:21 -0800375 * Obtains document ID for the given device ID.
376 * @param deviceId
377 * @return document ID
378 * @throws FileNotFoundException device ID has not been build.
379 */
380 public String getDeviceDocumentId(int deviceId) throws FileNotFoundException {
381 return mDatabase.getDeviceDocumentId(deviceId);
382 }
383
384 /**
Daichi Hironofda74742016-02-01 13:00:31 +0900385 * Resumes root scanner to handle the update of device list.
386 */
387 void resumeRootScanner() {
Daichi Hironoebd24052016-02-06 21:05:57 +0900388 if (DEBUG) {
389 Log.d(MtpDocumentsProvider.TAG, "resumeRootScanner");
390 }
Daichi Hironofda74742016-02-01 13:00:31 +0900391 mRootScanner.resume();
392 }
393
394 /**
Daichi Hironoe1d57712015-11-17 10:55:45 +0900395 * Finalize the content provider for unit tests.
396 */
397 @Override
398 public void shutdown() {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900399 synchronized (mDeviceListLock) {
400 try {
Daichi Hirono0f325372016-02-21 15:50:30 +0900401 // Copy the opened key set because it will be modified when closing devices.
402 final Integer[] keySet =
403 mDeviceToolkits.keySet().toArray(new Integer[mDeviceToolkits.size()]);
404 for (final int id : keySet) {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900405 closeDeviceInternal(id);
406 }
Daichi Hirono2e9a57b2016-02-26 17:41:45 +0900407 mRootScanner.pause();
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900408 } catch (InterruptedException|IOException e) {
409 // It should fail unit tests by throwing runtime exception.
410 throw new RuntimeException(e);
411 } finally {
412 mDatabase.close();
Daichi Hironob36b1552016-01-25 14:26:14 +0900413 mAppFuse.close();
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900414 super.shutdown();
415 }
Daichi Hironoe1d57712015-11-17 10:55:45 +0900416 }
417 }
418
Daichi Hirono5fecc6c2015-08-04 17:45:51 +0900419 private void notifyChildDocumentsChange(String parentDocumentId) {
420 mResolver.notifyChange(
421 DocumentsContract.buildChildDocumentsUri(AUTHORITY, parentDocumentId),
422 null,
423 false);
424 }
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900425
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900426 /**
Daichi Hironobe388482015-12-14 16:20:14 +0900427 * Clears MTP identifier in the database.
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900428 */
429 private void resume() {
430 synchronized (mDeviceListLock) {
431 mDatabase.getMapper().clearMapping();
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900432 }
433 }
434
435 private void closeDeviceInternal(int deviceId) throws IOException, InterruptedException {
436 // TODO: Flush the device before closing (if not closed externally).
Daichi Hironofda74742016-02-01 13:00:31 +0900437 if (!mDeviceToolkits.containsKey(deviceId)) {
438 return;
439 }
Daichi Hirono19aa9322016-02-04 14:19:52 +0900440 if (DEBUG) {
441 Log.d(TAG, "Close device " + deviceId);
442 }
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900443 getDeviceToolkit(deviceId).mDocumentLoader.close();
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900444 mDeviceToolkits.remove(deviceId);
445 mMtpManager.closeDevice(deviceId);
Daichi Hirono0f325372016-02-21 15:50:30 +0900446 if (mDeviceToolkits.size() == 0) {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900447 mRootScanner.pause();
448 }
449 }
450
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900451 private DeviceToolkit getDeviceToolkit(int deviceId) throws FileNotFoundException {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900452 synchronized (mDeviceListLock) {
Daichi Hironoe1d57712015-11-17 10:55:45 +0900453 final DeviceToolkit toolkit = mDeviceToolkits.get(deviceId);
454 if (toolkit == null) {
455 throw new FileNotFoundException();
456 }
457 return toolkit;
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900458 }
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900459 }
460
461 private PipeManager getPipeManager(Identifier identifier) throws FileNotFoundException {
462 return getDeviceToolkit(identifier.mDeviceId).mPipeManager;
463 }
464
465 private DocumentLoader getDocumentLoader(Identifier identifier) throws FileNotFoundException {
466 return getDeviceToolkit(identifier.mDeviceId).mDocumentLoader;
467 }
468
Daichi Hironof52ef002016-01-11 18:07:01 +0900469 private long getFileSize(String documentId) throws FileNotFoundException {
470 final Cursor cursor = mDatabase.queryDocument(
471 documentId,
472 MtpDatabase.strings(Document.COLUMN_SIZE, Document.COLUMN_DISPLAY_NAME));
473 try {
474 if (cursor.moveToNext()) {
475 return cursor.getLong(0);
476 } else {
477 throw new FileNotFoundException();
478 }
479 } finally {
480 cursor.close();
481 }
482 }
483
Daichi Hirono29657762016-02-10 16:55:37 -0800484 /**
485 * Creates empty cursor with specific error message.
486 *
487 * @param projection Column names.
488 * @param stringResId String resource ID of error message.
489 * @return Empty cursor with error message.
490 */
491 private Cursor createErrorCursor(String[] projection, int stringResId) {
492 final Bundle bundle = new Bundle();
493 bundle.putString(DocumentsContract.EXTRA_ERROR, mResources.getString(stringResId));
494 final Cursor cursor = new MatrixCursor(projection);
495 cursor.setExtras(bundle);
496 return cursor;
497 }
498
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900499 private static class DeviceToolkit {
500 public final PipeManager mPipeManager;
501 public final DocumentLoader mDocumentLoader;
Daichi Hirono0f325372016-02-21 15:50:30 +0900502 public final MtpDeviceRecord mDeviceRecord;
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900503
Daichi Hirono61ba9232016-02-26 12:58:39 +0900504 public DeviceToolkit(MtpManager manager,
505 ContentResolver resolver,
506 MtpDatabase database,
507 MtpDeviceRecord record) {
Daichi Hironof578fa22016-02-19 18:05:42 +0900508 mPipeManager = new PipeManager(database);
Daichi Hirono61ba9232016-02-26 12:58:39 +0900509 mDocumentLoader = new DocumentLoader(record, manager, resolver, database);
Daichi Hirono0f325372016-02-21 15:50:30 +0900510 mDeviceRecord = record;
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900511 }
512 }
Daichi Hironof52ef002016-01-11 18:07:01 +0900513
514 private class AppFuseCallback implements AppFuse.Callback {
Daichi Hironof52ef002016-01-11 18:07:01 +0900515 @Override
Daichi Hirono2f310f62016-01-27 12:34:29 +0900516 public long readObjectBytes(
517 int inode, long offset, long size, byte[] buffer) throws IOException {
Daichi Hironof52ef002016-01-11 18:07:01 +0900518 final Identifier identifier = mDatabase.createIdentifier(Integer.toString(inode));
Daichi Hirono0f325372016-02-21 15:50:30 +0900519 final MtpDeviceRecord record = getDeviceToolkit(identifier.mDeviceId).mDeviceRecord;
520 if (MtpDeviceRecord.isPartialReadSupported(record.operationsSupported, offset)) {
521 return mMtpManager.getPartialObject(
522 identifier.mDeviceId, identifier.mObjectHandle, offset, size, buffer);
523 } else {
524 throw new UnsupportedOperationException();
525 }
Daichi Hironof52ef002016-01-11 18:07:01 +0900526 }
527
528 @Override
529 public long getFileSize(int inode) throws FileNotFoundException {
530 return MtpDocumentsProvider.this.getFileSize(String.valueOf(inode));
531 }
532 }
Daichi Hironoc00d5d42015-05-28 11:17:41 -0700533}