Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 063a0f38 authored by Steve McKay's avatar Steve McKay Committed by Android (Google) Code Review
Browse files

Merge "Support for concurrent file operations."

parents 8788dadd bbeba526
Loading
Loading
Loading
Loading
+52 −30
Original line number Original line Diff line number Diff line
@@ -17,6 +17,10 @@
package com.android.documentsui.services;
package com.android.documentsui.services;


import static android.os.SystemClock.elapsedRealtime;
import static android.os.SystemClock.elapsedRealtime;
import static android.provider.DocumentsContract.buildChildDocumentsUri;
import static android.provider.DocumentsContract.buildDocumentUri;
import static android.provider.DocumentsContract.getDocumentId;
import static android.provider.DocumentsContract.isChildDocument;
import static com.android.documentsui.DocumentsApplication.acquireUnstableProviderOrThrow;
import static com.android.documentsui.DocumentsApplication.acquireUnstableProviderOrThrow;
import static com.android.documentsui.Shared.DEBUG;
import static com.android.documentsui.Shared.DEBUG;
import static com.android.documentsui.model.DocumentInfo.getCursorLong;
import static com.android.documentsui.model.DocumentInfo.getCursorLong;
@@ -44,6 +48,7 @@ import android.webkit.MimeTypeMap;
import com.android.documentsui.R;
import com.android.documentsui.R;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.services.FileOperationService.OpType;


import libcore.io.IoUtils;
import libcore.io.IoUtils;


