diff options
author | Etan Cohen <etancohen@google.com> | 2017-01-19 19:55:25 -0800 |
---|---|---|
committer | Etan Cohen <etancohen@google.com> | 2017-01-25 07:32:43 -0800 |
commit | dd4dcab629d1045b08f58f699a4a09ecc8cd23e3 (patch) | |
tree | 509281f562e867bc9c129442c9b5680a0ad102bc | |
parent | 6dada2c3bc67349ae08befd0f1fa76cf7e6ea028 (diff) |
Wi-Fi HAL device manager: baseline for init/start/stop
Wi-Fi HAL device manager. All Wi-Fi services should use this
manager to start/stop and monitor status of Wi-Fi.
Baseline: will be extended to coordinate interface.
Bug: 34474043
Test: unit tests
Change-Id: I3846cb57f301bcd91534f1b5943d996f4c84ed63
-rw-r--r-- | service/Android.mk | 4 | ||||
-rw-r--r-- | service/java/com/android/server/wifi/HalDeviceManager.java | 428 | ||||
-rw-r--r-- | tests/wifitests/src/com/android/server/wifi/HalDeviceManagerTest.java | 270 |
3 files changed, 702 insertions, 0 deletions
diff --git a/service/Android.mk b/service/Android.mk index 3b9eb43bd..33705e430 100644 --- a/service/Android.mk +++ b/service/Android.mk @@ -60,6 +60,10 @@ LOCAL_SRC_FILES := $(call all-java-files-under, java) \ $(call all-proto-files-under, proto) LOCAL_JAVA_LIBRARIES := bouncycastle conscrypt jsr305 services +LOCAL_STATIC_JAVA_LIBRARIES := \ + android.hardware.wifi@1.0-java-static \ + android.hidl.base@1.0-java-static \ + android.hidl.manager@1.0-java-static LOCAL_REQUIRED_MODULES := services LOCAL_MODULE_TAGS := LOCAL_MODULE := wifi-service diff --git a/service/java/com/android/server/wifi/HalDeviceManager.java b/service/java/com/android/server/wifi/HalDeviceManager.java new file mode 100644 index 000000000..da8a993cf --- /dev/null +++ b/service/java/com/android/server/wifi/HalDeviceManager.java @@ -0,0 +1,428 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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 com.android.server.wifi; + +import android.hardware.wifi.V1_0.IWifi; +import android.hardware.wifi.V1_0.IWifiEventCallback; +import android.hardware.wifi.V1_0.WifiStatus; +import android.hardware.wifi.V1_0.WifiStatusCode; +import android.hidl.manager.V1_0.IServiceManager; +import android.hidl.manager.V1_0.IServiceNotification; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.util.Log; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.HashSet; +import java.util.Set; + +/** + * Handles device management through the HAL (HIDL) interface. + */ +public class HalDeviceManager { + private static final String TAG = "HalDeviceManager"; + private static final boolean DBG = true; + + // public API + public HalDeviceManager() { + // empty + } + + /** + * Actually starts the HalDeviceManager: separate from constructor since may want to phase + * at a later time. + * + * TODO: if decide that no need for separating construction from initialization (e.g. both are + * done at injector) then move to constructor. + */ + public void initialize() { + initializeInternal(); + } + + /** + * Register a ManagerStatusCallback to get information about status of Wi-Fi. Use the + * isStarted() method to check status immediately after registration - don't expect callbacks + * for current status (no 'sticky' behavior). + * + * It is safe to re-register the same callback object - duplicates are detected and only a + * single copy kept. + * + * @param callback ManagerStatusCallback callback object. + * @param looper Looper on which to dispatch callbacks. Null implies current looper. + */ + public void registerStatusCallback(ManagerStatusCallback callback, Looper looper) { + synchronized (mLock) { + if (!mManagerStatusCallbacks.add(new ManagerStatusCallbackProxy(callback, + looper == null ? Looper.myLooper() : looper))) { + Log.w(TAG, "registerStatusCallback: duplicate registration ignored"); + } + } + } + + /** + * Returns the current status of Wi-Fi: started (true) or stopped (false). + * + * Note: direct call to HIDL. + */ + public boolean isStarted() { + return isWifiStarted(); + } + + /** + * Attempts to start Wi-Fi (using HIDL). Returns the success (true) or failure (false) or + * the start operation. Will also dispatch any registered ManagerStatusCallback.onStart() on + * success. + * + * Note: direct call to HIDL. + */ + public boolean start() { + return startWifi(); + } + + /** + * Stops Wi-Fi. Will also dispatch any registeredManagerStatusCallback.onStop(). + * + * Note: direct call to HIDL - failure is not-expected. + */ + public void stop() { + stopWifi(); + } + + /** + * HAL device manager status callbacks. + */ + public interface ManagerStatusCallback { + /** + * Indicates that Wi-Fi is up. + */ + void onStart(); + + /** + * Indicates that Wi-Fi is down. + */ + void onStop(); + } + + // internal state + private final Object mLock = new Object(); + + private IServiceManager mServiceManager; + private IWifi mWifi; + private final WifiEventCallback mWifiEventCallback = new WifiEventCallback(); + private final Set<ManagerStatusCallbackProxy> mManagerStatusCallbacks = new HashSet<>(); + + /** + * Wrapper function to access the HIDL services. Created to be mockable in unit-tests. + */ + protected IWifi getWifiServiceMockable() { + try { + return IWifi.getService(); + } catch (RemoteException e) { + Log.e(TAG, "Exception getting IWifi service: " + e); + return null; + } + } + + protected IServiceManager getServiceManagerMockable() { + try { + return IServiceManager.getService(); + } catch (RemoteException e) { + Log.e(TAG, "Exception getting IServiceManager: " + e); + return null; + } + } + + // internal implementation + private void initializeInternal() { + initIServiceManagerIfNecessary(); + } + + /** + * Failures of IServiceManager are most likely system breaking in any case. Behavior here + * will be to WTF and continue. + */ + private void initIServiceManagerIfNecessary() { + if (DBG) Log.d(TAG, "initIServiceManagerIfNecessary"); + + synchronized (mLock) { + if (mServiceManager != null) { + return; + } + + mServiceManager = getServiceManagerMockable(); + if (mServiceManager == null) { + Log.wtf(TAG, "Failed to get IServiceManager instance"); + } else { + try { + if (!mServiceManager.linkToDeath(cookie -> { + Log.wtf(TAG, "IServiceManager died: cookie=" + cookie); + synchronized (mLock) { + mServiceManager = null; + // theoretically can call initServiceManager again here - but + // there's no point since most likely system is going to reboot + } + }, /* don't care */ 0)) { + Log.wtf(TAG, "Error on linkToDeath on IServiceManager"); + mServiceManager = null; + return; + } + + if (!mServiceManager.registerForNotifications(IWifi.kInterfaceName, "", + new IServiceNotification.Stub() { + @Override + public void onRegistration(String fqName, String name, + boolean preexisting) { + Log.d(TAG, "IWifi registration notification: fqName=" + fqName + + ", name=" + name + ", preexisting=" + preexisting); + mWifi = null; // get rid of old copy! + initIWifiIfNecessary(); + } + })) { + Log.wtf(TAG, "Failed to register a listener for IWifi service"); + mServiceManager = null; + } + } catch (RemoteException e) { + Log.wtf(TAG, "Exception while operating on IServiceManager: " + e); + mServiceManager = null; + } + } + } + } + + + /** + * Initialize IWifi and register death listener and event callback. + * + * - It is possible that IWifi is not ready - we have a listener on IServiceManager for it. + * - It is not expected that any of the registrations will fail. Possible indication that + * service died after we obtained a handle to it. + * + * Here and elsewhere we assume that death listener will do the right thing! + */ + private void initIWifiIfNecessary() { + if (DBG) Log.d(TAG, "initIWifiIfNecessary"); + + synchronized (mLock) { + if (mWifi != null) { + return; + } + + try { + mWifi = getWifiServiceMockable(); + if (mWifi == null) { + Log.e(TAG, "IWifi not (yet) available - but have a listener for it ..."); + return; + } + + if (!mWifi.linkToDeath(cookie -> { + Log.e(TAG, "IWifi HAL service died! Have a listener for it ... cookie=" + + cookie); + synchronized (mLock) { // prevents race condition with surrounding method + mWifi = null; + managerStatusCallbackDispatchStop(); + // don't restart: wait for registration notification + } + }, /* don't care */ 0)) { + Log.e(TAG, "Error on linkToDeath on IWifi - will retry later"); + return; + } + + WifiStatus status = mWifi.registerEventCallback(mWifiEventCallback); + if (status.code != WifiStatusCode.SUCCESS) { + Log.e(TAG, "IWifi.registerEventCallback failed: " + statusString(status)); + } + } catch (RemoteException e) { + Log.e(TAG, "Exception while operating on IWifi: " + e); + } + } + } + + private boolean isWifiStarted() { + if (DBG) Log.d(TAG, "isWifiStart"); + + synchronized (mLock) { + try { + if (mWifi == null) { + Log.w(TAG, "isWifiStarted called but mWifi is null!?"); + return false; + } else { + return mWifi.isStarted(); + } + } catch (RemoteException e) { + Log.e(TAG, "isWifiStarted exception: " + e); + return false; + } + } + } + + private boolean startWifi() { + if (DBG) Log.d(TAG, "startWifi"); + + synchronized (mLock) { + try { + if (mWifi == null) { + Log.w(TAG, "startWifi called but mWifi is null!?"); + return false; + } else { + WifiStatus status = mWifi.start(); + boolean success = status.code == WifiStatusCode.SUCCESS; + if (success) { + managerStatusCallbackDispatchStart(); + } else { + Log.e(TAG, "Cannot start IWifi: " + statusString(status)); + } + return success; + } + } catch (RemoteException e) { + Log.e(TAG, "startWifi exception: " + e); + return false; + } + } + } + + private void stopWifi() { + if (DBG) Log.d(TAG, "stopWifi"); + + synchronized (mLock) { + try { + if (mWifi == null) { + Log.w(TAG, "stopWifi called but mWifi is null!?"); + } else { + WifiStatus status = mWifi.stop(); + if (status.code != WifiStatusCode.SUCCESS) { + Log.e(TAG, "Cannot stop IWifi: " + statusString(status)); + } + + // calling onStop for the callbacks even on failure since WTF?? + managerStatusCallbackDispatchStop(); + } + } catch (RemoteException e) { + Log.e(TAG, "stopWifi exception: " + e); + } + } + } + + private class WifiEventCallback extends IWifiEventCallback.Stub { + @Override + public void onStart() throws RemoteException { + if (DBG) Log.d(TAG, "IWifiEventCallback.onStart"); + // NOP: only happens in reaction to my calls - will handle directly + } + + @Override + public void onStop() throws RemoteException { + if (DBG) Log.d(TAG, "IWifiEventCallback.onStop"); + // NOP: only happens in reaction to my calls - will handle directly + } + + @Override + public void onFailure(WifiStatus status) throws RemoteException { + Log.e(TAG, "IWifiEventCallback.onFailure: " + statusString(status)); + managerStatusCallbackDispatchStop(); + + // No need to do anything else: listeners may (will) re-start Wi-Fi + } + } + + private void managerStatusCallbackDispatchStart() { + synchronized (mLock) { + for (ManagerStatusCallbackProxy cb : mManagerStatusCallbacks) { + cb.onStart(); + } + } + } + + private void managerStatusCallbackDispatchStop() { + synchronized (mLock) { + for (ManagerStatusCallbackProxy cb : mManagerStatusCallbacks) { + cb.onStop(); + } + } + } + + private class ManagerStatusCallbackProxy { + private static final int CALLBACK_ON_START = 0; + private static final int CALLBACK_ON_STOP = 1; + + private ManagerStatusCallback mCallback; + private Handler mHandler; + + void onStart() { + mHandler.sendMessage(mHandler.obtainMessage(CALLBACK_ON_START)); + } + + void onStop() { + mHandler.sendMessage(mHandler.obtainMessage(CALLBACK_ON_STOP)); + } + + // override equals & hash to make sure that the container HashSet is unique with respect to + // the contained callback + @Override + public boolean equals(Object obj) { + return mCallback == ((ManagerStatusCallbackProxy) obj).mCallback; + } + + @Override + public int hashCode() { + return mCallback.hashCode(); + } + + ManagerStatusCallbackProxy(ManagerStatusCallback callback, Looper looper) { + mCallback = callback; + mHandler = new Handler(looper) { + @Override + public void handleMessage(Message msg) { + if (DBG) { + Log.d(TAG, "ManagerStatusCallbackProxy.handleMessage: what=" + msg.what); + } + switch (msg.what) { + case CALLBACK_ON_START: + mCallback.onStart(); + break; + case CALLBACK_ON_STOP: + mCallback.onStop(); + break; + default: + Log.e(TAG, + "ManagerStatusCallbackProxy.handleMessage: unknown message " + + "what=" + + msg.what); + } + } + }; + } + } + + private String statusString(WifiStatus status) { + StringBuilder sb = new StringBuilder(); + sb.append(status.code).append(" (").append(status.description).append(")"); + return sb.toString(); + } + + /** + * Dump the internal state of the class. + */ + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("HalDeviceManager:"); + pw.println(" mServiceManager: " + mServiceManager); + pw.println(" mWifi: " + mWifi); + pw.println(" mManagerStatusCallbacks: " + mManagerStatusCallbacks); + } +} diff --git a/tests/wifitests/src/com/android/server/wifi/HalDeviceManagerTest.java b/tests/wifitests/src/com/android/server/wifi/HalDeviceManagerTest.java new file mode 100644 index 000000000..f762ced13 --- /dev/null +++ b/tests/wifitests/src/com/android/server/wifi/HalDeviceManagerTest.java @@ -0,0 +1,270 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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 com.android.server.wifi; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.hardware.wifi.V1_0.IWifi; +import android.hardware.wifi.V1_0.IWifiEventCallback; +import android.hardware.wifi.V1_0.WifiStatus; +import android.hardware.wifi.V1_0.WifiStatusCode; +import android.hidl.manager.V1_0.IServiceManager; +import android.hidl.manager.V1_0.IServiceNotification; +import android.os.IHwBinder; +import android.os.test.TestLooper; +import android.util.Log; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ErrorCollector; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.PrintWriter; +import java.io.StringWriter; + +/** + * Unit test harness for HalDeviceManagerTest. + */ +public class HalDeviceManagerTest { + private HalDeviceManager mDut; + @Mock IServiceManager mServiceManagerMock; + @Mock IWifi mWifiMock; + @Mock HalDeviceManager.ManagerStatusCallback mManagerStatusCallbackMock; + TestLooper mTestLooper; + private ArgumentCaptor<IHwBinder.DeathRecipient> mDeathRecipientCaptor = + ArgumentCaptor.forClass(IHwBinder.DeathRecipient.class); + private ArgumentCaptor<IServiceNotification.Stub> mServiceNotificationCaptor = + ArgumentCaptor.forClass(IServiceNotification.Stub.class); + private ArgumentCaptor<IWifiEventCallback> mWifiEventCallbackCaptor = ArgumentCaptor.forClass( + IWifiEventCallback.class); + private InOrder mInOrder; + @Rule public ErrorCollector collector = new ErrorCollector(); + private WifiStatus mStatusOk; + private WifiStatus mStatusFail; + + private class HalDeviceManagerSpy extends HalDeviceManager { + @Override + protected IWifi getWifiServiceMockable() { + return mWifiMock; + } + + @Override + protected IServiceManager getServiceManagerMockable() { + return mServiceManagerMock; + } + } + + @Before + public void before() throws Exception { + MockitoAnnotations.initMocks(this); + + mTestLooper = new TestLooper(); + + // initialize dummy status objects + mStatusOk = new WifiStatus(); + mStatusOk.code = WifiStatusCode.SUCCESS; + mStatusFail = new WifiStatus(); + mStatusFail.code = WifiStatusCode.ERROR_UNKNOWN; + + when(mServiceManagerMock.linkToDeath(any(IHwBinder.DeathRecipient.class), + anyLong())).thenReturn(true); + when(mServiceManagerMock.registerForNotifications(anyString(), anyString(), + any(IServiceNotification.Stub.class))).thenReturn(true); + when(mWifiMock.linkToDeath(any(IHwBinder.DeathRecipient.class), anyLong())).thenReturn( + true); + when(mWifiMock.registerEventCallback(any(IWifiEventCallback.class))).thenReturn(mStatusOk); + when(mWifiMock.start()).thenReturn(mStatusOk); + when(mWifiMock.stop()).thenReturn(mStatusOk); + + mInOrder = inOrder(mServiceManagerMock, mWifiMock, mManagerStatusCallbackMock); + + mDut = new HalDeviceManagerSpy(); + executeAndValidateInitializationSequence(); + } + + /** + * Print out the dump of the device manager after each test. Not used in test validation + * (internal state) - but can help in debugging failed tests. + */ + @After + public void after() throws Exception { + dumpDut("after: "); + } + + /** + * Test basic startup flow: + * - IServiceManager registrations + * - IWifi registrations + * - IWifi startup delayed + * - Start Wi-Fi -> onStart + * - Stop Wi-Fi -> onStop + */ + @Test + public void testStartStopFlow() throws Exception { + executeAndValidateStartupSequence(); + + // act: stop Wi-Fi + mDut.stop(); + mTestLooper.dispatchAll(); + + // verify: onStop called + mInOrder.verify(mWifiMock).stop(); + mInOrder.verify(mManagerStatusCallbackMock).onStop(); + + verifyNoMoreInteractions(mServiceManagerMock, mWifiMock, mManagerStatusCallbackMock); + } + + /** + * Validate that multiple callback registrations are called and that duplicate ones are + * only called once. + */ + @Test + public void testMultipleCallbackRegistrations() throws Exception { + // register another 2 callbacks - one of them twice + HalDeviceManager.ManagerStatusCallback callback1 = mock( + HalDeviceManager.ManagerStatusCallback.class); + HalDeviceManager.ManagerStatusCallback callback2 = mock( + HalDeviceManager.ManagerStatusCallback.class); + mDut.registerStatusCallback(callback2, mTestLooper.getLooper()); + mDut.registerStatusCallback(callback1, mTestLooper.getLooper()); + mDut.registerStatusCallback(callback2, mTestLooper.getLooper()); + + // startup + executeAndValidateStartupSequence(); + + // verify + verify(callback1).onStart(); + verify(callback2).onStart(); + + verifyNoMoreInteractions(mServiceManagerMock, mWifiMock, mManagerStatusCallbackMock, + callback1, callback2); + } + + /** + * Validate IWifi death listener and registration flow. + */ + @Test + public void testWifiDeathAndRegistration() throws Exception { + executeAndValidateStartupSequence(); + + // act: IWifi service death + mDeathRecipientCaptor.getValue().serviceDied(0); + mTestLooper.dispatchAll(); + + // verify: getting onStop + mInOrder.verify(mManagerStatusCallbackMock).onStop(); + + // act: service startup + mServiceNotificationCaptor.getValue().onRegistration(IWifi.kInterfaceName, "", false); + + // verify: initialization of IWifi + mInOrder.verify(mWifiMock).linkToDeath(mDeathRecipientCaptor.capture(), anyLong()); + mInOrder.verify(mWifiMock).registerEventCallback(mWifiEventCallbackCaptor.capture()); + + // act: start + mDut.start(); + mWifiEventCallbackCaptor.getValue().onStart(); + mTestLooper.dispatchAll(); + + // verify: service and callback calls + mInOrder.verify(mWifiMock).start(); + mInOrder.verify(mManagerStatusCallbackMock).onStart(); + + verifyNoMoreInteractions(mServiceManagerMock, mWifiMock, mManagerStatusCallbackMock); + } + + /** + * Validate IWifi onFailure causes notification + */ + @Test + public void testWifiFail() throws Exception { + executeAndValidateStartupSequence(); + + // act: IWifi failure + mWifiEventCallbackCaptor.getValue().onFailure(mStatusFail); + mTestLooper.dispatchAll(); + + // verify: getting onStop + mInOrder.verify(mManagerStatusCallbackMock).onStop(); + + // act: start again + mDut.start(); + mWifiEventCallbackCaptor.getValue().onStart(); + mTestLooper.dispatchAll(); + + // verify: service and callback calls + mInOrder.verify(mWifiMock).start(); + mInOrder.verify(mManagerStatusCallbackMock).onStart(); + + verifyNoMoreInteractions(mServiceManagerMock, mWifiMock, mManagerStatusCallbackMock); + } + + + // utilities + private void dumpDut(String prefix) { + StringWriter sw = new StringWriter(); + mDut.dump(null, new PrintWriter(sw), null); + Log.e("HalDeviceManager", prefix + sw.toString()); + } + + private void executeAndValidateInitializationSequence() throws Exception { + // act: + mDut.initialize(); + + // verify: service manager initialization sequence + mInOrder.verify(mServiceManagerMock).linkToDeath(any(IHwBinder.DeathRecipient.class), + anyLong()); + mInOrder.verify(mServiceManagerMock).registerForNotifications(eq(IWifi.kInterfaceName), + eq(""), mServiceNotificationCaptor.capture()); + + // act: get the service started (which happens even when service was already up) + mServiceNotificationCaptor.getValue().onRegistration(IWifi.kInterfaceName, "", true); + + // verify: wifi initialization sequence + mInOrder.verify(mWifiMock).linkToDeath(mDeathRecipientCaptor.capture(), anyLong()); + mInOrder.verify(mWifiMock).registerEventCallback(mWifiEventCallbackCaptor.capture()); + } + + private void executeAndValidateStartupSequence() throws Exception { + // act: register listener & start Wi-Fi + mDut.registerStatusCallback(mManagerStatusCallbackMock, mTestLooper.getLooper()); + mDut.start(); + + // verify + mInOrder.verify(mWifiMock).start(); + + // act: trigger onStart callback of IWifiEventCallback + mWifiEventCallbackCaptor.getValue().onStart(); + mTestLooper.dispatchAll(); + + // verify: onStart called on registered listener + mInOrder.verify(mManagerStatusCallbackMock).onStart(); + } +} |