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