blob: eb2d8aa7195507d1107a76112c49709c793efba4 [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 Hirono66fcb4b2017-03-23 15:24:13 +090020import android.content.ContentValues;
Daichi Hironof4e7fa82016-03-28 16:07:45 +090021import android.content.Context;
Daichi Hirono3bb37e72016-02-29 15:30:56 +090022import android.content.UriPermission;
Daichi Hirono3faa43a2015-08-05 17:15:35 +090023import android.content.res.AssetFileDescriptor;
Daichi Hirono17c8d8b2015-10-12 11:28:46 -070024import android.content.res.Resources;
Daichi Hironoc00d5d42015-05-28 11:17:41 -070025import android.database.Cursor;
Daichi Hirono66fcb4b2017-03-23 15:24:13 +090026import android.database.DatabaseUtils;
Daichi Hironoc18f8072016-02-10 14:59:52 -080027import android.database.MatrixCursor;
Daichi Hirono5884e1f2016-03-16 14:36:27 +090028import android.database.sqlite.SQLiteDiskIOException;
Daichi Hirono3faa43a2015-08-05 17:15:35 +090029import android.graphics.Point;
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +090030import android.media.MediaFile;
31import android.mtp.MtpConstants;
Tomasz Mikolajewskibb430fa2015-08-25 18:34:30 +090032import android.mtp.MtpObjectInfo;
Daichi Hirono3bb37e72016-02-29 15:30:56 +090033import android.net.Uri;
Daichi Hironoc18f8072016-02-10 14:59:52 -080034import android.os.Bundle;
Daichi Hironoc00d5d42015-05-28 11:17:41 -070035import android.os.CancellationSignal;
Daichi Hironofc7fb752016-03-15 19:19:31 +090036import android.os.FileUtils;
Daichi Hironoc00d5d42015-05-28 11:17:41 -070037import android.os.ParcelFileDescriptor;
Daichi Hironoe80ea382016-11-15 13:07:01 +090038import android.os.ProxyFileDescriptorCallback;
Daichi Hironof52ef002016-01-11 18:07:01 +090039import android.os.storage.StorageManager;
Daichi Hironoc00d5d42015-05-28 11:17:41 -070040import android.provider.DocumentsContract.Document;
Daichi Hironob9ffa2a2016-11-01 18:41:43 +090041import android.provider.DocumentsContract.Path;
Daichi Hironoc00d5d42015-05-28 11:17:41 -070042import android.provider.DocumentsContract.Root;
Tomasz Mikolajewskibb430fa2015-08-25 18:34:30 +090043import android.provider.DocumentsContract;
Daichi Hironoc00d5d42015-05-28 11:17:41 -070044import android.provider.DocumentsProvider;
Daichi Hirono3bb37e72016-02-29 15:30:56 +090045import android.provider.Settings;
Daichi Hironof4e7fa82016-03-28 16:07:45 +090046import android.system.ErrnoException;
Daichi Hironoe80ea382016-11-15 13:07:01 +090047import android.system.OsConstants;
Daichi Hironod5152422015-07-15 13:31:51 +090048import android.util.Log;
Daichi Hironoc00d5d42015-05-28 11:17:41 -070049
Daichi Hironoe1d57712015-11-17 10:55:45 +090050import com.android.internal.annotations.GuardedBy;
Daichi Hironod5152422015-07-15 13:31:51 +090051import com.android.internal.annotations.VisibleForTesting;
52
53import java.io.FileNotFoundException;
54import java.io.IOException;
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +090055import java.util.HashMap;
Daichi Hironob9ffa2a2016-11-01 18:41:43 +090056import java.util.LinkedList;
Daichi Hirono3bb37e72016-02-29 15:30:56 +090057import java.util.List;
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +090058import java.util.Map;
Daichi Hironoacb0e272016-03-14 21:49:14 +090059import java.util.concurrent.TimeoutException;
Daichi Hironoe80ea382016-11-15 13:07:01 +090060import libcore.io.IoUtils;
61
Daichi Hironod5152422015-07-15 13:31:51 +090062/**
63 * DocumentsProvider for MTP devices.
64 */
Daichi Hironoc00d5d42015-05-28 11:17:41 -070065public class MtpDocumentsProvider extends DocumentsProvider {
Daichi Hirono2efe4ca2015-07-27 16:47:46 +090066 static final String AUTHORITY = "com.android.mtp.documents";
67 static final String TAG = "MtpDocumentsProvider";
Daichi Hirono6baa16e2015-08-12 13:51:59 +090068 static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
Daichi Hironoc00d5d42015-05-28 11:17:41 -070069 Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON,
70 Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID,
71 Root.COLUMN_AVAILABLE_BYTES,
72 };
Daichi Hirono6baa16e2015-08-12 13:51:59 +090073 static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
Daichi Hironoc00d5d42015-05-28 11:17:41 -070074 Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE,
75 Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED,
76 Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
77 };
78
Daichi Hironof83ccbd2016-02-04 16:58:55 +090079 static final boolean DEBUG = false;
Daichi Hirono19aa9322016-02-04 14:19:52 +090080
Daichi Hironoe0282dd2015-11-26 15:20:08 +090081 private final Object mDeviceListLock = new Object();
82
Daichi Hirono2efe4ca2015-07-27 16:47:46 +090083 private static MtpDocumentsProvider sSingleton;
84
85 private MtpManager mMtpManager;
Daichi Hironod5152422015-07-15 13:31:51 +090086 private ContentResolver mResolver;
Daichi Hironoe0282dd2015-11-26 15:20:08 +090087 @GuardedBy("mDeviceListLock")
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +090088 private Map<Integer, DeviceToolkit> mDeviceToolkits;
Daichi Hirono8b9024f2015-08-12 12:59:09 +090089 private RootScanner mRootScanner;
Daichi Hirono17c8d8b2015-10-12 11:28:46 -070090 private Resources mResources;
Daichi Hironodc473442015-11-13 15:42:28 +090091 private MtpDatabase mDatabase;
Daichi Hironofda74742016-02-01 13:00:31 +090092 private ServiceIntentSender mIntentSender;
Daichi Hironof4e7fa82016-03-28 16:07:45 +090093 private Context mContext;
Daichi Hironoe80ea382016-11-15 13:07:01 +090094 private StorageManager mStorageManager;
Daichi Hironod5152422015-07-15 13:31:51 +090095
Daichi Hirono2efe4ca2015-07-27 16:47:46 +090096 /**
97 * Provides singleton instance to MtpDocumentsService.
98 */
99 static MtpDocumentsProvider getInstance() {
100 return sSingleton;
101 }
102
Daichi Hironoc00d5d42015-05-28 11:17:41 -0700103 @Override
104 public boolean onCreate() {
Daichi Hirono2efe4ca2015-07-27 16:47:46 +0900105 sSingleton = this;
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900106 mContext = getContext();
Daichi Hirono17c8d8b2015-10-12 11:28:46 -0700107 mResources = getContext().getResources();
Daichi Hirono2efe4ca2015-07-27 16:47:46 +0900108 mMtpManager = new MtpManager(getContext());
Daichi Hironod5152422015-07-15 13:31:51 +0900109 mResolver = getContext().getContentResolver();
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900110 mDeviceToolkits = new HashMap<Integer, DeviceToolkit>();
Daichi Hirono47eb1922015-11-16 13:01:31 +0900111 mDatabase = new MtpDatabase(getContext(), MtpDatabaseConstants.FLAG_DATABASE_IN_FILE);
Daichi Hironof83ccbd2016-02-04 16:58:55 +0900112 mRootScanner = new RootScanner(mResolver, mMtpManager, mDatabase);
Daichi Hironofda74742016-02-01 13:00:31 +0900113 mIntentSender = new ServiceIntentSender(getContext());
Daichi Hironoe80ea382016-11-15 13:07:01 +0900114 mStorageManager = getContext().getSystemService(StorageManager.class);
Daichi Hirono3bb37e72016-02-29 15:30:56 +0900115
116 // Check boot count and cleans database if it's first time to launch MtpDocumentsProvider
117 // after booting.
Daichi Hirono5884e1f2016-03-16 14:36:27 +0900118 try {
119 final int bootCount = Settings.Global.getInt(mResolver, Settings.Global.BOOT_COUNT, -1);
120 final int lastBootCount = mDatabase.getLastBootCount();
121 if (bootCount != -1 && bootCount != lastBootCount) {
122 mDatabase.setLastBootCount(bootCount);
123 final List<UriPermission> permissions =
124 mResolver.getOutgoingPersistedUriPermissions();
125 final Uri[] uris = new Uri[permissions.size()];
126 for (int i = 0; i < permissions.size(); i++) {
127 uris[i] = permissions.get(i).getUri();
128 }
129 mDatabase.cleanDatabase(uris);
Daichi Hirono3bb37e72016-02-29 15:30:56 +0900130 }
Daichi Hirono5884e1f2016-03-16 14:36:27 +0900131 } catch (SQLiteDiskIOException error) {
132 // It can happen due to disk shortage.
133 Log.e(TAG, "Failed to clean database.", error);
134 return false;
Daichi Hirono3bb37e72016-02-29 15:30:56 +0900135 }
136
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900137 resume();
Daichi Hironoc00d5d42015-05-28 11:17:41 -0700138 return true;
139 }
140
Daichi Hironod5152422015-07-15 13:31:51 +0900141 @VisibleForTesting
Daichi Hironob36b1552016-01-25 14:26:14 +0900142 boolean onCreateForTesting(
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900143 Context context,
Daichi Hironodc473442015-11-13 15:42:28 +0900144 Resources resources,
145 MtpManager mtpManager,
146 ContentResolver resolver,
Daichi Hironob36b1552016-01-25 14:26:14 +0900147 MtpDatabase database,
Daichi Hironofda74742016-02-01 13:00:31 +0900148 StorageManager storageManager,
149 ServiceIntentSender intentSender) {
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900150 mContext = context;
Daichi Hirono17c8d8b2015-10-12 11:28:46 -0700151 mResources = resources;
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900152 mMtpManager = mtpManager;
153 mResolver = resolver;
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900154 mDeviceToolkits = new HashMap<Integer, DeviceToolkit>();
Daichi Hironodc473442015-11-13 15:42:28 +0900155 mDatabase = database;
Daichi Hironof83ccbd2016-02-04 16:58:55 +0900156 mRootScanner = new RootScanner(mResolver, mMtpManager, mDatabase);
Daichi Hironofda74742016-02-01 13:00:31 +0900157 mIntentSender = intentSender;
Daichi Hironoe80ea382016-11-15 13:07:01 +0900158 mStorageManager = storageManager;
Daichi Hirono3bb37e72016-02-29 15:30:56 +0900159
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900160 resume();
Daichi Hironob36b1552016-01-25 14:26:14 +0900161 return true;
Daichi Hironod5152422015-07-15 13:31:51 +0900162 }
163
Daichi Hironoc00d5d42015-05-28 11:17:41 -0700164 @Override
165 public Cursor queryRoots(String[] projection) throws FileNotFoundException {
Daichi Hirono50d17aa2015-07-28 15:49:01 +0900166 if (projection == null) {
167 projection = MtpDocumentsProvider.DEFAULT_ROOT_PROJECTION;
168 }
Daichi Hironof83ccbd2016-02-04 16:58:55 +0900169 final Cursor cursor = mDatabase.queryRoots(mResources, projection);
Daichi Hirono50d17aa2015-07-28 15:49:01 +0900170 cursor.setNotificationUri(
171 mResolver, DocumentsContract.buildRootsUri(MtpDocumentsProvider.AUTHORITY));
172 return cursor;
Daichi Hironoc00d5d42015-05-28 11:17:41 -0700173 }
174
175 @Override
176 public Cursor queryDocument(String documentId, String[] projection)
177 throws FileNotFoundException {
Daichi Hironoe5323b72015-07-29 16:10:47 +0900178 if (projection == null) {
179 projection = MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION;
180 }
Daichi Hirono66fcb4b2017-03-23 15:24:13 +0900181 final Cursor cursor = mDatabase.queryDocument(documentId, projection);
182 final int cursorCount = cursor.getCount();
183 if (cursorCount == 0) {
184 cursor.close();
185 throw new FileNotFoundException();
186 } else if (cursorCount != 1) {
187 cursor.close();
188 Log.wtf(TAG, "Unexpected cursor size: " + cursorCount);
189 return null;
190 }
191
192 final Identifier identifier = mDatabase.createIdentifier(documentId);
193 if (identifier.mDocumentType != MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE) {
194 return cursor;
195 }
196 final String[] storageDocIds = mDatabase.getStorageDocumentIds(documentId);
197 if (storageDocIds.length != 1) {
198 return mDatabase.queryDocument(documentId, projection);
199 }
200
201 // If the documentId specifies a device having exact one storage, we repalce some device
202 // attributes with the storage attributes.
203 try {
204 final String storageName;
205 final int storageFlags;
206 try (final Cursor storageCursor = mDatabase.queryDocument(
207 storageDocIds[0],
208 MtpDatabase.strings(Document.COLUMN_DISPLAY_NAME, Document.COLUMN_FLAGS))) {
209 if (!storageCursor.moveToNext()) {
210 throw new FileNotFoundException();
211 }
212 storageName = storageCursor.getString(0);
213 storageFlags = storageCursor.getInt(1);
214 }
215
216 cursor.moveToNext();
217 final ContentValues values = new ContentValues();
218 DatabaseUtils.cursorRowToContentValues(cursor, values);
219 if (values.containsKey(Document.COLUMN_DISPLAY_NAME)) {
220 values.put(Document.COLUMN_DISPLAY_NAME, mResources.getString(
221 R.string.root_name,
222 values.getAsString(Document.COLUMN_DISPLAY_NAME),
223 storageName));
224 }
225 values.put(Document.COLUMN_FLAGS, storageFlags);
226 final MatrixCursor output = new MatrixCursor(projection, 1);
227 MtpDatabase.putValuesToCursor(values, output);
228 return output;
229 } finally {
230 cursor.close();
231 }
Daichi Hironoc00d5d42015-05-28 11:17:41 -0700232 }
233
234 @Override
Daichi Hirono124d0602015-08-11 17:08:35 +0900235 public Cursor queryChildDocuments(String parentDocumentId,
236 String[] projection, String sortOrder) throws FileNotFoundException {
Daichi Hirono19aa9322016-02-04 14:19:52 +0900237 if (DEBUG) {
238 Log.d(TAG, "queryChildDocuments: " + parentDocumentId);
239 }
Daichi Hirono124d0602015-08-11 17:08:35 +0900240 if (projection == null) {
241 projection = MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION;
242 }
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900243 Identifier parentIdentifier = mDatabase.createIdentifier(parentDocumentId);
Daichi Hirono124d0602015-08-11 17:08:35 +0900244 try {
Daichi Hironofda74742016-02-01 13:00:31 +0900245 openDevice(parentIdentifier.mDeviceId);
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900246 if (parentIdentifier.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE) {
Daichi Hirono29657762016-02-10 16:55:37 -0800247 final String[] storageDocIds = mDatabase.getStorageDocumentIds(parentDocumentId);
248 if (storageDocIds.length == 0) {
249 // Remote device does not provide storages. Maybe it is locked.
250 return createErrorCursor(projection, R.string.error_locked_device);
251 } else if (storageDocIds.length > 1) {
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900252 // Returns storage list from database.
253 return mDatabase.queryChildDocuments(projection, parentDocumentId);
254 }
Daichi Hirono29657762016-02-10 16:55:37 -0800255
256 // Exact one storage is found. Skip storage and returns object in the single
257 // storage.
258 parentIdentifier = mDatabase.createIdentifier(storageDocIds[0]);
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900259 }
Daichi Hirono29657762016-02-10 16:55:37 -0800260
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900261 // Returns object list from document loader.
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900262 return getDocumentLoader(parentIdentifier).queryChildDocuments(
263 projection, parentIdentifier);
Daichi Hironoc18f8072016-02-10 14:59:52 -0800264 } catch (BusyDeviceException exception) {
Daichi Hirono29657762016-02-10 16:55:37 -0800265 return createErrorCursor(projection, R.string.error_busy_device);
Daichi Hirono124d0602015-08-11 17:08:35 +0900266 } catch (IOException exception) {
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900267 Log.e(MtpDocumentsProvider.TAG, "queryChildDocuments", exception);
Daichi Hirono124d0602015-08-11 17:08:35 +0900268 throw new FileNotFoundException(exception.getMessage());
269 }
Daichi Hironoc00d5d42015-05-28 11:17:41 -0700270 }
271
272 @Override
Daichi Hirono8ba41912015-07-30 21:22:57 +0900273 public ParcelFileDescriptor openDocument(
274 String documentId, String mode, CancellationSignal signal)
275 throws FileNotFoundException {
Daichi Hirono6213cef2016-02-05 17:21:13 +0900276 if (DEBUG) {
277 Log.d(TAG, "openDocument: " + documentId);
278 }
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900279 final Identifier identifier = mDatabase.createIdentifier(documentId);
Daichi Hirono8ba41912015-07-30 21:22:57 +0900280 try {
Daichi Hironofda74742016-02-01 13:00:31 +0900281 openDevice(identifier.mDeviceId);
Daichi Hirono0f325372016-02-21 15:50:30 +0900282 final MtpDeviceRecord device = getDeviceToolkit(identifier.mDeviceId).mDeviceRecord;
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900283 // Turn off MODE_CREATE because openDocument does not allow to create new files.
284 final int modeFlag =
285 ParcelFileDescriptor.parseMode(mode) & ~ParcelFileDescriptor.MODE_CREATE;
286 if ((modeFlag & ParcelFileDescriptor.MODE_READ_ONLY) != 0) {
287 long fileSize;
288 try {
289 fileSize = getFileSize(documentId);
290 } catch (UnsupportedOperationException exception) {
291 fileSize = -1;
292 }
293 if (MtpDeviceRecord.isPartialReadSupported(
294 device.operationsSupported, fileSize)) {
Daichi Hironoe80ea382016-11-15 13:07:01 +0900295
296 return mStorageManager.openProxyFileDescriptor(
297 modeFlag,
298 new MtpProxyFileDescriptorCallback(Integer.parseInt(documentId)));
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900299 } else {
300 // If getPartialObject{|64} are not supported for the device, returns
301 // non-seekable pipe FD instead.
302 return getPipeManager(identifier).readDocument(mMtpManager, identifier);
303 }
304 } else if ((modeFlag & ParcelFileDescriptor.MODE_WRITE_ONLY) != 0) {
305 // TODO: Clear the parent document loader task (if exists) and call notify
306 // when writing is completed.
307 if (MtpDeviceRecord.isWritingSupported(device.operationsSupported)) {
Daichi Hironoe80ea382016-11-15 13:07:01 +0900308 return mStorageManager.openProxyFileDescriptor(
309 modeFlag,
310 new MtpProxyFileDescriptorCallback(Integer.parseInt(documentId)));
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900311 } else {
Tomasz Mikolajewskib80a3cf2015-08-24 16:10:51 +0900312 throw new UnsupportedOperationException(
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900313 "The device does not support writing operation.");
314 }
315 } else {
316 // TODO: Add support for "rw" mode.
317 throw new UnsupportedOperationException("The provider does not support 'rw' mode.");
Tomasz Mikolajewskib80a3cf2015-08-24 16:10:51 +0900318 }
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900319 } catch (FileNotFoundException | RuntimeException error) {
320 Log.e(MtpDocumentsProvider.TAG, "openDocument", error);
321 throw error;
Daichi Hirono8ba41912015-07-30 21:22:57 +0900322 } catch (IOException error) {
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900323 Log.e(MtpDocumentsProvider.TAG, "openDocument", error);
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900324 throw new IllegalStateException(error);
Daichi Hirono8ba41912015-07-30 21:22:57 +0900325 }
Daichi Hironod5152422015-07-15 13:31:51 +0900326 }
327
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900328 @Override
329 public AssetFileDescriptor openDocumentThumbnail(
330 String documentId,
331 Point sizeHint,
332 CancellationSignal signal) throws FileNotFoundException {
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900333 final Identifier identifier = mDatabase.createIdentifier(documentId);
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900334 try {
Daichi Hironofda74742016-02-01 13:00:31 +0900335 openDevice(identifier.mDeviceId);
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900336 return new AssetFileDescriptor(
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900337 getPipeManager(identifier).readThumbnail(mMtpManager, identifier),
Daichi Hirono573c1fb2015-08-11 19:31:30 +0900338 0, // Start offset.
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900339 AssetFileDescriptor.UNKNOWN_LENGTH);
340 } catch (IOException error) {
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900341 Log.e(MtpDocumentsProvider.TAG, "openDocumentThumbnail", error);
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900342 throw new FileNotFoundException(error.getMessage());
343 }
344 }
345
346 @Override
347 public void deleteDocument(String documentId) throws FileNotFoundException {
348 try {
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900349 final Identifier identifier = mDatabase.createIdentifier(documentId);
Daichi Hironofda74742016-02-01 13:00:31 +0900350 openDevice(identifier.mDeviceId);
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900351 final Identifier parentIdentifier = mDatabase.getParentIdentifier(documentId);
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900352 mMtpManager.deleteDocument(identifier.mDeviceId, identifier.mObjectHandle);
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900353 mDatabase.deleteDocument(documentId);
Daichi Hirono76be46f2016-04-08 09:48:02 +0900354 getDocumentLoader(parentIdentifier).cancelTask(parentIdentifier);
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900355 notifyChildDocumentsChange(parentIdentifier.mDocumentId);
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900356 if (parentIdentifier.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE) {
357 // If the parent is storage, the object might be appeared as child of device because
358 // we skip storage when the device has only one storage.
359 final Identifier deviceIdentifier = mDatabase.getParentIdentifier(
360 parentIdentifier.mDocumentId);
361 notifyChildDocumentsChange(deviceIdentifier.mDocumentId);
362 }
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900363 } catch (IOException error) {
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900364 Log.e(MtpDocumentsProvider.TAG, "deleteDocument", error);
Daichi Hirono3faa43a2015-08-05 17:15:35 +0900365 throw new FileNotFoundException(error.getMessage());
366 }
367 }
368
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900369 @Override
370 public void onTrimMemory(int level) {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900371 synchronized (mDeviceListLock) {
Daichi Hironoe1d57712015-11-17 10:55:45 +0900372 for (final DeviceToolkit toolkit : mDeviceToolkits.values()) {
373 toolkit.mDocumentLoader.clearCompletedTasks();
374 }
375 }
Daichi Hirono6baa16e2015-08-12 13:51:59 +0900376 }
377
Tomasz Mikolajewski87763e62015-08-10 10:10:22 +0900378 @Override
379 public String createDocument(String parentDocumentId, String mimeType, String displayName)
380 throws FileNotFoundException {
Daichi Hirono6213cef2016-02-05 17:21:13 +0900381 if (DEBUG) {
382 Log.d(TAG, "createDocument: " + displayName);
383 }
Daichi Hironofc7fb752016-03-15 19:19:31 +0900384 final Identifier parentId;
385 final MtpDeviceRecord record;
386 final ParcelFileDescriptor[] pipe;
Tomasz Mikolajewski87763e62015-08-10 10:10:22 +0900387 try {
Daichi Hironofc7fb752016-03-15 19:19:31 +0900388 parentId = mDatabase.createIdentifier(parentDocumentId);
Daichi Hironofda74742016-02-01 13:00:31 +0900389 openDevice(parentId.mDeviceId);
Daichi Hironofc7fb752016-03-15 19:19:31 +0900390 record = getDeviceToolkit(parentId.mDeviceId).mDeviceRecord;
Daichi Hirono0f325372016-02-21 15:50:30 +0900391 if (!MtpDeviceRecord.isWritingSupported(record.operationsSupported)) {
Daichi Hironofc7fb752016-03-15 19:19:31 +0900392 throw new UnsupportedOperationException(
393 "Writing operation is not supported by the device.");
Daichi Hirono0f325372016-02-21 15:50:30 +0900394 }
Daichi Hirono35b2ec52016-11-02 14:51:26 +0900395
396 final int parentObjectHandle;
397 final int storageId;
398 switch (parentId.mDocumentType) {
399 case MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE:
400 final String[] storageDocumentIds =
401 mDatabase.getStorageDocumentIds(parentId.mDocumentId);
402 if (storageDocumentIds.length == 1) {
403 final String newDocumentId =
404 createDocument(storageDocumentIds[0], mimeType, displayName);
405 notifyChildDocumentsChange(parentDocumentId);
406 return newDocumentId;
407 } else {
408 throw new UnsupportedOperationException(
409 "Cannot create a file under the device.");
410 }
411 case MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE:
412 storageId = parentId.mStorageId;
413 parentObjectHandle = -1;
414 break;
415 case MtpDatabaseConstants.DOCUMENT_TYPE_OBJECT:
416 storageId = parentId.mStorageId;
417 parentObjectHandle = parentId.mObjectHandle;
418 break;
419 default:
420 throw new IllegalArgumentException("Unexpected document type.");
421 }
422
Daichi Hironofc7fb752016-03-15 19:19:31 +0900423 pipe = ParcelFileDescriptor.createReliablePipe();
424 int objectHandle = -1;
425 MtpObjectInfo info = null;
426 try {
427 pipe[0].close(); // 0 bytes for a new document.
428
429 final int formatCode = Document.MIME_TYPE_DIR.equals(mimeType) ?
430 MtpConstants.FORMAT_ASSOCIATION :
431 MediaFile.getFormatCode(displayName, mimeType);
432 info = new MtpObjectInfo.Builder()
Daichi Hirono35b2ec52016-11-02 14:51:26 +0900433 .setStorageId(storageId)
434 .setParent(parentObjectHandle)
Daichi Hironofc7fb752016-03-15 19:19:31 +0900435 .setFormat(formatCode)
436 .setName(displayName)
437 .build();
438
439 final String[] parts = FileUtils.splitFileName(mimeType, displayName);
440 final String baseName = parts[0];
441 final String extension = parts[1];
442 for (int i = 0; i <= 32; i++) {
443 final MtpObjectInfo infoUniqueName;
444 if (i == 0) {
445 infoUniqueName = info;
446 } else {
Daichi Hirono4f04fd32016-03-18 18:29:56 +0900447 String suffixedName = baseName + " (" + i + " )";
448 if (!extension.isEmpty()) {
449 suffixedName += "." + extension;
450 }
451 infoUniqueName =
452 new MtpObjectInfo.Builder(info).setName(suffixedName).build();
Daichi Hironofc7fb752016-03-15 19:19:31 +0900453 }
454 try {
455 objectHandle = mMtpManager.createDocument(
456 parentId.mDeviceId, infoUniqueName, pipe[1]);
457 break;
458 } catch (SendObjectInfoFailure exp) {
459 // This can be caused when we have an existing file with the same name.
460 continue;
461 }
462 }
463 } finally {
464 pipe[1].close();
465 }
466 if (objectHandle == -1) {
467 throw new IllegalArgumentException(
468 "The file name \"" + displayName + "\" is conflicted with existing files " +
469 "and the provider failed to find unique name.");
470 }
Daichi Hirono9e8a4fa2015-11-19 16:13:38 +0900471 final MtpObjectInfo infoWithHandle =
472 new MtpObjectInfo.Builder(info).setObjectHandle(objectHandle).build();
473 final String documentId = mDatabase.putNewDocument(
Daichi Hirono61ba9232016-02-26 12:58:39 +0900474 parentId.mDeviceId, parentDocumentId, record.operationsSupported,
Daichi Hirono64111e02016-03-24 21:07:38 +0900475 infoWithHandle, 0l);
Daichi Hirono76be46f2016-04-08 09:48:02 +0900476 getDocumentLoader(parentId).cancelTask(parentId);
Tomasz Mikolajewski87763e62015-08-10 10:10:22 +0900477 notifyChildDocumentsChange(parentDocumentId);
478 return documentId;
Daichi Hironofc7fb752016-03-15 19:19:31 +0900479 } catch (FileNotFoundException | RuntimeException error) {
480 Log.e(TAG, "createDocument", error);
481 throw error;
Tomasz Mikolajewski87763e62015-08-10 10:10:22 +0900482 } catch (IOException error) {
Daichi Hirono6a5ea7e2016-02-02 16:35:03 +0900483 Log.e(TAG, "createDocument", error);
Daichi Hironofc7fb752016-03-15 19:19:31 +0900484 throw new IllegalStateException(error);
Tomasz Mikolajewski87763e62015-08-10 10:10:22 +0900485 }
486 }
487
Daichi Hironob9ffa2a2016-11-01 18:41:43 +0900488 @Override
Garfield Tanb690b4d2017-03-01 16:05:23 -0800489 public Path findDocumentPath(String parentDocumentId, String childDocumentId)
Daichi Hironob9ffa2a2016-11-01 18:41:43 +0900490 throws FileNotFoundException {
491 final LinkedList<String> ids = new LinkedList<>();
492 final Identifier childIdentifier = mDatabase.createIdentifier(childDocumentId);
493
494 Identifier i = childIdentifier;
495 outer: while (true) {
496 if (i.mDocumentId.equals(parentDocumentId)) {
497 ids.addFirst(i.mDocumentId);
498 break;
499 }
500 switch (i.mDocumentType) {
501 case MtpDatabaseConstants.DOCUMENT_TYPE_OBJECT:
502 ids.addFirst(i.mDocumentId);
503 i = mDatabase.getParentIdentifier(i.mDocumentId);
504 break;
505 case MtpDatabaseConstants.DOCUMENT_TYPE_STORAGE: {
506 // Check if there is the multiple storage.
507 final Identifier deviceIdentifier =
508 mDatabase.getParentIdentifier(i.mDocumentId);
509 final String[] storageIds =
510 mDatabase.getStorageDocumentIds(deviceIdentifier.mDocumentId);
511 // Add storage's document ID to the path only when the device has multiple
512 // storages.
513 if (storageIds.length > 1) {
514 ids.addFirst(i.mDocumentId);
515 break outer;
516 }
517 i = deviceIdentifier;
518 break;
519 }
520 case MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE:
521 ids.addFirst(i.mDocumentId);
522 break outer;
523 }
524 }
525
526 if (parentDocumentId != null) {
527 return new Path(null, ids);
528 } else {
529 return new Path(/* Should be same with root ID */ i.mDocumentId, ids);
530 }
531 }
532
Daichi Hirono29de7692016-11-07 10:58:50 +0900533 @Override
534 public boolean isChildDocument(String parentDocumentId, String documentId) {
535 try {
536 Identifier identifier = mDatabase.createIdentifier(documentId);
537 while (true) {
538 if (parentDocumentId.equals(identifier.mDocumentId)) {
539 return true;
540 }
541 if (identifier.mDocumentType == MtpDatabaseConstants.DOCUMENT_TYPE_DEVICE) {
542 return false;
543 }
544 identifier = mDatabase.getParentIdentifier(identifier.mDocumentId);
545 }
546 } catch (FileNotFoundException error) {
547 return false;
548 }
549 }
550
Daichi Hirono2efe4ca2015-07-27 16:47:46 +0900551 void openDevice(int deviceId) throws IOException {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900552 synchronized (mDeviceListLock) {
Daichi Hironofda74742016-02-01 13:00:31 +0900553 if (mDeviceToolkits.containsKey(deviceId)) {
554 return;
555 }
Daichi Hirono19aa9322016-02-04 14:19:52 +0900556 if (DEBUG) {
557 Log.d(TAG, "Open device " + deviceId);
558 }
Daichi Hirono0f325372016-02-21 15:50:30 +0900559 final MtpDeviceRecord device = mMtpManager.openDevice(deviceId);
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900560 final DeviceToolkit toolkit =
Daichi Hirono61ba9232016-02-26 12:58:39 +0900561 new DeviceToolkit(mMtpManager, mResolver, mDatabase, device);
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900562 mDeviceToolkits.put(deviceId, toolkit);
Daichi Hirono203be492017-04-03 12:20:42 +0900563 mIntentSender.sendUpdateNotificationIntent(getOpenedDeviceRecordsCache());
Daichi Hironofda74742016-02-01 13:00:31 +0900564 try {
565 mRootScanner.resume().await();
566 } catch (InterruptedException error) {
567 Log.e(TAG, "openDevice", error);
568 }
Daichi Hirono4e94b8d2016-02-21 22:42:41 +0900569 // Resume document loader to remap disconnected document ID. Must be invoked after the
570 // root scanner resumes.
571 toolkit.mDocumentLoader.resume();
Daichi Hironoe1d57712015-11-17 10:55:45 +0900572 }
Daichi Hironod5152422015-07-15 13:31:51 +0900573 }
574
Daichi Hironoe1d57712015-11-17 10:55:45 +0900575 void closeDevice(int deviceId) throws IOException, InterruptedException {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900576 synchronized (mDeviceListLock) {
577 closeDeviceInternal(deviceId);
Daichi Hirono203be492017-04-03 12:20:42 +0900578 mIntentSender.sendUpdateNotificationIntent(getOpenedDeviceRecordsCache());
Daichi Hironoe1d57712015-11-17 10:55:45 +0900579 }
Daichi Hirono20754c52015-12-15 18:52:26 +0900580 mRootScanner.resume();
Daichi Hironod5152422015-07-15 13:31:51 +0900581 }
582
Daichi Hirono0f325372016-02-21 15:50:30 +0900583 MtpDeviceRecord[] getOpenedDeviceRecordsCache() {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900584 synchronized (mDeviceListLock) {
Daichi Hirono0f325372016-02-21 15:50:30 +0900585 final MtpDeviceRecord[] records = new MtpDeviceRecord[mDeviceToolkits.size()];
586 int i = 0;
587 for (final DeviceToolkit toolkit : mDeviceToolkits.values()) {
588 records[i] = toolkit.mDeviceRecord;
589 i++;
Daichi Hirono20754c52015-12-15 18:52:26 +0900590 }
Daichi Hirono0f325372016-02-21 15:50:30 +0900591 return records;
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900592 }
Daichi Hirono2efe4ca2015-07-27 16:47:46 +0900593 }
Daichi Hirono50d17aa2015-07-28 15:49:01 +0900594
Daichi Hironoe1d57712015-11-17 10:55:45 +0900595 /**
Daichi Hirono1e374442016-02-11 10:08:21 -0800596 * Obtains document ID for the given device ID.
597 * @param deviceId
598 * @return document ID
599 * @throws FileNotFoundException device ID has not been build.
600 */
601 public String getDeviceDocumentId(int deviceId) throws FileNotFoundException {
602 return mDatabase.getDeviceDocumentId(deviceId);
603 }
604
605 /**
Daichi Hironofda74742016-02-01 13:00:31 +0900606 * Resumes root scanner to handle the update of device list.
607 */
608 void resumeRootScanner() {
Daichi Hironoebd24052016-02-06 21:05:57 +0900609 if (DEBUG) {
610 Log.d(MtpDocumentsProvider.TAG, "resumeRootScanner");
611 }
Daichi Hironofda74742016-02-01 13:00:31 +0900612 mRootScanner.resume();
613 }
614
615 /**
Daichi Hironoe1d57712015-11-17 10:55:45 +0900616 * Finalize the content provider for unit tests.
617 */
618 @Override
619 public void shutdown() {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900620 synchronized (mDeviceListLock) {
621 try {
Daichi Hirono0f325372016-02-21 15:50:30 +0900622 // Copy the opened key set because it will be modified when closing devices.
623 final Integer[] keySet =
624 mDeviceToolkits.keySet().toArray(new Integer[mDeviceToolkits.size()]);
625 for (final int id : keySet) {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900626 closeDeviceInternal(id);
627 }
Daichi Hirono2e9a57b2016-02-26 17:41:45 +0900628 mRootScanner.pause();
Daichi Hironoacb0e272016-03-14 21:49:14 +0900629 } catch (InterruptedException | IOException | TimeoutException e) {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900630 // It should fail unit tests by throwing runtime exception.
631 throw new RuntimeException(e);
632 } finally {
633 mDatabase.close();
634 super.shutdown();
635 }
Daichi Hironoe1d57712015-11-17 10:55:45 +0900636 }
637 }
638
Daichi Hirono5fecc6c2015-08-04 17:45:51 +0900639 private void notifyChildDocumentsChange(String parentDocumentId) {
640 mResolver.notifyChange(
641 DocumentsContract.buildChildDocumentsUri(AUTHORITY, parentDocumentId),
642 null,
643 false);
644 }
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900645
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900646 /**
Daichi Hironobe388482015-12-14 16:20:14 +0900647 * Clears MTP identifier in the database.
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900648 */
649 private void resume() {
650 synchronized (mDeviceListLock) {
651 mDatabase.getMapper().clearMapping();
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900652 }
653 }
654
655 private void closeDeviceInternal(int deviceId) throws IOException, InterruptedException {
656 // TODO: Flush the device before closing (if not closed externally).
Daichi Hironofda74742016-02-01 13:00:31 +0900657 if (!mDeviceToolkits.containsKey(deviceId)) {
658 return;
659 }
Daichi Hirono19aa9322016-02-04 14:19:52 +0900660 if (DEBUG) {
661 Log.d(TAG, "Close device " + deviceId);
662 }
Daichi Hirono24ab92a2016-03-04 17:53:03 +0900663 getDeviceToolkit(deviceId).close();
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900664 mDeviceToolkits.remove(deviceId);
665 mMtpManager.closeDevice(deviceId);
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900666 }
667
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900668 private DeviceToolkit getDeviceToolkit(int deviceId) throws FileNotFoundException {
Daichi Hironoe0282dd2015-11-26 15:20:08 +0900669 synchronized (mDeviceListLock) {
Daichi Hironoe1d57712015-11-17 10:55:45 +0900670 final DeviceToolkit toolkit = mDeviceToolkits.get(deviceId);
671 if (toolkit == null) {
672 throw new FileNotFoundException();
673 }
674 return toolkit;
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900675 }
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900676 }
677
678 private PipeManager getPipeManager(Identifier identifier) throws FileNotFoundException {
679 return getDeviceToolkit(identifier.mDeviceId).mPipeManager;
680 }
681
682 private DocumentLoader getDocumentLoader(Identifier identifier) throws FileNotFoundException {
683 return getDeviceToolkit(identifier.mDeviceId).mDocumentLoader;
684 }
685
Daichi Hironof52ef002016-01-11 18:07:01 +0900686 private long getFileSize(String documentId) throws FileNotFoundException {
687 final Cursor cursor = mDatabase.queryDocument(
688 documentId,
689 MtpDatabase.strings(Document.COLUMN_SIZE, Document.COLUMN_DISPLAY_NAME));
690 try {
691 if (cursor.moveToNext()) {
Daichi Hirono77a1c6562016-03-28 14:37:12 +0900692 if (cursor.isNull(0)) {
693 throw new UnsupportedOperationException();
694 }
Daichi Hironof52ef002016-01-11 18:07:01 +0900695 return cursor.getLong(0);
696 } else {
697 throw new FileNotFoundException();
698 }
699 } finally {
700 cursor.close();
701 }
702 }
703
Daichi Hirono29657762016-02-10 16:55:37 -0800704 /**
705 * Creates empty cursor with specific error message.
706 *
707 * @param projection Column names.
708 * @param stringResId String resource ID of error message.
709 * @return Empty cursor with error message.
710 */
711 private Cursor createErrorCursor(String[] projection, int stringResId) {
712 final Bundle bundle = new Bundle();
713 bundle.putString(DocumentsContract.EXTRA_ERROR, mResources.getString(stringResId));
714 final Cursor cursor = new MatrixCursor(projection);
715 cursor.setExtras(bundle);
716 return cursor;
717 }
718
Daichi Hirono24ab92a2016-03-04 17:53:03 +0900719 private static class DeviceToolkit implements AutoCloseable {
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900720 public final PipeManager mPipeManager;
721 public final DocumentLoader mDocumentLoader;
Daichi Hirono0f325372016-02-21 15:50:30 +0900722 public final MtpDeviceRecord mDeviceRecord;
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900723
Daichi Hirono61ba9232016-02-26 12:58:39 +0900724 public DeviceToolkit(MtpManager manager,
725 ContentResolver resolver,
726 MtpDatabase database,
727 MtpDeviceRecord record) {
Daichi Hironof578fa22016-02-19 18:05:42 +0900728 mPipeManager = new PipeManager(database);
Daichi Hirono61ba9232016-02-26 12:58:39 +0900729 mDocumentLoader = new DocumentLoader(record, manager, resolver, database);
Daichi Hirono0f325372016-02-21 15:50:30 +0900730 mDeviceRecord = record;
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900731 }
Daichi Hirono24ab92a2016-03-04 17:53:03 +0900732
733 @Override
734 public void close() throws InterruptedException {
735 mPipeManager.close();
736 mDocumentLoader.close();
737 }
Tomasz Mikolajewski4c1d3dd2015-09-02 13:27:46 +0900738 }
Daichi Hironof52ef002016-01-11 18:07:01 +0900739
Daichi Hironoe80ea382016-11-15 13:07:01 +0900740 private class MtpProxyFileDescriptorCallback extends ProxyFileDescriptorCallback {
741 private final int mInode;
742 private MtpFileWriter mWriter;
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900743
Daichi Hironoe80ea382016-11-15 13:07:01 +0900744 MtpProxyFileDescriptorCallback(int inode) {
745 mInode = inode;
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900746 }
747
Daichi Hironof52ef002016-01-11 18:07:01 +0900748 @Override
Daichi Hironoe80ea382016-11-15 13:07:01 +0900749 public long onGetSize() throws ErrnoException {
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900750 try {
Daichi Hironoe80ea382016-11-15 13:07:01 +0900751 return getFileSize(String.valueOf(mInode));
752 } catch (FileNotFoundException e) {
753 Log.e(TAG, e.getMessage(), e);
754 throw new ErrnoException("onGetSize", OsConstants.ENOENT);
755 }
756 }
757
758 @Override
759 public int onRead(long offset, int size, byte[] data) throws ErrnoException {
760 try {
761 final Identifier identifier = mDatabase.createIdentifier(Integer.toString(mInode));
762 final MtpDeviceRecord record = getDeviceToolkit(identifier.mDeviceId).mDeviceRecord;
763 if (MtpDeviceRecord.isSupported(
764 record.operationsSupported, MtpConstants.OPERATION_GET_PARTIAL_OBJECT_64)) {
765
766 return (int) mMtpManager.getPartialObject64(
767 identifier.mDeviceId, identifier.mObjectHandle, offset, size, data);
768
769 }
770 if (0 <= offset && offset <= 0xffffffffL && MtpDeviceRecord.isSupported(
771 record.operationsSupported, MtpConstants.OPERATION_GET_PARTIAL_OBJECT)) {
772 return (int) mMtpManager.getPartialObject(
773 identifier.mDeviceId, identifier.mObjectHandle, offset, size, data);
774 }
775 throw new ErrnoException("onRead", OsConstants.ENOTSUP);
776 } catch (IOException e) {
777 Log.e(TAG, e.getMessage(), e);
778 throw new ErrnoException("onRead", OsConstants.EIO);
779 }
780 }
781
782 @Override
783 public int onWrite(long offset, int size, byte[] data) throws ErrnoException {
784 try {
785 if (mWriter == null) {
786 mWriter = new MtpFileWriter(mContext, String.valueOf(mInode));
787 }
788 return mWriter.write(offset, size, data);
789 } catch (IOException e) {
790 Log.e(TAG, e.getMessage(), e);
791 throw new ErrnoException("onWrite", OsConstants.EIO);
792 }
793 }
794
795 @Override
796 public void onFsync() throws ErrnoException {
797 tryFsync();
798 }
799
800 @Override
801 public void onRelease() {
802 try {
803 tryFsync();
804 } catch (ErrnoException error) {
805 // Cannot recover from the error at onRelease. Client app should use fsync to
806 // ensure the provider writes data correctly.
807 Log.e(TAG, "Cannot recover from the error at onRelease.", error);
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900808 } finally {
Daichi Hironoe80ea382016-11-15 13:07:01 +0900809 if (mWriter != null) {
810 IoUtils.closeQuietly(mWriter);
811 }
812 }
813 }
814
815 private void tryFsync() throws ErrnoException {
816 try {
817 if (mWriter != null) {
818 final MtpDeviceRecord device =
819 getDeviceToolkit(mDatabase.createIdentifier(
820 mWriter.getDocumentId()).mDeviceId).mDeviceRecord;
821 mWriter.flush(mMtpManager, mDatabase, device.operationsSupported);
822 }
823 } catch (IOException e) {
824 Log.e(TAG, e.getMessage(), e);
825 throw new ErrnoException("onWrite", OsConstants.EIO);
Daichi Hironof4e7fa82016-03-28 16:07:45 +0900826 }
Daichi Hirono09ece6c2016-01-20 19:09:25 +0900827 }
Daichi Hironof52ef002016-01-11 18:07:01 +0900828 }
Daichi Hironoc00d5d42015-05-28 11:17:41 -0700829}