FileOperationService.java revision dd2b31c758e8d694867af58a425180689d25ead1
1edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project/*
2edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project * Copyright (C) 2015 The Android Open Source Project
3edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project *
4edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project * Licensed under the Apache License, Version 2.0 (the "License");
5edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project * you may not use this file except in compliance with the License.
6edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project * You may obtain a copy of the License at
7edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project *
8edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project *      http://www.apache.org/licenses/LICENSE-2.0
9edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project *
10edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project * Unless required by applicable law or agreed to in writing, software
11edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project * distributed under the License is distributed on an "AS IS" BASIS,
12edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project * See the License for the specific language governing permissions and
14edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project * limitations under the License.
15edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project */
16edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project
17edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Projectpackage com.android.documentsui.services;
18d8fb7b586f3cfac42694208547b58438d7f3b3edMathias Agopian
19edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Projectimport static com.android.documentsui.Shared.DEBUG;
20edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Projectimport static com.android.internal.util.Preconditions.checkArgument;
21518ec112f468eb67bf681b3eec896d7bfb4ff98dMathias Agopianimport static com.android.internal.util.Preconditions.checkNotNull;
22518ec112f468eb67bf681b3eec896d7bfb4ff98dMathias Agopianimport static com.android.internal.util.Preconditions.checkState;
23edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project
24edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Projectimport android.annotation.IntDef;
25edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Projectimport android.app.NotificationManager;
26edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Projectimport android.app.Service;
27edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Projectimport android.content.Intent;
28edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Projectimport android.os.IBinder;
29edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Projectimport android.os.PowerManager;
30edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Projectimport android.support.annotation.Nullable;
31edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Projectimport android.support.annotation.VisibleForTesting;
32edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Projectimport android.util.Log;
33edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project
34e03de9379410fd9947189f0f14e3ec457df2ebfeRomain Guyimport com.android.documentsui.Shared;
3524035338ed6329e4d85fb00cf99a91e2cdd55ba5Mathias Agopianimport com.android.documentsui.model.DocumentInfo;
369429e9c8ad8ae41104c693235a9376b3086da2e9Mathias Agopianimport com.android.documentsui.model.DocumentStack;
371cadb25da1ed875bdd078270e642966724a0c39aMathias Agopianimport com.android.documentsui.services.Job.Factory;
38edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project
39864f839e969ba3417d82ab3ff7906b2f69afa900David Liimport java.lang.annotation.Retention;
400469dd6d55fa331bfd7de9431da98b6340d82271Siva Velusamyimport java.lang.annotation.RetentionPolicy;
41518ec112f468eb67bf681b3eec896d7bfb4ff98dMathias Agopianimport java.util.HashMap;
42518ec112f468eb67bf681b3eec896d7bfb4ff98dMathias Agopianimport java.util.List;
43edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Projectimport java.util.Map;
44518ec112f468eb67bf681b3eec896d7bfb4ff98dMathias Agopianimport java.util.concurrent.ScheduledExecutorService;
45518ec112f468eb67bf681b3eec896d7bfb4ff98dMathias Agopianimport java.util.concurrent.ScheduledFuture;
46edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Projectimport java.util.concurrent.ScheduledThreadPoolExecutor;
47edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Projectimport java.util.concurrent.TimeUnit;
48edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project
49edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Projectimport javax.annotation.concurrent.GuardedBy;
50edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project
51ada798b7ca7cabc255aa159964b64975e7fdb2dfMathias Agopianpublic class FileOperationService extends Service implements Job.Listener {
52ada798b7ca7cabc255aa159964b64975e7fdb2dfMathias Agopian
53518ec112f468eb67bf681b3eec896d7bfb4ff98dMathias Agopian    private static final int DEFAULT_DELAY = 0;
54518ec112f468eb67bf681b3eec896d7bfb4ff98dMathias Agopian    private static final int MAX_DELAY = 10 * 1000;  // ten seconds
55edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project    private static final int POOL_SIZE = 2;  // "pool size", not *max* "pool size".
56edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project    private static final int NOTIFICATION_ID_PROGRESS = 0;
57edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project    private static final int NOTIFICATION_ID_FAILURE = 1;
58a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich    private static final int NOTIFICATION_ID_WARNING = 2;
59a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich
60a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich    public static final String TAG = "FileOperationService";
61a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich
62a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich    public static final String EXTRA_JOB_ID = "com.android.documentsui.JOB_ID";
63a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich    public static final String EXTRA_DELAY = "com.android.documentsui.DELAY";
64518ec112f468eb67bf681b3eec896d7bfb4ff98dMathias Agopian    public static final String EXTRA_OPERATION = "com.android.documentsui.OPERATION";
65518ec112f468eb67bf681b3eec896d7bfb4ff98dMathias Agopian    public static final String EXTRA_CANCEL = "com.android.documentsui.CANCEL";
66518ec112f468eb67bf681b3eec896d7bfb4ff98dMathias Agopian    public static final String EXTRA_SRC_LIST = "com.android.documentsui.SRC_LIST";
67518ec112f468eb67bf681b3eec896d7bfb4ff98dMathias Agopian    public static final String EXTRA_DIALOG_TYPE = "com.android.documentsui.DIALOG_TYPE";
68518ec112f468eb67bf681b3eec896d7bfb4ff98dMathias Agopian
69518ec112f468eb67bf681b3eec896d7bfb4ff98dMathias Agopian    // This extra is used only for moving and deleting. Currently it's not the case,
70a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich    // but in the future those files may be from multiple different parents. In
71a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich    // such case, this needs to be replaced with pairs of parent and child.
72a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich    public static final String EXTRA_SRC_PARENT = "com.android.documentsui.SRC_PARENT";
73a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich
74a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich    public static final int OPERATION_UNKNOWN = -1;
75a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich    public static final int OPERATION_COPY = 1;
76a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich    public static final int OPERATION_MOVE = 2;
77a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich    public static final int OPERATION_DELETE = 3;
78a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich
79518ec112f468eb67bf681b3eec896d7bfb4ff98dMathias Agopian    @IntDef(flag = true, value = {
80a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich            OPERATION_UNKNOWN,
81a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich            OPERATION_COPY,
82a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich            OPERATION_MOVE,
83518ec112f468eb67bf681b3eec896d7bfb4ff98dMathias Agopian            OPERATION_DELETE
84518ec112f468eb67bf681b3eec896d7bfb4ff98dMathias Agopian    })
85499c6f02e696622a532a504be9706896aea5a304David Li    @Retention(RetentionPolicy.SOURCE)
862f5a6557ef6a7b9fd33077cfd8a037904d41e3bdDavid Li    public @interface OpType {}
8793a826f78f6313db791e6fc880439189897651b3Siva Velusamy
8893a826f78f6313db791e6fc880439189897651b3Siva Velusamy    // TODO: Move it to a shared file when more operations are implemented.
8993a826f78f6313db791e6fc880439189897651b3Siva Velusamy    public static final int FAILURE_COPY = 1;
902f5a6557ef6a7b9fd33077cfd8a037904d41e3bdDavid Li
912f5a6557ef6a7b9fd33077cfd8a037904d41e3bdDavid Li    // The executor and job factory are visible for testing and non-final
922f5a6557ef6a7b9fd33077cfd8a037904d41e3bdDavid Li    // so we'll have a way to inject test doubles from the test. It's
932f5a6557ef6a7b9fd33077cfd8a037904d41e3bdDavid Li    // a sub-optimal arrangement.
940469dd6d55fa331bfd7de9431da98b6340d82271Siva Velusamy    @VisibleForTesting ScheduledExecutorService executor;
952f5a6557ef6a7b9fd33077cfd8a037904d41e3bdDavid Li    @VisibleForTesting Factory jobFactory;
960469dd6d55fa331bfd7de9431da98b6340d82271Siva Velusamy
9793a826f78f6313db791e6fc880439189897651b3Siva Velusamy    private PowerManager mPowerManager;
9893a826f78f6313db791e6fc880439189897651b3Siva Velusamy    private PowerManager.WakeLock mWakeLock;  // the wake lock, if held.
9993a826f78f6313db791e6fc880439189897651b3Siva Velusamy    private NotificationManager mNotificationManager;
100ccfa5c3364a88b0acdbe555b210bd2bc9feb6285Mathias Agopian
10193a826f78f6313db791e6fc880439189897651b3Siva Velusamy    @GuardedBy("mRunning")
102499c6f02e696622a532a504be9706896aea5a304David Li    private Map<String, JobRecord> mRunning = new HashMap<>();
1032f5a6557ef6a7b9fd33077cfd8a037904d41e3bdDavid Li
1042f5a6557ef6a7b9fd33077cfd8a037904d41e3bdDavid Li    private int mLastServiceId;
105499c6f02e696622a532a504be9706896aea5a304David Li
1060469dd6d55fa331bfd7de9431da98b6340d82271Siva Velusamy    @Override
1070469dd6d55fa331bfd7de9431da98b6340d82271Siva Velusamy    public void onCreate() {
10885f33a7168c5563aa7765b91d5d045fe62bcfcd8David Li        // Allow tests to pre-set these with test doubles.
109a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich        if (executor == null) {
110a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich            executor = new ScheduledThreadPoolExecutor(POOL_SIZE);
111518ec112f468eb67bf681b3eec896d7bfb4ff98dMathias Agopian        }
112518ec112f468eb67bf681b3eec896d7bfb4ff98dMathias Agopian
113a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich        if (jobFactory == null) {
114a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich            jobFactory = Job.Factory.instance;
115ccfa5c3364a88b0acdbe555b210bd2bc9feb6285Mathias Agopian        }
1162f5a6557ef6a7b9fd33077cfd8a037904d41e3bdDavid Li
1170469dd6d55fa331bfd7de9431da98b6340d82271Siva Velusamy        if (DEBUG) Log.d(TAG, "Created.");
118a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich        mPowerManager = getSystemService(PowerManager.class);
119a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich        mNotificationManager = getSystemService(NotificationManager.class);
120a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich    }
121a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich
122a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich    @Override
123a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich    public void onDestroy() {
124a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich        if (DEBUG) Log.d(TAG, "Shutting down executor.");
125a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich        List<Runnable> unfinished = executor.shutdownNow();
126a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich        if (!unfinished.isEmpty()) {
127a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich            Log.w(TAG, "Shutting down, but executor reports running jobs: " + unfinished);
128a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich        }
129518ec112f468eb67bf681b3eec896d7bfb4ff98dMathias Agopian        executor = null;
130a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich        if (DEBUG) Log.d(TAG, "Destroyed.");
131a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich    }
132a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich
133a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich    @Override
134518ec112f468eb67bf681b3eec896d7bfb4ff98dMathias Agopian    public int onStartCommand(Intent intent, int flags, int serviceId) {
135a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich        // TODO: Ensure we're not being called with retry or redeliver.
136a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich        // checkArgument(flags == 0);  // retry and redeliver are not supported.
137a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich
138a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich        String jobId = intent.getStringExtra(EXTRA_JOB_ID);
139a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich        @OpType int operationType = intent.getIntExtra(EXTRA_OPERATION, OPERATION_UNKNOWN);
140edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project        checkArgument(jobId != null);
141edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project
1426f0871222f04dfeb479d37fe9753d491e3150e42Mathias Agopian        if (intent.hasExtra(EXTRA_CANCEL)) {
143518ec112f468eb67bf681b3eec896d7bfb4ff98dMathias Agopian            handleCancel(intent);
144e6f43ddce78d6846af12550ff9193c5c6fe5844bSteve Block        } else {
145d274eae545ded690846416d6bfe987d8405eeabaMathias Agopian            checkArgument(operationType != OPERATION_UNKNOWN);
146ecfe091af3e3e5d7165fe64a5f9c84c4576a6c06Mathias Agopian            handleOperation(intent, serviceId, jobId, operationType);
147ecfe091af3e3e5d7165fe64a5f9c84c4576a6c06Mathias Agopian        }
148ecfe091af3e3e5d7165fe64a5f9c84c4576a6c06Mathias Agopian
149ecfe091af3e3e5d7165fe64a5f9c84c4576a6c06Mathias Agopian        return START_NOT_STICKY;
150ecfe091af3e3e5d7165fe64a5f9c84c4576a6c06Mathias Agopian    }
151ecfe091af3e3e5d7165fe64a5f9c84c4576a6c06Mathias Agopian
152ecfe091af3e3e5d7165fe64a5f9c84c4576a6c06Mathias Agopian    private void handleOperation(Intent intent, int serviceId, String jobId, int operationType) {
153d274eae545ded690846416d6bfe987d8405eeabaMathias Agopian        if (DEBUG) Log.d(TAG, "onStartCommand: " + jobId + " with serviceId " + serviceId);
1546f0871222f04dfeb479d37fe9753d491e3150e42Mathias Agopian
15505c53113e0c73c7cab61edf53524c61c20a547c2Mathias Agopian        // Track the service supplied id so we can stop the service once we're out of work to do.
15605c53113e0c73c7cab61edf53524c61c20a547c2Mathias Agopian        mLastServiceId = serviceId;
157edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project
158edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project        Job job = null;
159edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project        synchronized (mRunning) {
160edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project            if (mWakeLock == null) {
161edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project                mWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
162a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich            }
163a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich
164a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich            List<DocumentInfo> srcs = intent.getParcelableArrayListExtra(EXTRA_SRC_LIST);
165a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich            DocumentInfo srcParent = intent.getParcelableExtra(EXTRA_SRC_PARENT);
166edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project            DocumentStack stack = intent.getParcelableExtra(Shared.EXTRA_STACK);
167edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project
168618fa10949c42eb83fa5fe105fe542bcff833ddaMathias Agopian            job = createJob(operationType, jobId, srcs, srcParent, stack);
169edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project
170618fa10949c42eb83fa5fe105fe542bcff833ddaMathias Agopian            if (job == null) {
17105c53113e0c73c7cab61edf53524c61c20a547c2Mathias Agopian                return;
172a2dd6cf59962e3a21a47df29b2f243e904839ba7Jack Palevich            }
173edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project
174edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project            mWakeLock.acquire();
175edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project        }
176edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project
177edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project        checkState(job != null);
178518ec112f468eb67bf681b3eec896d7bfb4ff98dMathias Agopian        int delay = intent.getIntExtra(EXTRA_DELAY, DEFAULT_DELAY);
179edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project        checkArgument(delay <= MAX_DELAY);
1805b287a6ea8dfac7ab3e03ae1e98f9e2214cbae09Mathias Agopian        if (DEBUG) Log.d(
1813ede7c133af1fc9713d7f2aedd785ce6bad780e8Eric Hassold                TAG, "Scheduling job " + job.id + " to run in " + delay + " milliseconds.");
1825b287a6ea8dfac7ab3e03ae1e98f9e2214cbae09Mathias Agopian        ScheduledFuture<?> future = executor.schedule(job, delay, TimeUnit.MILLISECONDS);
1835b287a6ea8dfac7ab3e03ae1e98f9e2214cbae09Mathias Agopian        mRunning.put(jobId, new JobRecord(job, future));
1845b287a6ea8dfac7ab3e03ae1e98f9e2214cbae09Mathias Agopian    }
1855b287a6ea8dfac7ab3e03ae1e98f9e2214cbae09Mathias Agopian
1863ede7c133af1fc9713d7f2aedd785ce6bad780e8Eric Hassold    /**
1873ede7c133af1fc9713d7f2aedd785ce6bad780e8Eric Hassold     * Cancels the operation corresponding to job id, identified in "EXTRA_JOB_ID".
1883ede7c133af1fc9713d7f2aedd785ce6bad780e8Eric Hassold     *
1893ede7c133af1fc9713d7f2aedd785ce6bad780e8Eric Hassold     * @param intent The cancellation intent.
1907773c435bc5da8217433e1b242d3a6712a17b5f7Mathias Agopian     */
1915b287a6ea8dfac7ab3e03ae1e98f9e2214cbae09Mathias Agopian    private void handleCancel(Intent intent) {
1923ede7c133af1fc9713d7f2aedd785ce6bad780e8Eric Hassold        checkArgument(intent.hasExtra(EXTRA_CANCEL));
1935b287a6ea8dfac7ab3e03ae1e98f9e2214cbae09Mathias Agopian        String jobId = checkNotNull(intent.getStringExtra(EXTRA_JOB_ID));
1945b287a6ea8dfac7ab3e03ae1e98f9e2214cbae09Mathias Agopian
195edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project        if (DEBUG) Log.d(TAG, "handleCancel: " + jobId);
196ada798b7ca7cabc255aa159964b64975e7fdb2dfMathias Agopian
197edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project        synchronized (mRunning) {
198edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project            // Do nothing if the cancelled ID doesn't match the current job ID. This prevents racey
199edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project            // cancellation requests from affecting unrelated copy jobs.  However, if the current job ID
200edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project            // is null, the service most likely crashed and was revived by the incoming cancel intent.
201edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project            // In that case, always allow the cancellation to proceed.
202edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project            JobRecord record = mRunning.get(jobId);
203518ec112f468eb67bf681b3eec896d7bfb4ff98dMathias Agopian            if (record != null) {
204518ec112f468eb67bf681b3eec896d7bfb4ff98dMathias Agopian                record.job.cancel();
205076b1cc3a9b90aa5b381a1ed268ca0b548444c9bMathias Agopian
206076b1cc3a9b90aa5b381a1ed268ca0b548444c9bMathias Agopian                // If the job hasn't been started, cancel it and explicitly clean up.
207518ec112f468eb67bf681b3eec896d7bfb4ff98dMathias Agopian                // If it *has* been started, we wait for it to recognize this, then
208076b1cc3a9b90aa5b381a1ed268ca0b548444c9bMathias Agopian                // allow it stop working in an orderly fashion.
209076b1cc3a9b90aa5b381a1ed268ca0b548444c9bMathias Agopian                if (record.future.getDelay(TimeUnit.MILLISECONDS) > 0) {
2105b287a6ea8dfac7ab3e03ae1e98f9e2214cbae09Mathias Agopian                    record.future.cancel(false);
211076b1cc3a9b90aa5b381a1ed268ca0b548444c9bMathias Agopian                    onFinished(record.job);
212f0480de37492597a5c5cf1e6f8346f1467e3a552Mathias Agopian                }
213f0480de37492597a5c5cf1e6f8346f1467e3a552Mathias Agopian            }
214f0480de37492597a5c5cf1e6f8346f1467e3a552Mathias Agopian        }
215f0480de37492597a5c5cf1e6f8346f1467e3a552Mathias Agopian
216f0480de37492597a5c5cf1e6f8346f1467e3a552Mathias Agopian        // Dismiss the progress notification here rather than in the copy loop. This preserves
217f0480de37492597a5c5cf1e6f8346f1467e3a552Mathias Agopian        // interactivity for the user in case the copy loop is stalled.
218f0480de37492597a5c5cf1e6f8346f1467e3a552Mathias Agopian        // Try to cancel it even if we don't have a job id...in case there is some sad
219f0480de37492597a5c5cf1e6f8346f1467e3a552Mathias Agopian        // orphan notification.
220f0480de37492597a5c5cf1e6f8346f1467e3a552Mathias Agopian        mNotificationManager.cancel(jobId, NOTIFICATION_ID_PROGRESS);
221076b1cc3a9b90aa5b381a1ed268ca0b548444c9bMathias Agopian
222edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project        // TODO: Guarantee the job is being finalized
2235b287a6ea8dfac7ab3e03ae1e98f9e2214cbae09Mathias Agopian    }
2245b287a6ea8dfac7ab3e03ae1e98f9e2214cbae09Mathias Agopian
2255b287a6ea8dfac7ab3e03ae1e98f9e2214cbae09Mathias Agopian    /**
2265b287a6ea8dfac7ab3e03ae1e98f9e2214cbae09Mathias Agopian     * Creates a new job. Returns null if a job with {@code id} already exists.
227076b1cc3a9b90aa5b381a1ed268ca0b548444c9bMathias Agopian     * @return
228ada798b7ca7cabc255aa159964b64975e7fdb2dfMathias Agopian     */
229076b1cc3a9b90aa5b381a1ed268ca0b548444c9bMathias Agopian    @GuardedBy("mRunning")
230076b1cc3a9b90aa5b381a1ed268ca0b548444c9bMathias Agopian    private @Nullable Job createJob(
231923c661a86c9e0737b3f16ceffd77e71e023ca54Mathias Agopian            @OpType int operationType, String id, List<DocumentInfo> srcs, DocumentInfo srcParent,
232923c661a86c9e0737b3f16ceffd77e71e023ca54Mathias Agopian            DocumentStack stack) {
23348d438d05f14c2f4bd83ae89f520368cd49122dfMathias Agopian
23448d438d05f14c2f4bd83ae89f520368cd49122dfMathias Agopian        if (mRunning.containsKey(id)) {
23548d438d05f14c2f4bd83ae89f520368cd49122dfMathias Agopian            Log.w(TAG, "Duplicate job id: " + id
23648d438d05f14c2f4bd83ae89f520368cd49122dfMathias Agopian                    + ". Ignoring job request for srcs: " + srcs + ", stack: " + stack + ".");
23748d438d05f14c2f4bd83ae89f520368cd49122dfMathias Agopian            return null;
23848d438d05f14c2f4bd83ae89f520368cd49122dfMathias Agopian        }
23948d438d05f14c2f4bd83ae89f520368cd49122dfMathias Agopian
24048d438d05f14c2f4bd83ae89f520368cd49122dfMathias Agopian        Job job = null;
24148d438d05f14c2f4bd83ae89f520368cd49122dfMathias Agopian        switch (operationType) {
24248d438d05f14c2f4bd83ae89f520368cd49122dfMathias Agopian            case OPERATION_COPY:
24348d438d05f14c2f4bd83ae89f520368cd49122dfMathias Agopian                job = jobFactory.createCopy(this, getApplicationContext(), this, id, stack, srcs);
24448d438d05f14c2f4bd83ae89f520368cd49122dfMathias Agopian                break;
24548d438d05f14c2f4bd83ae89f520368cd49122dfMathias Agopian            case OPERATION_MOVE:
24648d438d05f14c2f4bd83ae89f520368cd49122dfMathias Agopian                job = jobFactory.createMove(this, getApplicationContext(), this, id, stack, srcs,
24748d438d05f14c2f4bd83ae89f520368cd49122dfMathias Agopian                        srcParent);
24848d438d05f14c2f4bd83ae89f520368cd49122dfMathias Agopian                break;
24948d438d05f14c2f4bd83ae89f520368cd49122dfMathias Agopian            case OPERATION_DELETE:
25048d438d05f14c2f4bd83ae89f520368cd49122dfMathias Agopian                job = jobFactory.createDelete(this, getApplicationContext(), this, id, stack, srcs,
25148d438d05f14c2f4bd83ae89f520368cd49122dfMathias Agopian                        srcParent);
25248d438d05f14c2f4bd83ae89f520368cd49122dfMathias Agopian                break;
253923c661a86c9e0737b3f16ceffd77e71e023ca54Mathias Agopian            default:
254a69e0ed4a38ded9778d37da453899d527c4396b9Mathias Agopian                throw new UnsupportedOperationException();
255923c661a86c9e0737b3f16ceffd77e71e023ca54Mathias Agopian        }
256923c661a86c9e0737b3f16ceffd77e71e023ca54Mathias Agopian
257518ec112f468eb67bf681b3eec896d7bfb4ff98dMathias Agopian        return checkNotNull(job);
258edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project    }
259923c661a86c9e0737b3f16ceffd77e71e023ca54Mathias Agopian
260923c661a86c9e0737b3f16ceffd77e71e023ca54Mathias Agopian    @GuardedBy("mRunning")
261edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project    private void deleteJob(Job job) {
262076b1cc3a9b90aa5b381a1ed268ca0b548444c9bMathias Agopian        if (DEBUG) Log.d(TAG, "deleteJob: " + job.id);
263de58697644a52a614ad9498aa087e95d4a223673Mathias Agopian
264923c661a86c9e0737b3f16ceffd77e71e023ca54Mathias Agopian        JobRecord record = mRunning.remove(job.id);
265518ec112f468eb67bf681b3eec896d7bfb4ff98dMathias Agopian        checkArgument(record != null);
266ada798b7ca7cabc255aa159964b64975e7fdb2dfMathias Agopian        record.job.cleanup();
267ada798b7ca7cabc255aa159964b64975e7fdb2dfMathias Agopian
268edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project        if (mRunning.isEmpty()) {
2697773c435bc5da8217433e1b242d3a6712a17b5f7Mathias Agopian            shutdown();
2707773c435bc5da8217433e1b242d3a6712a17b5f7Mathias Agopian        }
2717773c435bc5da8217433e1b242d3a6712a17b5f7Mathias Agopian    }
2727773c435bc5da8217433e1b242d3a6712a17b5f7Mathias Agopian
273ada798b7ca7cabc255aa159964b64975e7fdb2dfMathias Agopian    /**
274edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project     * Most likely shuts down. Won't shut down if service has a pending
275edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project     * message. Thread pool is deal with in onDestroy.
276ada798b7ca7cabc255aa159964b64975e7fdb2dfMathias Agopian     */
277edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project    private void shutdown() {
278edbf3b6af777b721cd2a1ef461947e51e88241e1The Android Open Source Project        if (DEBUG) Log.d(TAG, "Shutting down. Last serviceId was " + mLastServiceId);
279518ec112f468eb67bf681b3eec896d7bfb4ff98dMathias Agopian        mWakeLock.release();
280518ec112f468eb67bf681b3eec896d7bfb4ff98dMathias Agopian        mWakeLock = null;
281518ec112f468eb67bf681b3eec896d7bfb4ff98dMathias Agopian
282923c661a86c9e0737b3f16ceffd77e71e023ca54Mathias Agopian        // Turns out, for us, stopSelfResult always returns false in tests,
283518ec112f468eb67bf681b3eec896d7bfb4ff98dMathias Agopian        // so we can't guard executor shutdown. For this reason we move
284923c661a86c9e0737b3f16ceffd77e71e023ca54Mathias Agopian        // executor shutdown to #onDestroy.
285518ec112f468eb67bf681b3eec896d7bfb4ff98dMathias Agopian        boolean gonnaStop = stopSelfResult(mLastServiceId);
286923c661a86c9e0737b3f16ceffd77e71e023ca54Mathias Agopian        if (DEBUG) Log.d(TAG, "Stopping service: " + gonnaStop);
287923c661a86c9e0737b3f16ceffd77e71e023ca54Mathias Agopian        if (!gonnaStop) {
288076b1cc3a9b90aa5b381a1ed268ca0b548444c9bMathias Agopian            Log.w(TAG, "Service should be stopping, but reports otherwise.");
2891cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian        }
290e6f43ddce78d6846af12550ff9193c5c6fe5844bSteve Block    }
2911cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian
2921cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian    @VisibleForTesting
29348d438d05f14c2f4bd83ae89f520368cd49122dfMathias Agopian    boolean holdsWakeLock() {
29448d438d05f14c2f4bd83ae89f520368cd49122dfMathias Agopian        return mWakeLock != null && mWakeLock.isHeld();
29548d438d05f14c2f4bd83ae89f520368cd49122dfMathias Agopian    }
2961cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian
2971cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian    @Override
2981cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian    public void onStart(Job job) {
2991cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian        if (DEBUG) Log.d(TAG, "onStart: " + job.id);
3001cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian        mNotificationManager.notify(job.id, NOTIFICATION_ID_PROGRESS, job.getSetupNotification());
3011cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian    }
3021cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian
3031cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian    @Override
3041cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian    public void onFinished(Job job) {
3051cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian        if (DEBUG) Log.d(TAG, "onFinished: " + job.id);
3061cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian
3071cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian        // Dismiss the ongoing copy notification when the copy is done.
3081cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian        mNotificationManager.cancel(job.id, NOTIFICATION_ID_PROGRESS);
3091cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian
3101cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian        if (job.hasFailures()) {
3111cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian            Log.e(TAG, "Job failed on files: " + job.failedFiles.size() + ".");
3121cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian            mNotificationManager.notify(
3131cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian                job.id, NOTIFICATION_ID_FAILURE, job.getFailureNotification());
3141cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian        }
3151cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian
3161cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian        if (job.hasWarnings()) {
3171cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian            if (DEBUG) Log.d(TAG, "Job finished with warnings.");
3181cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian            mNotificationManager.notify(
3191cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian                    job.id, NOTIFICATION_ID_WARNING, job.getWarningNotification());
3201cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian        }
3211cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian
3221cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian        synchronized (mRunning) {
3231cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian            deleteJob(job);
3241cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian        }
3251cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian    }
3261cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian
3271cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian    @Override
3281cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian    public void onProgress(CopyJob job) {
3291cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian        if (DEBUG) Log.d(TAG, "onProgress: " + job.id);
3301cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian        mNotificationManager.notify(
3311cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian                job.id, NOTIFICATION_ID_PROGRESS, job.getProgressNotification());
3321cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian    }
3331cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian
3341cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian    private static final class JobRecord {
3351cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian        private final Job job;
3361cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian        private final ScheduledFuture<?> future;
3371cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian
3381cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian        public JobRecord(Job job, ScheduledFuture<?> future) {
3391cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian            this.job = job;
3401cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian            this.future = future;
3411cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian        }
3421cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian    }
3431cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian
3441cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian    @Override
3451cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian    public IBinder onBind(Intent intent) {
3461cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian        return null;  // Boilerplate. See super#onBind
3471cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian    }
3481cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian}
3491cadb25da1ed875bdd078270e642966724a0c39aMathias Agopian