@@ -80,9 +85,22 @@ class CopyJob extends Job {
     *
     *
     * @param srcs List of files to be copied.
     * @param srcs List of files to be copied.
     */
     */
    CopyJob(Context serviceContext, Context appContext, Listener listener,
    CopyJob(Context service, Context appContext, Listener listener,
            String id, DocumentStack destination, List<DocumentInfo> srcs) {
            String id, DocumentStack destination, List<DocumentInfo> srcs) {
        super(OPERATION_COPY, serviceContext, appContext, listener, id, destination);
        super(service, appContext, listener, OPERATION_COPY, id, destination);

        checkArgument(!srcs.isEmpty());
        this.mSrcFiles = srcs;
    }

    /**
     * @see @link {@link Job} constructor for most param descriptions.
     *
     * @param srcs List of files to be copied.
     */
    CopyJob(Context service, Context appContext, Listener listener,
            @OpType int opType, String id, DocumentStack destination, List<DocumentInfo> srcs) {
        super(service, appContext, listener, opType, id, destination);


        checkArgument(!srcs.isEmpty());
        checkArgument(!srcs.isEmpty());
        this.mSrcFiles = srcs;
        this.mSrcFiles = srcs;
@@ -91,15 +109,15 @@ class CopyJob extends Job {
    @Override
    @Override
    Builder createProgressBuilder() {
    Builder createProgressBuilder() {
        return super.createProgressBuilder(
        return super.createProgressBuilder(
                serviceContext.getString(R.string.copy_notification_title),
                service.getString(R.string.copy_notification_title),
                R.drawable.ic_menu_copy,
                R.drawable.ic_menu_copy,
                serviceContext.getString(android.R.string.cancel),
                service.getString(android.R.string.cancel),
                R.drawable.ic_cab_cancel);
                R.drawable.ic_cab_cancel);
    }
    }


    @Override
    @Override
    public Notification getSetupNotification() {
    public Notification getSetupNotification() {
        return getSetupNotification(serviceContext.getString(R.string.copy_preparing));
        return getSetupNotification(service.getString(R.string.copy_preparing));
    }
    }


    public boolean shouldUpdateProgress() {
    public boolean shouldUpdateProgress() {
@@ -113,7 +131,7 @@ class CopyJob extends Job {
        mProgressBuilder.setContentInfo(
        mProgressBuilder.setContentInfo(
                NumberFormat.getPercentInstance().format(completed));
                NumberFormat.getPercentInstance().format(completed));
        if (mRemainingTime > 0) {
        if (mRemainingTime > 0) {
            mProgressBuilder.setContentText(serviceContext.getString(msgId,
            mProgressBuilder.setContentText(service.getString(msgId,
                    DateUtils.formatDuration(mRemainingTime)));
                    DateUtils.formatDuration(mRemainingTime)));
        } else {
        } else {
            mProgressBuilder.setContentText(null);
            mProgressBuilder.setContentText(null);
@@ -164,7 +182,7 @@ class CopyJob extends Job {
    }
    }


    @Override
    @Override
    void run(FileOperationService service) throws RemoteException {
    void start() throws RemoteException {
        mStartTime = elapsedRealtime();
        mStartTime = elapsedRealtime();


        // Acquire content providers.
        // Acquire content providers.
@@ -186,16 +204,14 @@ class CopyJob extends Job {


            // Guard unsupported recursive operation.
            // Guard unsupported recursive operation.
            if (dstInfo.equals(srcInfo) || isDescendentOf(srcInfo, dstInfo)) {
            if (dstInfo.equals(srcInfo) || isDescendentOf(srcInfo, dstInfo)) {
                if (DEBUG) Log.d(TAG, "Skipping recursive operation on directory "
                onFileFailed(srcInfo,
                        + dstInfo.derivedUri);
                        "Skipping recursive operation on directory " + dstInfo.derivedUri + ".");
                onFileFailed(srcInfo);
                continue;
                continue;
            }
            }


            if (DEBUG) Log.d(TAG,
            if (DEBUG) Log.d(TAG,
                    "Performing op-type:" + type() + " of " + srcInfo.displayName
                    "Copying " + srcInfo.displayName + " (" + srcInfo.derivedUri + ")"
                    + " (" + srcInfo.derivedUri + ")" + " to " + dstInfo.displayName
                    + " to " + dstInfo.displayName + " (" + dstInfo.derivedUri + ")");
                    + " (" + dstInfo.derivedUri + ")");


            processDocument(srcInfo, dstInfo);
            processDocument(srcInfo, dstInfo);
        }
        }
@@ -219,7 +235,6 @@ class CopyJob extends Job {
     *
     *
     * @param srcInfo DocumentInfos for the documents to copy.
     * @param srcInfo DocumentInfos for the documents to copy.
     * @param dstDirInfo The destination directory.
     * @param dstDirInfo The destination directory.
     * @param mode The transfer mode (copy or move).
     * @return True on success, false on failure.
     * @return True on success, false on failure.
     * @throws RemoteException
     * @throws RemoteException
     */
     */
@@ -234,7 +249,8 @@ class CopyJob extends Job {
            if ((srcInfo.flags & Document.FLAG_SUPPORTS_COPY) != 0) {
            if ((srcInfo.flags & Document.FLAG_SUPPORTS_COPY) != 0) {
                if (DocumentsContract.copyDocument(srcClient, srcInfo.derivedUri,
                if (DocumentsContract.copyDocument(srcClient, srcInfo.derivedUri,
                        dstDirInfo.derivedUri) == null) {
                        dstDirInfo.derivedUri) == null) {
                    onFileFailed(srcInfo);
                    onFileFailed(srcInfo,
                            "Provider side copy failed for documents: " + srcInfo.derivedUri + ".");
                }
                }
                return false;
                return false;
            }
            }
@@ -249,6 +265,7 @@ class CopyJob extends Job {
        final String dstMimeType;
        final String dstMimeType;
        final String dstDisplayName;
        final String dstDisplayName;


        if (DEBUG) Log.d(TAG, "Doing byte copy of document: " + srcInfo);
        // If the file is virtual, but can be converted to another format, then try to copy it
        // If the file is virtual, but can be converted to another format, then try to copy it
        // as such format. Also, append an extension for the target mime type (if known).
        // as such format. Also, append an extension for the target mime type (if known).
        if (srcInfo.isVirtualDocument()) {
        if (srcInfo.isVirtualDocument()) {
@@ -261,9 +278,7 @@ class CopyJob extends Job {
                dstDisplayName = srcInfo.displayName +
                dstDisplayName = srcInfo.displayName +
                        (extension != null ? "." + extension : srcInfo.displayName);
                        (extension != null ? "." + extension : srcInfo.displayName);
            } else {
            } else {
                // The virtual file is not available as any alternative streamable format.
                onFileFailed(srcInfo, "Cannot copy virtual file. No streamable formats available.");
                // TODO: Log failures.
                onFileFailed(srcInfo);
                return false;
                return false;
            }
            }
        } else {
        } else {
@@ -277,7 +292,9 @@ class CopyJob extends Job {
                dstDirInfo.derivedUri, dstMimeType, dstDisplayName);
                dstDirInfo.derivedUri, dstMimeType, dstDisplayName);
        if (dstUri == null) {
        if (dstUri == null) {
            // If this is a directory, the entire subdir will not be copied over.
            // If this is a directory, the entire subdir will not be copied over.
            onFileFailed(srcInfo);
            onFileFailed(srcInfo,
                    "Couldn't create destination document " + dstDisplayName
                    + " in directory " + dstDirInfo.displayName + ".");
            return false;
            return false;
        }
        }


@@ -285,7 +302,8 @@ class CopyJob extends Job {
        try {
        try {
            dstInfo = DocumentInfo.fromUri(getContentResolver(), dstUri);
            dstInfo = DocumentInfo.fromUri(getContentResolver(), dstUri);
        } catch (FileNotFoundException e) {
        } catch (FileNotFoundException e) {
            onFileFailed(srcInfo);
            onFileFailed(srcInfo,
                    "Could not load DocumentInfo for newly created file: " + dstUri + ".");
            return false;
            return false;
        }
        }


@@ -327,7 +345,7 @@ class CopyJob extends Job {
                    srcDirInfo.documentId);
                    srcDirInfo.documentId);
            cursor = srcClient.query(queryUri, queryColumns, null, null, null);
            cursor = srcClient.query(queryUri, queryColumns, null, null, null);
            DocumentInfo srcInfo;
            DocumentInfo srcInfo;
            while (cursor.moveToNext()) {
            while (cursor.moveToNext() && !isCanceled()) {
                srcInfo = DocumentInfo.fromCursor(cursor, srcDirInfo.authority);
                srcInfo = DocumentInfo.fromCursor(cursor, srcDirInfo.authority);
                success &= processDocument(srcInfo, dstDirInfo);
                success &= processDocument(srcInfo, dstDirInfo);
            }
            }
@@ -374,7 +392,7 @@ class CopyJob extends Job {
            dstFile = dstClient.openFile(dstInfo.derivedUri, "w", canceller);
            dstFile = dstClient.openFile(dstInfo.derivedUri, "w", canceller);
            dst = new ParcelFileDescriptor.AutoCloseOutputStream(dstFile);
            dst = new ParcelFileDescriptor.AutoCloseOutputStream(dstFile);


            byte[] buffer = new byte[8192];
            byte[] buffer = new byte[32 * 1024];
            int len;
            int len;
            while ((len = src.read(buffer)) != -1) {
            while ((len = src.read(buffer)) != -1) {
                if (isCanceled()) {
                if (isCanceled()) {
@@ -389,7 +407,8 @@ class CopyJob extends Job {
            srcFile.checkError();
            srcFile.checkError();
        } catch (IOException e) {
        } catch (IOException e) {
            success = false;
            success = false;
            onFileFailed(srcInfo);
            onFileFailed(srcInfo, "Exception thrown while copying from "
                    + srcInfo.derivedUri + " to " + dstInfo.derivedUri + ".");


            if (dstFile != null) {
            if (dstFile != null) {
                try {
                try {
@@ -405,7 +424,7 @@ class CopyJob extends Job {
        }
        }


        if (!success) {
        if (!success) {
            // Clean up half-copied files.
            if (DEBUG) Log.d(TAG, "Cleaning up failed operation leftovers.");
            canceller.cancel();
            canceller.cancel();
            try {
            try {
                DocumentsContract.deleteDocument(dstClient, dstInfo.derivedUri);
                DocumentsContract.deleteDocument(dstClient, dstInfo.derivedUri);
@@ -452,8 +471,7 @@ class CopyJob extends Job {
    private static long calculateFileSizesRecursively(
    private static long calculateFileSizesRecursively(
            ContentProviderClient client, Uri uri) throws RemoteException {
            ContentProviderClient client, Uri uri) throws RemoteException {
        final String authority = uri.getAuthority();
        final String authority = uri.getAuthority();
        final Uri queryUri = DocumentsContract.buildChildDocumentsUri(authority,
        final Uri queryUri = buildChildDocumentsUri(authority, getDocumentId(uri));
                DocumentsContract.getDocumentId(uri));
        final String queryColumns[] = new String[] {
        final String queryColumns[] = new String[] {
                Document.COLUMN_DOCUMENT_ID,
                Document.COLUMN_DOCUMENT_ID,
                Document.COLUMN_MIME_TYPE,
                Document.COLUMN_MIME_TYPE,
@@ -468,7 +486,7 @@ class CopyJob extends Job {
                if (Document.MIME_TYPE_DIR.equals(
                if (Document.MIME_TYPE_DIR.equals(
                        getCursorString(cursor, Document.COLUMN_MIME_TYPE))) {
                        getCursorString(cursor, Document.COLUMN_MIME_TYPE))) {
                    // Recurse into directories.
                    // Recurse into directories.
                    final Uri dirUri = DocumentsContract.buildDocumentUri(authority,
                    final Uri dirUri = buildDocumentUri(authority,
                            getCursorString(cursor, Document.COLUMN_DOCUMENT_ID));
                            getCursorString(cursor, Document.COLUMN_DOCUMENT_ID));
                    result += calculateFileSizesRecursively(client, dirUri);
                    result += calculateFileSizesRecursively(client, dirUri);
                } else {
                } else {
@@ -497,9 +515,13 @@ class CopyJob extends Job {
    boolean isDescendentOf(DocumentInfo doc, DocumentInfo parentDoc)
    boolean isDescendentOf(DocumentInfo doc, DocumentInfo parentDoc)
            throws RemoteException {
            throws RemoteException {
        if (parentDoc.isDirectory() && doc.authority.equals(parentDoc.authority)) {
        if (parentDoc.isDirectory() && doc.authority.equals(parentDoc.authority)) {
            return DocumentsContract.isChildDocument(
            return isChildDocument(dstClient, doc.derivedUri, parentDoc.derivedUri);
                    dstClient, doc.derivedUri, parentDoc.derivedUri);
        }
        }
        return false;
        return false;
    }
    }

    private void onFileFailed(DocumentInfo file, String msg) {
        Log.w(TAG, msg);
        onFileFailed(file);
    }
}
}
+163 −103
Original line number Original line Diff line number Diff line
@@ -16,36 +16,47 @@


package com.android.documentsui.services;
package com.android.documentsui.services;


import static android.os.SystemClock.elapsedRealtime;
import static com.android.documentsui.Shared.DEBUG;
import static com.android.documentsui.Shared.DEBUG;
import static com.android.internal.util.Preconditions.checkArgument;
import static com.android.internal.util.Preconditions.checkArgument;
import static com.android.internal.util.Preconditions.checkNotNull;
import static com.android.internal.util.Preconditions.checkNotNull;
import static com.android.internal.util.Preconditions.checkState;
import static com.android.internal.util.Preconditions.checkState;


import android.annotation.IntDef;
import android.annotation.IntDef;
import android.app.IntentService;
import android.app.NotificationManager;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Intent;
import android.content.Intent;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.PowerManager;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
import android.util.Log;


import com.android.documentsui.Shared;
import com.android.documentsui.Shared;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.model.DocumentStack;

import com.android.documentsui.services.Job.Factory;
import com.google.common.base.Objects;


import java.lang.annotation.Retention;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import javax.annotation.concurrent.GuardedBy;

public class FileOperationService extends Service implements Job.Listener {

    private static final int DEFAULT_DELAY = 0;
    private static final int MAX_DELAY = 10 * 1000;  // ten seconds


public class FileOperationService extends IntentService implements Job.Listener {
    public static final String TAG = "FileOperationService";
    public static final String TAG = "FileOperationService";
    private static final int POOL_SIZE = 2;  // "pool size", not *max* "pool size".


    public static final String EXTRA_JOB_ID = "com.android.documentsui.JOB_ID";
    public static final String EXTRA_JOB_ID = "com.android.documentsui.JOB_ID";
    public static final String EXTRA_DELAY = "com.android.documentsui.DELAY";
    public static final String EXTRA_OPERATION = "com.android.documentsui.OPERATION";
    public static final String EXTRA_OPERATION = "com.android.documentsui.OPERATION";
    public static final String EXTRA_CANCEL = "com.android.documentsui.CANCEL";
    public static final String EXTRA_CANCEL = "com.android.documentsui.CANCEL";
    public static final String EXTRA_SRC_LIST = "com.android.documentsui.SRC_LIST";
    public static final String EXTRA_SRC_LIST = "com.android.documentsui.SRC_LIST";
@@ -68,23 +79,31 @@ public class FileOperationService extends IntentService implements Job.Listener
    // TODO: Move it to a shared file when more operations are implemented.
    // TODO: Move it to a shared file when more operations are implemented.
    public static final int FAILURE_COPY = 1;
    public static final int FAILURE_COPY = 1;


    private PowerManager mPowerManager;
    // The executor and job factory are visible for testing and non-final
    // so we'll have a way to inject test doubles from the test. It's
    // a sub-optimal arrangement.
    @VisibleForTesting ScheduledExecutorService executor;
    @VisibleForTesting Factory jobFactory;


    private PowerManager mPowerManager;
    private PowerManager.WakeLock mWakeLock;  // the wake lock, if held.
    private NotificationManager mNotificationManager;
    private NotificationManager mNotificationManager;


    // TODO: Rework service to support multiple concurrent jobs.
    @GuardedBy("mRunning")
    private volatile Job mJob;
    private Map<String, JobRecord> mRunning = new HashMap<>();

    // For testing only.
    @Nullable private TestOnlyListener mJobFinishedListener;


    public FileOperationService() {
    private int mLastStarted;
        super("FileOperationService");
    }


    @Override
    @Override
    public void onCreate() {
    public void onCreate() {
        super.onCreate();
        // Allow tests to pre-set these with test doubles.
        if (executor == null) {
            executor = new ScheduledThreadPoolExecutor(POOL_SIZE);
        }

        if (jobFactory == null) {
            jobFactory = Job.Factory.instance;
        }


        if (DEBUG) Log.d(TAG, "Created.");
        if (DEBUG) Log.d(TAG, "Created.");
        mPowerManager = getSystemService(PowerManager.class);
        mPowerManager = getSystemService(PowerManager.class);
@@ -92,69 +111,50 @@ public class FileOperationService extends IntentService implements Job.Listener
    }
    }


    @Override
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
    public int onStartCommand(Intent intent, int flags, int startTime) {
        if (DEBUG) Log.d(TAG, "onStartCommand: " + intent);
        // TODO: Ensure we're not being called with retry or redeliver.
        if (intent.hasExtra(EXTRA_CANCEL)) {
        // checkArgument(flags == 0);  // retry and redeliver are not supported.
            handleCancel(intent);
            return START_REDELIVER_INTENT;
        } else {
            return super.onStartCommand(intent, flags, startId);
        }
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        if (DEBUG) Log.d(TAG, "onHandleIntent: " + intent);


        String jobId = intent.getStringExtra(EXTRA_JOB_ID);
        String jobId = intent.getStringExtra(EXTRA_JOB_ID);
        @OpType int operationType = intent.getIntExtra(EXTRA_OPERATION, OPERATION_UNKNOWN);
        @OpType int operationType = intent.getIntExtra(EXTRA_OPERATION, OPERATION_UNKNOWN);
        checkArgument(jobId != null);
        checkArgument(jobId != null);

        if (intent.hasExtra(EXTRA_CANCEL)) {
        if (intent.hasExtra(EXTRA_CANCEL)) {
            handleCancel(intent);
            handleCancel(intent);
            return;
        } else {
        }

            checkArgument(operationType != OPERATION_UNKNOWN);
            checkArgument(operationType != OPERATION_UNKNOWN);
            handleOperation(intent, startTime, jobId, operationType);
        }


        PowerManager.WakeLock wakeLock = mPowerManager.newWakeLock(
        return START_NOT_STICKY;
                PowerManager.PARTIAL_WAKE_LOCK, TAG);
    }

        ArrayList<DocumentInfo> srcs = intent.getParcelableArrayListExtra(EXTRA_SRC_LIST);
        DocumentStack stack = intent.getParcelableExtra(Shared.EXTRA_STACK);

        Job job = createJob(operationType, jobId, srcs, stack);

        try {
            wakeLock.acquire();


            mNotificationManager.notify(job.id, 0, job.getSetupNotification());
    private void handleOperation(Intent intent, int startTime, String jobId, int operationType) {
            job.run(this);
        if (DEBUG) Log.d(TAG, "onStartCommand: " + jobId + " with start time " + startTime);


        } catch (Exception e) {
        // Track start time so we can stop the service once we're out of work to do.
            // Catch-all to prevent any copy errors from wedging the app.
        mLastStarted = startTime;
            Log.e(TAG, "Exceptions occurred during copying", e);
        } finally {
            if (DEBUG) Log.d(TAG, "Cleaning up after copy");


            job.cleanup();
        Job job = null;
            wakeLock.release();
        synchronized (mRunning) {
            if (mWakeLock == null) {
                mWakeLock = mPowerManager.newWakeLock(
                        PowerManager.PARTIAL_WAKE_LOCK, TAG);
            }


            // Dismiss the ongoing copy notification when the copy is done.
            List<DocumentInfo> srcs = intent.getParcelableArrayListExtra(EXTRA_SRC_LIST);
            mNotificationManager.cancel(job.id, 0);
            DocumentStack stack = intent.getParcelableExtra(Shared.EXTRA_STACK);


            if (job.failed()) {
            job = createJob(operationType, jobId, srcs, stack);
                Log.e(TAG, job.failedFiles.size() + " files failed to copy");
                mNotificationManager.notify(job.id, 0, job.getFailureNotification());
            }


            // TEST ONLY CODE...<raised eyebrows>
            mWakeLock.acquire();
            if (mJobFinishedListener != null) {
                mJobFinishedListener.onFinished(job.failedFiles);
        }
        }


            deleteJob(job);
        checkState(job != null);
            if (DEBUG) Log.d(TAG, "Done cleaning up");
        int delay = intent.getIntExtra(EXTRA_DELAY, DEFAULT_DELAY);
        }
        checkArgument(delay <= MAX_DELAY);
        ScheduledFuture<?> future = executor.schedule(job, delay, TimeUnit.MILLISECONDS);
        mRunning.put(jobId, new JobRecord(job, future));
    }
    }


    /**
    /**
@@ -166,12 +166,25 @@ public class FileOperationService extends IntentService implements Job.Listener
        checkArgument(intent.hasExtra(EXTRA_CANCEL));
        checkArgument(intent.hasExtra(EXTRA_CANCEL));
        String jobId = checkNotNull(intent.getStringExtra(EXTRA_JOB_ID));
        String jobId = checkNotNull(intent.getStringExtra(EXTRA_JOB_ID));


        if (DEBUG) Log.d(TAG, "handleCancel: " + jobId);

        synchronized (mRunning) {
            // Do nothing if the cancelled ID doesn't match the current job ID. This prevents racey
            // Do nothing if the cancelled ID doesn't match the current job ID. This prevents racey
            // cancellation requests from affecting unrelated copy jobs.  However, if the current job ID
            // cancellation requests from affecting unrelated copy jobs.  However, if the current job ID
            // is null, the service most likely crashed and was revived by the incoming cancel intent.
            // is null, the service most likely crashed and was revived by the incoming cancel intent.
            // In that case, always allow the cancellation to proceed.
            // In that case, always allow the cancellation to proceed.
        if (mJob != null && Objects.equal(jobId, mJob.id)) {
            JobRecord record = mRunning.get(jobId);
            mJob.cancel();
            if (record != null) {
                record.job.cancel();

                // If the job hasn't been started, cancel it and explicitly clean up.
                // If it *has* been started, we wait for it to recognize this, then
                // allow it stop working in an orderly fashion.
                if (record.future.getDelay(TimeUnit.MILLISECONDS) > 0) {
                    record.future.cancel(false);
                    onFinished(record.job);
                }
            }
        }
        }


        // Dismiss the progress notification here rather than in the copy loop. This preserves
        // Dismiss the progress notification here rather than in the copy loop. This preserves
@@ -179,24 +192,23 @@ public class FileOperationService extends IntentService implements Job.Listener
        // Try to cancel it even if we don't have a job id...in case there is some sad
        // Try to cancel it even if we don't have a job id...in case there is some sad
        // orphan notification.
        // orphan notification.
        mNotificationManager.cancel(jobId, 0);
        mNotificationManager.cancel(jobId, 0);
    }


    public static String createJobId() {
        // TODO: Guarantee the job is being finalized
        return String.valueOf(elapsedRealtime());
    }
    }


    Job createJob(
    @GuardedBy("mRunning")
            @OpType int operationType, String id, ArrayList<DocumentInfo> srcs,
    private Job createJob(
            DocumentStack stack) {
            @OpType int operationType, String id, List<DocumentInfo> srcs, DocumentStack stack) {


        checkState(mJob == null);
        checkArgument(!mRunning.containsKey(id));


        Job job = null;
        switch (operationType) {
        switch (operationType) {
            case OPERATION_COPY:
            case OPERATION_COPY:
                mJob = new CopyJob(this, getApplicationContext(), this, id, stack, srcs);
                job = jobFactory.createCopy(this, getApplicationContext(), this, id, stack, srcs);
                break;
                break;
            case OPERATION_MOVE:
            case OPERATION_MOVE:
                mJob = new MoveJob(this, getApplicationContext(), this, id, stack, srcs);
                job = jobFactory.createMove(this, getApplicationContext(), this, id, stack, srcs);
                break;
                break;
            case OPERATION_DELETE:
            case OPERATION_DELETE:
                throw new UnsupportedOperationException();
                throw new UnsupportedOperationException();
@@ -204,42 +216,90 @@ public class FileOperationService extends IntentService implements Job.Listener
                throw new UnsupportedOperationException();
                throw new UnsupportedOperationException();
        }
        }


        return checkNotNull(mJob);
        return checkNotNull(job);
    }

    @GuardedBy("mRunning")
    private void deleteJob(Job job) {
        if (DEBUG) Log.d(TAG, "deleteJob: " + job.id);

        JobRecord record = mRunning.remove(job.id);
        checkArgument(record != null);
        record.job.cleanup();

        if (mRunning.isEmpty()) {
            shutdown();
        }
    }

    /**
     * Most likely shuts down. Won't shut down if service has a pending
     * message.
     */
    private void shutdown() {
        if (DEBUG) Log.d(TAG, "Shutting down. Last start time: " + mLastStarted);
        mWakeLock.release();
        mWakeLock = null;
        boolean gonnaStop = stopSelfResult(mLastStarted);
        if (DEBUG) Log.d(TAG, "Stopping service: " + gonnaStop);
        if (!gonnaStop) {
            Log.w(TAG, "Service should be stopping, but reports otherwise.");
        }
        // Sadly "gonnaStop" is always false in tests, so we can't guard executor shutdown.
        List<Runnable> unfinished = executor.shutdownNow();
        checkState(unfinished.isEmpty());
    }

    @VisibleForTesting
    boolean holdsWakeLock() {
        return mWakeLock != null && mWakeLock.isHeld();
    }
    }


    void deleteJob(Job job) {
    @Override
        checkArgument(job == mJob);
    public void onStart(Job job) {
        mJob = null;
        if (DEBUG) Log.d(TAG, "onStart: " + job.id);
        mNotificationManager.notify(job.id, 0, job.getSetupNotification());
    }

    @Override
    public void onFinished(Job job) {
        if (DEBUG) Log.d(TAG, "onFinished: " + job.id);

        // Dismiss the ongoing copy notification when the copy is done.
        mNotificationManager.cancel(job.id, 0);

        synchronized (mRunning) {
            deleteJob(job);
        }
    }
    }


    @Override
    @Override
    public void onProgress(CopyJob job) {
    public void onProgress(CopyJob job) {
        if (DEBUG) Log.d(TAG, "On copy progress...");
        if (DEBUG) Log.d(TAG, "onProgress: " + job.id);
        mNotificationManager.notify(job.id, 0, job.getProgressNotification());
        mNotificationManager.notify(job.id, 0, job.getProgressNotification());
    }
    }


    @Override
    @Override
    public void onProgress(MoveJob job) {
    public void onFailed(Job job) {
        if (DEBUG) Log.d(TAG, "On move progress...");
        if (DEBUG) Log.d(TAG, "onFailed: " + job.id);
        mNotificationManager.notify(job.id, 0, job.getProgressNotification());
        checkArgument(job.failed());
        Log.e(TAG, "Job failed on files: " + job.failedFiles.size() + ".");
        mNotificationManager.notify(job.id, 0, job.getFailureNotification());
        onFinished(job);  // failed jobs don't call finished, so we do.
    }
    }


    /**
    private static final class JobRecord {
     * Sets a callback to be run when the next run job is finished.
        private final Job job;
     * This is test ONLY instrumentation. The alternative is for us to add
        private final ScheduledFuture<?> future;
     * broadcast intents SOLELY for the purpose of testing.

     * @param listener
        public JobRecord(Job job, ScheduledFuture<?> future) {
     */
            this.job = job;
    @VisibleForTesting
            this.future = future;
    void addFinishedListener(TestOnlyListener listener) {
        }
        this.mJobFinishedListener = listener;
    }
    }


    /**
    @Override
     * Only used for testing. Is that obvious enough?
    public IBinder onBind(Intent intent) {
     */
        return null;  // Boilerplate. See super#onBind
    @VisibleForTesting
    interface TestOnlyListener {
        void onFinished(List<DocumentInfo> failed);
    }
    }
}
}
+16 −10

File changed.

Preview size limit exceeded, changes collapsed.

+82 −30

File changed.

Preview size limit exceeded, changes collapsed.

+22 −32

File changed.

Preview size limit exceeded, changes collapsed.

Loading