1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.server.job.controllers; 18 19 import static android.net.NetworkCapabilities.LINK_BANDWIDTH_UNSPECIFIED; 20 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED; 21 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; 22 23 import android.app.job.JobInfo; 24 import android.net.ConnectivityManager; 25 import android.net.ConnectivityManager.NetworkCallback; 26 import android.net.INetworkPolicyListener; 27 import android.net.Network; 28 import android.net.NetworkCapabilities; 29 import android.net.NetworkInfo; 30 import android.net.NetworkPolicyManager; 31 import android.net.NetworkRequest; 32 import android.net.TrafficStats; 33 import android.os.UserHandle; 34 import android.text.format.DateUtils; 35 import android.util.ArraySet; 36 import android.util.Log; 37 import android.util.Slog; 38 import android.util.SparseArray; 39 import android.util.proto.ProtoOutputStream; 40 41 import com.android.internal.annotations.GuardedBy; 42 import com.android.internal.annotations.VisibleForTesting; 43 import com.android.internal.util.IndentingPrintWriter; 44 import com.android.server.job.JobSchedulerService; 45 import com.android.server.job.JobSchedulerService.Constants; 46 import com.android.server.job.JobServiceContext; 47 import com.android.server.job.StateControllerProto; 48 49 import java.util.Objects; 50 import java.util.function.Predicate; 51 52 /** 53 * Handles changes in connectivity. 54 * <p> 55 * Each app can have a different default networks or different connectivity 56 * status due to user-requested network policies, so we need to check 57 * constraints on a per-UID basis. 58 */ 59 public final class ConnectivityController extends StateController implements 60 ConnectivityManager.OnNetworkActiveListener { 61 private static final String TAG = "JobScheduler.Connectivity"; 62 private static final boolean DEBUG = JobSchedulerService.DEBUG 63 || Log.isLoggable(TAG, Log.DEBUG); 64 65 private final ConnectivityManager mConnManager; 66 private final NetworkPolicyManager mNetPolicyManager; 67 68 @GuardedBy("mLock") 69 private final ArraySet<JobStatus> mTrackedJobs = new ArraySet<>(); 70 71 public ConnectivityController(JobSchedulerService service) { 72 super(service); 73 74 mConnManager = mContext.getSystemService(ConnectivityManager.class); 75 mNetPolicyManager = mContext.getSystemService(NetworkPolicyManager.class); 76 77 // We're interested in all network changes; internally we match these 78 // network changes against the active network for each UID with jobs. 79 final NetworkRequest request = new NetworkRequest.Builder().clearCapabilities().build(); 80 mConnManager.registerNetworkCallback(request, mNetworkCallback); 81 82 mNetPolicyManager.registerListener(mNetPolicyListener); 83 } 84 85 @GuardedBy("mLock") 86 @Override 87 public void maybeStartTrackingJobLocked(JobStatus jobStatus, JobStatus lastJob) { 88 if (jobStatus.hasConnectivityConstraint()) { 89 updateConstraintsSatisfied(jobStatus); 90 mTrackedJobs.add(jobStatus); 91 jobStatus.setTrackingController(JobStatus.TRACKING_CONNECTIVITY); 92 } 93 } 94 95 @GuardedBy("mLock") 96 @Override 97 public void maybeStopTrackingJobLocked(JobStatus jobStatus, JobStatus incomingJob, 98 boolean forUpdate) { 99 if (jobStatus.clearTrackingController(JobStatus.TRACKING_CONNECTIVITY)) { 100 mTrackedJobs.remove(jobStatus); 101 } 102 } 103 104 /** 105 * Test to see if running the given job on the given network is insane. 106 * <p> 107 * For example, if a job is trying to send 10MB over a 128Kbps EDGE 108 * connection, it would take 10.4 minutes, and has no chance of succeeding 109 * before the job times out, so we'd be insane to try running it. 110 */ 111 @SuppressWarnings("unused") 112 private static boolean isInsane(JobStatus jobStatus, Network network, 113 NetworkCapabilities capabilities, Constants constants) { 114 final long estimatedBytes = jobStatus.getEstimatedNetworkBytes(); 115 if (estimatedBytes == JobInfo.NETWORK_BYTES_UNKNOWN) { 116 // We don't know how large the job is; cross our fingers! 117 return false; 118 } 119 120 // We don't ask developers to differentiate between upstream/downstream 121 // in their size estimates, so test against the slowest link direction. 122 final long slowest = NetworkCapabilities.minBandwidth( 123 capabilities.getLinkDownstreamBandwidthKbps(), 124 capabilities.getLinkUpstreamBandwidthKbps()); 125 if (slowest == LINK_BANDWIDTH_UNSPECIFIED) { 126 // We don't know what the network is like; cross our fingers! 127 return false; 128 } 129 130 final long estimatedMillis = ((estimatedBytes * DateUtils.SECOND_IN_MILLIS) 131 / (slowest * TrafficStats.KB_IN_BYTES / 8)); 132 if (estimatedMillis > JobServiceContext.EXECUTING_TIMESLICE_MILLIS) { 133 // If we'd never finish before the timeout, we'd be insane! 134 Slog.w(TAG, "Estimated " + estimatedBytes + " bytes over " + slowest 135 + " kbps network would take " + estimatedMillis + "ms; that's insane!"); 136 return true; 137 } else { 138 return false; 139 } 140 } 141 142 @SuppressWarnings("unused") 143 private static boolean isCongestionDelayed(JobStatus jobStatus, Network network, 144 NetworkCapabilities capabilities, Constants constants) { 145 // If network is congested, and job is less than 50% through the 146 // developer-requested window, then we're okay delaying the job. 147 if (!capabilities.hasCapability(NET_CAPABILITY_NOT_CONGESTED)) { 148 return jobStatus.getFractionRunTime() < constants.CONN_CONGESTION_DELAY_FRAC; 149 } else { 150 return false; 151 } 152 } 153 154 @SuppressWarnings("unused") 155 private static boolean isStrictSatisfied(JobStatus jobStatus, Network network, 156 NetworkCapabilities capabilities, Constants constants) { 157 return jobStatus.getJob().getRequiredNetwork().networkCapabilities 158 .satisfiedByNetworkCapabilities(capabilities); 159 } 160 161 @SuppressWarnings("unused") 162 private static boolean isRelaxedSatisfied(JobStatus jobStatus, Network network, 163 NetworkCapabilities capabilities, Constants constants) { 164 // Only consider doing this for prefetching jobs 165 if (!jobStatus.getJob().isPrefetch()) { 166 return false; 167 } 168 169 // See if we match after relaxing any unmetered request 170 final NetworkCapabilities relaxed = new NetworkCapabilities( 171 jobStatus.getJob().getRequiredNetwork().networkCapabilities) 172 .removeCapability(NET_CAPABILITY_NOT_METERED); 173 if (relaxed.satisfiedByNetworkCapabilities(capabilities)) { 174 // TODO: treat this as "maybe" response; need to check quotas 175 return jobStatus.getFractionRunTime() > constants.CONN_PREFETCH_RELAX_FRAC; 176 } else { 177 return false; 178 } 179 } 180 181 @VisibleForTesting 182 static boolean isSatisfied(JobStatus jobStatus, Network network, 183 NetworkCapabilities capabilities, Constants constants) { 184 // Zeroth, we gotta have a network to think about being satisfied 185 if (network == null || capabilities == null) return false; 186 187 // First, are we insane? 188 if (isInsane(jobStatus, network, capabilities, constants)) return false; 189 190 // Second, is the network congested? 191 if (isCongestionDelayed(jobStatus, network, capabilities, constants)) return false; 192 193 // Third, is the network a strict match? 194 if (isStrictSatisfied(jobStatus, network, capabilities, constants)) return true; 195 196 // Third, is the network a relaxed match? 197 if (isRelaxedSatisfied(jobStatus, network, capabilities, constants)) return true; 198 199 return false; 200 } 201 202 private boolean updateConstraintsSatisfied(JobStatus jobStatus) { 203 final Network network = mConnManager.getActiveNetworkForUid(jobStatus.getSourceUid()); 204 final NetworkCapabilities capabilities = mConnManager.getNetworkCapabilities(network); 205 return updateConstraintsSatisfied(jobStatus, network, capabilities); 206 } 207 208 private boolean updateConstraintsSatisfied(JobStatus jobStatus, Network network, 209 NetworkCapabilities capabilities) { 210 // TODO: consider matching against non-active networks 211 212 final boolean ignoreBlocked = (jobStatus.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0; 213 final NetworkInfo info = mConnManager.getNetworkInfoForUid(network, 214 jobStatus.getSourceUid(), ignoreBlocked); 215 216 final boolean connected = (info != null) && info.isConnected(); 217 final boolean satisfied = isSatisfied(jobStatus, network, capabilities, mConstants); 218 219 final boolean changed = jobStatus 220 .setConnectivityConstraintSatisfied(connected && satisfied); 221 222 // Pass along the evaluated network for job to use; prevents race 223 // conditions as default routes change over time, and opens the door to 224 // using non-default routes. 225 jobStatus.network = network; 226 227 if (DEBUG) { 228 Slog.i(TAG, "Connectivity " + (changed ? "CHANGED" : "unchanged") 229 + " for " + jobStatus + ": connected=" + connected 230 + " satisfied=" + satisfied); 231 } 232 return changed; 233 } 234 235 /** 236 * Update any jobs tracked by this controller that match given filters. 237 * 238 * @param filterUid only update jobs belonging to this UID, or {@code -1} to 239 * update all tracked jobs. 240 * @param filterNetwork only update jobs that would use this 241 * {@link Network}, or {@code null} to update all tracked jobs. 242 */ 243 private void updateTrackedJobs(int filterUid, Network filterNetwork) { 244 synchronized (mLock) { 245 // Since this is a really hot codepath, temporarily cache any 246 // answers that we get from ConnectivityManager. 247 final SparseArray<Network> uidToNetwork = new SparseArray<>(); 248 final SparseArray<NetworkCapabilities> networkToCapabilities = new SparseArray<>(); 249 250 boolean changed = false; 251 for (int i = mTrackedJobs.size() - 1; i >= 0; i--) { 252 final JobStatus js = mTrackedJobs.valueAt(i); 253 final int uid = js.getSourceUid(); 254 255 final boolean uidMatch = (filterUid == -1 || filterUid == uid); 256 if (uidMatch) { 257 Network network = uidToNetwork.get(uid); 258 if (network == null) { 259 network = mConnManager.getActiveNetworkForUid(uid); 260 uidToNetwork.put(uid, network); 261 } 262 263 // Update either when we have a network match, or when the 264 // job hasn't yet been evaluated against the currently 265 // active network; typically when we just lost a network. 266 final boolean networkMatch = (filterNetwork == null 267 || Objects.equals(filterNetwork, network)); 268 final boolean forceUpdate = !Objects.equals(js.network, network); 269 if (networkMatch || forceUpdate) { 270 final int netId = network != null ? network.netId : -1; 271 NetworkCapabilities capabilities = networkToCapabilities.get(netId); 272 if (capabilities == null) { 273 capabilities = mConnManager.getNetworkCapabilities(network); 274 networkToCapabilities.put(netId, capabilities); 275 } 276 changed |= updateConstraintsSatisfied(js, network, capabilities); 277 } 278 } 279 } 280 if (changed) { 281 mStateChangedListener.onControllerStateChanged(); 282 } 283 } 284 } 285 286 /** 287 * We know the network has just come up. We want to run any jobs that are ready. 288 */ 289 @Override 290 public void onNetworkActive() { 291 synchronized (mLock) { 292 for (int i = mTrackedJobs.size()-1; i >= 0; i--) { 293 final JobStatus js = mTrackedJobs.valueAt(i); 294 if (js.isReady()) { 295 if (DEBUG) { 296 Slog.d(TAG, "Running " + js + " due to network activity."); 297 } 298 mStateChangedListener.onRunJobNow(js); 299 } 300 } 301 } 302 } 303 304 private final NetworkCallback mNetworkCallback = new NetworkCallback() { 305 @Override 306 public void onCapabilitiesChanged(Network network, NetworkCapabilities capabilities) { 307 if (DEBUG) { 308 Slog.v(TAG, "onCapabilitiesChanged: " + network); 309 } 310 updateTrackedJobs(-1, network); 311 } 312 313 @Override 314 public void onLost(Network network) { 315 if (DEBUG) { 316 Slog.v(TAG, "onLost: " + network); 317 } 318 updateTrackedJobs(-1, network); 319 } 320 }; 321 322 private final INetworkPolicyListener mNetPolicyListener = new NetworkPolicyManager.Listener() { 323 @Override 324 public void onUidRulesChanged(int uid, int uidRules) { 325 if (DEBUG) { 326 Slog.v(TAG, "onUidRulesChanged: " + uid); 327 } 328 updateTrackedJobs(uid, null); 329 } 330 }; 331 332 @GuardedBy("mLock") 333 @Override 334 public void dumpControllerStateLocked(IndentingPrintWriter pw, 335 Predicate<JobStatus> predicate) { 336 for (int i = 0; i < mTrackedJobs.size(); i++) { 337 final JobStatus js = mTrackedJobs.valueAt(i); 338 if (predicate.test(js)) { 339 pw.print("#"); 340 js.printUniqueId(pw); 341 pw.print(" from "); 342 UserHandle.formatUid(pw, js.getSourceUid()); 343 pw.print(": "); 344 pw.print(js.getJob().getRequiredNetwork()); 345 pw.println(); 346 } 347 } 348 } 349 350 @GuardedBy("mLock") 351 @Override 352 public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, 353 Predicate<JobStatus> predicate) { 354 final long token = proto.start(fieldId); 355 final long mToken = proto.start(StateControllerProto.CONNECTIVITY); 356 357 for (int i = 0; i < mTrackedJobs.size(); i++) { 358 final JobStatus js = mTrackedJobs.valueAt(i); 359 if (!predicate.test(js)) { 360 continue; 361 } 362 final long jsToken = proto.start(StateControllerProto.ConnectivityController.TRACKED_JOBS); 363 js.writeToShortProto(proto, StateControllerProto.ConnectivityController.TrackedJob.INFO); 364 proto.write(StateControllerProto.ConnectivityController.TrackedJob.SOURCE_UID, 365 js.getSourceUid()); 366 NetworkRequest rn = js.getJob().getRequiredNetwork(); 367 if (rn != null) { 368 rn.writeToProto(proto, 369 StateControllerProto.ConnectivityController.TrackedJob.REQUIRED_NETWORK); 370 } 371 proto.end(jsToken); 372 } 373 374 proto.end(mToken); 375 proto.end(token); 376 } 377 } 378