/*
* Copyright (C) 2010 The Android Open Source Project
* Copyright (C) 2010-2011, The Linux Foundation. All rights reserved.
*
* 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 android.net;
import android.content.Context;
import android.net.LinkCapabilities.Role;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.util.Log;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
/*
* TODO: Capability tracking features are missing.
*/
/**
* An extension of Java sockets. It provides a client-side TCP socket with a way to manage
* wireless connections.
*
* Example Code:
*
* LinkSocket sock = new LinkSocket(instanceOfLinkSocketNotifier);
* LinkCapabilities capabilities = LinkCapabilities.createNeeds(LinkCapabilities.Role.WEB_BROSWER);
* capabilities.put(LinkCapabilities.Key.RW_DESIRED_FWD_BW, "1000"); // set 1Mbps down
* sock.setNeededCapabilities(capabilities); // set the capabilities
* sock.connect("www.google.com", 80); // Google's HTTP server
* sock.close();
*
*
* @see LinkSocketNotifier
* @hide
*/
public class LinkSocket extends Socket {
// Public error codes
/** Link Failure: Unknown failure */
public static final int ERROR_UNKNOWN = 0;
/** Link Failure: All networks are down */
public static final int ERROR_ALL_NETWORKS_DOWN = 1;
/** Link Failure: Networks are available, but none meet requirements */
public static final int ERROR_NETWORKS_FAIL_REQUIREMENTS = 2;
// Debugging
private final static String TAG = "LinkSocket";
private final static boolean DBG = true;
// Flags
private boolean isWaitingForResponse = false; // waiting for ConnectivityServices?
private final static int NOT_SET = -1;
// Members
private IConnectivityManager mService = null; // handle to ConnectivityService
private LinkCapabilities mNeededCapabilities = null; // requested capabilities
private LinkProperties mProperties = null; // socket properties
private LinkSocketNotifier mNotifier = null; // callback to application
private String mHostname = null; // destination hostname
private int mId = NOT_SET; // unique ID
private MessageHandler mMsgHandler = new MessageHandler(); // messages from ConnectivityServices
private Handler mHandler = null; // message handler
private MessageLoop mMsgLoop = new MessageLoop(); // message loop
/**
* Default constructor
*/
public LinkSocket() {
super();
if (DBG) Log.v(TAG, "LinkSocket() EX");
constructor(null);
}
/**
* Creates a new unconnected socket.
*
* @param notifier a reference to a class that implements
* {@code LinkSocketNotifier}
*/
public LinkSocket(LinkSocketNotifier notifier) {
super();
if (DBG) Log.v(TAG, "LinkSocket(notifier) EX");
constructor(notifier);
}
/**
* Creates a new unconnected socket using the given proxy type.
*
* @param notifier a reference to a class that implements
* {@code LinkSocketNotifier}
* @param proxy the specified proxy for this socket
* @throws IllegalArgumentException if the argument proxy is null or of an
* invalid type.
* @throws SecurityException if a security manager exists and it denies the
* permission to connect to the given proxy.
*/
public LinkSocket(LinkSocketNotifier notifier, Proxy proxy) {
super(proxy);
if (DBG) Log.v(TAG, "LinkSocket(notifier, proxy) EX");
constructor(notifier);
}
/**
* Creates a new unconnected socket with the same notifier and needs map as
* the source LinkSocket.
*
* @param source
*/
public LinkSocket(LinkSocket source) {
super();
if (DBG) Log.v(TAG, "LinkSocket(source) EX");
constructor(source.mNotifier);
setNeededCapabilities(source.getNeededCapabilities());
}
/**
* @return the {@code LinkProperties} for the socket
*/
public LinkProperties getLinkProperties() {
if (DBG) Log.v(TAG, "LinkProperties() EX");
return new LinkProperties(mProperties);
}
/**
* Set the {@code LinkCapabilities} needed for this socket. If the socket is
* already connected the request is ignored and {@code false} will be
* returned. A needs map can be created via the {@code createNeedsMap}
* static method.
*
* @param needs the needs of the socket
* @return {@code true} if needs are successfully set, {@code false}
* otherwise
*/
public boolean setNeededCapabilities(LinkCapabilities needs) {
if (DBG) Log.v(TAG, "setNeeds() EX");
// if mProperties is set, it is too late to set needs
if (mProperties != null) return false;
mNeededCapabilities = needs;
mNeededCapabilities.put(LinkCapabilities.Key.RO_TRANSPORT_PROTO_TYPE,"tcp");
if (mNotifier == null) {
// if mNotifier is null, then we cannot send notifications so we
// might as well disable them
mNeededCapabilities.put(LinkCapabilities.Key.RW_DISABLE_NOTIFICATIONS, "true");
}
return true;
}
/**
* @return the LinkCapabilites set by setNeededCapabilities, empty if none
* has been set
*/
public LinkCapabilities getNeededCapabilities() {
if (DBG) Log.v(TAG, "getNeeds() EX");
return new LinkCapabilities(mNeededCapabilities);
}
/**
* @return a LinkCapabilities object containing the READ ONLY capabilities
* of the LinkSocket
*/
public LinkCapabilities getCapabilities() {
if (DBG) Log.v(TAG, "getCapabilities() EX");
LinkCapabilities cap = null;
try {
// place all capabilities in an int array
final int[] keys = new int[] {
LinkCapabilities.Key.RO_MIN_AVAILABLE_FWD_BW,
LinkCapabilities.Key.RO_MAX_AVAILABLE_FWD_BW,
LinkCapabilities.Key.RO_MIN_AVAILABLE_REV_BW,
LinkCapabilities.Key.RO_MAX_AVAILABLE_REV_BW,
LinkCapabilities.Key.RO_CURRENT_FWD_LATENCY,
LinkCapabilities.Key.RO_CURRENT_REV_LATENCY,
LinkCapabilities.Key.RO_BOUND_INTERFACE,
LinkCapabilities.Key.RO_NETWORK_TYPE,
LinkCapabilities.Key.RO_PHYSICAL_INTERFACE,
LinkCapabilities.Key.RO_CARRIER_ROLE,
LinkCapabilities.Key.RO_QOS_STATE
};
cap = mService.requestCapabilities(mId, keys);
} catch (RemoteException ex) {
Log.d(TAG, "LinkSocket was unable to get capabilities from ConnectivityService");
// should not return a null reference
cap = new LinkCapabilities();
}
return cap;
}
/**
* Returns this LinkSockets set of capabilities, filtered according to the
* given {@code Set}. Capabilities in the Set but not available from the
* link will not be reported in the results. Capabilities of the link but
* not listed in the Set will also not be reported in the results.
*
* @param capability_keys {@code Set} of capabilities requested
* @return the filtered {@code LinkCapabilities} of this LinkSocket, may be
* empty
*/
public LinkCapabilities getCapabilities(Set capability_keys) {
if (DBG) Log.v(TAG, "getCapabilities(capabilities) EX");
LinkCapabilities cap = null;
int[] keys = new int[capability_keys.size()];
// convert capability_keys into an int array
Iterator it = capability_keys.iterator();
for (int i = 0; it.hasNext(); i++) keys[i] = it.next();
// send the request
try {
cap = mService.requestCapabilities(mId, keys);
} catch (RemoteException ex) {
Log.d(TAG, "LinkSocket was unable to get capabilities from ConnectivityService");
}
return cap;
}
/**
* Provide the set of capabilities the application is interested in tracking
* for this LinkSocket.
*
* @param capabilities a {@code Set} of capabilities to track
*/
public void setTrackedCapabilities(Set capabilities) {
if (DBG) Log.v(TAG, "setTrackedCapabilities(capabilities) EX");
// This feature is not implemented yet.
}
/**
* @return the {@code LinkCapabilities} that are tracked, empty if
* none has been set.
*/
public Set getTrackedCapabilities() {
if (DBG) Log.v(TAG, "getTrackedCapabilities() EX");
// This feature is not implemented yet.
return new HashSet();
}
/**
* Connects this socket to the given remote host address and port specified
* by dstName and dstPort.
*
* @param dstName the address of the remote host to connect to
* @param dstPort the port to connect to on the remote host
* @param timeout the timeout value in milliseconds or 0 for infinite
* timeout
* @throws UnknownHostException if the given dstName is invalid
* @throws IOException if the socket is already connected or an error occurs
* while connecting
* @throws SocketTimeoutException if the timeout fires
*/
public void connect(String dstName, int dstPort, int timeout) throws UnknownHostException,
IOException, SocketTimeoutException {
if (DBG) Log.v(TAG, "connect(dstName, dstPort, timeout) EX");
if (dstName == null) {
throw new UnknownHostException("destination address is not set");
}
if (dstPort < 0) {
throw new UnknownHostException("destination port is not set");
}
/*
* Currently RW_ALLOWED_NETWORKS and RW_PROHIBITED_NETWORKS are not
* implemented. If either of these keys are in use, throw an
* IOException.
*
* TODO: implement RW_ALLOWED_NETWORKS and RW_PROHIBITED_NETWORKS in
* next release.
*/
if (mNeededCapabilities.containsKey(LinkCapabilities.Key.RW_ALLOWED_NETWORKS)
|| mNeededCapabilities.containsKey(LinkCapabilities.Key.RW_PROHIBITED_NETWORKS)) {
throw new IOException("RW_ALLOWED_NETWORKS and RW_PROHIBITED_NETWORKS" +
" are not supported at this time");
}
// save the current time for timeouts
Calendar start = Calendar.getInstance();
// make sure message processing thread is ready before we try to connect
while (mHandler == null) Thread.yield();
/*
* Steps:
* 1. save the destination address for future use
* 2. request a network link
* 3. figure out which address to bind to
* 4. bind to the given address
* 5. connect
*/
// save the addresses for future use
mHostname = dstName;
// get need a network connection
synchronized (this) {
if (mId == NOT_SET) {
try {
isWaitingForResponse = true;
if (DBG) Log.v(TAG, "sending requestLink()");
mId = mService.requestLink(mNeededCapabilities, mMsgHandler);
if (DBG) Log.v(TAG, "Blocking: waiting for response");
while (isWaitingForResponse) {
if (timeout == 0) {
wait();
} else {
wait(timeout);
/*
* if 'timeout' time passes, wait() will stop
* blocking just as if it was interrupted or
* received a notification, and the while loop will
* begin again. We need track time, and throw and
* exception if appropriate. This also reduces the
* amount of time socket.connect() will wait based
* on how long it took to acquire the link.
*/
timeout -= (int) (Calendar.getInstance().getTimeInMillis()
- start.getTimeInMillis());
if (timeout <= 0) {
releaseLink();
throw new SocketTimeoutException(
"Socket timed out during link acquisition.");
}
}
if (DBG) Log.v(TAG, "Blocking: received notification or timeout");
}
if (DBG) Log.v(TAG, "Blocking: done");
} catch (InterruptedException ex) {
Log.d(TAG, "ConnectivityService failed to respond to request.");
releaseLink();
} catch (RemoteException ex) {
Log.w(TAG, "LinkSocket was unable to acquire a new network link. " + ex);
releaseLink();
}
}
}
// if mProperties is still null, we couldn't get a network
if (mProperties == null) {
releaseLink();
throw new IOException("Unable to find a network that meets requirements.");
}
// get first address from mProperties
Collection addresses = mProperties.getAddresses();
if (addresses == null || addresses.isEmpty()) {
releaseLink();
throw new IOException("No valid address to bind to");
}
InetAddress bindAddress = null;
for (InetAddress address : addresses) {
bindAddress = address;
break;
}
// bind and connect
if (DBG) Log.v(TAG, "attempting to bind: " + bindAddress);
super.bind(new InetSocketAddress(bindAddress, 0));
if (DBG) Log.v(TAG, "bind successful: " + getLocalSocketAddress());
if (DBG) Log.v(TAG, "attempting to connect: " + mHostname + ":" + super.getPort());
//request Qos for all sockets regardless of role type, service will
//handle this request appropriately
try {
mService.requestQoS(mId, super.getLocalPort(), bindAddress.getHostAddress());
} catch (RemoteException re) {
if (DBG) Log.v(TAG,"requestQoS experienced remote exception: " + re);
}
super.connect(new InetSocketAddress(dstName, dstPort), timeout);
if (DBG) Log.v(TAG, "connect successful: " + getInetAddress() + ":" + super.getPort());
}
/**
* Connects this socket to the given remote host address and port specified
* by dstName and dstPort.
*
* @param dstName the address of the remote host to connect to
* @param dstPort the port to connect to on the remote host
* @throws UnknownHostException if the given dstName is invalid
* @throws IOException if the socket is already connected or an error occurs
* while connecting
*/
public void connect(String dstName, int dstPort) throws UnknownHostException, IOException {
if (DBG) Log.v(TAG, "connect(dstName, dstPort) EX");
connect(dstName, dstPort, 0);
}
/**
* Connects this socket to the same remote host address and port as the
* source LinkSocket.
*
* @param source the LinkSocket from which to get the destination
* @param timeout the timeout value in milliseconds or 0 for infinite
* timeout
* @throws UnknownHostException if the given dstName is invalid
* @throws IOException if the socket is already connected or an error occurs
* while connecting
*/
public void connect(LinkSocket source, int timeout) throws UnknownHostException, IOException {
if (DBG) Log.v(TAG, "connect(source) EX");
connect(source.getHostname(), source.getPort(), timeout);
}
/**
* Connects this socket to the same remote host address and port as the
* source LinkSocket.
*
* @param source the LinkSocket from which to get the destination
* @throws UnknownHostException if the given dstName is invalid
* @throws IOException if the socket is already connected or an error occurs
* while connecting
*/
public void connect(LinkSocket source) throws UnknownHostException, IOException {
if (DBG) Log.v(TAG, "connect(source) EX");
connect(source.getHostname(), source.getPort(), 0);
}
/**
* Connects this socket to the given remote host address and port specified
* by the SocketAddress with the specified timeout.
*
* @deprecated Use {@link LinkSocket#connect(String, int, int)} instead.
* Using this method will result in an IllegalArgumentException.
* @param remoteAddr the address and port of the remote host to connect to
* @param timeout the timeout value in milliseconds or 0 for an infinite timeout
* @throws IllegalArgumentException always
*/
@Override
@Deprecated
public void connect(SocketAddress remoteAddr, int timeout) throws IOException,
SocketTimeoutException, IllegalArgumentException {
if (DBG) Log.v(TAG, "connect(remoteAddr, timeout) EX DEPRECATED");
throw new IllegalArgumentException("connect(remoteAddr, timeout) is deprecated");
}
/**
* Connects this socket to the given remote host address and port specified
* by the SocketAddress. Network selection happens during connect and may
* take 30 seconds.
*
* @deprecated Use {@link LinkSocket#connect(String, int)} instead. Using
* this method will result in an IllegalArgumentException.
* @param remoteAddr the address and port of the remote host to connect to.
* @throws IllegalArgumentException always
*/
@Override
@Deprecated
public void connect(SocketAddress remoteAddr) throws IOException, IllegalArgumentException {
if (DBG) Log.v(TAG, "connect(remoteAddr) EX DEPRECATED");
throw new IllegalArgumentException("connect(remoteAddr) is deprecated");
}
/**
* Closes the socket. It is not possible to reconnect or re-bind to this
* socket thereafter which means a new socket instance has to be created.
*
* @throws IOException if an error occurs while closing the socket
*/
@Override
public void close() throws IOException {
if (DBG) Log.v(TAG, "close() EX");
mMsgLoop.quit(); // disable callbacks
releaseLink();
super.close();
}
/**
* @deprecated LinkSocket will automatically pick the optimum interface to
* bind to
* @param localAddr the specific address and port on the local machine to
* bind to
* @throws IOException always as this method is deprecated for LinkSocket
*/
@Override
@Deprecated
public void bind(SocketAddress localAddr) throws UnsupportedOperationException {
if (DBG) Log.v(TAG, "bind(localAddr) EX throws Exception");
throw new UnsupportedOperationException("bind is deprecated for LinkSocket");
}
/**
* Gets the host name of the target host this socket is connected to.
*
* @return the host name of the connected target host, or {@code null} if
* this socket is not yet connected.
*/
public String getHostname() {
return mHostname;
}
/**
* Gets the port number of the target host this socket is connected to.
*
* @return the port number of the connected target host or 0 if this socket
* is not yet connected.
*/
public int getPort() {
return super.getPort();
}
@Override
public String toString() {
if (this.isConnected() == false) {
if (mId == NOT_SET) {
return "LinkSocket id:none unconnected";
} else {
return "LinkSocket id:" + mId + " unconnected";
}
} else {
return "LinkSocket id:" + mId + " addr:" + super.getInetAddress()
+ " port:" + super.getPort() + " local_port:" + super.getLocalPort();
}
}
/*
* Handles callbacks from ConnectivityServices
*/
private class MessageHandler extends ILinkSocketMessageHandler.Stub {
// Flags
private final static int ON_LINK_AVAIL = 0;
private final static int ON_GET_LINK_FAILURE = 1;
private final static int ON_BETTER_LINK_AVAIL = 2;
private final static int ON_LINK_LOST = 3;
private final static int ON_CAPABILITIES_CHANGED = 4;
public void onLinkAvail(LinkProperties properties) {
if (DBG) Log.v(TAG, "CallbackHandler.onLinkAvail(properties) EX");
mHandler.sendMessage(mHandler.obtainMessage(ON_LINK_AVAIL, properties));
}
public void onGetLinkFailure(int reason) {
if (DBG) Log.v(TAG, "CallbackHandler.onGetLinkFailure(reason) EX");
mHandler.sendMessage(mHandler.obtainMessage(ON_GET_LINK_FAILURE, reason));
}
public void onBetterLinkAvail() {
if (DBG) Log.v(TAG, "CallbackHandler.onBetterLinkAvail(properties) EX");
mHandler.sendMessage(mHandler.obtainMessage(ON_BETTER_LINK_AVAIL));
}
public void onLinkLost() {
if (DBG) Log.v(TAG, "CallbackHandler.onLinkLost() EX");
mHandler.sendMessage(mHandler.obtainMessage(ON_LINK_LOST));
}
public void onCapabilitiesChanged(LinkCapabilities changedCapabilities) {
if (DBG) Log.v(TAG, "CallbackHandler.onCapabilitiesChanged(changedCapabilities) EX");
mHandler.sendMessage(
mHandler.obtainMessage(ON_CAPABILITIES_CHANGED, changedCapabilities));
}
}
private void constructor(LinkSocketNotifier notifier) {
if (DBG) Log.v(TAG, "constructor(notifier, proxy) EX");
mMsgLoop.start(); // start up message processing thread
mNotifier = notifier;
setNeededCapabilities(LinkCapabilities.createNeeds(Role.DEFAULT));
mNeededCapabilities.put(LinkCapabilities.Key.RO_TRANSPORT_PROTO_TYPE,"tcp");
IBinder binder = ServiceManager.getService(Context.CONNECTIVITY_SERVICE);
mService = IConnectivityManager.Stub.asInterface(binder);
}
private void releaseLink() {
if (mId == NOT_SET) return; // nothing to release
if (DBG) Log.v(TAG, "releasing link");
try {
mService.releaseLink(mId);
} catch (RemoteException ex) {
Log.w(TAG, "LinkSocket was unable relinquish the current network link. " + ex);
}
mId = NOT_SET;
}
/*
* Handle messages from CallbackHandler
*/
private class MessageLoop extends Thread {
public void run() {
Looper.prepare();
mHandler = new Handler() {
public void handleMessage(Message msg) {
if (DBG) Log.v(TAG, "handleMessage(msg) EX");
switch (msg.what) {
case MessageHandler.ON_LINK_AVAIL:
callbackOnLinkAvail((LinkProperties) msg.obj);
break;
case MessageHandler.ON_GET_LINK_FAILURE:
callbackOnGetLinkFailure((Integer) msg.obj);
break;
case MessageHandler.ON_BETTER_LINK_AVAIL:
callbackOnBetterLinkAvail();
break;
case MessageHandler.ON_LINK_LOST:
callbackOnLinkLost();
break;
case MessageHandler.ON_CAPABILITIES_CHANGED:
callbackOnCapabilitiesChanged((LinkCapabilities) msg.obj);
break;
default:
Log.d(TAG, "LinkSocket received an unknown message type");
}
}
};
Looper.loop();
}
public void quit() {
if (mHandler != null) mHandler.getLooper().quit();
}
}
private void callbackOnLinkAvail(LinkProperties properties) {
if (DBG) Log.v(TAG, "onLinkAvail(properties) EX");
if (mProperties != null) {
/*
* this is an unexpected onLinkAvail(), it probably should be a
* onBetterLinkAvail() so we're going to call that
*
* TODO: remove once CND is updated to the new architecture
*/
callbackOnBetterLinkAvail();
return;
}
// ConnectivityService found a link!
mProperties = properties;
// stop blocking
isWaitingForResponse = false;
synchronized (this) {
notifyAll();
}
}
private void callbackOnGetLinkFailure(int reason) {
if (DBG) Log.v(TAG, "onGetLinkFailure(reason) EX");
if (mProperties != null) {
/*
* this is an unexpected onGetLinkFailure, it probably should just
* be ignored
*
* TODO: remove once CND is updated to the new architecture
*/
return;
}
// implied releaseLink()
mId = NOT_SET;
// stop blocking
isWaitingForResponse = false;
synchronized (this) {
notifyAll();
}
}
private void callbackOnBetterLinkAvail() {
if (DBG) Log.v(TAG, "onBetterLinkAvail() EX");
if (mNotifier == null) return;
// are notifications disabled?
String notify = mNeededCapabilities.get(LinkCapabilities.Key.RW_DISABLE_NOTIFICATIONS);
if (notify != null && notify.equalsIgnoreCase("true")) return;
mNotifier.onBetterLinkAvailable(this);
}
private void callbackOnLinkLost() {
if (DBG) Log.v(TAG, "onLinkLost() EX");
if (mNotifier == null) return;
// are notifications disabled?
String notify = mNeededCapabilities.get(LinkCapabilities.Key.RW_DISABLE_NOTIFICATIONS);
if (notify != null && notify.equalsIgnoreCase("true")) return;
mNotifier.onLinkLost(this);
}
private void callbackOnCapabilitiesChanged(LinkCapabilities changedCapabilities) {
if (DBG) Log.v(TAG, "onCapabilitiesChanged(changedCapabilities) EX");
if (mNotifier == null) return;
// are notifications disabled?
String notify = mNeededCapabilities.get(LinkCapabilities.Key.RW_DISABLE_NOTIFICATIONS);
if (notify != null && notify.equalsIgnoreCase("true")) return;
mNotifier.onCapabilitiesChanged(this, changedCapabilities);
}
}