blob: d329e3cdd375d83f7820e2f6952929f52cf159d7 [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 Hironoe0282dd2015-11-26 15:20:08 +0900327 mDeviceToolkits.put(
328 deviceId, new DeviceToolkit(mMtpManager, mResolver, mDatabase));
Daichi Hironofda74742016-02-01 13:00:31 +0900329 mIntentSender.sendUpdateNotificationIntent();
330 try {
331 mRootScanner.resume().await();
332 } catch (InterruptedException error) {
333 Log.e(TAG, "openDevice", error);
334 }
Daichi Hironoe1d57712015-11-17 10:55:45 +0900335 }
Daichi Hironod5152422015-07-15 13:31:51 +0900336 }
337
Daichi Hironoe1d57712015-11-17 10:55:45 +0900338 void closeDevice(int deviceId) throws IOException, InterruptedException {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900339 synchronized (mDeviceListLock) {
340 closeDeviceInternal(deviceId);
Daichi Hironoe1d57712015-11-17 10:55:45 +0900341 }
Daichi Hirono20754c52015-12-15 18:52:26 +0900342 mRootScanner.resume();
Daichi Hironofda74742016-02-01 13:00:31 +0900343 mIntentSender.sendUpdateNotificationIntent();
Daichi Hironod5152422015-07-15 13:31:51 +0900344 }
345
Daichi Hironoa57d9ed2015-12-07 10:52:42 +0900346 int[] getOpenedDeviceIds() {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900347 synchronized (mDeviceListLock) {
Daichi Hironoa57d9ed2015-12-07 10:52:42 +0900348 return mMtpManager.getOpenedDeviceIds();
349 }
350 }
351
352 String getDeviceName(int deviceId) throws IOException {
353 synchronized (mDeviceListLock) {
Daichi Hirono20754c52015-12-15 18:52:26 +0900354 for (final MtpDeviceRecord device : mMtpManager.getDevices()) {
355 if (device.deviceId == deviceId) {
356 return device.name;
357 }
358 }
359 throw new IOException("Not found the device: " + Integer.toString(deviceId));
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900360 }
Daichi Hirono2efe4ca2015-07-27 16:47:46 +0900361 }
Daichi Hirono50d17aa2015-07-28 15:49:01 +0900362
Daichi Hironoe1d57712015-11-17 10:55:45 +0900363 /**
Daichi Hirono1e374442016-02-11 10:08:21 -0800364 * Obtains document ID for the given device ID.
365 * @param deviceId
366 * @return document ID
367 * @throws FileNotFoundException device ID has not been build.
368 */
369 public String getDeviceDocumentId(int deviceId) throws FileNotFoundException {
370 return mDatabase.getDeviceDocumentId(deviceId);
371 }
372
373 /**
Daichi Hironofda74742016-02-01 13:00:31 +0900374 * Resumes root scanner to handle the update of device list.
375 */
376 void resumeRootScanner() {
Daichi Hironoebd24052016-02-06 21:05:57 +0900377 if (DEBUG) {
378 Log.d(MtpDocumentsProvider.TAG, "resumeRootScanner");
379 }
Daichi Hironofda74742016-02-01 13:00:31 +0900380 mRootScanner.resume();
381 }
382
383 /**
Daichi Hironoe1d57712015-11-17 10:55:45 +0900384 * Finalize the content provider for unit tests.
385 */
386 @Override
387 public void shutdown() {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900388 synchronized (mDeviceListLock) {
389 try {
390 for (final int id : mMtpManager.getOpenedDeviceIds()) {
391 closeDeviceInternal(id);
392 }
393 } catch (InterruptedException|IOException e) {
394 // It should fail unit tests by throwing runtime exception.
395 throw new RuntimeException(e);
396 } finally {
397 mDatabase.close();
Daichi Hironob36b1552016-01-25 14:26:14 +0900398 mAppFuse.close();
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900399 super.shutdown();
400 }
Daichi Hironoe1d57712015-11-17 10:55:45 +0900401 }
402 }
403
Daichi Hirono5fecc6c2015-08-04 17:45:51 +0900404 private void notifyChildDocumentsChange(String parentDocumentId) {
405 mResolver.notifyChange(
406 DocumentsContract.buildChildDocumentsUri(AUTHORITY, parentDocumentId),
407 null,
408 false);
409 }
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900410
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900411 /**
Daichi Hironobe388482015-12-14 16:20:14 +0900412 * Clears MTP identifier in the database.
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900413 */
414 private void resume() {
415 synchronized (mDeviceListLock) {
416 mDatabase.getMapper().clearMapping();
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900417 }
418 }
419
420 private void closeDeviceInternal(int deviceId) throws IOException, InterruptedException {
421 // TODO: Flush the device before closing (if not closed externally).
Daichi Hironofda74742016-02-01 13:00:31 +0900422 if (!mDeviceToolkits.containsKey(deviceId)) {
423 return;
424 }
Daichi Hirono19aa9322016-02-04 14:19:52 +0900425 if (DEBUG) {
426 Log.d(TAG, "Close device " + deviceId);
427 }
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900428 getDeviceToolkit(deviceId).mDocumentLoader.clearTasks();
429 mDeviceToolkits.remove(deviceId);
430 mMtpManager.closeDevice(deviceId);
Daichi Hironoa57d9ed2015-12-07 10:52:42 +0900431 if (getOpenedDeviceIds().length == 0) {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900432 mRootScanner.pause();
433 }
434 }
435
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900436 private DeviceToolkit getDeviceToolkit(int deviceId) throws FileNotFoundException {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900437 synchronized (mDeviceListLock) {
Daichi Hironoe1d57712015-11-17 10:55:45 +0900438 final DeviceToolkit toolkit = mDeviceToolkits.get(deviceId);
439 if (toolkit == null) {
440 throw new FileNotFoundException();
441 }
442 return toolkit;
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900443 }
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900444 }
445
446 private PipeManager getPipeManager(Identifier identifier) throws FileNotFoundException {
447 return getDeviceToolkit(identifier.mDeviceId).mPipeManager;
448 }
449
450 private DocumentLoader getDocumentLoader(Identifier identifier) throws FileNotFoundException {
451 return getDeviceToolkit(identifier.mDeviceId).mDocumentLoader;
452 }
453
Daichi Hironof52ef002016-01-11 18:07:01 +0900454 private long getFileSize(String documentId) throws FileNotFoundException {
455 final Cursor cursor = mDatabase.queryDocument(
456 documentId,
457 MtpDatabase.strings(Document.COLUMN_SIZE, Document.COLUMN_DISPLAY_NAME));
458 try {
459 if (cursor.moveToNext()) {
460 return cursor.getLong(0);
461 } else {
462 throw new FileNotFoundException();
463 }
464 } finally {
465 cursor.close();
466 }
467 }
468
Daichi Hirono29657762016-02-10 16:55:37 -0800469 /**
470 * Creates empty cursor with specific error message.
471 *
472 * @param projection Column names.
473 * @param stringResId String resource ID of error message.
474 * @return Empty cursor with error message.
475 */
476 private Cursor createErrorCursor(String[] projection, int stringResId) {
477 final Bundle bundle = new Bundle();
478 bundle.putString(DocumentsContract.EXTRA_ERROR, mResources.getString(stringResId));
479 final Cursor cursor = new MatrixCursor(projection);
480 cursor.setExtras(bundle);
481 return cursor;
482 }
483
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900484 private static class DeviceToolkit {
485 public final PipeManager mPipeManager;
486 public final DocumentLoader mDocumentLoader;
487
Daichi Hirono47eb1922015-11-16 13:01:31 +0900488 public DeviceToolkit(MtpManager manager, ContentResolver resolver, MtpDatabase database) {
Daichi Hironof578fa22016-02-19 18:05:42 +0900489 mPipeManager = new PipeManager(database);
Daichi Hirono47eb1922015-11-16 13:01:31 +0900490 mDocumentLoader = new DocumentLoader(manager, resolver, database);
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900491 }
492 }
Daichi Hironof52ef002016-01-11 18:07:01 +0900493
494 private class AppFuseCallback implements AppFuse.Callback {
Daichi Hironof52ef002016-01-11 18:07:01 +0900495 @Override
Daichi Hirono2f310f62016-01-27 12:34:29 +0900496 public long readObjectBytes(
497 int inode, long offset, long size, byte[] buffer) throws IOException {
Daichi Hironof52ef002016-01-11 18:07:01 +0900498 final Identifier identifier = mDatabase.createIdentifier(Integer.toString(inode));
Daichi Hirono2f310f62016-01-27 12:34:29 +0900499 return mMtpManager.getPartialObject(
500 identifier.mDeviceId, identifier.mObjectHandle, offset, size, buffer);
Daichi Hironof52ef002016-01-11 18:07:01 +0900501 }
502
503 @Override
504 public long getFileSize(int inode) throws FileNotFoundException {
505 return MtpDocumentsProvider.this.getFileSize(String.valueOf(inode));
506 }
507 }
Daichi Hironoc00d5d42015-05-28 11:17:41 -0700508}