/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.server.job.controllers; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.ServiceManager; import android.os.UserHandle; import android.util.Slog; import com.android.server.ConnectivityService; import com.android.server.job.JobSchedulerService; import com.android.server.job.StateChangedListener; import java.io.PrintWriter; import java.util.LinkedList; import java.util.List; /** * Handles changes in connectivity. * We are only interested in metered vs. unmetered networks, and we're interested in them on a * per-user basis. */ public class ConnectivityController extends StateController implements ConnectivityManager.OnNetworkActiveListener { private static final String TAG = "JobScheduler.Conn"; private final List<JobStatus> mTrackedJobs = new LinkedList<JobStatus>(); private final BroadcastReceiver mConnectivityChangedReceiver = new ConnectivityChangedReceiver(); /** Singleton. */ private static ConnectivityController mSingleton; private static Object sCreationLock = new Object(); /** Track whether the latest active network is metered. */ private boolean mNetworkUnmetered; /** Track whether the latest active network is connected. */ private boolean mNetworkConnected; public static ConnectivityController get(JobSchedulerService jms) { synchronized (sCreationLock) { if (mSingleton == null) { mSingleton = new ConnectivityController(jms, jms.getContext()); } return mSingleton; } } private ConnectivityController(StateChangedListener stateChangedListener, Context context) { super(stateChangedListener, context); // Register connectivity changed BR. IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); mContext.registerReceiverAsUser( mConnectivityChangedReceiver, UserHandle.ALL, intentFilter, null, null); ConnectivityService cs = (ConnectivityService)ServiceManager.getService(Context.CONNECTIVITY_SERVICE); if (cs != null) { if (cs.getActiveNetworkInfo() != null) { mNetworkConnected = cs.getActiveNetworkInfo().isConnected(); } mNetworkUnmetered = mNetworkConnected && !cs.isActiveNetworkMetered(); } } @Override public void maybeStartTrackingJob(JobStatus jobStatus) { if (jobStatus.hasConnectivityConstraint() || jobStatus.hasUnmeteredConstraint()) { synchronized (mTrackedJobs) { jobStatus.connectivityConstraintSatisfied.set(mNetworkConnected); jobStatus.unmeteredConstraintSatisfied.set(mNetworkUnmetered); mTrackedJobs.add(jobStatus); } } } @Override public void maybeStopTrackingJob(JobStatus jobStatus) { if (jobStatus.hasConnectivityConstraint() || jobStatus.hasUnmeteredConstraint()) { synchronized (mTrackedJobs) { mTrackedJobs.remove(jobStatus); } } } /** * @param userId Id of the user for whom we are updating the connectivity state. */ private void updateTrackedJobs(int userId) { synchronized (mTrackedJobs) { boolean changed = false; for (JobStatus js : mTrackedJobs) { if (js.getUserId() != userId) { continue; } boolean prevIsConnected = js.connectivityConstraintSatisfied.getAndSet(mNetworkConnected); boolean prevIsMetered = js.unmeteredConstraintSatisfied.getAndSet(mNetworkUnmetered); if (prevIsConnected != mNetworkConnected || prevIsMetered != mNetworkUnmetered) { changed = true; } } if (changed) { mStateChangedListener.onControllerStateChanged(); } } } /** * We know the network has just come up. We want to run any jobs that are ready. */ public synchronized void onNetworkActive() { synchronized (mTrackedJobs) { for (JobStatus js : mTrackedJobs) { if (js.isReady()) { if (DEBUG) { Slog.d(TAG, "Running " + js + " due to network activity."); } mStateChangedListener.onRunJobNow(js); } } } } class ConnectivityChangedReceiver extends BroadcastReceiver { /** * We'll receive connectivity changes for each user here, which we process independently. * We are only interested in the active network here. We're only interested in the active * network, b/c the end result of this will be for apps to try to hit the network. * @param context The Context in which the receiver is running. * @param intent The Intent being received. */ // TODO: Test whether this will be called twice for each user. @Override public void onReceive(Context context, Intent intent) { if (DEBUG) { Slog.d(TAG, "Received connectivity event: " + intent.getAction() + " u" + context.getUserId()); } final String action = intent.getAction(); if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) { final int networkType = intent.getIntExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, ConnectivityManager.TYPE_NONE); // Connectivity manager for THIS context - important! final ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); final NetworkInfo activeNetwork = connManager.getActiveNetworkInfo(); final int userid = context.getUserId(); // This broadcast gets sent a lot, only update if the active network has changed. if (activeNetwork == null) { mNetworkUnmetered = false; mNetworkConnected = false; updateTrackedJobs(userid); } else if (activeNetwork.getType() == networkType) { mNetworkUnmetered = false; mNetworkConnected = !intent.getBooleanExtra( ConnectivityManager.EXTRA_NO_CONNECTIVITY, false); if (mNetworkConnected) { // No point making the call if we know there's no conn. mNetworkUnmetered = !connManager.isActiveNetworkMetered(); } updateTrackedJobs(userid); } } else { if (DEBUG) { Slog.d(TAG, "Unrecognised action in intent: " + action); } } } }; @Override public void dumpControllerState(PrintWriter pw) { pw.println("Conn."); pw.println("connected: " + mNetworkConnected + " unmetered: " + mNetworkUnmetered); for (JobStatus js: mTrackedJobs) { pw.println(String.valueOf(js.hashCode()).substring(0, 3) + ".." + ": C=" + js.hasConnectivityConstraint() + ", UM=" + js.hasUnmeteredConstraint()); } } }