blob: 48499787c3b973660827dbb5157564936acec214 [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);
Tomasz Mikolajewskib80a3cf2015-08-24 16:10:51 +0900204 switch (mode) {
205 case "r":
Daichi Hironof52ef002016-01-11 18:07:01 +0900206 final long fileSize = getFileSize(documentId);
Daichi Hironofda74742016-02-01 13:00:31 +0900207 // MTP getPartialObject operation does not support files that are larger than
208 // 4GB. Fallback to non-seekable file descriptor.
Daichi Hironof52ef002016-01-11 18:07:01 +0900209 // TODO: Use getPartialObject64 for MTP devices that support Android vendor
210 // extension.
Daichi Hironob36b1552016-01-25 14:26:14 +0900211 if (fileSize <= 0xffffffffl) {
Daichi Hironof52ef002016-01-11 18:07:01 +0900212 return mAppFuse.openFile(Integer.parseInt(documentId));
213 } else {
214 return getPipeManager(identifier).readDocument(mMtpManager, identifier);
215 }
Tomasz Mikolajewskib80a3cf2015-08-24 16:10:51 +0900216 case "w":
Tomasz Mikolajewski81d74742015-09-01 13:45:33 +0900217 // TODO: Clear the parent document loader task (if exists) and call notify
218 // when writing is completed.
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900219 return getPipeManager(identifier).writeDocument(
220 getContext(), mMtpManager, identifier);
Daichi Hironof52ef002016-01-11 18:07:01 +0900221 case "rw":
222 // TODO: Add support for "rw" mode.
Tomasz Mikolajewskib80a3cf2015-08-24 16:10:51 +0900223 throw new UnsupportedOperationException(
Daichi Hironof52ef002016-01-11 18:07:01 +0900224 "The provider does not support 'rw' mode.");
225 default:
226 throw new IllegalArgumentException("Unknown mode for openDocument: " + mode);
Tomasz Mikolajewskib80a3cf2015-08-24 16:10:51 +0900227 }
Daichi Hirono8ba41912015-07-30 21:22:57 +0900228 } catch (IOException error) {
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900229 Log.e(MtpDocumentsProvider.TAG, "openDocument", error);
Daichi Hirono8ba41912015-07-30 21:22:57 +0900230 throw new FileNotFoundException(error.getMessage());
231 }
Daichi Hironod5152422015-07-15 13:31:51 +0900232 }
233
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900234 @Override
235 public AssetFileDescriptor openDocumentThumbnail(
236 String documentId,
237 Point sizeHint,
238 CancellationSignal signal) throws FileNotFoundException {
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900239 final Identifier identifier = mDatabase.createIdentifier(documentId);
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900240 try {
Daichi Hironofda74742016-02-01 13:00:31 +0900241 openDevice(identifier.mDeviceId);
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900242 return new AssetFileDescriptor(
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900243 getPipeManager(identifier).readThumbnail(mMtpManager, identifier),
Daichi Hirono573c1fb2015-08-11 19:31:30 +0900244 0, // Start offset.
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900245 AssetFileDescriptor.UNKNOWN_LENGTH);
246 } catch (IOException error) {
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900247 Log.e(MtpDocumentsProvider.TAG, "openDocumentThumbnail", error);
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900248 throw new FileNotFoundException(error.getMessage());
249 }
250 }
251
252 @Override
253 public void deleteDocument(String documentId) throws FileNotFoundException {
254 try {
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900255 final Identifier identifier = mDatabase.createIdentifier(documentId);
Daichi Hironofda74742016-02-01 13:00:31 +0900256 openDevice(identifier.mDeviceId);
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900257 final Identifier parentIdentifier = mDatabase.getParentIdentifier(documentId);
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900258 mMtpManager.deleteDocument(identifier.mDeviceId, identifier.mObjectHandle);
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900259 mDatabase.deleteDocument(documentId);
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900260 getDocumentLoader(parentIdentifier).clearTask(parentIdentifier);
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900261 notifyChildDocumentsChange(parentIdentifier.mDocumentId);
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900262 if (parentIdentifier.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE) {
263 // If the parent is storage, the object might be appeared as child of device because
264 // we skip storage when the device has only one storage.
265 final Identifier deviceIdentifier = mDatabase.getParentIdentifier(
266 parentIdentifier.mDocumentId);
267 notifyChildDocumentsChange(deviceIdentifier.mDocumentId);
268 }
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900269 } catch (IOException error) {
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900270 Log.e(MtpDocumentsProvider.TAG, "deleteDocument", error);
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900271 throw new FileNotFoundException(error.getMessage());
272 }
273 }
274
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900275 @Override
276 public void onTrimMemory(int level) {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900277 synchronized (mDeviceListLock) {
Daichi Hironoe1d57712015-11-17 10:55:45 +0900278 for (final DeviceToolkit toolkit : mDeviceToolkits.values()) {
279 toolkit.mDocumentLoader.clearCompletedTasks();
280 }
281 }
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900282 }
283
Tomasz Mikolajewski87763e62015-08-10 10:10:22 +0900284 @Override
285 public String createDocument(String parentDocumentId, String mimeType, String displayName)
286 throws FileNotFoundException {
Daichi Hirono6213cef2016-02-05 17:21:13 +0900287 if (DEBUG) {
288 Log.d(TAG, "createDocument: " + displayName);
289 }
Tomasz Mikolajewski87763e62015-08-10 10:10:22 +0900290 try {
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900291 final Identifier parentId = mDatabase.createIdentifier(parentDocumentId);
Daichi Hironofda74742016-02-01 13:00:31 +0900292 openDevice(parentId.mDeviceId);
Tomasz Mikolajewskidf544172015-08-31 10:59:43 +0900293 final ParcelFileDescriptor pipe[] = ParcelFileDescriptor.createReliablePipe();
294 pipe[0].close(); // 0 bytes for a new document.
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900295 final int formatCode = Document.MIME_TYPE_DIR.equals(mimeType) ?
296 MtpConstants.FORMAT_ASSOCIATION :
297 MediaFile.getFormatCode(displayName, mimeType);
298 final MtpObjectInfo info = new MtpObjectInfo.Builder()
299 .setStorageId(parentId.mStorageId)
300 .setParent(parentId.mObjectHandle)
301 .setFormat(formatCode)
302 .setName(displayName)
303 .build();
304 final int objectHandle = mMtpManager.createDocument(parentId.mDeviceId, info, pipe[1]);
305 final MtpObjectInfo infoWithHandle =
306 new MtpObjectInfo.Builder(info).setObjectHandle(objectHandle).build();
307 final String documentId = mDatabase.putNewDocument(
308 parentId.mDeviceId, parentDocumentId, infoWithHandle);
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900309 getDocumentLoader(parentId).clearTask(parentId);
Tomasz Mikolajewski87763e62015-08-10 10:10:22 +0900310 notifyChildDocumentsChange(parentDocumentId);
311 return documentId;
312 } catch (IOException error) {
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900313 Log.e(TAG, "createDocument", error);
Tomasz Mikolajewski87763e62015-08-10 10:10:22 +0900314 throw new FileNotFoundException(error.getMessage());
315 }
316 }
317
Daichi Hirono2efe4ca2015-07-27 16:47:46 +0900318 void openDevice(int deviceId) throws IOException {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900319 synchronized (mDeviceListLock) {
Daichi Hironofda74742016-02-01 13:00:31 +0900320 if (mDeviceToolkits.containsKey(deviceId)) {
321 return;
322 }
Daichi Hirono19aa9322016-02-04 14:19:52 +0900323 if (DEBUG) {
324 Log.d(TAG, "Open device " + deviceId);
325 }
Daichi Hironoe1d57712015-11-17 10:55:45 +0900326 mMtpManager.openDevice(deviceId);
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900327 final DeviceToolkit toolkit =
328 new DeviceToolkit(deviceId, mMtpManager, mResolver, mDatabase);
329 mDeviceToolkits.put(deviceId, toolkit);
Daichi Hironofda74742016-02-01 13:00:31 +0900330 mIntentSender.sendUpdateNotificationIntent();
331 try {
332 mRootScanner.resume().await();
333 } catch (InterruptedException error) {
334 Log.e(TAG, "openDevice", error);
335 }
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900336 // Resume document loader to remap disconnected document ID. Must be invoked after the
337 // root scanner resumes.
338 toolkit.mDocumentLoader.resume();
Daichi Hironoe1d57712015-11-17 10:55:45 +0900339 }
Daichi Hironod5152422015-07-15 13:31:51 +0900340 }
341
Daichi Hironoe1d57712015-11-17 10:55:45 +0900342 void closeDevice(int deviceId) throws IOException, InterruptedException {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900343 synchronized (mDeviceListLock) {
344 closeDeviceInternal(deviceId);
Daichi Hironoe1d57712015-11-17 10:55:45 +0900345 }
Daichi Hirono20754c52015-12-15 18:52:26 +0900346 mRootScanner.resume();
Daichi Hironofda74742016-02-01 13:00:31 +0900347 mIntentSender.sendUpdateNotificationIntent();
Daichi Hironod5152422015-07-15 13:31:51 +0900348 }
349
Daichi Hironoa57d9ed2015-12-07 10:52:42 +0900350 int[] getOpenedDeviceIds() {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900351 synchronized (mDeviceListLock) {
Daichi Hironoa57d9ed2015-12-07 10:52:42 +0900352 return mMtpManager.getOpenedDeviceIds();
353 }
354 }
355
356 String getDeviceName(int deviceId) throws IOException {
357 synchronized (mDeviceListLock) {
Daichi Hirono20754c52015-12-15 18:52:26 +0900358 for (final MtpDeviceRecord device : mMtpManager.getDevices()) {
359 if (device.deviceId == deviceId) {
360 return device.name;
361 }
362 }
363 throw new IOException("Not found the device: " + Integer.toString(deviceId));
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900364 }
Daichi Hirono2efe4ca2015-07-27 16:47:46 +0900365 }
Daichi Hirono50d17aa2015-07-28 15:49:01 +0900366
Daichi Hironoe1d57712015-11-17 10:55:45 +0900367 /**
Daichi Hirono1e374442016-02-11 10:08:21 -0800368 * Obtains document ID for the given device ID.
369 * @param deviceId
370 * @return document ID
371 * @throws FileNotFoundException device ID has not been build.
372 */
373 public String getDeviceDocumentId(int deviceId) throws FileNotFoundException {
374 return mDatabase.getDeviceDocumentId(deviceId);
375 }
376
377 /**
Daichi Hironofda74742016-02-01 13:00:31 +0900378 * Resumes root scanner to handle the update of device list.
379 */
380 void resumeRootScanner() {
Daichi Hironoebd24052016-02-06 21:05:57 +0900381 if (DEBUG) {
382 Log.d(MtpDocumentsProvider.TAG, "resumeRootScanner");
383 }
Daichi Hironofda74742016-02-01 13:00:31 +0900384 mRootScanner.resume();
385 }
386
387 /**
Daichi Hironoe1d57712015-11-17 10:55:45 +0900388 * Finalize the content provider for unit tests.
389 */
390 @Override
391 public void shutdown() {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900392 synchronized (mDeviceListLock) {
393 try {
394 for (final int id : mMtpManager.getOpenedDeviceIds()) {
395 closeDeviceInternal(id);
396 }
397 } catch (InterruptedException|IOException e) {
398 // It should fail unit tests by throwing runtime exception.
399 throw new RuntimeException(e);
400 } finally {
401 mDatabase.close();
Daichi Hironob36b1552016-01-25 14:26:14 +0900402 mAppFuse.close();
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900403 super.shutdown();
404 }
Daichi Hironoe1d57712015-11-17 10:55:45 +0900405 }
406 }
407
Daichi Hirono5fecc6c2015-08-04 17:45:51 +0900408 private void notifyChildDocumentsChange(String parentDocumentId) {
409 mResolver.notifyChange(
410 DocumentsContract.buildChildDocumentsUri(AUTHORITY, parentDocumentId),
411 null,
412 false);
413 }
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900414
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900415 /**
Daichi Hironobe388482015-12-14 16:20:14 +0900416 * Clears MTP identifier in the database.
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900417 */
418 private void resume() {
419 synchronized (mDeviceListLock) {
420 mDatabase.getMapper().clearMapping();
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900421 }
422 }
423
424 private void closeDeviceInternal(int deviceId) throws IOException, InterruptedException {
425 // TODO: Flush the device before closing (if not closed externally).
Daichi Hironofda74742016-02-01 13:00:31 +0900426 if (!mDeviceToolkits.containsKey(deviceId)) {
427 return;
428 }
Daichi Hirono19aa9322016-02-04 14:19:52 +0900429 if (DEBUG) {
430 Log.d(TAG, "Close device " + deviceId);
431 }
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900432 getDeviceToolkit(deviceId).mDocumentLoader.close();
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900433 mDeviceToolkits.remove(deviceId);
434 mMtpManager.closeDevice(deviceId);
Daichi Hironoa57d9ed2015-12-07 10:52:42 +0900435 if (getOpenedDeviceIds().length == 0) {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900436 mRootScanner.pause();
437 }
438 }
439
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900440 private DeviceToolkit getDeviceToolkit(int deviceId) throws FileNotFoundException {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900441 synchronized (mDeviceListLock) {
Daichi Hironoe1d57712015-11-17 10:55:45 +0900442 final DeviceToolkit toolkit = mDeviceToolkits.get(deviceId);
443 if (toolkit == null) {
444 throw new FileNotFoundException();
445 }
446 return toolkit;
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900447 }
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900448 }
449
450 private PipeManager getPipeManager(Identifier identifier) throws FileNotFoundException {
451 return getDeviceToolkit(identifier.mDeviceId).mPipeManager;
452 }
453
454 private DocumentLoader getDocumentLoader(Identifier identifier) throws FileNotFoundException {
455 return getDeviceToolkit(identifier.mDeviceId).mDocumentLoader;
456 }
457
Daichi Hironof52ef002016-01-11 18:07:01 +0900458 private long getFileSize(String documentId) throws FileNotFoundException {
459 final Cursor cursor = mDatabase.queryDocument(
460 documentId,
461 MtpDatabase.strings(Document.COLUMN_SIZE, Document.COLUMN_DISPLAY_NAME));
462 try {
463 if (cursor.moveToNext()) {
464 return cursor.getLong(0);
465 } else {
466 throw new FileNotFoundException();
467 }
468 } finally {
469 cursor.close();
470 }
471 }
472
Daichi Hirono29657762016-02-10 16:55:37 -0800473 /**
474 * Creates empty cursor with specific error message.
475 *
476 * @param projection Column names.
477 * @param stringResId String resource ID of error message.
478 * @return Empty cursor with error message.
479 */
480 private Cursor createErrorCursor(String[] projection, int stringResId) {
481 final Bundle bundle = new Bundle();
482 bundle.putString(DocumentsContract.EXTRA_ERROR, mResources.getString(stringResId));
483 final Cursor cursor = new MatrixCursor(projection);
484 cursor.setExtras(bundle);
485 return cursor;
486 }
487
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900488 private static class DeviceToolkit {
489 public final PipeManager mPipeManager;
490 public final DocumentLoader mDocumentLoader;
491
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900492 public DeviceToolkit(
493 int deviceId, MtpManager manager, ContentResolver resolver, MtpDatabase database) {
Daichi Hironof578fa22016-02-19 18:05:42 +0900494 mPipeManager = new PipeManager(database);
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900495 mDocumentLoader = new DocumentLoader(deviceId, manager, resolver, database);
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900496 }
497 }
Daichi Hironof52ef002016-01-11 18:07:01 +0900498
499 private class AppFuseCallback implements AppFuse.Callback {
Daichi Hironof52ef002016-01-11 18:07:01 +0900500 @Override
Daichi Hirono2f310f62016-01-27 12:34:29 +0900501 public long readObjectBytes(
502 int inode, long offset, long size, byte[] buffer) throws IOException {
Daichi Hironof52ef002016-01-11 18:07:01 +0900503 final Identifier identifier = mDatabase.createIdentifier(Integer.toString(inode));
Daichi Hirono2f310f62016-01-27 12:34:29 +0900504 return mMtpManager.getPartialObject(
505 identifier.mDeviceId, identifier.mObjectHandle, offset, size, buffer);
Daichi Hironof52ef002016-01-11 18:07:01 +0900506 }
507
508 @Override
509 public long getFileSize(int inode) throws FileNotFoundException {
510 return MtpDocumentsProvider.this.getFileSize(String.valueOf(inode));
511 }
512 }
Daichi Hironoc00d5d42015-05-28 11:17:41 -0700513}