diff options
Diffstat (limited to 'wifi')
3 files changed, 312 insertions, 69 deletions
diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityClientCallback.java b/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityClientCallback.java index f62bfd451d5e..d2b9be783bca 100644 --- a/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityClientCallback.java +++ b/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityClientCallback.java @@ -64,5 +64,24 @@ public interface SharedConnectivityClientCallback { * @param status The new status. */ void onKnownNetworkConnectionStatusChanged(@NonNull KnownNetworkConnectionStatus status); + + /** + * This method is being called when the service is ready to be used. + */ + void onServiceConnected(); + + /** + * This method is being called when the service is no longer available. + */ + void onServiceDisconnected(); + + /** + * This method is called when the registration of the callback with the shared connectivity + * service failed. + * + * @param exception The exception received from the system when trying to connect to the + * service. + */ + void onRegisterCallbackFailed(@NonNull Exception exception); } diff --git a/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManager.java b/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManager.java index 8aa369e31ce8..74b42d41d9d1 100644 --- a/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManager.java +++ b/wifi/java/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManager.java @@ -47,6 +47,12 @@ import java.util.concurrent.Executor; * This class is the library used by consumers of Shared Connectivity data to bind to the service, * receive callbacks from, and send user actions to the service. * + * The methods {@link #connectTetherNetwork}, {@link #disconnectTetherNetwork}, + * {@link #connectKnownNetwork} and {@link #forgetKnownNetwork} are not valid and will return false + * if not called between {@link SharedConnectivityClientCallback#onServiceConnected()} + * and {@link SharedConnectivityClientCallback#onServiceDisconnected()} or if + * {@link SharedConnectivityClientCallback#onRegisterCallbackFailed} was called. + * * @hide */ @SystemApi @@ -135,6 +141,10 @@ public class SharedConnectivityManager { private ISharedConnectivityService mService; private final Map<SharedConnectivityClientCallback, SharedConnectivityCallbackProxy> mProxyMap = new HashMap<>(); + private final Map<SharedConnectivityClientCallback, SharedConnectivityCallbackProxy> + mCallbackProxyCache = new HashMap<>(); + // Used for testing + private final ServiceConnection mServiceConnection; /** * Creates a new instance of {@link SharedConnectivityManager}. @@ -164,23 +174,58 @@ public class SharedConnectivityManager { private SharedConnectivityManager(@NonNull Context context, String servicePackageName, String serviceIntentAction) { - ServiceConnection serviceConnection = new ServiceConnection() { + mServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { mService = ISharedConnectivityService.Stub.asInterface(service); + if (!mCallbackProxyCache.isEmpty()) { + synchronized (mCallbackProxyCache) { + mCallbackProxyCache.keySet().forEach(callback -> { + registerCallbackInternal(callback, mCallbackProxyCache.get(callback)); + }); + mCallbackProxyCache.clear(); + } + } } @Override public void onServiceDisconnected(ComponentName name) { if (DEBUG) Log.i(TAG, "onServiceDisconnected"); mService = null; - mProxyMap.clear(); + if (!mCallbackProxyCache.isEmpty()) { + synchronized (mCallbackProxyCache) { + mCallbackProxyCache.keySet().forEach( + SharedConnectivityClientCallback::onServiceDisconnected); + mCallbackProxyCache.clear(); + } + } + if (!mProxyMap.isEmpty()) { + synchronized (mProxyMap) { + mProxyMap.keySet().forEach( + SharedConnectivityClientCallback::onServiceDisconnected); + mProxyMap.clear(); + } + } } }; context.bindService( new Intent().setPackage(servicePackageName).setAction(serviceIntentAction), - serviceConnection, Context.BIND_AUTO_CREATE); + mServiceConnection, Context.BIND_AUTO_CREATE); + } + + private void registerCallbackInternal(SharedConnectivityClientCallback callback, + SharedConnectivityCallbackProxy proxy) { + try { + mService.registerCallback(proxy); + synchronized (mProxyMap) { + mProxyMap.put(callback, proxy); + } + callback.onServiceConnected(); + } catch (RemoteException e) { + Log.e(TAG, "Exception in registerCallback", e); + callback.onRegisterCallbackFailed(e); + } } /** @@ -192,29 +237,45 @@ public class SharedConnectivityManager { } /** + * @hide + */ + @TestApi + @Nullable + public ServiceConnection getServiceConnection() { + return mServiceConnection; + } + + /** * Registers a callback for receiving updates to the list of Tether Networks and Known Networks. + * The {@link SharedConnectivityClientCallback#onRegisterCallbackFailed} will be called if the + * registration failed. * * @param executor The Executor used to invoke the callback. * @param callback The callback of type {@link SharedConnectivityClientCallback} that is invoked * when the service updates either the list of Tether Networks or Known * Networks. - * @return Returns true if the registration was successful, false otherwise. */ - public boolean registerCallback(@NonNull @CallbackExecutor Executor executor, + public void registerCallback(@NonNull @CallbackExecutor Executor executor, @NonNull SharedConnectivityClientCallback callback) { Objects.requireNonNull(executor, "executor cannot be null"); Objects.requireNonNull(callback, "callback cannot be null"); - if (mService == null || mProxyMap.containsKey(callback)) return false; - try { - SharedConnectivityCallbackProxy proxy = - new SharedConnectivityCallbackProxy(executor, callback); - mService.registerCallback(proxy); - mProxyMap.put(callback, proxy); - } catch (RemoteException e) { - Log.e(TAG, "Exception in registerCallback", e); - return false; + + if (mProxyMap.containsKey(callback) || mCallbackProxyCache.containsKey(callback)) { + Log.e(TAG, "Callback already registered"); + callback.onRegisterCallbackFailed(new IllegalStateException( + "Callback already registered")); + return; } - return true; + + SharedConnectivityCallbackProxy proxy = + new SharedConnectivityCallbackProxy(executor, callback); + if (mService == null) { + synchronized (mCallbackProxyCache) { + mCallbackProxyCache.put(callback, proxy); + } + return; + } + registerCallbackInternal(callback, proxy); } /** @@ -225,10 +286,24 @@ public class SharedConnectivityManager { public boolean unregisterCallback( @NonNull SharedConnectivityClientCallback callback) { Objects.requireNonNull(callback, "callback cannot be null"); - if (mService == null || !mProxyMap.containsKey(callback)) return false; + + if (!mProxyMap.containsKey(callback) && !mCallbackProxyCache.containsKey(callback)) { + Log.e(TAG, "Callback not found, cannot unregister"); + return false; + } + + if (mService == null) { + synchronized (mCallbackProxyCache) { + mCallbackProxyCache.remove(callback); + } + return true; + } + try { mService.unregisterCallback(mProxyMap.get(callback)); - mProxyMap.remove(callback); + synchronized (mProxyMap) { + mProxyMap.remove(callback); + } } catch (RemoteException e) { Log.e(TAG, "Exception in unregisterCallback", e); return false; @@ -246,7 +321,12 @@ public class SharedConnectivityManager { * connection was successful. */ public boolean connectTetherNetwork(@NonNull TetherNetwork network) { - if (mService == null) return false; + Objects.requireNonNull(network, "Tether network cannot be null"); + + if (mService == null) { + return false; + } + try { mService.connectTetherNetwork(network); } catch (RemoteException e) { @@ -264,7 +344,10 @@ public class SharedConnectivityManager { * disconnection was successful. */ public boolean disconnectTetherNetwork() { - if (mService == null) return false; + if (mService == null) { + return false; + } + try { mService.disconnectTetherNetwork(); } catch (RemoteException e) { @@ -284,7 +367,12 @@ public class SharedConnectivityManager { * connection was successful. */ public boolean connectKnownNetwork(@NonNull KnownNetwork network) { - if (mService == null) return false; + Objects.requireNonNull(network, "Known network cannot be null"); + + if (mService == null) { + return false; + } + try { mService.connectKnownNetwork(network); } catch (RemoteException e) { @@ -302,7 +390,12 @@ public class SharedConnectivityManager { * forget action was successful. */ public boolean forgetKnownNetwork(@NonNull KnownNetwork network) { - if (mService == null) return false; + Objects.requireNonNull(network, "Known network cannot be null"); + + if (mService == null) { + return false; + } + try { mService.forgetKnownNetwork(network); } catch (RemoteException e) { diff --git a/wifi/tests/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManagerTest.java b/wifi/tests/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManagerTest.java index 784e9c4731ee..815a012ea253 100644 --- a/wifi/tests/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManagerTest.java +++ b/wifi/tests/src/android/net/wifi/sharedconnectivity/app/SharedConnectivityManagerTest.java @@ -23,17 +23,20 @@ import static android.net.wifi.sharedconnectivity.app.KnownNetwork.NETWORK_SOURC import static android.net.wifi.sharedconnectivity.app.TetherNetwork.NETWORK_TYPE_CELLULAR; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.content.ComponentName; import android.content.Context; import android.content.res.Resources; import android.net.wifi.sharedconnectivity.service.ISharedConnectivityService; +import android.os.Parcel; import android.os.RemoteException; import androidx.test.filters.SmallTest; @@ -63,12 +66,9 @@ public class SharedConnectivityManagerTest { private static final String SSID = "TEST_SSID"; private static final int[] SECURITY_TYPES = {SECURITY_TYPE_WEP}; - private static final int SERVICE_PACKAGE_ID = 1; - private static final int SERVICE_CLASS_ID = 2; - private static final String SERVICE_PACKAGE_NAME = "TEST_PACKAGE"; - private static final String SERVICE_CLASS_NAME = "TEST_CLASS"; - private static final String PACKAGE_NAME = "TEST_PACKAGE"; + private static final String SERVICE_INTENT_ACTION = "TEST_INTENT_ACTION"; + @Mock Context mContext; @Mock @@ -77,6 +77,11 @@ public class SharedConnectivityManagerTest { @Mock SharedConnectivityClientCallback mClientCallback; @Mock Resources mResources; + @Mock + ISharedConnectivityService.Stub mIBinder; + + private static final ComponentName COMPONENT_NAME = + new ComponentName("dummypkg", "dummycls"); @Before public void setUp() { @@ -88,155 +93,281 @@ public class SharedConnectivityManagerTest { * Verifies constructor is binding to service. */ @Test - public void testBindingToService() { + public void bindingToService() { SharedConnectivityManager.create(mContext); verify(mContext).bindService(any(), any(), anyInt()); } /** - * Verifies callback is registered in the service only once and only when service is not null. + * Verifies create method returns null when resources are not specified */ @Test - public void testRegisterCallback() throws Exception { + public void resourcesNotDefined() { + when(mResources.getString(anyInt())).thenThrow(new Resources.NotFoundException()); + assertNull(SharedConnectivityManager.create(mContext)); + } + + /** + * Verifies registerCallback behavior. + */ + @Test + public void registerCallback_serviceNotConnected_registrationCachedThenConnected() + throws Exception { SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); manager.setService(null); - assertFalse(manager.registerCallback(mExecutor, mClientCallback)); + manager.registerCallback(mExecutor, mClientCallback); + manager.getServiceConnection().onServiceConnected(COMPONENT_NAME, mIBinder); + // Since the binder is embedded in a proxy class, the call to registerCallback is done on + // the proxy. So instead verifying that the proxy is calling the binder. + verify(mIBinder).transact(anyInt(), any(Parcel.class), any(Parcel.class), anyInt()); + } - manager = SharedConnectivityManager.create(mContext); + @Test + public void registerCallback_serviceNotConnected_canUnregisterAndReregister() { + SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); + manager.setService(null); + manager.registerCallback(mExecutor, mClientCallback); + manager.unregisterCallback(mClientCallback); + manager.registerCallback(mExecutor, mClientCallback); + verify(mClientCallback, never()).onRegisterCallbackFailed(any(Exception.class)); + } + + @Test + public void registerCallback_serviceConnected() throws Exception { + SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); manager.setService(mService); - assertTrue(manager.registerCallback(mExecutor, mClientCallback)); + manager.registerCallback(mExecutor, mClientCallback); verify(mService).registerCallback(any()); + verify(mClientCallback, never()).onRegisterCallbackFailed(any(Exception.class)); + } - // Registering the same callback twice should fail. - manager = SharedConnectivityManager.create(mContext); + @Test + public void registerCallback_doubleRegistration_shouldFail() throws Exception { + SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); manager.setService(mService); manager.registerCallback(mExecutor, mClientCallback); - assertFalse(manager.registerCallback(mExecutor, mClientCallback)); + manager.registerCallback(mExecutor, mClientCallback); + verify(mClientCallback).onRegisterCallbackFailed(any(IllegalStateException.class)); + } - manager = SharedConnectivityManager.create(mContext); + @Test + public void registerCallback_remoteException_shouldFail() throws Exception { + SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); manager.setService(mService); doThrow(new RemoteException()).when(mService).registerCallback(any()); - assertFalse(manager.registerCallback(mExecutor, mClientCallback)); + manager.registerCallback(mExecutor, mClientCallback); + verify(mClientCallback).onRegisterCallbackFailed(any(RemoteException.class)); } /** - * Verifies callback is unregistered from the service if it was registered before and only when - * service is not null. + * Verifies unregisterCallback behavior. */ @Test - public void testUnregisterCallback() throws Exception { + public void unregisterCallback_withoutRegisteringFirst_serviceNotConnected_shouldFail() { SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); manager.setService(null); assertFalse(manager.unregisterCallback(mClientCallback)); + } - manager = SharedConnectivityManager.create(mContext); + @Test + public void unregisterCallback_withoutRegisteringFirst_serviceConnected_shouldFail() { + SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); + manager.setService(mService); + assertFalse(manager.unregisterCallback(mClientCallback)); + } + + @Test + public void unregisterCallback() throws Exception { + SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); manager.setService(mService); manager.registerCallback(mExecutor, mClientCallback); assertTrue(manager.unregisterCallback(mClientCallback)); verify(mService).unregisterCallback(any()); + } - - manager = SharedConnectivityManager.create(mContext); + @Test + public void unregisterCallback_doubleUnregistration_serviceConnected_shouldFail() { + SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); manager.setService(mService); manager.registerCallback(mExecutor, mClientCallback); manager.unregisterCallback(mClientCallback); assertFalse(manager.unregisterCallback(mClientCallback)); + } - manager = SharedConnectivityManager.create(mContext); + @Test + public void unregisterCallback_doubleUnregistration_serviceNotConnected_shouldFail() { + SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); + manager.setService(null); + manager.registerCallback(mExecutor, mClientCallback); + manager.unregisterCallback(mClientCallback); + assertFalse(manager.unregisterCallback(mClientCallback)); + } + + @Test + public void unregisterCallback_remoteException_shouldFail() throws Exception { + SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); manager.setService(mService); doThrow(new RemoteException()).when(mService).unregisterCallback(any()); assertFalse(manager.unregisterCallback(mClientCallback)); } /** - * Verifies service is called when not null and exceptions are handles when calling - * connectTetherNetwork. + * Verifies callback is called when service is connected */ @Test - public void testConnectTetherNetwork() throws RemoteException { - TetherNetwork network = buildTetherNetwork(); + public void onServiceConnected_registerCallbackBeforeConnection() { + SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); + manager.registerCallback(mExecutor, mClientCallback); + manager.getServiceConnection().onServiceConnected(COMPONENT_NAME, mIBinder); + verify(mClientCallback).onServiceConnected(); + } + @Test + public void onServiceConnected_registerCallbackAfterConnection() { + SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); + manager.getServiceConnection().onServiceConnected(COMPONENT_NAME, mIBinder); + manager.registerCallback(mExecutor, mClientCallback); + verify(mClientCallback).onServiceConnected(); + } + + /** + * Verifies callback is called when service is disconnected + */ + @Test + public void onServiceDisconnected_registerCallbackBeforeConnection() { + SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); + manager.registerCallback(mExecutor, mClientCallback); + manager.getServiceConnection().onServiceConnected(COMPONENT_NAME, mIBinder); + manager.getServiceConnection().onServiceDisconnected(COMPONENT_NAME); + verify(mClientCallback).onServiceDisconnected(); + } + + @Test + public void onServiceDisconnected_registerCallbackAfterConnection() { + SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); + manager.getServiceConnection().onServiceConnected(COMPONENT_NAME, mIBinder); + manager.registerCallback(mExecutor, mClientCallback); + manager.getServiceConnection().onServiceDisconnected(COMPONENT_NAME); + verify(mClientCallback).onServiceDisconnected(); + } + + /** + * Verifies connectTetherNetwork behavior. + */ + @Test + public void connectTetherNetwork_serviceNotConnected_shouldFail() { + TetherNetwork network = buildTetherNetwork(); SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); manager.setService(null); assertFalse(manager.connectTetherNetwork(network)); + } - manager = SharedConnectivityManager.create(mContext); + @Test + public void connectTetherNetwork() throws RemoteException { + TetherNetwork network = buildTetherNetwork(); + SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); manager.setService(mService); manager.connectTetherNetwork(network); verify(mService).connectTetherNetwork(network); + } + @Test + public void connectTetherNetwork_remoteException_shouldFail() throws RemoteException { + TetherNetwork network = buildTetherNetwork(); + SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); + manager.setService(mService); doThrow(new RemoteException()).when(mService).connectTetherNetwork(network); assertFalse(manager.connectTetherNetwork(network)); } /** - * Verifies service is called when not null and exceptions are handles when calling - * disconnectTetherNetwork. + * Verifies disconnectTetherNetwork behavior. */ @Test - public void testDisconnectTetherNetwork() throws RemoteException { + public void disconnectTetherNetwork_serviceNotConnected_shouldFail() { SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); manager.setService(null); assertFalse(manager.disconnectTetherNetwork()); + } - manager = SharedConnectivityManager.create(mContext); + @Test + public void disconnectTetherNetwork() throws RemoteException { + SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); manager.setService(mService); manager.disconnectTetherNetwork(); verify(mService).disconnectTetherNetwork(); + } + @Test + public void disconnectTetherNetwork_remoteException_shouldFail() throws RemoteException { + SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); + manager.setService(mService); doThrow(new RemoteException()).when(mService).disconnectTetherNetwork(); assertFalse(manager.disconnectTetherNetwork()); } /** - * Verifies service is called when not null and exceptions are handles when calling - * connectKnownNetwork. + * Verifies connectKnownNetwork behavior. */ @Test - public void testConnectKnownNetwork() throws RemoteException { + public void connectKnownNetwork_serviceNotConnected_shouldFail() throws RemoteException { KnownNetwork network = buildKnownNetwork(); - SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); manager.setService(null); assertFalse(manager.connectKnownNetwork(network)); + } - manager = SharedConnectivityManager.create(mContext); + @Test + public void connectKnownNetwork() throws RemoteException { + KnownNetwork network = buildKnownNetwork(); + SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); manager.setService(mService); manager.connectKnownNetwork(network); verify(mService).connectKnownNetwork(network); + } + @Test + public void connectKnownNetwork_remoteException_shouldFail() throws RemoteException { + KnownNetwork network = buildKnownNetwork(); + SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); + manager.setService(mService); doThrow(new RemoteException()).when(mService).connectKnownNetwork(network); assertFalse(manager.connectKnownNetwork(network)); } /** - * Verifies service is called when not null and exceptions are handles when calling - * forgetKnownNetwork. + * Verifies forgetKnownNetwork behavior. */ @Test - public void testForgetKnownNetwork() throws RemoteException { + public void forgetKnownNetwork_serviceNotConnected_shouldFail() { KnownNetwork network = buildKnownNetwork(); - SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); manager.setService(null); assertFalse(manager.forgetKnownNetwork(network)); + } - manager = SharedConnectivityManager.create(mContext); + @Test + public void forgetKnownNetwork_serviceConnected() throws RemoteException { + KnownNetwork network = buildKnownNetwork(); + SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); manager.setService(mService); manager.forgetKnownNetwork(network); verify(mService).forgetKnownNetwork(network); + } + @Test + public void forgetKnownNetwork_remoteException_shouldFail() throws RemoteException { + KnownNetwork network = buildKnownNetwork(); + SharedConnectivityManager manager = SharedConnectivityManager.create(mContext); + manager.setService(mService); doThrow(new RemoteException()).when(mService).forgetKnownNetwork(network); assertFalse(manager.forgetKnownNetwork(network)); } private void setResources(@Mock Context context) { when(context.getResources()).thenReturn(mResources); - when(context.getPackageName()).thenReturn(PACKAGE_NAME); - when(mResources.getIdentifier(anyString(), anyString(), anyString())) - .thenReturn(SERVICE_PACKAGE_ID, SERVICE_CLASS_ID); - when(mResources.getString(SERVICE_PACKAGE_ID)).thenReturn(SERVICE_PACKAGE_NAME); - when(mResources.getString(SERVICE_CLASS_ID)).thenReturn(SERVICE_CLASS_NAME); + when(mResources.getString(anyInt())) + .thenReturn(SERVICE_PACKAGE_NAME, SERVICE_INTENT_ACTION); } private TetherNetwork buildTetherNetwork() { |