diff options
| -rw-r--r-- | apex/jobscheduler/service/java/com/android/server/job/JobStore.java | 60 | ||||
| -rw-r--r-- | services/tests/servicestests/src/com/android/server/job/JobStoreTest.java | 194 |
2 files changed, 233 insertions, 21 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java index 145ac52ccc8a..a1153e315954 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java @@ -91,8 +91,11 @@ public final class JobStore { /** Threshold to adjust how often we want to write to the db. */ private static final long JOB_PERSIST_DELAY = 2000L; - private static final String JOB_FILE_SPLIT_PREFIX = "jobs_"; + @VisibleForTesting + static final String JOB_FILE_SPLIT_PREFIX = "jobs_"; private static final int ALL_UIDS = -1; + @VisibleForTesting + static final int INVALID_UID = -2; final Object mLock; final Object mWriteScheduleLock; // used solely for invariants around write scheduling @@ -529,6 +532,25 @@ public final class JobStore { return values; } + @VisibleForTesting + static int extractUidFromJobFileName(@NonNull File file) { + final String fileName = file.getName(); + if (fileName.startsWith(JOB_FILE_SPLIT_PREFIX)) { + try { + final int subEnd = fileName.length() - 4; // -4 for ".xml" + final int uid = Integer.parseInt( + fileName.substring(JOB_FILE_SPLIT_PREFIX.length(), subEnd)); + if (uid < 0) { + return INVALID_UID; + } + return uid; + } catch (Exception e) { + Slog.e(TAG, "Unexpected file name format", e); + } + } + return INVALID_UID; + } + /** * Runnable that writes {@link #mJobSet} out to xml. * NOTE: This Runnable locks on mLock @@ -543,6 +565,42 @@ public final class JobStore { private void prepare() { mCopyAllJobs = !mUseSplitFiles || mPendingJobWriteUids.get(ALL_UIDS); + if (mUseSplitFiles) { + // Put the set of changed UIDs in the copy list so that we update each file, + // especially if we've dropped all jobs for that UID. + if (mPendingJobWriteUids.get(ALL_UIDS)) { + // ALL_UIDS is only used when we switch file splitting policy or for tests, + // so going through the file list here shouldn't be + // a large performance hit on user devices. + + final File[] files; + try { + files = mJobFileDirectory.listFiles(); + } catch (SecurityException e) { + Slog.wtf(TAG, "Not allowed to read job file directory", e); + return; + } + if (files == null) { + Slog.wtfStack(TAG, "Couldn't get job file list"); + } else { + for (File file : files) { + final int uid = extractUidFromJobFileName(file); + if (uid != INVALID_UID) { + mJobStoreCopy.put(uid, new ArrayList<>()); + } + } + } + } else { + for (int i = 0; i < mPendingJobWriteUids.size(); ++i) { + mJobStoreCopy.put(mPendingJobWriteUids.keyAt(i), new ArrayList<>()); + } + } + } else { + // Single file mode. + // Put the catchall UID in the copy list so that we update the single file, + // especially if we've dropped all persisted jobs. + mJobStoreCopy.put(ALL_UIDS, new ArrayList<>()); + } } @Override diff --git a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java index dc47b5eaea0e..0589b3a91225 100644 --- a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java +++ b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java @@ -8,6 +8,7 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -22,6 +23,7 @@ import android.os.Build; import android.os.PersistableBundle; import android.os.SystemClock; import android.test.RenamingDelegatingContext; +import android.util.ArraySet; import android.util.Log; import android.util.Pair; @@ -38,9 +40,9 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.io.File; import java.time.Clock; import java.time.ZoneOffset; -import java.util.Iterator; /** * Test reading and writing correctly from file. @@ -93,11 +95,147 @@ public class JobStoreTest { mTaskStoreUnderTest.waitForWriteToCompleteForTesting(5_000L); } + private void setUseSplitFiles(boolean useSplitFiles) throws Exception { + mTaskStoreUnderTest.setUseSplitFiles(useSplitFiles); + waitForPendingIo(); + } + private void waitForPendingIo() throws Exception { assertTrue("Timed out waiting for persistence I/O to complete", mTaskStoreUnderTest.waitForWriteToCompleteForTesting(5_000L)); } + /** Test that we properly remove the last job of an app from the persisted file. */ + @Test + public void testRemovingLastJob_singleFile() throws Exception { + setUseSplitFiles(false); + runRemovingLastJob(); + } + + /** Test that we properly remove the last job of an app from the persisted file. */ + @Test + public void testRemovingLastJob_splitFiles() throws Exception { + setUseSplitFiles(true); + runRemovingLastJob(); + } + + private void runRemovingLastJob() throws Exception { + final JobInfo task1 = new Builder(8, mComponent) + .setRequiresDeviceIdle(true) + .setPeriodic(10000L) + .setRequiresCharging(true) + .setPersisted(true) + .build(); + final JobInfo task2 = new Builder(12, mComponent) + .setMinimumLatency(5000L) + .setBackoffCriteria(15000L, JobInfo.BACKOFF_POLICY_LINEAR) + .setOverrideDeadline(30000L) + .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) + .setPersisted(true) + .build(); + final int uid1 = SOME_UID; + final int uid2 = uid1 + 1; + final JobStatus JobStatus1 = JobStatus.createFromJobInfo(task1, uid1, null, -1, null); + final JobStatus JobStatus2 = JobStatus.createFromJobInfo(task2, uid2, null, -1, null); + runWritingJobsToDisk(JobStatus1, JobStatus2); + + // Remove 1 job + mTaskStoreUnderTest.remove(JobStatus1, true); + waitForPendingIo(); + JobSet jobStatusSet = new JobSet(); + mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true); + assertEquals("Incorrect # of persisted tasks.", 1, jobStatusSet.size()); + JobStatus loaded = jobStatusSet.getAllJobs().iterator().next(); + + assertJobsEqual(JobStatus2, loaded); + assertTrue("JobStore#contains invalid.", mTaskStoreUnderTest.containsJob(JobStatus2)); + + // Remove 2nd job + mTaskStoreUnderTest.remove(JobStatus2, true); + waitForPendingIo(); + jobStatusSet = new JobSet(); + mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true); + assertEquals("Incorrect # of persisted tasks.", 0, jobStatusSet.size()); + } + + /** Test that we properly clear the persisted file when all jobs are dropped. */ + @Test + public void testClearJobs_singleFile() throws Exception { + setUseSplitFiles(false); + runClearJobs(); + } + + /** Test that we properly clear the persisted file when all jobs are dropped. */ + @Test + public void testClearJobs_splitFiles() throws Exception { + setUseSplitFiles(true); + runClearJobs(); + } + + private void runClearJobs() throws Exception { + final JobInfo task1 = new Builder(8, mComponent) + .setRequiresDeviceIdle(true) + .setPeriodic(10000L) + .setRequiresCharging(true) + .setPersisted(true) + .build(); + final JobInfo task2 = new Builder(12, mComponent) + .setMinimumLatency(5000L) + .setBackoffCriteria(15000L, JobInfo.BACKOFF_POLICY_LINEAR) + .setOverrideDeadline(30000L) + .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) + .setPersisted(true) + .build(); + final int uid1 = SOME_UID; + final int uid2 = uid1 + 1; + final JobStatus JobStatus1 = JobStatus.createFromJobInfo(task1, uid1, null, -1, null); + final JobStatus JobStatus2 = JobStatus.createFromJobInfo(task2, uid2, null, -1, null); + runWritingJobsToDisk(JobStatus1, JobStatus2); + + // Remove all jobs + mTaskStoreUnderTest.clear(); + waitForPendingIo(); + JobSet jobStatusSet = new JobSet(); + mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true); + assertEquals("Incorrect # of persisted tasks.", 0, jobStatusSet.size()); + } + + @Test + public void testExtractUidFromJobFileName() { + File file = new File(mTestContext.getFilesDir(), "randomName"); + assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file)); + + file = new File(mTestContext.getFilesDir(), "jobs.xml"); + assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file)); + + file = new File(mTestContext.getFilesDir(), ".xml"); + assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file)); + + file = new File(mTestContext.getFilesDir(), "1000.xml"); + assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file)); + + file = new File(mTestContext.getFilesDir(), "10000"); + assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file)); + + file = new File(mTestContext.getFilesDir(), JobStore.JOB_FILE_SPLIT_PREFIX); + assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file)); + + file = new File(mTestContext.getFilesDir(), JobStore.JOB_FILE_SPLIT_PREFIX + "text.xml"); + assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file)); + + file = new File(mTestContext.getFilesDir(), JobStore.JOB_FILE_SPLIT_PREFIX + ".xml"); + assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file)); + + file = new File(mTestContext.getFilesDir(), JobStore.JOB_FILE_SPLIT_PREFIX + "-10123.xml"); + assertEquals(JobStore.INVALID_UID, JobStore.extractUidFromJobFileName(file)); + + file = new File(mTestContext.getFilesDir(), JobStore.JOB_FILE_SPLIT_PREFIX + "1.xml"); + assertEquals(1, JobStore.extractUidFromJobFileName(file)); + + file = new File(mTestContext.getFilesDir(), JobStore.JOB_FILE_SPLIT_PREFIX + "101023.xml"); + assertEquals(101023, JobStore.extractUidFromJobFileName(file)); + } + @Test public void testStringToIntArrayAndIntArrayToString() { final int[] netCapabilitiesIntArray = { 1, 3, 5, 7, 9 }; @@ -144,13 +282,13 @@ public class JobStoreTest { @Test public void testWritingTwoJobsToDisk_singleFile() throws Exception { - mTaskStoreUnderTest.setUseSplitFiles(false); + setUseSplitFiles(false); runWritingTwoJobsToDisk(); } @Test public void testWritingTwoJobsToDisk_splitFiles() throws Exception { - mTaskStoreUnderTest.setUseSplitFiles(true); + setUseSplitFiles(true); runWritingTwoJobsToDisk(); } @@ -172,28 +310,44 @@ public class JobStoreTest { final int uid2 = uid1 + 1; final JobStatus taskStatus1 = JobStatus.createFromJobInfo(task1, uid1, null, -1, null); final JobStatus taskStatus2 = JobStatus.createFromJobInfo(task2, uid2, null, -1, null); - mTaskStoreUnderTest.add(taskStatus1); - mTaskStoreUnderTest.add(taskStatus2); + + runWritingJobsToDisk(taskStatus1, taskStatus2); + } + + private void runWritingJobsToDisk(JobStatus... jobStatuses) throws Exception { + ArraySet<JobStatus> expectedJobs = new ArraySet<>(); + for (JobStatus jobStatus : jobStatuses) { + mTaskStoreUnderTest.add(jobStatus); + expectedJobs.add(jobStatus); + } waitForPendingIo(); final JobSet jobStatusSet = new JobSet(); mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true); - assertEquals("Incorrect # of persisted tasks.", 2, jobStatusSet.size()); - Iterator<JobStatus> it = jobStatusSet.getAllJobs().iterator(); - JobStatus loaded1 = it.next(); - JobStatus loaded2 = it.next(); - - // Reverse them so we know which comparison to make. - if (loaded1.getJobId() != 8) { - JobStatus tmp = loaded1; - loaded1 = loaded2; - loaded2 = tmp; + assertEquals("Incorrect # of persisted tasks.", expectedJobs.size(), jobStatusSet.size()); + int count = 0; + final int expectedCount = expectedJobs.size(); + for (JobStatus loaded : jobStatusSet.getAllJobs()) { + count++; + for (int i = 0; i < expectedJobs.size(); ++i) { + JobStatus expected = expectedJobs.valueAt(i); + + try { + assertJobsEqual(expected, loaded); + expectedJobs.remove(expected); + break; + } catch (AssertionError e) { + // Not equal. Move along. + } + } + } + assertEquals("Loaded more jobs than expected", expectedCount, count); + if (expectedJobs.size() > 0) { + fail("Not all expected jobs were restored"); + } + for (JobStatus jobStatus : jobStatuses) { + assertTrue("JobStore#contains invalid.", mTaskStoreUnderTest.containsJob(jobStatus)); } - - assertJobsEqual(taskStatus1, loaded1); - assertJobsEqual(taskStatus2, loaded2); - assertTrue("JobStore#contains invalid.", mTaskStoreUnderTest.containsJob(taskStatus1)); - assertTrue("JobStore#contains invalid.", mTaskStoreUnderTest.containsJob(taskStatus2)); } @Test |