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

Commit 9d5b960f authored by Austin Tankiang's avatar Austin Tankiang
Browse files

Broadcast the progress of Jobs

Instead of using the per process monitor, create a new global one. This
to collate all job progress into one broadcast.

Also add tests for job progress. Do it in this CL, as the CL introducing
functions to get progress will get optimized out as the code wasn't
used.

Bug: 385841586
Test: atest -c 'DocumentsUIGoogleTests:com.android.documentsui.services'
Flag: com.android.documentsui.flags.visual_signals_ro

Change-Id: Ib0df69a303dbe8bf3ebd0c337491dc48fef0b87f
parent 72e54720
Loading
Loading
Loading
Loading
+74 −2
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.documentsui.services;

import static com.android.documentsui.base.SharedMinimal.DEBUG;
import static com.android.documentsui.util.FlagUtils.isVisualSignalsFlagEnabled;

import android.app.Notification;
import android.app.NotificationChannel;
@@ -126,9 +127,15 @@ public class FileOperationService extends Service implements Job.Listener {
    // Use a features to determine if notification channel is enabled.
    @VisibleForTesting Features features;

    // Used so tests can force the state of visual signals.
    @VisibleForTesting Boolean mVisualSignalsEnabled = isVisualSignalsFlagEnabled();

    @GuardedBy("mJobs")
    private final Map<String, JobRecord> mJobs = new LinkedHashMap<>();

    // Used to send periodic broadcasts for job progress.
    private GlobalJobMonitor mJobMonitor;

    // The job whose notification is used to keep the service in foreground mode.
    @GuardedBy("mJobs")
    private Job mForegroundJob;
@@ -162,6 +169,10 @@ public class FileOperationService extends Service implements Job.Listener {
            notificationManager = getSystemService(NotificationManager.class);
        }

        if (mVisualSignalsEnabled && mJobMonitor == null) {
            mJobMonitor = new GlobalJobMonitor();
        }

        UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE);
        features = new Features.RuntimeFeatures(getResources(), userManager);
        setUpNotificationChannel();
@@ -188,6 +199,10 @@ public class FileOperationService extends Service implements Job.Listener {
            Log.d(TAG, "Shutting down executor.");
        }

        if (mJobMonitor != null) {
            mJobMonitor.stop();
        }

        List<Runnable> unfinishedCopies = executor.shutdownNow();
        List<Runnable> unfinishedDeletions = deletionExecutor.shutdownNow();
        List<Runnable> unfinished =
@@ -330,6 +345,10 @@ public class FileOperationService extends Service implements Job.Listener {
        assert(record != null);
        record.job.cleanup();

        if (mVisualSignalsEnabled && mJobs.isEmpty()) {
            mJobMonitor.stop();
        }

        // Delay the shutdown until we've cleaned up all notifications. shutdown() is now posted in
        // onFinished(Job job) to main thread.
    }
