1 /* 2 * Copyright (C) 2016 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 17 package com.android.documentsui.services; 18 19 import static com.android.documentsui.base.Shared.DEBUG; 20 import static com.android.documentsui.services.FileOperationService.OPERATION_MOVE; 21 22 import android.app.Notification; 23 import android.app.Notification.Builder; 24 import android.content.ContentResolver; 25 import android.content.Context; 26 import android.net.Uri; 27 import android.os.Messenger; 28 import android.os.RemoteException; 29 import android.provider.DocumentsContract; 30 import android.provider.DocumentsContract.Document; 31 import android.util.Log; 32 33 import com.android.documentsui.Metrics; 34 import com.android.documentsui.R; 35 import com.android.documentsui.base.DocumentInfo; 36 import com.android.documentsui.base.DocumentStack; 37 import com.android.documentsui.base.Features; 38 import com.android.documentsui.clipping.UrisSupplier; 39 40 import java.io.FileNotFoundException; 41 42 import javax.annotation.Nullable; 43 44 // TODO: Stop extending CopyJob. 45 final class MoveJob extends CopyJob { 46 47 private static final String TAG = "MoveJob"; 48 49 private final @Nullable Uri mSrcParentUri; 50 51 // mSrcParent may be populated during setup. 52 private @Nullable DocumentInfo mSrcParent; 53 54 /** 55 * Moves files to a destination identified by {@code destination}. 56 * Performs most work by delegating to CopyJob, then deleting 57 * a file after it has been copied. 58 * 59 * @see @link {@link Job} constructor for most param descriptions. 60 */ MoveJob(Context service, Listener listener, String id, DocumentStack destination, UrisSupplier srcs, @Nullable Uri srcParent, Messenger messenger, Features features)61 MoveJob(Context service, Listener listener, String id, DocumentStack destination, 62 UrisSupplier srcs, @Nullable Uri srcParent, Messenger messenger, Features features) { 63 super(service, listener, id, OPERATION_MOVE, destination, srcs, messenger, features); 64 mSrcParentUri = srcParent; 65 } 66 67 @Override createProgressBuilder()68 Builder createProgressBuilder() { 69 return super.createProgressBuilder( 70 service.getString(R.string.move_notification_title), 71 R.drawable.ic_menu_copy, 72 service.getString(android.R.string.cancel), 73 R.drawable.ic_cab_cancel); 74 } 75 76 @Override getSetupNotification()77 public Notification getSetupNotification() { 78 return getSetupNotification(service.getString(R.string.move_preparing)); 79 } 80 81 @Override getProgressNotification()82 public Notification getProgressNotification() { 83 return getProgressNotification(R.string.copy_remaining); 84 } 85 86 @Override getFailureNotification()87 Notification getFailureNotification() { 88 return getFailureNotification( 89 R.plurals.move_error_notification_title, R.drawable.ic_menu_copy); 90 } 91 92 @Override setUp()93 public boolean setUp() { 94 if (mSrcParentUri != null) { 95 final ContentResolver resolver = appContext.getContentResolver(); 96 try { 97 mSrcParent = DocumentInfo.fromUri(resolver, mSrcParentUri); 98 } catch (FileNotFoundException e) { 99 Log.e(TAG, "Failed to create srcParent.", e); 100 failureCount = mResourceUris.getItemCount(); 101 return false; 102 } 103 } 104 105 return super.setUp(); 106 } 107 108 /** 109 * {@inheritDoc} 110 * 111 * Only check space for moves across authorities. For now we don't know if the doc in 112 * {@link #mSrcs} is in the same root of destination, and if it's optimized move in the same 113 * root it should succeed regardless of free space, but it's for sure a failure if there is no 114 * enough free space if docs are moved from another authority. 115 */ 116 @Override checkSpace()117 boolean checkSpace() { 118 long size = 0; 119 for (DocumentInfo src : mResolvedDocs) { 120 if (!src.authority.equals(stack.getRoot().authority)) { 121 if (src.isDirectory()) { 122 try { 123 size += calculateFileSizesRecursively(getClient(src), src.derivedUri); 124 } catch (RemoteException|ResourceException e) { 125 Log.w(TAG, "Failed to obtain client for %s" + src.derivedUri + ".", e); 126 127 // Failed to calculate size, but move may still succeed. 128 return true; 129 } 130 } else { 131 size += src.size; 132 } 133 } 134 } 135 136 return verifySpaceAvailable(size); 137 } 138 processDocument(DocumentInfo src, DocumentInfo srcParent, DocumentInfo dest)139 void processDocument(DocumentInfo src, DocumentInfo srcParent, DocumentInfo dest) 140 throws ResourceException { 141 142 // TODO: When optimized move kicks in, we're not making any progress updates. FIX IT! 143 144 // When moving within the same provider, try to use optimized moving. 145 // If not supported, then fallback to byte-by-byte copy/move. 146 if (src.authority.equals(dest.authority) && (srcParent != null || mSrcParent != null)) { 147 if ((src.flags & Document.FLAG_SUPPORTS_MOVE) != 0) { 148 try { 149 if (DocumentsContract.moveDocument(getClient(src), src.derivedUri, 150 srcParent != null ? srcParent.derivedUri : mSrcParent.derivedUri, 151 dest.derivedUri) != null) { 152 Metrics.logFileOperated( 153 appContext, operationType, Metrics.OPMODE_PROVIDER); 154 return; 155 } 156 } catch (RemoteException | RuntimeException e) { 157 Metrics.logFileOperationFailure( 158 appContext, Metrics.SUBFILEOP_QUICK_MOVE, src.derivedUri); 159 Log.e(TAG, "Provider side move failed for: " + src.derivedUri 160 + " due to an exception: ", e); 161 } 162 // If optimized move fails, then fallback to byte-by-byte copy. 163 if (DEBUG) Log.d(TAG, "Fallback to byte-by-byte move for: " + src.derivedUri); 164 } 165 } 166 167 // Moving virtual files by bytes is not supported. This is because, it would involve 168 // conversion, and the source file should not be deleted in such case (as it's a different 169 // file). 170 if (src.isVirtual()) { 171 throw new ResourceException("Cannot move virtual file %s byte by byte.", 172 src.derivedUri); 173 } 174 175 // If we couldn't do an optimized copy...we fall back to vanilla byte copy. 176 byteCopyDocument(src, dest); 177 178 // Remove the source document. 179 if(!isCanceled()) { 180 deleteDocument(src, srcParent); 181 } 182 } 183 184 @Override toString()185 public String toString() { 186 return new StringBuilder() 187 .append("MoveJob") 188 .append("{") 189 .append("id=" + id) 190 .append(", uris=" + mResourceUris) 191 .append(", docs=" + mResolvedDocs) 192 .append(", srcParent=" + mSrcParent) 193 .append(", destination=" + stack) 194 .append("}") 195 .toString(); 196 } 197 } 198