diff options
| author | 2023-11-06 22:25:49 +0000 | |
|---|---|---|
| committer | 2023-11-06 22:43:30 +0000 | |
| commit | 366583d33138d8565e8fa774b3a8dbc05418de9a (patch) | |
| tree | de8b5d9421f40a71302e8775ab5e9fb52e4d0adf | |
| parent | 896c5c42c7a2dd9271957872fc1b29d96c1884ea (diff) | |
Add transport affinities for flex scheduling.
Switch the flex network behavior from unmetered vs metered to looking at
network transports. Transport affinities indicate which transports are
preferred for running jobs, and which ones the system should try to
avoid, if possible. By default, when flex scheduling is enabled, the
system will avoid running jobs on the cellular network and prefer
running jobs on wifi and/or ethernet networks.
Watches tend to send most traffic over the bluetooth network. However,
there are plans to modify job network traffic in other ways for watches.
For now, watches will be excluded from network flex scheduling until
those plans are resolved.
Bug: 236261941
Bug: 299329948
Bug: 299346198
Test: atest CtsJobSchedulerTestCases:ConnectivityConstraintTest
Test: atest CtsJobSchedulerTestCases:FlexibilityConstraintTest
Test: atest frameworks/base/services/tests/mockingservicestests/src/com/android/server/job
Test: atest frameworks/base/services/tests/servicestests/src/com/android/server/job
Change-Id: I675d72b18ae6ddc43fb1503d481ff9bb56c2dcb6
6 files changed, 463 insertions, 71 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java index bff43534ce05..07475b4f2136 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -998,6 +998,7 @@ public class JobSchedulerService extends com.android.server.SystemService DEFAULT_SYSTEM_STOP_TO_FAILURE_RATIO); } + // TODO(141645789): move into ConnectivityController.CcConfig private void updateConnectivityConstantsLocked() { CONN_CONGESTION_DELAY_FRAC = DeviceConfig.getFloat(DeviceConfig.NAMESPACE_JOB_SCHEDULER, KEY_CONN_CONGESTION_DELAY_FRAC, diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java index ae4e99cfeef3..0cf0cc5dcd22 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java @@ -19,6 +19,9 @@ package com.android.server.job.controllers; import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED; import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX; import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; @@ -29,6 +32,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.job.JobInfo; +import android.content.pm.PackageManager; import android.net.ConnectivityManager; import android.net.ConnectivityManager.NetworkCallback; import android.net.INetworkPolicyListener; @@ -40,6 +44,7 @@ import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.UserHandle; +import android.provider.DeviceConfig; import android.telephony.CellSignalStrength; import android.telephony.SignalStrength; import android.telephony.TelephonyCallback; @@ -53,6 +58,7 @@ import android.util.Pools; import android.util.Slog; import android.util.SparseArray; import android.util.SparseBooleanArray; +import android.util.SparseIntArray; import android.util.TimeUtils; import android.util.proto.ProtoOutputStream; @@ -117,6 +123,26 @@ public final class ConnectivityController extends RestrictingController implemen | ConnectivityManager.BLOCKED_METERED_REASON_DATA_SAVER | ConnectivityManager.BLOCKED_METERED_REASON_USER_RESTRICTED); + @VisibleForTesting + static final int TRANSPORT_AFFINITY_UNDEFINED = 0; + @VisibleForTesting + static final int TRANSPORT_AFFINITY_PREFER = 1; + @VisibleForTesting + static final int TRANSPORT_AFFINITY_AVOID = 2; + /** + * Set of affinities to different network transports. If a given network has multiple + * transports, the avoided ones take priority --- a network with an avoided transport + * should be avoided if possible, even if the network has preferred transports as well. + */ + @VisibleForTesting + static final SparseIntArray sNetworkTransportAffinities = new SparseIntArray(); + static { + sNetworkTransportAffinities.put(TRANSPORT_CELLULAR, TRANSPORT_AFFINITY_AVOID); + sNetworkTransportAffinities.put(TRANSPORT_WIFI, TRANSPORT_AFFINITY_PREFER); + sNetworkTransportAffinities.put(TRANSPORT_ETHERNET, TRANSPORT_AFFINITY_PREFER); + } + + private final CcConfig mCcConfig; private final ConnectivityManager mConnManager; private final NetworkPolicyManager mNetPolicyManager; private final NetworkPolicyManagerInternal mNetPolicyManagerInternal; @@ -138,7 +164,7 @@ public final class ConnectivityController extends RestrictingController implemen * latest capabilities to avoid unnecessary calls into ConnectivityManager. */ @GuardedBy("mLock") - private final ArrayMap<Network, NetworkCapabilities> mAvailableNetworks = new ArrayMap<>(); + private final ArrayMap<Network, CachedNetworkMetadata> mAvailableNetworks = new ArrayMap<>(); private final SparseArray<UidDefaultNetworkCallback> mCurrentDefaultNetworkCallbacks = new SparseArray<>(); @@ -267,6 +293,7 @@ public final class ConnectivityController extends RestrictingController implemen @NonNull FlexibilityController flexibilityController) { super(service); mHandler = new CcHandler(AppSchedulingModuleThread.get().getLooper()); + mCcConfig = new CcConfig(); mConnManager = mContext.getSystemService(ConnectivityManager.class); mNetPolicyManager = mContext.getSystemService(NetworkPolicyManager.class); @@ -279,6 +306,11 @@ public final class ConnectivityController extends RestrictingController implemen mConnManager.registerNetworkCallback(request, mNetworkCallback); mNetPolicyManager.registerListener(mNetPolicyListener); + + if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)) { + // For now, we don't have network affinities on watches. + sNetworkTransportAffinities.clear(); + } } @GuardedBy("mLock") @@ -386,7 +418,9 @@ public final class ConnectivityController extends RestrictingController implemen synchronized (mLock) { for (int i = 0; i < mAvailableNetworks.size(); ++i) { final Network network = mAvailableNetworks.keyAt(i); - final NetworkCapabilities capabilities = mAvailableNetworks.valueAt(i); + final CachedNetworkMetadata metadata = mAvailableNetworks.valueAt(i); + final NetworkCapabilities capabilities = + metadata == null ? null : metadata.networkCapabilities; final boolean satisfied = isSatisfied(job, network, capabilities, mConstants); if (DEBUG) { Slog.v(TAG, "isNetworkAvailable(" + job + ") with network " + network @@ -589,6 +623,43 @@ public final class ConnectivityController extends RestrictingController implemen mHandler.sendEmptyMessage(MSG_UPDATE_ALL_TRACKED_JOBS); } + @Override + public void prepareForUpdatedConstantsLocked() { + mCcConfig.mShouldReprocessNetworkCapabilities = false; + mCcConfig.mFlexIsEnabled = mFlexibilityController.isEnabled(); + } + + @Override + public void processConstantLocked(@NonNull DeviceConfig.Properties properties, + @NonNull String key) { + mCcConfig.processConstantLocked(properties, key); + } + + @Override + public void onConstantsUpdatedLocked() { + if (mCcConfig.mShouldReprocessNetworkCapabilities + || (mFlexibilityController.isEnabled() != mCcConfig.mFlexIsEnabled)) { + AppSchedulingModuleThread.getHandler().post(() -> { + boolean shouldUpdateJobs = false; + for (int i = 0; i < mAvailableNetworks.size(); ++i) { + CachedNetworkMetadata metadata = mAvailableNetworks.valueAt(i); + if (metadata == null || metadata.networkCapabilities == null) { + continue; + } + boolean satisfies = satisfiesTransportAffinities(metadata.networkCapabilities); + if (metadata.satisfiesTransportAffinities != satisfies) { + metadata.satisfiesTransportAffinities = satisfies; + // Something changed. Update jobs. + shouldUpdateJobs = true; + } + } + if (shouldUpdateJobs) { + updateAllTrackedJobsLocked(false); + } + }); + } + } + private boolean isUsable(NetworkCapabilities capabilities) { return capabilities != null && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED); @@ -831,7 +902,7 @@ public final class ConnectivityController extends RestrictingController implemen if (!constants.CONN_USE_CELL_SIGNAL_STRENGTH) { return true; } - if (!capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { + if (!capabilities.hasTransport(TRANSPORT_CELLULAR)) { return true; } if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) { @@ -988,6 +1059,52 @@ public final class ConnectivityController extends RestrictingController implemen return false; } + private boolean satisfiesTransportAffinities(@Nullable NetworkCapabilities capabilities) { + if (!mFlexibilityController.isEnabled()) { + return true; + } + if (capabilities == null) { + Slog.wtf(TAG, "Network constraint satisfied with null capabilities"); + return !mCcConfig.AVOID_UNDEFINED_TRANSPORT_AFFINITY; + } + + if (sNetworkTransportAffinities.size() == 0) { + return !mCcConfig.AVOID_UNDEFINED_TRANSPORT_AFFINITY; + } + + final int[] transports = capabilities.getTransportTypes(); + if (transports.length == 0) { + return !mCcConfig.AVOID_UNDEFINED_TRANSPORT_AFFINITY; + } + + for (int t : transports) { + int affinity = sNetworkTransportAffinities.get(t, TRANSPORT_AFFINITY_UNDEFINED); + if (DEBUG) { + Slog.d(TAG, + "satisfiesTransportAffinities transport=" + t + " aff=" + affinity); + } + switch (affinity) { + case TRANSPORT_AFFINITY_UNDEFINED: + if (mCcConfig.AVOID_UNDEFINED_TRANSPORT_AFFINITY) { + // Avoided transports take precedence. + // Return as soon as we encounter a transport to avoid. + return false; + } + break; + case TRANSPORT_AFFINITY_PREFER: + // Nothing to do here. We like this transport. + break; + case TRANSPORT_AFFINITY_AVOID: + // Avoided transports take precedence. + // Return as soon as we encounter a transport to avoid. + return false; + } + } + + // Didn't see any transport to avoid. + return true; + } + @GuardedBy("mLock") private void maybeRegisterDefaultNetworkCallbackLocked(JobStatus jobStatus) { final int sourceUid = jobStatus.getSourceUid(); @@ -1172,6 +1289,12 @@ public final class ConnectivityController extends RestrictingController implemen @Nullable private NetworkCapabilities getNetworkCapabilities(@Nullable Network network) { + final CachedNetworkMetadata metadata = getNetworkMetadata(network); + return metadata == null ? null : metadata.networkCapabilities; + } + + @Nullable + private CachedNetworkMetadata getNetworkMetadata(@Nullable Network network) { if (network == null) { return null; } @@ -1234,14 +1357,16 @@ public final class ConnectivityController extends RestrictingController implemen return updateConstraintsSatisfied(jobStatus, nowElapsed, null, null); } final Network network = getNetworkLocked(jobStatus); - final NetworkCapabilities capabilities = getNetworkCapabilities(network); - return updateConstraintsSatisfied(jobStatus, nowElapsed, network, capabilities); + final CachedNetworkMetadata networkMetadata = getNetworkMetadata(network); + return updateConstraintsSatisfied(jobStatus, nowElapsed, network, networkMetadata); } private boolean updateConstraintsSatisfied(JobStatus jobStatus, final long nowElapsed, - Network network, NetworkCapabilities capabilities) { + Network network, @Nullable CachedNetworkMetadata networkMetadata) { // TODO: consider matching against non-default networks + final NetworkCapabilities capabilities = + networkMetadata == null ? null : networkMetadata.networkCapabilities; final boolean satisfied = isSatisfied(jobStatus, network, capabilities, mConstants); if (!satisfied && jobStatus.network != null @@ -1263,10 +1388,10 @@ public final class ConnectivityController extends RestrictingController implemen final boolean changed = jobStatus.setConnectivityConstraintSatisfied(nowElapsed, satisfied); - if (jobStatus.getPreferUnmetered()) { - jobStatus.setHasAccessToUnmetered(satisfied && capabilities != null - && capabilities.hasCapability(NET_CAPABILITY_NOT_METERED)); - + jobStatus.setTransportAffinitiesSatisfied(satisfied && networkMetadata != null + && networkMetadata.satisfiesTransportAffinities); + if (jobStatus.canApplyTransportAffinities()) { + // Only modify the flex constraint if the job actually needs it. jobStatus.setFlexibilityConstraintSatisfied(nowElapsed, mFlexibilityController.isFlexibilitySatisfiedLocked(jobStatus)); } @@ -1367,7 +1492,6 @@ public final class ConnectivityController extends RestrictingController implemen final JobStatus js = jobs.valueAt(i); final Network net = getNetworkLocked(js); - final NetworkCapabilities netCap = getNetworkCapabilities(net); final boolean match = (filterNetwork == null || Objects.equals(filterNetwork, net)); @@ -1375,7 +1499,7 @@ public final class ConnectivityController extends RestrictingController implemen // job hasn't yet been evaluated against the currently // active network; typically when we just lost a network. if (match || !Objects.equals(js.network, net)) { - changed |= updateConstraintsSatisfied(js, nowElapsed, net, netCap); + changed |= updateConstraintsSatisfied(js, nowElapsed, net, getNetworkMetadata(net)); } } return changed; @@ -1417,10 +1541,18 @@ public final class ConnectivityController extends RestrictingController implemen Slog.v(TAG, "onCapabilitiesChanged: " + network); } synchronized (mLock) { - final NetworkCapabilities oldCaps = mAvailableNetworks.put(network, capabilities); - if (oldCaps != null) { - maybeUnregisterSignalStrengthCallbackLocked(oldCaps); + CachedNetworkMetadata cnm = mAvailableNetworks.get(network); + if (cnm == null) { + cnm = new CachedNetworkMetadata(); + mAvailableNetworks.put(network, cnm); + } else { + final NetworkCapabilities oldCaps = cnm.networkCapabilities; + if (oldCaps != null) { + maybeUnregisterSignalStrengthCallbackLocked(oldCaps); + } } + cnm.networkCapabilities = capabilities; + cnm.satisfiesTransportAffinities = satisfiesTransportAffinities(capabilities); maybeRegisterSignalStrengthCallbackLocked(capabilities); updateTrackedJobsLocked(-1, network); postAdjustCallbacks(); @@ -1433,9 +1565,9 @@ public final class ConnectivityController extends RestrictingController implemen Slog.v(TAG, "onLost: " + network); } synchronized (mLock) { - final NetworkCapabilities capabilities = mAvailableNetworks.remove(network); - if (capabilities != null) { - maybeUnregisterSignalStrengthCallbackLocked(capabilities); + final CachedNetworkMetadata cnm = mAvailableNetworks.remove(network); + if (cnm != null && cnm.networkCapabilities != null) { + maybeUnregisterSignalStrengthCallbackLocked(cnm.networkCapabilities); } for (int u = 0; u < mCurrentDefaultNetworkCallbacks.size(); ++u) { UidDefaultNetworkCallback callback = mCurrentDefaultNetworkCallbacks.valueAt(u); @@ -1451,7 +1583,7 @@ public final class ConnectivityController extends RestrictingController implemen @GuardedBy("mLock") private void maybeRegisterSignalStrengthCallbackLocked( @NonNull NetworkCapabilities capabilities) { - if (!capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { + if (!capabilities.hasTransport(TRANSPORT_CELLULAR)) { return; } TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class); @@ -1476,14 +1608,17 @@ public final class ConnectivityController extends RestrictingController implemen @GuardedBy("mLock") private void maybeUnregisterSignalStrengthCallbackLocked( @NonNull NetworkCapabilities capabilities) { - if (!capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { + if (!capabilities.hasTransport(TRANSPORT_CELLULAR)) { return; } ArraySet<Integer> activeIds = new ArraySet<>(); for (int i = 0, size = mAvailableNetworks.size(); i < size; ++i) { - NetworkCapabilities nc = mAvailableNetworks.valueAt(i); - if (nc.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { - activeIds.addAll(nc.getSubscriptionIds()); + final CachedNetworkMetadata metadata = mAvailableNetworks.valueAt(i); + if (metadata == null || metadata.networkCapabilities == null) { + continue; + } + if (metadata.networkCapabilities.hasTransport(TRANSPORT_CELLULAR)) { + activeIds.addAll(metadata.networkCapabilities.getSubscriptionIds()); } } if (DEBUG) { @@ -1573,6 +1708,57 @@ public final class ConnectivityController extends RestrictingController implemen } } + @VisibleForTesting + class CcConfig { + private boolean mFlexIsEnabled = FlexibilityController.FcConfig.DEFAULT_FLEXIBILITY_ENABLED; + private boolean mShouldReprocessNetworkCapabilities = false; + + /** + * Prefix to use with all constant keys in order to "sub-namespace" the keys. + * "conn_" is used for legacy reasons. + */ + private static final String CC_CONFIG_PREFIX = "conn_"; + + @VisibleForTesting + static final String KEY_AVOID_UNDEFINED_TRANSPORT_AFFINITY = + CC_CONFIG_PREFIX + "avoid_undefined_transport_affinity"; + + private static final boolean DEFAULT_AVOID_UNDEFINED_TRANSPORT_AFFINITY = false; + + /** + * If true, will avoid network transports that don't have an explicitly defined affinity. + */ + public boolean AVOID_UNDEFINED_TRANSPORT_AFFINITY = + DEFAULT_AVOID_UNDEFINED_TRANSPORT_AFFINITY; + + @GuardedBy("mLock") + public void processConstantLocked(@NonNull DeviceConfig.Properties properties, + @NonNull String key) { + switch (key) { + case KEY_AVOID_UNDEFINED_TRANSPORT_AFFINITY: + final boolean avoid = properties.getBoolean(key, + DEFAULT_AVOID_UNDEFINED_TRANSPORT_AFFINITY); + if (AVOID_UNDEFINED_TRANSPORT_AFFINITY != avoid) { + AVOID_UNDEFINED_TRANSPORT_AFFINITY = avoid; + mShouldReprocessNetworkCapabilities = true; + } + break; + } + } + + private void dump(IndentingPrintWriter pw) { + pw.println(); + pw.print(ConnectivityController.class.getSimpleName()); + pw.println(":"); + pw.increaseIndent(); + + pw.print(KEY_AVOID_UNDEFINED_TRANSPORT_AFFINITY, + AVOID_UNDEFINED_TRANSPORT_AFFINITY).println(); + + pw.decreaseIndent(); + } + } + private class UidDefaultNetworkCallback extends NetworkCallback { private int mUid; @Nullable @@ -1676,6 +1862,18 @@ public final class ConnectivityController extends RestrictingController implemen } } + private static class CachedNetworkMetadata { + public NetworkCapabilities networkCapabilities; + public boolean satisfiesTransportAffinities; + + public String toString() { + return "CNM{" + + networkCapabilities.toString() + + ", satisfiesTransportAffinities=" + satisfiesTransportAffinities + + "}"; + } + } + private static class UidStats { public final int uid; public int baseBias; @@ -1739,6 +1937,17 @@ public final class ConnectivityController extends RestrictingController implemen } } + @VisibleForTesting + @NonNull + CcConfig getCcConfig() { + return mCcConfig; + } + + @Override + public void dumpConstants(IndentingPrintWriter pw) { + mCcConfig.dump(pw); + } + @GuardedBy("mLock") @Override public void dumpControllerStateLocked(IndentingPrintWriter pw, diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java index 0e03ea1ebe7d..70f9a52f3299 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java @@ -247,6 +247,12 @@ public final class FlexibilityController extends StateController { mPrefetchLifeCycleStart.delete(userId); } + boolean isEnabled() { + synchronized (mLock) { + return mFlexibilityEnabled; + } + } + /** Checks if the flexibility constraint is actively satisfied for a given job. */ @GuardedBy("mLock") boolean isFlexibilitySatisfiedLocked(JobStatus js) { @@ -262,7 +268,8 @@ public final class FlexibilityController extends StateController { int getNumSatisfiedRequiredConstraintsLocked(JobStatus js) { return Integer.bitCount(mSatisfiedFlexibleConstraints) // Connectivity is job-specific, so must be handled separately. - + (js.getHasAccessToUnmetered() ? 1 : 0); + + (js.canApplyTransportAffinities() + && js.areTransportAffinitiesSatisfied() ? 1 : 0); } /** @@ -495,7 +502,7 @@ public final class FlexibilityController extends StateController { final int curPercent = getCurPercentOfLifecycleLocked(js, nowElapsed); int toDrop = 0; final int jsMaxFlexibleConstraints = NUM_SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS - + (js.getPreferUnmetered() ? 1 : 0); + + (js.canApplyTransportAffinities() ? 1 : 0); for (int i = 0; i < jsMaxFlexibleConstraints; i++) { if (curPercent >= mPercentToDropConstraints[i]) { toDrop++; @@ -661,7 +668,6 @@ public final class FlexibilityController extends StateController { } } - @VisibleForTesting class FcConfig { private boolean mShouldReevaluateConstraints = false; @@ -682,7 +688,7 @@ public final class FlexibilityController extends StateController { static final String KEY_RESCHEDULED_JOB_DEADLINE_MS = FC_CONFIG_PREFIX + "rescheduled_job_deadline_ms"; - private static final boolean DEFAULT_FLEXIBILITY_ENABLED = false; + static final boolean DEFAULT_FLEXIBILITY_ENABLED = false; @VisibleForTesting static final long DEFAULT_DEADLINE_PROXIMITY_LIMIT_MS = 15 * MINUTE_IN_MILLIS; @VisibleForTesting diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java index cb6cc2bd58aa..d6ada4cd7fdc 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java @@ -16,7 +16,6 @@ package com.android.server.job.controllers; -import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; import static android.text.format.DateUtils.HOUR_IN_MILLIS; import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX; @@ -163,9 +162,6 @@ public final class JobStatus { */ private int mNumDroppedFlexibleConstraints; - /** If the job is going to be passed an unmetered network. */ - private boolean mHasAccessToUnmetered; - /** If the effective bucket has been downgraded once due to being buggy. */ private boolean mIsDowngradedDueToBuggyApp; @@ -562,11 +558,10 @@ public final class JobStatus { /** The job's dynamic requirements have been satisfied. */ private boolean mReadyDynamicSatisfied; - /** - * The job prefers an unmetered network if it has the connectivity constraint but is - * okay with any meteredness. - */ - private final boolean mPreferUnmetered; + /** Whether to apply the optimization transport preference logic to this job. */ + private final boolean mCanApplyTransportAffinities; + /** True if the optimization transport preference is satisfied for this job. */ + private boolean mTransportAffinitiesSatisfied; /** The reason a job most recently went from ready to not ready. */ private int mReasonReadyToUnready = JobParameters.STOP_REASON_UNDEFINED; @@ -671,12 +666,12 @@ public final class JobStatus { } mHasExemptedMediaUrisOnly = exemptedMediaUrisOnly; - mPreferUnmetered = job.getRequiredNetwork() != null - && !job.getRequiredNetwork().hasCapability(NET_CAPABILITY_NOT_METERED); + mCanApplyTransportAffinities = job.getRequiredNetwork() != null + && job.getRequiredNetwork().getTransportTypes().length == 0; final boolean lacksSomeFlexibleConstraints = ((~requiredConstraints) & SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS) != 0 - || mPreferUnmetered; + || mCanApplyTransportAffinities; final boolean satisfiesMinWindowException = (latestRunTimeElapsedMillis - earliestRunTimeElapsedMillis) >= MIN_WINDOW_FOR_FLEXIBILITY_MS; @@ -688,7 +683,7 @@ public final class JobStatus { && (numFailures + numSystemStops) != 1 && lacksSomeFlexibleConstraints) { mNumRequiredFlexibleConstraints = - NUM_SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS + (mPreferUnmetered ? 1 : 0); + NUM_SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS + (mCanApplyTransportAffinities ? 1 : 0); requiredConstraints |= CONSTRAINT_FLEXIBLE; } else { mNumRequiredFlexibleConstraints = 0; @@ -1585,17 +1580,16 @@ public final class JobStatus { mOriginalLatestRunTimeElapsedMillis = latestRunTimeElapsed; } - /** Sets the jobs access to an unmetered network. */ - void setHasAccessToUnmetered(boolean access) { - mHasAccessToUnmetered = access; + boolean areTransportAffinitiesSatisfied() { + return mTransportAffinitiesSatisfied; } - boolean getHasAccessToUnmetered() { - return mHasAccessToUnmetered; + void setTransportAffinitiesSatisfied(boolean isSatisfied) { + mTransportAffinitiesSatisfied = isSatisfied; } - boolean getPreferUnmetered() { - return mPreferUnmetered; + boolean canApplyTransportAffinities() { + return mCanApplyTransportAffinities; } @JobParameters.StopReason diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java index 64e86f9ab1fd..10f8510c7c70 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java @@ -24,7 +24,9 @@ import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED; import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED; import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_TEST; import static android.net.NetworkCapabilities.TRANSPORT_VPN; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; import static android.text.format.DateUtils.SECOND_IN_MILLIS; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; @@ -38,6 +40,10 @@ import static com.android.server.job.Flags.FLAG_RELAX_PREFETCH_CONNECTIVITY_CONS import static com.android.server.job.JobSchedulerService.FREQUENT_INDEX; import static com.android.server.job.JobSchedulerService.RARE_INDEX; import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX; +import static com.android.server.job.controllers.ConnectivityController.CcConfig.KEY_AVOID_UNDEFINED_TRANSPORT_AFFINITY; +import static com.android.server.job.controllers.ConnectivityController.TRANSPORT_AFFINITY_AVOID; +import static com.android.server.job.controllers.ConnectivityController.TRANSPORT_AFFINITY_PREFER; +import static com.android.server.job.controllers.ConnectivityController.TRANSPORT_AFFINITY_UNDEFINED; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -64,16 +70,19 @@ import android.net.ConnectivityManager.NetworkCallback; import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkPolicyManager; +import android.net.NetworkRequest; import android.os.Build; import android.os.Looper; import android.os.SystemClock; import android.platform.test.flag.junit.SetFlagsRule; +import android.provider.DeviceConfig; import android.telephony.CellSignalStrength; import android.telephony.SignalStrength; import android.telephony.TelephonyCallback; import android.telephony.TelephonyManager; import android.util.DataUnit; +import com.android.server.AppSchedulingModuleThread; import com.android.server.LocalServices; import com.android.server.job.JobSchedulerInternal; import com.android.server.job.JobSchedulerService; @@ -114,6 +123,7 @@ public class ConnectivityControllerTest { public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); private Constants mConstants; + private DeviceConfig.Properties.Builder mDeviceConfigPropertiesBuilder; private FlexibilityController mFlexibilityController; private static final int UID_RED = 10001; @@ -135,6 +145,9 @@ public class ConnectivityControllerTest { LocalServices.removeServiceForTest(JobSchedulerInternal.class); LocalServices.addService(JobSchedulerInternal.class, mock(JobSchedulerInternal.class)); + mDeviceConfigPropertiesBuilder = + new DeviceConfig.Properties.Builder(DeviceConfig.NAMESPACE_JOB_SCHEDULER); + // Freeze the clocks at this moment in time JobSchedulerService.sSystemClock = Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC); @@ -164,7 +177,7 @@ public class ConnectivityControllerTest { when(mPackageManager.hasSystemFeature( PackageManager.FEATURE_AUTOMOTIVE)).thenReturn(false); mFlexibilityController = - new FlexibilityController(mService, mock(PrefetchController.class)); + spy(new FlexibilityController(mService, mock(PrefetchController.class))); } @Test @@ -954,6 +967,12 @@ public class ConnectivityControllerTest { @Test public void testUpdates() throws Exception { + ConnectivityController.sNetworkTransportAffinities.put( + NetworkCapabilities.TRANSPORT_CELLULAR, TRANSPORT_AFFINITY_AVOID); + ConnectivityController.sNetworkTransportAffinities.put( + NetworkCapabilities.TRANSPORT_WIFI, TRANSPORT_AFFINITY_PREFER); + ConnectivityController.sNetworkTransportAffinities.put( + NetworkCapabilities.TRANSPORT_TEST, TRANSPORT_AFFINITY_UNDEFINED); final ArgumentCaptor<NetworkCallback> callbackCaptor = ArgumentCaptor.forClass(NetworkCallback.class); doNothing().when(mConnManager).registerNetworkCallback(any(), callbackCaptor.capture()); @@ -966,16 +985,24 @@ public class ConnectivityControllerTest { doNothing().when(mConnManager).registerDefaultNetworkCallbackForUid( eq(UID_BLUE), blueCallbackCaptor.capture(), any()); + doReturn(true).when(mFlexibilityController).isEnabled(); + final ConnectivityController controller = new ConnectivityController(mService, mFlexibilityController); - final Network meteredNet = mock(Network.class); final NetworkCapabilities meteredCaps = createCapabilitiesBuilder().build(); final Network unmeteredNet = mock(Network.class); final NetworkCapabilities unmeteredCaps = createCapabilitiesBuilder() .addCapability(NET_CAPABILITY_NOT_METERED) .build(); + final NetworkCapabilities meteredWifiCaps = createCapabilitiesBuilder() + .addTransportType(TRANSPORT_WIFI) + .build(); + final NetworkCapabilities unmeteredCelullarCaps = createCapabilitiesBuilder() + .addCapability(NET_CAPABILITY_NOT_METERED) + .addTransportType(TRANSPORT_CELLULAR) + .build(); final JobStatus red = createJobStatus(createJob() .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), 0) @@ -983,11 +1010,29 @@ public class ConnectivityControllerTest { final JobStatus blue = createJobStatus(createJob() .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), 0) .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY), UID_BLUE); - assertFalse(red.getPreferUnmetered()); - assertTrue(blue.getPreferUnmetered()); + final JobStatus red2 = createJobStatus(createJob() + .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), 0) + .setRequiredNetwork( + new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR) + .build()), + UID_RED); + final JobStatus blue2 = createJobStatus(createJob() + .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), 0) + .setRequiredNetwork( + new NetworkRequest.Builder() + .addTransportType(TRANSPORT_WIFI) + .build()), + UID_BLUE); + assertTrue(red.canApplyTransportAffinities()); + assertTrue(blue.canApplyTransportAffinities()); + assertFalse(red2.canApplyTransportAffinities()); + assertFalse(blue2.canApplyTransportAffinities()); controller.maybeStartTrackingJobLocked(red, null); controller.maybeStartTrackingJobLocked(blue, null); + controller.maybeStartTrackingJobLocked(red2, null); + controller.maybeStartTrackingJobLocked(blue2, null); final NetworkCallback generalCallback = callbackCaptor.getValue(); final NetworkCallback redCallback = redCallbackCaptor.getValue(); final NetworkCallback blueCallback = blueCallbackCaptor.getValue(); @@ -998,9 +1043,13 @@ public class ConnectivityControllerTest { answerNetwork(generalCallback, blueCallback, null, null, null); assertFalse(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); - assertFalse(red.getHasAccessToUnmetered()); assertFalse(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); - assertFalse(blue.getHasAccessToUnmetered()); + assertFalse(red2.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); + assertFalse(blue2.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); + assertFalse(red.areTransportAffinitiesSatisfied()); + assertFalse(blue.areTransportAffinitiesSatisfied()); + assertFalse(red2.areTransportAffinitiesSatisfied()); + assertFalse(blue2.areTransportAffinitiesSatisfied()); } // Metered network @@ -1011,12 +1060,26 @@ public class ConnectivityControllerTest { generalCallback.onCapabilitiesChanged(meteredNet, meteredCaps); assertFalse(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); - assertFalse(red.getHasAccessToUnmetered()); assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); - assertFalse(blue.getHasAccessToUnmetered()); + assertFalse(red2.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); + assertFalse(blue2.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); + // No transport is specified. Accept the network for transport affinity. + setDeviceConfigBoolean(controller, KEY_AVOID_UNDEFINED_TRANSPORT_AFFINITY, false); + controller.onConstantsUpdatedLocked(); + assertFalse(red.areTransportAffinitiesSatisfied()); + assertTrue(blue.areTransportAffinitiesSatisfied()); + assertFalse(red2.areTransportAffinitiesSatisfied()); + assertFalse(blue2.areTransportAffinitiesSatisfied()); + // No transport is specified. Avoid the network for transport affinity. + setDeviceConfigBoolean(controller, KEY_AVOID_UNDEFINED_TRANSPORT_AFFINITY, true); + controller.onConstantsUpdatedLocked(); + assertFalse(red.areTransportAffinitiesSatisfied()); + assertFalse(blue.areTransportAffinitiesSatisfied()); + assertFalse(red2.areTransportAffinitiesSatisfied()); + assertFalse(blue2.areTransportAffinitiesSatisfied()); } - // Unmetered network background + // Unmetered network background for general; metered network for apps { answerNetwork(generalCallback, redCallback, meteredNet, meteredNet, meteredCaps); answerNetwork(generalCallback, blueCallback, meteredNet, meteredNet, meteredCaps); @@ -1024,10 +1087,22 @@ public class ConnectivityControllerTest { generalCallback.onCapabilitiesChanged(unmeteredNet, unmeteredCaps); assertFalse(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); - assertFalse(red.getHasAccessToUnmetered()); - assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); - assertFalse(blue.getHasAccessToUnmetered()); + + // No transport is specified. Accept the network for transport affinity. + setDeviceConfigBoolean(controller, KEY_AVOID_UNDEFINED_TRANSPORT_AFFINITY, false); + controller.onConstantsUpdatedLocked(); + assertFalse(red.areTransportAffinitiesSatisfied()); + assertTrue(blue.areTransportAffinitiesSatisfied()); + assertFalse(red2.areTransportAffinitiesSatisfied()); + assertFalse(blue2.areTransportAffinitiesSatisfied()); + // No transport is specified. Avoid the network for transport affinity. + setDeviceConfigBoolean(controller, KEY_AVOID_UNDEFINED_TRANSPORT_AFFINITY, true); + controller.onConstantsUpdatedLocked(); + assertFalse(red.areTransportAffinitiesSatisfied()); + assertFalse(blue.areTransportAffinitiesSatisfied()); + assertFalse(red2.areTransportAffinitiesSatisfied()); + assertFalse(blue2.areTransportAffinitiesSatisfied()); } // Lost metered network @@ -1038,10 +1113,7 @@ public class ConnectivityControllerTest { generalCallback.onLost(meteredNet); assertTrue(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); - assertFalse(red.getHasAccessToUnmetered()); - assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); - assertTrue(blue.getHasAccessToUnmetered()); } // Specific UID was blocked @@ -1052,9 +1124,99 @@ public class ConnectivityControllerTest { generalCallback.onCapabilitiesChanged(unmeteredNet, unmeteredCaps); assertFalse(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); - assertFalse(red.getHasAccessToUnmetered()); assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); - assertTrue(blue.getHasAccessToUnmetered()); + } + + // Metered wifi + { + answerNetwork(generalCallback, redCallback, null, meteredNet, meteredWifiCaps); + answerNetwork(generalCallback, blueCallback, unmeteredNet, meteredNet, meteredWifiCaps); + + generalCallback.onCapabilitiesChanged(meteredNet, meteredWifiCaps); + + assertFalse(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); + assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); + assertFalse(red2.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); + assertTrue(blue2.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); + + // Wifi is preferred. + setDeviceConfigBoolean(controller, KEY_AVOID_UNDEFINED_TRANSPORT_AFFINITY, false); + controller.onConstantsUpdatedLocked(); + assertFalse(red.areTransportAffinitiesSatisfied()); + assertTrue(blue.areTransportAffinitiesSatisfied()); + assertFalse(red2.areTransportAffinitiesSatisfied()); + assertTrue(blue2.areTransportAffinitiesSatisfied()); + // Wifi is preferred. + setDeviceConfigBoolean(controller, KEY_AVOID_UNDEFINED_TRANSPORT_AFFINITY, true); + controller.onConstantsUpdatedLocked(); + assertFalse(red.areTransportAffinitiesSatisfied()); + assertTrue(blue.areTransportAffinitiesSatisfied()); + assertFalse(red2.areTransportAffinitiesSatisfied()); + assertTrue(blue2.areTransportAffinitiesSatisfied()); + } + + // Unmetered cellular + { + answerNetwork(generalCallback, redCallback, meteredNet, + unmeteredNet, unmeteredCelullarCaps); + answerNetwork(generalCallback, blueCallback, meteredNet, + unmeteredNet, unmeteredCelullarCaps); + + generalCallback.onCapabilitiesChanged(unmeteredNet, unmeteredCelullarCaps); + + assertTrue(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); + assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); + assertTrue(red2.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); + assertFalse(blue2.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); + + // Cellular is avoided. + setDeviceConfigBoolean(controller, KEY_AVOID_UNDEFINED_TRANSPORT_AFFINITY, false); + controller.onConstantsUpdatedLocked(); + assertFalse(red.areTransportAffinitiesSatisfied()); + assertFalse(blue.areTransportAffinitiesSatisfied()); + assertFalse(red2.areTransportAffinitiesSatisfied()); + assertFalse(blue2.areTransportAffinitiesSatisfied()); + // Cellular is avoided. + setDeviceConfigBoolean(controller, KEY_AVOID_UNDEFINED_TRANSPORT_AFFINITY, true); + controller.onConstantsUpdatedLocked(); + assertFalse(red.areTransportAffinitiesSatisfied()); + assertFalse(blue.areTransportAffinitiesSatisfied()); + assertFalse(red2.areTransportAffinitiesSatisfied()); + assertFalse(blue2.areTransportAffinitiesSatisfied()); + } + + // Undefined affinity + final NetworkCapabilities unmeteredTestCaps = createCapabilitiesBuilder() + .addCapability(NET_CAPABILITY_NOT_METERED) + .addTransportType(TRANSPORT_TEST) + .build(); + { + answerNetwork(generalCallback, redCallback, unmeteredNet, + unmeteredNet, unmeteredTestCaps); + answerNetwork(generalCallback, blueCallback, unmeteredNet, + unmeteredNet, unmeteredTestCaps); + + generalCallback.onCapabilitiesChanged(unmeteredNet, unmeteredTestCaps); + + assertTrue(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); + assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); + assertFalse(red2.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); + assertFalse(blue2.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)); + + // Undefined is preferred. + setDeviceConfigBoolean(controller, KEY_AVOID_UNDEFINED_TRANSPORT_AFFINITY, false); + controller.onConstantsUpdatedLocked(); + assertTrue(red.areTransportAffinitiesSatisfied()); + assertTrue(blue.areTransportAffinitiesSatisfied()); + assertFalse(red2.areTransportAffinitiesSatisfied()); + assertFalse(blue2.areTransportAffinitiesSatisfied()); + // Undefined is avoided. + setDeviceConfigBoolean(controller, KEY_AVOID_UNDEFINED_TRANSPORT_AFFINITY, true); + controller.onConstantsUpdatedLocked(); + assertFalse(red.areTransportAffinitiesSatisfied()); + assertFalse(blue.areTransportAffinitiesSatisfied()); + assertFalse(red2.areTransportAffinitiesSatisfied()); + assertFalse(blue2.areTransportAffinitiesSatisfied()); } } @@ -1462,4 +1624,24 @@ public class ConnectivityControllerTest { return new JobStatus(job.build(), uid, null, -1, 0, null, null, earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis, 0, 0, 0, null, 0, 0); } + + private void setDeviceConfigBoolean(ConnectivityController connectivityController, + String key, boolean val) { + mDeviceConfigPropertiesBuilder.setBoolean(key, val); + synchronized (connectivityController.mLock) { + connectivityController.prepareForUpdatedConstantsLocked(); + mFlexibilityController.prepareForUpdatedConstantsLocked(); + connectivityController.getCcConfig() + .processConstantLocked(mDeviceConfigPropertiesBuilder.build(), key); + mFlexibilityController.getFcConfig() + .processConstantLocked(mDeviceConfigPropertiesBuilder.build(), key); + connectivityController.onConstantsUpdatedLocked(); + mFlexibilityController.onConstantsUpdatedLocked(); + } + waitForNonDelayedMessagesProcessed(); + } + + private void waitForNonDelayedMessagesProcessed() { + AppSchedulingModuleThread.getHandler().runWithScissors(() -> {}, 15_000); + } } diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java index bb9dcf1c85cc..ee68b6d0e546 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java @@ -692,15 +692,15 @@ public class FlexibilityControllerTest { } @Test - public void testConnectionToUnMeteredNetwork() { + public void testTransportAffinity() { JobInfo.Builder jb = createJob(0).setRequiredNetworkType(NETWORK_TYPE_ANY); JobStatus js = createJobStatus("testTopAppBypass", jb); synchronized (mFlexibilityController.mLock) { - js.setHasAccessToUnmetered(false); + js.setTransportAffinitiesSatisfied(false); assertEquals(0, mFlexibilityController.getNumSatisfiedRequiredConstraintsLocked(js)); - js.setHasAccessToUnmetered(true); + js.setTransportAffinitiesSatisfied(true); assertEquals(1, mFlexibilityController.getNumSatisfiedRequiredConstraintsLocked(js)); - js.setHasAccessToUnmetered(false); + js.setTransportAffinitiesSatisfied(false); assertEquals(0, mFlexibilityController.getNumSatisfiedRequiredConstraintsLocked(js)); } } @@ -937,10 +937,10 @@ public class FlexibilityControllerTest { ArraySet<JobStatus> jobs = trackedJobs.get(i); for (int j = 0; j < jobs.size(); j++) { JobStatus js = jobs.valueAt(j); - final int isUnMetered = js.getPreferUnmetered() - && js.getHasAccessToUnmetered() ? 1 : 0; + final int transportAffinitySatisfied = js.canApplyTransportAffinities() + && js.areTransportAffinitiesSatisfied() ? 1 : 0; assertEquals(js.getNumRequiredFlexibleConstraints() - <= numSatisfiedConstraints + isUnMetered, + <= numSatisfiedConstraints + transportAffinitySatisfied, js.isConstraintSatisfied(CONSTRAINT_FLEXIBLE)); } } |