blob: 2e72fbd9593145e688fee1137acff84f78714610 [file] [log] [blame]
/*
* Copyright (C) 2018 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.location;
import android.content.Context;
import android.database.Cursor;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.net.NetworkRequest;
import android.os.Handler;
import android.os.Looper;
import android.os.PowerManager;
import android.provider.Telephony.Carriers;
import android.telephony.ServiceState;
import android.telephony.TelephonyManager;
import android.util.Log;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.HashMap;
/**
* Handles network connection requests and network state change updates for AGPS data download.
*/
class GnssNetworkConnectivityHandler {
static final String TAG = "GnssNetworkConnectivityHandler";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
// for mAGpsDataConnectionState
private static final int AGPS_DATA_CONNECTION_CLOSED = 0;
private static final int AGPS_DATA_CONNECTION_OPENING = 1;
private static final int AGPS_DATA_CONNECTION_OPEN = 2;
// these need to match AGnssStatusValue enum in IAGnssCallback.hal
/** AGPS status event values. */
private static final int GPS_REQUEST_AGPS_DATA_CONN = 1;
private static final int GPS_RELEASE_AGPS_DATA_CONN = 2;
private static final int GPS_AGPS_DATA_CONNECTED = 3;
private static final int GPS_AGPS_DATA_CONN_DONE = 4;
private static final int GPS_AGPS_DATA_CONN_FAILED = 5;
// these must match the ApnIpType enum in IAGnss.hal
private static final int APN_INVALID = 0;
private static final int APN_IPV4 = 1;
private static final int APN_IPV6 = 2;
private static final int APN_IPV4V6 = 3;
// these must match the NetworkCapability enum flags in IAGnssRil.hal
private static final int AGNSS_NET_CAPABILITY_NOT_METERED = 1 << 0;
private static final int AGNSS_NET_CAPABILITY_NOT_ROAMING = 1 << 1;
// these need to match AGnssType enum in IAGnssCallback.hal
public static final int AGPS_TYPE_SUPL = 1;
public static final int AGPS_TYPE_C2K = 2;
private static final int AGPS_TYPE_EIMS = 3;
private static final int AGPS_TYPE_IMS = 4;
// Default time limit in milliseconds for the ConnectivityManager to find a suitable
// network with SUPL connectivity or report an error.
private static final int SUPL_NETWORK_REQUEST_TIMEOUT_MILLIS = 10 * 1000;
private static final int HASH_MAP_INITIAL_CAPACITY_TO_TRACK_CONNECTED_NETWORKS = 5;
// Keeps track of networks and their state as notified by the network request callbacks.
// Limit initial capacity to 5 as the number of connected networks will likely be small.
// NOTE: Must be accessed/modified only through the mHandler thread.
private HashMap<Network, NetworkAttributes> mAvailableNetworkAttributes =
new HashMap<>(HASH_MAP_INITIAL_CAPACITY_TO_TRACK_CONNECTED_NETWORKS);
private final ConnectivityManager mConnMgr;
private final Handler mHandler;
private final GnssNetworkListener mGnssNetworkListener;
private int mAGpsDataConnectionState;
private InetAddress mAGpsDataConnectionIpAddr;
private int mAGpsType;
private final Context mContext;
// Wakelocks
private static final String WAKELOCK_KEY = "GnssNetworkConnectivityHandler";
private static final long WAKELOCK_TIMEOUT_MILLIS = 60 * 1000;
private final PowerManager.WakeLock mWakeLock;
/**
* Network attributes needed when updating HAL about network connectivity status changes.
*/
private static class NetworkAttributes {
private NetworkCapabilities mCapabilities;
private String mApn;
private int mType = ConnectivityManager.TYPE_NONE;
/**
* Returns true if the capabilities that we pass on to HAL change between {@curCapabilities}
* and {@code newCapabilities}.
*/
private static boolean hasCapabilitiesChanged(NetworkCapabilities curCapabilities,
NetworkCapabilities newCapabilities) {
if (curCapabilities == null || newCapabilities == null) {
return true;
}
// Monitor for roaming and metered capability changes.
return hasCapabilityChanged(curCapabilities, newCapabilities,
NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
|| hasCapabilityChanged(curCapabilities, newCapabilities,
NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
}
private static boolean hasCapabilityChanged(NetworkCapabilities curCapabilities,
NetworkCapabilities newCapabilities, int capability) {
return curCapabilities.hasCapability(capability)
!= newCapabilities.hasCapability(capability);
}
private static short getCapabilityFlags(NetworkCapabilities capabilities) {
short capabilityFlags = 0;
if (capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)) {
capabilityFlags |= AGNSS_NET_CAPABILITY_NOT_ROAMING;
}
if (capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)) {
capabilityFlags |= AGNSS_NET_CAPABILITY_NOT_METERED;
}
return capabilityFlags;
}
}
/**
* Callback used to listen for data connectivity changes.
*/
private ConnectivityManager.NetworkCallback mNetworkConnectivityCallback;
/**
* Callback used to listen for availability of a requested SUPL connection.
* It is kept as a separate instance from {@link #mNetworkConnectivityCallback} to be able to
* manage the registration/un-registration lifetimes separately.
*/
private ConnectivityManager.NetworkCallback mSuplConnectivityCallback;
/**
* Interface to listen for network availability changes.
*/
interface GnssNetworkListener {
void onNetworkAvailable();
}
GnssNetworkConnectivityHandler(Context context,
GnssNetworkListener gnssNetworkListener,
Looper looper) {
mContext = context;
mGnssNetworkListener = gnssNetworkListener;
PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_KEY);
mHandler = new Handler(looper);
mConnMgr = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
mSuplConnectivityCallback = createSuplConnectivityCallback();
}
void registerNetworkCallbacks() {
// register for connectivity change events.
NetworkRequest.Builder networkRequestBuilder = new NetworkRequest.Builder();
networkRequestBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
networkRequestBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED);
networkRequestBuilder.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN);
NetworkRequest networkRequest = networkRequestBuilder.build();
mNetworkConnectivityCallback = createNetworkConnectivityCallback();
mConnMgr.registerNetworkCallback(networkRequest, mNetworkConnectivityCallback, mHandler);
}
/**
* @return {@code true} if there is a data network available for outgoing connections,
* {@code false} otherwise.
*/
boolean isDataNetworkConnected() {
NetworkInfo activeNetworkInfo = mConnMgr.getActiveNetworkInfo();
return activeNetworkInfo != null && activeNetworkInfo.isConnected();
}
/**
* Called from native code to update AGPS connection status, or to request or release a SUPL
* connection.
*
* <p>Note: {@code suplIpAddr} parameter is not present from IAGnssCallback.hal@2.0 onwards
* and is set to {@code null}.
*/
void onReportAGpsStatus(int agpsType, int agpsStatus, byte[] suplIpAddr) {
if (DEBUG) Log.d(TAG, "AGPS_DATA_CONNECTION: " + agpsDataConnStatusAsString(agpsStatus));
switch (agpsStatus) {
case GPS_REQUEST_AGPS_DATA_CONN:
runOnHandler(() -> handleRequestSuplConnection(agpsType, suplIpAddr));
break;
case GPS_RELEASE_AGPS_DATA_CONN:
runOnHandler(() -> handleReleaseSuplConnection(GPS_RELEASE_AGPS_DATA_CONN));
break;
case GPS_AGPS_DATA_CONNECTED:
case GPS_AGPS_DATA_CONN_DONE:
case GPS_AGPS_DATA_CONN_FAILED:
break;
default:
Log.w(TAG, "Received unknown AGPS status: " + agpsStatus);
}
}
private ConnectivityManager.NetworkCallback createNetworkConnectivityCallback() {
return new ConnectivityManager.NetworkCallback() {
// Used to filter out network capabilities changes that we are not interested in.
// NOTE: Not using a ConcurrentHashMap and also not using locking around updates
// and access to the map object because it is all done inside the same
// handler thread invoking the callback methods.
private HashMap<Network, NetworkCapabilities>
mAvailableNetworkCapabilities = new HashMap<>(
HASH_MAP_INITIAL_CAPACITY_TO_TRACK_CONNECTED_NETWORKS);
@Override
public void onCapabilitiesChanged(Network network,
NetworkCapabilities capabilities) {
// This callback is invoked for any change in the network capabilities including
// initial availability, and changes while still available. Only process if the
// capabilities that we pass on to HAL change.
if (!NetworkAttributes.hasCapabilitiesChanged(
mAvailableNetworkCapabilities.get(network), capabilities)) {
if (VERBOSE) {
Log.v(TAG, "Relevant network capabilities unchanged. Capabilities: "
+ capabilities);
}
return;
}
mAvailableNetworkCapabilities.put(network, capabilities);
if (DEBUG) {
Log.d(TAG, "Network connected/capabilities updated. Available networks count: "
+ mAvailableNetworkCapabilities.size());
}
mGnssNetworkListener.onNetworkAvailable();
// Always on, notify HAL so it can get data it needs
handleUpdateNetworkState(network, true, capabilities);
}
@Override
public void onLost(Network network) {
if (mAvailableNetworkCapabilities.remove(network) == null) {
Log.w(TAG, "Incorrectly received network callback onLost() before"
+ " onCapabilitiesChanged() for network: " + network);
return;
}
Log.i(TAG, "Network connection lost. Available networks count: "
+ mAvailableNetworkCapabilities.size());
handleUpdateNetworkState(network, false, null);
}
};
}
private ConnectivityManager.NetworkCallback createSuplConnectivityCallback() {
return new ConnectivityManager.NetworkCallback() {
@Override
public void onAvailable(Network network) {
if (DEBUG) Log.d(TAG, "SUPL network connection available.");
// Specific to a change to a SUPL enabled network becoming ready
handleSuplConnectionAvailable(network);
}
@Override
public void onLost(Network network) {
Log.i(TAG, "SUPL network connection lost.");
handleReleaseSuplConnection(GPS_RELEASE_AGPS_DATA_CONN);
}
@Override
public void onUnavailable() {
Log.i(TAG, "SUPL network connection request timed out.");
// Could not setup the connection to the network in the specified time duration.
handleReleaseSuplConnection(GPS_AGPS_DATA_CONN_FAILED);
}
};
}
private void runOnHandler(Runnable event) {
// hold a wake lock until this message is delivered
// note that this assumes the message will not be removed from the queue before
// it is handled (otherwise the wake lock would be leaked).
mWakeLock.acquire(WAKELOCK_TIMEOUT_MILLIS);
if (!mHandler.post(runEventAndReleaseWakeLock(event))) {
mWakeLock.release();
}
}
private Runnable runEventAndReleaseWakeLock(Runnable event) {
return () -> {
try {
event.run();
} finally {
mWakeLock.release();
}
};
}
private void handleUpdateNetworkState(Network network, boolean isConnected,
NetworkCapabilities capabilities) {
boolean networkAvailable = isConnected && TelephonyManager.getDefault().getDataEnabled();
NetworkAttributes networkAttributes = updateTrackedNetworksState(isConnected, network,
capabilities);
String apn = networkAttributes.mApn;
int type = networkAttributes.mType;
// When isConnected is false, capabilities argument is null. So, use last received
// capabilities.
capabilities = networkAttributes.mCapabilities;
Log.i(TAG, String.format(
"updateNetworkState, state=%s, connected=%s, network=%s, capabilities=%s"
+ ", apn: %s, availableNetworkCount: %d",
agpsDataConnStateAsString(),
isConnected,
network,
capabilities,
apn,
mAvailableNetworkAttributes.size()));
if (native_is_agps_ril_supported()) {
native_update_network_state(
isConnected,
type,
!capabilities.hasTransport(
NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING), /* isRoaming */
networkAvailable,
apn != null ? apn : "",
network.getNetworkHandle(),
NetworkAttributes.getCapabilityFlags(capabilities));
} else if (DEBUG) {
Log.d(TAG, "Skipped network state update because GPS HAL AGPS-RIL is not supported");
}
}
private NetworkAttributes updateTrackedNetworksState(boolean isConnected, Network network,
NetworkCapabilities capabilities) {
if (!isConnected) {
// Connection lost event. So, remove it from tracked networks.
return mAvailableNetworkAttributes.remove(network);
}
NetworkAttributes networkAttributes = mAvailableNetworkAttributes.get(network);
if (networkAttributes != null) {
// Capabilities updated event for the connected network.
networkAttributes.mCapabilities = capabilities;
return networkAttributes;
}
// Initial capabilities event (equivalent to connection available event).
networkAttributes = new NetworkAttributes();
networkAttributes.mCapabilities = capabilities;
// TODO: The synchronous method ConnectivityManager.getNetworkInfo() should not be called
// inside the asynchronous ConnectivityManager.NetworkCallback methods.
NetworkInfo info = mConnMgr.getNetworkInfo(network);
if (info != null) {
networkAttributes.mApn = info.getExtraInfo();
networkAttributes.mType = info.getType();
}
// Start tracking this network for connection status updates.
mAvailableNetworkAttributes.put(network, networkAttributes);
return networkAttributes;
}
private void handleSuplConnectionAvailable(Network network) {
// TODO: The synchronous method ConnectivityManager.getNetworkInfo() should not be called
// inside the asynchronous ConnectivityManager.NetworkCallback methods.
NetworkInfo info = mConnMgr.getNetworkInfo(network);
String apn = null;
if (info != null) {
apn = info.getExtraInfo();
}
if (DEBUG) {
String message = String.format(
"handleSuplConnectionAvailable: state=%s, suplNetwork=%s, info=%s",
agpsDataConnStateAsString(),
network,
info);
Log.d(TAG, message);
}
if (mAGpsDataConnectionState == AGPS_DATA_CONNECTION_OPENING) {
if (apn == null) {
// assign a dummy value in the case of C2K as otherwise we will have a runtime
// exception in the following call to native_agps_data_conn_open
apn = "dummy-apn";
}
// Setting route to host is needed for GNSS HAL implementations earlier than
// @2.0::IAgnssCallback. The HAL @2.0::IAgnssCallback.agnssStatusCb() method does
// not require setting route to SUPL host and hence does not provide an IP address.
if (mAGpsDataConnectionIpAddr != null) {
setRouting();
}
int apnIpType = getApnIpType(apn);
if (DEBUG) {
String message = String.format(
"native_agps_data_conn_open: mAgpsApn=%s, mApnIpType=%s",
apn,
apnIpType);
Log.d(TAG, message);
}
native_agps_data_conn_open(network.getNetworkHandle(), apn, apnIpType);
mAGpsDataConnectionState = AGPS_DATA_CONNECTION_OPEN;
}
}
private void handleRequestSuplConnection(int agpsType, byte[] suplIpAddr) {
mAGpsDataConnectionIpAddr = null;
mAGpsType = agpsType;
if (suplIpAddr != null) {
if (VERBOSE) Log.v(TAG, "Received SUPL IP addr[]: " + Arrays.toString(suplIpAddr));
try {
mAGpsDataConnectionIpAddr = InetAddress.getByAddress(suplIpAddr);
if (DEBUG) Log.d(TAG, "IP address converted to: " + mAGpsDataConnectionIpAddr);
} catch (UnknownHostException e) {
Log.e(TAG, "Bad IP Address: " + suplIpAddr, e);
}
}
if (DEBUG) {
String message = String.format(
"requestSuplConnection, state=%s, agpsType=%s, address=%s",
agpsDataConnStateAsString(),
agpsTypeAsString(agpsType),
mAGpsDataConnectionIpAddr);
Log.d(TAG, message);
}
if (mAGpsDataConnectionState != AGPS_DATA_CONNECTION_CLOSED) {
return;
}
mAGpsDataConnectionState = AGPS_DATA_CONNECTION_OPENING;
// The transport type must be set to NetworkCapabilities.TRANSPORT_CELLULAR for the
// deprecated requestRouteToHostAddress() method in ConnectivityService to work for
// pre-gnss@2.0 devices.
NetworkRequest.Builder networkRequestBuilder = new NetworkRequest.Builder();
networkRequestBuilder.addCapability(getNetworkCapability(mAGpsType));
networkRequestBuilder.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
NetworkRequest networkRequest = networkRequestBuilder.build();
mConnMgr.requestNetwork(
networkRequest,
mSuplConnectivityCallback,
mHandler,
SUPL_NETWORK_REQUEST_TIMEOUT_MILLIS);
}
private int getNetworkCapability(int agpsType) {
switch (agpsType) {
case AGPS_TYPE_C2K:
case AGPS_TYPE_SUPL:
return NetworkCapabilities.NET_CAPABILITY_SUPL;
case AGPS_TYPE_EIMS:
return NetworkCapabilities.NET_CAPABILITY_EIMS;
case AGPS_TYPE_IMS:
return NetworkCapabilities.NET_CAPABILITY_IMS;
default:
throw new IllegalArgumentException("agpsType: " + agpsType);
}
}
private void handleReleaseSuplConnection(int agpsDataConnStatus) {
if (DEBUG) {
String message = String.format(
"releaseSuplConnection, state=%s, status=%s",
agpsDataConnStateAsString(),
agpsDataConnStatusAsString(agpsDataConnStatus));
Log.d(TAG, message);
}
if (mAGpsDataConnectionState == AGPS_DATA_CONNECTION_CLOSED) {
return;
}
mAGpsDataConnectionState = AGPS_DATA_CONNECTION_CLOSED;
mConnMgr.unregisterNetworkCallback(mSuplConnectivityCallback);
switch (agpsDataConnStatus) {
case GPS_AGPS_DATA_CONN_FAILED:
native_agps_data_conn_failed();
break;
case GPS_RELEASE_AGPS_DATA_CONN:
native_agps_data_conn_closed();
break;
default:
Log.e(TAG, "Invalid status to release SUPL connection: " + agpsDataConnStatus);
}
}
// TODO: Delete this method when all devices upgrade to HAL @2.0::IAGnssCallback
// interface which does not require setting route to host.
private void setRouting() {
boolean result = mConnMgr.requestRouteToHostAddress(
ConnectivityManager.TYPE_MOBILE_SUPL,
mAGpsDataConnectionIpAddr);
if (!result) {
Log.e(TAG, "Error requesting route to host: " + mAGpsDataConnectionIpAddr);
} else if (DEBUG) {
Log.d(TAG, "Successfully requested route to host: " + mAGpsDataConnectionIpAddr);
}
}
/**
* Ensures the calling function is running in the thread associated with {@link #mHandler}.
*/
private void ensureInHandlerThread() {
if (mHandler != null && Looper.myLooper() == mHandler.getLooper()) {
return;
}
throw new IllegalStateException("This method must run on the Handler thread.");
}
/**
* @return A string representing the current state stored in {@link #mAGpsDataConnectionState}.
*/
private String agpsDataConnStateAsString() {
switch (mAGpsDataConnectionState) {
case AGPS_DATA_CONNECTION_CLOSED:
return "CLOSED";
case AGPS_DATA_CONNECTION_OPEN:
return "OPEN";
case AGPS_DATA_CONNECTION_OPENING:
return "OPENING";
default:
return "<Unknown>(" + mAGpsDataConnectionState + ")";
}
}
/**
* @return A string representing the given GPS_AGPS_DATA status.
*/
private String agpsDataConnStatusAsString(int agpsDataConnStatus) {
switch (agpsDataConnStatus) {
case GPS_AGPS_DATA_CONNECTED:
return "CONNECTED";
case GPS_AGPS_DATA_CONN_DONE:
return "DONE";
case GPS_AGPS_DATA_CONN_FAILED:
return "FAILED";
case GPS_RELEASE_AGPS_DATA_CONN:
return "RELEASE";
case GPS_REQUEST_AGPS_DATA_CONN:
return "REQUEST";
default:
return "<Unknown>(" + agpsDataConnStatus + ")";
}
}
private String agpsTypeAsString(int agpsType) {
switch (agpsType) {
case AGPS_TYPE_SUPL:
return "SUPL";
case AGPS_TYPE_C2K:
return "C2K";
case AGPS_TYPE_EIMS:
return "EIMS";
case AGPS_TYPE_IMS:
return "IMS";
default:
return "<Unknown>(" + agpsType + ")";
}
}
private int getApnIpType(String apn) {
ensureInHandlerThread();
if (apn == null) {
return APN_INVALID;
}
TelephonyManager phone = (TelephonyManager)
mContext.getSystemService(Context.TELEPHONY_SERVICE);
ServiceState serviceState = phone.getServiceState();
String projection = null;
String selection = null;
// Carrier configuration may override framework roaming state, we need to use the actual
// modem roaming state instead of the framework roaming state.
if (serviceState != null && serviceState.getDataRoamingFromRegistration()) {
projection = Carriers.ROAMING_PROTOCOL;
} else {
projection = Carriers.PROTOCOL;
}
// No SIM case for emergency
if (TelephonyManager.NETWORK_TYPE_UNKNOWN == phone.getNetworkType()
&& AGPS_TYPE_EIMS == mAGpsType) {
selection = String.format(
"type like '%%emergency%%' and apn = '%s' and carrier_enabled = 1", apn);
} else {
selection = String.format("current = 1 and apn = '%s' and carrier_enabled = 1", apn);
}
try (Cursor cursor = mContext.getContentResolver().query(
Carriers.CONTENT_URI,
new String[]{projection},
selection,
null,
Carriers.DEFAULT_SORT_ORDER)) {
if (null != cursor && cursor.moveToFirst()) {
return translateToApnIpType(cursor.getString(0), apn);
} else {
Log.e(TAG, "No entry found in query for APN: " + apn);
}
} catch (Exception e) {
Log.e(TAG, "Error encountered on APN query for: " + apn, e);
}
return APN_IPV4V6;
}
private int translateToApnIpType(String ipProtocol, String apn) {
if ("IP".equals(ipProtocol)) {
return APN_IPV4;
}
if ("IPV6".equals(ipProtocol)) {
return APN_IPV6;
}
if ("IPV4V6".equals(ipProtocol)) {
return APN_IPV4V6;
}
// we hit the default case so the ipProtocol is not recognized
String message = String.format("Unknown IP Protocol: %s, for APN: %s", ipProtocol, apn);
Log.e(TAG, message);
return APN_IPV4V6;
}
// AGPS support
private native void native_agps_data_conn_open(long networkHandle, String apn, int apnIpType);
private native void native_agps_data_conn_closed();
private native void native_agps_data_conn_failed();
// AGPS ril support
private static native boolean native_is_agps_ril_supported();
private native void native_update_network_state(boolean connected, int type, boolean roaming,
boolean available, String apn, long networkHandle, short capabilities);
}