blob: 3a025c28c1d96891d808ae0de8ed87a6d2531403 [file] [log] [blame]
Steve McKayc83baa02016-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 McKayc83baa02016-01-06 18:32:13 -080019import static com.android.documentsui.Shared.DEBUG;
20import static com.android.internal.util.Preconditions.checkArgument;
21import static com.android.internal.util.Preconditions.checkNotNull;
22import static com.android.internal.util.Preconditions.checkState;
23
24import android.annotation.IntDef;
Steve McKayc83baa02016-01-06 18:32:13 -080025import android.app.NotificationManager;
Steve McKaybbeba522016-01-13 17:17:39 -080026import android.app.Service;
Steve McKayc83baa02016-01-06 18:32:13 -080027import android.content.Intent;
Steve McKaybbeba522016-01-13 17:17:39 -080028import android.os.IBinder;
Steve McKayc83baa02016-01-06 18:32:13 -080029import android.os.PowerManager;
Steve McKay97b4be42016-01-20 15:09:35 -080030import android.support.annotation.Nullable;
Steve McKayc83baa02016-01-06 18:32:13 -080031import android.support.annotation.VisibleForTesting;
32import android.util.Log;
33
34import com.android.documentsui.Shared;
35import com.android.documentsui.model.DocumentInfo;
36import com.android.documentsui.model.DocumentStack;
Steve McKaybbeba522016-01-13 17:17:39 -080037import com.android.documentsui.services.Job.Factory;
Steve McKayc83baa02016-01-06 18:32:13 -080038
39import java.lang.annotation.Retention;
40import java.lang.annotation.RetentionPolicy;
Steve McKaybbeba522016-01-13 17:17:39 -080041import java.util.HashMap;
Steve McKayc83baa02016-01-06 18:32:13 -080042import java.util.List;
Steve McKaybbeba522016-01-13 17:17:39 -080043import java.util.Map;
44import java.util.concurrent.ScheduledExecutorService;
45import java.util.concurrent.ScheduledFuture;
46import java.util.concurrent.ScheduledThreadPoolExecutor;
47import java.util.concurrent.TimeUnit;
Steve McKayc83baa02016-01-06 18:32:13 -080048
Steve McKaybbeba522016-01-13 17:17:39 -080049import javax.annotation.concurrent.GuardedBy;
50
51public class FileOperationService extends Service implements Job.Listener {
52
53 private static final int DEFAULT_DELAY = 0;
54 private static final int MAX_DELAY = 10 * 1000; // ten seconds
Tomasz Mikolajewski6b6c16e2016-01-21 17:48:03 +090055 private static final int POOL_SIZE = 2; // "pool size", not *max* "pool size".
56 private static final int NOTIFICATION_ID_PROGRESS = 0;
57 private static final int NOTIFICATION_ID_FAILURE = 1;
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +090058 private static final int NOTIFICATION_ID_WARNING = 2;
Steve McKaybbeba522016-01-13 17:17:39 -080059
Steve McKayc83baa02016-01-06 18:32:13 -080060 public static final String TAG = "FileOperationService";
61
62 public static final String EXTRA_JOB_ID = "com.android.documentsui.JOB_ID";
Steve McKaybbeba522016-01-13 17:17:39 -080063 public static final String EXTRA_DELAY = "com.android.documentsui.DELAY";
Steve McKayc83baa02016-01-06 18:32:13 -080064 public static final String EXTRA_OPERATION = "com.android.documentsui.OPERATION";
65 public static final String EXTRA_CANCEL = "com.android.documentsui.CANCEL";
66 public static final String EXTRA_SRC_LIST = "com.android.documentsui.SRC_LIST";
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +090067 public static final String EXTRA_DIALOG_TYPE = "com.android.documentsui.DIALOG_TYPE";
Steve McKayc83baa02016-01-06 18:32:13 -080068
Tomasz Mikolajewskie0094412016-01-25 16:20:15 +090069 // This extra is used only for moving and deleting. Currently it's not the case,
70 // but in the future those files may be from multiple different parents. In
71 // such case, this needs to be replaced with pairs of parent and child.
72 public static final String EXTRA_SRC_PARENT = "com.android.documentsui.SRC_PARENT";
73
Steve McKayc83baa02016-01-06 18:32:13 -080074 public static final int OPERATION_UNKNOWN = -1;
75 public static final int OPERATION_COPY = 1;
76 public static final int OPERATION_MOVE = 2;
77 public static final int OPERATION_DELETE = 3;
78
79 @IntDef(flag = true, value = {
80 OPERATION_UNKNOWN,
81 OPERATION_COPY,
82 OPERATION_MOVE,
83 OPERATION_DELETE
84 })
85 @Retention(RetentionPolicy.SOURCE)
86 public @interface OpType {}
87
88 // TODO: Move it to a shared file when more operations are implemented.
89 public static final int FAILURE_COPY = 1;
90
Steve McKaybbeba522016-01-13 17:17:39 -080091 // The executor and job factory are visible for testing and non-final
92 // so we'll have a way to inject test doubles from the test. It's
93 // a sub-optimal arrangement.
94 @VisibleForTesting ScheduledExecutorService executor;
95 @VisibleForTesting Factory jobFactory;
Steve McKayc83baa02016-01-06 18:32:13 -080096
Steve McKaybbeba522016-01-13 17:17:39 -080097 private PowerManager mPowerManager;
98 private PowerManager.WakeLock mWakeLock; // the wake lock, if held.
Steve McKayc83baa02016-01-06 18:32:13 -080099 private NotificationManager mNotificationManager;
100
Steve McKaybbeba522016-01-13 17:17:39 -0800101 @GuardedBy("mRunning")
102 private Map<String, JobRecord> mRunning = new HashMap<>();
Steve McKayc83baa02016-01-06 18:32:13 -0800103
Steve McKay97b4be42016-01-20 15:09:35 -0800104 private int mLastServiceId;
Steve McKayc83baa02016-01-06 18:32:13 -0800105
106 @Override
107 public void onCreate() {
Steve McKaybbeba522016-01-13 17:17:39 -0800108 // Allow tests to pre-set these with test doubles.
109 if (executor == null) {
110 executor = new ScheduledThreadPoolExecutor(POOL_SIZE);
111 }
112
113 if (jobFactory == null) {
114 jobFactory = Job.Factory.instance;
115 }
Steve McKayc83baa02016-01-06 18:32:13 -0800116
117 if (DEBUG) Log.d(TAG, "Created.");
118 mPowerManager = getSystemService(PowerManager.class);
119 mNotificationManager = getSystemService(NotificationManager.class);
120 }
121
122 @Override
Steve McKay97b4be42016-01-20 15:09:35 -0800123 public void onDestroy() {
124 if (DEBUG) Log.d(TAG, "Shutting down executor.");
125 List<Runnable> unfinished = executor.shutdownNow();
126 if (!unfinished.isEmpty()) {
127 Log.w(TAG, "Shutting down, but executor reports running jobs: " + unfinished);
128 }
129 executor = null;
130 if (DEBUG) Log.d(TAG, "Destroyed.");
131 }
132
133 @Override
134 public int onStartCommand(Intent intent, int flags, int serviceId) {
Steve McKaybbeba522016-01-13 17:17:39 -0800135 // TODO: Ensure we're not being called with retry or redeliver.
136 // checkArgument(flags == 0); // retry and redeliver are not supported.
Steve McKayc83baa02016-01-06 18:32:13 -0800137
138 String jobId = intent.getStringExtra(EXTRA_JOB_ID);
139 @OpType int operationType = intent.getIntExtra(EXTRA_OPERATION, OPERATION_UNKNOWN);
140 checkArgument(jobId != null);
Steve McKaybbeba522016-01-13 17:17:39 -0800141
Steve McKayc83baa02016-01-06 18:32:13 -0800142 if (intent.hasExtra(EXTRA_CANCEL)) {
143 handleCancel(intent);
Steve McKaybbeba522016-01-13 17:17:39 -0800144 } else {
145 checkArgument(operationType != OPERATION_UNKNOWN);
Steve McKay97b4be42016-01-20 15:09:35 -0800146 handleOperation(intent, serviceId, jobId, operationType);
Steve McKayc83baa02016-01-06 18:32:13 -0800147 }
148
Steve McKaybbeba522016-01-13 17:17:39 -0800149 return START_NOT_STICKY;
150 }
Steve McKayc83baa02016-01-06 18:32:13 -0800151
Steve McKay97b4be42016-01-20 15:09:35 -0800152 private void handleOperation(Intent intent, int serviceId, String jobId, int operationType) {
153 if (DEBUG) Log.d(TAG, "onStartCommand: " + jobId + " with serviceId " + serviceId);
Steve McKayc83baa02016-01-06 18:32:13 -0800154
Steve McKay97b4be42016-01-20 15:09:35 -0800155 // Track the service supplied id so we can stop the service once we're out of work to do.
156 mLastServiceId = serviceId;
Steve McKayc83baa02016-01-06 18:32:13 -0800157
Steve McKaybbeba522016-01-13 17:17:39 -0800158 Job job = null;
159 synchronized (mRunning) {
160 if (mWakeLock == null) {
Tomasz Mikolajewskie0094412016-01-25 16:20:15 +0900161 mWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
Steve McKayc83baa02016-01-06 18:32:13 -0800162 }
163
Steve McKaybbeba522016-01-13 17:17:39 -0800164 List<DocumentInfo> srcs = intent.getParcelableArrayListExtra(EXTRA_SRC_LIST);
Tomasz Mikolajewskie0094412016-01-25 16:20:15 +0900165 DocumentInfo srcParent = intent.getParcelableExtra(EXTRA_SRC_PARENT);
Steve McKaybbeba522016-01-13 17:17:39 -0800166 DocumentStack stack = intent.getParcelableExtra(Shared.EXTRA_STACK);
Steve McKayc83baa02016-01-06 18:32:13 -0800167
Tomasz Mikolajewskie0094412016-01-25 16:20:15 +0900168 job = createJob(operationType, jobId, srcs, srcParent, stack);
Steve McKaybbeba522016-01-13 17:17:39 -0800169
Steve McKay97b4be42016-01-20 15:09:35 -0800170 if (job == null) {
171 return;
172 }
173
Steve McKaybbeba522016-01-13 17:17:39 -0800174 mWakeLock.acquire();
Steve McKayc83baa02016-01-06 18:32:13 -0800175 }
Steve McKaybbeba522016-01-13 17:17:39 -0800176
177 checkState(job != null);
178 int delay = intent.getIntExtra(EXTRA_DELAY, DEFAULT_DELAY);
179 checkArgument(delay <= MAX_DELAY);
Steve McKay97b4be42016-01-20 15:09:35 -0800180 if (DEBUG) Log.d(
181 TAG, "Scheduling job " + job.id + " to run in " + delay + " milliseconds.");
Steve McKaybbeba522016-01-13 17:17:39 -0800182 ScheduledFuture<?> future = executor.schedule(job, delay, TimeUnit.MILLISECONDS);
183 mRunning.put(jobId, new JobRecord(job, future));
Steve McKayc83baa02016-01-06 18:32:13 -0800184 }
185
186 /**
187 * Cancels the operation corresponding to job id, identified in "EXTRA_JOB_ID".
188 *
189 * @param intent The cancellation intent.
190 */
191 private void handleCancel(Intent intent) {
192 checkArgument(intent.hasExtra(EXTRA_CANCEL));
193 String jobId = checkNotNull(intent.getStringExtra(EXTRA_JOB_ID));
194
Steve McKaybbeba522016-01-13 17:17:39 -0800195 if (DEBUG) Log.d(TAG, "handleCancel: " + jobId);
196
197 synchronized (mRunning) {
198 // Do nothing if the cancelled ID doesn't match the current job ID. This prevents racey
199 // cancellation requests from affecting unrelated copy jobs. However, if the current job ID
200 // is null, the service most likely crashed and was revived by the incoming cancel intent.
201 // In that case, always allow the cancellation to proceed.
202 JobRecord record = mRunning.get(jobId);
203 if (record != null) {
204 record.job.cancel();
205
206 // If the job hasn't been started, cancel it and explicitly clean up.
207 // If it *has* been started, we wait for it to recognize this, then
208 // allow it stop working in an orderly fashion.
209 if (record.future.getDelay(TimeUnit.MILLISECONDS) > 0) {
210 record.future.cancel(false);
211 onFinished(record.job);
212 }
213 }
Steve McKayc83baa02016-01-06 18:32:13 -0800214 }
215
216 // Dismiss the progress notification here rather than in the copy loop. This preserves
217 // interactivity for the user in case the copy loop is stalled.
218 // Try to cancel it even if we don't have a job id...in case there is some sad
219 // orphan notification.
Tomasz Mikolajewski6b6c16e2016-01-21 17:48:03 +0900220 mNotificationManager.cancel(jobId, NOTIFICATION_ID_PROGRESS);
Steve McKaybbeba522016-01-13 17:17:39 -0800221
222 // TODO: Guarantee the job is being finalized
Steve McKayc83baa02016-01-06 18:32:13 -0800223 }
224
Steve McKay97b4be42016-01-20 15:09:35 -0800225 /**
226 * Creates a new job. Returns null if a job with {@code id} already exists.
227 * @return
228 */
Steve McKaybbeba522016-01-13 17:17:39 -0800229 @GuardedBy("mRunning")
Steve McKay97b4be42016-01-20 15:09:35 -0800230 private @Nullable Job createJob(
Tomasz Mikolajewskie0094412016-01-25 16:20:15 +0900231 @OpType int operationType, String id, List<DocumentInfo> srcs, DocumentInfo srcParent,
232 DocumentStack stack) {
Steve McKayc83baa02016-01-06 18:32:13 -0800233
Steve McKay97b4be42016-01-20 15:09:35 -0800234 if (mRunning.containsKey(id)) {
235 Log.w(TAG, "Duplicate job id: " + id
236 + ". Ignoring job request for srcs: " + srcs + ", stack: " + stack + ".");
237 return null;
238 }
Steve McKayc83baa02016-01-06 18:32:13 -0800239
Steve McKaybbeba522016-01-13 17:17:39 -0800240 Job job = null;
Steve McKayc83baa02016-01-06 18:32:13 -0800241 switch (operationType) {
242 case OPERATION_COPY:
Steve McKaybbeba522016-01-13 17:17:39 -0800243 job = jobFactory.createCopy(this, getApplicationContext(), this, id, stack, srcs);
Steve McKayc83baa02016-01-06 18:32:13 -0800244 break;
245 case OPERATION_MOVE:
Tomasz Mikolajewskie0094412016-01-25 16:20:15 +0900246 job = jobFactory.createMove(this, getApplicationContext(), this, id, stack, srcs,
247 srcParent);
Steve McKayc83baa02016-01-06 18:32:13 -0800248 break;
249 case OPERATION_DELETE:
Tomasz Mikolajewskie0094412016-01-25 16:20:15 +0900250 job = jobFactory.createDelete(this, getApplicationContext(), this, id, stack, srcs,
251 srcParent);
Steve McKay97b4be42016-01-20 15:09:35 -0800252 break;
Steve McKayc83baa02016-01-06 18:32:13 -0800253 default:
254 throw new UnsupportedOperationException();
255 }
256
Steve McKaybbeba522016-01-13 17:17:39 -0800257 return checkNotNull(job);
Steve McKayc83baa02016-01-06 18:32:13 -0800258 }
259
Steve McKaybbeba522016-01-13 17:17:39 -0800260 @GuardedBy("mRunning")
261 private void deleteJob(Job job) {
262 if (DEBUG) Log.d(TAG, "deleteJob: " + job.id);
263
264 JobRecord record = mRunning.remove(job.id);
265 checkArgument(record != null);
266 record.job.cleanup();
267
268 if (mRunning.isEmpty()) {
269 shutdown();
270 }
271 }
272
273 /**
274 * Most likely shuts down. Won't shut down if service has a pending
Steve McKay97b4be42016-01-20 15:09:35 -0800275 * message. Thread pool is deal with in onDestroy.
Steve McKaybbeba522016-01-13 17:17:39 -0800276 */
277 private void shutdown() {
Steve McKay97b4be42016-01-20 15:09:35 -0800278 if (DEBUG) Log.d(TAG, "Shutting down. Last serviceId was " + mLastServiceId);
Steve McKaybbeba522016-01-13 17:17:39 -0800279 mWakeLock.release();
280 mWakeLock = null;
Steve McKay97b4be42016-01-20 15:09:35 -0800281
282 // Turns out, for us, stopSelfResult always returns false in tests,
283 // so we can't guard executor shutdown. For this reason we move
284 // executor shutdown to #onDestroy.
285 boolean gonnaStop = stopSelfResult(mLastServiceId);
Steve McKaybbeba522016-01-13 17:17:39 -0800286 if (DEBUG) Log.d(TAG, "Stopping service: " + gonnaStop);
287 if (!gonnaStop) {
288 Log.w(TAG, "Service should be stopping, but reports otherwise.");
289 }
Steve McKaybbeba522016-01-13 17:17:39 -0800290 }
291
292 @VisibleForTesting
293 boolean holdsWakeLock() {
294 return mWakeLock != null && mWakeLock.isHeld();
295 }
296
297 @Override
298 public void onStart(Job job) {
299 if (DEBUG) Log.d(TAG, "onStart: " + job.id);
Tomasz Mikolajewski6b6c16e2016-01-21 17:48:03 +0900300 mNotificationManager.notify(job.id, NOTIFICATION_ID_PROGRESS, job.getSetupNotification());
Steve McKaybbeba522016-01-13 17:17:39 -0800301 }
302
303 @Override
304 public void onFinished(Job job) {
305 if (DEBUG) Log.d(TAG, "onFinished: " + job.id);
306
307 // Dismiss the ongoing copy notification when the copy is done.
Tomasz Mikolajewski6b6c16e2016-01-21 17:48:03 +0900308 mNotificationManager.cancel(job.id, NOTIFICATION_ID_PROGRESS);
Steve McKaybbeba522016-01-13 17:17:39 -0800309
Tomasz Mikolajewskidd2b31c2016-01-22 16:22:51 +0900310 if (job.hasFailures()) {
311 Log.e(TAG, "Job failed on files: " + job.failedFiles.size() + ".");
312 mNotificationManager.notify(
313 job.id, NOTIFICATION_ID_FAILURE, job.getFailureNotification());
314 }
315
316 if (job.hasWarnings()) {
317 if (DEBUG) Log.d(TAG, "Job finished with warnings.");
318 mNotificationManager.notify(
319 job.id, NOTIFICATION_ID_WARNING, job.getWarningNotification());
320 }
321
Steve McKaybbeba522016-01-13 17:17:39 -0800322 synchronized (mRunning) {
323 deleteJob(job);
324 }
Steve McKayc83baa02016-01-06 18:32:13 -0800325 }
326
327 @Override
328 public void onProgress(CopyJob job) {
Steve McKaybbeba522016-01-13 17:17:39 -0800329 if (DEBUG) Log.d(TAG, "onProgress: " + job.id);
Tomasz Mikolajewski6b6c16e2016-01-21 17:48:03 +0900330 mNotificationManager.notify(
331 job.id, NOTIFICATION_ID_PROGRESS, job.getProgressNotification());
Steve McKayc83baa02016-01-06 18:32:13 -0800332 }
333
Steve McKaybbeba522016-01-13 17:17:39 -0800334 private static final class JobRecord {
335 private final Job job;
336 private final ScheduledFuture<?> future;
337
338 public JobRecord(Job job, ScheduledFuture<?> future) {
339 this.job = job;
340 this.future = future;
341 }
Steve McKayc83baa02016-01-06 18:32:13 -0800342 }
343
Steve McKaybbeba522016-01-13 17:17:39 -0800344 @Override
345 public IBinder onBind(Intent intent) {
346 return null; // Boilerplate. See super#onBind
Steve McKayc83baa02016-01-06 18:32:13 -0800347 }
348}