@@ -389,9 +408,13 @@ public class FileOperationService extends Service implements Job.Listener {
        }

        // Set up related monitor
        if (mVisualSignalsEnabled) {
            mJobMonitor.start();
        } else {
            JobMonitor monitor = new JobMonitor(job);
            monitor.start();
        }
    }

    @Override
    public void onFinished(Job job) {
@@ -399,6 +422,9 @@ public class FileOperationService extends Service implements Job.Listener {
        if (DEBUG) {
            Log.d(TAG, "onFinished: " + job.id);
        }
        if (mVisualSignalsEnabled) {
            mJobMonitor.sendProgress();
        }

        synchronized (mJobs) {
            // Delete the job from mJobs first to avoid this job being selected as the foreground
@@ -545,6 +571,52 @@ public class FileOperationService extends Service implements Job.Listener {
        }
    }

    /**
     * A class used to periodically poll the state of every running job.
     *
     * We need to be sending the progress of every job, so rather than having a single monitor per
     * job, have one for the whole service.
     */
    private final class GlobalJobMonitor implements Runnable {
        private static final long PROGRESS_INTERVAL_MILLIS = 500L;
        private boolean mRunning = false;

        private void start() {
            if (!mRunning) {
                handler.post(this);
            }
            mRunning = true;
        }

        private void stop() {
            mRunning = false;
            handler.removeCallbacks(this);
        }

        private void sendProgress() {
            var progress = new ArrayList<JobProgress>();
            synchronized (mJobs) {
                for (JobRecord rec : mJobs.values()) {
                    progress.add(rec.job.getJobProgress());
                }
            }
            Intent intent = new Intent();
            intent.setPackage(getPackageName());
            intent.setAction("com.android.documentsui.PROGRESS");
            intent.putExtra("id", 0);
            intent.putParcelableArrayListExtra("progress", progress);
            sendBroadcast(intent);
        }

        @Override
        public void run() {
            sendProgress();
            if (mRunning) {
                handler.postDelayed(this, PROGRESS_INTERVAL_MILLIS);
            }
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;  // Boilerplate. See super#onBind
+5 −0
Original line number Diff line number Diff line
@@ -44,6 +44,11 @@ class FlagUtils {
            return Flags.desktopFileHandlingRo()
        }

        @JvmStatic
        fun isVisualSignalsFlagEnabled(): Boolean {
            return Flags.visualSignalsRo() && isUseMaterial3FlagEnabled()
        }

        @JvmStatic
        fun isHideRootsOnDesktopFlagEnabled(): Boolean {
            return Flags.hideRootsOnDesktopRo()
+55 −5
Original line number Diff line number Diff line
@@ -44,6 +44,23 @@ public abstract class AbstractCopyJobTest<T extends CopyJob> extends AbstractJob
        mOpType = opType;
    }

    private String getVerb() {
        switch(mOpType) {
            case FileOperationService.OPERATION_COPY:
            case FileOperationService.OPERATION_EXTRACT:
                return "Copying";
            case FileOperationService.OPERATION_COMPRESS:
                return "Zipping";
            case FileOperationService.OPERATION_MOVE:
                return "Moving";
            case FileOperationService.OPERATION_DELETE:
                // DeleteJob does not inherit from CopyJob
            case FileOperationService.OPERATION_UNKNOWN:
            default:
                return "";
        }
    }

    public void runCopyFilesTest() throws Exception {
        Uri testFile1 = mDocs.createDocument(mSrcRoot, "text/plain", "test1.txt");
        mDocs.writeDocument(testFile1, HAM_BYTES);
@@ -51,7 +68,11 @@ public abstract class AbstractCopyJobTest<T extends CopyJob> extends AbstractJob
        Uri testFile2 = mDocs.createDocument(mSrcRoot, "text/plain", "test2.txt");
        mDocs.writeDocument(testFile2, FRUITY_BYTES);

        createJob(newArrayList(testFile1, testFile2)).run();
        CopyJob job = createJob(newArrayList(testFile1, testFile2));
        JobProgress progress = job.getJobProgress();
        assertEquals(Job.STATE_CREATED, progress.state);

        job.run();
        mJobListener.waitForFinished();

        mDocs.assertChildCount(mDestRoot, 2);
@@ -59,6 +80,13 @@ public abstract class AbstractCopyJobTest<T extends CopyJob> extends AbstractJob
        mDocs.assertHasFile(mDestRoot, "test2.txt");
        mDocs.assertFileContents(mDestRoot.documentId, "test1.txt", HAM_BYTES);
        mDocs.assertFileContents(mDestRoot.documentId, "test2.txt", FRUITY_BYTES);

        progress = job.getJobProgress();
        assertEquals(Job.STATE_COMPLETED, progress.state);
        assertFalse(progress.hasFailures);
        assertEquals(getVerb() + " 2 files to " + mDestRoot.title, progress.msg);
        assertEquals(HAM_BYTES.length + FRUITY_BYTES.length, progress.currentBytes);
        assertEquals(HAM_BYTES.length + FRUITY_BYTES.length, progress.requiredBytes);
    }

    public void runCopyVirtualTypedFileTest() throws Exception {
@@ -66,13 +94,20 @@ public abstract class AbstractCopyJobTest<T extends CopyJob> extends AbstractJob
                mSrcRoot, "/virtual.sth", "virtual/mime-type",
                FRUITY_BYTES, "application/pdf", "text/html");

        createJob(newArrayList(testFile)).run();

        CopyJob job = createJob(newArrayList(testFile));
        job.run();
        waitForJobFinished();

        mDocs.assertChildCount(mDestRoot, 1);
        mDocs.assertHasFile(mDestRoot, "virtual.sth.pdf");  // copy should convert file to PDF.
        mDocs.assertFileContents(mDestRoot.documentId, "virtual.sth.pdf", FRUITY_BYTES);

        JobProgress progress = job.getJobProgress();
        assertEquals(Job.STATE_COMPLETED, progress.state);
        assertFalse(progress.hasFailures);
        assertEquals("Copying virtual.sth to " + mDestRoot.title, progress.msg);
        assertEquals(FRUITY_BYTES.length, progress.currentBytes);
        assertEquals(FRUITY_BYTES.length, progress.requiredBytes);
    }

    public void runCopyVirtualNonTypedFileTest() throws Exception {
@@ -80,13 +115,21 @@ public abstract class AbstractCopyJobTest<T extends CopyJob> extends AbstractJob
                mSrcRoot, "/virtual.sth", "virtual/mime-type",
                FRUITY_BYTES);

        createJob(newArrayList(testFile)).run();

        CopyJob job = createJob(newArrayList(testFile));
        job.run();
        waitForJobFinished();

        mJobListener.assertFailed();
        mJobListener.assertFilesFailed(newArrayList("virtual.sth"));

        mDocs.assertChildCount(mDestRoot, 0);

        JobProgress progress = job.getJobProgress();
        assertEquals(Job.STATE_COMPLETED, progress.state);
        assertTrue(progress.hasFailures);
        assertEquals(getVerb() + " virtual.sth to " + mDestRoot.title, progress.msg);
        assertEquals(0, progress.currentBytes);
        assertEquals(FRUITY_BYTES.length, progress.requiredBytes);
    }

    public void runCopyEmptyDirTest() throws Exception {
@@ -105,6 +148,13 @@ public abstract class AbstractCopyJobTest<T extends CopyJob> extends AbstractJob

        mDocs.assertChildCount(mDestRoot, 1);
        mDocs.assertHasDirectory(mDestRoot, "emptyDir");

        JobProgress progress = job.getJobProgress();
        assertEquals(Job.STATE_COMPLETED, progress.state);
        assertFalse(progress.hasFailures);
        assertEquals(getVerb() + " emptyDir to " + mDestRoot.title, progress.msg);
        assertEquals(-1, progress.currentBytes);
        assertEquals(-1, progress.requiredBytes);
    }

    public void runCopyDirRecursivelyTest() throws Exception {
+9 −1
Original line number Diff line number Diff line
@@ -52,11 +52,19 @@ public class CopyJobTest extends AbstractCopyJobTest<CopyJob> {
                Document.FLAG_VIRTUAL_DOCUMENT | Document.FLAG_SUPPORTS_COPY
                        | Document.FLAG_SUPPORTS_MOVE, "application/pdf");

        createJob(newArrayList(testFile)).run();
        CopyJob job = createJob(newArrayList(testFile));
        job.run();

        waitForJobFinished();
        mDocs.assertChildCount(mDestRoot, 1);
        mDocs.assertHasFile(mDestRoot, "tokyo.sth.pdf");  // Copy should convert file to PDF.

        JobProgress progress = job.getJobProgress();
        assertEquals(Job.STATE_COMPLETED, progress.state);
        assertFalse(progress.hasFailures);
        assertEquals("Copying tokyo.sth to " + mDestRoot.title, progress.msg);
        assertEquals(-1, progress.currentBytes);
        assertEquals(-1, progress.requiredBytes);
    }

    public void testCopyEmptyDir() throws Exception {
+14 −3
Original line number Diff line number Diff line
@@ -37,11 +37,17 @@ public class DeleteJobTest extends AbstractJobTest<DeleteJob> {
        Uri testFile2 = mDocs.createDocument(mSrcRoot, "text/plain", "test2.txt");
        mDocs.writeDocument(testFile2, FRUITY_BYTES);

        createJob(newArrayList(testFile1, testFile2),
                DocumentsContract.buildDocumentUri(AUTHORITY, mSrcRoot.documentId)).run();
        DeleteJob job = createJob(newArrayList(testFile1, testFile2),
                DocumentsContract.buildDocumentUri(AUTHORITY, mSrcRoot.documentId));
        job.run();
        mJobListener.waitForFinished();

        mDocs.assertChildCount(mSrcRoot, 0);

        var progress = job.getJobProgress();
        assertEquals(Job.STATE_COMPLETED, progress.state);
        assertFalse(progress.hasFailures);
        assertEquals("Deleting 2 files", progress.msg);
    }

    public void testDeleteFiles_NoSrcParent() throws Exception {
@@ -51,10 +57,15 @@ public class DeleteJobTest extends AbstractJobTest<DeleteJob> {
        Uri testFile2 = mDocs.createDocument(mSrcRoot, "text/plain", "test2.txt");
        mDocs.writeDocument(testFile2, FRUITY_BYTES);

        createJob(newArrayList(testFile1, testFile2), null).run();
        DeleteJob job = createJob(newArrayList(testFile1, testFile2), null);
        job.run();
        mJobListener.waitForFinished();

        mDocs.assertChildCount(mSrcRoot, 0);
        var progress = job.getJobProgress();
        assertEquals(Job.STATE_COMPLETED, progress.state);
        assertFalse(progress.hasFailures);
        assertEquals("Deleting 2 files", progress.msg);
    }

    /**
Loading