Home | History | Annotate | Download | only in services
      1 /*
      2  * Copyright (C) 2015 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.base.SharedMinimal.DEBUG;
     20 
     21 import android.annotation.IntDef;
     22 import android.app.Notification;
     23 import android.app.NotificationChannel;
     24 import android.app.NotificationManager;
     25 import android.app.Service;
     26 import android.content.Intent;
     27 import android.os.Handler;
     28 import android.os.IBinder;
     29 import android.os.PowerManager;
     30 import android.os.UserManager;
     31 import android.support.annotation.VisibleForTesting;
     32 import android.util.Log;
     33 
     34 import com.android.documentsui.R;
     35 import com.android.documentsui.base.Features;
     36 
     37 import java.lang.annotation.Retention;
     38 import java.lang.annotation.RetentionPolicy;
     39 import java.util.ArrayList;
     40 import java.util.HashMap;
     41 import java.util.List;
     42 import java.util.Map;
     43 import java.util.concurrent.ExecutorService;
     44 import java.util.concurrent.Executors;
     45 import java.util.concurrent.Future;
     46 import java.util.concurrent.atomic.AtomicReference;
     47 
     48 import javax.annotation.concurrent.GuardedBy;
     49 
     50 public class FileOperationService extends Service implements Job.Listener {
     51 
     52     public static final String TAG = "FileOperationService";
     53 
     54     // Extra used for OperationDialogFragment, Notifications and picking copy destination.
     55     public static final String EXTRA_OPERATION_TYPE = "com.android.documentsui.OPERATION_TYPE";
     56 
     57     // Extras used for OperationDialogFragment...
     58     public static final String EXTRA_DIALOG_TYPE = "com.android.documentsui.DIALOG_TYPE";
     59     public static final String EXTRA_SRC_LIST = "com.android.documentsui.SRC_LIST";
     60 
     61     public static final String EXTRA_FAILED_URIS = "com.android.documentsui.FAILED_URIS";
     62     public static final String EXTRA_FAILED_DOCS = "com.android.documentsui.FAILED_DOCS";
     63 
     64     // Extras used to start or cancel a file operation...
     65     public static final String EXTRA_JOB_ID = "com.android.documentsui.JOB_ID";
     66     public static final String EXTRA_OPERATION = "com.android.documentsui.OPERATION";
     67     public static final String EXTRA_CANCEL = "com.android.documentsui.CANCEL";
     68 
     69     @IntDef({
     70             OPERATION_UNKNOWN,
     71             OPERATION_COPY,
     72             OPERATION_COMPRESS,
     73             OPERATION_EXTRACT,
     74             OPERATION_MOVE,
     75             OPERATION_DELETE
     76     })
     77     @Retention(RetentionPolicy.SOURCE)
     78     public @interface OpType {}
     79     public static final int OPERATION_UNKNOWN = -1;
     80     public static final int OPERATION_COPY = 1;
     81     public static final int OPERATION_EXTRACT = 2;
     82     public static final int OPERATION_COMPRESS = 3;
     83     public static final int OPERATION_MOVE = 4;
     84     public static final int OPERATION_DELETE = 5;
     85 
     86     @IntDef({
     87             MESSAGE_PROGRESS,
     88             MESSAGE_FINISH
     89     })
     90     @Retention(RetentionPolicy.SOURCE)
     91     public @interface MessageType {}
     92     public static final int MESSAGE_PROGRESS = 0;
     93     public static final int MESSAGE_FINISH = 1;
     94 
     95     // TODO: Move it to a shared file when more operations are implemented.
     96     public static final int FAILURE_COPY = 1;
     97 
     98     static final String NOTIFICATION_CHANNEL_ID = "channel_id";
     99 
    100     private static final int POOL_SIZE = 2;  // "pool size", not *max* "pool size".
    101 
    102     private static final int NOTIFICATION_ID_PROGRESS = 0;
    103     private static final int NOTIFICATION_ID_FAILURE = 1;
    104     private static final int NOTIFICATION_ID_WARNING = 2;
    105 
    106     // The executor and job factory are visible for testing and non-final
    107     // so we'll have a way to inject test doubles from the test. It's
    108     // a sub-optimal arrangement.
    109     @VisibleForTesting ExecutorService executor;
    110 
    111     // Use a separate thread pool to prioritize deletions.
    112     @VisibleForTesting ExecutorService deletionExecutor;
    113 
    114     // Use a handler to schedule monitor tasks.
    115     @VisibleForTesting Handler handler;
    116 
    117     // Use a foreground manager to change foreground state of this service.
    118     @VisibleForTesting ForegroundManager foregroundManager;
    119 
    120     // Use a notification manager to post and cancel notifications for jobs.
    121     @VisibleForTesting NotificationManager notificationManager;
    122 
    123     // Use a features to determine if notification channel is enabled.
    124     @VisibleForTesting Features features;
    125 
    126     @GuardedBy("mJobs")
    127     private final Map<String, JobRecord> mJobs = new HashMap<>();
    128 
    129     // The job whose notification is used to keep the service in foreground mode.
    130     private final AtomicReference<Job> mForegroundJob = new AtomicReference<>();
    131 
    132     private PowerManager mPowerManager;
    133     private PowerManager.WakeLock mWakeLock;  // the wake lock, if held.
    134 
    135     private int mLastServiceId;
    136 
    137     @Override
    138     public void onCreate() {
    139         // Allow tests to pre-set these with test doubles.
    140         if (executor == null) {
    141             executor = Executors.newFixedThreadPool(POOL_SIZE);
    142         }
    143 
    144         if (deletionExecutor == null) {
    145             deletionExecutor = Executors.newCachedThreadPool();
    146         }
    147 
    148         if (handler == null) {
    149             // Monitor tasks are small enough to schedule them on main thread.
    150             handler = new Handler();
    151         }
    152 
    153         if (foregroundManager == null) {
    154             foregroundManager = createForegroundManager(this);
    155         }
    156 
    157         if (notificationManager == null) {
    158             notificationManager = getSystemService(NotificationManager.class);
    159         }
    160 
    161         features = new Features.RuntimeFeatures(getResources(), UserManager.get(this));
    162         setUpNotificationChannel();
    163 
    164         if (DEBUG) Log.d(TAG, "Created.");
    165         mPowerManager = getSystemService(PowerManager.class);
    166     }
    167 
    168     private void setUpNotificationChannel() {
    169         if (features.isNotificationChannelEnabled()) {
    170             NotificationChannel channel = new NotificationChannel(
    171                     NOTIFICATION_CHANNEL_ID,
    172                     getString(R.string.app_label),
    173                     NotificationManager.IMPORTANCE_LOW);
    174             notificationManager.createNotificationChannel(channel);
    175         }
    176     }
    177 
    178     @Override
    179     public void onDestroy() {
    180         if (DEBUG) Log.d(TAG, "Shutting down executor.");
    181 
    182         List<Runnable> unfinishedCopies = executor.shutdownNow();
    183         List<Runnable> unfinishedDeletions = deletionExecutor.shutdownNow();
    184         List<Runnable> unfinished =
    185                 new ArrayList<>(unfinishedCopies.size() + unfinishedDeletions.size());
    186         unfinished.addAll(unfinishedCopies);
    187         unfinished.addAll(unfinishedDeletions);
    188         if (!unfinished.isEmpty()) {
    189             Log.w(TAG, "Shutting down, but executor reports running jobs: " + unfinished);
    190         }
    191 
    192         executor = null;
    193         deletionExecutor = null;
    194         handler = null;
    195 
    196         if (DEBUG) Log.d(TAG, "Destroyed.");
    197     }
    198 
    199     @Override
    200     public int onStartCommand(Intent intent, int flags, int serviceId) {
    201         // TODO: Ensure we're not being called with retry or redeliver.
    202         // checkArgument(flags == 0);  // retry and redeliver are not supported.
    203 
    204         String jobId = intent.getStringExtra(EXTRA_JOB_ID);
    205         assert(jobId != null);
    206 
    207         if (DEBUG) Log.d(TAG, "onStartCommand: " + jobId + " with serviceId " + serviceId);
    208 
    209         if (intent.hasExtra(EXTRA_CANCEL)) {
    210             handleCancel(intent);
    211         } else {
    212             FileOperation operation = intent.getParcelableExtra(EXTRA_OPERATION);
    213             handleOperation(jobId, operation);
    214         }
    215 
    216         // Track the service supplied id so we can stop the service once we're out of work to do.
    217         mLastServiceId = serviceId;
    218 
    219         return START_NOT_STICKY;
    220     }
    221 
    222     private void handleOperation(String jobId, FileOperation operation) {
    223         synchronized (mJobs) {
    224             if (mWakeLock == null) {
    225                 mWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
    226             }
    227 
    228             if (mJobs.containsKey(jobId)) {
    229                 Log.w(TAG, "Duplicate job id: " + jobId
    230                         + ". Ignoring job request for operation: " + operation + ".");
    231                 return;
    232             }
    233 
    234             Job job = operation.createJob(this, this, jobId, features);
    235 
    236             if (job == null) {
    237                 return;
    238             }
    239 
    240             assert (job != null);
    241             if (DEBUG) Log.d(TAG, "Scheduling job " + job.id + ".");
    242             Future<?> future = getExecutorService(operation.getOpType()).submit(job);
    243             mJobs.put(jobId, new JobRecord(job, future));
    244 
    245             // Acquire wake lock to keep CPU running until we finish all jobs. Acquire wake lock
    246             // after we create a job and put it in mJobs to avoid potential leaking of wake lock
    247             // in case where job creation fails.
    248             mWakeLock.acquire();
    249         }
    250     }
    251 
    252     /**
    253      * Cancels the operation corresponding to job id, identified in "EXTRA_JOB_ID".
    254      *
    255      * @param intent The cancellation intent.
    256      */
    257     private void handleCancel(Intent intent) {
    258         assert(intent.hasExtra(EXTRA_CANCEL));
    259         assert(intent.getStringExtra(EXTRA_JOB_ID) != null);
    260 
    261         String jobId = intent.getStringExtra(EXTRA_JOB_ID);
    262 
    263         if (DEBUG) Log.d(TAG, "handleCancel: " + jobId);
    264 
    265         synchronized (mJobs) {
    266             // Do nothing if the cancelled ID doesn't match the current job ID. This prevents racey
    267             // cancellation requests from affecting unrelated copy jobs.  However, if the current job ID
    268             // is null, the service most likely crashed and was revived by the incoming cancel intent.
    269             // In that case, always allow the cancellation to proceed.
    270             JobRecord record = mJobs.get(jobId);
    271             if (record != null) {
    272                 record.job.cancel();
    273             }
    274         }
    275 
    276         // Dismiss the progress notification here rather than in the copy loop. This preserves
    277         // interactivity for the user in case the copy loop is stalled.
    278         // Try to cancel it even if we don't have a job id...in case there is some sad
    279         // orphan notification.
    280         notificationManager.cancel(jobId, NOTIFICATION_ID_PROGRESS);
    281 
    282         // TODO: Guarantee the job is being finalized
    283     }
    284 
    285     private ExecutorService getExecutorService(@OpType int operationType) {
    286         switch (operationType) {
    287             case OPERATION_COPY:
    288             case OPERATION_COMPRESS:
    289             case OPERATION_EXTRACT:
    290             case OPERATION_MOVE:
    291                 return executor;
    292             case OPERATION_DELETE:
    293                 return deletionExecutor;
    294             default:
    295                 throw new UnsupportedOperationException();
    296         }
    297     }
    298 
    299     @GuardedBy("mJobs")
    300     private void deleteJob(Job job) {
    301         if (DEBUG) Log.d(TAG, "deleteJob: " + job.id);
    302 
    303         // Release wake lock before clearing jobs just in case we fail to clean them up.
    304         mWakeLock.release();
    305         if (!mWakeLock.isHeld()) {
    306             mWakeLock = null;
    307         }
    308 
    309         JobRecord record = mJobs.remove(job.id);
    310         assert(record != null);
    311         record.job.cleanup();
    312 
    313         // Delay the shutdown until we've cleaned up all notifications. shutdown() is now posted in
    314         // onFinished(Job job) to main thread.
    315     }
    316 
    317     /**
    318      * Most likely shuts down. Won't shut down if service has a pending
    319      * message. Thread pool is deal with in onDestroy.
    320      */
    321     private void shutdown() {
    322         if (DEBUG) Log.d(TAG, "Shutting down. Last serviceId was " + mLastServiceId);
    323         assert(mWakeLock == null);
    324 
    325         // Turns out, for us, stopSelfResult always returns false in tests,
    326         // so we can't guard executor shutdown. For this reason we move
    327         // executor shutdown to #onDestroy.
    328         boolean gonnaStop = stopSelfResult(mLastServiceId);
    329         if (DEBUG) Log.d(TAG, "Stopping service: " + gonnaStop);
    330         if (!gonnaStop) {
    331             Log.w(TAG, "Service should be stopping, but reports otherwise.");
    332         }
    333     }
    334 
    335     @VisibleForTesting
    336     boolean holdsWakeLock() {
    337         return mWakeLock != null && mWakeLock.isHeld();
    338     }
    339 
    340     @Override
    341     public void onStart(Job job) {
    342         if (DEBUG) Log.d(TAG, "onStart: " + job.id);
    343 
    344         Notification notification = job.getSetupNotification();
    345         // If there is no foreground job yet, set this job to foreground job.
    346         if (mForegroundJob.compareAndSet(null, job)) {
    347             if (DEBUG) Log.d(TAG, "Set foreground job to " + job.id);
    348             foregroundManager.startForeground(NOTIFICATION_ID_PROGRESS, notification);
    349         }
    350 
    351         // Show start up notification
    352         if (DEBUG) Log.d(TAG, "Posting notification for " + job.id);
    353         notificationManager.notify(
    354                 job.id, NOTIFICATION_ID_PROGRESS, notification);
    355 
    356         // Set up related monitor
    357         JobMonitor monitor = new JobMonitor(job, notificationManager, handler, mJobs);
    358         monitor.start();
    359     }
    360 
    361     @Override
    362     public void onFinished(Job job) {
    363         assert(job.isFinished());
    364         if (DEBUG) Log.d(TAG, "onFinished: " + job.id);
    365 
    366         synchronized (mJobs) {
    367             // Delete the job from mJobs first to avoid this job being selected as the foreground
    368             // task again if we need to swap the foreground job.
    369             deleteJob(job);
    370 
    371             // Update foreground state before cleaning up notification. If the finishing job is the
    372             // foreground job, we would need to switch to another one or go to background before
    373             // we can clean up notifications.
    374             updateForegroundState(job);
    375 
    376             // Use the same thread of monitors to tackle notifications to avoid race conditions.
    377             // Otherwise we may fail to dismiss progress notification.
    378             handler.post(() -> cleanUpNotification(job));
    379 
    380             // Post the shutdown message to main thread after cleanUpNotification() to give it a
    381             // chance to run. Otherwise this process may be torn down by Android before we've
    382             // cleaned up the notifications of the last job.
    383             if (mJobs.isEmpty()) {
    384                 handler.post(this::shutdown);
    385             }
    386         }
    387     }
    388 
    389     @GuardedBy("mJobs")
    390     private void updateForegroundState(Job job) {
    391         Job candidate = mJobs.isEmpty() ? null : mJobs.values().iterator().next().job;
    392 
    393         // If foreground job is retiring and there is still work to do, we need to set it to a new
    394         // job.
    395         if (mForegroundJob.compareAndSet(job, candidate)) {
    396             if (candidate == null) {
    397                 if (DEBUG) Log.d(TAG, "Stop foreground");
    398                 // Remove the notification here just in case we're torn down before we have the
    399                 // chance to clean up notifications.
    400                 foregroundManager.stopForeground(true);
    401             } else {
    402                 if (DEBUG) Log.d(TAG, "Switch foreground job to " + candidate.id);
    403 
    404                 Notification notification = (candidate.getState() == Job.STATE_STARTED)
    405                         ? candidate.getSetupNotification()
    406                         : candidate.getProgressNotification();
    407                 foregroundManager.startForeground(NOTIFICATION_ID_PROGRESS, notification);
    408                 notificationManager.notify(candidate.id, NOTIFICATION_ID_PROGRESS,
    409                         notification);
    410             }
    411         }
    412     }
    413 
    414     private void cleanUpNotification(Job job) {
    415 
    416         if (DEBUG) Log.d(TAG, "Canceling notification for " + job.id);
    417         // Dismiss the ongoing copy notification when the copy is done.
    418         notificationManager.cancel(job.id, NOTIFICATION_ID_PROGRESS);
    419 
    420         if (job.hasFailures()) {
    421             if (!job.failedUris.isEmpty()) {
    422                 Log.e(TAG, "Job failed to resolve uris: " + job.failedUris + ".");
    423             }
    424             if (!job.failedDocs.isEmpty()) {
    425                 Log.e(TAG, "Job failed to process docs: " + job.failedDocs + ".");
    426             }
    427             notificationManager.notify(
    428                     job.id, NOTIFICATION_ID_FAILURE, job.getFailureNotification());
    429         }
    430 
    431         if (job.hasWarnings()) {
    432             if (DEBUG) Log.d(TAG, "Job finished with warnings.");
    433             notificationManager.notify(
    434                     job.id, NOTIFICATION_ID_WARNING, job.getWarningNotification());
    435         }
    436     }
    437 
    438     private static final class JobRecord {
    439         private final Job job;
    440         private final Future<?> future;
    441 
    442         public JobRecord(Job job, Future<?> future) {
    443             this.job = job;
    444             this.future = future;
    445         }
    446     }
    447 
    448     /**
    449      * A class used to periodically polls state of a job.
    450      *
    451      * <p>It's possible that jobs hang because underlying document providers stop responding. We
    452      * still need to update notifications if jobs hang, so instead of jobs pushing their states,
    453      * we poll states of jobs.
    454      */
    455     private static final class JobMonitor implements Runnable {
    456         private static final long PROGRESS_INTERVAL_MILLIS = 500L;
    457 
    458         private final Job mJob;
    459         private final NotificationManager mNotificationManager;
    460         private final Handler mHandler;
    461         private final Object mJobsLock;
    462 
    463         private JobMonitor(Job job, NotificationManager notificationManager, Handler handler,
    464                 Object jobsLock) {
    465             mJob = job;
    466             mNotificationManager = notificationManager;
    467             mHandler = handler;
    468             mJobsLock = jobsLock;
    469         }
    470 
    471         private void start() {
    472             mHandler.post(this);
    473         }
    474 
    475         @Override
    476         public void run() {
    477             synchronized (mJobsLock) {
    478                 if (mJob.isFinished()) {
    479                     // Finish notification is already shown. Progress notification is removed.
    480                     // Just finish itself.
    481                     return;
    482                 }
    483 
    484                 // Only job in set up state has progress bar
    485                 if (mJob.getState() == Job.STATE_SET_UP) {
    486                     mNotificationManager.notify(
    487                             mJob.id, NOTIFICATION_ID_PROGRESS, mJob.getProgressNotification());
    488                 }
    489 
    490                 mHandler.postDelayed(this, PROGRESS_INTERVAL_MILLIS);
    491             }
    492         }
    493     }
    494 
    495     @Override
    496     public IBinder onBind(Intent intent) {
    497         return null;  // Boilerplate. See super#onBind
    498     }
    499 
    500     private static ForegroundManager createForegroundManager(final Service service) {
    501         return new ForegroundManager() {
    502             @Override
    503             public void startForeground(int id, Notification notification) {
    504                 service.startForeground(id, notification);
    505             }
    506 
    507             @Override
    508             public void stopForeground(boolean removeNotification) {
    509                 service.stopForeground(removeNotification);
    510             }
    511         };
    512     }
    513 
    514     @VisibleForTesting
    515     interface ForegroundManager {
    516         void startForeground(int id, Notification notification);
    517         void stopForeground(boolean removeNotification);
    518     }
    519 }
    520