blob: 7b8011a3601bc859440fa3cec395a53a951295fe [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;
23import static com.android.documentsui.services.FileOperationService.EXTRA_OPERATION;
24import 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;
26import static com.android.internal.util.Preconditions.checkArgument;
Steve McKay97b4be42016-01-20 15:09:35 -080027import static com.android.internal.util.Preconditions.checkNotNull;
Steve McKayc83baa02016-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 McKay97b4be42016-01-20 15:09:35 -080034import android.content.ContentProviderClient;
Steve McKayc83baa02016-01-06 18:32:13 -080035import android.content.ContentResolver;
36import android.content.Context;
37import android.content.Intent;
38import 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
43import com.android.documentsui.FilesActivity;
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +090044import com.android.documentsui.OperationDialogFragment;
Steve McKayc83baa02016-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 McKay97b4be42016-01-20 15:09:35 -080052import java.util.HashMap;
Steve McKaybbeba522016-01-13 17:17:39 -080053import java.util.List;
Steve McKay97b4be42016-01-20 15:09:35 -080054import java.util.Map;
Steve McKayc83baa02016-01-06 18:32:13 -080055
Steve McKaybbeba522016-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 Mikolajewskidd2b31c2016-01-22 16:22:51 +090060abstract public class Job implements Runnable {
Steve McKay97b4be42016-01-20 15:09:35 -080061 private static final String TAG = "Job";
Steve McKaybbeba522016-01-13 17:17:39 -080062 final Context service;
Steve McKayc83baa02016-01-06 18:32:13 -080063 final Context appContext;
64 final Listener listener;
65
Steve McKaybbeba522016-01-13 17:17:39 -080066 final @OpType int operationType;
Steve McKayc83baa02016-01-06 18:32:13 -080067 final String id;
68 final DocumentStack stack;
69
70 final ArrayList<DocumentInfo> failedFiles = new ArrayList<>();
71 final Notification.Builder mProgressBuilder;
72
Steve McKay97b4be42016-01-20 15:09:35 -080073 private final Map<String, ContentProviderClient> mClients = new HashMap<>();
Steve McKayc83baa02016-01-06 18:32:13 -080074 private volatile boolean mCanceled;
75
76 /**
77 * A simple progressable job, much like an AsyncTask, but with support
78 * for providing various related notification, progress and navigation information.
Steve McKaybbeba522016-01-13 17:17:39 -080079 * @param operationType
Steve McKayc83baa02016-01-06 18:32:13 -080080 *
Steve McKaybbeba522016-01-13 17:17:39 -080081 * @param service The service context in which this job is running.
Steve McKayc83baa02016-01-06 18:32:13 -080082 * @param appContext The context of the invoking application. This is usually
83 * just {@code getApplicationContext()}.
84 * @param listener
85 * @param id Arbitrary string ID
86 * @param stack The documents stack context relating to this request. This is the
87 * destination in the Files app where the user will be take when the
88 * navigation intent is invoked (presumably from notification).
89 */
Steve McKaybbeba522016-01-13 17:17:39 -080090 Job(Context service, Context appContext, Listener listener,
91 @OpType int operationType, String id, DocumentStack stack) {
Steve McKayc83baa02016-01-06 18:32:13 -080092
Steve McKaybbeba522016-01-13 17:17:39 -080093 checkArgument(operationType != OPERATION_UNKNOWN);
94
95 this.service = service;
Steve McKayc83baa02016-01-06 18:32:13 -080096 this.appContext = appContext;
97 this.listener = listener;
Steve McKaybbeba522016-01-13 17:17:39 -080098 this.operationType = operationType;
Steve McKayc83baa02016-01-06 18:32:13 -080099
100 this.id = id;
101 this.stack = stack;
102
103 mProgressBuilder = createProgressBuilder();
104 }
105
Steve McKaybbeba522016-01-13 17:17:39 -0800106 @Override
107 public final void run() {
108 listener.onStart(this);
109 try {
110 start();
111 } catch (Exception e) {
112 // In the case of an unmanaged failure, we still want
113 // to resolve business in an orderly fashion. That'll
114 // ensure the service is shut down and notifications
115 // shown/closed.
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +0900116 Log.e(TAG, "Operation failed due to an exception.", e);
Steve McKaybbeba522016-01-13 17:17:39 -0800117 } finally {
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +0900118 listener.onFinished(this);
Steve McKaybbeba522016-01-13 17:17:39 -0800119 }
Steve McKayc83baa02016-01-06 18:32:13 -0800120 }
121
Steve McKaybbeba522016-01-13 17:17:39 -0800122 abstract void start() throws RemoteException;
123
Steve McKayc83baa02016-01-06 18:32:13 -0800124 abstract Notification getSetupNotification();
125 // TODO: Progress notification for deletes.
126 // abstract Notification getProgressNotification(long bytesCopied);
127 abstract Notification getFailureNotification();
128
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +0900129 abstract Notification getWarningNotification();
130
Steve McKay97b4be42016-01-20 15:09:35 -0800131 ContentProviderClient getClient(DocumentInfo doc) throws RemoteException {
132 ContentProviderClient client = mClients.get(doc.authority);
133 if (client == null) {
134 // Acquire content providers.
135 client = acquireUnstableProviderOrThrow(
136 getContentResolver(),
137 doc.authority);
138
139 mClients.put(doc.authority, client);
140 }
141
142 return checkNotNull(client);
143 }
144
145 final void cleanup() {
146 for (ContentProviderClient client : mClients.values()) {
147 ContentProviderClient.releaseQuietly(client);
148 }
149 }
150
Steve McKayc83baa02016-01-06 18:32:13 -0800151 final void cancel() {
152 mCanceled = true;
153 }
154
155 final boolean isCanceled() {
156 return mCanceled;
157 }
158
159 final ContentResolver getContentResolver() {
Steve McKaybbeba522016-01-13 17:17:39 -0800160 return service.getContentResolver();
Steve McKayc83baa02016-01-06 18:32:13 -0800161 }
162
163 void onFileFailed(DocumentInfo file) {
164 failedFiles.add(file);
165 }
166
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +0900167 final boolean hasFailures() {
Steve McKayc83baa02016-01-06 18:32:13 -0800168 return !failedFiles.isEmpty();
169 }
170
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +0900171 boolean hasWarnings() {
172 return false;
173 }
174
Steve McKay97b4be42016-01-20 15:09:35 -0800175 final boolean deleteDocument(DocumentInfo doc) {
176 try {
177 DocumentsContract.deleteDocument(getClient(doc), doc.derivedUri);
178 } catch (RemoteException e) {
179 Log.w(TAG, "Failed to delete file: " + doc.derivedUri, e);
180 return false;
181 }
182
183 return true; // victory dance!
184 }
185
Steve McKayc83baa02016-01-06 18:32:13 -0800186 Notification getSetupNotification(String content) {
187 mProgressBuilder.setProgress(0, 0, true);
188 mProgressBuilder.setContentText(content);
189 return mProgressBuilder.build();
190 }
191
192 Notification getFailureNotification(@PluralsRes int titleId, @DrawableRes int icon) {
193 final Intent navigateIntent = buildNavigateIntent();
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +0900194 navigateIntent.putExtra(EXTRA_DIALOG_TYPE, OperationDialogFragment.DIALOG_TYPE_FAILURE);
Steve McKaybbeba522016-01-13 17:17:39 -0800195 navigateIntent.putExtra(EXTRA_OPERATION, operationType);
Steve McKayc83baa02016-01-06 18:32:13 -0800196
Steve McKaybbeba522016-01-13 17:17:39 -0800197 navigateIntent.putParcelableArrayListExtra(EXTRA_SRC_LIST, failedFiles);
Steve McKayc83baa02016-01-06 18:32:13 -0800198
Steve McKaybbeba522016-01-13 17:17:39 -0800199 final Notification.Builder errorBuilder = new Notification.Builder(service)
200 .setContentTitle(service.getResources().getQuantityString(titleId,
Steve McKayc83baa02016-01-06 18:32:13 -0800201 failedFiles.size(), failedFiles.size()))
Steve McKaybbeba522016-01-13 17:17:39 -0800202 .setContentText(service.getString(R.string.notification_touch_for_details))
Steve McKayc83baa02016-01-06 18:32:13 -0800203 .setContentIntent(PendingIntent.getActivity(appContext, 0, navigateIntent,
204 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT))
205 .setCategory(Notification.CATEGORY_ERROR)
206 .setSmallIcon(icon)
207 .setAutoCancel(true);
208 return errorBuilder.build();
209 }
210
211 abstract Builder createProgressBuilder();
212
213 final Builder createProgressBuilder(
214 String title, @DrawableRes int icon,
215 String actionTitle, @DrawableRes int actionIcon) {
Steve McKaybbeba522016-01-13 17:17:39 -0800216 Notification.Builder progressBuilder = new Notification.Builder(service)
Steve McKayc83baa02016-01-06 18:32:13 -0800217 .setContentTitle(title)
218 .setContentIntent(
219 PendingIntent.getActivity(appContext, 0, buildNavigateIntent(), 0))
220 .setCategory(Notification.CATEGORY_PROGRESS)
221 .setSmallIcon(icon)
222 .setOngoing(true);
223
224 final Intent cancelIntent = createCancelIntent();
225
226 progressBuilder.addAction(
227 actionIcon,
228 actionTitle,
229 PendingIntent.getService(
Steve McKaybbeba522016-01-13 17:17:39 -0800230 service,
Steve McKayc83baa02016-01-06 18:32:13 -0800231 0,
232 cancelIntent,
233 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT));
234
235 return progressBuilder;
236 }
237
238 /**
239 * Creates an intent for navigating back to the destination directory.
240 */
241 Intent buildNavigateIntent() {
Steve McKaybbeba522016-01-13 17:17:39 -0800242 Intent intent = new Intent(service, FilesActivity.class);
Steve McKayc83baa02016-01-06 18:32:13 -0800243 intent.setAction(DocumentsContract.ACTION_BROWSE);
244 intent.putExtra(Shared.EXTRA_STACK, (Parcelable) stack);
245 return intent;
246 }
247
248 Intent createCancelIntent() {
Steve McKaybbeba522016-01-13 17:17:39 -0800249 final Intent cancelIntent = new Intent(service, FileOperationService.class);
250 cancelIntent.putExtra(EXTRA_CANCEL, true);
251 cancelIntent.putExtra(EXTRA_JOB_ID, id);
Steve McKayc83baa02016-01-06 18:32:13 -0800252 return cancelIntent;
253 }
254
Steve McKay97b4be42016-01-20 15:09:35 -0800255 @Override
256 public String toString() {
257 return new StringBuilder()
258 .append("Job")
259 .append("{")
260 .append("id=" + id)
261 .append("}")
262 .toString();
263 }
264
Steve McKaybbeba522016-01-13 17:17:39 -0800265 /**
266 * Factory class that facilitates our testing FileOperationService.
267 */
268 static class Factory {
269
270 static final Factory instance = new Factory();
271
272 Job createCopy(Context service, Context appContext, Listener listener,
273 String id, DocumentStack stack, List<DocumentInfo> srcs) {
274 return new CopyJob(service, appContext, listener, id, stack, srcs);
275 }
276
277 Job createMove(Context service, Context appContext, Listener listener,
Tomasz Mikolajewskie0094412016-01-25 16:20:15 +0900278 String id, DocumentStack stack, List<DocumentInfo> srcs,
279 DocumentInfo srcParent) {
280 return new MoveJob(service, appContext, listener, id, stack, srcs, srcParent);
Steve McKaybbeba522016-01-13 17:17:39 -0800281 }
Steve McKay97b4be42016-01-20 15:09:35 -0800282
283 Job createDelete(Context service, Context appContext, Listener listener,
Tomasz Mikolajewskie0094412016-01-25 16:20:15 +0900284 String id, DocumentStack stack, List<DocumentInfo> srcs,
285 DocumentInfo srcParent) {
286 return new DeleteJob(service, appContext, listener, id, stack, srcs, srcParent);
Steve McKay97b4be42016-01-20 15:09:35 -0800287 }
Steve McKaybbeba522016-01-13 17:17:39 -0800288 }
289
290 /**
291 * Listener interface employed by the service that owns us as well as tests.
292 */
Steve McKayc83baa02016-01-06 18:32:13 -0800293 interface Listener {
Steve McKaybbeba522016-01-13 17:17:39 -0800294 void onStart(Job job);
Steve McKaybbeba522016-01-13 17:17:39 -0800295 void onFinished(Job job);
Steve McKayc83baa02016-01-06 18:32:13 -0800296 void onProgress(CopyJob job);
Steve McKayc83baa02016-01-06 18:32:13 -0800297 }
298}