blob: 1823711acd597f2cf9b8ab5cf0ba7893c93dc919 [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 Hironof4e7fa82016-03-28 16:07:45 +090020import android.content.Context;
Daichi Hirono3bb37e72016-02-29 15:30:56 +090021import android.content.UriPermission;
Daichi Hirono3faa43a2015-08-05 17:15:35 +090022import android.content.res.AssetFileDescriptor;
Daichi Hirono17c8d8b2015-10-12 11:28:46 -070023import android.content.res.Resources;
Daichi Hironoc00d5d42015-05-28 11:17:41 -070024import android.database.Cursor;
Daichi Hironoc18f8072016-02-10 14:59:52 -080025import android.database.MatrixCursor;
Daichi Hirono5884e1f2016-03-16 14:36:27 +090026import android.database.sqlite.SQLiteDiskIOException;
Daichi Hirono3faa43a2015-08-05 17:15:35 +090027import android.graphics.Point;
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +090028import android.media.MediaFile;
29import android.mtp.MtpConstants;
Tomasz Mikolajewskibb430fa2015-08-25 18:34:30 +090030import android.mtp.MtpObjectInfo;
Daichi Hirono3bb37e72016-02-29 15:30:56 +090031import android.net.Uri;
Daichi Hironoc18f8072016-02-10 14:59:52 -080032import android.os.Bundle;
Daichi Hironoc00d5d42015-05-28 11:17:41 -070033import android.os.CancellationSignal;
Daichi Hironofc7fb752016-03-15 19:19:31 +090034import android.os.FileUtils;
Daichi Hironoc00d5d42015-05-28 11:17:41 -070035import android.os.ParcelFileDescriptor;
Daichi Hironof52ef002016-01-11 18:07:01 +090036import android.os.storage.StorageManager;
Daichi Hironoc00d5d42015-05-28 11:17:41 -070037import android.provider.DocumentsContract.Document;
38import android.provider.DocumentsContract.Root;
Tomasz Mikolajewskibb430fa2015-08-25 18:34:30 +090039import android.provider.DocumentsContract;
Daichi Hironoc00d5d42015-05-28 11:17:41 -070040import android.provider.DocumentsProvider;
Daichi Hirono3bb37e72016-02-29 15:30:56 +090041import android.provider.Settings;
Daichi Hironof4e7fa82016-03-28 16:07:45 +090042import android.system.ErrnoException;
43import android.system.Os;
44import android.system.OsConstants;
Daichi Hironod5152422015-07-15 13:31:51 +090045import android.util.Log;
Daichi Hironoc00d5d42015-05-28 11:17:41 -070046
Daichi Hironoe1d57712015-11-17 10:55:45 +090047import com.android.internal.annotations.GuardedBy;
Daichi Hironod5152422015-07-15 13:31:51 +090048import com.android.internal.annotations.VisibleForTesting;
49
Daichi Hironof4e7fa82016-03-28 16:07:45 +090050import java.io.File;
51import java.io.FileDescriptor;
Daichi Hironod5152422015-07-15 13:31:51 +090052import java.io.FileNotFoundException;
53import java.io.IOException;
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +090054import java.util.HashMap;
Daichi Hirono3bb37e72016-02-29 15:30:56 +090055import java.util.List;
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +090056import java.util.Map;
Daichi Hironoacb0e272016-03-14 21:49:14 +090057import java.util.concurrent.TimeoutException;
Daichi Hironod5152422015-07-15 13:31:51 +090058
59/**
60 * DocumentsProvider for MTP devices.
61 */
Daichi Hironoc00d5d42015-05-28 11:17:41 -070062public class MtpDocumentsProvider extends DocumentsProvider {
Daichi Hirono2efe4ca2015-07-27 16:47:46 +090063 static final String AUTHORITY = "com.android.mtp.documents";
64 static final String TAG = "MtpDocumentsProvider";
Daichi Hirono6baa16e2015-08-12 13:51:59 +090065 static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
Daichi Hironoc00d5d42015-05-28 11:17:41 -070066 Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON,
67 Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID,
68 Root.COLUMN_AVAILABLE_BYTES,
69 };
Daichi Hirono6baa16e2015-08-12 13:51:59 +090070 static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
Daichi Hironoc00d5d42015-05-28 11:17:41 -070071 Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE,
72 Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED,
73 Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
74 };
75
Daichi Hironof83ccbd2016-02-04 16:58:55 +090076 static final boolean DEBUG = false;
Daichi Hirono19aa9322016-02-04 14:19:52 +090077
Daichi Hironoe0282dd2015-11-26 15:20:08 +090078 private final Object mDeviceListLock = new Object();
79
Daichi Hirono2efe4ca2015-07-27 16:47:46 +090080 private static MtpDocumentsProvider sSingleton;
81
82 private MtpManager mMtpManager;
Daichi Hironod5152422015-07-15 13:31:51 +090083 private ContentResolver mResolver;
Daichi Hironoe0282dd2015-11-26 15:20:08 +090084 @GuardedBy("mDeviceListLock")
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +090085 private Map<Integer, DeviceToolkit> mDeviceToolkits;
Daichi Hirono8b9024f2015-08-12 12:59:09 +090086 private RootScanner mRootScanner;
Daichi Hirono17c8d8b2015-10-12 11:28:46 -070087 private Resources mResources;
Daichi Hironodc473442015-11-13 15:42:28 +090088 private MtpDatabase mDatabase;
Daichi Hironof52ef002016-01-11 18:07:01 +090089 private AppFuse mAppFuse;
Daichi Hironofda74742016-02-01 13:00:31 +090090 private ServiceIntentSender mIntentSender;
Daichi Hironof4e7fa82016-03-28 16:07:45 +090091 private Context mContext;
Daichi Hironod5152422015-07-15 13:31:51 +090092
Daichi Hirono2efe4ca2015-07-27 16:47:46 +090093 /**
94 * Provides singleton instance to MtpDocumentsService.
95 */
96 static MtpDocumentsProvider getInstance() {
97 return sSingleton;
98 }
99
Daichi Hironoc00d5d42015-05-28 11:17:41 -0700100 @Override
101 public boolean onCreate() {
Daichi Hirono2efe4ca2015-07-27 16:47:46 +0900102 sSingleton = this;
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900103 mContext = getContext();
Daichi Hirono17c8d8b2015-10-12 11:28:46 -0700104 mResources = getContext().getResources();
Daichi Hirono2efe4ca2015-07-27 16:47:46 +0900105 mMtpManager = new MtpManager(getContext());
Daichi Hironod5152422015-07-15 13:31:51 +0900106 mResolver = getContext().getContentResolver();
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900107 mDeviceToolkits = new HashMap<Integer, DeviceToolkit>();
Daichi Hirono47eb1922015-11-16 13:01:31 +0900108 mDatabase = new MtpDatabase(getContext(), MtpDatabaseConstants.FLAG_DATABASE_IN_FILE);
Daichi Hironof83ccbd2016-02-04 16:58:55 +0900109 mRootScanner = new RootScanner(mResolver, mMtpManager, mDatabase);
Daichi Hironof52ef002016-01-11 18:07:01 +0900110 mAppFuse = new AppFuse(TAG, new AppFuseCallback());
Daichi Hironofda74742016-02-01 13:00:31 +0900111 mIntentSender = new ServiceIntentSender(getContext());
Daichi Hirono3bb37e72016-02-29 15:30:56 +0900112
113 // Check boot count and cleans database if it's first time to launch MtpDocumentsProvider
114 // after booting.
Daichi Hirono5884e1f2016-03-16 14:36:27 +0900115 try {
116 final int bootCount = Settings.Global.getInt(mResolver, Settings.Global.BOOT_COUNT, -1);
117 final int lastBootCount = mDatabase.getLastBootCount();
118 if (bootCount != -1 && bootCount != lastBootCount) {
119 mDatabase.setLastBootCount(bootCount);
120 final List<UriPermission> permissions =
121 mResolver.getOutgoingPersistedUriPermissions();
122 final Uri[] uris = new Uri[permissions.size()];
123 for (int i = 0; i < permissions.size(); i++) {
124 uris[i] = permissions.get(i).getUri();
125 }
126 mDatabase.cleanDatabase(uris);
Daichi Hirono3bb37e72016-02-29 15:30:56 +0900127 }
Daichi Hirono5884e1f2016-03-16 14:36:27 +0900128 } catch (SQLiteDiskIOException error) {
129 // It can happen due to disk shortage.
130 Log.e(TAG, "Failed to clean database.", error);
131 return false;
Daichi Hirono3bb37e72016-02-29 15:30:56 +0900132 }
133
Daichi Hironof52ef002016-01-11 18:07:01 +0900134 // TODO: Mount AppFuse on demands.
Daichi Hironoe6054c02016-01-20 15:36:04 +0900135 try {
136 mAppFuse.mount(getContext().getSystemService(StorageManager.class));
Daichi Hirono5884e1f2016-03-16 14:36:27 +0900137 } catch (IOException error) {
138 Log.e(TAG, "Failed to start app fuse.", error);
Daichi Hironoe6054c02016-01-20 15:36:04 +0900139 return false;
140 }
Daichi Hirono5884e1f2016-03-16 14:36:27 +0900141
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900142 resume();
Daichi Hironoc00d5d42015-05-28 11:17:41 -0700143 return true;
144 }
145
Daichi Hironod5152422015-07-15 13:31:51 +0900146 @VisibleForTesting
Daichi Hironob36b1552016-01-25 14:26:14 +0900147 boolean onCreateForTesting(
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900148 Context context,
Daichi Hironodc473442015-11-13 15:42:28 +0900149 Resources resources,
150 MtpManager mtpManager,
151 ContentResolver resolver,
Daichi Hironob36b1552016-01-25 14:26:14 +0900152 MtpDatabase database,
Daichi Hironofda74742016-02-01 13:00:31 +0900153 StorageManager storageManager,
154 ServiceIntentSender intentSender) {
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900155 mContext = context;
Daichi Hirono17c8d8b2015-10-12 11:28:46 -0700156 mResources = resources;
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900157 mMtpManager = mtpManager;
158 mResolver = resolver;
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900159 mDeviceToolkits = new HashMap<Integer, DeviceToolkit>();
Daichi Hironodc473442015-11-13 15:42:28 +0900160 mDatabase = database;
Daichi Hironof83ccbd2016-02-04 16:58:55 +0900161 mRootScanner = new RootScanner(mResolver, mMtpManager, mDatabase);
Daichi Hironob36b1552016-01-25 14:26:14 +0900162 mAppFuse = new AppFuse(TAG, new AppFuseCallback());
Daichi Hironofda74742016-02-01 13:00:31 +0900163 mIntentSender = intentSender;
Daichi Hirono3bb37e72016-02-29 15:30:56 +0900164
Daichi Hironob36b1552016-01-25 14:26:14 +0900165 // TODO: Mount AppFuse on demands.
166 try {
167 mAppFuse.mount(storageManager);
168 } catch (IOException e) {
169 Log.e(TAG, "Failed to start app fuse.", e);
170 return false;
171 }
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900172 resume();
Daichi Hironob36b1552016-01-25 14:26:14 +0900173 return true;
Daichi Hironod5152422015-07-15 13:31:51 +0900174 }
175
Daichi Hironoc00d5d42015-05-28 11:17:41 -0700176 @Override
177 public Cursor queryRoots(String[] projection) throws FileNotFoundException {
Daichi Hirono50d17aa2015-07-28 15:49:01 +0900178 if (projection == null) {
179 projection = MtpDocumentsProvider.DEFAULT_ROOT_PROJECTION;
180 }
Daichi Hironof83ccbd2016-02-04 16:58:55 +0900181 final Cursor cursor = mDatabase.queryRoots(mResources, projection);
Daichi Hirono50d17aa2015-07-28 15:49:01 +0900182 cursor.setNotificationUri(
183 mResolver, DocumentsContract.buildRootsUri(MtpDocumentsProvider.AUTHORITY));
184 return cursor;
Daichi Hironoc00d5d42015-05-28 11:17:41 -0700185 }
186
187 @Override
188 public Cursor queryDocument(String documentId, String[] projection)
189 throws FileNotFoundException {
Daichi Hironoe5323b72015-07-29 16:10:47 +0900190 if (projection == null) {
191 projection = MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION;
192 }
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900193 return mDatabase.queryDocument(documentId, projection);
Daichi Hironoc00d5d42015-05-28 11:17:41 -0700194 }
195
196 @Override
Daichi Hirono124d0602015-08-11 17:08:35 +0900197 public Cursor queryChildDocuments(String parentDocumentId,
198 String[] projection, String sortOrder) throws FileNotFoundException {
Daichi Hirono19aa9322016-02-04 14:19:52 +0900199 if (DEBUG) {
200 Log.d(TAG, "queryChildDocuments: " + parentDocumentId);
201 }
Daichi Hirono124d0602015-08-11 17:08:35 +0900202 if (projection == null) {
203 projection = MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION;
204 }
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900205 Identifier parentIdentifier = mDatabase.createIdentifier(parentDocumentId);
Daichi Hirono124d0602015-08-11 17:08:35 +0900206 try {
Daichi Hironofda74742016-02-01 13:00:31 +0900207 openDevice(parentIdentifier.mDeviceId);
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900208 if (parentIdentifier.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE) {
Daichi Hirono29657762016-02-10 16:55:37 -0800209 final String[] storageDocIds = mDatabase.getStorageDocumentIds(parentDocumentId);
210 if (storageDocIds.length == 0) {
211 // Remote device does not provide storages. Maybe it is locked.
212 return createErrorCursor(projection, R.string.error_locked_device);
213 } else if (storageDocIds.length > 1) {
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900214 // Returns storage list from database.
215 return mDatabase.queryChildDocuments(projection, parentDocumentId);
216 }
Daichi Hirono29657762016-02-10 16:55:37 -0800217
218 // Exact one storage is found. Skip storage and returns object in the single
219 // storage.
220 parentIdentifier = mDatabase.createIdentifier(storageDocIds[0]);
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900221 }
Daichi Hirono29657762016-02-10 16:55:37 -0800222
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900223 // Returns object list from document loader.
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900224 return getDocumentLoader(parentIdentifier).queryChildDocuments(
225 projection, parentIdentifier);
Daichi Hironoc18f8072016-02-10 14:59:52 -0800226 } catch (BusyDeviceException exception) {
Daichi Hirono29657762016-02-10 16:55:37 -0800227 return createErrorCursor(projection, R.string.error_busy_device);
Daichi Hirono124d0602015-08-11 17:08:35 +0900228 } catch (IOException exception) {
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900229 Log.e(MtpDocumentsProvider.TAG, "queryChildDocuments", exception);
Daichi Hirono124d0602015-08-11 17:08:35 +0900230 throw new FileNotFoundException(exception.getMessage());
231 }
Daichi Hironoc00d5d42015-05-28 11:17:41 -0700232 }
233
234 @Override
Daichi Hirono8ba41912015-07-30 21:22:57 +0900235 public ParcelFileDescriptor openDocument(
236 String documentId, String mode, CancellationSignal signal)
237 throws FileNotFoundException {
Daichi Hirono6213cef2016-02-05 17:21:13 +0900238 if (DEBUG) {
239 Log.d(TAG, "openDocument: " + documentId);
240 }
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900241 final Identifier identifier = mDatabase.createIdentifier(documentId);
Daichi Hirono8ba41912015-07-30 21:22:57 +0900242 try {
Daichi Hironofda74742016-02-01 13:00:31 +0900243 openDevice(identifier.mDeviceId);
Daichi Hirono0f325372016-02-21 15:50:30 +0900244 final MtpDeviceRecord device = getDeviceToolkit(identifier.mDeviceId).mDeviceRecord;
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900245 // Turn off MODE_CREATE because openDocument does not allow to create new files.
246 final int modeFlag =
247 ParcelFileDescriptor.parseMode(mode) & ~ParcelFileDescriptor.MODE_CREATE;
248 if ((modeFlag & ParcelFileDescriptor.MODE_READ_ONLY) != 0) {
249 long fileSize;
250 try {
251 fileSize = getFileSize(documentId);
252 } catch (UnsupportedOperationException exception) {
253 fileSize = -1;
254 }
255 if (MtpDeviceRecord.isPartialReadSupported(
256 device.operationsSupported, fileSize)) {
257 return mAppFuse.openFile(Integer.parseInt(documentId), modeFlag);
258 } else {
259 // If getPartialObject{|64} are not supported for the device, returns
260 // non-seekable pipe FD instead.
261 return getPipeManager(identifier).readDocument(mMtpManager, identifier);
262 }
263 } else if ((modeFlag & ParcelFileDescriptor.MODE_WRITE_ONLY) != 0) {
264 // TODO: Clear the parent document loader task (if exists) and call notify
265 // when writing is completed.
266 if (MtpDeviceRecord.isWritingSupported(device.operationsSupported)) {
267 return mAppFuse.openFile(Integer.parseInt(documentId), modeFlag);
268 } else {
Tomasz Mikolajewskib80a3cf2015-08-24 16:10:51 +0900269 throw new UnsupportedOperationException(
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900270 "The device does not support writing operation.");
271 }
272 } else {
273 // TODO: Add support for "rw" mode.
274 throw new UnsupportedOperationException("The provider does not support 'rw' mode.");
Tomasz Mikolajewskib80a3cf2015-08-24 16:10:51 +0900275 }
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900276 } catch (FileNotFoundException | RuntimeException error) {
277 Log.e(MtpDocumentsProvider.TAG, "openDocument", error);
278 throw error;
Daichi Hirono8ba41912015-07-30 21:22:57 +0900279 } catch (IOException error) {
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900280 Log.e(MtpDocumentsProvider.TAG, "openDocument", error);
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900281 throw new IllegalStateException(error);
Daichi Hirono8ba41912015-07-30 21:22:57 +0900282 }
Daichi Hironod5152422015-07-15 13:31:51 +0900283 }
284
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900285 @Override
286 public AssetFileDescriptor openDocumentThumbnail(
287 String documentId,
288 Point sizeHint,
289 CancellationSignal signal) throws FileNotFoundException {
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900290 final Identifier identifier = mDatabase.createIdentifier(documentId);
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900291 try {
Daichi Hironofda74742016-02-01 13:00:31 +0900292 openDevice(identifier.mDeviceId);
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900293 return new AssetFileDescriptor(
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900294 getPipeManager(identifier).readThumbnail(mMtpManager, identifier),
Daichi Hirono573c1fb2015-08-11 19:31:30 +0900295 0, // Start offset.
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900296 AssetFileDescriptor.UNKNOWN_LENGTH);
297 } catch (IOException error) {
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900298 Log.e(MtpDocumentsProvider.TAG, "openDocumentThumbnail", error);
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900299 throw new FileNotFoundException(error.getMessage());
300 }
301 }
302
303 @Override
304 public void deleteDocument(String documentId) throws FileNotFoundException {
305 try {
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900306 final Identifier identifier = mDatabase.createIdentifier(documentId);
Daichi Hironofda74742016-02-01 13:00:31 +0900307 openDevice(identifier.mDeviceId);
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900308 final Identifier parentIdentifier = mDatabase.getParentIdentifier(documentId);
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900309 mMtpManager.deleteDocument(identifier.mDeviceId, identifier.mObjectHandle);
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900310 mDatabase.deleteDocument(documentId);
Daichi Hirono76be46f2016-04-08 09:48:02 +0900311 getDocumentLoader(parentIdentifier).cancelTask(parentIdentifier);
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900312 notifyChildDocumentsChange(parentIdentifier.mDocumentId);
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900313 if (parentIdentifier.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE) {
314 // If the parent is storage, the object might be appeared as child of device because
315 // we skip storage when the device has only one storage.
316 final Identifier deviceIdentifier = mDatabase.getParentIdentifier(
317 parentIdentifier.mDocumentId);
318 notifyChildDocumentsChange(deviceIdentifier.mDocumentId);
319 }
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900320 } catch (IOException error) {
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900321 Log.e(MtpDocumentsProvider.TAG, "deleteDocument", error);
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900322 throw new FileNotFoundException(error.getMessage());
323 }
324 }
325
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900326 @Override
327 public void onTrimMemory(int level) {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900328 synchronized (mDeviceListLock) {
Daichi Hironoe1d57712015-11-17 10:55:45 +0900329 for (final DeviceToolkit toolkit : mDeviceToolkits.values()) {
330 toolkit.mDocumentLoader.clearCompletedTasks();
331 }
332 }
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900333 }
334
Tomasz Mikolajewski87763e62015-08-10 10:10:22 +0900335 @Override
336 public String createDocument(String parentDocumentId, String mimeType, String displayName)
337 throws FileNotFoundException {
Daichi Hirono6213cef2016-02-05 17:21:13 +0900338 if (DEBUG) {
339 Log.d(TAG, "createDocument: " + displayName);
340 }
Daichi Hironofc7fb752016-03-15 19:19:31 +0900341 final Identifier parentId;
342 final MtpDeviceRecord record;
343 final ParcelFileDescriptor[] pipe;
Tomasz Mikolajewski87763e62015-08-10 10:10:22 +0900344 try {
Daichi Hironofc7fb752016-03-15 19:19:31 +0900345 parentId = mDatabase.createIdentifier(parentDocumentId);
Daichi Hironofda74742016-02-01 13:00:31 +0900346 openDevice(parentId.mDeviceId);
Daichi Hironofc7fb752016-03-15 19:19:31 +0900347 record = getDeviceToolkit(parentId.mDeviceId).mDeviceRecord;
Daichi Hirono0f325372016-02-21 15:50:30 +0900348 if (!MtpDeviceRecord.isWritingSupported(record.operationsSupported)) {
Daichi Hironofc7fb752016-03-15 19:19:31 +0900349 throw new UnsupportedOperationException(
350 "Writing operation is not supported by the device.");
Daichi Hirono0f325372016-02-21 15:50:30 +0900351 }
Daichi Hironofc7fb752016-03-15 19:19:31 +0900352 pipe = ParcelFileDescriptor.createReliablePipe();
353 int objectHandle = -1;
354 MtpObjectInfo info = null;
355 try {
356 pipe[0].close(); // 0 bytes for a new document.
357
358 final int formatCode = Document.MIME_TYPE_DIR.equals(mimeType) ?
359 MtpConstants.FORMAT_ASSOCIATION :
360 MediaFile.getFormatCode(displayName, mimeType);
361 info = new MtpObjectInfo.Builder()
362 .setStorageId(parentId.mStorageId)
363 .setParent(parentId.mObjectHandle)
364 .setFormat(formatCode)
365 .setName(displayName)
366 .build();
367
368 final String[] parts = FileUtils.splitFileName(mimeType, displayName);
369 final String baseName = parts[0];
370 final String extension = parts[1];
371 for (int i = 0; i <= 32; i++) {
372 final MtpObjectInfo infoUniqueName;
373 if (i == 0) {
374 infoUniqueName = info;
375 } else {
Daichi Hirono4f04fd32016-03-18 18:29:56 +0900376 String suffixedName = baseName + " (" + i + " )";
377 if (!extension.isEmpty()) {
378 suffixedName += "." + extension;
379 }
380 infoUniqueName =
381 new MtpObjectInfo.Builder(info).setName(suffixedName).build();
Daichi Hironofc7fb752016-03-15 19:19:31 +0900382 }
383 try {
384 objectHandle = mMtpManager.createDocument(
385 parentId.mDeviceId, infoUniqueName, pipe[1]);
386 break;
387 } catch (SendObjectInfoFailure exp) {
388 // This can be caused when we have an existing file with the same name.
389 continue;
390 }
391 }
392 } finally {
393 pipe[1].close();
394 }
395 if (objectHandle == -1) {
396 throw new IllegalArgumentException(
397 "The file name \"" + displayName + "\" is conflicted with existing files " +
398 "and the provider failed to find unique name.");
399 }
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900400 final MtpObjectInfo infoWithHandle =
401 new MtpObjectInfo.Builder(info).setObjectHandle(objectHandle).build();
402 final String documentId = mDatabase.putNewDocument(
Daichi Hirono61ba9232016-02-26 12:58:39 +0900403 parentId.mDeviceId, parentDocumentId, record.operationsSupported,
Daichi Hirono64111e02016-03-24 21:07:38 +0900404 infoWithHandle, 0l);
Daichi Hirono76be46f2016-04-08 09:48:02 +0900405 getDocumentLoader(parentId).cancelTask(parentId);
Tomasz Mikolajewski87763e62015-08-10 10:10:22 +0900406 notifyChildDocumentsChange(parentDocumentId);
407 return documentId;
Daichi Hironofc7fb752016-03-15 19:19:31 +0900408 } catch (FileNotFoundException | RuntimeException error) {
409 Log.e(TAG, "createDocument", error);
410 throw error;
Tomasz Mikolajewski87763e62015-08-10 10:10:22 +0900411 } catch (IOException error) {
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900412 Log.e(TAG, "createDocument", error);
Daichi Hironofc7fb752016-03-15 19:19:31 +0900413 throw new IllegalStateException(error);
Tomasz Mikolajewski87763e62015-08-10 10:10:22 +0900414 }
415 }
416
Daichi Hirono2efe4ca2015-07-27 16:47:46 +0900417 void openDevice(int deviceId) throws IOException {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900418 synchronized (mDeviceListLock) {
Daichi Hironofda74742016-02-01 13:00:31 +0900419 if (mDeviceToolkits.containsKey(deviceId)) {
420 return;
421 }
Daichi Hirono19aa9322016-02-04 14:19:52 +0900422 if (DEBUG) {
423 Log.d(TAG, "Open device " + deviceId);
424 }
Daichi Hirono0f325372016-02-21 15:50:30 +0900425 final MtpDeviceRecord device = mMtpManager.openDevice(deviceId);
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900426 final DeviceToolkit toolkit =
Daichi Hirono61ba9232016-02-26 12:58:39 +0900427 new DeviceToolkit(mMtpManager, mResolver, mDatabase, device);
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900428 mDeviceToolkits.put(deviceId, toolkit);
Daichi Hironofda74742016-02-01 13:00:31 +0900429 mIntentSender.sendUpdateNotificationIntent();
430 try {
431 mRootScanner.resume().await();
432 } catch (InterruptedException error) {
433 Log.e(TAG, "openDevice", error);
434 }
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900435 // Resume document loader to remap disconnected document ID. Must be invoked after the
436 // root scanner resumes.
437 toolkit.mDocumentLoader.resume();
Daichi Hironoe1d57712015-11-17 10:55:45 +0900438 }
Daichi Hironod5152422015-07-15 13:31:51 +0900439 }
440
Daichi Hironoe1d57712015-11-17 10:55:45 +0900441 void closeDevice(int deviceId) throws IOException, InterruptedException {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900442 synchronized (mDeviceListLock) {
443 closeDeviceInternal(deviceId);
Daichi Hironoe1d57712015-11-17 10:55:45 +0900444 }
Daichi Hirono20754c52015-12-15 18:52:26 +0900445 mRootScanner.resume();
Daichi Hironofda74742016-02-01 13:00:31 +0900446 mIntentSender.sendUpdateNotificationIntent();
Daichi Hironod5152422015-07-15 13:31:51 +0900447 }
448
Daichi Hirono0f325372016-02-21 15:50:30 +0900449 MtpDeviceRecord[] getOpenedDeviceRecordsCache() {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900450 synchronized (mDeviceListLock) {
Daichi Hirono0f325372016-02-21 15:50:30 +0900451 final MtpDeviceRecord[] records = new MtpDeviceRecord[mDeviceToolkits.size()];
452 int i = 0;
453 for (final DeviceToolkit toolkit : mDeviceToolkits.values()) {
454 records[i] = toolkit.mDeviceRecord;
455 i++;
Daichi Hirono20754c52015-12-15 18:52:26 +0900456 }
Daichi Hirono0f325372016-02-21 15:50:30 +0900457 return records;
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900458 }
Daichi Hirono2efe4ca2015-07-27 16:47:46 +0900459 }
Daichi Hirono50d17aa2015-07-28 15:49:01 +0900460
Daichi Hironoe1d57712015-11-17 10:55:45 +0900461 /**
Daichi Hirono1e374442016-02-11 10:08:21 -0800462 * Obtains document ID for the given device ID.
463 * @param deviceId
464 * @return document ID
465 * @throws FileNotFoundException device ID has not been build.
466 */
467 public String getDeviceDocumentId(int deviceId) throws FileNotFoundException {
468 return mDatabase.getDeviceDocumentId(deviceId);
469 }
470
471 /**
Daichi Hironofda74742016-02-01 13:00:31 +0900472 * Resumes root scanner to handle the update of device list.
473 */
474 void resumeRootScanner() {
Daichi Hironoebd24052016-02-06 21:05:57 +0900475 if (DEBUG) {
476 Log.d(MtpDocumentsProvider.TAG, "resumeRootScanner");
477 }
Daichi Hironofda74742016-02-01 13:00:31 +0900478 mRootScanner.resume();
479 }
480
481 /**
Daichi Hironoe1d57712015-11-17 10:55:45 +0900482 * Finalize the content provider for unit tests.
483 */
484 @Override
485 public void shutdown() {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900486 synchronized (mDeviceListLock) {
487 try {
Daichi Hirono0f325372016-02-21 15:50:30 +0900488 // Copy the opened key set because it will be modified when closing devices.
489 final Integer[] keySet =
490 mDeviceToolkits.keySet().toArray(new Integer[mDeviceToolkits.size()]);
491 for (final int id : keySet) {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900492 closeDeviceInternal(id);
493 }
Daichi Hirono2e9a57b2016-02-26 17:41:45 +0900494 mRootScanner.pause();
Daichi Hironoacb0e272016-03-14 21:49:14 +0900495 } catch (InterruptedException | IOException | TimeoutException e) {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900496 // It should fail unit tests by throwing runtime exception.
497 throw new RuntimeException(e);
498 } finally {
499 mDatabase.close();
Daichi Hironob36b1552016-01-25 14:26:14 +0900500 mAppFuse.close();
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900501 super.shutdown();
502 }
Daichi Hironoe1d57712015-11-17 10:55:45 +0900503 }
504 }
505
Daichi Hirono5fecc6c2015-08-04 17:45:51 +0900506 private void notifyChildDocumentsChange(String parentDocumentId) {
507 mResolver.notifyChange(
508 DocumentsContract.buildChildDocumentsUri(AUTHORITY, parentDocumentId),
509 null,
510 false);
511 }
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900512
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900513 /**
Daichi Hironobe388482015-12-14 16:20:14 +0900514 * Clears MTP identifier in the database.
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900515 */
516 private void resume() {
517 synchronized (mDeviceListLock) {
518 mDatabase.getMapper().clearMapping();
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900519 }
520 }
521
522 private void closeDeviceInternal(int deviceId) throws IOException, InterruptedException {
523 // TODO: Flush the device before closing (if not closed externally).
Daichi Hironofda74742016-02-01 13:00:31 +0900524 if (!mDeviceToolkits.containsKey(deviceId)) {
525 return;
526 }
Daichi Hirono19aa9322016-02-04 14:19:52 +0900527 if (DEBUG) {
528 Log.d(TAG, "Close device " + deviceId);
529 }
Daichi Hirono24ab92a2016-03-04 17:53:03 +0900530 getDeviceToolkit(deviceId).close();
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900531 mDeviceToolkits.remove(deviceId);
532 mMtpManager.closeDevice(deviceId);
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900533 }
534
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900535 private DeviceToolkit getDeviceToolkit(int deviceId) throws FileNotFoundException {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900536 synchronized (mDeviceListLock) {
Daichi Hironoe1d57712015-11-17 10:55:45 +0900537 final DeviceToolkit toolkit = mDeviceToolkits.get(deviceId);
538 if (toolkit == null) {
539 throw new FileNotFoundException();
540 }
541 return toolkit;
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900542 }
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900543 }
544
545 private PipeManager getPipeManager(Identifier identifier) throws FileNotFoundException {
546 return getDeviceToolkit(identifier.mDeviceId).mPipeManager;
547 }
548
549 private DocumentLoader getDocumentLoader(Identifier identifier) throws FileNotFoundException {
550 return getDeviceToolkit(identifier.mDeviceId).mDocumentLoader;
551 }
552
Daichi Hironof52ef002016-01-11 18:07:01 +0900553 private long getFileSize(String documentId) throws FileNotFoundException {
554 final Cursor cursor = mDatabase.queryDocument(
555 documentId,
556 MtpDatabase.strings(Document.COLUMN_SIZE, Document.COLUMN_DISPLAY_NAME));
557 try {
558 if (cursor.moveToNext()) {
Daichi Hirono77a1c6562016-03-28 14:37:12 +0900559 if (cursor.isNull(0)) {
560 throw new UnsupportedOperationException();
561 }
Daichi Hironof52ef002016-01-11 18:07:01 +0900562 return cursor.getLong(0);
563 } else {
564 throw new FileNotFoundException();
565 }
566 } finally {
567 cursor.close();
568 }
569 }
570
Daichi Hirono29657762016-02-10 16:55:37 -0800571 /**
572 * Creates empty cursor with specific error message.
573 *
574 * @param projection Column names.
575 * @param stringResId String resource ID of error message.
576 * @return Empty cursor with error message.
577 */
578 private Cursor createErrorCursor(String[] projection, int stringResId) {
579 final Bundle bundle = new Bundle();
580 bundle.putString(DocumentsContract.EXTRA_ERROR, mResources.getString(stringResId));
581 final Cursor cursor = new MatrixCursor(projection);
582 cursor.setExtras(bundle);
583 return cursor;
584 }
585
Daichi Hirono24ab92a2016-03-04 17:53:03 +0900586 private static class DeviceToolkit implements AutoCloseable {
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900587 public final PipeManager mPipeManager;
588 public final DocumentLoader mDocumentLoader;
Daichi Hirono0f325372016-02-21 15:50:30 +0900589 public final MtpDeviceRecord mDeviceRecord;
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900590
Daichi Hirono61ba9232016-02-26 12:58:39 +0900591 public DeviceToolkit(MtpManager manager,
592 ContentResolver resolver,
593 MtpDatabase database,
594 MtpDeviceRecord record) {
Daichi Hironof578fa22016-02-19 18:05:42 +0900595 mPipeManager = new PipeManager(database);
Daichi Hirono61ba9232016-02-26 12:58:39 +0900596 mDocumentLoader = new DocumentLoader(record, manager, resolver, database);
Daichi Hirono0f325372016-02-21 15:50:30 +0900597 mDeviceRecord = record;
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900598 }
Daichi Hirono24ab92a2016-03-04 17:53:03 +0900599
600 @Override
601 public void close() throws InterruptedException {
602 mPipeManager.close();
603 mDocumentLoader.close();
604 }
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900605 }
Daichi Hironof52ef002016-01-11 18:07:01 +0900606
607 private class AppFuseCallback implements AppFuse.Callback {
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900608 private final Map<Long, MtpFileWriter> mWriters = new HashMap<>();
609
610 @Override
611 public long getFileSize(int inode) throws FileNotFoundException {
612 return MtpDocumentsProvider.this.getFileSize(String.valueOf(inode));
613 }
614
Daichi Hironof52ef002016-01-11 18:07:01 +0900615 @Override
Daichi Hirono2f310f62016-01-27 12:34:29 +0900616 public long readObjectBytes(
617 int inode, long offset, long size, byte[] buffer) throws IOException {
Daichi Hironof52ef002016-01-11 18:07:01 +0900618 final Identifier identifier = mDatabase.createIdentifier(Integer.toString(inode));
Daichi Hirono0f325372016-02-21 15:50:30 +0900619 final MtpDeviceRecord record = getDeviceToolkit(identifier.mDeviceId).mDeviceRecord;
Daichi Hirono77a1c6562016-03-28 14:37:12 +0900620
621 if (MtpDeviceRecord.isSupported(
622 record.operationsSupported, MtpConstants.OPERATION_GET_PARTIAL_OBJECT_64)) {
623 return mMtpManager.getPartialObject64(
624 identifier.mDeviceId, identifier.mObjectHandle, offset, size, buffer);
625 }
626
627 if (0 <= offset && offset <= 0xffffffffL && MtpDeviceRecord.isSupported(
628 record.operationsSupported, MtpConstants.OPERATION_GET_PARTIAL_OBJECT)) {
Daichi Hirono0f325372016-02-21 15:50:30 +0900629 return mMtpManager.getPartialObject(
630 identifier.mDeviceId, identifier.mObjectHandle, offset, size, buffer);
Daichi Hirono0f325372016-02-21 15:50:30 +0900631 }
Daichi Hirono77a1c6562016-03-28 14:37:12 +0900632
633 throw new UnsupportedOperationException();
Daichi Hironof52ef002016-01-11 18:07:01 +0900634 }
635
636 @Override
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900637 public int writeObjectBytes(
638 long fileHandle, int inode, long offset, int size, byte[] bytes)
639 throws IOException, ErrnoException {
640 final MtpFileWriter writer;
641 if (mWriters.containsKey(fileHandle)) {
642 writer = mWriters.get(fileHandle);
643 } else {
644 writer = new MtpFileWriter(mContext, String.valueOf(inode));
645 mWriters.put(fileHandle, writer);
646 }
647 return writer.write(offset, size, bytes);
Daichi Hironof52ef002016-01-11 18:07:01 +0900648 }
Daichi Hirono09ece6c2016-01-20 19:09:25 +0900649
650 @Override
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900651 public void flushFileHandle(long fileHandle) throws IOException, ErrnoException {
652 final MtpFileWriter writer = mWriters.get(fileHandle);
653 if (writer == null) {
654 // File handle for reading.
655 return;
656 }
657 final MtpDeviceRecord device = getDeviceToolkit(
658 mDatabase.createIdentifier(writer.getDocumentId()).mDeviceId).mDeviceRecord;
659 writer.flush(mMtpManager, mDatabase, device.operationsSupported);
660 }
661
662 @Override
663 public void closeFileHandle(long fileHandle) throws IOException, ErrnoException {
664 final MtpFileWriter writer = mWriters.get(fileHandle);
665 if (writer == null) {
666 // File handle for reading.
667 return;
668 }
669 try {
670 writer.close();
671 } finally {
672 mWriters.remove(fileHandle);
673 }
Daichi Hirono09ece6c2016-01-20 19:09:25 +0900674 }
Daichi Hironof52ef002016-01-11 18:07:01 +0900675 }
Daichi Hironoc00d5d42015-05-28 11:17:41 -0700676}