blob: c723ac60c334ed4893f03456e672733eaf69446a [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;
Steve McKay14e827a2016-01-06 18:32:13 -080026
27import android.annotation.DrawableRes;
28import android.annotation.PluralsRes;
29import android.app.Notification;
30import android.app.Notification.Builder;
31import android.app.PendingIntent;
Steve McKay35645432016-01-20 15:09:35 -080032import android.content.ContentProviderClient;
Steve McKay14e827a2016-01-06 18:32:13 -080033import android.content.ContentResolver;
34import android.content.Context;
35import android.content.Intent;
Tomasz Mikolajewskicd270152016-02-01 12:01:14 +090036import android.net.Uri;
Steve McKay14e827a2016-01-06 18:32:13 -080037import android.os.Parcelable;
38import android.os.RemoteException;
39import android.provider.DocumentsContract;
Steve McKay35645432016-01-20 15:09:35 -080040import android.util.Log;
Steve McKay14e827a2016-01-06 18:32:13 -080041
42import com.android.documentsui.FilesActivity;
Ben Kwad5b2af12016-01-28 16:39:57 -080043import com.android.documentsui.Metrics;
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +090044import com.android.documentsui.OperationDialogFragment;
Steve McKay14e827a2016-01-06 18:32:13 -080045import com.android.documentsui.R;
46import com.android.documentsui.Shared;
47import com.android.documentsui.model.DocumentInfo;
48import com.android.documentsui.model.DocumentStack;
49import com.android.documentsui.services.FileOperationService.OpType;
50
51import java.util.ArrayList;
Steve McKay35645432016-01-20 15:09:35 -080052import java.util.HashMap;
Steve McKayecbf3c52016-01-13 17:17:39 -080053import java.util.List;
Steve McKay35645432016-01-20 15:09:35 -080054import java.util.Map;
Steve McKay14e827a2016-01-06 18:32:13 -080055
Steve McKayecbf3c52016-01-13 17:17:39 -080056/**
57 * A mashup of work item and ui progress update factory. Used by {@link FileOperationService}
58 * to do work and show progress relating to this work.
59 */
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +090060abstract public class Job implements Runnable {
Steve McKay35645432016-01-20 15:09:35 -080061 private static final String TAG = "Job";
Tomasz Mikolajewskicd270152016-02-01 12:01:14 +090062
63 static final String INTENT_TAG_WARNING = "warning";
64 static final String INTENT_TAG_FAILURE = "failure";
65 static final String INTENT_TAG_PROGRESS = "progress";
66 static final String INTENT_TAG_CANCEL = "cancel";
67
Steve McKayecbf3c52016-01-13 17:17:39 -080068 final Context service;
Steve McKay14e827a2016-01-06 18:32:13 -080069 final Context appContext;
70 final Listener listener;
71
Steve McKayecbf3c52016-01-13 17:17:39 -080072 final @OpType int operationType;
Steve McKay14e827a2016-01-06 18:32:13 -080073 final String id;
74 final DocumentStack stack;
75
76 final ArrayList<DocumentInfo> failedFiles = new ArrayList<>();
77 final Notification.Builder mProgressBuilder;
78
Steve McKay35645432016-01-20 15:09:35 -080079 private final Map<String, ContentProviderClient> mClients = new HashMap<>();
Steve McKay14e827a2016-01-06 18:32:13 -080080 private volatile boolean mCanceled;
81
82 /**
83 * A simple progressable job, much like an AsyncTask, but with support
84 * for providing various related notification, progress and navigation information.
Steve McKayecbf3c52016-01-13 17:17:39 -080085 * @param operationType
Steve McKay14e827a2016-01-06 18:32:13 -080086 *
Steve McKayecbf3c52016-01-13 17:17:39 -080087 * @param service The service context in which this job is running.
Steve McKay14e827a2016-01-06 18:32:13 -080088 * @param appContext The context of the invoking application. This is usually
89 * just {@code getApplicationContext()}.
90 * @param listener
91 * @param id Arbitrary string ID
92 * @param stack The documents stack context relating to this request. This is the
93 * destination in the Files app where the user will be take when the
94 * navigation intent is invoked (presumably from notification).
95 */
Steve McKayecbf3c52016-01-13 17:17:39 -080096 Job(Context service, Context appContext, Listener listener,
97 @OpType int operationType, String id, DocumentStack stack) {
Steve McKay14e827a2016-01-06 18:32:13 -080098
Steve McKaya1f76802016-02-25 13:34:03 -080099 assert(operationType != OPERATION_UNKNOWN);
Steve McKayecbf3c52016-01-13 17:17:39 -0800100
101 this.service = service;
Steve McKay14e827a2016-01-06 18:32:13 -0800102 this.appContext = appContext;
103 this.listener = listener;
Steve McKayecbf3c52016-01-13 17:17:39 -0800104 this.operationType = operationType;
Steve McKay14e827a2016-01-06 18:32:13 -0800105
106 this.id = id;
107 this.stack = stack;
108
109 mProgressBuilder = createProgressBuilder();
110 }
111
Steve McKayecbf3c52016-01-13 17:17:39 -0800112 @Override
113 public final void run() {
114 listener.onStart(this);
115 try {
116 start();
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900117 } catch (RuntimeException e) {
118 // No exceptions should be thrown here, as all calls to the provider must be
119 // handled within Job implementations. However, just in case catch them here.
120 Log.e(TAG, "Operation failed due to an unhandled runtime exception.", e);
Ben Kwad5b2af12016-01-28 16:39:57 -0800121 Metrics.logFileOperationErrors(service, operationType, failedFiles);
Steve McKayecbf3c52016-01-13 17:17:39 -0800122 } finally {
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +0900123 listener.onFinished(this);
Steve McKayecbf3c52016-01-13 17:17:39 -0800124 }
Steve McKay14e827a2016-01-06 18:32:13 -0800125 }
126
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900127 abstract void start();
Steve McKayecbf3c52016-01-13 17:17:39 -0800128
Steve McKay14e827a2016-01-06 18:32:13 -0800129 abstract Notification getSetupNotification();
130 // TODO: Progress notification for deletes.
131 // abstract Notification getProgressNotification(long bytesCopied);
132 abstract Notification getFailureNotification();
133
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +0900134 abstract Notification getWarningNotification();
135
Tomasz Mikolajewskicd270152016-02-01 12:01:14 +0900136 Uri getDataUriForIntent(String tag) {
137 return Uri.parse(String.format("data,%s-%s", tag, id));
138 }
139
Steve McKay35645432016-01-20 15:09:35 -0800140 ContentProviderClient getClient(DocumentInfo doc) throws RemoteException {
141 ContentProviderClient client = mClients.get(doc.authority);
142 if (client == null) {
143 // Acquire content providers.
144 client = acquireUnstableProviderOrThrow(
145 getContentResolver(),
146 doc.authority);
147
148 mClients.put(doc.authority, client);
149 }
150
Steve McKaya1f76802016-02-25 13:34:03 -0800151 assert(client != null);
152 return client;
Steve McKay35645432016-01-20 15:09:35 -0800153 }
154
155 final void cleanup() {
156 for (ContentProviderClient client : mClients.values()) {
157 ContentProviderClient.releaseQuietly(client);
158 }
159 }
160
Steve McKay14e827a2016-01-06 18:32:13 -0800161 final void cancel() {
162 mCanceled = true;
Ben Kwad5b2af12016-01-28 16:39:57 -0800163 Metrics.logFileOperationCancelled(service, operationType);
Steve McKay14e827a2016-01-06 18:32:13 -0800164 }
165
166 final boolean isCanceled() {
167 return mCanceled;
168 }
169
170 final ContentResolver getContentResolver() {
Steve McKayecbf3c52016-01-13 17:17:39 -0800171 return service.getContentResolver();
Steve McKay14e827a2016-01-06 18:32:13 -0800172 }
173
174 void onFileFailed(DocumentInfo file) {
175 failedFiles.add(file);
176 }
177
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +0900178 final boolean hasFailures() {
Steve McKay14e827a2016-01-06 18:32:13 -0800179 return !failedFiles.isEmpty();
180 }
181
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +0900182 boolean hasWarnings() {
183 return false;
184 }
185
Tomasz Mikolajewskidb875432016-02-23 15:12:54 +0900186 final void deleteDocument(DocumentInfo doc, DocumentInfo parent) throws ResourceException {
Steve McKay35645432016-01-20 15:09:35 -0800187 try {
Tomasz Mikolajewskidb875432016-02-23 15:12:54 +0900188 if (doc.isRemoveSupported()) {
189 DocumentsContract.removeDocument(getClient(doc), doc.derivedUri, parent.derivedUri);
190 } else if (doc.isDeleteSupported()) {
191 DocumentsContract.deleteDocument(getClient(doc), doc.derivedUri);
192 } else {
193 throw new ResourceException("Unable to delete source document as the file is " +
194 "not deletable nor removable: %s.", doc.derivedUri);
195 }
196 } catch (RemoteException | RuntimeException e) {
Tomasz Mikolajewski0fa97e82016-02-18 16:45:44 +0900197 throw new ResourceException("Failed to delete file %s due to an exception.",
198 doc.derivedUri, e);
Steve McKay35645432016-01-20 15:09:35 -0800199 }
Steve McKay35645432016-01-20 15:09:35 -0800200 }
201
Steve McKay14e827a2016-01-06 18:32:13 -0800202 Notification getSetupNotification(String content) {
Steve McKay7a3b8112016-02-23 10:06:50 -0800203 mProgressBuilder.setProgress(0, 0, true)
204 .setContentText(content);
Steve McKay14e827a2016-01-06 18:32:13 -0800205 return mProgressBuilder.build();
206 }
207
208 Notification getFailureNotification(@PluralsRes int titleId, @DrawableRes int icon) {
Tomasz Mikolajewskicd270152016-02-01 12:01:14 +0900209 final Intent navigateIntent = buildNavigateIntent(INTENT_TAG_FAILURE);
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +0900210 navigateIntent.putExtra(EXTRA_DIALOG_TYPE, OperationDialogFragment.DIALOG_TYPE_FAILURE);
Steve McKayecbf3c52016-01-13 17:17:39 -0800211 navigateIntent.putExtra(EXTRA_OPERATION, operationType);
Steve McKayecbf3c52016-01-13 17:17:39 -0800212 navigateIntent.putParcelableArrayListExtra(EXTRA_SRC_LIST, failedFiles);
Steve McKay14e827a2016-01-06 18:32:13 -0800213
Steve McKayecbf3c52016-01-13 17:17:39 -0800214 final Notification.Builder errorBuilder = new Notification.Builder(service)
215 .setContentTitle(service.getResources().getQuantityString(titleId,
Steve McKay14e827a2016-01-06 18:32:13 -0800216 failedFiles.size(), failedFiles.size()))
Steve McKayecbf3c52016-01-13 17:17:39 -0800217 .setContentText(service.getString(R.string.notification_touch_for_details))
Steve McKay14e827a2016-01-06 18:32:13 -0800218 .setContentIntent(PendingIntent.getActivity(appContext, 0, navigateIntent,
219 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT))
220 .setCategory(Notification.CATEGORY_ERROR)
221 .setSmallIcon(icon)
222 .setAutoCancel(true);
Steve McKay7a3b8112016-02-23 10:06:50 -0800223
Steve McKay14e827a2016-01-06 18:32:13 -0800224 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}