Loading src/com/android/documentsui/services/FileOperationService.java +74 −2 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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(); Loading @@ -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 = Loading Loading @@ -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. } Loading Loading @@ -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) { Loading @@ -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 Loading Loading @@ -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 Loading src/com/android/documentsui/util/FlagUtils.kt +5 −0 Original line number Diff line number Diff line Loading @@ -44,6 +44,11 @@ class FlagUtils { return Flags.desktopFileHandlingRo() } @JvmStatic fun isVisualSignalsFlagEnabled(): Boolean { return Flags.visualSignalsRo() && isUseMaterial3FlagEnabled() } @JvmStatic fun isHideRootsOnDesktopFlagEnabled(): Boolean { return Flags.hideRootsOnDesktopRo() Loading tests/functional/com/android/documentsui/services/AbstractCopyJobTest.java +55 −5 Original line number Diff line number Diff line Loading @@ -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); Loading @@ -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); Loading @@ -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 { Loading @@ -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 { Loading @@ -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 { Loading @@ -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 { Loading tests/functional/com/android/documentsui/services/CopyJobTest.java +9 −1 Original line number Diff line number Diff line Loading @@ -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 { Loading tests/functional/com/android/documentsui/services/DeleteJobTest.java +14 −3 Original line number Diff line number Diff line Loading @@ -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 { Loading @@ -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 Loading
src/com/android/documentsui/services/FileOperationService.java +74 −2 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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(); Loading @@ -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 = Loading Loading @@ -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. } Loading Loading @@ -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) { Loading @@ -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 Loading Loading @@ -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 Loading
src/com/android/documentsui/util/FlagUtils.kt +5 −0 Original line number Diff line number Diff line Loading @@ -44,6 +44,11 @@ class FlagUtils { return Flags.desktopFileHandlingRo() } @JvmStatic fun isVisualSignalsFlagEnabled(): Boolean { return Flags.visualSignalsRo() && isUseMaterial3FlagEnabled() } @JvmStatic fun isHideRootsOnDesktopFlagEnabled(): Boolean { return Flags.hideRootsOnDesktopRo() Loading
tests/functional/com/android/documentsui/services/AbstractCopyJobTest.java +55 −5 Original line number Diff line number Diff line Loading @@ -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); Loading @@ -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); Loading @@ -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 { Loading @@ -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 { Loading @@ -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 { Loading @@ -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 { Loading
tests/functional/com/android/documentsui/services/CopyJobTest.java +9 −1 Original line number Diff line number Diff line Loading @@ -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 { Loading
tests/functional/com/android/documentsui/services/DeleteJobTest.java +14 −3 Original line number Diff line number Diff line Loading @@ -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 { Loading @@ -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