114e827a958f713d9cdec550e0b1099622cda9479Steve McKay/* 214e827a958f713d9cdec550e0b1099622cda9479Steve McKay * Copyright (C) 2016 The Android Open Source Project 314e827a958f713d9cdec550e0b1099622cda9479Steve McKay * 414e827a958f713d9cdec550e0b1099622cda9479Steve McKay * Licensed under the Apache License, Version 2.0 (the "License"); 514e827a958f713d9cdec550e0b1099622cda9479Steve McKay * you may not use this file except in compliance with the License. 614e827a958f713d9cdec550e0b1099622cda9479Steve McKay * You may obtain a copy of the License at 714e827a958f713d9cdec550e0b1099622cda9479Steve McKay * 814e827a958f713d9cdec550e0b1099622cda9479Steve McKay * http://www.apache.org/licenses/LICENSE-2.0 914e827a958f713d9cdec550e0b1099622cda9479Steve McKay * 1014e827a958f713d9cdec550e0b1099622cda9479Steve McKay * Unless required by applicable law or agreed to in writing, software 1114e827a958f713d9cdec550e0b1099622cda9479Steve McKay * distributed under the License is distributed on an "AS IS" BASIS, 1214e827a958f713d9cdec550e0b1099622cda9479Steve McKay * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1314e827a958f713d9cdec550e0b1099622cda9479Steve McKay * See the License for the specific language governing permissions and 1414e827a958f713d9cdec550e0b1099622cda9479Steve McKay * limitations under the License. 1514e827a958f713d9cdec550e0b1099622cda9479Steve McKay */ 1614e827a958f713d9cdec550e0b1099622cda9479Steve McKay 1714e827a958f713d9cdec550e0b1099622cda9479Steve McKaypackage com.android.documentsui.services; 1814e827a958f713d9cdec550e0b1099622cda9479Steve McKay 193564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKayimport static com.android.documentsui.DocumentsApplication.acquireUnstableProviderOrThrow; 20ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKayimport static com.android.documentsui.services.FileOperationService.EXTRA_CANCEL; 21748ea8cc785b6f037518703308ffd3eb2a151c5aTomasz Mikolajewskiimport static com.android.documentsui.services.FileOperationService.EXTRA_DIALOG_TYPE; 22ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKayimport static com.android.documentsui.services.FileOperationService.EXTRA_JOB_ID; 23ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKayimport static com.android.documentsui.services.FileOperationService.EXTRA_OPERATION; 24ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKayimport static com.android.documentsui.services.FileOperationService.EXTRA_SRC_LIST; 2514e827a958f713d9cdec550e0b1099622cda9479Steve McKayimport static com.android.documentsui.services.FileOperationService.OPERATION_UNKNOWN; 2614e827a958f713d9cdec550e0b1099622cda9479Steve McKay 2714e827a958f713d9cdec550e0b1099622cda9479Steve McKayimport android.annotation.DrawableRes; 2814e827a958f713d9cdec550e0b1099622cda9479Steve McKayimport android.annotation.PluralsRes; 2914e827a958f713d9cdec550e0b1099622cda9479Steve McKayimport android.app.Notification; 3014e827a958f713d9cdec550e0b1099622cda9479Steve McKayimport android.app.Notification.Builder; 3114e827a958f713d9cdec550e0b1099622cda9479Steve McKayimport android.app.PendingIntent; 323564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKayimport android.content.ContentProviderClient; 3314e827a958f713d9cdec550e0b1099622cda9479Steve McKayimport android.content.ContentResolver; 3414e827a958f713d9cdec550e0b1099622cda9479Steve McKayimport android.content.Context; 3514e827a958f713d9cdec550e0b1099622cda9479Steve McKayimport android.content.Intent; 36cd270153ff8844f37c2ddc3c3dc1246700f4974cTomasz Mikolajewskiimport android.net.Uri; 3714e827a958f713d9cdec550e0b1099622cda9479Steve McKayimport android.os.Parcelable; 3814e827a958f713d9cdec550e0b1099622cda9479Steve McKayimport android.os.RemoteException; 3914e827a958f713d9cdec550e0b1099622cda9479Steve McKayimport android.provider.DocumentsContract; 403564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKayimport android.util.Log; 4114e827a958f713d9cdec550e0b1099622cda9479Steve McKay 4214e827a958f713d9cdec550e0b1099622cda9479Steve McKayimport com.android.documentsui.FilesActivity; 43d5b2af1544629d48175d857785db32f2b8957f3aBen Kwaimport com.android.documentsui.Metrics; 44748ea8cc785b6f037518703308ffd3eb2a151c5aTomasz Mikolajewskiimport com.android.documentsui.OperationDialogFragment; 4514e827a958f713d9cdec550e0b1099622cda9479Steve McKayimport com.android.documentsui.R; 4614e827a958f713d9cdec550e0b1099622cda9479Steve McKayimport com.android.documentsui.Shared; 4714e827a958f713d9cdec550e0b1099622cda9479Steve McKayimport com.android.documentsui.model.DocumentInfo; 4814e827a958f713d9cdec550e0b1099622cda9479Steve McKayimport com.android.documentsui.model.DocumentStack; 4914e827a958f713d9cdec550e0b1099622cda9479Steve McKayimport com.android.documentsui.services.FileOperationService.OpType; 5014e827a958f713d9cdec550e0b1099622cda9479Steve McKay 5114e827a958f713d9cdec550e0b1099622cda9479Steve McKayimport java.util.ArrayList; 523564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKayimport java.util.HashMap; 53ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKayimport java.util.List; 543564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKayimport java.util.Map; 5514e827a958f713d9cdec550e0b1099622cda9479Steve McKay 56ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay/** 57ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay * A mashup of work item and ui progress update factory. Used by {@link FileOperationService} 58ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay * to do work and show progress relating to this work. 59ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay */ 60748ea8cc785b6f037518703308ffd3eb2a151c5aTomasz Mikolajewskiabstract public class Job implements Runnable { 613564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay private static final String TAG = "Job"; 62cd270153ff8844f37c2ddc3c3dc1246700f4974cTomasz Mikolajewski 63cd270153ff8844f37c2ddc3c3dc1246700f4974cTomasz Mikolajewski static final String INTENT_TAG_WARNING = "warning"; 64cd270153ff8844f37c2ddc3c3dc1246700f4974cTomasz Mikolajewski static final String INTENT_TAG_FAILURE = "failure"; 65cd270153ff8844f37c2ddc3c3dc1246700f4974cTomasz Mikolajewski static final String INTENT_TAG_PROGRESS = "progress"; 66cd270153ff8844f37c2ddc3c3dc1246700f4974cTomasz Mikolajewski static final String INTENT_TAG_CANCEL = "cancel"; 67cd270153ff8844f37c2ddc3c3dc1246700f4974cTomasz Mikolajewski 68ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay final Context service; 6914e827a958f713d9cdec550e0b1099622cda9479Steve McKay final Context appContext; 7014e827a958f713d9cdec550e0b1099622cda9479Steve McKay final Listener listener; 7114e827a958f713d9cdec550e0b1099622cda9479Steve McKay 72ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay final @OpType int operationType; 7314e827a958f713d9cdec550e0b1099622cda9479Steve McKay final String id; 7414e827a958f713d9cdec550e0b1099622cda9479Steve McKay final DocumentStack stack; 7514e827a958f713d9cdec550e0b1099622cda9479Steve McKay 7614e827a958f713d9cdec550e0b1099622cda9479Steve McKay final ArrayList<DocumentInfo> failedFiles = new ArrayList<>(); 7714e827a958f713d9cdec550e0b1099622cda9479Steve McKay final Notification.Builder mProgressBuilder; 7814e827a958f713d9cdec550e0b1099622cda9479Steve McKay 793564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay private final Map<String, ContentProviderClient> mClients = new HashMap<>(); 8014e827a958f713d9cdec550e0b1099622cda9479Steve McKay private volatile boolean mCanceled; 8114e827a958f713d9cdec550e0b1099622cda9479Steve McKay 8214e827a958f713d9cdec550e0b1099622cda9479Steve McKay /** 8314e827a958f713d9cdec550e0b1099622cda9479Steve McKay * A simple progressable job, much like an AsyncTask, but with support 8414e827a958f713d9cdec550e0b1099622cda9479Steve McKay * for providing various related notification, progress and navigation information. 85ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay * @param operationType 8614e827a958f713d9cdec550e0b1099622cda9479Steve McKay * 87ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay * @param service The service context in which this job is running. 8814e827a958f713d9cdec550e0b1099622cda9479Steve McKay * @param appContext The context of the invoking application. This is usually 8914e827a958f713d9cdec550e0b1099622cda9479Steve McKay * just {@code getApplicationContext()}. 9014e827a958f713d9cdec550e0b1099622cda9479Steve McKay * @param listener 9114e827a958f713d9cdec550e0b1099622cda9479Steve McKay * @param id Arbitrary string ID 9214e827a958f713d9cdec550e0b1099622cda9479Steve McKay * @param stack The documents stack context relating to this request. This is the 9314e827a958f713d9cdec550e0b1099622cda9479Steve McKay * destination in the Files app where the user will be take when the 9414e827a958f713d9cdec550e0b1099622cda9479Steve McKay * navigation intent is invoked (presumably from notification). 9514e827a958f713d9cdec550e0b1099622cda9479Steve McKay */ 96ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay Job(Context service, Context appContext, Listener listener, 97ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay @OpType int operationType, String id, DocumentStack stack) { 98ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay 99a1f7680f535a30aa816d129c072870031c8a2eb6Steve McKay assert(operationType != OPERATION_UNKNOWN); 10014e827a958f713d9cdec550e0b1099622cda9479Steve McKay 101ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay this.service = service; 10214e827a958f713d9cdec550e0b1099622cda9479Steve McKay this.appContext = appContext; 10314e827a958f713d9cdec550e0b1099622cda9479Steve McKay this.listener = listener; 104ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay this.operationType = operationType; 10514e827a958f713d9cdec550e0b1099622cda9479Steve McKay 10614e827a958f713d9cdec550e0b1099622cda9479Steve McKay this.id = id; 10714e827a958f713d9cdec550e0b1099622cda9479Steve McKay this.stack = stack; 10814e827a958f713d9cdec550e0b1099622cda9479Steve McKay 10914e827a958f713d9cdec550e0b1099622cda9479Steve McKay mProgressBuilder = createProgressBuilder(); 11014e827a958f713d9cdec550e0b1099622cda9479Steve McKay } 11114e827a958f713d9cdec550e0b1099622cda9479Steve McKay 112ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay @Override 113ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay public final void run() { 114ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay listener.onStart(this); 115ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay try { 116ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay start(); 1170fa97e85e45505b58e5b5956541277f61aa5458bTomasz Mikolajewski } catch (RuntimeException e) { 1180fa97e85e45505b58e5b5956541277f61aa5458bTomasz Mikolajewski // No exceptions should be thrown here, as all calls to the provider must be 1190fa97e85e45505b58e5b5956541277f61aa5458bTomasz Mikolajewski // handled within Job implementations. However, just in case catch them here. 1200fa97e85e45505b58e5b5956541277f61aa5458bTomasz Mikolajewski Log.e(TAG, "Operation failed due to an unhandled runtime exception.", e); 121d5b2af1544629d48175d857785db32f2b8957f3aBen Kwa Metrics.logFileOperationErrors(service, operationType, failedFiles); 122ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay } finally { 123748ea8cc785b6f037518703308ffd3eb2a151c5aTomasz Mikolajewski listener.onFinished(this); 124ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay } 12514e827a958f713d9cdec550e0b1099622cda9479Steve McKay } 12614e827a958f713d9cdec550e0b1099622cda9479Steve McKay 1270fa97e85e45505b58e5b5956541277f61aa5458bTomasz Mikolajewski abstract void start(); 128ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay 12914e827a958f713d9cdec550e0b1099622cda9479Steve McKay abstract Notification getSetupNotification(); 13014e827a958f713d9cdec550e0b1099622cda9479Steve McKay // TODO: Progress notification for deletes. 13114e827a958f713d9cdec550e0b1099622cda9479Steve McKay // abstract Notification getProgressNotification(long bytesCopied); 13214e827a958f713d9cdec550e0b1099622cda9479Steve McKay abstract Notification getFailureNotification(); 13314e827a958f713d9cdec550e0b1099622cda9479Steve McKay 134748ea8cc785b6f037518703308ffd3eb2a151c5aTomasz Mikolajewski abstract Notification getWarningNotification(); 135748ea8cc785b6f037518703308ffd3eb2a151c5aTomasz Mikolajewski 136cd270153ff8844f37c2ddc3c3dc1246700f4974cTomasz Mikolajewski Uri getDataUriForIntent(String tag) { 137cd270153ff8844f37c2ddc3c3dc1246700f4974cTomasz Mikolajewski return Uri.parse(String.format("data,%s-%s", tag, id)); 138cd270153ff8844f37c2ddc3c3dc1246700f4974cTomasz Mikolajewski } 139cd270153ff8844f37c2ddc3c3dc1246700f4974cTomasz Mikolajewski 1403564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay ContentProviderClient getClient(DocumentInfo doc) throws RemoteException { 1413564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay ContentProviderClient client = mClients.get(doc.authority); 1423564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay if (client == null) { 1433564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay // Acquire content providers. 1443564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay client = acquireUnstableProviderOrThrow( 1453564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay getContentResolver(), 1463564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay doc.authority); 1473564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay 1483564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay mClients.put(doc.authority, client); 1493564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay } 1503564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay 151a1f7680f535a30aa816d129c072870031c8a2eb6Steve McKay assert(client != null); 152a1f7680f535a30aa816d129c072870031c8a2eb6Steve McKay return client; 1533564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay } 1543564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay 1553564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay final void cleanup() { 1563564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay for (ContentProviderClient client : mClients.values()) { 1573564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay ContentProviderClient.releaseQuietly(client); 1583564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay } 1593564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay } 1603564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay 16114e827a958f713d9cdec550e0b1099622cda9479Steve McKay final void cancel() { 16214e827a958f713d9cdec550e0b1099622cda9479Steve McKay mCanceled = true; 163d5b2af1544629d48175d857785db32f2b8957f3aBen Kwa Metrics.logFileOperationCancelled(service, operationType); 16414e827a958f713d9cdec550e0b1099622cda9479Steve McKay } 16514e827a958f713d9cdec550e0b1099622cda9479Steve McKay 16614e827a958f713d9cdec550e0b1099622cda9479Steve McKay final boolean isCanceled() { 16714e827a958f713d9cdec550e0b1099622cda9479Steve McKay return mCanceled; 16814e827a958f713d9cdec550e0b1099622cda9479Steve McKay } 16914e827a958f713d9cdec550e0b1099622cda9479Steve McKay 17014e827a958f713d9cdec550e0b1099622cda9479Steve McKay final ContentResolver getContentResolver() { 171ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay return service.getContentResolver(); 17214e827a958f713d9cdec550e0b1099622cda9479Steve McKay } 17314e827a958f713d9cdec550e0b1099622cda9479Steve McKay 17414e827a958f713d9cdec550e0b1099622cda9479Steve McKay void onFileFailed(DocumentInfo file) { 17514e827a958f713d9cdec550e0b1099622cda9479Steve McKay failedFiles.add(file); 17614e827a958f713d9cdec550e0b1099622cda9479Steve McKay } 17714e827a958f713d9cdec550e0b1099622cda9479Steve McKay 178748ea8cc785b6f037518703308ffd3eb2a151c5aTomasz Mikolajewski final boolean hasFailures() { 17914e827a958f713d9cdec550e0b1099622cda9479Steve McKay return !failedFiles.isEmpty(); 18014e827a958f713d9cdec550e0b1099622cda9479Steve McKay } 18114e827a958f713d9cdec550e0b1099622cda9479Steve McKay 182748ea8cc785b6f037518703308ffd3eb2a151c5aTomasz Mikolajewski boolean hasWarnings() { 183748ea8cc785b6f037518703308ffd3eb2a151c5aTomasz Mikolajewski return false; 184748ea8cc785b6f037518703308ffd3eb2a151c5aTomasz Mikolajewski } 185748ea8cc785b6f037518703308ffd3eb2a151c5aTomasz Mikolajewski 186db87543d82f1916b0372f732045f0bc2860ed3f2Tomasz Mikolajewski final void deleteDocument(DocumentInfo doc, DocumentInfo parent) throws ResourceException { 1873564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay try { 188db87543d82f1916b0372f732045f0bc2860ed3f2Tomasz Mikolajewski if (doc.isRemoveSupported()) { 189db87543d82f1916b0372f732045f0bc2860ed3f2Tomasz Mikolajewski DocumentsContract.removeDocument(getClient(doc), doc.derivedUri, parent.derivedUri); 190db87543d82f1916b0372f732045f0bc2860ed3f2Tomasz Mikolajewski } else if (doc.isDeleteSupported()) { 191db87543d82f1916b0372f732045f0bc2860ed3f2Tomasz Mikolajewski DocumentsContract.deleteDocument(getClient(doc), doc.derivedUri); 192db87543d82f1916b0372f732045f0bc2860ed3f2Tomasz Mikolajewski } else { 193db87543d82f1916b0372f732045f0bc2860ed3f2Tomasz Mikolajewski throw new ResourceException("Unable to delete source document as the file is " + 194db87543d82f1916b0372f732045f0bc2860ed3f2Tomasz Mikolajewski "not deletable nor removable: %s.", doc.derivedUri); 195db87543d82f1916b0372f732045f0bc2860ed3f2Tomasz Mikolajewski } 196db87543d82f1916b0372f732045f0bc2860ed3f2Tomasz Mikolajewski } catch (RemoteException | RuntimeException e) { 1970fa97e85e45505b58e5b5956541277f61aa5458bTomasz Mikolajewski throw new ResourceException("Failed to delete file %s due to an exception.", 1980fa97e85e45505b58e5b5956541277f61aa5458bTomasz Mikolajewski doc.derivedUri, e); 1993564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay } 2003564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay } 2013564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay 20214e827a958f713d9cdec550e0b1099622cda9479Steve McKay Notification getSetupNotification(String content) { 2037a3b811122b6db0ac103630c2b7acde1d9f4c128Steve McKay mProgressBuilder.setProgress(0, 0, true) 2047a3b811122b6db0ac103630c2b7acde1d9f4c128Steve McKay .setContentText(content); 20514e827a958f713d9cdec550e0b1099622cda9479Steve McKay return mProgressBuilder.build(); 20614e827a958f713d9cdec550e0b1099622cda9479Steve McKay } 20714e827a958f713d9cdec550e0b1099622cda9479Steve McKay 20814e827a958f713d9cdec550e0b1099622cda9479Steve McKay Notification getFailureNotification(@PluralsRes int titleId, @DrawableRes int icon) { 209cd270153ff8844f37c2ddc3c3dc1246700f4974cTomasz Mikolajewski final Intent navigateIntent = buildNavigateIntent(INTENT_TAG_FAILURE); 210748ea8cc785b6f037518703308ffd3eb2a151c5aTomasz Mikolajewski navigateIntent.putExtra(EXTRA_DIALOG_TYPE, OperationDialogFragment.DIALOG_TYPE_FAILURE); 211ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay navigateIntent.putExtra(EXTRA_OPERATION, operationType); 212ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay navigateIntent.putParcelableArrayListExtra(EXTRA_SRC_LIST, failedFiles); 21314e827a958f713d9cdec550e0b1099622cda9479Steve McKay 214ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay final Notification.Builder errorBuilder = new Notification.Builder(service) 215ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay .setContentTitle(service.getResources().getQuantityString(titleId, 21614e827a958f713d9cdec550e0b1099622cda9479Steve McKay failedFiles.size(), failedFiles.size())) 217ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay .setContentText(service.getString(R.string.notification_touch_for_details)) 21814e827a958f713d9cdec550e0b1099622cda9479Steve McKay .setContentIntent(PendingIntent.getActivity(appContext, 0, navigateIntent, 21914e827a958f713d9cdec550e0b1099622cda9479Steve McKay PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT)) 22014e827a958f713d9cdec550e0b1099622cda9479Steve McKay .setCategory(Notification.CATEGORY_ERROR) 22114e827a958f713d9cdec550e0b1099622cda9479Steve McKay .setSmallIcon(icon) 22214e827a958f713d9cdec550e0b1099622cda9479Steve McKay .setAutoCancel(true); 2237a3b811122b6db0ac103630c2b7acde1d9f4c128Steve McKay 22414e827a958f713d9cdec550e0b1099622cda9479Steve McKay return errorBuilder.build(); 22514e827a958f713d9cdec550e0b1099622cda9479Steve McKay } 22614e827a958f713d9cdec550e0b1099622cda9479Steve McKay 22714e827a958f713d9cdec550e0b1099622cda9479Steve McKay abstract Builder createProgressBuilder(); 22814e827a958f713d9cdec550e0b1099622cda9479Steve McKay 22914e827a958f713d9cdec550e0b1099622cda9479Steve McKay final Builder createProgressBuilder( 23014e827a958f713d9cdec550e0b1099622cda9479Steve McKay String title, @DrawableRes int icon, 23114e827a958f713d9cdec550e0b1099622cda9479Steve McKay String actionTitle, @DrawableRes int actionIcon) { 232ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay Notification.Builder progressBuilder = new Notification.Builder(service) 23314e827a958f713d9cdec550e0b1099622cda9479Steve McKay .setContentTitle(title) 23414e827a958f713d9cdec550e0b1099622cda9479Steve McKay .setContentIntent( 235cd270153ff8844f37c2ddc3c3dc1246700f4974cTomasz Mikolajewski PendingIntent.getActivity(appContext, 0, 236cd270153ff8844f37c2ddc3c3dc1246700f4974cTomasz Mikolajewski buildNavigateIntent(INTENT_TAG_PROGRESS), 0)) 23714e827a958f713d9cdec550e0b1099622cda9479Steve McKay .setCategory(Notification.CATEGORY_PROGRESS) 23814e827a958f713d9cdec550e0b1099622cda9479Steve McKay .setSmallIcon(icon) 23914e827a958f713d9cdec550e0b1099622cda9479Steve McKay .setOngoing(true); 24014e827a958f713d9cdec550e0b1099622cda9479Steve McKay 24114e827a958f713d9cdec550e0b1099622cda9479Steve McKay final Intent cancelIntent = createCancelIntent(); 24214e827a958f713d9cdec550e0b1099622cda9479Steve McKay 24314e827a958f713d9cdec550e0b1099622cda9479Steve McKay progressBuilder.addAction( 24414e827a958f713d9cdec550e0b1099622cda9479Steve McKay actionIcon, 24514e827a958f713d9cdec550e0b1099622cda9479Steve McKay actionTitle, 24614e827a958f713d9cdec550e0b1099622cda9479Steve McKay PendingIntent.getService( 247ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay service, 24814e827a958f713d9cdec550e0b1099622cda9479Steve McKay 0, 24914e827a958f713d9cdec550e0b1099622cda9479Steve McKay cancelIntent, 25014e827a958f713d9cdec550e0b1099622cda9479Steve McKay PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT)); 25114e827a958f713d9cdec550e0b1099622cda9479Steve McKay 25214e827a958f713d9cdec550e0b1099622cda9479Steve McKay return progressBuilder; 25314e827a958f713d9cdec550e0b1099622cda9479Steve McKay } 25414e827a958f713d9cdec550e0b1099622cda9479Steve McKay 25514e827a958f713d9cdec550e0b1099622cda9479Steve McKay /** 25614e827a958f713d9cdec550e0b1099622cda9479Steve McKay * Creates an intent for navigating back to the destination directory. 25714e827a958f713d9cdec550e0b1099622cda9479Steve McKay */ 258cd270153ff8844f37c2ddc3c3dc1246700f4974cTomasz Mikolajewski Intent buildNavigateIntent(String tag) { 259ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay Intent intent = new Intent(service, FilesActivity.class); 260cd270153ff8844f37c2ddc3c3dc1246700f4974cTomasz Mikolajewski intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 26114e827a958f713d9cdec550e0b1099622cda9479Steve McKay intent.setAction(DocumentsContract.ACTION_BROWSE); 262cd270153ff8844f37c2ddc3c3dc1246700f4974cTomasz Mikolajewski intent.setData(getDataUriForIntent(tag)); 26314e827a958f713d9cdec550e0b1099622cda9479Steve McKay intent.putExtra(Shared.EXTRA_STACK, (Parcelable) stack); 26414e827a958f713d9cdec550e0b1099622cda9479Steve McKay return intent; 26514e827a958f713d9cdec550e0b1099622cda9479Steve McKay } 26614e827a958f713d9cdec550e0b1099622cda9479Steve McKay 26714e827a958f713d9cdec550e0b1099622cda9479Steve McKay Intent createCancelIntent() { 268ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay final Intent cancelIntent = new Intent(service, FileOperationService.class); 269cd270153ff8844f37c2ddc3c3dc1246700f4974cTomasz Mikolajewski cancelIntent.setData(getDataUriForIntent(INTENT_TAG_CANCEL)); 270ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay cancelIntent.putExtra(EXTRA_CANCEL, true); 271ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay cancelIntent.putExtra(EXTRA_JOB_ID, id); 27214e827a958f713d9cdec550e0b1099622cda9479Steve McKay return cancelIntent; 27314e827a958f713d9cdec550e0b1099622cda9479Steve McKay } 27414e827a958f713d9cdec550e0b1099622cda9479Steve McKay 2753564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay @Override 2763564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay public String toString() { 2773564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay return new StringBuilder() 2783564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay .append("Job") 2793564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay .append("{") 2803564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay .append("id=" + id) 2813564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay .append("}") 2823564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay .toString(); 2833564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay } 2843564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay 285ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay /** 286ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay * Factory class that facilitates our testing FileOperationService. 287ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay */ 288ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay static class Factory { 289ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay 290ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay static final Factory instance = new Factory(); 291ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay 292ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay Job createCopy(Context service, Context appContext, Listener listener, 293ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay String id, DocumentStack stack, List<DocumentInfo> srcs) { 29455625a675fcbf85654027bf1c8ff89d0084fa24fSteve McKay assert(!srcs.isEmpty()); 29555625a675fcbf85654027bf1c8ff89d0084fa24fSteve McKay assert(stack.peek().isCreateSupported()); 296ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay return new CopyJob(service, appContext, listener, id, stack, srcs); 297ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay } 298ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay 299ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay Job createMove(Context service, Context appContext, Listener listener, 300b8436af1b84a31b2ba3d50510ebf10bb721f2b6cTomasz Mikolajewski String id, DocumentStack stack, List<DocumentInfo> srcs, 301b8436af1b84a31b2ba3d50510ebf10bb721f2b6cTomasz Mikolajewski DocumentInfo srcParent) { 30255625a675fcbf85654027bf1c8ff89d0084fa24fSteve McKay assert(!srcs.isEmpty()); 30355625a675fcbf85654027bf1c8ff89d0084fa24fSteve McKay assert(stack.peek().isCreateSupported()); 304b8436af1b84a31b2ba3d50510ebf10bb721f2b6cTomasz Mikolajewski return new MoveJob(service, appContext, listener, id, stack, srcs, srcParent); 305ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay } 3063564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay 3073564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay Job createDelete(Context service, Context appContext, Listener listener, 308b8436af1b84a31b2ba3d50510ebf10bb721f2b6cTomasz Mikolajewski String id, DocumentStack stack, List<DocumentInfo> srcs, 309b8436af1b84a31b2ba3d50510ebf10bb721f2b6cTomasz Mikolajewski DocumentInfo srcParent) { 31055625a675fcbf85654027bf1c8ff89d0084fa24fSteve McKay assert(!srcs.isEmpty()); 31101958b11ef37c220940e816667556e8259024df8Garfield, Tan // stack is empty if we delete docs from recent. 31201958b11ef37c220940e816667556e8259024df8Garfield, Tan // we can't currently delete from archives. 31301958b11ef37c220940e816667556e8259024df8Garfield, Tan assert(stack.isEmpty() || stack.peek().isDirectory()); 314b8436af1b84a31b2ba3d50510ebf10bb721f2b6cTomasz Mikolajewski return new DeleteJob(service, appContext, listener, id, stack, srcs, srcParent); 3153564543e1ece2211a2d5b729fbbf8dd7ac94944cSteve McKay } 316ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay } 317ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay 318ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay /** 319ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay * Listener interface employed by the service that owns us as well as tests. 320ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay */ 32114e827a958f713d9cdec550e0b1099622cda9479Steve McKay interface Listener { 322ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay void onStart(Job job); 323ecbf3c504c5ec7ccb3e2be7f4dd175ebe634139dSteve McKay void onFinished(Job job); 32414e827a958f713d9cdec550e0b1099622cda9479Steve McKay void onProgress(CopyJob job); 32514e827a958f713d9cdec550e0b1099622cda9479Steve McKay } 32614e827a958f713d9cdec550e0b1099622cda9479Steve McKay} 327