Merge "Make concurrency manager code more testable."
This commit is contained in:
@@ -34,6 +34,7 @@ import android.content.IntentFilter;
|
||||
import android.content.pm.UserInfo;
|
||||
import android.os.BatteryStats;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.PowerManager;
|
||||
import android.os.RemoteException;
|
||||
import android.os.ServiceManager;
|
||||
@@ -181,6 +182,7 @@ class JobConcurrencyManager {
|
||||
private final JobSchedulerService mService;
|
||||
private final Context mContext;
|
||||
private final Handler mHandler;
|
||||
private final Injector mInjector;
|
||||
|
||||
private PowerManager mPowerManager;
|
||||
|
||||
@@ -378,9 +380,15 @@ class JobConcurrencyManager {
|
||||
}
|
||||
|
||||
JobConcurrencyManager(JobSchedulerService service) {
|
||||
this(service, new Injector());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
JobConcurrencyManager(JobSchedulerService service, Injector injector) {
|
||||
mService = service;
|
||||
mLock = mService.mLock;
|
||||
mContext = service.getTestableContext();
|
||||
mInjector = injector;
|
||||
|
||||
mHandler = JobSchedulerBackgroundThread.getHandler();
|
||||
|
||||
@@ -414,7 +422,7 @@ class JobConcurrencyManager {
|
||||
ServiceManager.getService(BatteryStats.SERVICE_NAME));
|
||||
for (int i = 0; i < STANDARD_CONCURRENCY_LIMIT; i++) {
|
||||
mIdleContexts.add(
|
||||
new JobServiceContext(mService, this, batteryStats,
|
||||
mInjector.createJobServiceContext(mService, this, batteryStats,
|
||||
mService.mJobPackageTracker, mContext.getMainLooper()));
|
||||
}
|
||||
}
|
||||
@@ -657,15 +665,40 @@ class JobConcurrencyManager {
|
||||
return;
|
||||
}
|
||||
|
||||
prepareForAssignmentDeterminationLocked(
|
||||
mRecycledIdle, mRecycledPreferredUidOnly, mRecycledStoppable);
|
||||
|
||||
if (DEBUG) {
|
||||
Slog.d(TAG, printAssignments("running jobs initial",
|
||||
mRecycledStoppable, mRecycledPreferredUidOnly));
|
||||
}
|
||||
|
||||
determineAssignmentsLocked(
|
||||
mRecycledChanged, mRecycledIdle, mRecycledPreferredUidOnly, mRecycledStoppable);
|
||||
|
||||
if (DEBUG) {
|
||||
Slog.d(TAG, printAssignments("running jobs final",
|
||||
mRecycledStoppable, mRecycledPreferredUidOnly, mRecycledChanged));
|
||||
|
||||
Slog.d(TAG, "work count results: " + mWorkCountTracker);
|
||||
}
|
||||
|
||||
carryOutAssignmentChangesLocked(mRecycledChanged);
|
||||
|
||||
cleanUpAfterAssignmentChangesLocked(
|
||||
mRecycledChanged, mRecycledIdle, mRecycledPreferredUidOnly, mRecycledStoppable);
|
||||
|
||||
noteConcurrency();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@GuardedBy("mLock")
|
||||
void prepareForAssignmentDeterminationLocked(final ArraySet<ContextAssignment> idle,
|
||||
final List<ContextAssignment> preferredUidOnly,
|
||||
final List<ContextAssignment> stoppable) {
|
||||
final PendingJobQueue pendingJobQueue = mService.getPendingJobQueue();
|
||||
final List<JobServiceContext> activeServices = mActiveServices;
|
||||
|
||||
// To avoid GC churn, we recycle the arrays.
|
||||
final ArraySet<ContextAssignment> changed = mRecycledChanged;
|
||||
final ArraySet<ContextAssignment> idle = mRecycledIdle;
|
||||
final ArrayList<ContextAssignment> preferredUidOnly = mRecycledPreferredUidOnly;
|
||||
final ArrayList<ContextAssignment> stoppable = mRecycledStoppable;
|
||||
|
||||
updateCounterConfigLocked();
|
||||
// Reset everything since we'll re-evaluate the current state.
|
||||
mWorkCountTracker.resetCounts();
|
||||
@@ -719,15 +752,21 @@ class JobConcurrencyManager {
|
||||
assignment.context = jsc;
|
||||
idle.add(assignment);
|
||||
}
|
||||
if (DEBUG) {
|
||||
Slog.d(TAG, printAssignments("running jobs initial", stoppable, preferredUidOnly));
|
||||
}
|
||||
|
||||
mWorkCountTracker.onCountDone();
|
||||
}
|
||||
|
||||
JobStatus nextPending;
|
||||
@VisibleForTesting
|
||||
@GuardedBy("mLock")
|
||||
void determineAssignmentsLocked(final ArraySet<ContextAssignment> changed,
|
||||
final ArraySet<ContextAssignment> idle,
|
||||
final List<ContextAssignment> preferredUidOnly,
|
||||
final List<ContextAssignment> stoppable) {
|
||||
final PendingJobQueue pendingJobQueue = mService.getPendingJobQueue();
|
||||
final List<JobServiceContext> activeServices = mActiveServices;
|
||||
pendingJobQueue.resetIterator();
|
||||
int projectedRunningCount = numRunningJobs;
|
||||
JobStatus nextPending;
|
||||
int projectedRunningCount = activeServices.size();
|
||||
while ((nextPending = pendingJobQueue.next()) != null) {
|
||||
if (mRunningJobs.contains(nextPending)) {
|
||||
// Should never happen.
|
||||
@@ -895,13 +934,10 @@ class JobConcurrencyManager {
|
||||
packageStats);
|
||||
}
|
||||
}
|
||||
if (DEBUG) {
|
||||
Slog.d(TAG, printAssignments("running jobs final",
|
||||
stoppable, preferredUidOnly, changed));
|
||||
|
||||
Slog.d(TAG, "assignJobsToContexts: " + mWorkCountTracker.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private void carryOutAssignmentChangesLocked(final ArraySet<ContextAssignment> changed) {
|
||||
for (int c = changed.size() - 1; c >= 0; --c) {
|
||||
final ContextAssignment assignment = changed.valueAt(c);
|
||||
final JobStatus js = assignment.context.getRunningJobLocked();
|
||||
@@ -925,6 +961,13 @@ class JobConcurrencyManager {
|
||||
assignment.clear();
|
||||
mContextAssignmentPool.release(assignment);
|
||||
}
|
||||
}
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private void cleanUpAfterAssignmentChangesLocked(final ArraySet<ContextAssignment> changed,
|
||||
final ArraySet<ContextAssignment> idle,
|
||||
final List<ContextAssignment> preferredUidOnly,
|
||||
final List<ContextAssignment> stoppable) {
|
||||
for (int s = stoppable.size() - 1; s >= 0; --s) {
|
||||
final ContextAssignment assignment = stoppable.get(s);
|
||||
assignment.clear();
|
||||
@@ -947,7 +990,6 @@ class JobConcurrencyManager {
|
||||
preferredUidOnly.clear();
|
||||
mWorkCountTracker.resetStagingCount();
|
||||
mActivePkgStats.forEach(mPackageStatsStagingCountClearer);
|
||||
noteConcurrency();
|
||||
}
|
||||
|
||||
@GuardedBy("mLock")
|
||||
@@ -1496,7 +1538,7 @@ class JobConcurrencyManager {
|
||||
|
||||
@NonNull
|
||||
private JobServiceContext createNewJobServiceContext() {
|
||||
return new JobServiceContext(mService, this,
|
||||
return mInjector.createJobServiceContext(mService, this,
|
||||
IBatteryStats.Stub.asInterface(
|
||||
ServiceManager.getService(BatteryStats.SERVICE_NAME)),
|
||||
mService.mJobPackageTracker, mContext.getMainLooper());
|
||||
@@ -1777,6 +1819,10 @@ class JobConcurrencyManager {
|
||||
|
||||
@VisibleForTesting
|
||||
static class WorkTypeConfig {
|
||||
@VisibleForTesting
|
||||
static final String KEY_PREFIX_MAX = CONFIG_KEY_PREFIX_CONCURRENCY + "max_";
|
||||
@VisibleForTesting
|
||||
static final String KEY_PREFIX_MIN = CONFIG_KEY_PREFIX_CONCURRENCY + "min_";
|
||||
@VisibleForTesting
|
||||
static final String KEY_PREFIX_MAX_TOTAL = CONFIG_KEY_PREFIX_CONCURRENCY + "max_total_";
|
||||
private static final String KEY_PREFIX_MAX_TOP = CONFIG_KEY_PREFIX_CONCURRENCY + "max_top_";
|
||||
@@ -2329,7 +2375,8 @@ class JobConcurrencyManager {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ContextAssignment {
|
||||
@VisibleForTesting
|
||||
static final class ContextAssignment {
|
||||
public JobServiceContext context;
|
||||
public int preferredUid = JobServiceContext.NO_PREFERRED_UID;
|
||||
public int workType = WORK_TYPE_NONE;
|
||||
@@ -2378,4 +2425,15 @@ class JobConcurrencyManager {
|
||||
mActivePkgStats.add(userId, packageName, packageStats);
|
||||
return packageStats;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static class Injector {
|
||||
@NonNull
|
||||
JobServiceContext createJobServiceContext(JobSchedulerService service,
|
||||
JobConcurrencyManager concurrencyManager, IBatteryStats batteryStats,
|
||||
JobPackageTracker tracker, Looper looper) {
|
||||
return new JobServiceContext(service, concurrencyManager, batteryStats,
|
||||
tracker, looper);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,30 +16,48 @@
|
||||
|
||||
package com.android.server.job;
|
||||
|
||||
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
|
||||
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
|
||||
import static com.android.server.job.JobConcurrencyManager.KEY_PKG_CONCURRENCY_LIMIT_EJ;
|
||||
import static com.android.server.job.JobConcurrencyManager.KEY_PKG_CONCURRENCY_LIMIT_REGULAR;
|
||||
|
||||
import static junit.framework.Assert.assertEquals;
|
||||
import static junit.framework.Assert.assertFalse;
|
||||
import static junit.framework.Assert.assertTrue;
|
||||
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.anyLong;
|
||||
import static org.mockito.Mockito.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.annotation.Nullable;
|
||||
import android.app.ActivityManagerInternal;
|
||||
import android.app.AppGlobals;
|
||||
import android.app.job.JobInfo;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.pm.IPackageManager;
|
||||
import android.content.pm.UserInfo;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Looper;
|
||||
import android.os.UserHandle;
|
||||
import android.provider.DeviceConfig;
|
||||
import android.util.ArraySet;
|
||||
|
||||
import androidx.test.filters.SmallTest;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
|
||||
import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_BG;
|
||||
import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_BGUSER;
|
||||
import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_BGUSER_IMPORTANT;
|
||||
import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_EJ;
|
||||
import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_FGS;
|
||||
import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_NONE;
|
||||
import static com.android.server.job.JobConcurrencyManager.WORK_TYPE_TOP;
|
||||
|
||||
import com.android.internal.R;
|
||||
import com.android.internal.app.IBatteryStats;
|
||||
import com.android.server.LocalServices;
|
||||
import com.android.server.job.JobConcurrencyManager.GracePeriodObserver;
|
||||
import com.android.server.job.JobConcurrencyManager.WorkTypeConfig;
|
||||
@@ -52,6 +70,12 @@ import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoSession;
|
||||
import org.mockito.quality.Strictness;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@SmallTest
|
||||
@@ -64,10 +88,24 @@ public final class JobConcurrencyManagerTest {
|
||||
private int mDefaultUserId;
|
||||
private GracePeriodObserver mGracePeriodObserver;
|
||||
private Context mContext;
|
||||
private InjectorForTest mInjector;
|
||||
private MockitoSession mMockingSession;
|
||||
private Resources mResources;
|
||||
private PendingJobQueue mPendingJobQueue;
|
||||
private DeviceConfig.Properties.Builder mConfigBuilder;
|
||||
|
||||
@Mock
|
||||
private IPackageManager mIPackageManager;
|
||||
|
||||
static class InjectorForTest extends JobConcurrencyManager.Injector {
|
||||
@Override
|
||||
JobServiceContext createJobServiceContext(JobSchedulerService service,
|
||||
JobConcurrencyManager concurrencyManager, IBatteryStats batteryStats,
|
||||
JobPackageTracker tracker, Looper looper) {
|
||||
return mock(JobServiceContext.class);
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeClass
|
||||
public static void setUpOnce() {
|
||||
LocalServices.addService(UserManagerInternal.class, mock(UserManagerInternal.class));
|
||||
@@ -83,6 +121,11 @@ public final class JobConcurrencyManagerTest {
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mMockingSession = mockitoSession()
|
||||
.initMocks(this)
|
||||
.mockStatic(AppGlobals.class)
|
||||
.strictness(Strictness.LENIENT)
|
||||
.startMocking();
|
||||
final JobSchedulerService jobSchedulerService = mock(JobSchedulerService.class);
|
||||
mContext = mock(Context.class);
|
||||
mResources = mock(Resources.class);
|
||||
@@ -93,7 +136,9 @@ public final class JobConcurrencyManagerTest {
|
||||
mConfigBuilder = new DeviceConfig.Properties.Builder(DeviceConfig.NAMESPACE_JOB_SCHEDULER);
|
||||
mPendingJobQueue = new PendingJobQueue();
|
||||
doReturn(mPendingJobQueue).when(jobSchedulerService).getPendingJobQueue();
|
||||
mJobConcurrencyManager = new JobConcurrencyManager(jobSchedulerService);
|
||||
doReturn(mIPackageManager).when(AppGlobals::getPackageManager);
|
||||
mInjector = new InjectorForTest();
|
||||
mJobConcurrencyManager = new JobConcurrencyManager(jobSchedulerService, mInjector);
|
||||
mGracePeriodObserver = mock(GracePeriodObserver.class);
|
||||
mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
|
||||
mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
|
||||
@@ -106,6 +151,74 @@ public final class JobConcurrencyManagerTest {
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
resetConfig();
|
||||
if (mMockingSession != null) {
|
||||
mMockingSession.finishMocking();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPrepareForAssignmentDetermination_noJobs() {
|
||||
mPendingJobQueue.clear();
|
||||
|
||||
final ArraySet<JobConcurrencyManager.ContextAssignment> idle = new ArraySet<>();
|
||||
final List<JobConcurrencyManager.ContextAssignment> preferredUidOnly = new ArrayList<>();
|
||||
final List<JobConcurrencyManager.ContextAssignment> stoppable = new ArrayList<>();
|
||||
mJobConcurrencyManager
|
||||
.prepareForAssignmentDeterminationLocked(idle, preferredUidOnly, stoppable);
|
||||
|
||||
assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, idle.size());
|
||||
assertEquals(0, preferredUidOnly.size());
|
||||
assertEquals(0, stoppable.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPrepareForAssignmentDetermination_onlyPendingJobs() {
|
||||
final ArraySet<JobStatus> jobs = new ArraySet<>();
|
||||
for (int i = 0; i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT; ++i) {
|
||||
JobStatus job = createJob(mDefaultUserId * UserHandle.PER_USER_RANGE + i);
|
||||
mPendingJobQueue.add(job);
|
||||
jobs.add(job);
|
||||
}
|
||||
|
||||
final ArraySet<JobConcurrencyManager.ContextAssignment> idle = new ArraySet<>();
|
||||
final List<JobConcurrencyManager.ContextAssignment> preferredUidOnly = new ArrayList<>();
|
||||
final List<JobConcurrencyManager.ContextAssignment> stoppable = new ArrayList<>();
|
||||
mJobConcurrencyManager
|
||||
.prepareForAssignmentDeterminationLocked(idle, preferredUidOnly, stoppable);
|
||||
|
||||
assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, idle.size());
|
||||
assertEquals(0, preferredUidOnly.size());
|
||||
assertEquals(0, stoppable.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDetermineAssignments_allRegular() throws Exception {
|
||||
setConcurrencyConfig(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT,
|
||||
new TypeConfig(WORK_TYPE_BG, 0, JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT));
|
||||
final ArraySet<JobStatus> jobs = new ArraySet<>();
|
||||
for (int i = 0; i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT; ++i) {
|
||||
final int uid = mDefaultUserId * UserHandle.PER_USER_RANGE + i;
|
||||
final String sourcePkgName = "com.source.package." + UserHandle.getAppId(uid);
|
||||
setPackageUid(sourcePkgName, uid);
|
||||
final JobStatus job = createJob(uid, sourcePkgName);
|
||||
mPendingJobQueue.add(job);
|
||||
jobs.add(job);
|
||||
}
|
||||
|
||||
final ArraySet<JobConcurrencyManager.ContextAssignment> changed = new ArraySet<>();
|
||||
final ArraySet<JobConcurrencyManager.ContextAssignment> idle = new ArraySet<>();
|
||||
final List<JobConcurrencyManager.ContextAssignment> preferredUidOnly = new ArrayList<>();
|
||||
final List<JobConcurrencyManager.ContextAssignment> stoppable = new ArrayList<>();
|
||||
mJobConcurrencyManager
|
||||
.prepareForAssignmentDeterminationLocked(idle, preferredUidOnly, stoppable);
|
||||
mJobConcurrencyManager
|
||||
.determineAssignmentsLocked(changed, idle, preferredUidOnly, stoppable);
|
||||
|
||||
assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, changed.size());
|
||||
for (int i = changed.size() - 1; i >= 0; --i) {
|
||||
jobs.remove(changed.valueAt(i).newJob);
|
||||
}
|
||||
assertTrue("Some jobs weren't assigned", jobs.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -403,16 +516,58 @@ public final class JobConcurrencyManagerTest {
|
||||
}
|
||||
|
||||
private static JobStatus createJob(int uid) {
|
||||
return createJob(uid, 1);
|
||||
return createJob(uid, 1, null);
|
||||
}
|
||||
|
||||
private static JobStatus createJob(int uid, String sourcePackageName) {
|
||||
return createJob(uid, 1, sourcePackageName);
|
||||
}
|
||||
|
||||
private static JobStatus createJob(int uid, int jobId) {
|
||||
return JobStatus.createFromJobInfo(
|
||||
new JobInfo.Builder(jobId, new ComponentName("foo", "bar")).build(), uid,
|
||||
null, UserHandle.getUserId(uid), "JobConcurrencyManagerTest");
|
||||
return createJob(uid, jobId, null);
|
||||
}
|
||||
|
||||
private void setConcurrencyConfig(int total) throws Exception {
|
||||
private static JobStatus createJob(int uid, int jobId, @Nullable String sourcePackageName) {
|
||||
return JobStatus.createFromJobInfo(
|
||||
new JobInfo.Builder(jobId, new ComponentName("foo", "bar")).build(), uid,
|
||||
sourcePackageName, UserHandle.getUserId(uid), "JobConcurrencyManagerTest");
|
||||
}
|
||||
|
||||
private static final class TypeConfig {
|
||||
public final String workTypeString;
|
||||
public final int min;
|
||||
public final int max;
|
||||
|
||||
private TypeConfig(@JobConcurrencyManager.WorkType int workType, int min, int max) {
|
||||
switch (workType) {
|
||||
case WORK_TYPE_TOP:
|
||||
workTypeString = "top";
|
||||
break;
|
||||
case WORK_TYPE_FGS:
|
||||
workTypeString = "fgs";
|
||||
break;
|
||||
case WORK_TYPE_EJ:
|
||||
workTypeString = "ej";
|
||||
break;
|
||||
case WORK_TYPE_BG:
|
||||
workTypeString = "bg";
|
||||
break;
|
||||
case WORK_TYPE_BGUSER:
|
||||
workTypeString = "bguser";
|
||||
break;
|
||||
case WORK_TYPE_BGUSER_IMPORTANT:
|
||||
workTypeString = "bguser_important";
|
||||
break;
|
||||
case WORK_TYPE_NONE:
|
||||
default:
|
||||
throw new IllegalArgumentException("invalid work type: " + workType);
|
||||
}
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
}
|
||||
}
|
||||
|
||||
private void setConcurrencyConfig(int total, TypeConfig... typeConfigs) throws Exception {
|
||||
// Set the values for all memory states so we don't have to worry about memory on the device
|
||||
// during testing.
|
||||
final String[] identifiers = {
|
||||
@@ -422,10 +577,23 @@ public final class JobConcurrencyManagerTest {
|
||||
for (String identifier : identifiers) {
|
||||
mConfigBuilder
|
||||
.setInt(WorkTypeConfig.KEY_PREFIX_MAX_TOTAL + identifier, total);
|
||||
for (TypeConfig config : typeConfigs) {
|
||||
mConfigBuilder.setInt(
|
||||
WorkTypeConfig.KEY_PREFIX_MAX + config.workTypeString + "_" + identifier,
|
||||
config.max);
|
||||
mConfigBuilder.setInt(
|
||||
WorkTypeConfig.KEY_PREFIX_MIN + config.workTypeString + "_" + identifier,
|
||||
config.min);
|
||||
}
|
||||
}
|
||||
updateDeviceConfig();
|
||||
}
|
||||
|
||||
private void setPackageUid(final String pkgName, final int uid) throws Exception {
|
||||
doReturn(uid).when(mIPackageManager)
|
||||
.getPackageUid(eq(pkgName), anyLong(), eq(UserHandle.getUserId(uid)));
|
||||
}
|
||||
|
||||
private void updateDeviceConfig() throws Exception {
|
||||
DeviceConfig.setProperties(mConfigBuilder.build());
|
||||
mJobConcurrencyManager.updateConfigLocked();
|
||||
|
||||
Reference in New Issue
Block a user