blob: 8c93a14df5e429935d813d680eed6adfd5b7007c [file] [log] [blame]
package com.android.clockwork.bluetooth;
import static com.android.clockwork.bluetooth.proxy.WearProxyConstants.PROXY_UUID;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.WorkerThread;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.IBluetooth;
import android.bluetooth.IBluetoothManagerCallback;
import android.content.Context;
import android.net.ConnectivityManager;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.RemoteException;
import android.util.Log;
import com.android.clockwork.bluetooth.proxy.ProxyNetworkAgent;
import com.android.clockwork.bluetooth.proxy.ProxyServiceHelper;
import com.android.clockwork.bluetooth.proxy.WearProxyConstants.Reason;
import com.android.clockwork.common.DebugAssert;
import com.android.clockwork.common.Util;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import java.io.Closeable;
import java.lang.reflect.Method;
/**
* Manages connection to the companion sysproxy network
*
* This class handles connecting to the remote device using the
* bluetooth network and configuring the sysproxy to setup the
* proper network to allow IP traffic to be utilized by Android.
*
* Steps to connect to the companion sysproxy.
*
* 1. Get a bluetooth rfcomm socket.
* This will actually establish a bluetooth connection from the device to the companion.
* 2. Pass this rfcomm socket to the sysproxy module.
* The sysproxy module will formulate the necessary network configuration to allow
* IP traffic to flow over the bluetooth socket connection.
* 3. Get acknowledgement that the sysproxy module initialized.
* This may or may not be completed successfully as indicated by the jni callback
* indicating connection or failure.
*/
public class CompanionProxyShard implements Closeable {
private static final String TAG = WearBluetoothConstants.LOG_TAG;
private static final int WHAT_START_SYSPROXY = 1;
private static final int WHAT_JNI_ACTIVE_NETWORK_STATE = 2;
private static final int WHAT_JNI_DISCONNECTED = 3;
private static final int WHAT_RESET_CONNECTION = 4;
private static final int TYPE_RFCOMM = 1;
private static final int SEC_FLAG_ENCRYPT = 1 << 0;
private static final int SEC_FLAG_AUTH = 1 << 1;
// Relative unitless network retry values
private static final int BACKOFF_BASE_INTERVAL = 2;
private static final int BACKOFF_BASE_PERIOD = 5;
private static final int BACKOFF_MAX_INTERVAL = 300;
private static final int IS_NOT_METERED = 0;
private static final int IS_METERED = 1;
private static final boolean IS_CONNECTED = true;
private static final boolean IS_DISCONNECTED = !IS_CONNECTED;
private static final boolean PHONE_WITH_INTERNET = true;
private static final boolean PHONE_NO_INTERNET = !PHONE_WITH_INTERNET;
private static int sInstance;
/** An async disconnect request is outstanding and waiting for a JNI response */
private static boolean sWaitingForAsyncDisconnectResponse;
static native void classInitNative();
@VisibleForTesting native boolean connectNative(int fd);
@VisibleForTesting native boolean disconnectNative();
static {
try {
System.loadLibrary("wear-bluetooth-jni");
classInitNative();
} catch (UnsatisfiedLinkError e) {
// Invoked during testing
Log.e(TAG, "Unable to load wear bluetooth sysproxy jni native"
+ " libraries");
}
}
private final int mInstance;
@VisibleForTesting int mStartAttempts;
/**
* Current active value of {@link ConnectivityManager#TYPE} from
* {@link NetworkInfo#getType} on phone.
**/
@interface NetworkType { }
@NetworkType
@VisibleForTesting int mNetworkType;
private boolean mIsSysproxyConnected;
private boolean mIsMetered;
private boolean mIsConnected;
private boolean mPhoneNoInternet;
/** The sysproxy network state
*
* The sysproxy network may be in one of the following states:
*
* 1. Disconnected from phone.
* There is no bluetooth rfcomm socket connected to the phone.
* 2. Connected with Internet access via phone.
* There is a valid rfcomm socket connected to phone and the phone has a default
* network that had validated access to the Internet.
* 3. Connected without Internet access via phone.
* There is a vaid rfcomm socket connected to phone but the phone has no validated
* network to the Internet.
*/
private enum SysproxyNetworkState {
DISCONNECTED,
CONNECTED_NO_INTERNET,
CONNECTED_WITH_INTERNET,
}
@NonNull private final Context mContext;
@NonNull private final ProxyServiceHelper mProxyServiceHelper;
@NonNull private final BluetoothDevice mCompanionDevice;
@NonNull private final Listener mListener;
private final MultistageExponentialBackoff mReconnectBackoff;
@VisibleForTesting boolean mIsClosed;
/**
* Callback executed when the sysproxy connection changes state.
*
* This may send duplicate disconnect events, because failed reconnect
* attempts are indistinguishable from actual disconnects.
* Listeners should appropriately deduplicate these disconnect events.
*/
public interface Listener {
void onProxyConnectionChange(boolean isConnected, int proxyScore, boolean phoneNoInternet);
}
public CompanionProxyShard(
@NonNull final Context context,
@NonNull final ProxyServiceHelper proxyServiceHelper,
@NonNull final BluetoothDevice companionDevice,
@NonNull final Listener listener,
final int networkScore) {
DebugAssert.isMainThread();
mContext = context;
mProxyServiceHelper = proxyServiceHelper;
mCompanionDevice = companionDevice;
mListener = listener;
mInstance = sInstance++;
mReconnectBackoff = new MultistageExponentialBackoff(BACKOFF_BASE_INTERVAL,
BACKOFF_BASE_PERIOD, BACKOFF_MAX_INTERVAL);
maybeLogDebug("Created companion proxy shard");
mProxyServiceHelper.setCompanionName(companionDevice.getName());
mProxyServiceHelper.setNetworkScore(networkScore);
startNetwork();
}
/** Completely shuts down companion proxy network */
@MainThread
@Override // Closable
public void close() {
DebugAssert.isMainThread();
if (mIsClosed) {
Log.w(TAG, logInstance() + "Already closed");
return;
}
updateAndNotify(SysproxyNetworkState.DISCONNECTED, Reason.CLOSABLE);
// notify mListener of our intended disconnect before setting mIsClosed to true
mIsClosed = true;
disconnectNativeInBackground();
mHandler.removeMessages(WHAT_START_SYSPROXY);
maybeLogDebug("Closed companion proxy shard");
}
@MainThread
public void startNetwork() {
DebugAssert.isMainThread();
maybeLogDebug("startNetwork()");
mHandler.sendEmptyMessage(WHAT_START_SYSPROXY);
}
@MainThread
public void updateNetwork(final int networkScore) {
DebugAssert.isMainThread();
mProxyServiceHelper.setNetworkScore(networkScore);
notifyConnectionChange(mIsConnected, mPhoneNoInternet);
}
/** Serialize state change requests here */
@VisibleForTesting
final Handler mHandler = new Handler() {
@MainThread
@Override
public void handleMessage(Message msg) {
DebugAssert.isMainThread();
switch (msg.what) {
case WHAT_START_SYSPROXY:
mHandler.removeMessages(WHAT_START_SYSPROXY);
if (mIsClosed) {
maybeLogDebug("start sysproxy but shard closed...will bail");
return;
} else if (sWaitingForAsyncDisconnectResponse) {
maybeLogDebug("waiting for sysproxy to disconnect...will retry");
mHandler.sendEmptyMessage(WHAT_RESET_CONNECTION);
return;
}
mStartAttempts++;
if (connectedWithInternet()) {
maybeLogDebug("start sysproxy already running set connected");
updateAndNotify(SysproxyNetworkState.CONNECTED_WITH_INTERNET,
Reason.SYSPROXY_WAS_CONNECTED);
} else if (connectedNoInternet()) {
maybeLogDebug("start sysproxy already running but with no internet access");
updateAndNotify(SysproxyNetworkState.CONNECTED_NO_INTERNET,
Reason.SYSPROXY_NO_INTERNET);
} else {
maybeLogDebug("start up new sysproxy connection");
connectSysproxyInBackground();
}
break;
case WHAT_JNI_ACTIVE_NETWORK_STATE:
if (mIsClosed) {
maybeLogDebug("JNI onActiveNetworkState shard closed...will bail");
return;
}
mNetworkType = msg.arg1;
mIsMetered = msg.arg2 == IS_METERED;
mIsSysproxyConnected = true;
if (connectedWithInternet()) {
updateAndNotify(SysproxyNetworkState.CONNECTED_WITH_INTERNET,
Reason.SYSPROXY_CONNECTED);
mProxyServiceHelper.setMetered(mIsMetered);
} else if (connectedNoInternet()) {
updateAndNotify(SysproxyNetworkState.CONNECTED_NO_INTERNET,
Reason.SYSPROXY_NO_INTERNET);
}
mReconnectBackoff.reset();
maybeLogDebug("JNI sysproxy process complete networkType:" + mNetworkType
+ " metered:" + mIsMetered);
break;
case WHAT_JNI_DISCONNECTED:
final int status = msg.arg1;
mIsSysproxyConnected = false;
sWaitingForAsyncDisconnectResponse = false;
maybeLogDebug("JNI onDisconnect isClosed:" + mIsClosed + " status:" + status);
updateAndNotify(SysproxyNetworkState.DISCONNECTED,
Reason.SYSPROXY_DISCONNECTED);
setUpRetryIfNotClosed();
break;
case WHAT_RESET_CONNECTION:
// Take a hammer to reset everything on sysproxy side to initial state.
maybeLogDebug("Reset companion proxy network connection isClosed:" + mIsClosed);
mHandler.removeMessages(WHAT_START_SYSPROXY);
mHandler.removeMessages(WHAT_RESET_CONNECTION);
disconnectNativeInBackground();
setUpRetryIfNotClosed();
break;
}
}
};
private void setUpRetryIfNotClosed() {
if (!mIsClosed) {
final int nextRetry = mReconnectBackoff.getNextBackoff();
mHandler.sendEmptyMessageDelayed(WHAT_START_SYSPROXY, nextRetry * 1000);
Log.w(TAG, logInstance() + "Attempting reconnect in " + nextRetry + " seconds");
}
}
@MainThread
private void updateAndNotify(final SysproxyNetworkState state, final String reason) {
DebugAssert.isMainThread();
if (state == SysproxyNetworkState.CONNECTED_WITH_INTERNET) {
mProxyServiceHelper.startNetworkSession(reason,
/**
* Called when the current network agent is no longer wanted
* by {@link ConnectivityService}. Try to restart the network.
*/
new ProxyNetworkAgent.Listener() {
@Override
public void onNetworkAgentUnwanted(int netId) {
Log.d(TAG, "Network agent unwanted netId:" + netId);
startNetwork();
}
});
notifyConnectionChange(IS_CONNECTED, PHONE_WITH_INTERNET);
} else if (state == SysproxyNetworkState.CONNECTED_NO_INTERNET) {
mProxyServiceHelper.stopNetworkSession(reason);
notifyConnectionChange(IS_CONNECTED, PHONE_NO_INTERNET);
} else {
mProxyServiceHelper.stopNetworkSession(reason);
notifyConnectionChange(IS_DISCONNECTED);
}
}
/**
* Request an rfcomm socket to the companion device.
*
* Connect to the companion device with a bluetooth rfcomm socket.
* The integer filedescriptor portion of the {@link ParcelFileDescriptor}
* is used to pass to the sysproxy native code via
* {@link CompanionProxyShard#connectNativeInBackground}
*
* Failures in any of these steps enter into a delayed retry mode.
*/
@MainThread
private void connectSysproxyInBackground() {
DebugAssert.isMainThread();
maybeLogDebug("Retrieving bluetooth network socket");
new DefaultPriorityAsyncTask<Void, Void, ParcelFileDescriptor>() {
@Override
protected ParcelFileDescriptor doInBackgroundDefaultPriority() {
try {
final IBluetooth bluetoothProxy = getBluetoothService(mInstance);
if (bluetoothProxy == null) {
Log.e(TAG, logInstance() + "Unable to get binder proxy to IBluetooth");
return null;
}
ParcelFileDescriptor parcelFd = bluetoothProxy.getSocketManager().connectSocket(
mCompanionDevice,
TYPE_RFCOMM,
PROXY_UUID,
0 /* port */,
SEC_FLAG_AUTH | SEC_FLAG_ENCRYPT
);
maybeLogDebug("parcelFd:" + parcelFd);
return parcelFd;
} catch (RemoteException e) {
Log.e(TAG, logInstance() + "Unable to get bluetooth service", e);
return null;
}
}
@Override
protected void onPostExecute(@Nullable ParcelFileDescriptor parcelFd) {
DebugAssert.isMainThread();
if (mIsClosed) {
maybeLogDebug("Shard closed after retrieving bluetooth socket");
Util.close(parcelFd);
return;
} else if (parcelFd != null) {
final int fd = parcelFd.detachFd();
maybeLogDebug("Retrieved bluetooth network socket parcelFd:" + parcelFd
+ " fd:" + fd);
connectNativeInBackground(fd);
} else {
Log.e(TAG, logInstance() + "Unable to request bluetooth network socket");
mHandler.sendEmptyMessage(WHAT_RESET_CONNECTION);
}
Util.close(parcelFd);
}
}.execute();
}
/**
* Pass connected socket to sysproxy module.
*
* Hand off a connected socket to the native sysproxy code to
* provide bidirectional network connectivity for the system.
*/
@MainThread
private void connectNativeInBackground(Integer fd) {
DebugAssert.isMainThread();
new PassSocketAsyncTask() {
@Override
protected Boolean doInBackgroundDefaultPriority(Integer fileDescriptor) {
Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
final int fd = fileDescriptor.intValue();
maybeLogDebug("connectNativeInBackground fd:" + fd);
final boolean rc = connectNative(fd);
return new Boolean(rc);
}
@Override
protected void onPostExecute(Boolean result) {
DebugAssert.isMainThread();
if (mIsClosed) {
maybeLogDebug("Shard closed after sending bluetooth socket");
return;
}
if (result) {
maybeLogDebug("proxy socket delivered fd:" + fd);
} else {
Log.w(TAG, logInstance() + "Unable to deliver socket to sysproxy module");
mHandler.sendEmptyMessage(WHAT_RESET_CONNECTION);
}
}
}.execute(fd);
}
/**
* Disconnect the current sysproxy network session.
*
* Inform the native sysproxy module to teardown the current sysproxy session.
*/
@MainThread
private void disconnectNativeInBackground() {
DebugAssert.isMainThread();
if (!mIsSysproxyConnected) {
maybeLogDebug("JNI has already disconnected");
return;
}
new DefaultPriorityAsyncTask<Void, Void, Boolean>() {
@Override
protected Boolean doInBackgroundDefaultPriority() {
maybeLogDebug("JNI Disconnect request to sysproxy module");
return disconnectNative();
}
@MainThread
@Override
protected void onPostExecute(Boolean result) {
DebugAssert.isMainThread();
// Double check if sysproxy is still connected and did not
// initiate a disconnect during this async operation.
// see: bug 111653688
if (mIsSysproxyConnected) {
sWaitingForAsyncDisconnectResponse = result;
}
maybeLogDebug("JNI Disconnect response result:" + result
+ " mIsSysproxyConnected:" + mIsSysproxyConnected
+ " isClosed:" + mIsClosed);
}
}.execute();
}
/**
* This method is called from JNI in a background thread when the companion proxy
* network state changes on the phone.
*/
@WorkerThread
protected void onActiveNetworkState(@NetworkType final int networkType,
final boolean isMetered) {
mHandler.sendMessage(mHandler.obtainMessage(WHAT_JNI_ACTIVE_NETWORK_STATE, networkType,
isMetered ? IS_METERED : IS_NOT_METERED));
}
/** This method is called from JNI in a background thread when the proxy has disconnected. */
@WorkerThread
protected void onDisconnect(final int status) {
mHandler.sendMessage(mHandler.obtainMessage(WHAT_JNI_DISCONNECTED, status, 0));
}
/**
* This method notifies the listener about the state of the sysproxy network.
*
* NOTE: CompanionProxyShard should never call onProxyConnectionChange directly!
* Use the notifyConnectionChange method instead.
*/
@MainThread
private void notifyConnectionChange(final boolean isConnected) {
DebugAssert.isMainThread();
notifyConnectionChange(isConnected, false);
}
@MainThread
private void notifyConnectionChange(final boolean isConnected, final boolean phoneNoInternet) {
DebugAssert.isMainThread();
mIsConnected = isConnected;
mPhoneNoInternet = phoneNoInternet;
doNotifyConnectionChange(mProxyServiceHelper.getNetworkScore());
}
@MainThread
private void doNotifyConnectionChange(final int networkScore) {
DebugAssert.isMainThread();
if (!mIsClosed) {
mListener.onProxyConnectionChange(mIsConnected, networkScore, mPhoneNoInternet);
}
}
private boolean connectedWithInternet() {
return mIsSysproxyConnected && mNetworkType != ConnectivityManager.TYPE_NONE;
}
private boolean connectedNoInternet() {
return mIsSysproxyConnected && mNetworkType == ConnectivityManager.TYPE_NONE;
}
private abstract static class DefaultPriorityAsyncTask<Params, Progress, Result>
extends AsyncTask<Params, Progress, Result> {
@Override
protected Result doInBackground(Params... params) {
Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
return doInBackgroundDefaultPriority();
}
protected abstract Result doInBackgroundDefaultPriority();
}
private abstract static class PassSocketAsyncTask extends AsyncTask<Integer, Void, Boolean> {
@Override
protected Boolean doInBackground(Integer... params) {
Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
final Integer fileDescriptor = params[0];
return doInBackgroundDefaultPriority(fileDescriptor);
}
protected abstract Boolean doInBackgroundDefaultPriority(Integer fd);
}
/** Returns the shared instance of IBluetooth using reflection (method is package private). */
private static IBluetooth getBluetoothService(final int instance) {
try {
final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
final Method getBluetoothService = adapter.getClass()
.getDeclaredMethod("getBluetoothService", IBluetoothManagerCallback.class);
getBluetoothService.setAccessible(true);
return (IBluetooth) getBluetoothService.invoke(adapter, new Object[] { null });
} catch (Exception e) {
Log.e(TAG, "CompanionProxyShard [ " + instance + " ] Error retrieving IBluetooth: ", e);
return null;
}
}
private void maybeLogDebug(final String msg) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, logInstance() + msg);
}
}
private String logInstance() {
return "CompanionProxyShard [ " + mInstance + " ] ";
}
public void dump(@NonNull final IndentingPrintWriter ipw) {
ipw.printf("Companion proxy [ %s ] %s", mCompanionDevice, mCompanionDevice.getName());
ipw.println();
ipw.increaseIndent();
ipw.printPair("isClosed", mIsClosed);
ipw.printPair("Start attempts", mStartAttempts);
ipw.printPair("Start connection scheduled", mHandler.hasMessages(WHAT_START_SYSPROXY));
ipw.printPair("Instance", mInstance);
ipw.printPair("networkTypeName", ConnectivityManager.getNetworkTypeName(mNetworkType));
ipw.printPair("isMetered", mIsMetered);
ipw.printPair("isSysproxyConnected", mIsSysproxyConnected);
ipw.println();
mProxyServiceHelper.dump(ipw);
ipw.decreaseIndent();
}
}