blob: 8c8116bd342ae0f120a53b1bc9a6c1108cf58bbe [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
Steve McKay5a10ff12017-08-01 15:02:50 -070019import android.annotation.Nullable;
Daichi Hironod5152422015-07-15 13:31:51 +090020import android.content.ContentResolver;
Daichi Hirono66fcb4b2017-03-23 15:24:13 +090021import android.content.ContentValues;
Daichi Hironof4e7fa82016-03-28 16:07:45 +090022import android.content.Context;
Daichi Hirono3bb37e72016-02-29 15:30:56 +090023import android.content.UriPermission;
Daichi Hirono3faa43a2015-08-05 17:15:35 +090024import android.content.res.AssetFileDescriptor;
Daichi Hirono17c8d8b2015-10-12 11:28:46 -070025import android.content.res.Resources;
Daichi Hironoc00d5d42015-05-28 11:17:41 -070026import android.database.Cursor;
Daichi Hirono66fcb4b2017-03-23 15:24:13 +090027import android.database.DatabaseUtils;
Daichi Hironoc18f8072016-02-10 14:59:52 -080028import android.database.MatrixCursor;
Daichi Hirono5884e1f2016-03-16 14:36:27 +090029import android.database.sqlite.SQLiteDiskIOException;
Daichi Hirono3faa43a2015-08-05 17:15:35 +090030import android.graphics.Point;
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +090031import android.media.MediaFile;
32import android.mtp.MtpConstants;
Tomasz Mikolajewskibb430fa2015-08-25 18:34:30 +090033import android.mtp.MtpObjectInfo;
Daichi Hirono3bb37e72016-02-29 15:30:56 +090034import android.net.Uri;
Daichi Hironoc18f8072016-02-10 14:59:52 -080035import android.os.Bundle;
Daichi Hironoc00d5d42015-05-28 11:17:41 -070036import android.os.CancellationSignal;
Daichi Hironofc7fb752016-03-15 19:19:31 +090037import android.os.FileUtils;
Daichi Hironoc00d5d42015-05-28 11:17:41 -070038import android.os.ParcelFileDescriptor;
Daichi Hironoe80ea382016-11-15 13:07:01 +090039import android.os.ProxyFileDescriptorCallback;
Daichi Hironof52ef002016-01-11 18:07:01 +090040import android.os.storage.StorageManager;
Steve McKay5a10ff12017-08-01 15:02:50 -070041import android.provider.DocumentsContract;
Daichi Hironoc00d5d42015-05-28 11:17:41 -070042import android.provider.DocumentsContract.Document;
Daichi Hironob9ffa2a2016-11-01 18:41:43 +090043import android.provider.DocumentsContract.Path;
Daichi Hironoc00d5d42015-05-28 11:17:41 -070044import android.provider.DocumentsContract.Root;
45import android.provider.DocumentsProvider;
Steve McKay5a10ff12017-08-01 15:02:50 -070046import android.provider.MetadataReader;
Daichi Hirono3bb37e72016-02-29 15:30:56 +090047import android.provider.Settings;
Daichi Hironof4e7fa82016-03-28 16:07:45 +090048import android.system.ErrnoException;
Daichi Hironoe80ea382016-11-15 13:07:01 +090049import android.system.OsConstants;
Daichi Hironod5152422015-07-15 13:31:51 +090050import android.util.Log;
Daichi Hironoc00d5d42015-05-28 11:17:41 -070051
Daichi Hironoe1d57712015-11-17 10:55:45 +090052import com.android.internal.annotations.GuardedBy;
Daichi Hironod5152422015-07-15 13:31:51 +090053import com.android.internal.annotations.VisibleForTesting;
54
Steve McKay5a10ff12017-08-01 15:02:50 -070055import libcore.io.IoUtils;
56
Daichi Hironod5152422015-07-15 13:31:51 +090057import java.io.FileNotFoundException;
58import java.io.IOException;
Steve McKay5a10ff12017-08-01 15:02:50 -070059import java.io.InputStream;
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +090060import java.util.HashMap;
Daichi Hironob9ffa2a2016-11-01 18:41:43 +090061import java.util.LinkedList;
Daichi Hirono3bb37e72016-02-29 15:30:56 +090062import java.util.List;
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +090063import java.util.Map;
Daichi Hironoacb0e272016-03-14 21:49:14 +090064import java.util.concurrent.TimeoutException;
Daichi Hironoe80ea382016-11-15 13:07:01 +090065
Daichi Hironod5152422015-07-15 13:31:51 +090066/**
67 * DocumentsProvider for MTP devices.
68 */
Daichi Hironoc00d5d42015-05-28 11:17:41 -070069public class MtpDocumentsProvider extends DocumentsProvider {
Daichi Hirono2efe4ca2015-07-27 16:47:46 +090070 static final String AUTHORITY = "com.android.mtp.documents";
71 static final String TAG = "MtpDocumentsProvider";
Daichi Hirono6baa16e2015-08-12 13:51:59 +090072 static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
Daichi Hironoc00d5d42015-05-28 11:17:41 -070073 Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON,
74 Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID,
75 Root.COLUMN_AVAILABLE_BYTES,
76 };
Daichi Hirono6baa16e2015-08-12 13:51:59 +090077 static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
Daichi Hironoc00d5d42015-05-28 11:17:41 -070078 Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE,
79 Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED,
80 Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
81 };
82
Daichi Hironof83ccbd2016-02-04 16:58:55 +090083 static final boolean DEBUG = false;
Daichi Hirono19aa9322016-02-04 14:19:52 +090084
Daichi Hironoe0282dd2015-11-26 15:20:08 +090085 private final Object mDeviceListLock = new Object();
86
Daichi Hirono2efe4ca2015-07-27 16:47:46 +090087 private static MtpDocumentsProvider sSingleton;
88
89 private MtpManager mMtpManager;
Daichi Hironod5152422015-07-15 13:31:51 +090090 private ContentResolver mResolver;
Daichi Hironoe0282dd2015-11-26 15:20:08 +090091 @GuardedBy("mDeviceListLock")
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +090092 private Map<Integer, DeviceToolkit> mDeviceToolkits;
Daichi Hirono8b9024f2015-08-12 12:59:09 +090093 private RootScanner mRootScanner;
Daichi Hirono17c8d8b2015-10-12 11:28:46 -070094 private Resources mResources;
Daichi Hironodc473442015-11-13 15:42:28 +090095 private MtpDatabase mDatabase;
Daichi Hironofda74742016-02-01 13:00:31 +090096 private ServiceIntentSender mIntentSender;
Daichi Hironof4e7fa82016-03-28 16:07:45 +090097 private Context mContext;
Daichi Hironoe80ea382016-11-15 13:07:01 +090098 private StorageManager mStorageManager;
Daichi Hironod5152422015-07-15 13:31:51 +090099
Daichi Hirono2efe4ca2015-07-27 16:47:46 +0900100 /**
101 * Provides singleton instance to MtpDocumentsService.
102 */
103 static MtpDocumentsProvider getInstance() {
104 return sSingleton;
105 }
106
Daichi Hironoc00d5d42015-05-28 11:17:41 -0700107 @Override
108 public boolean onCreate() {
Daichi Hirono2efe4ca2015-07-27 16:47:46 +0900109 sSingleton = this;
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900110 mContext = getContext();
Daichi Hirono17c8d8b2015-10-12 11:28:46 -0700111 mResources = getContext().getResources();
Daichi Hirono2efe4ca2015-07-27 16:47:46 +0900112 mMtpManager = new MtpManager(getContext());
Daichi Hironod5152422015-07-15 13:31:51 +0900113 mResolver = getContext().getContentResolver();
Steve McKay5a10ff12017-08-01 15:02:50 -0700114 mDeviceToolkits = new HashMap<>();
Daichi Hirono47eb1922015-11-16 13:01:31 +0900115 mDatabase = new MtpDatabase(getContext(), MtpDatabaseConstants.FLAG_DATABASE_IN_FILE);
Daichi Hironof83ccbd2016-02-04 16:58:55 +0900116 mRootScanner = new RootScanner(mResolver, mMtpManager, mDatabase);
Daichi Hironofda74742016-02-01 13:00:31 +0900117 mIntentSender = new ServiceIntentSender(getContext());
Daichi Hironoe80ea382016-11-15 13:07:01 +0900118 mStorageManager = getContext().getSystemService(StorageManager.class);
Daichi Hirono3bb37e72016-02-29 15:30:56 +0900119
120 // Check boot count and cleans database if it's first time to launch MtpDocumentsProvider
121 // after booting.
Daichi Hirono5884e1f2016-03-16 14:36:27 +0900122 try {
123 final int bootCount = Settings.Global.getInt(mResolver, Settings.Global.BOOT_COUNT, -1);
124 final int lastBootCount = mDatabase.getLastBootCount();
125 if (bootCount != -1 && bootCount != lastBootCount) {
126 mDatabase.setLastBootCount(bootCount);
127 final List<UriPermission> permissions =
128 mResolver.getOutgoingPersistedUriPermissions();
129 final Uri[] uris = new Uri[permissions.size()];
130 for (int i = 0; i < permissions.size(); i++) {
131 uris[i] = permissions.get(i).getUri();
132 }
133 mDatabase.cleanDatabase(uris);
Daichi Hirono3bb37e72016-02-29 15:30:56 +0900134 }
Daichi Hirono5884e1f2016-03-16 14:36:27 +0900135 } catch (SQLiteDiskIOException error) {
136 // It can happen due to disk shortage.
137 Log.e(TAG, "Failed to clean database.", error);
138 return false;
Daichi Hirono3bb37e72016-02-29 15:30:56 +0900139 }
140
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900141 resume();
Daichi Hironoc00d5d42015-05-28 11:17:41 -0700142 return true;
143 }
144
Daichi Hironod5152422015-07-15 13:31:51 +0900145 @VisibleForTesting
Daichi Hironob36b1552016-01-25 14:26:14 +0900146 boolean onCreateForTesting(
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900147 Context context,
Daichi Hironodc473442015-11-13 15:42:28 +0900148 Resources resources,
149 MtpManager mtpManager,
150 ContentResolver resolver,
Daichi Hironob36b1552016-01-25 14:26:14 +0900151 MtpDatabase database,
Daichi Hironofda74742016-02-01 13:00:31 +0900152 StorageManager storageManager,
153 ServiceIntentSender intentSender) {
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900154 mContext = context;
Daichi Hirono17c8d8b2015-10-12 11:28:46 -0700155 mResources = resources;
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900156 mMtpManager = mtpManager;
157 mResolver = resolver;
Steve McKay5a10ff12017-08-01 15:02:50 -0700158 mDeviceToolkits = new HashMap<>();
Daichi Hironodc473442015-11-13 15:42:28 +0900159 mDatabase = database;
Daichi Hironof83ccbd2016-02-04 16:58:55 +0900160 mRootScanner = new RootScanner(mResolver, mMtpManager, mDatabase);
Daichi Hironofda74742016-02-01 13:00:31 +0900161 mIntentSender = intentSender;
Daichi Hironoe80ea382016-11-15 13:07:01 +0900162 mStorageManager = storageManager;
Daichi Hirono3bb37e72016-02-29 15:30:56 +0900163
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900164 resume();
Daichi Hironob36b1552016-01-25 14:26:14 +0900165 return true;
Daichi Hironod5152422015-07-15 13:31:51 +0900166 }
167
Daichi Hironoc00d5d42015-05-28 11:17:41 -0700168 @Override
169 public Cursor queryRoots(String[] projection) throws FileNotFoundException {
Daichi Hirono50d17aa2015-07-28 15:49:01 +0900170 if (projection == null) {
171 projection = MtpDocumentsProvider.DEFAULT_ROOT_PROJECTION;
172 }
Daichi Hironof83ccbd2016-02-04 16:58:55 +0900173 final Cursor cursor = mDatabase.queryRoots(mResources, projection);
Daichi Hirono50d17aa2015-07-28 15:49:01 +0900174 cursor.setNotificationUri(
175 mResolver, DocumentsContract.buildRootsUri(MtpDocumentsProvider.AUTHORITY));
176 return cursor;
Daichi Hironoc00d5d42015-05-28 11:17:41 -0700177 }
178
179 @Override
180 public Cursor queryDocument(String documentId, String[] projection)
181 throws FileNotFoundException {
Daichi Hironoe5323b72015-07-29 16:10:47 +0900182 if (projection == null) {
183 projection = MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION;
184 }
Daichi Hirono66fcb4b2017-03-23 15:24:13 +0900185 final Cursor cursor = mDatabase.queryDocument(documentId, projection);
186 final int cursorCount = cursor.getCount();
187 if (cursorCount == 0) {
188 cursor.close();
189 throw new FileNotFoundException();
190 } else if (cursorCount != 1) {
191 cursor.close();
192 Log.wtf(TAG, "Unexpected cursor size: " + cursorCount);
193 return null;
194 }
195
196 final Identifier identifier = mDatabase.createIdentifier(documentId);
197 if (identifier.mDocumentType != MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE) {
198 return cursor;
199 }
200 final String[] storageDocIds = mDatabase.getStorageDocumentIds(documentId);
201 if (storageDocIds.length != 1) {
202 return mDatabase.queryDocument(documentId, projection);
203 }
204
205 // If the documentId specifies a device having exact one storage, we repalce some device
206 // attributes with the storage attributes.
207 try {
208 final String storageName;
209 final int storageFlags;
210 try (final Cursor storageCursor = mDatabase.queryDocument(
211 storageDocIds[0],
212 MtpDatabase.strings(Document.COLUMN_DISPLAY_NAME, Document.COLUMN_FLAGS))) {
213 if (!storageCursor.moveToNext()) {
214 throw new FileNotFoundException();
215 }
216 storageName = storageCursor.getString(0);
217 storageFlags = storageCursor.getInt(1);
218 }
219
220 cursor.moveToNext();
221 final ContentValues values = new ContentValues();
222 DatabaseUtils.cursorRowToContentValues(cursor, values);
223 if (values.containsKey(Document.COLUMN_DISPLAY_NAME)) {
224 values.put(Document.COLUMN_DISPLAY_NAME, mResources.getString(
225 R.string.root_name,
226 values.getAsString(Document.COLUMN_DISPLAY_NAME),
227 storageName));
228 }
229 values.put(Document.COLUMN_FLAGS, storageFlags);
230 final MatrixCursor output = new MatrixCursor(projection, 1);
231 MtpDatabase.putValuesToCursor(values, output);
232 return output;
233 } finally {
234 cursor.close();
235 }
Daichi Hironoc00d5d42015-05-28 11:17:41 -0700236 }
237
238 @Override
Daichi Hirono124d0602015-08-11 17:08:35 +0900239 public Cursor queryChildDocuments(String parentDocumentId,
240 String[] projection, String sortOrder) throws FileNotFoundException {
Daichi Hirono19aa9322016-02-04 14:19:52 +0900241 if (DEBUG) {
242 Log.d(TAG, "queryChildDocuments: " + parentDocumentId);
243 }
Daichi Hirono124d0602015-08-11 17:08:35 +0900244 if (projection == null) {
245 projection = MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION;
246 }
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900247 Identifier parentIdentifier = mDatabase.createIdentifier(parentDocumentId);
Daichi Hirono124d0602015-08-11 17:08:35 +0900248 try {
Daichi Hironofda74742016-02-01 13:00:31 +0900249 openDevice(parentIdentifier.mDeviceId);
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900250 if (parentIdentifier.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE) {
Daichi Hirono29657762016-02-10 16:55:37 -0800251 final String[] storageDocIds = mDatabase.getStorageDocumentIds(parentDocumentId);
252 if (storageDocIds.length == 0) {
253 // Remote device does not provide storages. Maybe it is locked.
254 return createErrorCursor(projection, R.string.error_locked_device);
255 } else if (storageDocIds.length > 1) {
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900256 // Returns storage list from database.
257 return mDatabase.queryChildDocuments(projection, parentDocumentId);
258 }
Daichi Hirono29657762016-02-10 16:55:37 -0800259
260 // Exact one storage is found. Skip storage and returns object in the single
261 // storage.
262 parentIdentifier = mDatabase.createIdentifier(storageDocIds[0]);
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900263 }
Daichi Hirono29657762016-02-10 16:55:37 -0800264
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900265 // Returns object list from document loader.
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900266 return getDocumentLoader(parentIdentifier).queryChildDocuments(
267 projection, parentIdentifier);
Daichi Hironoc18f8072016-02-10 14:59:52 -0800268 } catch (BusyDeviceException exception) {
Daichi Hirono29657762016-02-10 16:55:37 -0800269 return createErrorCursor(projection, R.string.error_busy_device);
Daichi Hirono124d0602015-08-11 17:08:35 +0900270 } catch (IOException exception) {
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900271 Log.e(MtpDocumentsProvider.TAG, "queryChildDocuments", exception);
Daichi Hirono124d0602015-08-11 17:08:35 +0900272 throw new FileNotFoundException(exception.getMessage());
273 }
Daichi Hironoc00d5d42015-05-28 11:17:41 -0700274 }
275
276 @Override
Daichi Hirono8ba41912015-07-30 21:22:57 +0900277 public ParcelFileDescriptor openDocument(
278 String documentId, String mode, CancellationSignal signal)
279 throws FileNotFoundException {
Daichi Hirono6213cef2016-02-05 17:21:13 +0900280 if (DEBUG) {
281 Log.d(TAG, "openDocument: " + documentId);
282 }
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900283 final Identifier identifier = mDatabase.createIdentifier(documentId);
Daichi Hirono8ba41912015-07-30 21:22:57 +0900284 try {
Daichi Hironofda74742016-02-01 13:00:31 +0900285 openDevice(identifier.mDeviceId);
Daichi Hirono0f325372016-02-21 15:50:30 +0900286 final MtpDeviceRecord device = getDeviceToolkit(identifier.mDeviceId).mDeviceRecord;
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900287 // Turn off MODE_CREATE because openDocument does not allow to create new files.
288 final int modeFlag =
289 ParcelFileDescriptor.parseMode(mode) & ~ParcelFileDescriptor.MODE_CREATE;
290 if ((modeFlag & ParcelFileDescriptor.MODE_READ_ONLY) != 0) {
291 long fileSize;
292 try {
293 fileSize = getFileSize(documentId);
294 } catch (UnsupportedOperationException exception) {
295 fileSize = -1;
296 }
297 if (MtpDeviceRecord.isPartialReadSupported(
298 device.operationsSupported, fileSize)) {
Daichi Hironoe80ea382016-11-15 13:07:01 +0900299
300 return mStorageManager.openProxyFileDescriptor(
301 modeFlag,
302 new MtpProxyFileDescriptorCallback(Integer.parseInt(documentId)));
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900303 } else {
304 // If getPartialObject{|64} are not supported for the device, returns
305 // non-seekable pipe FD instead.
306 return getPipeManager(identifier).readDocument(mMtpManager, identifier);
307 }
308 } else if ((modeFlag & ParcelFileDescriptor.MODE_WRITE_ONLY) != 0) {
309 // TODO: Clear the parent document loader task (if exists) and call notify
310 // when writing is completed.
311 if (MtpDeviceRecord.isWritingSupported(device.operationsSupported)) {
Daichi Hironoe80ea382016-11-15 13:07:01 +0900312 return mStorageManager.openProxyFileDescriptor(
313 modeFlag,
314 new MtpProxyFileDescriptorCallback(Integer.parseInt(documentId)));
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900315 } else {
Tomasz Mikolajewskib80a3cf2015-08-24 16:10:51 +0900316 throw new UnsupportedOperationException(
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900317 "The device does not support writing operation.");
318 }
319 } else {
320 // TODO: Add support for "rw" mode.
321 throw new UnsupportedOperationException("The provider does not support 'rw' mode.");
Tomasz Mikolajewskib80a3cf2015-08-24 16:10:51 +0900322 }
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900323 } catch (FileNotFoundException | RuntimeException error) {
324 Log.e(MtpDocumentsProvider.TAG, "openDocument", error);
325 throw error;
Daichi Hirono8ba41912015-07-30 21:22:57 +0900326 } catch (IOException error) {
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900327 Log.e(MtpDocumentsProvider.TAG, "openDocument", error);
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900328 throw new IllegalStateException(error);
Daichi Hirono8ba41912015-07-30 21:22:57 +0900329 }
Daichi Hironod5152422015-07-15 13:31:51 +0900330 }
331
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900332 @Override
333 public AssetFileDescriptor openDocumentThumbnail(
334 String documentId,
335 Point sizeHint,
336 CancellationSignal signal) throws FileNotFoundException {
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900337 final Identifier identifier = mDatabase.createIdentifier(documentId);
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900338 try {
Daichi Hironofda74742016-02-01 13:00:31 +0900339 openDevice(identifier.mDeviceId);
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900340 return new AssetFileDescriptor(
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900341 getPipeManager(identifier).readThumbnail(mMtpManager, identifier),
Daichi Hirono573c1fb2015-08-11 19:31:30 +0900342 0, // Start offset.
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900343 AssetFileDescriptor.UNKNOWN_LENGTH);
344 } catch (IOException error) {
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900345 Log.e(MtpDocumentsProvider.TAG, "openDocumentThumbnail", error);
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900346 throw new FileNotFoundException(error.getMessage());
347 }
348 }
349
350 @Override
351 public void deleteDocument(String documentId) throws FileNotFoundException {
352 try {
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900353 final Identifier identifier = mDatabase.createIdentifier(documentId);
Daichi Hironofda74742016-02-01 13:00:31 +0900354 openDevice(identifier.mDeviceId);
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900355 final Identifier parentIdentifier = mDatabase.getParentIdentifier(documentId);
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900356 mMtpManager.deleteDocument(identifier.mDeviceId, identifier.mObjectHandle);
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900357 mDatabase.deleteDocument(documentId);
Daichi Hirono76be46f2016-04-08 09:48:02 +0900358 getDocumentLoader(parentIdentifier).cancelTask(parentIdentifier);
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900359 notifyChildDocumentsChange(parentIdentifier.mDocumentId);
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900360 if (parentIdentifier.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE) {
361 // If the parent is storage, the object might be appeared as child of device because
362 // we skip storage when the device has only one storage.
363 final Identifier deviceIdentifier = mDatabase.getParentIdentifier(
364 parentIdentifier.mDocumentId);
365 notifyChildDocumentsChange(deviceIdentifier.mDocumentId);
366 }
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900367 } catch (IOException error) {
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900368 Log.e(MtpDocumentsProvider.TAG, "deleteDocument", error);
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900369 throw new FileNotFoundException(error.getMessage());
370 }
371 }
372
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900373 @Override
374 public void onTrimMemory(int level) {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900375 synchronized (mDeviceListLock) {
Daichi Hironoe1d57712015-11-17 10:55:45 +0900376 for (final DeviceToolkit toolkit : mDeviceToolkits.values()) {
377 toolkit.mDocumentLoader.clearCompletedTasks();
378 }
379 }
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900380 }
381
Tomasz Mikolajewski87763e62015-08-10 10:10:22 +0900382 @Override
383 public String createDocument(String parentDocumentId, String mimeType, String displayName)
384 throws FileNotFoundException {
Daichi Hirono6213cef2016-02-05 17:21:13 +0900385 if (DEBUG) {
386 Log.d(TAG, "createDocument: " + displayName);
387 }
Daichi Hironofc7fb752016-03-15 19:19:31 +0900388 final Identifier parentId;
389 final MtpDeviceRecord record;
390 final ParcelFileDescriptor[] pipe;
Tomasz Mikolajewski87763e62015-08-10 10:10:22 +0900391 try {
Daichi Hironofc7fb752016-03-15 19:19:31 +0900392 parentId = mDatabase.createIdentifier(parentDocumentId);
Daichi Hironofda74742016-02-01 13:00:31 +0900393 openDevice(parentId.mDeviceId);
Daichi Hironofc7fb752016-03-15 19:19:31 +0900394 record = getDeviceToolkit(parentId.mDeviceId).mDeviceRecord;
Daichi Hirono0f325372016-02-21 15:50:30 +0900395 if (!MtpDeviceRecord.isWritingSupported(record.operationsSupported)) {
Daichi Hironofc7fb752016-03-15 19:19:31 +0900396 throw new UnsupportedOperationException(
397 "Writing operation is not supported by the device.");
Daichi Hirono0f325372016-02-21 15:50:30 +0900398 }
Daichi Hirono35b2ec52016-11-02 14:51:26 +0900399
400 final int parentObjectHandle;
401 final int storageId;
402 switch (parentId.mDocumentType) {
403 case MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE:
404 final String[] storageDocumentIds =
405 mDatabase.getStorageDocumentIds(parentId.mDocumentId);
406 if (storageDocumentIds.length == 1) {
407 final String newDocumentId =
408 createDocument(storageDocumentIds[0], mimeType, displayName);
409 notifyChildDocumentsChange(parentDocumentId);
410 return newDocumentId;
411 } else {
412 throw new UnsupportedOperationException(
413 "Cannot create a file under the device.");
414 }
415 case MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE:
416 storageId = parentId.mStorageId;
417 parentObjectHandle = -1;
418 break;
419 case MtpDatabaseConstants.DOCUMENT_TYPE_OBJECT:
420 storageId = parentId.mStorageId;
421 parentObjectHandle = parentId.mObjectHandle;
422 break;
423 default:
424 throw new IllegalArgumentException("Unexpected document type.");
425 }
426
Daichi Hironofc7fb752016-03-15 19:19:31 +0900427 pipe = ParcelFileDescriptor.createReliablePipe();
428 int objectHandle = -1;
429 MtpObjectInfo info = null;
430 try {
431 pipe[0].close(); // 0 bytes for a new document.
432
433 final int formatCode = Document.MIME_TYPE_DIR.equals(mimeType) ?
434 MtpConstants.FORMAT_ASSOCIATION :
435 MediaFile.getFormatCode(displayName, mimeType);
436 info = new MtpObjectInfo.Builder()
Daichi Hirono35b2ec52016-11-02 14:51:26 +0900437 .setStorageId(storageId)
438 .setParent(parentObjectHandle)
Daichi Hironofc7fb752016-03-15 19:19:31 +0900439 .setFormat(formatCode)
440 .setName(displayName)
441 .build();
442
443 final String[] parts = FileUtils.splitFileName(mimeType, displayName);
444 final String baseName = parts[0];
445 final String extension = parts[1];
446 for (int i = 0; i <= 32; i++) {
447 final MtpObjectInfo infoUniqueName;
448 if (i == 0) {
449 infoUniqueName = info;
450 } else {
Daichi Hirono4f04fd32016-03-18 18:29:56 +0900451 String suffixedName = baseName + " (" + i + " )";
452 if (!extension.isEmpty()) {
453 suffixedName += "." + extension;
454 }
455 infoUniqueName =
456 new MtpObjectInfo.Builder(info).setName(suffixedName).build();
Daichi Hironofc7fb752016-03-15 19:19:31 +0900457 }
458 try {
459 objectHandle = mMtpManager.createDocument(
460 parentId.mDeviceId, infoUniqueName, pipe[1]);
461 break;
462 } catch (SendObjectInfoFailure exp) {
463 // This can be caused when we have an existing file with the same name.
464 continue;
465 }
466 }
467 } finally {
468 pipe[1].close();
469 }
470 if (objectHandle == -1) {
471 throw new IllegalArgumentException(
472 "The file name \"" + displayName + "\" is conflicted with existing files " +
473 "and the provider failed to find unique name.");
474 }
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900475 final MtpObjectInfo infoWithHandle =
476 new MtpObjectInfo.Builder(info).setObjectHandle(objectHandle).build();
477 final String documentId = mDatabase.putNewDocument(
Daichi Hirono61ba9232016-02-26 12:58:39 +0900478 parentId.mDeviceId, parentDocumentId, record.operationsSupported,
Daichi Hirono64111e02016-03-24 21:07:38 +0900479 infoWithHandle, 0l);
Daichi Hirono76be46f2016-04-08 09:48:02 +0900480 getDocumentLoader(parentId).cancelTask(parentId);
Tomasz Mikolajewski87763e62015-08-10 10:10:22 +0900481 notifyChildDocumentsChange(parentDocumentId);
482 return documentId;
Daichi Hironofc7fb752016-03-15 19:19:31 +0900483 } catch (FileNotFoundException | RuntimeException error) {
484 Log.e(TAG, "createDocument", error);
485 throw error;
Tomasz Mikolajewski87763e62015-08-10 10:10:22 +0900486 } catch (IOException error) {
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900487 Log.e(TAG, "createDocument", error);
Daichi Hironofc7fb752016-03-15 19:19:31 +0900488 throw new IllegalStateException(error);
Tomasz Mikolajewski87763e62015-08-10 10:10:22 +0900489 }
490 }
491
Daichi Hironob9ffa2a2016-11-01 18:41:43 +0900492 @Override
Garfield Tanb690b4d2017-03-01 16:05:23 -0800493 public Path findDocumentPath(String parentDocumentId, String childDocumentId)
Daichi Hironob9ffa2a2016-11-01 18:41:43 +0900494 throws FileNotFoundException {
495 final LinkedList<String> ids = new LinkedList<>();
496 final Identifier childIdentifier = mDatabase.createIdentifier(childDocumentId);
497
498 Identifier i = childIdentifier;
499 outer: while (true) {
500 if (i.mDocumentId.equals(parentDocumentId)) {
501 ids.addFirst(i.mDocumentId);
502 break;
503 }
504 switch (i.mDocumentType) {
505 case MtpDatabaseConstants.DOCUMENT_TYPE_OBJECT:
506 ids.addFirst(i.mDocumentId);
507 i = mDatabase.getParentIdentifier(i.mDocumentId);
508 break;
509 case MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE: {
510 // Check if there is the multiple storage.
511 final Identifier deviceIdentifier =
512 mDatabase.getParentIdentifier(i.mDocumentId);
513 final String[] storageIds =
514 mDatabase.getStorageDocumentIds(deviceIdentifier.mDocumentId);
515 // Add storage's document ID to the path only when the device has multiple
516 // storages.
517 if (storageIds.length > 1) {
518 ids.addFirst(i.mDocumentId);
519 break outer;
520 }
521 i = deviceIdentifier;
522 break;
523 }
524 case MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE:
525 ids.addFirst(i.mDocumentId);
526 break outer;
527 }
528 }
529
530 if (parentDocumentId != null) {
531 return new Path(null, ids);
532 } else {
533 return new Path(/* Should be same with root ID */ i.mDocumentId, ids);
534 }
535 }
536
Daichi Hirono29de7692016-11-07 10:58:50 +0900537 @Override
538 public boolean isChildDocument(String parentDocumentId, String documentId) {
539 try {
540 Identifier identifier = mDatabase.createIdentifier(documentId);
541 while (true) {
542 if (parentDocumentId.equals(identifier.mDocumentId)) {
543 return true;
544 }
545 if (identifier.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE) {
546 return false;
547 }
548 identifier = mDatabase.getParentIdentifier(identifier.mDocumentId);
549 }
550 } catch (FileNotFoundException error) {
551 return false;
552 }
553 }
554
Steve McKay5a10ff12017-08-01 15:02:50 -0700555 @Override
556 public @Nullable Bundle getDocumentMetadata(String docId) throws FileNotFoundException {
557 String mimeType = getDocumentType(docId);
558
559 if (!MetadataReader.isSupportedMimeType(mimeType)) {
560 return null;
561 }
562
563 InputStream stream = null;
564 try {
565 stream = new ParcelFileDescriptor.AutoCloseInputStream(
566 openDocument(docId, "r", null));
567 Bundle metadata = new Bundle();
568 MetadataReader.getMetadata(metadata, stream, mimeType, null);
569 return metadata;
570 } catch (IOException e) {
571 Log.e(TAG, "An error occurred retrieving the metadata", e);
572 return null;
573 } finally {
574 IoUtils.closeQuietly(stream);
575 }
576 }
577
Daichi Hirono2efe4ca2015-07-27 16:47:46 +0900578 void openDevice(int deviceId) throws IOException {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900579 synchronized (mDeviceListLock) {
Daichi Hironofda74742016-02-01 13:00:31 +0900580 if (mDeviceToolkits.containsKey(deviceId)) {
581 return;
582 }
Daichi Hirono19aa9322016-02-04 14:19:52 +0900583 if (DEBUG) {
584 Log.d(TAG, "Open device " + deviceId);
585 }
Daichi Hirono0f325372016-02-21 15:50:30 +0900586 final MtpDeviceRecord device = mMtpManager.openDevice(deviceId);
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900587 final DeviceToolkit toolkit =
Daichi Hirono61ba9232016-02-26 12:58:39 +0900588 new DeviceToolkit(mMtpManager, mResolver, mDatabase, device);
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900589 mDeviceToolkits.put(deviceId, toolkit);
Daichi Hirono203be492017-04-03 12:20:42 +0900590 mIntentSender.sendUpdateNotificationIntent(getOpenedDeviceRecordsCache());
Daichi Hironofda74742016-02-01 13:00:31 +0900591 try {
592 mRootScanner.resume().await();
593 } catch (InterruptedException error) {
594 Log.e(TAG, "openDevice", error);
595 }
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900596 // Resume document loader to remap disconnected document ID. Must be invoked after the
597 // root scanner resumes.
598 toolkit.mDocumentLoader.resume();
Daichi Hironoe1d57712015-11-17 10:55:45 +0900599 }
Daichi Hironod5152422015-07-15 13:31:51 +0900600 }
601
Daichi Hironoe1d57712015-11-17 10:55:45 +0900602 void closeDevice(int deviceId) throws IOException, InterruptedException {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900603 synchronized (mDeviceListLock) {
604 closeDeviceInternal(deviceId);
Daichi Hirono203be492017-04-03 12:20:42 +0900605 mIntentSender.sendUpdateNotificationIntent(getOpenedDeviceRecordsCache());
Daichi Hironoe1d57712015-11-17 10:55:45 +0900606 }
Daichi Hirono20754c52015-12-15 18:52:26 +0900607 mRootScanner.resume();
Daichi Hironod5152422015-07-15 13:31:51 +0900608 }
609
Daichi Hirono0f325372016-02-21 15:50:30 +0900610 MtpDeviceRecord[] getOpenedDeviceRecordsCache() {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900611 synchronized (mDeviceListLock) {
Daichi Hirono0f325372016-02-21 15:50:30 +0900612 final MtpDeviceRecord[] records = new MtpDeviceRecord[mDeviceToolkits.size()];
613 int i = 0;
614 for (final DeviceToolkit toolkit : mDeviceToolkits.values()) {
615 records[i] = toolkit.mDeviceRecord;
616 i++;
Daichi Hirono20754c52015-12-15 18:52:26 +0900617 }
Daichi Hirono0f325372016-02-21 15:50:30 +0900618 return records;
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900619 }
Daichi Hirono2efe4ca2015-07-27 16:47:46 +0900620 }
Daichi Hirono50d17aa2015-07-28 15:49:01 +0900621
Daichi Hironoe1d57712015-11-17 10:55:45 +0900622 /**
Daichi Hirono1e374442016-02-11 10:08:21 -0800623 * Obtains document ID for the given device ID.
624 * @param deviceId
625 * @return document ID
626 * @throws FileNotFoundException device ID has not been build.
627 */
628 public String getDeviceDocumentId(int deviceId) throws FileNotFoundException {
629 return mDatabase.getDeviceDocumentId(deviceId);
630 }
631
632 /**
Daichi Hironofda74742016-02-01 13:00:31 +0900633 * Resumes root scanner to handle the update of device list.
634 */
635 void resumeRootScanner() {
Daichi Hironoebd24052016-02-06 21:05:57 +0900636 if (DEBUG) {
637 Log.d(MtpDocumentsProvider.TAG, "resumeRootScanner");
638 }
Daichi Hironofda74742016-02-01 13:00:31 +0900639 mRootScanner.resume();
640 }
641
642 /**
Daichi Hironoe1d57712015-11-17 10:55:45 +0900643 * Finalize the content provider for unit tests.
644 */
645 @Override
646 public void shutdown() {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900647 synchronized (mDeviceListLock) {
648 try {
Daichi Hirono0f325372016-02-21 15:50:30 +0900649 // Copy the opened key set because it will be modified when closing devices.
650 final Integer[] keySet =
651 mDeviceToolkits.keySet().toArray(new Integer[mDeviceToolkits.size()]);
652 for (final int id : keySet) {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900653 closeDeviceInternal(id);
654 }
Daichi Hirono2e9a57b2016-02-26 17:41:45 +0900655 mRootScanner.pause();
Daichi Hironoacb0e272016-03-14 21:49:14 +0900656 } catch (InterruptedException | IOException | TimeoutException e) {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900657 // It should fail unit tests by throwing runtime exception.
658 throw new RuntimeException(e);
659 } finally {
660 mDatabase.close();
661 super.shutdown();
662 }
Daichi Hironoe1d57712015-11-17 10:55:45 +0900663 }
664 }
665
Daichi Hirono5fecc6c2015-08-04 17:45:51 +0900666 private void notifyChildDocumentsChange(String parentDocumentId) {
667 mResolver.notifyChange(
668 DocumentsContract.buildChildDocumentsUri(AUTHORITY, parentDocumentId),
669 null,
670 false);
671 }
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900672
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900673 /**
Daichi Hironobe388482015-12-14 16:20:14 +0900674 * Clears MTP identifier in the database.
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900675 */
676 private void resume() {
677 synchronized (mDeviceListLock) {
678 mDatabase.getMapper().clearMapping();
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900679 }
680 }
681
682 private void closeDeviceInternal(int deviceId) throws IOException, InterruptedException {
683 // TODO: Flush the device before closing (if not closed externally).
Daichi Hironofda74742016-02-01 13:00:31 +0900684 if (!mDeviceToolkits.containsKey(deviceId)) {
685 return;
686 }
Daichi Hirono19aa9322016-02-04 14:19:52 +0900687 if (DEBUG) {
688 Log.d(TAG, "Close device " + deviceId);
689 }
Daichi Hirono24ab92a2016-03-04 17:53:03 +0900690 getDeviceToolkit(deviceId).close();
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900691 mDeviceToolkits.remove(deviceId);
692 mMtpManager.closeDevice(deviceId);
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900693 }
694
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900695 private DeviceToolkit getDeviceToolkit(int deviceId) throws FileNotFoundException {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900696 synchronized (mDeviceListLock) {
Daichi Hironoe1d57712015-11-17 10:55:45 +0900697 final DeviceToolkit toolkit = mDeviceToolkits.get(deviceId);
698 if (toolkit == null) {
699 throw new FileNotFoundException();
700 }
701 return toolkit;
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900702 }
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900703 }
704
705 private PipeManager getPipeManager(Identifier identifier) throws FileNotFoundException {
706 return getDeviceToolkit(identifier.mDeviceId).mPipeManager;
707 }
708
709 private DocumentLoader getDocumentLoader(Identifier identifier) throws FileNotFoundException {
710 return getDeviceToolkit(identifier.mDeviceId).mDocumentLoader;
711 }
712
Daichi Hironof52ef002016-01-11 18:07:01 +0900713 private long getFileSize(String documentId) throws FileNotFoundException {
714 final Cursor cursor = mDatabase.queryDocument(
715 documentId,
716 MtpDatabase.strings(Document.COLUMN_SIZE, Document.COLUMN_DISPLAY_NAME));
717 try {
718 if (cursor.moveToNext()) {
Daichi Hirono77a1c6562016-03-28 14:37:12 +0900719 if (cursor.isNull(0)) {
720 throw new UnsupportedOperationException();
721 }
Daichi Hironof52ef002016-01-11 18:07:01 +0900722 return cursor.getLong(0);
723 } else {
724 throw new FileNotFoundException();
725 }
726 } finally {
727 cursor.close();
728 }
729 }
730
Daichi Hirono29657762016-02-10 16:55:37 -0800731 /**
732 * Creates empty cursor with specific error message.
733 *
734 * @param projection Column names.
735 * @param stringResId String resource ID of error message.
736 * @return Empty cursor with error message.
737 */
738 private Cursor createErrorCursor(String[] projection, int stringResId) {
739 final Bundle bundle = new Bundle();
740 bundle.putString(DocumentsContract.EXTRA_ERROR, mResources.getString(stringResId));
741 final Cursor cursor = new MatrixCursor(projection);
742 cursor.setExtras(bundle);
743 return cursor;
744 }
745
Daichi Hirono24ab92a2016-03-04 17:53:03 +0900746 private static class DeviceToolkit implements AutoCloseable {
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900747 public final PipeManager mPipeManager;
748 public final DocumentLoader mDocumentLoader;
Daichi Hirono0f325372016-02-21 15:50:30 +0900749 public final MtpDeviceRecord mDeviceRecord;
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900750
Daichi Hirono61ba9232016-02-26 12:58:39 +0900751 public DeviceToolkit(MtpManager manager,
752 ContentResolver resolver,
753 MtpDatabase database,
754 MtpDeviceRecord record) {
Daichi Hironof578fa22016-02-19 18:05:42 +0900755 mPipeManager = new PipeManager(database);
Daichi Hirono61ba9232016-02-26 12:58:39 +0900756 mDocumentLoader = new DocumentLoader(record, manager, resolver, database);
Daichi Hirono0f325372016-02-21 15:50:30 +0900757 mDeviceRecord = record;
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900758 }
Daichi Hirono24ab92a2016-03-04 17:53:03 +0900759
760 @Override
761 public void close() throws InterruptedException {
762 mPipeManager.close();
763 mDocumentLoader.close();
764 }
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900765 }
Daichi Hironof52ef002016-01-11 18:07:01 +0900766
Daichi Hironoe80ea382016-11-15 13:07:01 +0900767 private class MtpProxyFileDescriptorCallback extends ProxyFileDescriptorCallback {
768 private final int mInode;
769 private MtpFileWriter mWriter;
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900770
Daichi Hironoe80ea382016-11-15 13:07:01 +0900771 MtpProxyFileDescriptorCallback(int inode) {
772 mInode = inode;
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900773 }
774
Daichi Hironof52ef002016-01-11 18:07:01 +0900775 @Override
Daichi Hironoe80ea382016-11-15 13:07:01 +0900776 public long onGetSize() throws ErrnoException {
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900777 try {
Daichi Hironoe80ea382016-11-15 13:07:01 +0900778 return getFileSize(String.valueOf(mInode));
779 } catch (FileNotFoundException e) {
780 Log.e(TAG, e.getMessage(), e);
781 throw new ErrnoException("onGetSize", OsConstants.ENOENT);
782 }
783 }
784
785 @Override
786 public int onRead(long offset, int size, byte[] data) throws ErrnoException {
787 try {
788 final Identifier identifier = mDatabase.createIdentifier(Integer.toString(mInode));
789 final MtpDeviceRecord record = getDeviceToolkit(identifier.mDeviceId).mDeviceRecord;
790 if (MtpDeviceRecord.isSupported(
791 record.operationsSupported, MtpConstants.OPERATION_GET_PARTIAL_OBJECT_64)) {
792
793 return (int) mMtpManager.getPartialObject64(
794 identifier.mDeviceId, identifier.mObjectHandle, offset, size, data);
795
796 }
797 if (0 <= offset && offset <= 0xffffffffL && MtpDeviceRecord.isSupported(
798 record.operationsSupported, MtpConstants.OPERATION_GET_PARTIAL_OBJECT)) {
799 return (int) mMtpManager.getPartialObject(
800 identifier.mDeviceId, identifier.mObjectHandle, offset, size, data);
801 }
802 throw new ErrnoException("onRead", OsConstants.ENOTSUP);
803 } catch (IOException e) {
804 Log.e(TAG, e.getMessage(), e);
805 throw new ErrnoException("onRead", OsConstants.EIO);
806 }
807 }
808
809 @Override
810 public int onWrite(long offset, int size, byte[] data) throws ErrnoException {
811 try {
812 if (mWriter == null) {
813 mWriter = new MtpFileWriter(mContext, String.valueOf(mInode));
814 }
815 return mWriter.write(offset, size, data);
816 } catch (IOException e) {
817 Log.e(TAG, e.getMessage(), e);
818 throw new ErrnoException("onWrite", OsConstants.EIO);
819 }
820 }
821
822 @Override
823 public void onFsync() throws ErrnoException {
824 tryFsync();
825 }
826
827 @Override
828 public void onRelease() {
829 try {
830 tryFsync();
831 } catch (ErrnoException error) {
832 // Cannot recover from the error at onRelease. Client app should use fsync to
833 // ensure the provider writes data correctly.
834 Log.e(TAG, "Cannot recover from the error at onRelease.", error);
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900835 } finally {
Daichi Hironoe80ea382016-11-15 13:07:01 +0900836 if (mWriter != null) {
837 IoUtils.closeQuietly(mWriter);
838 }
839 }
840 }
841
842 private void tryFsync() throws ErrnoException {
843 try {
844 if (mWriter != null) {
845 final MtpDeviceRecord device =
846 getDeviceToolkit(mDatabase.createIdentifier(
847 mWriter.getDocumentId()).mDeviceId).mDeviceRecord;
848 mWriter.flush(mMtpManager, mDatabase, device.operationsSupported);
849 }
850 } catch (IOException e) {
851 Log.e(TAG, e.getMessage(), e);
852 throw new ErrnoException("onWrite", OsConstants.EIO);
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900853 }
Daichi Hirono09ece6c2016-01-20 19:09:25 +0900854 }
Daichi Hironof52ef002016-01-11 18:07:01 +0900855 }
Daichi Hironoc00d5d42015-05-28 11:17:41 -0700856}