blob: 580fa38258a66a1f915faeec2c58d1f74812eb2b [file] [log] [blame]
Steve McKay14e827a2016-01-06 18:32:13 -08001/*
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
17package com.android.documentsui.services;
18
Steve McKay14e827a2016-01-06 18:32:13 -080019import static com.android.documentsui.Shared.DEBUG;
Steve McKay14e827a2016-01-06 18:32:13 -080020
21import android.annotation.IntDef;
Steve McKay14e827a2016-01-06 18:32:13 -080022import android.app.NotificationManager;
Steve McKayecbf3c52016-01-13 17:17:39 -080023import android.app.Service;
Steve McKay14e827a2016-01-06 18:32:13 -080024import android.content.Intent;
Steve McKayecbf3c52016-01-13 17:17:39 -080025import android.os.IBinder;
Steve McKay14e827a2016-01-06 18:32:13 -080026import android.os.PowerManager;
Steve McKay35645432016-01-20 15:09:35 -080027import android.support.annotation.Nullable;
Steve McKay14e827a2016-01-06 18:32:13 -080028import android.support.annotation.VisibleForTesting;
29import android.util.Log;
30
31import com.android.documentsui.Shared;
32import com.android.documentsui.model.DocumentInfo;
33import com.android.documentsui.model.DocumentStack;
Steve McKayecbf3c52016-01-13 17:17:39 -080034import com.android.documentsui.services.Job.Factory;
Steve McKay14e827a2016-01-06 18:32:13 -080035
36import java.lang.annotation.Retention;
37import java.lang.annotation.RetentionPolicy;
Steve McKayecbf3c52016-01-13 17:17:39 -080038import java.util.HashMap;
Steve McKay14e827a2016-01-06 18:32:13 -080039import java.util.List;
Steve McKayecbf3c52016-01-13 17:17:39 -080040import java.util.Map;
41import java.util.concurrent.ScheduledExecutorService;
42import java.util.concurrent.ScheduledFuture;
43import java.util.concurrent.ScheduledThreadPoolExecutor;
44import java.util.concurrent.TimeUnit;
Steve McKay14e827a2016-01-06 18:32:13 -080045
Steve McKayecbf3c52016-01-13 17:17:39 -080046import javax.annotation.concurrent.GuardedBy;
47
48public class FileOperationService extends Service implements Job.Listener {
49
50 private static final int DEFAULT_DELAY = 0;
51 private static final int MAX_DELAY = 10 * 1000; // ten seconds
Tomasz Mikolajewskife0c7352016-01-21 17:48:03 +090052 private static final int POOL_SIZE = 2; // "pool size", not *max* "pool size".
53 private static final int NOTIFICATION_ID_PROGRESS = 0;
54 private static final int NOTIFICATION_ID_FAILURE = 1;
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +090055 private static final int NOTIFICATION_ID_WARNING = 2;
Steve McKayecbf3c52016-01-13 17:17:39 -080056
Steve McKay14e827a2016-01-06 18:32:13 -080057 public static final String TAG = "FileOperationService";
58
59 public static final String EXTRA_JOB_ID = "com.android.documentsui.JOB_ID";
Steve McKayecbf3c52016-01-13 17:17:39 -080060 public static final String EXTRA_DELAY = "com.android.documentsui.DELAY";
Steve McKay14e827a2016-01-06 18:32:13 -080061 public static final String EXTRA_OPERATION = "com.android.documentsui.OPERATION";
62 public static final String EXTRA_CANCEL = "com.android.documentsui.CANCEL";
63 public static final String EXTRA_SRC_LIST = "com.android.documentsui.SRC_LIST";
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +090064 public static final String EXTRA_DIALOG_TYPE = "com.android.documentsui.DIALOG_TYPE";
Steve McKay14e827a2016-01-06 18:32:13 -080065
Tomasz Mikolajewskib8436af2016-01-25 16:20:15 +090066 // This extra is used only for moving and deleting. Currently it's not the case,
67 // but in the future those files may be from multiple different parents. In
68 // such case, this needs to be replaced with pairs of parent and child.
69 public static final String EXTRA_SRC_PARENT = "com.android.documentsui.SRC_PARENT";
70
Steve McKay14e827a2016-01-06 18:32:13 -080071 @IntDef(flag = true, value = {
72 OPERATION_UNKNOWN,
73 OPERATION_COPY,
74 OPERATION_MOVE,
75 OPERATION_DELETE
76 })
77 @Retention(RetentionPolicy.SOURCE)
78 public @interface OpType {}
Steve McKaya6bbeab2016-02-17 15:02:01 -080079 public static final int OPERATION_UNKNOWN = -1;
80 public static final int OPERATION_COPY = 1;
81 public static final int OPERATION_MOVE = 2;
82 public static final int OPERATION_DELETE = 3;
Steve McKay14e827a2016-01-06 18:32:13 -080083
84 // TODO: Move it to a shared file when more operations are implemented.
85 public static final int FAILURE_COPY = 1;
86
Steve McKayecbf3c52016-01-13 17:17:39 -080087 // The executor and job factory are visible for testing and non-final
88 // so we'll have a way to inject test doubles from the test. It's
89 // a sub-optimal arrangement.
90 @VisibleForTesting ScheduledExecutorService executor;
91 @VisibleForTesting Factory jobFactory;
Steve McKay14e827a2016-01-06 18:32:13 -080092
Steve McKayecbf3c52016-01-13 17:17:39 -080093 private PowerManager mPowerManager;
94 private PowerManager.WakeLock mWakeLock; // the wake lock, if held.
Steve McKay14e827a2016-01-06 18:32:13 -080095 private NotificationManager mNotificationManager;
96
Steve McKayecbf3c52016-01-13 17:17:39 -080097 @GuardedBy("mRunning")
98 private Map<String, JobRecord> mRunning = new HashMap<>();
Steve McKay14e827a2016-01-06 18:32:13 -080099
Steve McKay35645432016-01-20 15:09:35 -0800100 private int mLastServiceId;
Steve McKay14e827a2016-01-06 18:32:13 -0800101
102 @Override
103 public void onCreate() {
Steve McKayecbf3c52016-01-13 17:17:39 -0800104 // Allow tests to pre-set these with test doubles.
105 if (executor == null) {
106 executor = new ScheduledThreadPoolExecutor(POOL_SIZE);
107 }
108
109 if (jobFactory == null) {
110 jobFactory = Job.Factory.instance;
111 }
Steve McKay14e827a2016-01-06 18:32:13 -0800112
113 if (DEBUG) Log.d(TAG, "Created.");
114 mPowerManager = getSystemService(PowerManager.class);
115 mNotificationManager = getSystemService(NotificationManager.class);
116 }
117
118 @Override
Steve McKay35645432016-01-20 15:09:35 -0800119 public void onDestroy() {
120 if (DEBUG) Log.d(TAG, "Shutting down executor.");
121 List<Runnable> unfinished = executor.shutdownNow();
122 if (!unfinished.isEmpty()) {
123 Log.w(TAG, "Shutting down, but executor reports running jobs: " + unfinished);
124 }
125 executor = null;
126 if (DEBUG) Log.d(TAG, "Destroyed.");
127 }
128
129 @Override
130 public int onStartCommand(Intent intent, int flags, int serviceId) {
Steve McKayecbf3c52016-01-13 17:17:39 -0800131 // TODO: Ensure we're not being called with retry or redeliver.
132 // checkArgument(flags == 0); // retry and redeliver are not supported.
Steve McKay14e827a2016-01-06 18:32:13 -0800133
134 String jobId = intent.getStringExtra(EXTRA_JOB_ID);
135 @OpType int operationType = intent.getIntExtra(EXTRA_OPERATION, OPERATION_UNKNOWN);
Steve McKaya1f76802016-02-25 13:34:03 -0800136 assert(jobId != null);
Steve McKayecbf3c52016-01-13 17:17:39 -0800137
Steve McKay14e827a2016-01-06 18:32:13 -0800138 if (intent.hasExtra(EXTRA_CANCEL)) {
139 handleCancel(intent);
Steve McKayecbf3c52016-01-13 17:17:39 -0800140 } else {
Steve McKaya1f76802016-02-25 13:34:03 -0800141 assert(operationType != OPERATION_UNKNOWN);
Steve McKay35645432016-01-20 15:09:35 -0800142 handleOperation(intent, serviceId, jobId, operationType);
Steve McKay14e827a2016-01-06 18:32:13 -0800143 }
144
Steve McKayecbf3c52016-01-13 17:17:39 -0800145 return START_NOT_STICKY;
146 }
Steve McKay14e827a2016-01-06 18:32:13 -0800147
Steve McKay35645432016-01-20 15:09:35 -0800148 private void handleOperation(Intent intent, int serviceId, String jobId, int operationType) {
149 if (DEBUG) Log.d(TAG, "onStartCommand: " + jobId + " with serviceId " + serviceId);
Steve McKay14e827a2016-01-06 18:32:13 -0800150
Steve McKay35645432016-01-20 15:09:35 -0800151 // Track the service supplied id so we can stop the service once we're out of work to do.
152 mLastServiceId = serviceId;
Steve McKay14e827a2016-01-06 18:32:13 -0800153
Steve McKayecbf3c52016-01-13 17:17:39 -0800154 Job job = null;
155 synchronized (mRunning) {
156 if (mWakeLock == null) {
Tomasz Mikolajewskib8436af2016-01-25 16:20:15 +0900157 mWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
Steve McKay14e827a2016-01-06 18:32:13 -0800158 }
159
Steve McKayecbf3c52016-01-13 17:17:39 -0800160 List<DocumentInfo> srcs = intent.getParcelableArrayListExtra(EXTRA_SRC_LIST);
Tomasz Mikolajewskib8436af2016-01-25 16:20:15 +0900161 DocumentInfo srcParent = intent.getParcelableExtra(EXTRA_SRC_PARENT);
Steve McKayecbf3c52016-01-13 17:17:39 -0800162 DocumentStack stack = intent.getParcelableExtra(Shared.EXTRA_STACK);
Steve McKay14e827a2016-01-06 18:32:13 -0800163
Tomasz Mikolajewskib8436af2016-01-25 16:20:15 +0900164 job = createJob(operationType, jobId, srcs, srcParent, stack);
Steve McKayecbf3c52016-01-13 17:17:39 -0800165
Steve McKay35645432016-01-20 15:09:35 -0800166 if (job == null) {
167 return;
168 }
169
Steve McKayecbf3c52016-01-13 17:17:39 -0800170 mWakeLock.acquire();
Steve McKay14e827a2016-01-06 18:32:13 -0800171 }
Steve McKayecbf3c52016-01-13 17:17:39 -0800172
Steve McKaya1f76802016-02-25 13:34:03 -0800173 assert(job != null);
Steve McKayecbf3c52016-01-13 17:17:39 -0800174 int delay = intent.getIntExtra(EXTRA_DELAY, DEFAULT_DELAY);
Steve McKaya1f76802016-02-25 13:34:03 -0800175 assert(delay <= MAX_DELAY);
Steve McKay35645432016-01-20 15:09:35 -0800176 if (DEBUG) Log.d(
177 TAG, "Scheduling job " + job.id + " to run in " + delay + " milliseconds.");
Steve McKayecbf3c52016-01-13 17:17:39 -0800178 ScheduledFuture<?> future = executor.schedule(job, delay, TimeUnit.MILLISECONDS);
179 mRunning.put(jobId, new JobRecord(job, future));
Steve McKay14e827a2016-01-06 18:32:13 -0800180 }
181
182 /**
183 * Cancels the operation corresponding to job id, identified in "EXTRA_JOB_ID".
184 *
185 * @param intent The cancellation intent.
186 */
187 private void handleCancel(Intent intent) {
Steve McKaya1f76802016-02-25 13:34:03 -0800188 assert(intent.hasExtra(EXTRA_CANCEL));
189 assert(intent.getStringExtra(EXTRA_JOB_ID) != null);
190
191 String jobId = intent.getStringExtra(EXTRA_JOB_ID);
Steve McKay14e827a2016-01-06 18:32:13 -0800192
Steve McKayecbf3c52016-01-13 17:17:39 -0800193 if (DEBUG) Log.d(TAG, "handleCancel: " + jobId);
194
195 synchronized (mRunning) {
196 // Do nothing if the cancelled ID doesn't match the current job ID. This prevents racey
197 // cancellation requests from affecting unrelated copy jobs. However, if the current job ID
198 // is null, the service most likely crashed and was revived by the incoming cancel intent.
199 // In that case, always allow the cancellation to proceed.
200 JobRecord record = mRunning.get(jobId);
201 if (record != null) {
202 record.job.cancel();
203
204 // If the job hasn't been started, cancel it and explicitly clean up.
205 // If it *has* been started, we wait for it to recognize this, then
206 // allow it stop working in an orderly fashion.
207 if (record.future.getDelay(TimeUnit.MILLISECONDS) > 0) {
208 record.future.cancel(false);
209 onFinished(record.job);
210 }
211 }
Steve McKay14e827a2016-01-06 18:32:13 -0800212 }
213
214 // Dismiss the progress notification here rather than in the copy loop. This preserves
215 // interactivity for the user in case the copy loop is stalled.
216 // Try to cancel it even if we don't have a job id...in case there is some sad
217 // orphan notification.
Tomasz Mikolajewskife0c7352016-01-21 17:48:03 +0900218 mNotificationManager.cancel(jobId, NOTIFICATION_ID_PROGRESS);
Steve McKayecbf3c52016-01-13 17:17:39 -0800219
220 // TODO: Guarantee the job is being finalized
Steve McKay14e827a2016-01-06 18:32:13 -0800221 }
222
Steve McKay35645432016-01-20 15:09:35 -0800223 /**
224 * Creates a new job. Returns null if a job with {@code id} already exists.
225 * @return
226 */
Steve McKayecbf3c52016-01-13 17:17:39 -0800227 @GuardedBy("mRunning")
Steve McKay35645432016-01-20 15:09:35 -0800228 private @Nullable Job createJob(
Tomasz Mikolajewskib8436af2016-01-25 16:20:15 +0900229 @OpType int operationType, String id, List<DocumentInfo> srcs, DocumentInfo srcParent,
230 DocumentStack stack) {
Steve McKay14e827a2016-01-06 18:32:13 -0800231
Steve McKay35645432016-01-20 15:09:35 -0800232 if (mRunning.containsKey(id)) {
233 Log.w(TAG, "Duplicate job id: " + id
234 + ". Ignoring job request for srcs: " + srcs + ", stack: " + stack + ".");
235 return null;
236 }
Steve McKay14e827a2016-01-06 18:32:13 -0800237
Steve McKayecbf3c52016-01-13 17:17:39 -0800238 Job job = null;
Steve McKay14e827a2016-01-06 18:32:13 -0800239 switch (operationType) {
240 case OPERATION_COPY:
Steve McKayecbf3c52016-01-13 17:17:39 -0800241 job = jobFactory.createCopy(this, getApplicationContext(), this, id, stack, srcs);
Steve McKay14e827a2016-01-06 18:32:13 -0800242 break;
243 case OPERATION_MOVE:
Tomasz Mikolajewskib8436af2016-01-25 16:20:15 +0900244 job = jobFactory.createMove(this, getApplicationContext(), this, id, stack, srcs,
245 srcParent);
Steve McKay14e827a2016-01-06 18:32:13 -0800246 break;
247 case OPERATION_DELETE:
Tomasz Mikolajewskib8436af2016-01-25 16:20:15 +0900248 job = jobFactory.createDelete(this, getApplicationContext(), this, id, stack, srcs,
249 srcParent);
Steve McKay35645432016-01-20 15:09:35 -0800250 break;
Steve McKay14e827a2016-01-06 18:32:13 -0800251 default:
252 throw new UnsupportedOperationException();
253 }
254
Steve McKaya1f76802016-02-25 13:34:03 -0800255 assert(job != null);
256 return job;
Steve McKay14e827a2016-01-06 18:32:13 -0800257 }
258
Steve McKayecbf3c52016-01-13 17:17:39 -0800259 @GuardedBy("mRunning")
260 private void deleteJob(Job job) {
261 if (DEBUG) Log.d(TAG, "deleteJob: " + job.id);
262
263 JobRecord record = mRunning.remove(job.id);
Steve McKaya1f76802016-02-25 13:34:03 -0800264 assert(record != null);
Steve McKayecbf3c52016-01-13 17:17:39 -0800265 record.job.cleanup();
266
267 if (mRunning.isEmpty()) {
268 shutdown();
269 }
270 }
271
272 /**
273 * Most likely shuts down. Won't shut down if service has a pending
Steve McKay35645432016-01-20 15:09:35 -0800274 * message. Thread pool is deal with in onDestroy.
Steve McKayecbf3c52016-01-13 17:17:39 -0800275 */
276 private void shutdown() {
Steve McKay35645432016-01-20 15:09:35 -0800277 if (DEBUG) Log.d(TAG, "Shutting down. Last serviceId was " + mLastServiceId);
Steve McKayecbf3c52016-01-13 17:17:39 -0800278 mWakeLock.release();
279 mWakeLock = null;
Steve McKay35645432016-01-20 15:09:35 -0800280
281 // Turns out, for us, stopSelfResult always returns false in tests,
282 // so we can't guard executor shutdown. For this reason we move
283 // executor shutdown to #onDestroy.
284 boolean gonnaStop = stopSelfResult(mLastServiceId);
Steve McKayecbf3c52016-01-13 17:17:39 -0800285 if (DEBUG) Log.d(TAG, "Stopping service: " + gonnaStop);
286 if (!gonnaStop) {
287 Log.w(TAG, "Service should be stopping, but reports otherwise.");
288 }
Steve McKayecbf3c52016-01-13 17:17:39 -0800289 }
290
291 @VisibleForTesting
292 boolean holdsWakeLock() {
293 return mWakeLock != null && mWakeLock.isHeld();
294 }
295
296 @Override
297 public void onStart(Job job) {
298 if (DEBUG) Log.d(TAG, "onStart: " + job.id);
Tomasz Mikolajewskife0c7352016-01-21 17:48:03 +0900299 mNotificationManager.notify(job.id, NOTIFICATION_ID_PROGRESS, job.getSetupNotification());
Steve McKayecbf3c52016-01-13 17:17:39 -0800300 }
301
302 @Override
303 public void onFinished(Job job) {
304 if (DEBUG) Log.d(TAG, "onFinished: " + job.id);
305
306 // Dismiss the ongoing copy notification when the copy is done.
Tomasz Mikolajewskife0c7352016-01-21 17:48:03 +0900307 mNotificationManager.cancel(job.id, NOTIFICATION_ID_PROGRESS);
Steve McKayecbf3c52016-01-13 17:17:39 -0800308
Tomasz Mikolajewski748ea8c2016-01-22 16:22:51 +0900309 if (job.hasFailures()) {
310 Log.e(TAG, "Job failed on files: " + job.failedFiles.size() + ".");
311 mNotificationManager.notify(
312 job.id, NOTIFICATION_ID_FAILURE, job.getFailureNotification());
313 }
314
315 if (job.hasWarnings()) {
316 if (DEBUG) Log.d(TAG, "Job finished with warnings.");
317 mNotificationManager.notify(
318 job.id, NOTIFICATION_ID_WARNING, job.getWarningNotification());
319 }
320
Steve McKayecbf3c52016-01-13 17:17:39 -0800321 synchronized (mRunning) {
322 deleteJob(job);
323 }
Steve McKay14e827a2016-01-06 18:32:13 -0800324 }
325
326 @Override
327 public void onProgress(CopyJob job) {
Steve McKayecbf3c52016-01-13 17:17:39 -0800328 if (DEBUG) Log.d(TAG, "onProgress: " + job.id);
Tomasz Mikolajewskife0c7352016-01-21 17:48:03 +0900329 mNotificationManager.notify(
330 job.id, NOTIFICATION_ID_PROGRESS, job.getProgressNotification());
Steve McKay14e827a2016-01-06 18:32:13 -0800331 }
332
Steve McKayecbf3c52016-01-13 17:17:39 -0800333 private static final class JobRecord {
334 private final Job job;
335 private final ScheduledFuture<?> future;
336
337 public JobRecord(Job job, ScheduledFuture<?> future) {
338 this.job = job;
339 this.future = future;
340 }
Steve McKay14e827a2016-01-06 18:32:13 -0800341 }
342
Steve McKayecbf3c52016-01-13 17:17:39 -0800343 @Override
344 public IBinder onBind(Intent intent) {
345 return null; // Boilerplate. See super#onBind
Steve McKay14e827a2016-01-06 18:32:13 -0800346 }
347}