[go: nahoru, domu]

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
17package com.android.documentsui.services;
18
19import static com.android.documentsui.Shared.DEBUG;
20
21import android.annotation.IntDef;
22import android.app.NotificationManager;
23import android.app.Service;
24import android.content.Intent;
25import android.os.IBinder;
26import android.os.PowerManager;
27import android.support.annotation.Nullable;
28import 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;
34import com.android.documentsui.services.Job.Factory;
35
36import java.lang.annotation.Retention;
37import java.lang.annotation.RetentionPolicy;
38import java.util.HashMap;
39import java.util.List;
40import java.util.Map;
41import java.util.concurrent.ScheduledExecutorService;
42import java.util.concurrent.ScheduledFuture;
43import java.util.concurrent.ScheduledThreadPoolExecutor;
44import java.util.concurrent.TimeUnit;
45
46import 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
52    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;
55    private static final int NOTIFICATION_ID_WARNING = 2;
56
57    public static final String TAG = "FileOperationService";
58
59    public static final String EXTRA_JOB_ID = "com.android.documentsui.JOB_ID";
60    public static final String EXTRA_DELAY = "com.android.documentsui.DELAY";
61    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";
64    public static final String EXTRA_DIALOG_TYPE = "com.android.documentsui.DIALOG_TYPE";
65
66    // 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
71    @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 {}
79    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;
83
84    // TODO: Move it to a shared file when more operations are implemented.
85    public static final int FAILURE_COPY = 1;
86
87    // 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;
92
93    private PowerManager mPowerManager;
94    private PowerManager.WakeLock mWakeLock;  // the wake lock, if held.
95    private NotificationManager mNotificationManager;
96
97    @GuardedBy("mRunning")
98    private Map<String, JobRecord> mRunning = new HashMap<>();
99
100    private int mLastServiceId;
101
102    @Override
103    public void onCreate() {
104        // 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        }
112
113        if (DEBUG) Log.d(TAG, "Created.");
114        mPowerManager = getSystemService(PowerManager.class);
115        mNotificationManager = getSystemService(NotificationManager.class);
116    }
117
118    @Override
119    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) {
131        // TODO: Ensure we're not being called with retry or redeliver.
132        // checkArgument(flags == 0);  // retry and redeliver are not supported.
133
134        String jobId = intent.getStringExtra(EXTRA_JOB_ID);
135        @OpType int operationType = intent.getIntExtra(EXTRA_OPERATION, OPERATION_UNKNOWN);
136        assert(jobId != null);
137
138        if (intent.hasExtra(EXTRA_CANCEL)) {
139            handleCancel(intent);
140        } else {
141            assert(operationType != OPERATION_UNKNOWN);
142            handleOperation(intent, serviceId, jobId, operationType);
143        }
144
145        return START_NOT_STICKY;
146    }
147
148    private void handleOperation(Intent intent, int serviceId, String jobId, int operationType) {
149        if (DEBUG) Log.d(TAG, "onStartCommand: " + jobId + " with serviceId " + serviceId);
150
151        // Track the service supplied id so we can stop the service once we're out of work to do.
152        mLastServiceId = serviceId;
153
154        Job job = null;
155        synchronized (mRunning) {
156            if (mWakeLock == null) {
157                mWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
158            }
159
160            List<DocumentInfo> srcs = intent.getParcelableArrayListExtra(EXTRA_SRC_LIST);
161            DocumentInfo srcParent = intent.getParcelableExtra(EXTRA_SRC_PARENT);
162            DocumentStack stack = intent.getParcelableExtra(Shared.EXTRA_STACK);
163
164            job = createJob(operationType, jobId, srcs, srcParent, stack);
165
166            if (job == null) {
167                return;
168            }
169
170            mWakeLock.acquire();
171        }
172
173        assert(job != null);
174        int delay = intent.getIntExtra(EXTRA_DELAY, DEFAULT_DELAY);
175        assert(delay <= MAX_DELAY);
176        if (DEBUG) Log.d(
177                TAG, "Scheduling job " + job.id + " to run in " + delay + " milliseconds.");
178        ScheduledFuture<?> future = executor.schedule(job, delay, TimeUnit.MILLISECONDS);
179        mRunning.put(jobId, new JobRecord(job, future));
180    }
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) {
188        assert(intent.hasExtra(EXTRA_CANCEL));
189        assert(intent.getStringExtra(EXTRA_JOB_ID) != null);
190
191        String jobId = intent.getStringExtra(EXTRA_JOB_ID);
192
193        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            }
212        }
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.
218        mNotificationManager.cancel(jobId, NOTIFICATION_ID_PROGRESS);
219
220        // TODO: Guarantee the job is being finalized
221    }
222
223    /**
224     * Creates a new job. Returns null if a job with {@code id} already exists.
225     * @return
226     */
227    @GuardedBy("mRunning")
228    private @Nullable Job createJob(
229            @OpType int operationType, String id, List<DocumentInfo> srcs, DocumentInfo srcParent,
230            DocumentStack stack) {
231
232        if (srcs.isEmpty()) {
233            Log.w(TAG, "Ignoring job request with empty srcs list. Id: " + id);
234            return null;
235        }
236
237        if (mRunning.containsKey(id)) {
238            Log.w(TAG, "Duplicate job id: " + id
239                    + ". Ignoring job request for srcs: " + srcs + ", stack: " + stack + ".");
240            return null;
241        }
242
243        switch (operationType) {
244            case OPERATION_COPY:
245                return jobFactory.createCopy(
246                        this, getApplicationContext(), this, id, stack, srcs);
247            case OPERATION_MOVE:
248                return jobFactory.createMove(
249                        this, getApplicationContext(), this, id, stack, srcs,
250                        srcParent);
251            case OPERATION_DELETE:
252                return jobFactory.createDelete(
253                        this, getApplicationContext(), this, id, stack, srcs,
254                        srcParent);
255            default:
256                throw new UnsupportedOperationException();
257        }
258    }
259
260    @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        assert(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
275     * message. Thread pool is deal with in onDestroy.
276     */
277    private void shutdown() {
278        if (DEBUG) Log.d(TAG, "Shutting down. Last serviceId was " + mLastServiceId);
279        mWakeLock.release();
280        mWakeLock = null;
281
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);
286        if (DEBUG) Log.d(TAG, "Stopping service: " + gonnaStop);
287        if (!gonnaStop) {
288            Log.w(TAG, "Service should be stopping, but reports otherwise.");
289        }
290    }
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);
300        mNotificationManager.notify(job.id, NOTIFICATION_ID_PROGRESS, job.getSetupNotification());
301    }
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.
308        mNotificationManager.cancel(job.id, NOTIFICATION_ID_PROGRESS);
309
310        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
322        synchronized (mRunning) {
323            deleteJob(job);
324        }
325    }
326
327    @Override
328    public void onProgress(CopyJob job) {
329        if (DEBUG) Log.d(TAG, "onProgress: " + job.id);
330        mNotificationManager.notify(
331                job.id, NOTIFICATION_ID_PROGRESS, job.getProgressNotification());
332    }
333
334    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        }
342    }
343
344    @Override
345    public IBinder onBind(Intent intent) {
346        return null;  // Boilerplate. See super#onBind
347    }
348}
349