blob: 3cec4a47eafa673b7e62e52b284e8c0cd5cac904 [file] [log] [blame]
Steve McKayc83baa02016-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 McKay97b4be42016-01-20 15:09:35 -080019import static com.android.documentsui.DocumentsApplication.acquireUnstableProviderOrThrow;
Steve McKaybbeba522016-01-13 17:17:39 -080020import static com.android.documentsui.services.FileOperationService.EXTRA_CANCEL;
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +090021import static com.android.documentsui.services.FileOperationService.EXTRA_DIALOG_TYPE;
Steve McKaybbeba522016-01-13 17:17:39 -080022import static com.android.documentsui.services.FileOperationService.EXTRA_JOB_ID;
Garfield, Tan48334772016-06-28 17:17:38 -070023import static com.android.documentsui.services.FileOperationService.EXTRA_OPERATION_TYPE;
Steve McKaybbeba522016-01-13 17:17:39 -080024import static com.android.documentsui.services.FileOperationService.EXTRA_SRC_LIST;
Steve McKayc83baa02016-01-06 18:32:13 -080025import static com.android.documentsui.services.FileOperationService.OPERATION_UNKNOWN;
Steve McKayc83baa02016-01-06 18:32:13 -080026
27import android.annotation.DrawableRes;
Garfield, Tan48ef36f2016-06-09 12:04:22 -070028import android.annotation.IntDef;
Steve McKayc83baa02016-01-06 18:32:13 -080029import android.annotation.PluralsRes;
30import android.app.Notification;
31import android.app.Notification.Builder;
32import android.app.PendingIntent;
Steve McKay97b4be42016-01-20 15:09:35 -080033import android.content.ContentProviderClient;
Steve McKayc83baa02016-01-06 18:32:13 -080034import android.content.ContentResolver;
35import android.content.Context;
36import android.content.Intent;
Tomasz Mikolajewski63e2aae2016-02-01 12:01:14 +090037import android.net.Uri;
Steve McKayc83baa02016-01-06 18:32:13 -080038import android.os.Parcelable;
39import android.os.RemoteException;
40import android.provider.DocumentsContract;
Steve McKay97b4be42016-01-20 15:09:35 -080041import android.util.Log;
Steve McKayc83baa02016-01-06 18:32:13 -080042
Garfield, Tan9666ce62016-07-12 11:02:09 -070043import com.android.documentsui.clipping.UrisSupplier;
Steve McKayb6006b22016-09-29 09:23:45 -070044import com.android.documentsui.files.FilesActivity;
Ben Kwafaa27202016-01-28 16:39:57 -080045import com.android.documentsui.Metrics;
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +090046import com.android.documentsui.OperationDialogFragment;
Steve McKayc83baa02016-01-06 18:32:13 -080047import com.android.documentsui.R;
Steve McKayd0805062016-09-15 14:30:38 -070048import com.android.documentsui.base.DocumentInfo;
49import com.android.documentsui.base.DocumentStack;
Steve McKayd9caa6a2016-09-15 16:36:45 -070050import com.android.documentsui.base.Shared;
Steve McKayc83baa02016-01-06 18:32:13 -080051import com.android.documentsui.services.FileOperationService.OpType;
52
Garfield Tan03a3a392016-12-12 14:06:45 -080053import javax.annotation.Nullable;
Garfield, Tan48ef36f2016-06-09 12:04:22 -070054import java.lang.annotation.Retention;
55import java.lang.annotation.RetentionPolicy;
Steve McKayc83baa02016-01-06 18:32:13 -080056import java.util.ArrayList;
Steve McKay97b4be42016-01-20 15:09:35 -080057import java.util.HashMap;
Steve McKay97b4be42016-01-20 15:09:35 -080058import java.util.Map;
Steve McKayc83baa02016-01-06 18:32:13 -080059
Steve McKaybbeba522016-01-13 17:17:39 -080060/**
61 * A mashup of work item and ui progress update factory. Used by {@link FileOperationService}
62 * to do work and show progress relating to this work.
63 */
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +090064abstract public class Job implements Runnable {
Steve McKay97b4be42016-01-20 15:09:35 -080065 private static final String TAG = "Job";
Tomasz Mikolajewski63e2aae2016-02-01 12:01:14 +090066
Garfield, Tan48ef36f2016-06-09 12:04:22 -070067 @Retention(RetentionPolicy.SOURCE)
Garfield, Tanedce5542016-06-17 15:32:28 -070068 @IntDef({STATE_CREATED, STATE_STARTED, STATE_SET_UP, STATE_COMPLETED, STATE_CANCELED})
Garfield, Tan48ef36f2016-06-09 12:04:22 -070069 @interface State {}
70 static final int STATE_CREATED = 0;
71 static final int STATE_STARTED = 1;
Garfield, Tanedce5542016-06-17 15:32:28 -070072 static final int STATE_SET_UP = 2;
73 static final int STATE_COMPLETED = 3;
Garfield, Tan48ef36f2016-06-09 12:04:22 -070074 /**
75 * A job is in canceled state as long as {@link #cancel()} is called on it, even after it is
76 * completed.
77 */
Garfield, Tanedce5542016-06-17 15:32:28 -070078 static final int STATE_CANCELED = 4;
Garfield, Tan48ef36f2016-06-09 12:04:22 -070079
Tomasz Mikolajewski63e2aae2016-02-01 12:01:14 +090080 static final String INTENT_TAG_WARNING = "warning";
81 static final String INTENT_TAG_FAILURE = "failure";
82 static final String INTENT_TAG_PROGRESS = "progress";
83 static final String INTENT_TAG_CANCEL = "cancel";
84
Steve McKaybbeba522016-01-13 17:17:39 -080085 final Context service;
Steve McKayc83baa02016-01-06 18:32:13 -080086 final Context appContext;
87 final Listener listener;
88
Steve McKaybbeba522016-01-13 17:17:39 -080089 final @OpType int operationType;
Steve McKayc83baa02016-01-06 18:32:13 -080090 final String id;
91 final DocumentStack stack;
Garfield, Tan48334772016-06-28 17:17:38 -070092 final UrisSupplier srcs;
Steve McKayc83baa02016-01-06 18:32:13 -080093
Garfield, Tanedce5542016-06-17 15:32:28 -070094 int failedFileCount = 0;
Steve McKayc83baa02016-01-06 18:32:13 -080095 final ArrayList<DocumentInfo> failedFiles = new ArrayList<>();
96 final Notification.Builder mProgressBuilder;
97
Steve McKay97b4be42016-01-20 15:09:35 -080098 private final Map<String, ContentProviderClient> mClients = new HashMap<>();
Garfield, Tan48ef36f2016-06-09 12:04:22 -070099 private volatile @State int mState = STATE_CREATED;
Steve McKayc83baa02016-01-06 18:32:13 -0800100
101 /**
102 * A simple progressable job, much like an AsyncTask, but with support
103 * for providing various related notification, progress and navigation information.
Steve McKaybbeba522016-01-13 17:17:39 -0800104 * @param service The service context in which this job is running.
Steve McKayc83baa02016-01-06 18:32:13 -0800105 * @param listener
106 * @param id Arbitrary string ID
107 * @param stack The documents stack context relating to this request. This is the
108 * destination in the Files app where the user will be take when the
109 * navigation intent is invoked (presumably from notification).
Garfield, Tan48334772016-06-28 17:17:38 -0700110 * @param srcs the list of docs to operate on
Steve McKayc83baa02016-01-06 18:32:13 -0800111 */
Garfield, Tan48334772016-06-28 17:17:38 -0700112 Job(Context service, Listener listener, String id,
113 @OpType int opType, DocumentStack stack, UrisSupplier srcs) {
Steve McKayc83baa02016-01-06 18:32:13 -0800114
Garfield, Tan48334772016-06-28 17:17:38 -0700115 assert(opType != OPERATION_UNKNOWN);
Steve McKaybbeba522016-01-13 17:17:39 -0800116
117 this.service = service;
Garfield, Tan48334772016-06-28 17:17:38 -0700118 this.appContext = service.getApplicationContext();
Steve McKayc83baa02016-01-06 18:32:13 -0800119 this.listener = listener;
Garfield, Tan48334772016-06-28 17:17:38 -0700120 this.operationType = opType;
Steve McKayc83baa02016-01-06 18:32:13 -0800121
122 this.id = id;
123 this.stack = stack;
Garfield, Tan48334772016-06-28 17:17:38 -0700124 this.srcs = srcs;
Steve McKayc83baa02016-01-06 18:32:13 -0800125
126 mProgressBuilder = createProgressBuilder();
127 }
128
Steve McKaybbeba522016-01-13 17:17:39 -0800129 @Override
130 public final void run() {
Garfield, Tan48ef36f2016-06-09 12:04:22 -0700131 if (isCanceled()) {
132 // Canceled before running
133 return;
134 }
135
136 mState = STATE_STARTED;
Steve McKaybbeba522016-01-13 17:17:39 -0800137 listener.onStart(this);
138 try {
Garfield, Tanedce5542016-06-17 15:32:28 -0700139 boolean result = setUp();
140 if (result && !isCanceled()) {
141 mState = STATE_SET_UP;
142 start();
143 }
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900144 } catch (RuntimeException e) {
145 // No exceptions should be thrown here, as all calls to the provider must be
146 // handled within Job implementations. However, just in case catch them here.
147 Log.e(TAG, "Operation failed due to an unhandled runtime exception.", e);
Ben Kwafaa27202016-01-28 16:39:57 -0800148 Metrics.logFileOperationErrors(service, operationType, failedFiles);
Steve McKaybbeba522016-01-13 17:17:39 -0800149 } finally {
Garfield, Tanedce5542016-06-17 15:32:28 -0700150 mState = (mState == STATE_STARTED || mState == STATE_SET_UP) ? STATE_COMPLETED : mState;
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +0900151 listener.onFinished(this);
Garfield, Tanedce5542016-06-17 15:32:28 -0700152
153 // NOTE: If this details is a JumboClipDetails, and it's still referred in primary clip
154 // at this point, user won't be able to paste it to anywhere else because the underlying
Garfield, Tanb7e5f6b2016-06-30 18:27:47 -0700155 srcs.dispose();
Steve McKaybbeba522016-01-13 17:17:39 -0800156 }
Steve McKayc83baa02016-01-06 18:32:13 -0800157 }
158
Garfield, Tanedce5542016-06-17 15:32:28 -0700159 boolean setUp() {
160 return true;
161 }
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900162 abstract void start();
Steve McKaybbeba522016-01-13 17:17:39 -0800163
Steve McKayc83baa02016-01-06 18:32:13 -0800164 abstract Notification getSetupNotification();
Garfield, Tan48ef36f2016-06-09 12:04:22 -0700165 abstract Notification getProgressNotification();
Steve McKayc83baa02016-01-06 18:32:13 -0800166 abstract Notification getFailureNotification();
167
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +0900168 abstract Notification getWarningNotification();
169
Tomasz Mikolajewski63e2aae2016-02-01 12:01:14 +0900170 Uri getDataUriForIntent(String tag) {
171 return Uri.parse(String.format("data,%s-%s", tag, id));
172 }
173
Steve McKay97b4be42016-01-20 15:09:35 -0800174 ContentProviderClient getClient(DocumentInfo doc) throws RemoteException {
175 ContentProviderClient client = mClients.get(doc.authority);
176 if (client == null) {
177 // Acquire content providers.
178 client = acquireUnstableProviderOrThrow(
179 getContentResolver(),
180 doc.authority);
181
182 mClients.put(doc.authority, client);
183 }
184
Steve McKay0af8afd2016-02-25 13:34:03 -0800185 assert(client != null);
186 return client;
Steve McKay97b4be42016-01-20 15:09:35 -0800187 }
188
189 final void cleanup() {
190 for (ContentProviderClient client : mClients.values()) {
191 ContentProviderClient.releaseQuietly(client);
192 }
193 }
194
Garfield, Tan48ef36f2016-06-09 12:04:22 -0700195 final @State int getState() {
196 return mState;
197 }
198
Steve McKayc83baa02016-01-06 18:32:13 -0800199 final void cancel() {
Garfield, Tan48ef36f2016-06-09 12:04:22 -0700200 mState = STATE_CANCELED;
Ben Kwafaa27202016-01-28 16:39:57 -0800201 Metrics.logFileOperationCancelled(service, operationType);
Steve McKayc83baa02016-01-06 18:32:13 -0800202 }
203
204 final boolean isCanceled() {
Garfield, Tan48ef36f2016-06-09 12:04:22 -0700205 return mState == STATE_CANCELED;
206 }
207
208 final boolean isFinished() {
209 return mState == STATE_CANCELED || mState == STATE_COMPLETED;
Steve McKayc83baa02016-01-06 18:32:13 -0800210 }
211
212 final ContentResolver getContentResolver() {
Steve McKaybbeba522016-01-13 17:17:39 -0800213 return service.getContentResolver();
Steve McKayc83baa02016-01-06 18:32:13 -0800214 }
215
216 void onFileFailed(DocumentInfo file) {
Garfield, Tanedce5542016-06-17 15:32:28 -0700217 ++failedFileCount;
Steve McKayc83baa02016-01-06 18:32:13 -0800218 failedFiles.add(file);
219 }
220
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +0900221 final boolean hasFailures() {
Garfield, Tanedce5542016-06-17 15:32:28 -0700222 return failedFileCount > 0;
Steve McKayc83baa02016-01-06 18:32:13 -0800223 }
224
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +0900225 boolean hasWarnings() {
226 return false;
227 }
228
Garfield Tan03a3a392016-12-12 14:06:45 -0800229 final void deleteDocument(DocumentInfo doc, @Nullable DocumentInfo parent)
230 throws ResourceException {
Steve McKay97b4be42016-01-20 15:09:35 -0800231 try {
Garfield Tan03a3a392016-12-12 14:06:45 -0800232 if (parent != null && doc.isRemoveSupported()) {
Tomasz Mikolajewski8fe54982016-02-23 15:12:54 +0900233 DocumentsContract.removeDocument(getClient(doc), doc.derivedUri, parent.derivedUri);
234 } else if (doc.isDeleteSupported()) {
235 DocumentsContract.deleteDocument(getClient(doc), doc.derivedUri);
236 } else {
237 throw new ResourceException("Unable to delete source document as the file is " +
238 "not deletable nor removable: %s.", doc.derivedUri);
239 }
240 } catch (RemoteException | RuntimeException e) {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900241 throw new ResourceException("Failed to delete file %s due to an exception.",
242 doc.derivedUri, e);
Steve McKay97b4be42016-01-20 15:09:35 -0800243 }
Steve McKay97b4be42016-01-20 15:09:35 -0800244 }
245
Steve McKayc83baa02016-01-06 18:32:13 -0800246 Notification getSetupNotification(String content) {
Steve McKay003097d2016-02-23 10:06:50 -0800247 mProgressBuilder.setProgress(0, 0, true)
248 .setContentText(content);
Steve McKayc83baa02016-01-06 18:32:13 -0800249 return mProgressBuilder.build();
250 }
251
252 Notification getFailureNotification(@PluralsRes int titleId, @DrawableRes int icon) {
Tomasz Mikolajewski63e2aae2016-02-01 12:01:14 +0900253 final Intent navigateIntent = buildNavigateIntent(INTENT_TAG_FAILURE);
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +0900254 navigateIntent.putExtra(EXTRA_DIALOG_TYPE, OperationDialogFragment.DIALOG_TYPE_FAILURE);
Garfield, Tan48334772016-06-28 17:17:38 -0700255 navigateIntent.putExtra(EXTRA_OPERATION_TYPE, operationType);
Steve McKaybbeba522016-01-13 17:17:39 -0800256 navigateIntent.putParcelableArrayListExtra(EXTRA_SRC_LIST, failedFiles);
Steve McKayc83baa02016-01-06 18:32:13 -0800257
Steve McKaybbeba522016-01-13 17:17:39 -0800258 final Notification.Builder errorBuilder = new Notification.Builder(service)
259 .setContentTitle(service.getResources().getQuantityString(titleId,
Garfield, Tanedce5542016-06-17 15:32:28 -0700260 failedFileCount, failedFileCount))
Steve McKaybbeba522016-01-13 17:17:39 -0800261 .setContentText(service.getString(R.string.notification_touch_for_details))
Steve McKayc83baa02016-01-06 18:32:13 -0800262 .setContentIntent(PendingIntent.getActivity(appContext, 0, navigateIntent,
263 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT))
264 .setCategory(Notification.CATEGORY_ERROR)
265 .setSmallIcon(icon)
266 .setAutoCancel(true);
Steve McKay003097d2016-02-23 10:06:50 -0800267
Steve McKayc83baa02016-01-06 18:32:13 -0800268 return errorBuilder.build();
269 }
270
271 abstract Builder createProgressBuilder();
272
273 final Builder createProgressBuilder(
274 String title, @DrawableRes int icon,
275 String actionTitle, @DrawableRes int actionIcon) {
Steve McKaybbeba522016-01-13 17:17:39 -0800276 Notification.Builder progressBuilder = new Notification.Builder(service)
Steve McKayc83baa02016-01-06 18:32:13 -0800277 .setContentTitle(title)
278 .setContentIntent(
Tomasz Mikolajewski63e2aae2016-02-01 12:01:14 +0900279 PendingIntent.getActivity(appContext, 0,
280 buildNavigateIntent(INTENT_TAG_PROGRESS), 0))
Steve McKayc83baa02016-01-06 18:32:13 -0800281 .setCategory(Notification.CATEGORY_PROGRESS)
282 .setSmallIcon(icon)
283 .setOngoing(true);
284
285 final Intent cancelIntent = createCancelIntent();
286
287 progressBuilder.addAction(
288 actionIcon,
289 actionTitle,
290 PendingIntent.getService(
Steve McKaybbeba522016-01-13 17:17:39 -0800291 service,
Steve McKayc83baa02016-01-06 18:32:13 -0800292 0,
293 cancelIntent,
294 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT));
295
296 return progressBuilder;
297 }
298
299 /**
300 * Creates an intent for navigating back to the destination directory.
301 */
Tomasz Mikolajewski63e2aae2016-02-01 12:01:14 +0900302 Intent buildNavigateIntent(String tag) {
Steve McKayb6006b22016-09-29 09:23:45 -0700303 Intent intent = new Intent(service, FilesActivity.class);
Tomasz Mikolajewski63e2aae2016-02-01 12:01:14 +0900304 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Steve McKayc83baa02016-01-06 18:32:13 -0800305 intent.setAction(DocumentsContract.ACTION_BROWSE);
Tomasz Mikolajewski63e2aae2016-02-01 12:01:14 +0900306 intent.setData(getDataUriForIntent(tag));
Steve McKayc83baa02016-01-06 18:32:13 -0800307 intent.putExtra(Shared.EXTRA_STACK, (Parcelable) stack);
308 return intent;
309 }
310
311 Intent createCancelIntent() {
Steve McKaybbeba522016-01-13 17:17:39 -0800312 final Intent cancelIntent = new Intent(service, FileOperationService.class);
Tomasz Mikolajewski63e2aae2016-02-01 12:01:14 +0900313 cancelIntent.setData(getDataUriForIntent(INTENT_TAG_CANCEL));
Steve McKaybbeba522016-01-13 17:17:39 -0800314 cancelIntent.putExtra(EXTRA_CANCEL, true);
315 cancelIntent.putExtra(EXTRA_JOB_ID, id);
Steve McKayc83baa02016-01-06 18:32:13 -0800316 return cancelIntent;
317 }
318
Steve McKay97b4be42016-01-20 15:09:35 -0800319 @Override
320 public String toString() {
321 return new StringBuilder()
322 .append("Job")
323 .append("{")
324 .append("id=" + id)
325 .append("}")
326 .toString();
327 }
328
Steve McKaybbeba522016-01-13 17:17:39 -0800329 /**
Steve McKaybbeba522016-01-13 17:17:39 -0800330 * Listener interface employed by the service that owns us as well as tests.
331 */
Steve McKayc83baa02016-01-06 18:32:13 -0800332 interface Listener {
Steve McKaybbeba522016-01-13 17:17:39 -0800333 void onStart(Job job);
Steve McKaybbeba522016-01-13 17:17:39 -0800334 void onFinished(Job job);
Steve McKayc83baa02016-01-06 18:32:13 -0800335 }
336}