blob: b6f83df6312fda3ddaa0516193caf3b2119cd4bc [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
Jeff Sharkey2c0b4852019-02-15 15:53:47 -070019import static android.content.ContentResolver.wrap;
20
Steve McKay97b4be42016-01-20 15:09:35 -080021import static com.android.documentsui.DocumentsApplication.acquireUnstableProviderOrThrow;
Steve McKaybbeba522016-01-13 17:17:39 -080022import static com.android.documentsui.services.FileOperationService.EXTRA_CANCEL;
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +090023import static com.android.documentsui.services.FileOperationService.EXTRA_DIALOG_TYPE;
Steve McKay99f1dc32016-12-29 16:02:01 -080024import static com.android.documentsui.services.FileOperationService.EXTRA_FAILED_DOCS;
25import static com.android.documentsui.services.FileOperationService.EXTRA_FAILED_URIS;
Steve McKaybbeba522016-01-13 17:17:39 -080026import static com.android.documentsui.services.FileOperationService.EXTRA_JOB_ID;
Garfield, Tan48334772016-06-28 17:17:38 -070027import static com.android.documentsui.services.FileOperationService.EXTRA_OPERATION_TYPE;
Steve McKayc83baa02016-01-06 18:32:13 -080028import static com.android.documentsui.services.FileOperationService.OPERATION_UNKNOWN;
Steve McKayc83baa02016-01-06 18:32:13 -080029
Jeff Sharkeya4ff00f2018-07-09 14:57:51 -060030import androidx.annotation.DrawableRes;
31import androidx.annotation.IntDef;
32import androidx.annotation.PluralsRes;
Steve McKayc83baa02016-01-06 18:32:13 -080033import android.app.Notification;
34import android.app.Notification.Builder;
35import android.app.PendingIntent;
Steve McKay97b4be42016-01-20 15:09:35 -080036import android.content.ContentProviderClient;
Steve McKayc83baa02016-01-06 18:32:13 -080037import android.content.ContentResolver;
38import android.content.Context;
39import android.content.Intent;
Tomasz Mikolajewski63e2aae2016-02-01 12:01:14 +090040import android.net.Uri;
Jeff Sharkey2e3a6f62018-02-01 16:04:14 -070041import android.os.CancellationSignal;
Jeff Sharkeybb68a652019-02-19 11:17:30 -070042import android.os.FileUtils;
Steve McKayc83baa02016-01-06 18:32:13 -080043import android.os.Parcelable;
44import android.os.RemoteException;
45import android.provider.DocumentsContract;
Steve McKay97b4be42016-01-20 15:09:35 -080046import android.util.Log;
Steve McKayc83baa02016-01-06 18:32:13 -080047
Ben Kwafaa27202016-01-28 16:39:57 -080048import com.android.documentsui.Metrics;
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +090049import com.android.documentsui.OperationDialogFragment;
Steve McKayc83baa02016-01-06 18:32:13 -080050import com.android.documentsui.R;
Steve McKayd0805062016-09-15 14:30:38 -070051import com.android.documentsui.base.DocumentInfo;
52import com.android.documentsui.base.DocumentStack;
Garfield Tan3a968cd2017-04-25 10:30:45 -070053import com.android.documentsui.base.Features;
Steve McKayd9caa6a2016-09-15 16:36:45 -070054import com.android.documentsui.base.Shared;
Steve McKay99f1dc32016-12-29 16:02:01 -080055import com.android.documentsui.clipping.UrisSupplier;
56import com.android.documentsui.files.FilesActivity;
Steve McKayc83baa02016-01-06 18:32:13 -080057import com.android.documentsui.services.FileOperationService.OpType;
58
Jeff Sharkey844989e2018-12-07 15:16:08 -070059import java.io.FileNotFoundException;
Garfield, Tan48ef36f2016-06-09 12:04:22 -070060import java.lang.annotation.Retention;
61import java.lang.annotation.RetentionPolicy;
Steve McKayc83baa02016-01-06 18:32:13 -080062import java.util.ArrayList;
Steve McKay97b4be42016-01-20 15:09:35 -080063import java.util.HashMap;
Steve McKay97b4be42016-01-20 15:09:35 -080064import java.util.Map;
Steve McKayc83baa02016-01-06 18:32:13 -080065
Steve McKay99f1dc32016-12-29 16:02:01 -080066import javax.annotation.Nullable;
67
Steve McKaybbeba522016-01-13 17:17:39 -080068/**
69 * A mashup of work item and ui progress update factory. Used by {@link FileOperationService}
70 * to do work and show progress relating to this work.
71 */
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +090072abstract public class Job implements Runnable {
Steve McKay97b4be42016-01-20 15:09:35 -080073 private static final String TAG = "Job";
Tomasz Mikolajewski63e2aae2016-02-01 12:01:14 +090074
Garfield, Tan48ef36f2016-06-09 12:04:22 -070075 @Retention(RetentionPolicy.SOURCE)
Garfield, Tanedce5542016-06-17 15:32:28 -070076 @IntDef({STATE_CREATED, STATE_STARTED, STATE_SET_UP, STATE_COMPLETED, STATE_CANCELED})
Garfield, Tan48ef36f2016-06-09 12:04:22 -070077 @interface State {}
78 static final int STATE_CREATED = 0;
79 static final int STATE_STARTED = 1;
Garfield, Tanedce5542016-06-17 15:32:28 -070080 static final int STATE_SET_UP = 2;
81 static final int STATE_COMPLETED = 3;
Garfield, Tan48ef36f2016-06-09 12:04:22 -070082 /**
83 * A job is in canceled state as long as {@link #cancel()} is called on it, even after it is
84 * completed.
85 */
Garfield, Tanedce5542016-06-17 15:32:28 -070086 static final int STATE_CANCELED = 4;
Garfield, Tan48ef36f2016-06-09 12:04:22 -070087
Tomasz Mikolajewski63e2aae2016-02-01 12:01:14 +090088 static final String INTENT_TAG_WARNING = "warning";
89 static final String INTENT_TAG_FAILURE = "failure";
90 static final String INTENT_TAG_PROGRESS = "progress";
91 static final String INTENT_TAG_CANCEL = "cancel";
92
Steve McKaybbeba522016-01-13 17:17:39 -080093 final Context service;
Steve McKayc83baa02016-01-06 18:32:13 -080094 final Context appContext;
95 final Listener listener;
96
Steve McKaybbeba522016-01-13 17:17:39 -080097 final @OpType int operationType;
Steve McKayc83baa02016-01-06 18:32:13 -080098 final String id;
99 final DocumentStack stack;
100
Steve McKay99f1dc32016-12-29 16:02:01 -0800101 final UrisSupplier mResourceUris;
102
103 int failureCount = 0;
104 final ArrayList<DocumentInfo> failedDocs = new ArrayList<>();
105 final ArrayList<Uri> failedUris = new ArrayList<>();
106
Steve McKayc83baa02016-01-06 18:32:13 -0800107 final Notification.Builder mProgressBuilder;
108
Jeff Sharkey2e3a6f62018-02-01 16:04:14 -0700109 final CancellationSignal mSignal = new CancellationSignal();
110
Steve McKay97b4be42016-01-20 15:09:35 -0800111 private final Map<String, ContentProviderClient> mClients = new HashMap<>();
Garfield Tan3a968cd2017-04-25 10:30:45 -0700112 private final Features mFeatures;
113
Garfield, Tan48ef36f2016-06-09 12:04:22 -0700114 private volatile @State int mState = STATE_CREATED;
Steve McKayc83baa02016-01-06 18:32:13 -0800115
116 /**
117 * A simple progressable job, much like an AsyncTask, but with support
118 * for providing various related notification, progress and navigation information.
Steve McKaybbeba522016-01-13 17:17:39 -0800119 * @param service The service context in which this job is running.
Steve McKayc83baa02016-01-06 18:32:13 -0800120 * @param listener
121 * @param id Arbitrary string ID
122 * @param stack The documents stack context relating to this request. This is the
123 * destination in the Files app where the user will be take when the
124 * navigation intent is invoked (presumably from notification).
Garfield, Tan48334772016-06-28 17:17:38 -0700125 * @param srcs the list of docs to operate on
Steve McKayc83baa02016-01-06 18:32:13 -0800126 */
Garfield, Tan48334772016-06-28 17:17:38 -0700127 Job(Context service, Listener listener, String id,
Garfield Tan3a968cd2017-04-25 10:30:45 -0700128 @OpType int opType, DocumentStack stack, UrisSupplier srcs, Features features) {
Steve McKayc83baa02016-01-06 18:32:13 -0800129
Garfield, Tan48334772016-06-28 17:17:38 -0700130 assert(opType != OPERATION_UNKNOWN);
Steve McKaybbeba522016-01-13 17:17:39 -0800131
132 this.service = service;
Garfield, Tan48334772016-06-28 17:17:38 -0700133 this.appContext = service.getApplicationContext();
Steve McKayc83baa02016-01-06 18:32:13 -0800134 this.listener = listener;
Garfield, Tan48334772016-06-28 17:17:38 -0700135 this.operationType = opType;
Steve McKayc83baa02016-01-06 18:32:13 -0800136
137 this.id = id;
138 this.stack = stack;
Steve McKay99f1dc32016-12-29 16:02:01 -0800139 this.mResourceUris = srcs;
Steve McKayc83baa02016-01-06 18:32:13 -0800140
Garfield Tan3a968cd2017-04-25 10:30:45 -0700141 mFeatures = features;
142
Steve McKayc83baa02016-01-06 18:32:13 -0800143 mProgressBuilder = createProgressBuilder();
144 }
145
Steve McKaybbeba522016-01-13 17:17:39 -0800146 @Override
147 public final void run() {
Garfield, Tan48ef36f2016-06-09 12:04:22 -0700148 if (isCanceled()) {
149 // Canceled before running
150 return;
151 }
152
153 mState = STATE_STARTED;
Steve McKaybbeba522016-01-13 17:17:39 -0800154 listener.onStart(this);
Steve McKay99f1dc32016-12-29 16:02:01 -0800155
Steve McKaybbeba522016-01-13 17:17:39 -0800156 try {
Garfield, Tanedce5542016-06-17 15:32:28 -0700157 boolean result = setUp();
158 if (result && !isCanceled()) {
159 mState = STATE_SET_UP;
160 start();
161 }
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900162 } catch (RuntimeException e) {
163 // No exceptions should be thrown here, as all calls to the provider must be
164 // handled within Job implementations. However, just in case catch them here.
165 Log.e(TAG, "Operation failed due to an unhandled runtime exception.", e);
shawnlin9cee68f2019-01-25 11:20:18 +0800166 Metrics.logFileOperationErrors(operationType, failedDocs, failedUris);
Steve McKaybbeba522016-01-13 17:17:39 -0800167 } finally {
Garfield, Tanedce5542016-06-17 15:32:28 -0700168 mState = (mState == STATE_STARTED || mState == STATE_SET_UP) ? STATE_COMPLETED : mState;
Tomasz Mikolajewskied11f1c2017-01-30 11:07:04 +0900169 finish();
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +0900170 listener.onFinished(this);
Garfield, Tanedce5542016-06-17 15:32:28 -0700171
172 // NOTE: If this details is a JumboClipDetails, and it's still referred in primary clip
173 // at this point, user won't be able to paste it to anywhere else because the underlying
Steve McKay99f1dc32016-12-29 16:02:01 -0800174 mResourceUris.dispose();
Steve McKaybbeba522016-01-13 17:17:39 -0800175 }
Steve McKayc83baa02016-01-06 18:32:13 -0800176 }
177
Garfield, Tanedce5542016-06-17 15:32:28 -0700178 boolean setUp() {
179 return true;
180 }
Steve McKay99f1dc32016-12-29 16:02:01 -0800181
Søren Gjesse09bd5bc2017-12-01 10:48:17 +0100182 abstract void finish();
Steve McKaybbeba522016-01-13 17:17:39 -0800183
Tomasz Mikolajewskied11f1c2017-01-30 11:07:04 +0900184 abstract void start();
Steve McKayc83baa02016-01-06 18:32:13 -0800185 abstract Notification getSetupNotification();
Garfield, Tan48ef36f2016-06-09 12:04:22 -0700186 abstract Notification getProgressNotification();
Steve McKayc83baa02016-01-06 18:32:13 -0800187 abstract Notification getFailureNotification();
188
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +0900189 abstract Notification getWarningNotification();
190
Tomasz Mikolajewski63e2aae2016-02-01 12:01:14 +0900191 Uri getDataUriForIntent(String tag) {
192 return Uri.parse(String.format("data,%s-%s", tag, id));
193 }
194
Tomasz Mikolajewskia0115862017-03-02 15:42:08 +0900195 ContentProviderClient getClient(Uri uri) throws RemoteException {
196 ContentProviderClient client = mClients.get(uri.getAuthority());
Steve McKay97b4be42016-01-20 15:09:35 -0800197 if (client == null) {
198 // Acquire content providers.
199 client = acquireUnstableProviderOrThrow(
200 getContentResolver(),
Tomasz Mikolajewskia0115862017-03-02 15:42:08 +0900201 uri.getAuthority());
Steve McKay97b4be42016-01-20 15:09:35 -0800202
Tomasz Mikolajewskia0115862017-03-02 15:42:08 +0900203 mClients.put(uri.getAuthority(), client);
Steve McKay97b4be42016-01-20 15:09:35 -0800204 }
205
Steve McKay0af8afd2016-02-25 13:34:03 -0800206 assert(client != null);
207 return client;
Steve McKay97b4be42016-01-20 15:09:35 -0800208 }
209
Tomasz Mikolajewskia0115862017-03-02 15:42:08 +0900210 ContentProviderClient getClient(DocumentInfo doc) throws RemoteException {
211 return getClient(doc.derivedUri);
212 }
213
Steve McKay97b4be42016-01-20 15:09:35 -0800214 final void cleanup() {
215 for (ContentProviderClient client : mClients.values()) {
Jeff Sharkeybb68a652019-02-19 11:17:30 -0700216 FileUtils.closeQuietly(client);
Steve McKay97b4be42016-01-20 15:09:35 -0800217 }
218 }
219
Garfield, Tan48ef36f2016-06-09 12:04:22 -0700220 final @State int getState() {
221 return mState;
222 }
223
Steve McKayc83baa02016-01-06 18:32:13 -0800224 final void cancel() {
Garfield, Tan48ef36f2016-06-09 12:04:22 -0700225 mState = STATE_CANCELED;
Jeff Sharkey2e3a6f62018-02-01 16:04:14 -0700226 mSignal.cancel();
shawnlin9cee68f2019-01-25 11:20:18 +0800227 Metrics.logFileOperationCancelled(operationType);
Steve McKayc83baa02016-01-06 18:32:13 -0800228 }
229
230 final boolean isCanceled() {
Garfield, Tan48ef36f2016-06-09 12:04:22 -0700231 return mState == STATE_CANCELED;
232 }
233
234 final boolean isFinished() {
235 return mState == STATE_CANCELED || mState == STATE_COMPLETED;
Steve McKayc83baa02016-01-06 18:32:13 -0800236 }
237
238 final ContentResolver getContentResolver() {
Steve McKaybbeba522016-01-13 17:17:39 -0800239 return service.getContentResolver();
Steve McKayc83baa02016-01-06 18:32:13 -0800240 }
241
242 void onFileFailed(DocumentInfo file) {
Steve McKay99f1dc32016-12-29 16:02:01 -0800243 failureCount++;
244 failedDocs.add(file);
245 }
246
247 void onResolveFailed(Uri uri) {
248 failureCount++;
249 failedUris.add(uri);
Steve McKayc83baa02016-01-06 18:32:13 -0800250 }
251
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +0900252 final boolean hasFailures() {
Steve McKay99f1dc32016-12-29 16:02:01 -0800253 return failureCount > 0;
Steve McKayc83baa02016-01-06 18:32:13 -0800254 }
255
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +0900256 boolean hasWarnings() {
257 return false;
258 }
259
Garfield Tan03a3a392016-12-12 14:06:45 -0800260 final void deleteDocument(DocumentInfo doc, @Nullable DocumentInfo parent)
261 throws ResourceException {
Steve McKay97b4be42016-01-20 15:09:35 -0800262 try {
Garfield Tan03a3a392016-12-12 14:06:45 -0800263 if (parent != null && doc.isRemoveSupported()) {
Jeff Sharkey2c0b4852019-02-15 15:53:47 -0700264 DocumentsContract.removeDocument(wrap(getClient(doc)), doc.derivedUri,
265 parent.derivedUri);
Tomasz Mikolajewski8fe54982016-02-23 15:12:54 +0900266 } else if (doc.isDeleteSupported()) {
Jeff Sharkey2c0b4852019-02-15 15:53:47 -0700267 DocumentsContract.deleteDocument(wrap(getClient(doc)), doc.derivedUri);
Tomasz Mikolajewski8fe54982016-02-23 15:12:54 +0900268 } else {
Steve McKay99f1dc32016-12-29 16:02:01 -0800269 throw new ResourceException("Unable to delete source document. "
270 + "File is not deletable or removable: %s.", doc.derivedUri);
Tomasz Mikolajewski8fe54982016-02-23 15:12:54 +0900271 }
Jeff Sharkey844989e2018-12-07 15:16:08 -0700272 } catch (FileNotFoundException | RemoteException | RuntimeException e) {
Tomasz Mikolajewskic5151502016-02-18 16:45:44 +0900273 throw new ResourceException("Failed to delete file %s due to an exception.",
274 doc.derivedUri, e);
Steve McKay97b4be42016-01-20 15:09:35 -0800275 }
Steve McKay97b4be42016-01-20 15:09:35 -0800276 }
277
Steve McKayc83baa02016-01-06 18:32:13 -0800278 Notification getSetupNotification(String content) {
Steve McKay003097d2016-02-23 10:06:50 -0800279 mProgressBuilder.setProgress(0, 0, true)
280 .setContentText(content);
Steve McKayc83baa02016-01-06 18:32:13 -0800281 return mProgressBuilder.build();
282 }
283
284 Notification getFailureNotification(@PluralsRes int titleId, @DrawableRes int icon) {
Tomasz Mikolajewski63e2aae2016-02-01 12:01:14 +0900285 final Intent navigateIntent = buildNavigateIntent(INTENT_TAG_FAILURE);
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +0900286 navigateIntent.putExtra(EXTRA_DIALOG_TYPE, OperationDialogFragment.DIALOG_TYPE_FAILURE);
Garfield, Tan48334772016-06-28 17:17:38 -0700287 navigateIntent.putExtra(EXTRA_OPERATION_TYPE, operationType);
Steve McKay99f1dc32016-12-29 16:02:01 -0800288 navigateIntent.putParcelableArrayListExtra(EXTRA_FAILED_DOCS, failedDocs);
289 navigateIntent.putParcelableArrayListExtra(EXTRA_FAILED_URIS, failedUris);
Steve McKayc83baa02016-01-06 18:32:13 -0800290
Garfield Tan3a968cd2017-04-25 10:30:45 -0700291 final Notification.Builder errorBuilder = createNotificationBuilder()
Steve McKaybbeba522016-01-13 17:17:39 -0800292 .setContentTitle(service.getResources().getQuantityString(titleId,
Steve McKay99f1dc32016-12-29 16:02:01 -0800293 failureCount, failureCount))
Steve McKaybbeba522016-01-13 17:17:39 -0800294 .setContentText(service.getString(R.string.notification_touch_for_details))
Steve McKayc83baa02016-01-06 18:32:13 -0800295 .setContentIntent(PendingIntent.getActivity(appContext, 0, navigateIntent,
296 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT))
297 .setCategory(Notification.CATEGORY_ERROR)
298 .setSmallIcon(icon)
299 .setAutoCancel(true);
Steve McKay003097d2016-02-23 10:06:50 -0800300
Steve McKayc83baa02016-01-06 18:32:13 -0800301 return errorBuilder.build();
302 }
303
304 abstract Builder createProgressBuilder();
305
306 final Builder createProgressBuilder(
307 String title, @DrawableRes int icon,
308 String actionTitle, @DrawableRes int actionIcon) {
Garfield Tan3a968cd2017-04-25 10:30:45 -0700309 Notification.Builder progressBuilder = createNotificationBuilder()
Steve McKayc83baa02016-01-06 18:32:13 -0800310 .setContentTitle(title)
311 .setContentIntent(
Tomasz Mikolajewski63e2aae2016-02-01 12:01:14 +0900312 PendingIntent.getActivity(appContext, 0,
313 buildNavigateIntent(INTENT_TAG_PROGRESS), 0))
Steve McKayc83baa02016-01-06 18:32:13 -0800314 .setCategory(Notification.CATEGORY_PROGRESS)
315 .setSmallIcon(icon)
316 .setOngoing(true);
317
318 final Intent cancelIntent = createCancelIntent();
319
320 progressBuilder.addAction(
321 actionIcon,
322 actionTitle,
323 PendingIntent.getService(
Steve McKaybbeba522016-01-13 17:17:39 -0800324 service,
Steve McKayc83baa02016-01-06 18:32:13 -0800325 0,
326 cancelIntent,
327 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT));
328
329 return progressBuilder;
330 }
331
Garfield Tan3a968cd2017-04-25 10:30:45 -0700332 Notification.Builder createNotificationBuilder() {
333 return mFeatures.isNotificationChannelEnabled()
334 ? new Notification.Builder(service, FileOperationService.NOTIFICATION_CHANNEL_ID)
335 : new Notification.Builder(service);
336 }
337
Steve McKayc83baa02016-01-06 18:32:13 -0800338 /**
339 * Creates an intent for navigating back to the destination directory.
340 */
Tomasz Mikolajewski63e2aae2016-02-01 12:01:14 +0900341 Intent buildNavigateIntent(String tag) {
Garfield Tanc5efea02017-02-22 12:58:29 -0800342 // TODO (b/35721285): Reuse an existing task rather than creating a new one every time.
Steve McKayb6006b22016-09-29 09:23:45 -0700343 Intent intent = new Intent(service, FilesActivity.class);
Tomasz Mikolajewski63e2aae2016-02-01 12:01:14 +0900344 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Tomasz Mikolajewski63e2aae2016-02-01 12:01:14 +0900345 intent.setData(getDataUriForIntent(tag));
Steve McKayc83baa02016-01-06 18:32:13 -0800346 intent.putExtra(Shared.EXTRA_STACK, (Parcelable) stack);
347 return intent;
348 }
349
350 Intent createCancelIntent() {
Steve McKaybbeba522016-01-13 17:17:39 -0800351 final Intent cancelIntent = new Intent(service, FileOperationService.class);
Tomasz Mikolajewski63e2aae2016-02-01 12:01:14 +0900352 cancelIntent.setData(getDataUriForIntent(INTENT_TAG_CANCEL));
Steve McKaybbeba522016-01-13 17:17:39 -0800353 cancelIntent.putExtra(EXTRA_CANCEL, true);
354 cancelIntent.putExtra(EXTRA_JOB_ID, id);
Steve McKayc83baa02016-01-06 18:32:13 -0800355 return cancelIntent;
356 }
357
Steve McKay97b4be42016-01-20 15:09:35 -0800358 @Override
359 public String toString() {
360 return new StringBuilder()
361 .append("Job")
362 .append("{")
363 .append("id=" + id)
364 .append("}")
365 .toString();
366 }
367
Steve McKaybbeba522016-01-13 17:17:39 -0800368 /**
Steve McKaybbeba522016-01-13 17:17:39 -0800369 * Listener interface employed by the service that owns us as well as tests.
370 */
Steve McKayc83baa02016-01-06 18:32:13 -0800371 interface Listener {
Steve McKaybbeba522016-01-13 17:17:39 -0800372 void onStart(Job job);
Steve McKaybbeba522016-01-13 17:17:39 -0800373 void onFinished(Job job);
Steve McKayc83baa02016-01-06 18:32:13 -0800374 }
lumarkdbf65122018-05-28 18:42:58 +0800375
376 /**
377 * Interface for tracking job progress.
378 */
379 interface ProgressTracker {
380 default double getProgress() { return -1; }
381 default long getRemainingTimeEstimate() {
382 return -1;
383 }
384 }
Steve McKayc83baa02016-01-06 18:32:13 -0800385}