/* * Copyright (C) 2010, 2011, The Linux Foundation. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of The Linux Foundation nor * the names of its contributors may be used to endorse or promote * products derived from this software without specific prior written * permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NON-INFRINGEMENT ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * */ package com.android.server; import com.android.internal.net.IPVersion; import com.android.internal.telephony.Phone; import com.android.internal.telephony.TelephonyIntents; import com.android.internal.telephony.Phone.DataState; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.ConnectivityManager; import android.net.DhcpInfo; import android.net.ILinkSocketMessageHandler; import android.net.LinkCapabilities; import android.net.LinkInfo; import android.net.LinkProperties; import android.net.LinkSocket; import android.net.NetworkInfo; import android.net.LinkProvider.LinkRequirement; import android.net.wifi.WifiManager; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.os.SystemProperties; import android.telephony.TelephonyManager; import android.util.Log; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * Public implementation of the ILinkManager interface. * Provides basic LinkManager functionality. */ public class LinkManager implements ILinkManager { private static final boolean DBG = true; private static final String TAG = "LinkManager"; private CarrierProfile mCarrierProfile; private Context mContext; private ConnectivityService mConnectivityService; private WifiManager mWifiManager; private TelephonyManager mTelephonyManager; private Map mActiveLinks; // SocketID -> LinkSocketInfo private HandlerThread mHandlerThread; private LMHandler mHandler; private int mDefaultNetwork; /** * Storage for LinkSocket-related data. */ private class LinkSocketInfo { public int id; // Socket ID public LinkCapabilities capabilities; public ILinkSocketMessageHandler callback; public int assignedNetwork; // ConnectivityManager.TYPE_WIFI or TYPE_MOBILE public int status; // State: unassigned, assigned, lost? static final int UNASSIGNED = 0; static final int ASSIGNED = 1; static final int LOST = 2; static final int NETWORK_UNKNOWN = -1; } public LinkManager(Context context, ConnectivityService cs) { if (DBG) Log.v(TAG, "LinkManager constructor"); mContext = context; mConnectivityService = cs; mCarrierProfile = new CarrierProfile(); String carrierPolicyFilename = SystemProperties.get("persist.cne.loc.policy.op"); try { mCarrierProfile.parse(new FileInputStream(carrierPolicyFilename)); } catch (FileNotFoundException e) { Log.e(TAG, "Carrier Policy file not found: " + carrierPolicyFilename); } mDefaultNetwork = mCarrierProfile.getPreferredNetworks(0).get(0); if (DBG) Log.v(TAG, "Default network = " + networkIntToString(mDefaultNetwork)); mConnectivityService.setDefaultRoute(mDefaultNetwork); mHandlerThread = new HandlerThread("LMHandler"); mHandlerThread.start(); mHandler = new LMHandler(mHandlerThread.getLooper()); mActiveLinks = new ConcurrentHashMap(); mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); IntentFilter filter = new IntentFilter(); filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION); filter.addAction(TelephonyIntents.ACTION_ANY_DATA_CONNECTION_STATE_CHANGED); context.registerReceiver(mIntentReceiver, filter); } BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); if (action.equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) { // Turn WiFi Intents into LinkManager Handler events (connected/disconnected) NetworkInfo networkInfo = (NetworkInfo) intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO); NetworkInfo.State state = (networkInfo == null ? NetworkInfo.State.UNKNOWN : networkInfo.getState()); if (state == NetworkInfo.State.CONNECTED) { Message msg = mHandler.obtainMessage(LMHandler.NETWORK_CONNECTED, ConnectivityManager.TYPE_WIFI, 0); msg.sendToTarget(); } else if (state == NetworkInfo.State.DISCONNECTED) { Message msg = mHandler.obtainMessage(LMHandler.NETWORK_DISCONNECTED, ConnectivityManager.TYPE_WIFI, 0); msg.sendToTarget(); } } else if (action.equals(TelephonyIntents.ACTION_ANY_DATA_CONNECTION_STATE_CHANGED)) { // Turn Telephony Intents into LinkManager Handler events (connected/disconnected) String apnType = intent.getStringExtra(Phone.DATA_APN_TYPES_KEY); if (apnType.equals(Phone.APN_TYPE_DEFAULT)) { DataState dataState = Enum.valueOf(Phone.DataState.class, intent.getStringExtra(Phone.DATA_APN_TYPE_STATE)); if (dataState == DataState.CONNECTED) { Message msg = mHandler.obtainMessage(LMHandler.NETWORK_CONNECTED, ConnectivityManager.TYPE_MOBILE, 0); msg.sendToTarget(); } else if (dataState == DataState.DISCONNECTED) { Message msg = mHandler.obtainMessage(LMHandler.NETWORK_DISCONNECTED, ConnectivityManager.TYPE_MOBILE, 0); msg.sendToTarget(); } } } else { if (DBG) Log.d(TAG, "Received unexpected action: " + action); } } }; /** * LinkManager event handler class. * The majority of the LinkManager logic resides here. */ class LMHandler extends Handler { // Message types static final int REQUEST_LINK = 1; static final int RELEASE_LINK = 2; static final int NETWORK_CONNECTED = 3; static final int NETWORK_DISCONNECTED = 4; static final int CONNECTION_TIMED_OUT = 5; // Constants static final int CONNECTION_TIMEOUT = 10000; // ms public LMHandler(Looper looper) { super(looper); } public void handleMessage(Message msg) { switch (msg.what) { case REQUEST_LINK: handleRequestLink(msg); break; case RELEASE_LINK: handleReleaseLink(msg); break; case NETWORK_CONNECTED: handleNetworkConnected(msg); break; case NETWORK_DISCONNECTED: handleNetworkDisconnected(msg); break; case CONNECTION_TIMED_OUT: handleConnectionTimedOut(msg); break; default: if (DBG) Log.d(TAG, "LMHandler handleMessage: unknown message"); break; } } private void handleRequestLink(Message msg) { int id = msg.arg1; LinkSocketInfo lsInfo = mActiveLinks.get(id); LinkCapabilities capabilities = lsInfo.capabilities; // needed capabilities int role = roleStringToInt(capabilities.get(LinkCapabilities.Key.RW_ROLE)); List preferredNetworks = mCarrierProfile.getPreferredNetworks(role); applyCapabilitiesToNetworks(capabilities, preferredNetworks); if (DBG) Log.v(TAG, "handleRequestLink id: " + id + " role: " + role); // assign to the first active network for (int network : preferredNetworks) { if (isNetworkConnected(network)) { getMinAvailableForwardBandwidth(network); // Stub for bandwidth estimation // TODO: add a timer for bandwidth estimate changed. if (DBG) Log.v(TAG, "assigning id " + lsInfo.id + " to network " + networkIntToString(network)); lsInfo.assignedNetwork = network; lsInfo.status = LinkSocketInfo.ASSIGNED; break; } } if (lsInfo.status == LinkSocketInfo.ASSIGNED) { // no changes are necessary, send the onLinkAvail notification try { lsInfo.callback.onLinkAvail(propertiesForNetwork(lsInfo.assignedNetwork)); } catch (RemoteException e) { if (DBG) Log.d(TAG, "RemoteException sending onLinkAvail()"); } } else { // No active networks are acceptable - bring up the first preference int network = preferredNetworks.get(0); lsInfo.assignedNetwork = network; if (DBG) Log.d(TAG, "cannot assign to available networks - bringing up " + networkIntToString(network)); mConnectivityService.reconnect(network); // Start timer for connection Message timeoutMsg = mHandler.obtainMessage(CONNECTION_TIMED_OUT, id, network); mHandler.sendMessageDelayed(timeoutMsg, CONNECTION_TIMEOUT); } } private void handleReleaseLink(Message msg) { LinkSocketInfo released = (LinkSocketInfo) msg.obj; int releasedNetwork = released.assignedNetwork; if (DBG) Log.v(TAG, "handleReleaseLink net: " + releasedNetwork); // Trigger teardown if no other links are using the network // (but don't tear down the last network or the default network) if (anotherNetworkIsAvailable(releasedNetwork) && releasedNetwork != mDefaultNetwork) { boolean shouldBringDownNetwork = true; for (LinkSocketInfo lsInfo : mActiveLinks.values()) { if (lsInfo.assignedNetwork == releasedNetwork) { shouldBringDownNetwork = false; break; } } if (shouldBringDownNetwork) { if (DBG) Log.v(TAG, "bringing down network: " + releasedNetwork); mConnectivityService.teardown(releasedNetwork); } } } private void handleNetworkConnected(Message msg) { int network = msg.arg1; if (DBG) Log.v(TAG, "handleNetworkConnected: " + networkIntToString(network)); // check for waiting connections for (LinkSocketInfo lsInfo : mActiveLinks.values()) { if (lsInfo.status == LinkSocketInfo.UNASSIGNED && lsInfo.assignedNetwork == network) { lsInfo.status = LinkSocketInfo.ASSIGNED; try { lsInfo.callback.onLinkAvail(propertiesForNetwork(lsInfo.assignedNetwork)); } catch (RemoteException e) { if (DBG) Log.d(TAG, "RemoteException sending onLinkAvail()"); } } } bringDownUnusedNetworks(); } private void handleNetworkDisconnected(Message msg) { int network = msg.arg1; if (DBG) Log.v(TAG, "handleNetworkDisconnected: " + networkIntToString(network)); // send notifications to all affected links for (LinkSocketInfo lsInfo : mActiveLinks.values()) { if (lsInfo.assignedNetwork == network) { lsInfo.status = LinkSocketInfo.LOST; try { lsInfo.callback.onLinkLost(); } catch (RemoteException e) { if (DBG) Log.d(TAG, "RemoteException sending onLinkLost()"); } if (anotherNetworkIsAvailable(network)) { try { lsInfo.callback.onBetterLinkAvail(); } catch (RemoteException e) { if (DBG) Log.d(TAG, "RemoteException sending onBetterLinkAvail()"); } } } } } private void handleConnectionTimedOut(Message msg) { int id = msg.arg1; int network = msg.arg2; if (DBG) Log.v(TAG, "handleConnectionTimedOut (possibly) id: " + id + " net: " + network); // check if the connection actually timed out if (mActiveLinks.get(id).status == LinkSocketInfo.ASSIGNED) return; if (DBG) Log.v(TAG, "handleConnectionTimedOut actual timeout id: " + id + " net: " + network); // bring down the failing network if (network != mDefaultNetwork) { mConnectivityService.teardown(network); } // timed out. send failure. try { mActiveLinks.get(id).callback.onGetLinkFailure(LinkSocket.ERROR_ALL_NETWORKS_DOWN); } catch (RemoteException e) { if (DBG) Log.d(TAG, "RemoteException sending onGetLinkFailure()"); } } private boolean anotherNetworkIsAvailable(int network) { int otherNetwork = LinkSocketInfo.NETWORK_UNKNOWN; if (network == ConnectivityManager.TYPE_MOBILE) otherNetwork = ConnectivityManager.TYPE_WIFI; else otherNetwork = ConnectivityManager.TYPE_MOBILE; return isNetworkConnected(otherNetwork); } private void bringDownUnusedNetworks() { if (DBG) Log.v(TAG, "bringDownUnusedNetworks() EX"); int[] allNetworks = {ConnectivityManager.TYPE_MOBILE, ConnectivityManager.TYPE_WIFI}; for (int i = 0; i < allNetworks.length; i++) { int network = allNetworks[i]; if (network == mDefaultNetwork) continue; // Don't bring down default network if (!anotherNetworkIsAvailable(network)) continue; // Don't bring down the only active network boolean networkInUse = false; for (LinkSocketInfo lsInfo : mActiveLinks.values()) { if (lsInfo.assignedNetwork == network) { networkInUse = true; break; } } if (!networkInUse) { if (DBG) Log.v(TAG, "bringing down unused network: " + networkIntToString(network)); mConnectivityService.teardown(network); } } } /** * Modifies preferredNetworks list according to keys in capabilities. * * @param capabilities The needed LinkCapabilities. * @param preferredNetworks The list of networks to modify. */ private void applyCapabilitiesToNetworks(LinkCapabilities capabilities, List preferredNetworks) { if (DBG) Log.v(TAG, "applyCapabilitiesToNetworks: caps" + capabilities + "nets" + preferredNetworks); // Apply "allowed networks" rule if (capabilities.containsKey(LinkCapabilities.Key.RW_ALLOWED_NETWORKS)) { String allowed = capabilities.get(LinkCapabilities.Key.RW_ALLOWED_NETWORKS); List allowedNetworks = csNetworkStringToList(allowed); // remove all networks that aren't allowed preferredNetworks.retainAll(allowedNetworks); } // Apply "prohibited networks" rule if (capabilities.containsKey(LinkCapabilities.Key.RW_PROHIBITED_NETWORKS)) { String prohibited = capabilities.get(LinkCapabilities.Key.RW_PROHIBITED_NETWORKS); List prohibitedNetworks = csNetworkStringToList(prohibited); preferredNetworks.removeAll(prohibitedNetworks); } if (DBG) Log.v(TAG, "applyCapabilitiesToNetworks resulting nets: " + preferredNetworks); } /** * Converts a comma-separated list of networks (as used in * LinkCapabilities) into a list of network ID's, as defined by * ConnectivityManager. * * @param networks Comma-separated network string, e.g. "wifi, mobile" * @return List of network ID's, e.g. { * ConnectivityManager.NETWORK_WIFI, * ConnectivityManager.NETWORK_MOBILE } */ private List csNetworkStringToList(String networks) { String[] netNames = networks.split(",\\s?"); List netList = new ArrayList(netNames.length); for (String name : netNames) { int network = networkStringToInt(name); if (network != LinkSocketInfo.NETWORK_UNKNOWN) { netList.add(network); } } return netList; } } public int requestLink(LinkCapabilities capabilities, IBinder binder) { int id = getNextSocketId(); if (DBG) Log.v(TAG, "requestLink capabilities: " + capabilities); LinkSocketInfo info = new LinkSocketInfo(); info.id = id; info.capabilities = capabilities; info.callback = ILinkSocketMessageHandler.Stub.asInterface(binder); info.assignedNetwork = LinkSocketInfo.NETWORK_UNKNOWN; info.status = LinkSocketInfo.UNASSIGNED; mActiveLinks.put(info.id, info); Message msg = mHandler.obtainMessage(LMHandler.REQUEST_LINK, id, 0); msg.sendToTarget(); return id; } public void releaseLink(int id) { if (DBG) Log.v(TAG, "releaseLink id: " + id); LinkSocketInfo removed = mActiveLinks.remove(id); Message msg = mHandler.obtainMessage(LMHandler.RELEASE_LINK, removed); msg.sendToTarget(); } public boolean requestQoS(int id, int localPort, String localAddress) { if (DBG) Log.e(TAG, "requestQoS is not supported"); return false; } /** * Minimum available forward link (download) bandwidth for the socket. This value is * in kilobits per second (kbps). */ public String getMinAvailableForwardBandwidth(int id) { // Implementation left to OEMs return null; } /** * Maximum available forward link (download) bandwidth for the socket. This value is * in kilobits per second (kbps). */ public String getMaxAvailableForwardBandwidth(int id) { // Implementation left to OEMs return null; } /** * Minimum available reverse link (upload) bandwidth for the socket. This value is * in kilobits per second (kbps). */ public String getMinAvailableReverseBandwidth(int id) { // Implementation left to OEMs return null; } /** * Maximum available reverse link (upload) bandwidth for the socket. This value is * in kilobits per second (kbps). */ public String getMaxAvailableReverseBandwidth(int id) { // Implementation left to OEMs return null; } /** * Current estimated downlink latency of the socket, in milliseconds. */ public String getCurrentFwdLatency(int id) { // Implementation left to OEMs return null; } /** * Current estimated uplink latency of the socket, in milliseconds. */ public String getCurrentRevLatency(int id) { // Implementation left to OEMs return null; } /** * Current Quality of Service state of the socket */ public String getQosState(int id) { // Implementation left to OEMs return "inactive"; } /** * An integer representing the network type. * @param id LinkSocket ID * @see ConnectivityManager */ public int getNetworkType(int id) { int netType = -1; LinkSocketInfo info = mActiveLinks.get(id); if (info != null) { netType = mActiveLinks.get(id).assignedNetwork; } else { if (DBG) Log.d(TAG, "getNetworkType called with invalid id: " + id); } return netType; } /** * Converts a role string to a role ID (as used in OperatorPolicy.xml) * @param string Role * @return Role ID */ private int roleStringToInt(String roleString) { int roleInt = 0; if (roleString == null) roleInt = 0; else if (roleString.equals(LinkCapabilities.Role.DEFAULT)) roleInt = 0; else if (roleString.equals(LinkCapabilities.Role.VIDEO_STREAMING_1080P)) roleInt = 1; else if (roleString.equals(LinkCapabilities.Role.WEB_BROWSER)) roleInt = 2; else Log.d(TAG, roleString + " is not a known role."); return roleInt; } /** * Converts network names (as used in Carrier Profile, e.g. "WWAN"; or in * LinkCapabilities, e.g. "mobile") to ConnectivityManager constants. * * @param network Name of network * @return The corresponding network constant from ConnectivityManager. */ private int networkStringToInt(String network) { int networkInt = LinkSocketInfo.NETWORK_UNKNOWN; if (network == null) { if (DBG) Log.d(TAG, "networkStringToInt: null string"); } else if (network.equals("WLAN") || network.equals("wifi")) { networkInt = ConnectivityManager.TYPE_WIFI; } else if (network.equals("WWAN") || network.equals("mobile")) { networkInt = ConnectivityManager.TYPE_MOBILE; } else { if (DBG) Log.d(TAG, network + " is not a known network name."); } if (DBG) Log.v(TAG, "networkStringToInt: " + network + ":" + networkInt); return networkInt; } private String networkIntToString(int network) { switch (network) { case ConnectivityManager.TYPE_WIFI: return "WiFi"; case ConnectivityManager.TYPE_MOBILE: return "Mobile"; default: // fall through } return "unknown network " + network; } private boolean isNetworkConnected(int networkType) { if (DBG) Log.v(TAG, "isNetworkConnected: " + networkIntToString(networkType)); NetworkInfo networkInfo = mConnectivityService.getNetworkInfo(networkType); return networkInfo.isConnected(); } // TODO: Find out if there's an easier way to get network addresses. Aren't they saved anywhere already? private LinkProperties propertiesForNetwork(int assignedNetwork) { if (assignedNetwork == ConnectivityManager.TYPE_MOBILE) { return getWwanProperties(); } else if (assignedNetwork == ConnectivityManager.TYPE_WIFI) { return getWlanProperties(); } return null; } private LinkProperties getWwanProperties() { LinkProperties props = new LinkProperties(); if (mTelephonyManager == null) { if (DBG) Log.d(TAG, "mTelephonyManager is null"); return props; } String interfaceName = mTelephonyManager.getActiveInterfaceName(Phone.APN_TYPE_DEFAULT, IPVersion.INET); String addressString = mTelephonyManager.getActiveIpAddress(Phone.APN_TYPE_DEFAULT, IPVersion.INET); try { NetworkInterface networkInterface = NetworkInterface.getByName(interfaceName); props.setInterface(networkInterface); InetAddress address = InetAddress.getByName(addressString); props.addAddress(address); } catch (UnknownHostException e) { if (DBG) Log.d(TAG, "getWwanProperties: Invalid host name " + addressString); } catch (SocketException e) { if (DBG) Log.d(TAG, "could not get network interface for " + interfaceName); } return props; } private LinkProperties getWlanProperties() { LinkProperties props = new LinkProperties(); String ipAddr = null; DhcpInfo dhcpInfo = mWifiManager.getDhcpInfo(); if (dhcpInfo == null) { if (DBG) Log.d(TAG, "DhcpInfo is null. V4 address is not yet available."); return props; } int ipAddressInt = dhcpInfo.ipAddress; if (ipAddressInt != 0) { ipAddr = ((ipAddressInt) & 0xff) + "." + ((ipAddressInt >> 8) & 0xff) + "." + ((ipAddressInt >> 16) & 0xff) + "." + ((ipAddressInt >> 24) & 0xff); } try { InetAddress inetAddr = InetAddress.getByName(ipAddr); NetworkInterface netIface = NetworkInterface.getByInetAddress(inetAddr); props.addAddress(inetAddr); props.setInterface(netIface); } catch (UnknownHostException e) { if (DBG) Log.d(TAG, "getWlanProperties: Invalid host name " + ipAddr); } catch (SocketException e) { if (DBG) Log.d(TAG, "could not get network interface for " + ipAddr); } return props; } private static int mSocketId = 0; synchronized private static int getNextSocketId() { if (mSocketId == Integer.MAX_VALUE) mSocketId = 0; return mSocketId++; } /** * The default connection is specified via Carrier Policy. This API is ignored. */ public void setDefaultConnectionNwPref(int preference) { if (DBG) Log.v(TAG, "setDefaultConnectionNwPref " + preference + " ignored"); } /** * This seems redundant. We should probably remove this API. */ public void sendDefaultNwPref2Cne(int preference) { setDefaultConnectionNwPref(preference); } /* FMC API stubs */ public boolean startFmc(IBinder listener) { return false; } public boolean stopFmc(IBinder listener) { return false; } public boolean getFmcStatus(IBinder listener) { return false; } /* Old LinkProvider API stubs */ public boolean switchLink_LP(int role, int mPid, LinkInfo info, boolean isNotifyBetterLink) { return false; } public boolean reportLinkSatisfaction_LP(int role, int mPid, LinkInfo info, boolean isSatisfied, boolean isNotifyBetterCon) { return false; } public boolean getLink_LP(int role, Map linkReqs, int mPid, IBinder listener) { return false; } public boolean rejectSwitch_LP(int role, int mPid, LinkInfo info, boolean isNotifyBetterLink) { return false; } public boolean releaseLink_LP(int role, int mPid) { return false; } }