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
19d9caa6ab53aa784acaf241c0ded3c4ae2d342bf8Steve McKayimport static com.android.documentsui.base.Shared.DEBUG;
20c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay
21c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKayimport android.annotation.IntDef;
220bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tanimport android.app.Notification;
23e97112e02142fda6eaba18bd1fcaf000acb6e5dcGarfield Tanimport android.app.NotificationChannel;
24c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKayimport android.app.NotificationManager;
25bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKayimport android.app.Service;
26c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKayimport android.content.Intent;
2748ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tanimport android.os.Handler;
28bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKayimport android.os.IBinder;
29c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKayimport android.os.PowerManager;
30e97112e02142fda6eaba18bd1fcaf000acb6e5dcGarfield Tanimport android.os.UserManager;
31c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKayimport android.support.annotation.VisibleForTesting;
32c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKayimport android.util.Log;
33c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay
34e97112e02142fda6eaba18bd1fcaf000acb6e5dcGarfield Tanimport com.android.documentsui.R;
35e97112e02142fda6eaba18bd1fcaf000acb6e5dcGarfield Tanimport com.android.documentsui.base.Features;
36e97112e02142fda6eaba18bd1fcaf000acb6e5dcGarfield Tan
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;
460bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tanimport java.util.concurrent.atomic.AtomicReference;
47bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay
48bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKayimport javax.annotation.concurrent.GuardedBy;
49bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay
50bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKaypublic class FileOperationService extends Service implements Job.Listener {
51bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay
52c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay    public static final String TAG = "FileOperationService";
53c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay
544833477d7d42fa79ee42956bae4aebad77074e4bGarfield, Tan    // Extra used for OperationDialogFragment, Notifications and picking copy destination.
554833477d7d42fa79ee42956bae4aebad77074e4bGarfield, Tan    public static final String EXTRA_OPERATION_TYPE = "com.android.documentsui.OPERATION_TYPE";
564833477d7d42fa79ee42956bae4aebad77074e4bGarfield, Tan
574833477d7d42fa79ee42956bae4aebad77074e4bGarfield, Tan    // Extras used for OperationDialogFragment...
584833477d7d42fa79ee42956bae4aebad77074e4bGarfield, Tan    public static final String EXTRA_DIALOG_TYPE = "com.android.documentsui.DIALOG_TYPE";
594833477d7d42fa79ee42956bae4aebad77074e4bGarfield, Tan    public static final String EXTRA_SRC_LIST = "com.android.documentsui.SRC_LIST";
604833477d7d42fa79ee42956bae4aebad77074e4bGarfield, Tan
6199f1dc3da6defec0596864934bfe76adc96a1d62Steve McKay    public static final String EXTRA_FAILED_URIS = "com.android.documentsui.FAILED_URIS";
6299f1dc3da6defec0596864934bfe76adc96a1d62Steve McKay    public static final String EXTRA_FAILED_DOCS = "com.android.documentsui.FAILED_DOCS";
6399f1dc3da6defec0596864934bfe76adc96a1d62Steve McKay
644833477d7d42fa79ee42956bae4aebad77074e4bGarfield, Tan    // Extras used to start or cancel a file operation...
65c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay    public static final String EXTRA_JOB_ID = "com.android.documentsui.JOB_ID";
66c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay    public static final String EXTRA_OPERATION = "com.android.documentsui.OPERATION";
67c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay    public static final String EXTRA_CANCEL = "com.android.documentsui.CANCEL";
68e0094419cd5f4e1cb55bcabc30c7e066d065827cTomasz Mikolajewski
69c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay    @IntDef(flag = true, value = {
70c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay            OPERATION_UNKNOWN,
71c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay            OPERATION_COPY,
724f2254941c4e495e0653bd2773fc8a2ecc6daef3Tomasz Mikolajewski            OPERATION_COMPRESS,
734f2254941c4e495e0653bd2773fc8a2ecc6daef3Tomasz Mikolajewski            OPERATION_EXTRACT,
74c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay            OPERATION_MOVE,
75c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay            OPERATION_DELETE
76c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay    })
77c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay    @Retention(RetentionPolicy.SOURCE)
78c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay    public @interface OpType {}
79f171934ad7a9d901e81a33bbd0ec74aa34a48787Steve McKay    public static final int OPERATION_UNKNOWN = -1;
80f171934ad7a9d901e81a33bbd0ec74aa34a48787Steve McKay    public static final int OPERATION_COPY = 1;
814f2254941c4e495e0653bd2773fc8a2ecc6daef3Tomasz Mikolajewski    public static final int OPERATION_EXTRACT = 2;
824f2254941c4e495e0653bd2773fc8a2ecc6daef3Tomasz Mikolajewski    public static final int OPERATION_COMPRESS = 3;
834f2254941c4e495e0653bd2773fc8a2ecc6daef3Tomasz Mikolajewski    public static final int OPERATION_MOVE = 4;
844f2254941c4e495e0653bd2773fc8a2ecc6daef3Tomasz Mikolajewski    public static final int OPERATION_DELETE = 5;
85c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay
86c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay    // TODO: Move it to a shared file when more operations are implemented.
87c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay    public static final int FAILURE_COPY = 1;
88c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay
89e97112e02142fda6eaba18bd1fcaf000acb6e5dcGarfield Tan    static final String NOTIFICATION_CHANNEL_ID = "channel_id";
90e97112e02142fda6eaba18bd1fcaf000acb6e5dcGarfield Tan
91e97112e02142fda6eaba18bd1fcaf000acb6e5dcGarfield Tan    private static final int POOL_SIZE = 2;  // "pool size", not *max* "pool size".
92e97112e02142fda6eaba18bd1fcaf000acb6e5dcGarfield Tan
93e97112e02142fda6eaba18bd1fcaf000acb6e5dcGarfield Tan    private static final int NOTIFICATION_ID_PROGRESS = 0;
94e97112e02142fda6eaba18bd1fcaf000acb6e5dcGarfield Tan    private static final int NOTIFICATION_ID_FAILURE = 1;
95e97112e02142fda6eaba18bd1fcaf000acb6e5dcGarfield Tan    private static final int NOTIFICATION_ID_WARNING = 2;
96e97112e02142fda6eaba18bd1fcaf000acb6e5dcGarfield Tan
97bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay    // The executor and job factory are visible for testing and non-final
98bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay    // so we'll have a way to inject test doubles from the test. It's
99bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay    // a sub-optimal arrangement.
100810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan    @VisibleForTesting ExecutorService executor;
101810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan
10248ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan    // Use a separate thread pool to prioritize deletions.
103810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan    @VisibleForTesting ExecutorService deletionExecutor;
104c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay
10548ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan    // Use a handler to schedule monitor tasks.
10648ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan    @VisibleForTesting Handler handler;
10748ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan
1080bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan    // Use a foreground manager to change foreground state of this service.
1090bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan    @VisibleForTesting ForegroundManager foregroundManager;
1100bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan
1110bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan    // Use a notification manager to post and cancel notifications for jobs.
1120bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan    @VisibleForTesting NotificationManager notificationManager;
1130bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan
114e97112e02142fda6eaba18bd1fcaf000acb6e5dcGarfield Tan    // Use a features to determine if notification channel is enabled.
115e97112e02142fda6eaba18bd1fcaf000acb6e5dcGarfield Tan    @VisibleForTesting Features features;
116e97112e02142fda6eaba18bd1fcaf000acb6e5dcGarfield Tan
1170bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan    @GuardedBy("mJobs")
1180bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan    private final Map<String, JobRecord> mJobs = new HashMap<>();
1190bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan
1200bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan    // The job whose notification is used to keep the service in foreground mode.
1210bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan    private final AtomicReference<Job> mForegroundJob = new AtomicReference<>();
12200ee050745f8c3558e7ee93d56abeb0a4ad82472Steve McKay
123bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay    private PowerManager mPowerManager;
124bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay    private PowerManager.WakeLock mWakeLock;  // the wake lock, if held.
125c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay
12697b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay    private int mLastServiceId;
127c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay
128c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay    @Override
129c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay    public void onCreate() {
130bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay        // Allow tests to pre-set these with test doubles.
131bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay        if (executor == null) {
132810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan            executor = Executors.newFixedThreadPool(POOL_SIZE);
133810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan        }
134810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan
135810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan        if (deletionExecutor == null) {
136810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan            deletionExecutor = Executors.newCachedThreadPool();
137bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay        }
138bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay
13948ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan        if (handler == null) {
14048ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan            // Monitor tasks are small enough to schedule them on main thread.
14148ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan            handler = new Handler();
14248ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan        }
14348ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan
1440bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        if (foregroundManager == null) {
1450bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            foregroundManager = createForegroundManager(this);
1460bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        }
1470bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan
1480bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        if (notificationManager == null) {
1490bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            notificationManager = getSystemService(NotificationManager.class);
1500bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        }
1510bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan
152e97112e02142fda6eaba18bd1fcaf000acb6e5dcGarfield Tan        features = new Features.RuntimeFeatures(getResources(), UserManager.get(this));
153e97112e02142fda6eaba18bd1fcaf000acb6e5dcGarfield Tan        setUpNotificationChannel();
154e97112e02142fda6eaba18bd1fcaf000acb6e5dcGarfield Tan
155c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay        if (DEBUG) Log.d(TAG, "Created.");
156c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay        mPowerManager = getSystemService(PowerManager.class);
157c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay    }
158c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay
159e97112e02142fda6eaba18bd1fcaf000acb6e5dcGarfield Tan    private void setUpNotificationChannel() {
160e97112e02142fda6eaba18bd1fcaf000acb6e5dcGarfield Tan        if (features.isNotificationChannelEnabled()) {
161e97112e02142fda6eaba18bd1fcaf000acb6e5dcGarfield Tan            NotificationChannel channel = new NotificationChannel(
162e97112e02142fda6eaba18bd1fcaf000acb6e5dcGarfield Tan                    NOTIFICATION_CHANNEL_ID,
163e97112e02142fda6eaba18bd1fcaf000acb6e5dcGarfield Tan                    getString(R.string.app_label),
164e97112e02142fda6eaba18bd1fcaf000acb6e5dcGarfield Tan                    NotificationManager.IMPORTANCE_LOW);
165e97112e02142fda6eaba18bd1fcaf000acb6e5dcGarfield Tan            notificationManager.createNotificationChannel(channel);
166e97112e02142fda6eaba18bd1fcaf000acb6e5dcGarfield Tan        }
167e97112e02142fda6eaba18bd1fcaf000acb6e5dcGarfield Tan    }
168e97112e02142fda6eaba18bd1fcaf000acb6e5dcGarfield Tan
169c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay    @Override
17097b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay    public void onDestroy() {
17197b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay        if (DEBUG) Log.d(TAG, "Shutting down executor.");
17248ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan
17348ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan        List<Runnable> unfinishedCopies = executor.shutdownNow();
17448ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan        List<Runnable> unfinishedDeletions = deletionExecutor.shutdownNow();
17548ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan        List<Runnable> unfinished =
17648ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan                new ArrayList<>(unfinishedCopies.size() + unfinishedDeletions.size());
17748ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan        unfinished.addAll(unfinishedCopies);
17848ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan        unfinished.addAll(unfinishedDeletions);
17997b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay        if (!unfinished.isEmpty()) {
18097b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay            Log.w(TAG, "Shutting down, but executor reports running jobs: " + unfinished);
18197b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay        }
18248ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan
18397b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay        executor = null;
18448ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan        deletionExecutor = null;
18548ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan        handler = null;
186edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan
18797b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay        if (DEBUG) Log.d(TAG, "Destroyed.");
18897b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay    }
18997b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay
19097b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay    @Override
19197b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay    public int onStartCommand(Intent intent, int flags, int serviceId) {
192bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay        // TODO: Ensure we're not being called with retry or redeliver.
193bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay        // checkArgument(flags == 0);  // retry and redeliver are not supported.
194c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay
195c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay        String jobId = intent.getStringExtra(EXTRA_JOB_ID);
1960af8afd3309538dec784ed0c9c35b252a8213123Steve McKay        assert(jobId != null);
197bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay
198edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan        if (DEBUG) Log.d(TAG, "onStartCommand: " + jobId + " with serviceId " + serviceId);
199edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan
200c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay        if (intent.hasExtra(EXTRA_CANCEL)) {
201c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay            handleCancel(intent);
202bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay        } else {
2034833477d7d42fa79ee42956bae4aebad77074e4bGarfield, Tan            FileOperation operation = intent.getParcelableExtra(EXTRA_OPERATION);
2044833477d7d42fa79ee42956bae4aebad77074e4bGarfield, Tan            handleOperation(jobId, operation);
205c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay        }
206c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay
20797b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay        // Track the service supplied id so we can stop the service once we're out of work to do.
20897b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay        mLastServiceId = serviceId;
209c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay
210edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan        return START_NOT_STICKY;
211edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan    }
212edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan
2134833477d7d42fa79ee42956bae4aebad77074e4bGarfield, Tan    private void handleOperation(String jobId, FileOperation operation) {
2140bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        synchronized (mJobs) {
215bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay            if (mWakeLock == null) {
216e0094419cd5f4e1cb55bcabc30c7e066d065827cTomasz Mikolajewski                mWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
217c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay            }
218c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay
2190bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            if (mJobs.containsKey(jobId)) {
2204833477d7d42fa79ee42956bae4aebad77074e4bGarfield, Tan                Log.w(TAG, "Duplicate job id: " + jobId
2214833477d7d42fa79ee42956bae4aebad77074e4bGarfield, Tan                        + ". Ignoring job request for operation: " + operation + ".");
2224833477d7d42fa79ee42956bae4aebad77074e4bGarfield, Tan                return;
2234833477d7d42fa79ee42956bae4aebad77074e4bGarfield, Tan            }
224c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay
225e97112e02142fda6eaba18bd1fcaf000acb6e5dcGarfield Tan            Job job = operation.createJob(this, this, jobId, features);
226bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay
22797b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay            if (job == null) {
22897b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay                return;
22997b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay            }
23097b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay
231ecc05c0286433c00bf9beb78066e64f1a80fc066Garfield, Tan            assert (job != null);
232ecc05c0286433c00bf9beb78066e64f1a80fc066Garfield, Tan            if (DEBUG) Log.d(TAG, "Scheduling job " + job.id + ".");
2334833477d7d42fa79ee42956bae4aebad77074e4bGarfield, Tan            Future<?> future = getExecutorService(operation.getOpType()).submit(job);
2340bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            mJobs.put(jobId, new JobRecord(job, future));
2358b98ab7dba92ec3c9bfe293e7ba24359abd6ddcbGarfield Tan
23618430f69bcf46df19f5752926bc943a93bda20beGarfield Tan            // Acquire wake lock to keep CPU running until we finish all jobs. Acquire wake lock
2370bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            // after we create a job and put it in mJobs to avoid potential leaking of wake lock
23818430f69bcf46df19f5752926bc943a93bda20beGarfield Tan            // in case where job creation fails.
2398b98ab7dba92ec3c9bfe293e7ba24359abd6ddcbGarfield Tan            mWakeLock.acquire();
240ecc05c0286433c00bf9beb78066e64f1a80fc066Garfield, Tan        }
241c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay    }
242c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay
243c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay    /**
244c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay     * Cancels the operation corresponding to job id, identified in "EXTRA_JOB_ID".
245c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay     *
246c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay     * @param intent The cancellation intent.
247c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay     */
248c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay    private void handleCancel(Intent intent) {
2490af8afd3309538dec784ed0c9c35b252a8213123Steve McKay        assert(intent.hasExtra(EXTRA_CANCEL));
2500af8afd3309538dec784ed0c9c35b252a8213123Steve McKay        assert(intent.getStringExtra(EXTRA_JOB_ID) != null);
2510af8afd3309538dec784ed0c9c35b252a8213123Steve McKay
2520af8afd3309538dec784ed0c9c35b252a8213123Steve McKay        String jobId = intent.getStringExtra(EXTRA_JOB_ID);
253c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay
254bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay        if (DEBUG) Log.d(TAG, "handleCancel: " + jobId);
255bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay
2560bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        synchronized (mJobs) {
257bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay            // Do nothing if the cancelled ID doesn't match the current job ID. This prevents racey
258bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay            // cancellation requests from affecting unrelated copy jobs.  However, if the current job ID
259bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay            // is null, the service most likely crashed and was revived by the incoming cancel intent.
260bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay            // In that case, always allow the cancellation to proceed.
2610bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            JobRecord record = mJobs.get(jobId);
262bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay            if (record != null) {
263bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay                record.job.cancel();
264bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay            }
265c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay        }
266c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay
267c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay        // Dismiss the progress notification here rather than in the copy loop. This preserves
268c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay        // interactivity for the user in case the copy loop is stalled.
269c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay        // Try to cancel it even if we don't have a job id...in case there is some sad
270c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay        // orphan notification.
2710bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        notificationManager.cancel(jobId, NOTIFICATION_ID_PROGRESS);
272c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay
273bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay        // TODO: Guarantee the job is being finalized
274c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay    }
275c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay
276810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan    private ExecutorService getExecutorService(@OpType int operationType) {
277810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan        switch (operationType) {
278810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan            case OPERATION_COPY:
2794f2254941c4e495e0653bd2773fc8a2ecc6daef3Tomasz Mikolajewski            case OPERATION_COMPRESS:
2804f2254941c4e495e0653bd2773fc8a2ecc6daef3Tomasz Mikolajewski            case OPERATION_EXTRACT:
281810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan            case OPERATION_MOVE:
282810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan                return executor;
283810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan            case OPERATION_DELETE:
284810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan                return deletionExecutor;
285810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan            default:
286810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan                throw new UnsupportedOperationException();
287810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan        }
288810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan    }
289810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan
2900bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan    @GuardedBy("mJobs")
291bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay    private void deleteJob(Job job) {
292bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay        if (DEBUG) Log.d(TAG, "deleteJob: " + job.id);
293bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay
29418430f69bcf46df19f5752926bc943a93bda20beGarfield Tan        // Release wake lock before clearing jobs just in case we fail to clean them up.
2958b98ab7dba92ec3c9bfe293e7ba24359abd6ddcbGarfield Tan        mWakeLock.release();
2968b98ab7dba92ec3c9bfe293e7ba24359abd6ddcbGarfield Tan        if (!mWakeLock.isHeld()) {
2978b98ab7dba92ec3c9bfe293e7ba24359abd6ddcbGarfield Tan            mWakeLock = null;
2988b98ab7dba92ec3c9bfe293e7ba24359abd6ddcbGarfield Tan        }
2998b98ab7dba92ec3c9bfe293e7ba24359abd6ddcbGarfield Tan
3000bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        JobRecord record = mJobs.remove(job.id);
30118430f69bcf46df19f5752926bc943a93bda20beGarfield Tan        assert(record != null);
30218430f69bcf46df19f5752926bc943a93bda20beGarfield Tan        record.job.cleanup();
30318430f69bcf46df19f5752926bc943a93bda20beGarfield Tan
3040bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        // Delay the shutdown until we've cleaned up all notifications. shutdown() is now posted in
3050bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        // onFinished(Job job) to main thread.
306bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay    }
307bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay
308bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay    /**
309bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay     * Most likely shuts down. Won't shut down if service has a pending
31097b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay     * message. Thread pool is deal with in onDestroy.
311bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay     */
312bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay    private void shutdown() {
31397b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay        if (DEBUG) Log.d(TAG, "Shutting down. Last serviceId was " + mLastServiceId);
31418430f69bcf46df19f5752926bc943a93bda20beGarfield Tan        assert(mWakeLock == null);
31597b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay
31697b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay        // Turns out, for us, stopSelfResult always returns false in tests,
31797b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay        // so we can't guard executor shutdown. For this reason we move
31897b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay        // executor shutdown to #onDestroy.
31997b4be4638f1697895e5d1f3fdf8bf28a3bb9c13Steve McKay        boolean gonnaStop = stopSelfResult(mLastServiceId);
320bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay        if (DEBUG) Log.d(TAG, "Stopping service: " + gonnaStop);
321bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay        if (!gonnaStop) {
322bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay            Log.w(TAG, "Service should be stopping, but reports otherwise.");
323bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay        }
324bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay    }
325bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay
326bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay    @VisibleForTesting
327bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay    boolean holdsWakeLock() {
328bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay        return mWakeLock != null && mWakeLock.isHeld();
329bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay    }
330bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay
331bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay    @Override
332bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay    public void onStart(Job job) {
333bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay        if (DEBUG) Log.d(TAG, "onStart: " + job.id);
33448ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan
3350bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        Notification notification = job.getSetupNotification();
3360bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        // If there is no foreground job yet, set this job to foreground job.
3370bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        if (mForegroundJob.compareAndSet(null, job)) {
3380bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            if (DEBUG) Log.d(TAG, "Set foreground job to " + job.id);
3390bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            foregroundManager.startForeground(NOTIFICATION_ID_PROGRESS, notification);
3400bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        }
3410bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan
34248ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan        // Show start up notification
3430bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        if (DEBUG) Log.d(TAG, "Posting notification for " + job.id);
3440bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        notificationManager.notify(
3450bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan                job.id, NOTIFICATION_ID_PROGRESS, notification);
34648ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan
34748ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan        // Set up related monitor
3480bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        JobMonitor monitor = new JobMonitor(job, notificationManager, handler);
34948ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan        monitor.start();
350c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay    }
351c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay
352bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay    @Override
353bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay    public void onFinished(Job job) {
35448ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan        assert(job.isFinished());
355bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay        if (DEBUG) Log.d(TAG, "onFinished: " + job.id);
356bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay
3570bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        synchronized (mJobs) {
3580bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            // Delete the job from mJobs first to avoid this job being selected as the foreground
3590bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            // task again if we need to swap the foreground job.
3600bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            deleteJob(job);
3610bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan
3620bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            // Update foreground state before cleaning up notification. If the finishing job is the
3630bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            // foreground job, we would need to switch to another one or go to background before
3640bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            // we can clean up notifications.
3650bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            updateForegroundState(job);
3660bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan
3670bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            // Use the same thread of monitors to tackle notifications to avoid race conditions.
3680bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            // Otherwise we may fail to dismiss progress notification.
3690bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            handler.post(() -> cleanUpNotification(job));
3700bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan
3710bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            // Post the shutdown message to main thread after cleanUpNotification() to give it a
3720bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            // chance to run. Otherwise this process may be torn down by Android before we've
3730bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            // cleaned up the notifications of the last job.
3740bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            if (mJobs.isEmpty()) {
3750bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan                handler.post(this::shutdown);
3760bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            }
3770bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        }
3780bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan    }
3790bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan
3800bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan    @GuardedBy("mJobs")
3810bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan    private void updateForegroundState(Job job) {
3820bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        Job candidate = mJobs.isEmpty() ? null : mJobs.values().iterator().next().job;
3830bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan
3840bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        // If foreground job is retiring and there is still work to do, we need to set it to a new
3850bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        // job.
3860bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        if (mForegroundJob.compareAndSet(job, candidate)) {
3870bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            if (candidate == null) {
3880bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan                if (DEBUG) Log.d(TAG, "Stop foreground");
3890bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan                // Remove the notification here just in case we're torn down before we have the
3900bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan                // chance to clean up notifications.
3910bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan                foregroundManager.stopForeground(true);
3920bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            } else {
3930bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan                if (DEBUG) Log.d(TAG, "Switch foreground job to " + candidate.id);
3940bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan
3950bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan                Notification notification = (candidate.getState() == Job.STATE_STARTED)
3960bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan                        ? candidate.getSetupNotification()
3970bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan                        : candidate.getProgressNotification();
3980bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan                foregroundManager.startForeground(NOTIFICATION_ID_PROGRESS, notification);
3990bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan                notificationManager.notify(candidate.id, NOTIFICATION_ID_PROGRESS,
4000bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan                        notification);
40148ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan            }
4020bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        }
4030bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan    }
404dd2b31c758e8d694867af58a425180689d25ead1Tomasz Mikolajewski
4050bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan    private void cleanUpNotification(Job job) {
4060bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan
4070bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        if (DEBUG) Log.d(TAG, "Canceling notification for " + job.id);
4080bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        // Dismiss the ongoing copy notification when the copy is done.
4090bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        notificationManager.cancel(job.id, NOTIFICATION_ID_PROGRESS);
4100bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan
4110bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        if (job.hasFailures()) {
4120bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            if (!job.failedUris.isEmpty()) {
4130bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan                Log.e(TAG, "Job failed to resolve uris: " + job.failedUris + ".");
41448ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan            }
4150bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            if (!job.failedDocs.isEmpty()) {
4160bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan                Log.e(TAG, "Job failed to process docs: " + job.failedDocs + ".");
4170bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            }
4180bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            notificationManager.notify(
4190bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan                    job.id, NOTIFICATION_ID_FAILURE, job.getFailureNotification());
4200bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        }
421dd2b31c758e8d694867af58a425180689d25ead1Tomasz Mikolajewski
4220bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        if (job.hasWarnings()) {
4230bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            if (DEBUG) Log.d(TAG, "Job finished with warnings.");
4240bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            notificationManager.notify(
4250bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan                    job.id, NOTIFICATION_ID_WARNING, job.getWarningNotification());
426bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay        }
427c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay    }
428c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay
429bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay    private static final class JobRecord {
430bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay        private final Job job;
431810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan        private final Future<?> future;
432bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay
433810fb82f4461544ce34a5702668cf0e3123582c2Garfield, Tan        public JobRecord(Job job, Future<?> future) {
434bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay            this.job = job;
435bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay            this.future = future;
436bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay        }
437c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay    }
438c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay
43948ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan    /**
44048ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan     * A class used to periodically polls state of a job.
44148ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan     *
44248ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan     * <p>It's possible that jobs hang because underlying document providers stop responding. We
44348ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan     * still need to update notifications if jobs hang, so instead of jobs pushing their states,
44448ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan     * we poll states of jobs.
44548ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan     */
44648ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan    private static final class JobMonitor implements Runnable {
44748ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan        private static final long PROGRESS_INTERVAL_MILLIS = 500L;
44848ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan
44948ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan        private final Job mJob;
45048ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan        private final NotificationManager mNotificationManager;
45148ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan        private final Handler mHandler;
45248ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan
45348ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan        private JobMonitor(Job job, NotificationManager notificationManager, Handler handler) {
45448ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan            mJob = job;
45548ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan            mNotificationManager = notificationManager;
45648ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan            mHandler = handler;
45748ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan        }
45848ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan
45948ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan        private void start() {
460edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan            mHandler.post(this);
46148ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan        }
46248ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan
46348ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan        @Override
46448ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan        public void run() {
46548ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan            if (mJob.isFinished()) {
46648ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan                // Finish notification is already shown. Progress notification is removed.
46748ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan                // Just finish itself.
46848ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan                return;
46948ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan            }
47048ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan
471edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan            // Only job in set up state has progress bar
472edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan            if (mJob.getState() == Job.STATE_SET_UP) {
473edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan                mNotificationManager.notify(
474edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan                        mJob.id, NOTIFICATION_ID_PROGRESS, mJob.getProgressNotification());
475edce554c3eaff20a9bf349c1bc18c0b49e812c74Garfield, Tan            }
47648ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan
47748ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan            mHandler.postDelayed(this, PROGRESS_INTERVAL_MILLIS);
47848ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan        }
47948ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan    }
48048ef36fc843a46077f2a503db13eaba3afe5b548Garfield, Tan
481bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay    @Override
482bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay    public IBinder onBind(Intent intent) {
483bbeba5265c5baa42c6db93fc8030c6055747da4dSteve McKay        return null;  // Boilerplate. See super#onBind
484c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay    }
4850bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan
4860bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan    private static ForegroundManager createForegroundManager(final Service service) {
4870bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        return new ForegroundManager() {
4880bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            @Override
4890bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            public void startForeground(int id, Notification notification) {
4900bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan                service.startForeground(id, notification);
4910bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            }
4920bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan
4930bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            @Override
4940bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            public void stopForeground(boolean removeNotification) {
4950bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan                service.stopForeground(removeNotification);
4960bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan            }
4970bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        };
4980bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan    }
4990bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan
5000bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan    @VisibleForTesting
5010bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan    interface ForegroundManager {
5020bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        void startForeground(int id, Notification notification);
5030bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan        void stopForeground(boolean removeNotification);
5040bed4ce4cfc5b40fd2027681be1f498135e6486dGarfield Tan    }
505c83baa0574ee9e34c0e06bda1ff08928d880ee36Steve McKay}
506