blob: c7939ebd8b185035f68db89045ce643f6ebfe0f3 [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 McKaybbeba522016-01-13 17:17:39 -080019import static com.android.documentsui.services.FileOperationService.EXTRA_CANCEL;
20import static com.android.documentsui.services.FileOperationService.EXTRA_FAILURE;
21import static com.android.documentsui.services.FileOperationService.EXTRA_JOB_ID;
22import static com.android.documentsui.services.FileOperationService.EXTRA_OPERATION;
23import static com.android.documentsui.services.FileOperationService.EXTRA_SRC_LIST;
24import static com.android.documentsui.services.FileOperationService.FAILURE_COPY;
Steve McKayc83baa02016-01-06 18:32:13 -080025import static com.android.documentsui.services.FileOperationService.OPERATION_UNKNOWN;
26import static com.android.internal.util.Preconditions.checkArgument;
27
28import android.annotation.DrawableRes;
29import android.annotation.PluralsRes;
30import android.app.Notification;
31import android.app.Notification.Builder;
32import android.app.PendingIntent;
33import android.content.ContentResolver;
34import android.content.Context;
35import android.content.Intent;
36import android.os.Parcelable;
37import android.os.RemoteException;
38import android.provider.DocumentsContract;
39
40import com.android.documentsui.FilesActivity;
41import com.android.documentsui.R;
42import com.android.documentsui.Shared;
43import com.android.documentsui.model.DocumentInfo;
44import com.android.documentsui.model.DocumentStack;
45import com.android.documentsui.services.FileOperationService.OpType;
46
47import java.util.ArrayList;
Steve McKaybbeba522016-01-13 17:17:39 -080048import java.util.List;
Steve McKayc83baa02016-01-06 18:32:13 -080049
Steve McKaybbeba522016-01-13 17:17:39 -080050/**
51 * A mashup of work item and ui progress update factory. Used by {@link FileOperationService}
52 * to do work and show progress relating to this work.
53 */
54abstract class Job implements Runnable {
Steve McKayc83baa02016-01-06 18:32:13 -080055
Steve McKaybbeba522016-01-13 17:17:39 -080056 final Context service;
Steve McKayc83baa02016-01-06 18:32:13 -080057 final Context appContext;
58 final Listener listener;
59
Steve McKaybbeba522016-01-13 17:17:39 -080060 final @OpType int operationType;
Steve McKayc83baa02016-01-06 18:32:13 -080061 final String id;
62 final DocumentStack stack;
63
64 final ArrayList<DocumentInfo> failedFiles = new ArrayList<>();
65 final Notification.Builder mProgressBuilder;
66
67 private volatile boolean mCanceled;
68
69 /**
70 * A simple progressable job, much like an AsyncTask, but with support
71 * for providing various related notification, progress and navigation information.
Steve McKaybbeba522016-01-13 17:17:39 -080072 * @param operationType
Steve McKayc83baa02016-01-06 18:32:13 -080073 *
Steve McKaybbeba522016-01-13 17:17:39 -080074 * @param service The service context in which this job is running.
Steve McKayc83baa02016-01-06 18:32:13 -080075 * @param appContext The context of the invoking application. This is usually
76 * just {@code getApplicationContext()}.
77 * @param listener
78 * @param id Arbitrary string ID
79 * @param stack The documents stack context relating to this request. This is the
80 * destination in the Files app where the user will be take when the
81 * navigation intent is invoked (presumably from notification).
82 */
Steve McKaybbeba522016-01-13 17:17:39 -080083 Job(Context service, Context appContext, Listener listener,
84 @OpType int operationType, String id, DocumentStack stack) {
Steve McKayc83baa02016-01-06 18:32:13 -080085
Steve McKaybbeba522016-01-13 17:17:39 -080086 checkArgument(operationType != OPERATION_UNKNOWN);
87
88 this.service = service;
Steve McKayc83baa02016-01-06 18:32:13 -080089 this.appContext = appContext;
90 this.listener = listener;
Steve McKaybbeba522016-01-13 17:17:39 -080091 this.operationType = operationType;
Steve McKayc83baa02016-01-06 18:32:13 -080092
93 this.id = id;
94 this.stack = stack;
95
96 mProgressBuilder = createProgressBuilder();
97 }
98
Steve McKaybbeba522016-01-13 17:17:39 -080099 @Override
100 public final void run() {
101 listener.onStart(this);
102 try {
103 start();
104 } catch (Exception e) {
105 // In the case of an unmanaged failure, we still want
106 // to resolve business in an orderly fashion. That'll
107 // ensure the service is shut down and notifications
108 // shown/closed.
109 listener.onFailed(this);
110 } finally {
111 if (failed()) {
112 listener.onFailed(this);
113 } else {
114 listener.onFinished(this);
115 }
116 }
Steve McKayc83baa02016-01-06 18:32:13 -0800117 }
118
Steve McKaybbeba522016-01-13 17:17:39 -0800119 abstract void start() throws RemoteException;
120
121 // Service will call this when it is done with the job.
122 abstract void cleanup();
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
129 final void cancel() {
130 mCanceled = true;
131 }
132
133 final boolean isCanceled() {
134 return mCanceled;
135 }
136
137 final ContentResolver getContentResolver() {
Steve McKaybbeba522016-01-13 17:17:39 -0800138 return service.getContentResolver();
Steve McKayc83baa02016-01-06 18:32:13 -0800139 }
140
141 void onFileFailed(DocumentInfo file) {
142 failedFiles.add(file);
143 }
144
145 final boolean failed() {
146 return !failedFiles.isEmpty();
147 }
148
149 Notification getSetupNotification(String content) {
150 mProgressBuilder.setProgress(0, 0, true);
151 mProgressBuilder.setContentText(content);
152 return mProgressBuilder.build();
153 }
154
155 Notification getFailureNotification(@PluralsRes int titleId, @DrawableRes int icon) {
156 final Intent navigateIntent = buildNavigateIntent();
Steve McKaybbeba522016-01-13 17:17:39 -0800157 navigateIntent.putExtra(EXTRA_FAILURE, FAILURE_COPY);
158 navigateIntent.putExtra(EXTRA_OPERATION, operationType);
Steve McKayc83baa02016-01-06 18:32:13 -0800159
Steve McKaybbeba522016-01-13 17:17:39 -0800160 navigateIntent.putParcelableArrayListExtra(EXTRA_SRC_LIST, failedFiles);
Steve McKayc83baa02016-01-06 18:32:13 -0800161
Steve McKaybbeba522016-01-13 17:17:39 -0800162 final Notification.Builder errorBuilder = new Notification.Builder(service)
163 .setContentTitle(service.getResources().getQuantityString(titleId,
Steve McKayc83baa02016-01-06 18:32:13 -0800164 failedFiles.size(), failedFiles.size()))
Steve McKaybbeba522016-01-13 17:17:39 -0800165 .setContentText(service.getString(R.string.notification_touch_for_details))
Steve McKayc83baa02016-01-06 18:32:13 -0800166 .setContentIntent(PendingIntent.getActivity(appContext, 0, navigateIntent,
167 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT))
168 .setCategory(Notification.CATEGORY_ERROR)
169 .setSmallIcon(icon)
170 .setAutoCancel(true);
171 return errorBuilder.build();
172 }
173
174 abstract Builder createProgressBuilder();
175
176 final Builder createProgressBuilder(
177 String title, @DrawableRes int icon,
178 String actionTitle, @DrawableRes int actionIcon) {
Steve McKaybbeba522016-01-13 17:17:39 -0800179 Notification.Builder progressBuilder = new Notification.Builder(service)
Steve McKayc83baa02016-01-06 18:32:13 -0800180 .setContentTitle(title)
181 .setContentIntent(
182 PendingIntent.getActivity(appContext, 0, buildNavigateIntent(), 0))
183 .setCategory(Notification.CATEGORY_PROGRESS)
184 .setSmallIcon(icon)
185 .setOngoing(true);
186
187 final Intent cancelIntent = createCancelIntent();
188
189 progressBuilder.addAction(
190 actionIcon,
191 actionTitle,
192 PendingIntent.getService(
Steve McKaybbeba522016-01-13 17:17:39 -0800193 service,
Steve McKayc83baa02016-01-06 18:32:13 -0800194 0,
195 cancelIntent,
196 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT));
197
198 return progressBuilder;
199 }
200
201 /**
202 * Creates an intent for navigating back to the destination directory.
203 */
204 Intent buildNavigateIntent() {
Steve McKaybbeba522016-01-13 17:17:39 -0800205 Intent intent = new Intent(service, FilesActivity.class);
Steve McKayc83baa02016-01-06 18:32:13 -0800206 intent.setAction(DocumentsContract.ACTION_BROWSE);
207 intent.putExtra(Shared.EXTRA_STACK, (Parcelable) stack);
208 return intent;
209 }
210
211 Intent createCancelIntent() {
Steve McKaybbeba522016-01-13 17:17:39 -0800212 final Intent cancelIntent = new Intent(service, FileOperationService.class);
213 cancelIntent.putExtra(EXTRA_CANCEL, true);
214 cancelIntent.putExtra(EXTRA_JOB_ID, id);
Steve McKayc83baa02016-01-06 18:32:13 -0800215 return cancelIntent;
216 }
217
Steve McKaybbeba522016-01-13 17:17:39 -0800218 /**
219 * Factory class that facilitates our testing FileOperationService.
220 */
221 static class Factory {
222
223 static final Factory instance = new Factory();
224
225 Job createCopy(Context service, Context appContext, Listener listener,
226 String id, DocumentStack stack, List<DocumentInfo> srcs) {
227 return new CopyJob(service, appContext, listener, id, stack, srcs);
228 }
229
230 Job createMove(Context service, Context appContext, Listener listener,
231 String id, DocumentStack stack, List<DocumentInfo> srcs) {
232 return new MoveJob(service, appContext, listener, id, stack, srcs);
233 }
234 }
235
236 /**
237 * Listener interface employed by the service that owns us as well as tests.
238 */
Steve McKayc83baa02016-01-06 18:32:13 -0800239 interface Listener {
Steve McKaybbeba522016-01-13 17:17:39 -0800240 void onStart(Job job);
241 void onFailed(Job job);
242 void onFinished(Job job);
Steve McKayc83baa02016-01-06 18:32:13 -0800243 void onProgress(CopyJob job);
Steve McKayc83baa02016-01-06 18:32:13 -0800244 }
245}