FileOperationService.java revision edce554c3eaff20a9bf349c1bc18c0b49e812c74
1c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay/* 2c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay * Copyright (C) 2015 The Android Open Source Project 3c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay * 4c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay * Licensed under the Apache License, Version 2.0 (the "License"); 5c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay * you may not use this file except in compliance with the License. 6c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay * You may obtain a copy of the License at 7c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay * 8c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay * http://www.apache.org/licenses/LICENSE-2.0 9c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay * 10c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay * Unless required by applicable law or agreed to in writing, software 11c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay * distributed under the License is distributed on an "AS IS" BASIS, 12c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay * See the License for the specific language governing permissions and 14c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay * limitations under the License. 15c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay */ 16c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay 17c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKaypackage com.android.documentsui.services; 18c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay 19c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKayimport static com.android.documentsui.Shared.DEBUG; 20c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay 21c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKayimport android.annotation.IntDef; 22c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKayimport android.app.NotificationManager; 23bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKayimport android.app.Service; 24c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKayimport android.content.Intent; 2548ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tanimport android.os.Handler; 26bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKayimport android.os.IBinder; 27c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKayimport android.os.PowerManager; 2897b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKayimport android.support.annotation.Nullable; 29c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKayimport android.support.annotation.VisibleForTesting; 30c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKayimport android.util.Log; 31c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay 32edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tanimport com.android.documentsui.ClipDetails; 33c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKayimport com.android.documentsui.Shared; 34c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKayimport com.android.documentsui.model.DocumentStack; 35bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKayimport com.android.documentsui.services.Job.Factory; 36c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay 37c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKayimport java.lang.annotation.Retention; 38c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKayimport java.lang.annotation.RetentionPolicy; 3948ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tanimport java.util.ArrayList; 40bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKayimport java.util.HashMap; 41c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKayimport java.util.List; 42bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKayimport java.util.Map; 43810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tanimport java.util.concurrent.ExecutorService; 44810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tanimport java.util.concurrent.Executors; 45810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tanimport java.util.concurrent.Future; 46bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay 47bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKayimport javax.annotation.concurrent.GuardedBy; 48bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay 49bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKaypublic class FileOperationService extends Service implements Job.Listener { 50bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay 516b6c16e6a20d37beb06c1820ec63c763ff53aa67Tomasz Mikolajewski private static final int POOL_SIZE = 2; // "pool size", not *max* "pool size". 526b6c16e6a20d37beb06c1820ec63c763ff53aa67Tomasz Mikolajewski private static final int NOTIFICATION_ID_PROGRESS = 0; 536b6c16e6a20d37beb06c1820ec63c763ff53aa67Tomasz Mikolajewski private static final int NOTIFICATION_ID_FAILURE = 1; 54dd2b31c758e8d694867af58a425180689d25ead1Tomasz Mikolajewski private static final int NOTIFICATION_ID_WARNING = 2; 55c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay 56c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay public static final String TAG = "FileOperationService"; 57c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay 58c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay public static final String EXTRA_JOB_ID = "com.android.documentsui.JOB_ID"; 59c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay public static final String EXTRA_OPERATION = "com.android.documentsui.OPERATION"; 60c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay public static final String EXTRA_CANCEL = "com.android.documentsui.CANCEL"; 61edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan public static final String EXTRA_CLIP_DETAILS = "com.android.documentsui.SRC_CLIP_DETAIL"; 62dd2b31c758e8d694867af58a425180689d25ead1Tomasz Mikolajewski public static final String EXTRA_DIALOG_TYPE = "com.android.documentsui.DIALOG_TYPE"; 63c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay 64edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan public static final String EXTRA_SRC_LIST = "com.android.documentsui.SRC_LIST"; 65e0094419cd5f4e1cb55bcabc30c7e066d065827cTomasz Mikolajewski 66c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay @IntDef(flag = true, value = { 67c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay OPERATION_UNKNOWN, 68c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay OPERATION_COPY, 69c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay OPERATION_MOVE, 70c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay OPERATION_DELETE 71c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay }) 72c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay @Retention(RetentionPolicy.SOURCE) 73c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay public @interface OpType {} 74f171934ad7a9d901e81a33bbd0ec74aa34a48787Steve McKay public static final int OPERATION_UNKNOWN = -1; 75f171934ad7a9d901e81a33bbd0ec74aa34a48787Steve McKay public static final int OPERATION_COPY = 1; 76f171934ad7a9d901e81a33bbd0ec74aa34a48787Steve McKay public static final int OPERATION_MOVE = 2; 77f171934ad7a9d901e81a33bbd0ec74aa34a48787Steve McKay public static final int OPERATION_DELETE = 3; 78c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay 79c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay // TODO: Move it to a shared file when more operations are implemented. 80c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay public static final int FAILURE_COPY = 1; 81c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay 82bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay // The executor and job factory are visible for testing and non-final 83bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay // so we'll have a way to inject test doubles from the test. It's 84bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay // a sub-optimal arrangement. 85810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan @VisibleForTesting ExecutorService executor; 86810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan 8748ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan // Use a separate thread pool to prioritize deletions. 88810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan @VisibleForTesting ExecutorService deletionExecutor; 89bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay @VisibleForTesting Factory jobFactory; 90c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay 9148ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan // Use a handler to schedule monitor tasks. 9248ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan @VisibleForTesting Handler handler; 9348ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan 94bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay private PowerManager mPowerManager; 95bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay private PowerManager.WakeLock mWakeLock; // the wake lock, if held. 96c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay private NotificationManager mNotificationManager; 97c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay 98bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay @GuardedBy("mRunning") 99bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay private Map<String, JobRecord> mRunning = new HashMap<>(); 100c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay 10197b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay private int mLastServiceId; 102c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay 103c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay @Override 104c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay public void onCreate() { 105bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay // Allow tests to pre-set these with test doubles. 106bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay if (executor == null) { 107810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan executor = Executors.newFixedThreadPool(POOL_SIZE); 108810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan } 109810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan 110810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan if (deletionExecutor == null) { 111810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan deletionExecutor = Executors.newCachedThreadPool(); 112bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay } 113bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay 114bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay if (jobFactory == null) { 115bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay jobFactory = Job.Factory.instance; 116bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay } 117c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay 11848ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan if (handler == null) { 11948ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan // Monitor tasks are small enough to schedule them on main thread. 12048ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan handler = new Handler(); 12148ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan } 12248ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan 123c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay if (DEBUG) Log.d(TAG, "Created."); 124c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay mPowerManager = getSystemService(PowerManager.class); 125c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay mNotificationManager = getSystemService(NotificationManager.class); 126c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay } 127c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay 128c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay @Override 12997b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay public void onDestroy() { 13097b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay if (DEBUG) Log.d(TAG, "Shutting down executor."); 13148ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan 13248ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan List<Runnable> unfinishedCopies = executor.shutdownNow(); 13348ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan List<Runnable> unfinishedDeletions = deletionExecutor.shutdownNow(); 13448ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan List<Runnable> unfinished = 13548ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan new ArrayList<>(unfinishedCopies.size() + unfinishedDeletions.size()); 13648ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan unfinished.addAll(unfinishedCopies); 13748ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan unfinished.addAll(unfinishedDeletions); 13897b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay if (!unfinished.isEmpty()) { 13997b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay Log.w(TAG, "Shutting down, but executor reports running jobs: " + unfinished); 14097b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay } 14148ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan 14297b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay executor = null; 14348ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan deletionExecutor = null; 14448ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan handler = null; 145edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan 14697b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay if (DEBUG) Log.d(TAG, "Destroyed."); 14797b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay } 14897b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay 14997b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay @Override 15097b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay public int onStartCommand(Intent intent, int flags, int serviceId) { 151bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay // TODO: Ensure we're not being called with retry or redeliver. 152bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay // checkArgument(flags == 0); // retry and redeliver are not supported. 153c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay 154c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay String jobId = intent.getStringExtra(EXTRA_JOB_ID); 1550af8afd3309538dec784ed0c9c35b252a8213123Steve McKay assert(jobId != null); 156bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay 157edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan if (DEBUG) Log.d(TAG, "onStartCommand: " + jobId + " with serviceId " + serviceId); 158edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan 159c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay if (intent.hasExtra(EXTRA_CANCEL)) { 160c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay handleCancel(intent); 161bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay } else { 162edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan ClipDetails details = intent.getParcelableExtra(EXTRA_CLIP_DETAILS); 163edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan assert(details.getOpType() != OPERATION_UNKNOWN); 164edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan handleOperation(intent, jobId, details); 165c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay } 166c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay 16797b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay // Track the service supplied id so we can stop the service once we're out of work to do. 16897b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay mLastServiceId = serviceId; 169c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay 170edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan return START_NOT_STICKY; 171edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan } 172edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan 173edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan private void handleOperation(Intent intent, String jobId, ClipDetails details) { 174bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay synchronized (mRunning) { 175bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay if (mWakeLock == null) { 176e0094419cd5f4e1cb55bcabc30c7e066d065827cTomasz Mikolajewski mWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); 177c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay } 178c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay 179bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay DocumentStack stack = intent.getParcelableExtra(Shared.EXTRA_STACK); 180c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay 181edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan Job job = createJob(jobId, details, stack); 182bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay 18397b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay if (job == null) { 18497b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay return; 18597b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay } 18697b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay 187bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay mWakeLock.acquire(); 188bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay 189ecc05c0286433c00bf9beb78066e64f1a80fc066Garfield, Tan assert (job != null); 190ecc05c0286433c00bf9beb78066e64f1a80fc066Garfield, Tan if (DEBUG) Log.d(TAG, "Scheduling job " + job.id + "."); 191edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan Future<?> future = getExecutorService(details.getOpType()).submit(job); 192ecc05c0286433c00bf9beb78066e64f1a80fc066Garfield, Tan mRunning.put(jobId, new JobRecord(job, future)); 193ecc05c0286433c00bf9beb78066e64f1a80fc066Garfield, Tan } 194c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay } 195c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay 196c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay /** 197c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay * Cancels the operation corresponding to job id, identified in "EXTRA_JOB_ID". 198c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay * 199c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay * @param intent The cancellation intent. 200c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay */ 201c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay private void handleCancel(Intent intent) { 2020af8afd3309538dec784ed0c9c35b252a8213123Steve McKay assert(intent.hasExtra(EXTRA_CANCEL)); 2030af8afd3309538dec784ed0c9c35b252a8213123Steve McKay assert(intent.getStringExtra(EXTRA_JOB_ID) != null); 2040af8afd3309538dec784ed0c9c35b252a8213123Steve McKay 2050af8afd3309538dec784ed0c9c35b252a8213123Steve McKay String jobId = intent.getStringExtra(EXTRA_JOB_ID); 206c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay 207bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay if (DEBUG) Log.d(TAG, "handleCancel: " + jobId); 208bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay 209bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay synchronized (mRunning) { 210bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay // Do nothing if the cancelled ID doesn't match the current job ID. This prevents racey 211bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay // cancellation requests from affecting unrelated copy jobs. However, if the current job ID 212bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay // is null, the service most likely crashed and was revived by the incoming cancel intent. 213bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay // In that case, always allow the cancellation to proceed. 214bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay JobRecord record = mRunning.get(jobId); 215bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay if (record != null) { 216bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay record.job.cancel(); 217bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay } 218c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay } 219c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay 220c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay // Dismiss the progress notification here rather than in the copy loop. This preserves 221c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay // interactivity for the user in case the copy loop is stalled. 222c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay // Try to cancel it even if we don't have a job id...in case there is some sad 223c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay // orphan notification. 2246b6c16e6a20d37beb06c1820ec63c763ff53aa67Tomasz Mikolajewski mNotificationManager.cancel(jobId, NOTIFICATION_ID_PROGRESS); 225c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay 226bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay // TODO: Guarantee the job is being finalized 227c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay } 228c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay 22997b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay /** 23097b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay * Creates a new job. Returns null if a job with {@code id} already exists. 23197b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay * @return 23297b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay */ 233bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay @GuardedBy("mRunning") 23497b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay private @Nullable Job createJob( 235edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan String id, ClipDetails details, DocumentStack stack) { 236c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay 237edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan assert(details.getItemCount() > 0); 2383d68b2b3af9e248937258636329c72bcf78c0ae5Steve McKay 23997b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay if (mRunning.containsKey(id)) { 24097b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay Log.w(TAG, "Duplicate job id: " + id 241edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan + ". Ignoring job request for details: " + details + ", stack: " + stack + "."); 24297b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay return null; 24397b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay } 244c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay 245edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan switch (details.getOpType()) { 246c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay case OPERATION_COPY: 2473d68b2b3af9e248937258636329c72bcf78c0ae5Steve McKay return jobFactory.createCopy( 248edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan this, getApplicationContext(), this, id, stack, details); 249c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay case OPERATION_MOVE: 2503d68b2b3af9e248937258636329c72bcf78c0ae5Steve McKay return jobFactory.createMove( 251edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan this, getApplicationContext(), this, id, stack, details); 252c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay case OPERATION_DELETE: 2533d68b2b3af9e248937258636329c72bcf78c0ae5Steve McKay return jobFactory.createDelete( 254edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan this, getApplicationContext(), this, id, stack, details); 255c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay default: 256c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay throw new UnsupportedOperationException(); 257c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay } 258bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay } 259bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay 260810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan private ExecutorService getExecutorService(@OpType int operationType) { 261810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan switch (operationType) { 262810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan case OPERATION_COPY: 263810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan case OPERATION_MOVE: 264810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan return executor; 265810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan case OPERATION_DELETE: 266810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan return deletionExecutor; 267810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan default: 268810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan throw new UnsupportedOperationException(); 269810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan } 270810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan } 271810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan 272bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay @GuardedBy("mRunning") 273bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay private void deleteJob(Job job) { 274bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay if (DEBUG) Log.d(TAG, "deleteJob: " + job.id); 275bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay 276bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay JobRecord record = mRunning.remove(job.id); 2770af8afd3309538dec784ed0c9c35b252a8213123Steve McKay assert(record != null); 278bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay record.job.cleanup(); 279bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay 280bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay if (mRunning.isEmpty()) { 281bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay shutdown(); 282bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay } 283bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay } 284bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay 285bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay /** 286bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay * Most likely shuts down. Won't shut down if service has a pending 28797b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay * message. Thread pool is deal with in onDestroy. 288bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay */ 289bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay private void shutdown() { 29097b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay if (DEBUG) Log.d(TAG, "Shutting down. Last serviceId was " + mLastServiceId); 291bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay mWakeLock.release(); 292bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay mWakeLock = null; 29397b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay 29497b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay // Turns out, for us, stopSelfResult always returns false in tests, 29597b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay // so we can't guard executor shutdown. For this reason we move 29697b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay // executor shutdown to #onDestroy. 29797b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay boolean gonnaStop = stopSelfResult(mLastServiceId); 298bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay if (DEBUG) Log.d(TAG, "Stopping service: " + gonnaStop); 299bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay if (!gonnaStop) { 300bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay Log.w(TAG, "Service should be stopping, but reports otherwise."); 301bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay } 302bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay } 303bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay 304bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay @VisibleForTesting 305bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay boolean holdsWakeLock() { 306bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay return mWakeLock != null && mWakeLock.isHeld(); 307bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay } 308bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay 309bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay @Override 310bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay public void onStart(Job job) { 311bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay if (DEBUG) Log.d(TAG, "onStart: " + job.id); 31248ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan 31348ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan // Show start up notification 31448ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan mNotificationManager.notify( 31548ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan job.id, NOTIFICATION_ID_PROGRESS, job.getSetupNotification()); 31648ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan 31748ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan // Set up related monitor 31848ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan JobMonitor monitor = new JobMonitor(job, mNotificationManager, handler); 31948ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan monitor.start(); 320c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay } 321c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay 322bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay @Override 323bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay public void onFinished(Job job) { 32448ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan assert(job.isFinished()); 325bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay if (DEBUG) Log.d(TAG, "onFinished: " + job.id); 326bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay 32748ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan // Use the same thread of monitors to tackle notifications to avoid race conditions. 32848ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan // Otherwise we may fail to dismiss progress notification. 32948ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan handler.post(() -> { 33048ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan // Dismiss the ongoing copy notification when the copy is done. 33148ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan mNotificationManager.cancel(job.id, NOTIFICATION_ID_PROGRESS); 332bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay 33348ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan if (job.hasFailures()) { 334edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan Log.e(TAG, "Job failed on files: " + job.failedFileCount + "."); 33548ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan mNotificationManager.notify( 33648ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan job.id, NOTIFICATION_ID_FAILURE, job.getFailureNotification()); 33748ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan } 338dd2b31c758e8d694867af58a425180689d25ead1Tomasz Mikolajewski 33948ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan if (job.hasWarnings()) { 34048ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan if (DEBUG) Log.d(TAG, "Job finished with warnings."); 34148ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan mNotificationManager.notify( 34248ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan job.id, NOTIFICATION_ID_WARNING, job.getWarningNotification()); 34348ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan } 34448ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan }); 345dd2b31c758e8d694867af58a425180689d25ead1Tomasz Mikolajewski 346bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay synchronized (mRunning) { 347bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay deleteJob(job); 348bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay } 349c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay } 350c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay 351bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay private static final class JobRecord { 352bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay private final Job job; 353810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan private final Future<?> future; 354bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay 355810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan public JobRecord(Job job, Future<?> future) { 356bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay this.job = job; 357bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay this.future = future; 358bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay } 359c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay } 360c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay 36148ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan /** 36248ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan * A class used to periodically polls state of a job. 36348ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan * 36448ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan * <p>It's possible that jobs hang because underlying document providers stop responding. We 36548ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan * still need to update notifications if jobs hang, so instead of jobs pushing their states, 36648ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan * we poll states of jobs. 36748ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan */ 36848ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan private static final class JobMonitor implements Runnable { 36948ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan private static final long PROGRESS_INTERVAL_MILLIS = 500L; 37048ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan 37148ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan private final Job mJob; 37248ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan private final NotificationManager mNotificationManager; 37348ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan private final Handler mHandler; 37448ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan 37548ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan private JobMonitor(Job job, NotificationManager notificationManager, Handler handler) { 37648ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan mJob = job; 37748ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan mNotificationManager = notificationManager; 37848ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan mHandler = handler; 37948ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan } 38048ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan 38148ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan private void start() { 382edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan mHandler.post(this); 38348ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan } 38448ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan 38548ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan @Override 38648ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan public void run() { 38748ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan if (mJob.isFinished()) { 38848ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan // Finish notification is already shown. Progress notification is removed. 38948ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan // Just finish itself. 39048ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan return; 39148ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan } 39248ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan 393edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan // Only job in set up state has progress bar 394edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan if (mJob.getState() == Job.STATE_SET_UP) { 395edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan mNotificationManager.notify( 396edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan mJob.id, NOTIFICATION_ID_PROGRESS, mJob.getProgressNotification()); 397edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan } 39848ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan 39948ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan mHandler.postDelayed(this, PROGRESS_INTERVAL_MILLIS); 40048ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan } 40148ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan } 40248ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan 403bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay @Override 404bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay public IBinder onBind(Intent intent) { 405bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay return null; // Boilerplate. See super#onBind 406c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay } 407c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay} 408