Home | History | Annotate | Download | only in controllers
      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