Handle Android 4.4 User Switching
Android 4.4 introduces user switching which results in multiple
AntRadioServices binding to the HAL. This fix should allow this scenario
to work properly, always giving preference to the foreground user and
optionally allowing access to background users when the foreground user
is not using ANT.
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index cfec40f..629ac10 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -16,7 +16,7 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.dsi.ant.server"
- android:versionName="3.2.0"
+ android:versionName="3.2/ft_userswitch/1"
android:versionCode="030200"
android:sharedUserId="android.uid.system">
@@ -74,4 +74,7 @@
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+ <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
+ <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
+ <uses-permission android:name="android.permission.MANAGE_USERS"/>
</manifest>
diff --git a/src/com/dsi/ant/server/AntService.java b/src/com/dsi/ant/server/AntService.java
index 4e6f891..67878c4 100644
--- a/src/com/dsi/ant/server/AntService.java
+++ b/src/com/dsi/ant/server/AntService.java
@@ -23,14 +23,16 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.app.ActivityManagerNative;
import android.app.Service;
import android.os.Binder;
+import android.os.Build;
import android.os.IBinder;
import android.os.RemoteException;
-import android.provider.Settings;
+import android.os.UserHandle;
import android.util.Log;
-import android.os.SystemProperties;
-
import com.dsi.ant.core.*;
import com.dsi.ant.server.AntHalDefine;
@@ -38,12 +40,24 @@
import com.dsi.ant.server.IAntHalCallback;
import com.dsi.ant.server.Version;
+import java.util.HashMap;
+
public class AntService extends Service
{
private static final String TAG = "AntHalService";
private static final boolean DEBUG = false;
+ private static final boolean HAS_MULTI_USER_API =
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1;
+
+ /**
+ * This flag determines if background users are allowed to use the ANT radio or not. Note that
+ * even if this flag is set, the active foreground user always has priority for using the
+ * ANT radio.
+ */
+ private static final boolean ALLOW_BACKGROUND_USAGE = true;
+
public static final String ANT_SERVICE = "AntService";
/**
@@ -82,9 +96,23 @@
/** Callback object for sending events to the upper layers */
private volatile IAntHalCallback mCallback;
- /** Used for synchronizing changes to {@link #mCallback}
- * The synchronization is needed because of how {@link #doUnregisterAntHalCallback(IAntHalCallback)} works */
- private final Object mCallback_LOCK = new Object();
+ /**
+ * Used for synchronizing changes to {@link #mCallback}, {@link #mCallbackMap}, and
+ * {@link #mCurrentUser}. Does not need to be used where a one-time read of the
+ * {@link #mCallback} value is being done, however ALL WRITE ACCESSES must use this lock.
+ */
+ private final Object mUserCallback_LOCK = new Object();
+
+ /**
+ * The user handle associated with the current active user of the ANT HAL service.
+ */
+ private volatile UserHandle mCurrentUser;
+
+ /**
+ * Map containing the callback set for each current user.
+ */
+ private final HashMap<UserHandle, IAntHalCallback> mCallbackMap =
+ new HashMap<UserHandle, IAntHalCallback>();
/**
* Receives Bluetooth State Changed intent and sends {@link ACTION_REQUEST_ENABLE}
@@ -95,12 +123,14 @@
/**
* Receives {@link ACTION_REQUEST_ENABLE} and {@link ACTION_REQUEST_DISABLE}
* intents to enable and disable ANT.
+ * Also receives {@link Intent#ACTION_USER_SWITCHED} when we are not allowing background users
+ * in order to clear the current user at the appropriate time.
*/
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
if (mRequiresBluetoothOn) {
- String action = intent.getAction();
if (ACTION_REQUEST_ENABLE.equals(action)) {
if (mEnablePending) {
asyncSetAntPowerState(true);
@@ -114,6 +144,14 @@
}
}
}
+ if(!ALLOW_BACKGROUND_USAGE)
+ {
+ if(HAS_MULTI_USER_API &&
+ Intent.ACTION_USER_SWITCHED.equals(action))
+ {
+ clearCurrentUser();
+ }
+ }
}
};
@@ -164,10 +202,112 @@
}
/**
+ * Clear the current user, telling the associated ARS instance that the chip is disabled.
+ */
+ private void clearCurrentUser()
+ {
+ if (DEBUG) Log.i(TAG, "Clearing active user");
+ synchronized (mUserCallback_LOCK)
+ {
+ setState(AntHalDefine.ANT_HAL_STATE_DISABLED);
+ mCurrentUser = null;
+ mCallback = null;
+ doSetAntState(AntHalDefine.ANT_HAL_STATE_DISABLED);
+ }
+ }
+
+ /**
+ * Attempt to change the current user to the calling user.
+ * @return True if the calling user is now the current user (even if they were before the call
+ * was made), False otherwise.
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+ private boolean trySwitchToCallingUser()
+ {
+ // Lock held here to avoid ordering issues if it is needed within the function.
+ synchronized (mChangeAntPowerState_LOCK)
+ {
+ synchronized (mUserCallback_LOCK)
+ {
+ UserHandle callingUser = Binder.getCallingUserHandle();
+ if(DEBUG) Log.d(TAG, "Trying to make user: " + callingUser + " the current user.");
+ boolean isActiveUser = false;
+ boolean shouldSwitch = false;
+ long id = 0;
+
+ // Always allow if they already are the current user.
+ if(callingUser.equals(mCurrentUser))
+ {
+ shouldSwitch = true;
+ }
+
+ try
+ {
+ // Check foreground user using ANT HAL Service permissions.
+ id = Binder.clearCallingIdentity();
+ UserHandle activeUser =
+ ActivityManagerNative.getDefault().getCurrentUser().getUserHandle();
+ isActiveUser = activeUser.equals(callingUser);
+ } catch (RemoteException e)
+ {
+ if(DEBUG) Log.w(TAG, "Could not determine the foreground user.");
+ // don't know who the current user is, assume they are not the active user and
+ // continue.
+ } finally
+ {
+ // always restore our identity.
+ Binder.restoreCallingIdentity(id);
+ }
+
+ if(isActiveUser)
+ {
+ // Always allow the active user to become the current user.
+ shouldSwitch = true;
+ }
+
+ if(ALLOW_BACKGROUND_USAGE)
+ {
+ // Allow anyone to become the current user if there is no current user.
+ if(mCurrentUser == null)
+ {
+ shouldSwitch = true;
+ }
+ }
+
+ if(shouldSwitch)
+ {
+ // Only actually do the switch if the users are different.
+ if(!callingUser.equals(mCurrentUser))
+ {
+ if (DEBUG) Log.i(TAG, "Making " + callingUser + " the current user.");
+ // Need to send state updates as the current user switches.
+ // The mChangeAntPowerState_LOCK needs to be held across these calls to
+ // prevent state updates during the user switch. It is held for this entire
+ // function to prevent lock ordering issues.
+ setState(AntHalDefine.ANT_HAL_STATE_DISABLED);
+ mCurrentUser = callingUser;
+ mCallback = mCallbackMap.get(callingUser);
+ setState(doGetAntState(true));
+ } else
+ {
+ if (DEBUG) Log.d(TAG, callingUser + " is already the current user.");
+ }
+ } else
+ {
+ if (DEBUG) Log.d(TAG, callingUser + " is not allowed to become the current user.");
+ }
+
+ return shouldSwitch;
+ }
+ }
+ }
+
+ /**
* Requests to change the state
* @param state The desired state to change to
* @return An {@link AntHalDefine} result
*/
+ @SuppressLint("NewApi")
private int doSetAntState(int state)
{
synchronized(mChangeAntPowerState_LOCK) {
@@ -179,6 +319,17 @@
{
result = AntHalDefine.ANT_HAL_RESULT_FAIL_NOT_ENABLED;
+ // On platforms with multiple users the enable call is where we try to switch
+ // the current user.
+ if(HAS_MULTI_USER_API)
+ {
+ if(!trySwitchToCallingUser())
+ {
+ // If we cannot become the current user, fail the enable call.
+ break;
+ }
+ }
+
boolean waitForBluetoothToEnable = false;
if (mRequiresBluetoothOn) {
@@ -220,7 +371,28 @@
}
case AntHalDefine.ANT_HAL_STATE_DISABLED:
{
- result = asyncSetAntPowerState(false);
+ if(HAS_MULTI_USER_API)
+ {
+ UserHandle user = Binder.getCallingUserHandle();
+ if(!user.equals(mCurrentUser))
+ {
+ // All disables succeed for non current users.
+ result = AntHalDefine.ANT_HAL_RESULT_SUCCESS;
+ break;
+ }
+
+ result = asyncSetAntPowerState(false);
+
+ if(result == AntHalDefine.ANT_HAL_RESULT_SUCCESS &&
+ user.equals(mCurrentUser))
+ {
+ // To match setting the current user in enable.
+ clearCurrentUser();
+ }
+ } else
+ {
+ result = asyncSetAntPowerState(false);
+ }
break;
}
case AntHalDefine.ANT_HAL_STATE_RESET:
@@ -238,11 +410,18 @@
* Queries the native code for state
* @return An {@link AntHalDefine} state
*/
- private int doGetAntState()
+ @SuppressLint("NewApi")
+ private int doGetAntState(boolean internalCall)
{
if(DEBUG) Log.v(TAG, "doGetAntState start");
- int retState = mJAnt.getRadioEnabledStatus(); // ANT state is native state
+ int retState = AntHalDefine.ANT_HAL_STATE_DISABLED;
+ if(!HAS_MULTI_USER_API || // If there is no multi-user api we don't have to fake a disabled state.
+ internalCall ||
+ Binder.getCallingUserHandle().equals(mCurrentUser))
+ {
+ retState = mJAnt.getRadioEnabledStatus(); // ANT state is native state
+ }
if(DEBUG) Log.i(TAG, "Get ANT State = "+ retState +" / "+ AntHalDefine.getAntHalStateString(retState));
@@ -261,7 +440,7 @@
synchronized (mChangeAntPowerState_LOCK) {
// Check we are not already in/transitioning to the state we want
- int currentState = doGetAntState();
+ int currentState = doGetAntState(true);
if (state) {
if ((AntHalDefine.ANT_HAL_STATE_ENABLED == currentState)
@@ -436,33 +615,69 @@
return result;
}
+ @SuppressLint("NewApi")
private int doRegisterAntHalCallback(IAntHalCallback callback)
{
- if(DEBUG) Log.i(TAG, "Registering callback: "+ callback.toString());
-
- synchronized(mCallback_LOCK)
+ synchronized (mUserCallback_LOCK)
{
- mCallback = callback;
+ if(HAS_MULTI_USER_API)
+ {
+ UserHandle user = Binder.getCallingUserHandle();
+ if(DEBUG) Log.i(TAG, "Registering callback: "+ callback + " for user: " + user);
+ mCallbackMap.put(user, callback);
+ if(user.equals(mCurrentUser))
+ {
+ mCallback = callback;
+ }
+ } else
+ {
+ if(DEBUG) Log.i(TAG, "Registering callback: "+ callback);
+ mCallback = callback;
+ }
}
return AntHalDefine.ANT_HAL_RESULT_SUCCESS;
}
+ @SuppressLint("NewApi")
private int doUnregisterAntHalCallback(IAntHalCallback callback)
{
- if(DEBUG) Log.i(TAG, "UNRegistering callback: "+ callback.toString());
-
int result = AntHalDefine.ANT_HAL_RESULT_FAIL_UNKNOWN;
- synchronized (mCallback_LOCK)
+ if(HAS_MULTI_USER_API)
{
- if(mCallback.asBinder() == callback.asBinder())
+ UserHandle user = Binder.getCallingUserHandle();
+ if(DEBUG) Log.i(TAG, "Unregistering callback: "+ callback.toString() + " for user: " +
+ user);
+ synchronized(mUserCallback_LOCK)
{
- mCallback = null;
- result = AntHalDefine.ANT_HAL_RESULT_SUCCESS;
+ IAntHalCallback currentCallback = mCallbackMap.get(user);
+ if(callback != null && currentCallback != null &&
+ callback.asBinder().equals(currentCallback.asBinder()))
+ {
+ mCallbackMap.remove(user);
+ result = AntHalDefine.ANT_HAL_RESULT_SUCCESS;
+ }
+ // Regardless of state, if the current user is leaving we need to allow others to
+ // take over.
+ if(user.equals(mCurrentUser))
+ {
+ clearCurrentUser();
+ }
+ }
+ } else
+ {
+ if(DEBUG) Log.i(TAG, "Unregistering callback: "+ callback.toString());
+ synchronized(mUserCallback_LOCK)
+ {
+ if(callback != null && mCallback != null &&
+ callback.asBinder().equals(mCallback.asBinder()))
+ {
+ mCallback = null;
+ result = AntHalDefine.ANT_HAL_RESULT_SUCCESS;
+ }
}
}
-
return result;
}
@@ -508,7 +723,7 @@
public int getAntState()
{
- return doGetAntState();
+ return doGetAntState(false);
}
public int ANTTxMessage(byte[] message)
@@ -573,6 +788,15 @@
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_REQUEST_ENABLE);
filter.addAction(ACTION_REQUEST_DISABLE);
+ if(HAS_MULTI_USER_API)
+ {
+ if(!ALLOW_BACKGROUND_USAGE)
+ {
+ // If we don't allow background users, we need to monitor user switches to clear the
+ // active user.
+ filter.addAction(Intent.ACTION_USER_SWITCHED);
+ }
+ }
registerReceiver(mReceiver, filter);
if (mRequiresBluetoothOn) {
@@ -601,8 +825,9 @@
}
}
- synchronized (mCallback_LOCK)
+ synchronized(mUserCallback_LOCK)
{
+ mCallbackMap.clear();
mCallback = null;
}
}
@@ -647,9 +872,10 @@
{
if (DEBUG) Log.d(TAG, "onUnbind() entered");
- synchronized (mCallback_LOCK)
+ synchronized(mUserCallback_LOCK)
{
mCallback = null;
+ mCallbackMap.clear();
}
return super.onUnbind(intent);