summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Austin Tankiang <austinct@google.com> 2025-03-06 13:25:12 -0800
committer Android (Google) Code Review <android-gerrit@google.com> 2025-03-06 13:25:12 -0800
commitb6d83baa452320fa5c2d35afb8bd31785041574e (patch)
tree98fbaa038938bcd35f1d235efa9bd3f9a0654098
parent800e5f08ccdde397b2ae935e3b176c6b88a46ed6 (diff)
parent9d5b960f0229d9a0e10bcca4851073bce2ead0eb (diff)
Merge "Broadcast the progress of Jobs" into main
-rw-r--r--src/com/android/documentsui/services/FileOperationService.java76
-rw-r--r--src/com/android/documentsui/util/FlagUtils.kt5
-rw-r--r--tests/functional/com/android/documentsui/services/AbstractCopyJobTest.java60
-rw-r--r--tests/functional/com/android/documentsui/services/CopyJobTest.java10
-rw-r--r--tests/functional/com/android/documentsui/services/DeleteJobTest.java17
-rw-r--r--tests/functional/com/android/documentsui/services/FileOperationServiceTest.java2
6 files changed, 159 insertions, 11 deletions
diff --git a/src/com/android/documentsui/services/FileOperationService.java b/src/com/android/documentsui/services/FileOperationService.java
index c7be5f4fc..dcb2c1db4 100644
--- a/src/com/android/documentsui/services/FileOperationService.java
+++ b/src/com/android/documentsui/services/FileOperationService.java
@@ -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,8 +408,12 @@ public class FileOperationService extends Service implements Job.Listener {
}
// Set up related monitor
- JobMonitor monitor = new JobMonitor(job);
- monitor.start();
+ if (mVisualSignalsEnabled) {
+ mJobMonitor.start();
+ } else {
+ JobMonitor monitor = new JobMonitor(job);
+ monitor.start();
+ }
}
@Override
@@ -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
diff --git a/src/com/android/documentsui/util/FlagUtils.kt b/src/com/android/documentsui/util/FlagUtils.kt
index 606f071f2..eee51be89 100644
--- a/src/com/android/documentsui/util/FlagUtils.kt
+++ b/src/com/android/documentsui/util/FlagUtils.kt
@@ -45,6 +45,11 @@ class FlagUtils {
}
@JvmStatic
+ fun isVisualSignalsFlagEnabled(): Boolean {
+ return Flags.visualSignalsRo() && isUseMaterial3FlagEnabled()
+ }
+
+ @JvmStatic
fun isHideRootsOnDesktopFlagEnabled(): Boolean {
return Flags.hideRootsOnDesktopRo()
}
diff --git a/tests/functional/com/android/documentsui/services/AbstractCopyJobTest.java b/tests/functional/com/android/documentsui/services/AbstractCopyJobTest.java
index 525397bc0..b20942473 100644
--- a/tests/functional/com/android/documentsui/services/AbstractCopyJobTest.java
+++ b/tests/functional/com/android/documentsui/services/AbstractCopyJobTest.java
@@ -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 {
diff --git a/tests/functional/com/android/documentsui/services/CopyJobTest.java b/tests/functional/com/android/documentsui/services/CopyJobTest.java
index fd552d1c0..9074ef4c3 100644
--- a/tests/functional/com/android/documentsui/services/CopyJobTest.java
+++ b/tests/functional/com/android/documentsui/services/CopyJobTest.java
@@ -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 {
diff --git a/tests/functional/com/android/documentsui/services/DeleteJobTest.java b/tests/functional/com/android/documentsui/services/DeleteJobTest.java
index 55e804484..86d9930a2 100644
--- a/tests/functional/com/android/documentsui/services/DeleteJobTest.java
+++ b/tests/functional/com/android/documentsui/services/DeleteJobTest.java
@@ -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);
}
/**
diff --git a/tests/functional/com/android/documentsui/services/FileOperationServiceTest.java b/tests/functional/com/android/documentsui/services/FileOperationServiceTest.java
index 1816ed540..5820f460f 100644
--- a/tests/functional/com/android/documentsui/services/FileOperationServiceTest.java
+++ b/tests/functional/com/android/documentsui/services/FileOperationServiceTest.java
@@ -109,6 +109,8 @@ public class FileOperationServiceTest extends ServiceTestCase<FileOperationServi
assertNull(mService.features);
mService.features = features;
+
+ mService.mVisualSignalsEnabled = false;
}
@Override