blob: afb3374699c99867af34ffe031767851591447db [file] [log] [blame]
Steve McKay14e827a2016-01-06 18:32:13 -08001/*
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
17package com.android.documentsui.services;
18
Steve McKay35645432016-01-20 15:09:35 -080019import static com.android.documentsui.DocumentsApplication.acquireUnstableProviderOrThrow;
Steve McKayecbf3c52016-01-13 17:17:39 -080020import static com.android.documentsui.services.FileOperationService.EXTRA_CANCEL;
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +090021import static com.android.documentsui.services.FileOperationService.EXTRA_DIALOG_TYPE;
Steve McKayecbf3c52016-01-13 17:17:39 -080022import static com.android.documentsui.services.FileOperationService.EXTRA_JOB_ID;
23import static com.android.documentsui.services.FileOperationService.EXTRA_OPERATION;
24import static com.android.documentsui.services.FileOperationService.EXTRA_SRC_LIST;
Steve McKay14e827a2016-01-06 18:32:13 -080025import static com.android.documentsui.services.FileOperationService.OPERATION_UNKNOWN;
26import static com.android.internal.util.Preconditions.checkArgument;
Steve McKay35645432016-01-20 15:09:35 -080027import static com.android.internal.util.Preconditions.checkNotNull;
Steve McKay14e827a2016-01-06 18:32:13 -080028
29import android.annotation.DrawableRes;
30import android.annotation.PluralsRes;
31import android.app.Notification;
32import android.app.Notification.Builder;
33import android.app.PendingIntent;
Steve McKay35645432016-01-20 15:09:35 -080034import android.content.ContentProviderClient;
Steve McKay14e827a2016-01-06 18:32:13 -080035import android.content.ContentResolver;
36import android.content.Context;
37import android.content.Intent;
Tomasz Mikolajewskicd270152016-02-01 12:01:14 +090038import android.net.Uri;
Steve McKay14e827a2016-01-06 18:32:13 -080039import android.os.Parcelable;
40import android.os.RemoteException;
41import android.provider.DocumentsContract;
Steve McKay35645432016-01-20 15:09:35 -080042import android.util.Log;
Steve McKay14e827a2016-01-06 18:32:13 -080043
44import com.android.documentsui.FilesActivity;
Ben Kwad5b2af12016-01-28 16:39:57 -080045import com.android.documentsui.Metrics;
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +090046import com.android.documentsui.OperationDialogFragment;
Steve McKay14e827a2016-01-06 18:32:13 -080047import com.android.documentsui.R;
48import com.android.documentsui.Shared;
49import com.android.documentsui.model.DocumentInfo;
50import com.android.documentsui.model.DocumentStack;
51import com.android.documentsui.services.FileOperationService.OpType;
52
53import java.util.ArrayList;
Steve McKay35645432016-01-20 15:09:35 -080054import java.util.HashMap;
Steve McKayecbf3c52016-01-13 17:17:39 -080055import java.util.List;
Steve McKay35645432016-01-20 15:09:35 -080056import java.util.Map;
Steve McKay14e827a2016-01-06 18:32:13 -080057
Steve McKayecbf3c52016-01-13 17:17:39 -080058/**
59 * A mashup of work item and ui progress update factory. Used by {@link FileOperationService}
60 * to do work and show progress relating to this work.
61 */
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +090062abstract public class Job implements Runnable {
Steve McKay35645432016-01-20 15:09:35 -080063 private static final String TAG = "Job";
Tomasz Mikolajewskicd270152016-02-01 12:01:14 +090064
65 static final String INTENT_TAG_WARNING = "warning";
66 static final String INTENT_TAG_FAILURE = "failure";
67 static final String INTENT_TAG_PROGRESS = "progress";
68 static final String INTENT_TAG_CANCEL = "cancel";
69
Steve McKayecbf3c52016-01-13 17:17:39 -080070 final Context service;
Steve McKay14e827a2016-01-06 18:32:13 -080071 final Context appContext;
72 final Listener listener;
73
Steve McKayecbf3c52016-01-13 17:17:39 -080074 final @OpType int operationType;
Steve McKay14e827a2016-01-06 18:32:13 -080075 final String id;
76 final DocumentStack stack;
77
78 final ArrayList<DocumentInfo> failedFiles = new ArrayList<>();
79 final Notification.Builder mProgressBuilder;
80
Steve McKay35645432016-01-20 15:09:35 -080081 private final Map<String, ContentProviderClient> mClients = new HashMap<>();
Steve McKay14e827a2016-01-06 18:32:13 -080082 private volatile boolean mCanceled;
83
84 /**
85 * A simple progressable job, much like an AsyncTask, but with support
86 * for providing various related notification, progress and navigation information.
Steve McKayecbf3c52016-01-13 17:17:39 -080087 * @param operationType
Steve McKay14e827a2016-01-06 18:32:13 -080088 *
Steve McKayecbf3c52016-01-13 17:17:39 -080089 * @param service The service context in which this job is running.
Steve McKay14e827a2016-01-06 18:32:13 -080090 * @param appContext The context of the invoking application. This is usually
91 * just {@code getApplicationContext()}.
92 * @param listener
93 * @param id Arbitrary string ID
94 * @param stack The documents stack context relating to this request. This is the
95 * destination in the Files app where the user will be take when the
96 * navigation intent is invoked (presumably from notification).
97 */
Steve McKayecbf3c52016-01-13 17:17:39 -080098 Job(Context service, Context appContext, Listener listener,
99 @OpType int operationType, String id, DocumentStack stack) {
Steve McKay14e827a2016-01-06 18:32:13 -0800100
Steve McKayecbf3c52016-01-13 17:17:39 -0800101 checkArgument(operationType != OPERATION_UNKNOWN);
102
103 this.service = service;
Steve McKay14e827a2016-01-06 18:32:13 -0800104 this.appContext = appContext;
105 this.listener = listener;
Steve McKayecbf3c52016-01-13 17:17:39 -0800106 this.operationType = operationType;
Steve McKay14e827a2016-01-06 18:32:13 -0800107
108 this.id = id;
109 this.stack = stack;
110
111 mProgressBuilder = createProgressBuilder();
112 }
113
Steve McKayecbf3c52016-01-13 17:17:39 -0800114 @Override
115 public final void run() {
116 listener.onStart(this);
117 try {
118 start();
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900119 } catch (RuntimeException e) {
120 // No exceptions should be thrown here, as all calls to the provider must be
121 // handled within Job implementations. However, just in case catch them here.
122 Log.e(TAG, "Operation failed due to an unhandled runtime exception.", e);
Ben Kwad5b2af12016-01-28 16:39:57 -0800123 Metrics.logFileOperationErrors(service, operationType, failedFiles);
Steve McKayecbf3c52016-01-13 17:17:39 -0800124 } finally {
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +0900125 listener.onFinished(this);
Steve McKayecbf3c52016-01-13 17:17:39 -0800126 }
Steve McKay14e827a2016-01-06 18:32:13 -0800127 }
128
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900129 abstract void start();
Steve McKayecbf3c52016-01-13 17:17:39 -0800130
Steve McKay14e827a2016-01-06 18:32:13 -0800131 abstract Notification getSetupNotification();
132 // TODO: Progress notification for deletes.
133 // abstract Notification getProgressNotification(long bytesCopied);
134 abstract Notification getFailureNotification();
135
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +0900136 abstract Notification getWarningNotification();
137
Tomasz Mikolajewskicd270152016-02-01 12:01:14 +0900138 Uri getDataUriForIntent(String tag) {
139 return Uri.parse(String.format("data,%s-%s", tag, id));
140 }
141
Steve McKay35645432016-01-20 15:09:35 -0800142 ContentProviderClient getClient(DocumentInfo doc) throws RemoteException {
143 ContentProviderClient client = mClients.get(doc.authority);
144 if (client == null) {
145 // Acquire content providers.
146 client = acquireUnstableProviderOrThrow(
147 getContentResolver(),
148 doc.authority);
149
150 mClients.put(doc.authority, client);
151 }
152
153 return checkNotNull(client);
154 }
155
156 final void cleanup() {
157 for (ContentProviderClient client : mClients.values()) {
158 ContentProviderClient.releaseQuietly(client);
159 }
160 }
161
Steve McKay14e827a2016-01-06 18:32:13 -0800162 final void cancel() {
163 mCanceled = true;
Ben Kwad5b2af12016-01-28 16:39:57 -0800164 Metrics.logFileOperationCancelled(service, operationType);
Steve McKay14e827a2016-01-06 18:32:13 -0800165 }
166
167 final boolean isCanceled() {
168 return mCanceled;
169 }
170
171 final ContentResolver getContentResolver() {
Steve McKayecbf3c52016-01-13 17:17:39 -0800172 return service.getContentResolver();
Steve McKay14e827a2016-01-06 18:32:13 -0800173 }
174
175 void onFileFailed(DocumentInfo file) {
176 failedFiles.add(file);
177 }
178
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +0900179 final boolean hasFailures() {
Steve McKay14e827a2016-01-06 18:32:13 -0800180 return !failedFiles.isEmpty();
181 }
182
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +0900183 boolean hasWarnings() {
184 return false;
185 }
186
Tomasz Mikolajewskidb875432016-02-23 15:12:54 +0900187 final void deleteDocument(DocumentInfo doc, DocumentInfo parent) throws ResourceException {
Steve McKay35645432016-01-20 15:09:35 -0800188 try {
Tomasz Mikolajewskidb875432016-02-23 15:12:54 +0900189 if (doc.isRemoveSupported()) {
190 DocumentsContract.removeDocument(getClient(doc), doc.derivedUri, parent.derivedUri);
191 } else if (doc.isDeleteSupported()) {
192 DocumentsContract.deleteDocument(getClient(doc), doc.derivedUri);
193 } else {
194 throw new ResourceException("Unable to delete source document as the file is " +
195 "not deletable nor removable: %s.", doc.derivedUri);
196 }
197 } catch (RemoteException | RuntimeException e) {
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900198 throw new ResourceException("Failed to delete file %s due to an exception.",
199 doc.derivedUri, e);
Steve McKay35645432016-01-20 15:09:35 -0800200 }
Steve McKay35645432016-01-20 15:09:35 -0800201 }
202
Steve McKay14e827a2016-01-06 18:32:13 -0800203 Notification getSetupNotification(String content) {
204 mProgressBuilder.setProgress(0, 0, true);
205 mProgressBuilder.setContentText(content);
206 return mProgressBuilder.build();
207 }
208
209 Notification getFailureNotification(@PluralsRes int titleId, @DrawableRes int icon) {
Tomasz Mikolajewskicd270152016-02-01 12:01:14 +0900210 final Intent navigateIntent = buildNavigateIntent(INTENT_TAG_FAILURE);
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +0900211 navigateIntent.putExtra(EXTRA_DIALOG_TYPE, OperationDialogFragment.DIALOG_TYPE_FAILURE);
Steve McKayecbf3c52016-01-13 17:17:39 -0800212 navigateIntent.putExtra(EXTRA_OPERATION, operationType);
Steve McKayecbf3c52016-01-13 17:17:39 -0800213 navigateIntent.putParcelableArrayListExtra(EXTRA_SRC_LIST, failedFiles);
Steve McKay14e827a2016-01-06 18:32:13 -0800214
Steve McKayecbf3c52016-01-13 17:17:39 -0800215 final Notification.Builder errorBuilder = new Notification.Builder(service)
216 .setContentTitle(service.getResources().getQuantityString(titleId,
Steve McKay14e827a2016-01-06 18:32:13 -0800217 failedFiles.size(), failedFiles.size()))
Steve McKayecbf3c52016-01-13 17:17:39 -0800218 .setContentText(service.getString(R.string.notification_touch_for_details))
Steve McKay14e827a2016-01-06 18:32:13 -0800219 .setContentIntent(PendingIntent.getActivity(appContext, 0, navigateIntent,
220 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT))
221 .setCategory(Notification.CATEGORY_ERROR)
222 .setSmallIcon(icon)
223 .setAutoCancel(true);
224 return errorBuilder.build();
225 }
226
227 abstract Builder createProgressBuilder();
228
229 final Builder createProgressBuilder(
230 String title, @DrawableRes int icon,
231 String actionTitle, @DrawableRes int actionIcon) {
Steve McKayecbf3c52016-01-13 17:17:39 -0800232 Notification.Builder progressBuilder = new Notification.Builder(service)
Steve McKay14e827a2016-01-06 18:32:13 -0800233 .setContentTitle(title)
234 .setContentIntent(
Tomasz Mikolajewskicd270152016-02-01 12:01:14 +0900235 PendingIntent.getActivity(appContext, 0,
236 buildNavigateIntent(INTENT_TAG_PROGRESS), 0))
Steve McKay14e827a2016-01-06 18:32:13 -0800237 .setCategory(Notification.CATEGORY_PROGRESS)
238 .setSmallIcon(icon)
239 .setOngoing(true);
240
241 final Intent cancelIntent = createCancelIntent();
242
243 progressBuilder.addAction(
244 actionIcon,
245 actionTitle,
246 PendingIntent.getService(
Steve McKayecbf3c52016-01-13 17:17:39 -0800247 service,
Steve McKay14e827a2016-01-06 18:32:13 -0800248 0,
249 cancelIntent,
250 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT));
251
252 return progressBuilder;
253 }
254
255 /**
256 * Creates an intent for navigating back to the destination directory.
257 */
Tomasz Mikolajewskicd270152016-02-01 12:01:14 +0900258 Intent buildNavigateIntent(String tag) {
Steve McKayecbf3c52016-01-13 17:17:39 -0800259 Intent intent = new Intent(service, FilesActivity.class);
Tomasz Mikolajewskicd270152016-02-01 12:01:14 +0900260 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Steve McKay14e827a2016-01-06 18:32:13 -0800261 intent.setAction(DocumentsContract.ACTION_BROWSE);
Tomasz Mikolajewskicd270152016-02-01 12:01:14 +0900262 intent.setData(getDataUriForIntent(tag));
Steve McKay14e827a2016-01-06 18:32:13 -0800263 intent.putExtra(Shared.EXTRA_STACK, (Parcelable) stack);
264 return intent;
265 }
266
267 Intent createCancelIntent() {
Steve McKayecbf3c52016-01-13 17:17:39 -0800268 final Intent cancelIntent = new Intent(service, FileOperationService.class);
Tomasz Mikolajewskicd270152016-02-01 12:01:14 +0900269 cancelIntent.setData(getDataUriForIntent(INTENT_TAG_CANCEL));
Steve McKayecbf3c52016-01-13 17:17:39 -0800270 cancelIntent.putExtra(EXTRA_CANCEL, true);
271 cancelIntent.putExtra(EXTRA_JOB_ID, id);
Steve McKay14e827a2016-01-06 18:32:13 -0800272 return cancelIntent;
273 }
274
Steve McKay35645432016-01-20 15:09:35 -0800275 @Override
276 public String toString() {
277 return new StringBuilder()
278 .append("Job")
279 .append("{")
280 .append("id=" + id)
281 .append("}")
282 .toString();
283 }
284
Steve McKayecbf3c52016-01-13 17:17:39 -0800285 /**
286 * Factory class that facilitates our testing FileOperationService.
287 */
288 static class Factory {
289
290 static final Factory instance = new Factory();
291
292 Job createCopy(Context service, Context appContext, Listener listener,
293 String id, DocumentStack stack, List<DocumentInfo> srcs) {
294 return new CopyJob(service, appContext, listener, id, stack, srcs);
295 }
296
297 Job createMove(Context service, Context appContext, Listener listener,
Tomasz Mikolajewskib8436af2016-01-25 16:20:15 +0900298 String id, DocumentStack stack, List<DocumentInfo> srcs,
299 DocumentInfo srcParent) {
300 return new MoveJob(service, appContext, listener, id, stack, srcs, srcParent);
Steve McKayecbf3c52016-01-13 17:17:39 -0800301 }
Steve McKay35645432016-01-20 15:09:35 -0800302
303 Job createDelete(Context service, Context appContext, Listener listener,
Tomasz Mikolajewskib8436af2016-01-25 16:20:15 +0900304 String id, DocumentStack stack, List<DocumentInfo> srcs,
305 DocumentInfo srcParent) {
306 return new DeleteJob(service, appContext, listener, id, stack, srcs, srcParent);
Steve McKay35645432016-01-20 15:09:35 -0800307 }
Steve McKayecbf3c52016-01-13 17:17:39 -0800308 }
309
310 /**
311 * Listener interface employed by the service that owns us as well as tests.
312 */
Steve McKay14e827a2016-01-06 18:32:13 -0800313 interface Listener {
Steve McKayecbf3c52016-01-13 17:17:39 -0800314 void onStart(Job job);
Steve McKayecbf3c52016-01-13 17:17:39 -0800315 void onFinished(Job job);
Steve McKay14e827a2016-01-06 18:32:13 -0800316 void onProgress(CopyJob job);
Steve McKay14e827a2016-01-06 18:32:13 -0800317 }
318}