/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.documentsui.services; import static com.android.documentsui.services.FileOperationService.OPERATION_COPY; import static com.android.documentsui.services.FileOperationService.OPERATION_DELETE; import static com.android.documentsui.services.FileOperations.createBaseIntent; import static com.android.documentsui.services.FileOperations.createJobId; import static org.junit.Assert.fail; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; import android.test.ServiceTestCase; import androidx.test.InstrumentationRegistry; import androidx.test.filters.MediumTest; import com.android.documentsui.R; import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.DocumentStack; import com.android.documentsui.base.Features; import com.android.documentsui.clipping.UrisSupplier; import com.android.documentsui.services.FileOperationService.OpType; import com.android.documentsui.testing.DocsProviders; import com.android.documentsui.testing.TestFeatures; import com.android.documentsui.testing.TestHandler; import com.android.documentsui.testing.TestScheduledExecutorService; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @MediumTest public class FileOperationServiceTest extends ServiceTestCase { private static final Uri SRC_PARENT = Uri.parse("content://com.android.documentsui.testing/parent"); private static final DocumentInfo ALPHA_DOC = createDoc("alpha"); private static final DocumentInfo BETA_DOC = createDoc("alpha"); private static final DocumentInfo GAMMA_DOC = createDoc("gamma"); private static final DocumentInfo DELTA_DOC = createDoc("delta"); private final List mCopyJobs = new ArrayList<>(); private final List mDeleteJobs = new ArrayList<>(); private FileOperationService mService; private TestScheduledExecutorService mExecutor; private TestScheduledExecutorService mDeletionExecutor; private TestHandler mHandler; private TestForegroundManager mForegroundManager; private TestNotificationManager mTestNotificationManager; public FileOperationServiceTest() { super(FileOperationService.class); } @Override protected void setUp() throws Exception { super.setUp(); setupService(); // must be called first for our test setup to work correctly. mExecutor = new TestScheduledExecutorService(); mDeletionExecutor = new TestScheduledExecutorService(); mHandler = new TestHandler(); mTestNotificationManager = new TestNotificationManager(); mForegroundManager = new TestForegroundManager(mTestNotificationManager); TestFeatures features = new TestFeatures(); features.notificationChannel = InstrumentationRegistry.getTargetContext() .getResources().getBoolean(R.bool.feature_notification_channel); mCopyJobs.clear(); mDeleteJobs.clear(); // Install test doubles. mService = getService(); assertNull(mService.executor); mService.executor = mExecutor; assertNull(mService.deletionExecutor); mService.deletionExecutor = mDeletionExecutor; assertNull(mService.handler); mService.handler = mHandler; assertNull(mService.foregroundManager); mService.foregroundManager = mForegroundManager; assertNull(mService.notificationManager); mService.notificationManager = mTestNotificationManager.createNotificationManager(); assertNull(mService.features); mService.features = features; } @Override protected void tearDown() { // Release all possibly held wake lock here mExecutor.runAll(); mDeletionExecutor.runAll(); // There are lots of progress notifications generated in this test case. // Dismiss all of them here. mHandler.dispatchAllMessages(); } public void testRunsCopyJobs() throws Exception { startService(createCopyIntent(Arrays.asList(ALPHA_DOC), BETA_DOC)); startService(createCopyIntent(Arrays.asList(GAMMA_DOC), DELTA_DOC)); mExecutor.runAll(); assertAllCopyJobsStarted(); } public void testRunsCopyJobs_AfterExceptionInJobCreation() throws Exception { try { startService(createCopyIntent(new ArrayList<>(), BETA_DOC)); fail("Should have throw exception."); } catch (IllegalArgumentException expected) { // We're sending a naughty empty list that should result in an IllegalArgumentException. } startService(createCopyIntent(Arrays.asList(GAMMA_DOC), DELTA_DOC)); assertJobsCreated(1); mExecutor.runAll(); assertAllCopyJobsStarted(); } public void testRunsCopyJobs_AfterFailure() throws Exception { startService(createCopyIntent(Arrays.asList(ALPHA_DOC), BETA_DOC)); startService(createCopyIntent(Arrays.asList(GAMMA_DOC), DELTA_DOC)); mCopyJobs.get(0).fail(ALPHA_DOC); mExecutor.runAll(); assertAllCopyJobsStarted(); } public void testRunsCopyJobs_notRunsDeleteJobs() throws Exception { startService(createCopyIntent(Arrays.asList(ALPHA_DOC), BETA_DOC)); startService(createDeleteIntent(Arrays.asList(GAMMA_DOC))); mExecutor.runAll(); assertNoDeleteJobsStarted(); } public void testRunsDeleteJobs() throws Exception { startService(createDeleteIntent(Arrays.asList(ALPHA_DOC))); mDeletionExecutor.runAll(); assertAllDeleteJobsStarted(); } public void testRunsDeleteJobs_NotRunsCopyJobs() throws Exception { startService(createCopyIntent(Arrays.asList(ALPHA_DOC), BETA_DOC)); startService(createDeleteIntent(Arrays.asList(GAMMA_DOC))); mDeletionExecutor.runAll(); assertNoCopyJobsStarted(); } public void testUpdatesNotification() throws Exception { startService(createCopyIntent(Arrays.asList(ALPHA_DOC), BETA_DOC)); mExecutor.runAll(); // Assert monitoring continues until job is done assertTrue(mHandler.hasScheduledMessage()); // Two notifications -- one for setup; one for progress assertEquals(2, mCopyJobs.get(0).getNumOfNotifications()); } public void testStopsUpdatingNotificationAfterFinished() throws Exception { startService(createCopyIntent(Arrays.asList(ALPHA_DOC), BETA_DOC)); mExecutor.runAll(); mHandler.dispatchNextMessage(); // Assert monitoring stops once job is completed. assertFalse(mHandler.hasScheduledMessage()); // Assert no more notification is generated after finish. assertEquals(2, mCopyJobs.get(0).getNumOfNotifications()); } public void testHoldsWakeLockWhileWorking() throws Exception { startService(createCopyIntent(Arrays.asList(ALPHA_DOC), BETA_DOC)); assertTrue(mService.holdsWakeLock()); } public void testReleasesWakeLock_AfterSuccess() throws Exception { startService(createCopyIntent(Arrays.asList(ALPHA_DOC), BETA_DOC)); assertTrue(mService.holdsWakeLock()); mExecutor.runAll(); assertFalse(mService.holdsWakeLock()); } public void testReleasesWakeLock_AfterFailure() throws Exception { startService(createCopyIntent(Arrays.asList(ALPHA_DOC), BETA_DOC)); assertTrue(mService.holdsWakeLock()); mExecutor.runAll(); assertFalse(mService.holdsWakeLock()); } public void testShutdownStopsExecutor_AfterSuccess() throws Exception { startService(createCopyIntent(Arrays.asList(ALPHA_DOC), BETA_DOC)); mExecutor.assertAlive(); mDeletionExecutor.assertAlive(); mExecutor.runAll(); shutdownService(); assertExecutorsShutdown(); } public void testShutdownStopsExecutor_AfterMixedFailures() throws Exception { startService(createCopyIntent(Arrays.asList(ALPHA_DOC), BETA_DOC)); startService(createCopyIntent(Arrays.asList(GAMMA_DOC), DELTA_DOC)); mCopyJobs.get(0).fail(ALPHA_DOC); mExecutor.runAll(); shutdownService(); assertExecutorsShutdown(); } public void testShutdownStopsExecutor_AfterTotalFailure() throws Exception { startService(createCopyIntent(Arrays.asList(ALPHA_DOC), BETA_DOC)); startService(createCopyIntent(Arrays.asList(GAMMA_DOC), DELTA_DOC)); mCopyJobs.get(0).fail(ALPHA_DOC); mCopyJobs.get(1).fail(GAMMA_DOC); mExecutor.runAll(); shutdownService(); assertExecutorsShutdown(); } public void testRunsInForeground_MultipleJobs() throws Exception { startService(createCopyIntent(Arrays.asList(ALPHA_DOC), BETA_DOC)); startService(createCopyIntent(Arrays.asList(GAMMA_DOC), DELTA_DOC)); mExecutor.run(0); mForegroundManager.assertInForeground(); mHandler.dispatchAllMessages(); mForegroundManager.assertInForeground(); } public void testFinishesInBackground_MultipleJobs() throws Exception { startService(createCopyIntent(Arrays.asList(ALPHA_DOC), BETA_DOC)); startService(createCopyIntent(Arrays.asList(GAMMA_DOC), DELTA_DOC)); mExecutor.run(0); mForegroundManager.assertInForeground(); mHandler.dispatchAllMessages(); mForegroundManager.assertInForeground(); mExecutor.run(0); mHandler.dispatchAllMessages(); mForegroundManager.assertInBackground(); } public void testAllNotificationsDismissedAfterShutdown() throws Exception { startService(createCopyIntent(Arrays.asList(ALPHA_DOC), BETA_DOC)); startService(createCopyIntent(Arrays.asList(GAMMA_DOC), DELTA_DOC)); mExecutor.runAll(); mHandler.dispatchAllMessages(); mTestNotificationManager.assertNumberOfNotifications(0); } public void testNotificationUpdateAfterForegroundJobSwitch() throws Exception { startService(createCopyIntent(Arrays.asList(ALPHA_DOC), BETA_DOC)); startService(createCopyIntent(Arrays.asList(GAMMA_DOC), DELTA_DOC)); Job job1 = mCopyJobs.get(0); Job job2 = mCopyJobs.get(1); mService.onStart(job1); mTestNotificationManager.assertHasNotification( FileOperationService.NOTIFICATION_ID_PROGRESS, null); mService.onStart(job2); mTestNotificationManager.assertHasNotification( FileOperationService.NOTIFICATION_ID_PROGRESS, job2.id); job1.cancel(); mService.onFinished(job1); mTestNotificationManager.assertHasNotification( FileOperationService.NOTIFICATION_ID_PROGRESS, null); mTestNotificationManager.assertNoNotification( FileOperationService.NOTIFICATION_ID_PROGRESS, job2.id); job2.cancel(); mService.onFinished(job2); mTestNotificationManager.assertNumberOfNotifications(0); } private Intent createCopyIntent(List files, DocumentInfo dest) throws Exception { DocumentStack stack = new DocumentStack(); stack.push(dest); List uris = new ArrayList<>(files.size()); for (DocumentInfo file : files) { uris.add(file.derivedUri); } UrisSupplier urisSupplier = DocsProviders.createDocsProvider(uris); TestFileOperation operation = new TestFileOperation(OPERATION_COPY, urisSupplier, stack); return createBaseIntent(getContext(), createJobId(), operation); } private Intent createDeleteIntent(List files) { DocumentStack stack = new DocumentStack(); List uris = new ArrayList<>(files.size()); for (DocumentInfo file : files) { uris.add(file.derivedUri); } UrisSupplier urisSupplier = DocsProviders.createDocsProvider(uris); TestFileOperation operation = new TestFileOperation(OPERATION_DELETE, urisSupplier, stack); return createBaseIntent(getContext(), createJobId(), operation); } private static DocumentInfo createDoc(String name) { // Doesn't need to be valid content Uri, just some urly looking thing. Uri uri = new Uri.Builder() .scheme("content") .authority("com.android.documentsui.testing") .path(name) .build(); return createDoc(uri); } void assertAllCopyJobsStarted() { for (TestJob job : mCopyJobs) { job.assertStarted(); } } void assertAllDeleteJobsStarted() { for (TestJob job : mDeleteJobs) { job.assertStarted(); } } void assertNoCopyJobsStarted() { for (TestJob job : mCopyJobs) { job.assertNotStarted(); } } void assertNoDeleteJobsStarted() { for (TestJob job : mDeleteJobs) { job.assertNotStarted(); } } void assertJobsCreated(int expected) { assertEquals(expected, mCopyJobs.size() + mDeleteJobs.size()); } private static DocumentInfo createDoc(Uri destination) { DocumentInfo destDoc = new DocumentInfo(); destDoc.derivedUri = destination; return destDoc; } private void assertExecutorsShutdown() { mExecutor.assertShutdown(); mDeletionExecutor.assertShutdown(); } private final class TestFileOperation extends FileOperation { private final Runnable mJobRunnable = () -> { // The following statement is executed concurrently to Job.start() in real situation. // Call it in TestJob.start() to mimic this behavior. mHandler.dispatchNextMessage(); }; private final @OpType int mOpType; private final UrisSupplier mSrcs; private final DocumentStack mDestination; private TestFileOperation( @OpType int opType, UrisSupplier srcs, DocumentStack destination) { super(opType, srcs, destination); mOpType = opType; mSrcs = srcs; mDestination = destination; } @Override public Job createJob(Context service, Job.Listener listener, String id, Features features) { TestJob job = new TestJob( service, listener, id, mOpType, mDestination, mSrcs, mJobRunnable, features); if (mOpType == OPERATION_COPY) { mCopyJobs.add(job); } if (mOpType == OPERATION_DELETE) { mDeleteJobs.add(job); } return job; } /** * CREATOR is required for Parcelables, but we never pass this class via parcel. */ public Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override public TestFileOperation createFromParcel(Parcel source) { throw new UnsupportedOperationException("Can't create from a parcel."); } @Override public TestFileOperation[] newArray(int size) { throw new UnsupportedOperationException("Can't create a new array."); } }; } }