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