Loading apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java +44 −5 Original line number Diff line number Diff line Loading @@ -618,6 +618,26 @@ public final class JobServiceContext implements ServiceConnection { return mRunningJob; } @VisibleForTesting void setRunningJobLockedForTest(JobStatus job) { mRunningJob = job; } @VisibleForTesting void setJobParamsLockedForTest(JobParameters params) { mParams = params; } @VisibleForTesting void setRunningCallbackLockedForTest(JobCallback callback) { mRunningCallback = callback; } @VisibleForTesting void setPendingStopReasonLockedForTest(int stopReason) { mPendingStopReason = stopReason; } @JobConcurrencyManager.WorkType int getRunningJobWorkType() { return mRunningJobWorkType; Loading Loading @@ -786,6 +806,7 @@ public final class JobServiceContext implements ServiceConnection { executing = getRunningJobLocked(); } if (executing != null && jobId == executing.getJobId()) { executing.setAbandoned(true); final StringBuilder stateSuffix = new StringBuilder(); stateSuffix.append(TRACE_ABANDONED_JOB); stateSuffix.append(executing.getBatteryName()); Loading Loading @@ -1364,8 +1385,9 @@ public final class JobServiceContext implements ServiceConnection { } /** Process MSG_TIMEOUT here. */ @VisibleForTesting @GuardedBy("mLock") private void handleOpTimeoutLocked() { void handleOpTimeoutLocked() { switch (mVerb) { case VERB_BINDING: // The system may have been too busy. Don't drop the job or trigger an ANR. Loading Loading @@ -1427,9 +1449,25 @@ public final class JobServiceContext implements ServiceConnection { // Not an error - client ran out of time. Slog.i(TAG, "Client timed out while executing (no jobFinished received)." + " Sending onStop: " + getRunningJobNameLocked()); mParams.setStopReason(JobParameters.STOP_REASON_TIMEOUT, JobParameters.INTERNAL_STOP_REASON_TIMEOUT, "client timed out"); sendStopMessageLocked("timeout while executing"); final JobStatus executing = getRunningJobLocked(); int stopReason = JobParameters.STOP_REASON_TIMEOUT; int internalStopReason = JobParameters.INTERNAL_STOP_REASON_TIMEOUT; final StringBuilder stopMessage = new StringBuilder("timeout while executing"); final StringBuilder debugStopReason = new StringBuilder("client timed out"); if (android.app.job.Flags.handleAbandonedJobs() && executing != null && executing.isAbandoned()) { final String abandonedMessage = " and maybe abandoned"; stopReason = JobParameters.STOP_REASON_TIMEOUT_ABANDONED; internalStopReason = JobParameters.INTERNAL_STOP_REASON_TIMEOUT_ABANDONED; stopMessage.append(abandonedMessage); debugStopReason.append(abandonedMessage); } mParams.setStopReason(stopReason, internalStopReason, debugStopReason.toString()); sendStopMessageLocked(stopMessage.toString()); } else if (nowElapsed >= earliestStopTimeElapsed) { // We've given the app the minimum execution time. See if we should stop it or // let it continue running Loading Loading @@ -1479,8 +1517,9 @@ public final class JobServiceContext implements ServiceConnection { * Already running, need to stop. Will switch {@link #mVerb} from VERB_EXECUTING -> * VERB_STOPPING. */ @VisibleForTesting @GuardedBy("mLock") private void sendStopMessageLocked(@Nullable String reason) { void sendStopMessageLocked(@Nullable String reason) { removeOpTimeOutLocked(); if (mVerb != VERB_EXECUTING) { Slog.e(TAG, "Sending onStopJob for a job that isn't started. " + mRunningJob); Loading apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java +19 −0 Original line number Diff line number Diff line Loading @@ -575,6 +575,13 @@ public final class JobStatus { /** The system trace tag for this job. */ private String mSystemTraceTag; /** * Job maybe abandoned by not calling * {@link android.app.job.JobService#jobFinished(JobParameters, boolean)} while * the strong reference to {@link android.app.job.JobParameters} is lost */ private boolean mIsAbandoned; /** * Core constructor for JobStatus instances. All other ctors funnel down to this one. * Loading Loading @@ -725,6 +732,8 @@ public final class JobStatus { updateNetworkBytesLocked(); updateMediaBackupExemptionStatus(); mIsAbandoned = false; } /** Copy constructor: used specifically when cloning JobStatus objects for persistence, Loading Loading @@ -1061,6 +1070,16 @@ public final class JobStatus { return job.getTraceTag(); } /** Returns if the job maybe abandoned */ public boolean isAbandoned() { return mIsAbandoned; } /** Set the job maybe abandoned state*/ public void setAbandoned(boolean abandoned) { mIsAbandoned = abandoned; } /** Returns a trace tag using debug information provided by job scheduler service. */ @NonNull public String computeSystemTraceTag() { Loading services/tests/mockingservicestests/src/com/android/server/job/JobServiceContextTest.java 0 → 100644 +270 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.server.job; import static android.app.job.Flags.FLAG_HANDLE_ABANDONED_JOBS; import static android.text.format.DateUtils.MINUTE_IN_MILLIS; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.app.AppGlobals; import android.app.job.JobParameters; import android.content.Context; import android.os.Looper; import android.os.PowerManager; import android.os.SystemClock; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import com.android.internal.app.IBatteryStats; import com.android.server.job.JobServiceContext.JobCallback; import com.android.server.job.controllers.JobStatus; import org.junit.After; import org.junit.Before; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoSession; import org.mockito.quality.Strictness; import java.time.Clock; import java.time.Duration; import java.time.ZoneOffset; public class JobServiceContextTest { private static final String TAG = JobServiceContextTest.class.getSimpleName(); @ClassRule public static final SetFlagsRule.ClassRule mSetFlagsClassRule = new SetFlagsRule.ClassRule(); @Rule public final SetFlagsRule mSetFlagsRule = mSetFlagsClassRule.createSetFlagsRule(); @Mock private JobSchedulerService mMockJobSchedulerService; @Mock private JobConcurrencyManager mMockConcurrencyManager; @Mock private JobNotificationCoordinator mMockNotificationCoordinator; @Mock private IBatteryStats.Stub mMockBatteryStats; @Mock private JobPackageTracker mMockJobPackageTracker; @Mock private Looper mMockLooper; @Mock private Context mMockContext; @Mock private JobStatus mMockJobStatus; @Mock private JobParameters mMockJobParameters; @Mock private JobCallback mMockJobCallback; private MockitoSession mMockingSession; private JobServiceContext mJobServiceContext; private Object mLock; @Before public void setUp() throws Exception { mMockingSession = mockitoSession() .initMocks(this) .mockStatic(AppGlobals.class) .strictness(Strictness.LENIENT) .startMocking(); JobSchedulerService.sElapsedRealtimeClock = Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC); doReturn(mock(PowerManager.class)).when(mMockContext).getSystemService(PowerManager.class); doReturn(mMockContext).when(mMockJobSchedulerService).getContext(); mLock = new Object(); doReturn(mLock).when(mMockJobSchedulerService).getLock(); mJobServiceContext = new JobServiceContext( mMockJobSchedulerService, mMockConcurrencyManager, mMockNotificationCoordinator, mMockBatteryStats, mMockJobPackageTracker, mMockLooper); spyOn(mJobServiceContext); mJobServiceContext.setJobParamsLockedForTest(mMockJobParameters); } @After public void tearDown() throws Exception { if (mMockingSession != null) { mMockingSession.finishMocking(); } } private Clock getAdvancedClock(Clock clock, long incrementMs) { return Clock.offset(clock, Duration.ofMillis(incrementMs)); } private void advanceElapsedClock(long incrementMs) { JobSchedulerService.sElapsedRealtimeClock = getAdvancedClock(JobSchedulerService.sElapsedRealtimeClock, incrementMs); } /** * Test that Abandoned jobs that are timed out are stopped with the correct stop reason */ @Test @EnableFlags(FLAG_HANDLE_ABANDONED_JOBS) public void testJobServiceContext_TimeoutAbandonedJob() { mJobServiceContext.mVerb = JobServiceContext.VERB_EXECUTING; ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class); doNothing().when(mJobServiceContext).sendStopMessageLocked(captor.capture()); advanceElapsedClock(30 * MINUTE_IN_MILLIS); // 30 minutes mJobServiceContext.setPendingStopReasonLockedForTest(JobParameters.STOP_REASON_UNDEFINED); mJobServiceContext.setRunningJobLockedForTest(mMockJobStatus); doReturn(true).when(mMockJobStatus).isAbandoned(); mJobServiceContext.mVerb = JobServiceContext.VERB_EXECUTING; mJobServiceContext.handleOpTimeoutLocked(); String stopMessage = captor.getValue(); assertEquals("timeout while executing and maybe abandoned", stopMessage); verify(mMockJobParameters) .setStopReason( JobParameters.STOP_REASON_TIMEOUT_ABANDONED, JobParameters.INTERNAL_STOP_REASON_TIMEOUT_ABANDONED, "client timed out and maybe abandoned"); } /** * Test that non-abandoned jobs that are timed out are stopped with the correct stop reason */ @Test @EnableFlags(FLAG_HANDLE_ABANDONED_JOBS) public void testJobServiceContext_TimeoutNoAbandonedJob() { mJobServiceContext.mVerb = JobServiceContext.VERB_EXECUTING; ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class); doNothing().when(mJobServiceContext).sendStopMessageLocked(captor.capture()); advanceElapsedClock(30 * MINUTE_IN_MILLIS); // 30 minutes mJobServiceContext.setPendingStopReasonLockedForTest(JobParameters.STOP_REASON_UNDEFINED); mJobServiceContext.setRunningJobLockedForTest(mMockJobStatus); doReturn(false).when(mMockJobStatus).isAbandoned(); mJobServiceContext.mVerb = JobServiceContext.VERB_EXECUTING; mJobServiceContext.handleOpTimeoutLocked(); String stopMessage = captor.getValue(); assertEquals("timeout while executing", stopMessage); verify(mMockJobParameters) .setStopReason( JobParameters.STOP_REASON_TIMEOUT, JobParameters.INTERNAL_STOP_REASON_TIMEOUT, "client timed out"); } /** * Test that abandoned jobs that are timed out while the flag is disabled * are stopped with the correct stop reason */ @Test @DisableFlags(FLAG_HANDLE_ABANDONED_JOBS) public void testJobServiceContext_TimeoutAbandonedJob_flagHandleAbandonedJobsDisabled() { mJobServiceContext.mVerb = JobServiceContext.VERB_EXECUTING; ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class); doNothing().when(mJobServiceContext).sendStopMessageLocked(captor.capture()); advanceElapsedClock(30 * MINUTE_IN_MILLIS); // 30 minutes mJobServiceContext.setPendingStopReasonLockedForTest(JobParameters.STOP_REASON_UNDEFINED); mJobServiceContext.setRunningJobLockedForTest(mMockJobStatus); doReturn(true).when(mMockJobStatus).isAbandoned(); mJobServiceContext.mVerb = JobServiceContext.VERB_EXECUTING; mJobServiceContext.handleOpTimeoutLocked(); String stopMessage = captor.getValue(); assertEquals("timeout while executing", stopMessage); verify(mMockJobParameters) .setStopReason( JobParameters.STOP_REASON_TIMEOUT, JobParameters.INTERNAL_STOP_REASON_TIMEOUT, "client timed out"); } /** * Test that the JobStatus is marked as abandoned when the JobServiceContext * receives a MSG_HANDLE_ABANDONED_JOB message */ @Test @EnableFlags(FLAG_HANDLE_ABANDONED_JOBS) public void testJobServiceContext_HandleAbandonedJob() { final int jobId = 123; mJobServiceContext.setRunningJobLockedForTest(mMockJobStatus); mJobServiceContext.setRunningCallbackLockedForTest(mMockJobCallback); doReturn(jobId).when(mMockJobStatus).getJobId(); mJobServiceContext.doHandleAbandonedJob(mMockJobCallback, jobId); verify(mMockJobStatus).setAbandoned(true); } /** * Test that the JobStatus is not marked as abandoned when the * JobServiceContext receives a MSG_HANDLE_ABANDONED_JOB message and the * JobServiceContext is not running a job */ @Test @EnableFlags(FLAG_HANDLE_ABANDONED_JOBS) public void testJobServiceContext_HandleAbandonedJob_notRunningJob() { final int jobId = 123; mJobServiceContext.setRunningJobLockedForTest(null); mJobServiceContext.setRunningCallbackLockedForTest(mMockJobCallback); mJobServiceContext.doHandleAbandonedJob(mMockJobCallback, jobId); verify(mMockJobStatus, never()).setAbandoned(true); } /** * Test that the JobStatus is not marked as abandoned when the * JobServiceContext receives a MSG_HANDLE_ABANDONED_JOB message and the * JobServiceContext is running a job with a different jobId */ @Test @EnableFlags(FLAG_HANDLE_ABANDONED_JOBS) public void testJobServiceContext_HandleAbandonedJob_differentJobId() { final int jobId = 123; final int differentJobId = 456; mJobServiceContext.setRunningJobLockedForTest(mMockJobStatus); mJobServiceContext.setRunningCallbackLockedForTest(mMockJobCallback); doReturn(differentJobId).when(mMockJobStatus).getJobId(); mJobServiceContext.doHandleAbandonedJob(mMockJobCallback, jobId); verify(mMockJobStatus, never()).setAbandoned(true); } } Loading
apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java +44 −5 Original line number Diff line number Diff line Loading @@ -618,6 +618,26 @@ public final class JobServiceContext implements ServiceConnection { return mRunningJob; } @VisibleForTesting void setRunningJobLockedForTest(JobStatus job) { mRunningJob = job; } @VisibleForTesting void setJobParamsLockedForTest(JobParameters params) { mParams = params; } @VisibleForTesting void setRunningCallbackLockedForTest(JobCallback callback) { mRunningCallback = callback; } @VisibleForTesting void setPendingStopReasonLockedForTest(int stopReason) { mPendingStopReason = stopReason; } @JobConcurrencyManager.WorkType int getRunningJobWorkType() { return mRunningJobWorkType; Loading Loading @@ -786,6 +806,7 @@ public final class JobServiceContext implements ServiceConnection { executing = getRunningJobLocked(); } if (executing != null && jobId == executing.getJobId()) { executing.setAbandoned(true); final StringBuilder stateSuffix = new StringBuilder(); stateSuffix.append(TRACE_ABANDONED_JOB); stateSuffix.append(executing.getBatteryName()); Loading Loading @@ -1364,8 +1385,9 @@ public final class JobServiceContext implements ServiceConnection { } /** Process MSG_TIMEOUT here. */ @VisibleForTesting @GuardedBy("mLock") private void handleOpTimeoutLocked() { void handleOpTimeoutLocked() { switch (mVerb) { case VERB_BINDING: // The system may have been too busy. Don't drop the job or trigger an ANR. Loading Loading @@ -1427,9 +1449,25 @@ public final class JobServiceContext implements ServiceConnection { // Not an error - client ran out of time. Slog.i(TAG, "Client timed out while executing (no jobFinished received)." + " Sending onStop: " + getRunningJobNameLocked()); mParams.setStopReason(JobParameters.STOP_REASON_TIMEOUT, JobParameters.INTERNAL_STOP_REASON_TIMEOUT, "client timed out"); sendStopMessageLocked("timeout while executing"); final JobStatus executing = getRunningJobLocked(); int stopReason = JobParameters.STOP_REASON_TIMEOUT; int internalStopReason = JobParameters.INTERNAL_STOP_REASON_TIMEOUT; final StringBuilder stopMessage = new StringBuilder("timeout while executing"); final StringBuilder debugStopReason = new StringBuilder("client timed out"); if (android.app.job.Flags.handleAbandonedJobs() && executing != null && executing.isAbandoned()) { final String abandonedMessage = " and maybe abandoned"; stopReason = JobParameters.STOP_REASON_TIMEOUT_ABANDONED; internalStopReason = JobParameters.INTERNAL_STOP_REASON_TIMEOUT_ABANDONED; stopMessage.append(abandonedMessage); debugStopReason.append(abandonedMessage); } mParams.setStopReason(stopReason, internalStopReason, debugStopReason.toString()); sendStopMessageLocked(stopMessage.toString()); } else if (nowElapsed >= earliestStopTimeElapsed) { // We've given the app the minimum execution time. See if we should stop it or // let it continue running Loading Loading @@ -1479,8 +1517,9 @@ public final class JobServiceContext implements ServiceConnection { * Already running, need to stop. Will switch {@link #mVerb} from VERB_EXECUTING -> * VERB_STOPPING. */ @VisibleForTesting @GuardedBy("mLock") private void sendStopMessageLocked(@Nullable String reason) { void sendStopMessageLocked(@Nullable String reason) { removeOpTimeOutLocked(); if (mVerb != VERB_EXECUTING) { Slog.e(TAG, "Sending onStopJob for a job that isn't started. " + mRunningJob); Loading
apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java +19 −0 Original line number Diff line number Diff line Loading @@ -575,6 +575,13 @@ public final class JobStatus { /** The system trace tag for this job. */ private String mSystemTraceTag; /** * Job maybe abandoned by not calling * {@link android.app.job.JobService#jobFinished(JobParameters, boolean)} while * the strong reference to {@link android.app.job.JobParameters} is lost */ private boolean mIsAbandoned; /** * Core constructor for JobStatus instances. All other ctors funnel down to this one. * Loading Loading @@ -725,6 +732,8 @@ public final class JobStatus { updateNetworkBytesLocked(); updateMediaBackupExemptionStatus(); mIsAbandoned = false; } /** Copy constructor: used specifically when cloning JobStatus objects for persistence, Loading Loading @@ -1061,6 +1070,16 @@ public final class JobStatus { return job.getTraceTag(); } /** Returns if the job maybe abandoned */ public boolean isAbandoned() { return mIsAbandoned; } /** Set the job maybe abandoned state*/ public void setAbandoned(boolean abandoned) { mIsAbandoned = abandoned; } /** Returns a trace tag using debug information provided by job scheduler service. */ @NonNull public String computeSystemTraceTag() { Loading
services/tests/mockingservicestests/src/com/android/server/job/JobServiceContextTest.java 0 → 100644 +270 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.server.job; import static android.app.job.Flags.FLAG_HANDLE_ABANDONED_JOBS; import static android.text.format.DateUtils.MINUTE_IN_MILLIS; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.app.AppGlobals; import android.app.job.JobParameters; import android.content.Context; import android.os.Looper; import android.os.PowerManager; import android.os.SystemClock; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import com.android.internal.app.IBatteryStats; import com.android.server.job.JobServiceContext.JobCallback; import com.android.server.job.controllers.JobStatus; import org.junit.After; import org.junit.Before; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoSession; import org.mockito.quality.Strictness; import java.time.Clock; import java.time.Duration; import java.time.ZoneOffset; public class JobServiceContextTest { private static final String TAG = JobServiceContextTest.class.getSimpleName(); @ClassRule public static final SetFlagsRule.ClassRule mSetFlagsClassRule = new SetFlagsRule.ClassRule(); @Rule public final SetFlagsRule mSetFlagsRule = mSetFlagsClassRule.createSetFlagsRule(); @Mock private JobSchedulerService mMockJobSchedulerService; @Mock private JobConcurrencyManager mMockConcurrencyManager; @Mock private JobNotificationCoordinator mMockNotificationCoordinator; @Mock private IBatteryStats.Stub mMockBatteryStats; @Mock private JobPackageTracker mMockJobPackageTracker; @Mock private Looper mMockLooper; @Mock private Context mMockContext; @Mock private JobStatus mMockJobStatus; @Mock private JobParameters mMockJobParameters; @Mock private JobCallback mMockJobCallback; private MockitoSession mMockingSession; private JobServiceContext mJobServiceContext; private Object mLock; @Before public void setUp() throws Exception { mMockingSession = mockitoSession() .initMocks(this) .mockStatic(AppGlobals.class) .strictness(Strictness.LENIENT) .startMocking(); JobSchedulerService.sElapsedRealtimeClock = Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC); doReturn(mock(PowerManager.class)).when(mMockContext).getSystemService(PowerManager.class); doReturn(mMockContext).when(mMockJobSchedulerService).getContext(); mLock = new Object(); doReturn(mLock).when(mMockJobSchedulerService).getLock(); mJobServiceContext = new JobServiceContext( mMockJobSchedulerService, mMockConcurrencyManager, mMockNotificationCoordinator, mMockBatteryStats, mMockJobPackageTracker, mMockLooper); spyOn(mJobServiceContext); mJobServiceContext.setJobParamsLockedForTest(mMockJobParameters); } @After public void tearDown() throws Exception { if (mMockingSession != null) { mMockingSession.finishMocking(); } } private Clock getAdvancedClock(Clock clock, long incrementMs) { return Clock.offset(clock, Duration.ofMillis(incrementMs)); } private void advanceElapsedClock(long incrementMs) { JobSchedulerService.sElapsedRealtimeClock = getAdvancedClock(JobSchedulerService.sElapsedRealtimeClock, incrementMs); } /** * Test that Abandoned jobs that are timed out are stopped with the correct stop reason */ @Test @EnableFlags(FLAG_HANDLE_ABANDONED_JOBS) public void testJobServiceContext_TimeoutAbandonedJob() { mJobServiceContext.mVerb = JobServiceContext.VERB_EXECUTING; ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class); doNothing().when(mJobServiceContext).sendStopMessageLocked(captor.capture()); advanceElapsedClock(30 * MINUTE_IN_MILLIS); // 30 minutes mJobServiceContext.setPendingStopReasonLockedForTest(JobParameters.STOP_REASON_UNDEFINED); mJobServiceContext.setRunningJobLockedForTest(mMockJobStatus); doReturn(true).when(mMockJobStatus).isAbandoned(); mJobServiceContext.mVerb = JobServiceContext.VERB_EXECUTING; mJobServiceContext.handleOpTimeoutLocked(); String stopMessage = captor.getValue(); assertEquals("timeout while executing and maybe abandoned", stopMessage); verify(mMockJobParameters) .setStopReason( JobParameters.STOP_REASON_TIMEOUT_ABANDONED, JobParameters.INTERNAL_STOP_REASON_TIMEOUT_ABANDONED, "client timed out and maybe abandoned"); } /** * Test that non-abandoned jobs that are timed out are stopped with the correct stop reason */ @Test @EnableFlags(FLAG_HANDLE_ABANDONED_JOBS) public void testJobServiceContext_TimeoutNoAbandonedJob() { mJobServiceContext.mVerb = JobServiceContext.VERB_EXECUTING; ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class); doNothing().when(mJobServiceContext).sendStopMessageLocked(captor.capture()); advanceElapsedClock(30 * MINUTE_IN_MILLIS); // 30 minutes mJobServiceContext.setPendingStopReasonLockedForTest(JobParameters.STOP_REASON_UNDEFINED); mJobServiceContext.setRunningJobLockedForTest(mMockJobStatus); doReturn(false).when(mMockJobStatus).isAbandoned(); mJobServiceContext.mVerb = JobServiceContext.VERB_EXECUTING; mJobServiceContext.handleOpTimeoutLocked(); String stopMessage = captor.getValue(); assertEquals("timeout while executing", stopMessage); verify(mMockJobParameters) .setStopReason( JobParameters.STOP_REASON_TIMEOUT, JobParameters.INTERNAL_STOP_REASON_TIMEOUT, "client timed out"); } /** * Test that abandoned jobs that are timed out while the flag is disabled * are stopped with the correct stop reason */ @Test @DisableFlags(FLAG_HANDLE_ABANDONED_JOBS) public void testJobServiceContext_TimeoutAbandonedJob_flagHandleAbandonedJobsDisabled() { mJobServiceContext.mVerb = JobServiceContext.VERB_EXECUTING; ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class); doNothing().when(mJobServiceContext).sendStopMessageLocked(captor.capture()); advanceElapsedClock(30 * MINUTE_IN_MILLIS); // 30 minutes mJobServiceContext.setPendingStopReasonLockedForTest(JobParameters.STOP_REASON_UNDEFINED); mJobServiceContext.setRunningJobLockedForTest(mMockJobStatus); doReturn(true).when(mMockJobStatus).isAbandoned(); mJobServiceContext.mVerb = JobServiceContext.VERB_EXECUTING; mJobServiceContext.handleOpTimeoutLocked(); String stopMessage = captor.getValue(); assertEquals("timeout while executing", stopMessage); verify(mMockJobParameters) .setStopReason( JobParameters.STOP_REASON_TIMEOUT, JobParameters.INTERNAL_STOP_REASON_TIMEOUT, "client timed out"); } /** * Test that the JobStatus is marked as abandoned when the JobServiceContext * receives a MSG_HANDLE_ABANDONED_JOB message */ @Test @EnableFlags(FLAG_HANDLE_ABANDONED_JOBS) public void testJobServiceContext_HandleAbandonedJob() { final int jobId = 123; mJobServiceContext.setRunningJobLockedForTest(mMockJobStatus); mJobServiceContext.setRunningCallbackLockedForTest(mMockJobCallback); doReturn(jobId).when(mMockJobStatus).getJobId(); mJobServiceContext.doHandleAbandonedJob(mMockJobCallback, jobId); verify(mMockJobStatus).setAbandoned(true); } /** * Test that the JobStatus is not marked as abandoned when the * JobServiceContext receives a MSG_HANDLE_ABANDONED_JOB message and the * JobServiceContext is not running a job */ @Test @EnableFlags(FLAG_HANDLE_ABANDONED_JOBS) public void testJobServiceContext_HandleAbandonedJob_notRunningJob() { final int jobId = 123; mJobServiceContext.setRunningJobLockedForTest(null); mJobServiceContext.setRunningCallbackLockedForTest(mMockJobCallback); mJobServiceContext.doHandleAbandonedJob(mMockJobCallback, jobId); verify(mMockJobStatus, never()).setAbandoned(true); } /** * Test that the JobStatus is not marked as abandoned when the * JobServiceContext receives a MSG_HANDLE_ABANDONED_JOB message and the * JobServiceContext is running a job with a different jobId */ @Test @EnableFlags(FLAG_HANDLE_ABANDONED_JOBS) public void testJobServiceContext_HandleAbandonedJob_differentJobId() { final int jobId = 123; final int differentJobId = 456; mJobServiceContext.setRunningJobLockedForTest(mMockJobStatus); mJobServiceContext.setRunningCallbackLockedForTest(mMockJobCallback); doReturn(differentJobId).when(mMockJobStatus).getJobId(); mJobServiceContext.doHandleAbandonedJob(mMockJobCallback, jobId); verify(mMockJobStatus, never()).setAbandoned(true); } }