From fc7fb7533f46b53247d1e6e6edca6e6c9ac676fe Mon Sep 17 00:00:00 2001 From: Daichi Hirono Date: Tue, 15 Mar 2016 19:19:31 +0900 Subject: [PATCH] Add suffix number when copying a file. If we have an existing file in the destination directory, which has the same name with the source file, adding suffix number is DocumentsProvider's responsibility. Because MTP does not provide a way to check existance of files with given name, the logic is implemented as try-and error strategy. The CL lets If we MtpDocumentsProvider assume we have a file that shares the same name with the source file if it failed to invoke MtpDevice#sendObjectInfo. In this case MtpDocumentsProvider retry to invoke sendObjectInfo with new name with suffix number. BUG=26991190 Change-Id: I223ac5031f079bc91eb27709b0356f621a1ed55b --- core/java/android/os/FileUtils.java | 37 ++++++--- .../{exceptions => }/BusyDeviceException.java | 7 +- .../com/android/mtp/MtpDocumentsProvider.java | 75 ++++++++++++++----- .../src/com/android/mtp/MtpManager.java | 3 +- .../android/mtp/SendObjectInfoFailure.java | 28 +++++++ .../android/mtp/MtpDocumentsProviderTest.java | 2 - 6 files changed, 118 insertions(+), 34 deletions(-) rename packages/MtpDocumentsProvider/src/com/android/mtp/{exceptions => }/BusyDeviceException.java (83%) create mode 100644 packages/MtpDocumentsProvider/src/com/android/mtp/SendObjectInfoFailure.java diff --git a/core/java/android/os/FileUtils.java b/core/java/android/os/FileUtils.java index 1b79497b1870a..dd73e53f65e6b 100644 --- a/core/java/android/os/FileUtils.java +++ b/core/java/android/os/FileUtils.java @@ -605,6 +605,30 @@ public class FileUtils { */ public static File buildUniqueFile(File parent, String mimeType, String displayName) throws FileNotFoundException { + final String[] parts = splitFileName(mimeType, displayName); + final String name = parts[0]; + final String ext = parts[1]; + File file = buildFile(parent, name, ext); + + // If conflicting file, try adding counter suffix + int n = 0; + while (file.exists()) { + if (n++ >= 32) { + throw new FileNotFoundException("Failed to create unique file"); + } + file = buildFile(parent, name + " (" + n + ")", ext); + } + + return file; + } + + /** + * Splits file name into base name and extension. + * If the display name doesn't have an extension that matches the requested MIME type, the + * extension is regarded as a part of filename and default extension for that MIME type is + * appended. + */ + public static String[] splitFileName(String mimeType, String displayName) { String name; String ext; @@ -642,18 +666,11 @@ public class FileUtils { } } - File file = buildFile(parent, name, ext); - - // If conflicting file, try adding counter suffix - int n = 0; - while (file.exists()) { - if (n++ >= 32) { - throw new FileNotFoundException("Failed to create unique file"); - } - file = buildFile(parent, name + " (" + n + ")", ext); + if (ext == null) { + ext = ""; } - return file; + return new String[] { name, ext }; } private static File buildFile(File parent, String name, String ext) { diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/exceptions/BusyDeviceException.java b/packages/MtpDocumentsProvider/src/com/android/mtp/BusyDeviceException.java similarity index 83% rename from packages/MtpDocumentsProvider/src/com/android/mtp/exceptions/BusyDeviceException.java rename to packages/MtpDocumentsProvider/src/com/android/mtp/BusyDeviceException.java index 55f55b0edbc3f..83488cd5b4903 100644 --- a/packages/MtpDocumentsProvider/src/com/android/mtp/exceptions/BusyDeviceException.java +++ b/packages/MtpDocumentsProvider/src/com/android/mtp/BusyDeviceException.java @@ -14,12 +14,15 @@ * limitations under the License. */ -package com.android.mtp.exceptions; +package com.android.mtp; import java.io.IOException; /** * Exception thrown when the device is busy and the requested operation cannon be completed. */ -public class BusyDeviceException extends IOException { +class BusyDeviceException extends IOException { + BusyDeviceException() { + super("The MTP device is busy."); + } } diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java index d4d45912aecc3..bc3c2ae9944be 100644 --- a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java +++ b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java @@ -29,6 +29,8 @@ import android.mtp.MtpObjectInfo; import android.net.Uri; import android.os.Bundle; import android.os.CancellationSignal; +import android.os.FileUriExposedException; +import android.os.FileUtils; import android.os.ParcelFileDescriptor; import android.os.storage.StorageManager; import android.provider.DocumentsContract.Document; @@ -36,12 +38,10 @@ import android.provider.DocumentsContract.Root; import android.provider.DocumentsContract; import android.provider.DocumentsProvider; import android.provider.Settings; -import android.provider.Settings.SettingNotFoundException; import android.util.Log; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; -import com.android.mtp.exceptions.BusyDeviceException; import java.io.FileNotFoundException; import java.io.IOException; @@ -315,25 +315,61 @@ public class MtpDocumentsProvider extends DocumentsProvider { if (DEBUG) { Log.d(TAG, "createDocument: " + displayName); } + final Identifier parentId; + final MtpDeviceRecord record; + final ParcelFileDescriptor[] pipe; try { - final Identifier parentId = mDatabase.createIdentifier(parentDocumentId); + parentId = mDatabase.createIdentifier(parentDocumentId); openDevice(parentId.mDeviceId); - final MtpDeviceRecord record = getDeviceToolkit(parentId.mDeviceId).mDeviceRecord; + record = getDeviceToolkit(parentId.mDeviceId).mDeviceRecord; if (!MtpDeviceRecord.isWritingSupported(record.operationsSupported)) { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException( + "Writing operation is not supported by the device."); + } + pipe = ParcelFileDescriptor.createReliablePipe(); + int objectHandle = -1; + MtpObjectInfo info = null; + try { + pipe[0].close(); // 0 bytes for a new document. + + final int formatCode = Document.MIME_TYPE_DIR.equals(mimeType) ? + MtpConstants.FORMAT_ASSOCIATION : + MediaFile.getFormatCode(displayName, mimeType); + info = new MtpObjectInfo.Builder() + .setStorageId(parentId.mStorageId) + .setParent(parentId.mObjectHandle) + .setFormat(formatCode) + .setName(displayName) + .build(); + + final String[] parts = FileUtils.splitFileName(mimeType, displayName); + final String baseName = parts[0]; + final String extension = parts[1]; + for (int i = 0; i <= 32; i++) { + final MtpObjectInfo infoUniqueName; + if (i == 0) { + infoUniqueName = info; + } else { + infoUniqueName = new MtpObjectInfo.Builder(info).setName( + baseName + " (" + i + ")." + extension).build(); + } + try { + objectHandle = mMtpManager.createDocument( + parentId.mDeviceId, infoUniqueName, pipe[1]); + break; + } catch (SendObjectInfoFailure exp) { + // This can be caused when we have an existing file with the same name. + continue; + } + } + } finally { + pipe[1].close(); + } + if (objectHandle == -1) { + throw new IllegalArgumentException( + "The file name \"" + displayName + "\" is conflicted with existing files " + + "and the provider failed to find unique name."); } - final ParcelFileDescriptor pipe[] = ParcelFileDescriptor.createReliablePipe(); - pipe[0].close(); // 0 bytes for a new document. - final int formatCode = Document.MIME_TYPE_DIR.equals(mimeType) ? - MtpConstants.FORMAT_ASSOCIATION : - MediaFile.getFormatCode(displayName, mimeType); - final MtpObjectInfo info = new MtpObjectInfo.Builder() - .setStorageId(parentId.mStorageId) - .setParent(parentId.mObjectHandle) - .setFormat(formatCode) - .setName(displayName) - .build(); - final int objectHandle = mMtpManager.createDocument(parentId.mDeviceId, info, pipe[1]); final MtpObjectInfo infoWithHandle = new MtpObjectInfo.Builder(info).setObjectHandle(objectHandle).build(); final String documentId = mDatabase.putNewDocument( @@ -342,9 +378,12 @@ public class MtpDocumentsProvider extends DocumentsProvider { getDocumentLoader(parentId).clearTask(parentId); notifyChildDocumentsChange(parentDocumentId); return documentId; + } catch (FileNotFoundException | RuntimeException error) { + Log.e(TAG, "createDocument", error); + throw error; } catch (IOException error) { Log.e(TAG, "createDocument", error); - throw new FileNotFoundException(error.getMessage()); + throw new IllegalStateException(error); } } diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpManager.java b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpManager.java index 020234394f879..6fb2a786be805 100644 --- a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpManager.java +++ b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpManager.java @@ -33,7 +33,6 @@ import android.util.Log; import android.util.SparseArray; import com.android.internal.annotations.VisibleForTesting; -import com.android.mtp.exceptions.BusyDeviceException; import java.io.FileNotFoundException; import java.io.IOException; @@ -190,7 +189,7 @@ class MtpManager { synchronized (device) { final MtpObjectInfo sendObjectInfoResult = device.sendObjectInfo(objectInfo); if (sendObjectInfoResult == null) { - throw new IOException("Failed to create a document"); + throw new SendObjectInfoFailure(); } if (objectInfo.getFormat() != MtpConstants.FORMAT_ASSOCIATION) { if (!device.sendObject(sendObjectInfoResult.getObjectHandle(), diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/SendObjectInfoFailure.java b/packages/MtpDocumentsProvider/src/com/android/mtp/SendObjectInfoFailure.java new file mode 100644 index 0000000000000..db7d777e280ef --- /dev/null +++ b/packages/MtpDocumentsProvider/src/com/android/mtp/SendObjectInfoFailure.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.mtp; + +import java.io.IOException; + +/** + * Exception thrown when sendObjectInfo failed. + */ +class SendObjectInfoFailure extends IOException { + SendObjectInfoFailure() { + super("Failed to MtpDevice#sendObjectInfo."); + } +} diff --git a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java index d6ad0f3c630ce..ba1c32a857661 100644 --- a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java +++ b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java @@ -30,8 +30,6 @@ import android.provider.DocumentsContract; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.MediumTest; -import com.android.mtp.exceptions.BusyDeviceException; - import java.io.FileNotFoundException; import java.io.IOException; import java.util.Arrays;