• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
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 
17 package com.android.documentsui.services;
18 
19 import static android.content.ContentResolver.wrap;
20 
21 import static com.android.documentsui.DocumentsApplication.acquireUnstableProviderOrThrow;
22 import static com.android.documentsui.services.FileOperationService.EXTRA_CANCEL;
23 import static com.android.documentsui.services.FileOperationService.EXTRA_DIALOG_TYPE;
24 import static com.android.documentsui.services.FileOperationService.EXTRA_FAILED_DOCS;
25 import static com.android.documentsui.services.FileOperationService.EXTRA_FAILED_URIS;
26 import static com.android.documentsui.services.FileOperationService.EXTRA_JOB_ID;
27 import static com.android.documentsui.services.FileOperationService.EXTRA_OPERATION_TYPE;
28 import static com.android.documentsui.services.FileOperationService.OPERATION_UNKNOWN;
29 
30 import android.app.Notification;
31 import android.app.Notification.Builder;
32 import android.app.PendingIntent;
33 import android.content.ContentProviderClient;
34 import android.content.ContentResolver;
35 import android.content.Context;
36 import android.content.Intent;
37 import android.net.Uri;
38 import android.os.CancellationSignal;
39 import android.os.DeadObjectException;
40 import android.os.FileUtils;
41 import android.os.Parcelable;
42 import android.os.RemoteException;
43 import android.provider.DocumentsContract;
44 import android.util.Log;
45 
46 import androidx.annotation.DrawableRes;
47 import androidx.annotation.IntDef;
48 import androidx.annotation.PluralsRes;
49 
50 import com.android.documentsui.Metrics;
51 import com.android.documentsui.OperationDialogFragment;
52 import com.android.documentsui.R;
53 import com.android.documentsui.base.DocumentInfo;
54 import com.android.documentsui.base.DocumentStack;
55 import com.android.documentsui.base.Features;
56 import com.android.documentsui.base.Shared;
57 import com.android.documentsui.clipping.UrisSupplier;
58 import com.android.documentsui.files.FilesActivity;
59 import com.android.documentsui.services.FileOperationService.OpType;
60 
61 import java.io.FileNotFoundException;
62 import java.lang.annotation.Retention;
63 import java.lang.annotation.RetentionPolicy;
64 import java.util.ArrayList;
65 import java.util.HashMap;
66 import java.util.Map;
67 
68 import javax.annotation.Nullable;
69 
70 /**
71  * A mashup of work item and ui progress update factory. Used by {@link FileOperationService}
72  * to do work and show progress relating to this work.
73  */
74 abstract public class Job implements Runnable {
75     private static final String TAG = "Job";
76 
77     @Retention(RetentionPolicy.SOURCE)
78     @IntDef({STATE_CREATED, STATE_STARTED, STATE_SET_UP, STATE_COMPLETED, STATE_CANCELED})
79     public @interface State {}
80     public static final int STATE_CREATED = 0;
81     public static final int STATE_STARTED = 1;
82     public static final int STATE_SET_UP = 2;
83     public static final int STATE_COMPLETED = 3;
84     /**
85      * A job is in canceled state as long as {@link #cancel()} is called on it, even after it is
86      * completed.
87      */
88     static final int STATE_CANCELED = 4;
89 
90     static final String INTENT_TAG_WARNING = "warning";
91     static final String INTENT_TAG_FAILURE = "failure";
92     static final String INTENT_TAG_PROGRESS = "progress";
93     static final String INTENT_TAG_CANCEL = "cancel";
94 
95     final Context service;
96     final Context appContext;
97     final Listener listener;
98 
99     final @OpType int operationType;
100     final String id;
101     final DocumentStack stack;
102 
103     final UrisSupplier mResourceUris;
104 
105     int failureCount = 0;
106     final ArrayList<DocumentInfo> failedDocs = new ArrayList<>();
107     final ArrayList<Uri> failedUris = new ArrayList<>();
108 
109     final Notification.Builder mProgressBuilder;
110 
111     final CancellationSignal mSignal = new CancellationSignal();
112 
113     private final Map<String, ContentProviderClient> mClients = new HashMap<>();
114     private final Features mFeatures;
115 
116     private volatile @State int mState = STATE_CREATED;
117 
118     /**
119      * A simple progressable job, much like an AsyncTask, but with support
120      * for providing various related notification, progress and navigation information.
121      * @param service The service context in which this job is running.
122      * @param listener
123      * @param id Arbitrary string ID
124      * @param stack The documents stack context relating to this request. This is the
125      *     destination in the Files app where the user will be take when the
126      *     navigation intent is invoked (presumably from notification).
127      * @param srcs the list of docs to operate on
128      */
Job(Context service, Listener listener, String id, @OpType int opType, DocumentStack stack, UrisSupplier srcs, Features features)129     Job(Context service, Listener listener, String id,
130             @OpType int opType, DocumentStack stack, UrisSupplier srcs, Features features) {
131 
132         assert(opType != OPERATION_UNKNOWN);
133 
134         this.service = service;
135         this.appContext = service.getApplicationContext();
136         this.listener = listener;
137         this.operationType = opType;
138 
139         this.id = id;
140         this.stack = stack;
141         this.mResourceUris = srcs;
142 
143         mFeatures = features;
144 
145         mProgressBuilder = createProgressBuilder();
146     }
147 
148     @Override
run()149     public final void run() {
150         if (isCanceled()) {
151             // Canceled before running
152             return;
153         }
154 
155         mState = STATE_STARTED;
156         listener.onStart(this);
157 
158         try {
159             boolean result = setUp();
160             if (result && !isCanceled()) {
161                 mState = STATE_SET_UP;
162                 start();
163             }
164         } catch (RuntimeException e) {
165             // No exceptions should be thrown here, as all calls to the provider must be
166             // handled within Job implementations. However, just in case catch them here.
167             Log.e(TAG, "Operation failed due to an unhandled runtime exception.", e);
168             Metrics.logFileOperationErrors(operationType, failedDocs, failedUris);
169         } finally {
170             mState = (mState == STATE_STARTED || mState == STATE_SET_UP) ? STATE_COMPLETED : mState;
171             finish();
172             listener.onFinished(this);
173 
174             // NOTE: If this details is a JumboClipDetails, and it's still referred in primary clip
175             // at this point, user won't be able to paste it to anywhere else because the underlying
176             mResourceUris.dispose();
177         }
178     }
179 
setUp()180     boolean setUp() {
181         return true;
182     }
183 
finish()184     abstract void finish();
185 
start()186     abstract void start();
getSetupNotification()187     abstract Notification getSetupNotification();
getProgressNotification()188     abstract Notification getProgressNotification();
getFailureNotification()189     abstract Notification getFailureNotification();
190 
getWarningNotification()191     abstract Notification getWarningNotification();
192 
getJobProgress()193     abstract JobProgress getJobProgress();
194 
getDataUriForIntent(String tag)195     Uri getDataUriForIntent(String tag) {
196         return Uri.parse(String.format("data,%s-%s", tag, id));
197     }
198 
getClient(Uri uri)199     ContentProviderClient getClient(Uri uri) throws RemoteException {
200         ContentProviderClient client = mClients.get(uri.getAuthority());
201         if (client == null) {
202             // Acquire content providers.
203             client = acquireUnstableProviderOrThrow(
204                     getContentResolver(),
205                     uri.getAuthority());
206 
207             mClients.put(uri.getAuthority(), client);
208         }
209 
210         assert(client != null);
211         return client;
212     }
213 
getClient(DocumentInfo doc)214     ContentProviderClient getClient(DocumentInfo doc) throws RemoteException {
215         return getClient(doc.derivedUri);
216     }
217 
releaseClient(Uri uri)218     void releaseClient(Uri uri) {
219         ContentProviderClient client = mClients.get(uri.getAuthority());
220         if (client != null) {
221             client.close();
222             mClients.remove(uri.getAuthority());
223         }
224     }
225 
releaseClient(DocumentInfo doc)226     void releaseClient(DocumentInfo doc) {
227         releaseClient(doc.derivedUri);
228     }
229 
cleanup()230     final void cleanup() {
231         for (ContentProviderClient client : mClients.values()) {
232             FileUtils.closeQuietly(client);
233         }
234     }
235 
getState()236     final @State int getState() {
237         return mState;
238     }
239 
cancel()240     final void cancel() {
241         mState = STATE_CANCELED;
242         mSignal.cancel();
243         Metrics.logFileOperationCancelled(operationType);
244     }
245 
isCanceled()246     final boolean isCanceled() {
247         return mState == STATE_CANCELED;
248     }
249 
isFinished()250     final boolean isFinished() {
251         return mState == STATE_CANCELED || mState == STATE_COMPLETED;
252     }
253 
getContentResolver()254     final ContentResolver getContentResolver() {
255         return service.getContentResolver();
256     }
257 
onFileFailed(DocumentInfo file)258     void onFileFailed(DocumentInfo file) {
259         failureCount++;
260         failedDocs.add(file);
261     }
262 
onResolveFailed(Uri uri)263     void onResolveFailed(Uri uri) {
264         failureCount++;
265         failedUris.add(uri);
266     }
267 
hasFailures()268     final boolean hasFailures() {
269         return failureCount > 0;
270     }
271 
hasWarnings()272     boolean hasWarnings() {
273         return false;
274     }
275 
deleteDocument(DocumentInfo doc, @Nullable DocumentInfo parent)276     final void deleteDocument(DocumentInfo doc, @Nullable DocumentInfo parent)
277             throws ResourceException {
278         try {
279             if (parent != null && doc.isRemoveSupported()) {
280                 DocumentsContract.removeDocument(wrap(getClient(doc)), doc.derivedUri,
281                         parent.derivedUri);
282             } else if (doc.isDeleteSupported()) {
283                 DocumentsContract.deleteDocument(wrap(getClient(doc)), doc.derivedUri);
284             } else {
285                 throw new ResourceException("Unable to delete source document. "
286                         + "File is not deletable or removable: %s.", doc.derivedUri);
287             }
288         } catch (FileNotFoundException | RemoteException | RuntimeException e) {
289             if (e instanceof DeadObjectException) {
290                 releaseClient(doc);
291             }
292             throw new ResourceException("Failed to delete file %s due to an exception.",
293                     doc.derivedUri, e);
294         }
295     }
296 
getSetupNotification(String content)297     Notification getSetupNotification(String content) {
298         mProgressBuilder.setProgress(0, 0, true)
299                 .setContentText(content);
300         return mProgressBuilder.build();
301     }
302 
getFailureNotification(@luralsRes int titleId, @DrawableRes int icon)303     Notification getFailureNotification(@PluralsRes int titleId, @DrawableRes int icon) {
304         final Intent navigateIntent = buildNavigateIntent(INTENT_TAG_FAILURE);
305         navigateIntent.putExtra(EXTRA_DIALOG_TYPE, OperationDialogFragment.DIALOG_TYPE_FAILURE);
306         navigateIntent.putExtra(EXTRA_OPERATION_TYPE, operationType);
307         navigateIntent.putParcelableArrayListExtra(EXTRA_FAILED_DOCS, failedDocs);
308         navigateIntent.putParcelableArrayListExtra(EXTRA_FAILED_URIS, failedUris);
309 
310         final Notification.Builder errorBuilder = createNotificationBuilder()
311                 .setContentTitle(service.getResources().getQuantityString(titleId,
312                         failureCount, failureCount))
313                 .setContentText(service.getString(R.string.notification_touch_for_details))
314                 .setContentIntent(PendingIntent.getActivity(appContext, 0, navigateIntent,
315                         PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT
316                         | PendingIntent.FLAG_MUTABLE))
317                 .setCategory(Notification.CATEGORY_ERROR)
318                 .setSmallIcon(icon)
319                 .setAutoCancel(true);
320 
321         return errorBuilder.build();
322     }
323 
createProgressBuilder()324     abstract Builder createProgressBuilder();
325 
createProgressBuilder( String title, @DrawableRes int icon, String actionTitle, @DrawableRes int actionIcon)326     final Builder createProgressBuilder(
327             String title, @DrawableRes int icon,
328             String actionTitle, @DrawableRes int actionIcon) {
329         Notification.Builder progressBuilder = createNotificationBuilder()
330                 .setContentTitle(title)
331                 .setContentIntent(
332                         PendingIntent.getActivity(appContext, 0,
333                                 buildNavigateIntent(INTENT_TAG_PROGRESS),
334                                 PendingIntent.FLAG_IMMUTABLE))
335                 .setCategory(Notification.CATEGORY_PROGRESS)
336                 .setSmallIcon(icon)
337                 .setOngoing(true);
338 
339         final Intent cancelIntent = createCancelIntent();
340 
341         progressBuilder.addAction(
342                 actionIcon,
343                 actionTitle,
344                 PendingIntent.getService(
345                         service,
346                         0,
347                         cancelIntent,
348                         PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT
349                         | PendingIntent.FLAG_MUTABLE));
350 
351         return progressBuilder;
352     }
353 
createNotificationBuilder()354     Notification.Builder createNotificationBuilder() {
355         return mFeatures.isNotificationChannelEnabled()
356                 ? new Notification.Builder(service, FileOperationService.NOTIFICATION_CHANNEL_ID)
357                 : new Notification.Builder(service);
358     }
359 
360     /**
361      * Creates an intent for navigating back to the destination directory.
362      */
buildNavigateIntent(String tag)363     Intent buildNavigateIntent(String tag) {
364         // TODO (b/35721285): Reuse an existing task rather than creating a new one every time.
365         Intent intent = new Intent(service, FilesActivity.class);
366         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
367         intent.setData(getDataUriForIntent(tag));
368         intent.putExtra(Shared.EXTRA_STACK, (Parcelable) stack);
369         return intent;
370     }
371 
createCancelIntent()372     Intent createCancelIntent() {
373         final Intent cancelIntent = new Intent(service, FileOperationService.class);
374         cancelIntent.setData(getDataUriForIntent(INTENT_TAG_CANCEL));
375         cancelIntent.putExtra(EXTRA_CANCEL, true);
376         cancelIntent.putExtra(EXTRA_JOB_ID, id);
377         return cancelIntent;
378     }
379 
380     @Override
toString()381     public String toString() {
382         return new StringBuilder()
383                 .append("Job")
384                 .append("{")
385                 .append("id=" + id)
386                 .append("}")
387                 .toString();
388     }
389 
390     /**
391      * Listener interface employed by the service that owns us as well as tests.
392      */
393     interface Listener {
onStart(Job job)394         void onStart(Job job);
onFinished(Job job)395         void onFinished(Job job);
396     }
397 
398     /**
399      * Interface for tracking job progress.
400      */
401     interface ProgressTracker {
getProgress()402         default double getProgress() {  return -1; }
getRemainingTimeEstimate()403         default long getRemainingTimeEstimate() {
404             return -1;
405         }
406     }
407 }
408