diff options
| author | 2021-11-05 14:32:23 -0700 | |
|---|---|---|
| committer | 2021-11-18 04:48:07 +0000 | |
| commit | 999f26ca03f1a02967e42286c53b64161b49df0c (patch) | |
| tree | 4d534d688832da75ec073d50a17949af4ae7add3 | |
| parent | 6a2929d6ceaf6d0efd7a4f05d583e9185150b441 (diff) | |
health: BatteryService etc. use health AIDL HAL
Also add tests to ensure that HealthServiceWrapper.create
prioritize using the AIDL HAL, and service callbacks are
working.
Test: frameworks test
Test: install HAL on cuttlefish then boot
Bug: 177269435
Change-Id: Ic667a49528c63343fe602f193c0958f2ee57c04e
4 files changed, 450 insertions, 15 deletions
diff --git a/services/core/java/com/android/server/health/HealthRegCallbackAidl.java b/services/core/java/com/android/server/health/HealthRegCallbackAidl.java new file mode 100644 index 000000000000..629011a86bb4 --- /dev/null +++ b/services/core/java/com/android/server/health/HealthRegCallbackAidl.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2021 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.health; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.hardware.health.HealthInfo; +import android.hardware.health.IHealth; +import android.hardware.health.IHealthInfoCallback; +import android.os.RemoteException; +import android.os.Trace; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; + +/** + * On service registration, {@link #onRegistration} is called, which registers {@code this}, an + * {@link IHealthInfoCallback}, to the health service. + * + * <p>When the health service has updates to health info via {@link IHealthInfoCallback}, {@link + * HealthInfoCallback#update} is called. + * + * <p>AIDL variant of {@link HealthHalCallbackHidl}. + * + * @hide + */ +// It is made public so Mockito can access this class. It should have been package private if not +// for testing. +@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) +public class HealthRegCallbackAidl { + private static final String TAG = "HealthRegCallbackAidl"; + private final HealthInfoCallback mServiceInfoCallback; + private final IHealthInfoCallback mHalInfoCallback = new HalInfoCallback(); + + HealthRegCallbackAidl(@Nullable HealthInfoCallback healthInfoCallback) { + mServiceInfoCallback = healthInfoCallback; + } + + /** + * Called when the service manager sees {@code newService} replacing {@code oldService}. + * This unregisters the health info callback from the old service (ignoring errors), then + * registers the health info callback to the new service. + * + * @param oldService the old IHealth service + * @param newService the new IHealth service + */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + public void onRegistration(@Nullable IHealth oldService, @NonNull IHealth newService) { + if (mServiceInfoCallback == null) return; + + Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "HealthUnregisterCallbackAidl"); + try { + unregisterCallback(oldService, mHalInfoCallback); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); + } + + Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "HealthRegisterCallbackAidl"); + try { + registerCallback(newService, mHalInfoCallback); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); + } + } + + private static void unregisterCallback(@Nullable IHealth oldService, IHealthInfoCallback cb) { + if (oldService == null) return; + try { + oldService.unregisterCallback(cb); + } catch (RemoteException e) { + // Ignore errors. The service might have died. + Slog.w( + TAG, + "health: cannot unregister previous callback (transaction error): " + + e.getMessage()); + } + } + + private static void registerCallback(@NonNull IHealth newService, IHealthInfoCallback cb) { + try { + newService.registerCallback(cb); + } catch (RemoteException e) { + Slog.e( + TAG, + "health: cannot register callback, framework may cease to" + + " receive updates on health / battery info!", + e); + return; + } + // registerCallback does NOT guarantee that update is called immediately, so request a + // manual update here. + try { + newService.update(); + } catch (RemoteException e) { + Slog.e(TAG, "health: cannot update after registering health info callback", e); + } + } + + private class HalInfoCallback extends IHealthInfoCallback.Stub { + @Override + public void healthInfoChanged(HealthInfo healthInfo) throws RemoteException { + mServiceInfoCallback.update(healthInfo); + } + } +} diff --git a/services/core/java/com/android/server/health/HealthServiceWrapper.java b/services/core/java/com/android/server/health/HealthServiceWrapper.java index 9b97554ee259..25d1a885bc18 100644 --- a/services/core/java/com/android/server/health/HealthServiceWrapper.java +++ b/services/core/java/com/android/server/health/HealthServiceWrapper.java @@ -81,6 +81,8 @@ public abstract class HealthServiceWrapper { public static HealthServiceWrapper create(@Nullable HealthInfoCallback healthInfoCallback) throws RemoteException, NoSuchElementException { return create( + healthInfoCallback == null ? null : new HealthRegCallbackAidl(healthInfoCallback), + new HealthServiceWrapperAidl.ServiceManagerStub() {}, healthInfoCallback == null ? null : new HealthHalCallbackHidl(healthInfoCallback), new HealthServiceWrapperHidl.IServiceManagerSupplier() {}, new HealthServiceWrapperHidl.IHealthSupplier() {}); @@ -89,6 +91,9 @@ public abstract class HealthServiceWrapper { /** * Create a new HealthServiceWrapper instance for testing. * + * @param aidlRegCallback callback for AIDL service registration, or {@code null} if the client + * does not care about AIDL service registration notifications + * @param aidlServiceManager Stub for AIDL ServiceManager * @param hidlRegCallback callback for HIDL service registration, or {@code null} if the client * does not care about HIDL service registration notifications * @param hidlServiceManagerSupplier supplier of HIDL service manager @@ -97,10 +102,17 @@ public abstract class HealthServiceWrapper { */ @VisibleForTesting static @NonNull HealthServiceWrapper create( + @Nullable HealthRegCallbackAidl aidlRegCallback, + @NonNull HealthServiceWrapperAidl.ServiceManagerStub aidlServiceManager, @Nullable HealthServiceWrapperHidl.Callback hidlRegCallback, @NonNull HealthServiceWrapperHidl.IServiceManagerSupplier hidlServiceManagerSupplier, @NonNull HealthServiceWrapperHidl.IHealthSupplier hidlHealthSupplier) throws RemoteException, NoSuchElementException { + try { + return new HealthServiceWrapperAidl(aidlRegCallback, aidlServiceManager); + } catch (NoSuchElementException e) { + // Ignore, try HIDL + } return new HealthServiceWrapperHidl( hidlRegCallback, hidlServiceManagerSupplier, hidlHealthSupplier); } diff --git a/services/core/java/com/android/server/health/HealthServiceWrapperAidl.java b/services/core/java/com/android/server/health/HealthServiceWrapperAidl.java new file mode 100644 index 000000000000..4f2ed68974fa --- /dev/null +++ b/services/core/java/com/android/server/health/HealthServiceWrapperAidl.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2021 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.health; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.hardware.health.HealthInfo; +import android.hardware.health.IHealth; +import android.os.BatteryManager; +import android.os.BatteryProperty; +import android.os.Binder; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.IServiceCallback; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.ServiceSpecificException; +import android.os.Trace; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Implement {@link HealthServiceWrapper} backed by the AIDL HAL. + * + * @hide + */ +class HealthServiceWrapperAidl extends HealthServiceWrapper { + private static final String TAG = "HealthServiceWrapperAidl"; + @VisibleForTesting static final String SERVICE_NAME = IHealth.DESCRIPTOR + "/default"; + private final HandlerThread mHandlerThread = new HandlerThread("HealthServiceBinder"); + private final AtomicReference<IHealth> mLastService = new AtomicReference<>(); + private final IServiceCallback mServiceCallback = new ServiceCallback(); + private final HealthRegCallbackAidl mRegCallback; + + /** Stub interface into {@link ServiceManager} for testing. */ + interface ServiceManagerStub { + default @Nullable IHealth waitForDeclaredService(@NonNull String name) { + return IHealth.Stub.asInterface(ServiceManager.waitForDeclaredService(name)); + } + + default void registerForNotifications( + @NonNull String name, @NonNull IServiceCallback callback) throws RemoteException { + ServiceManager.registerForNotifications(name, callback); + } + } + + HealthServiceWrapperAidl( + @Nullable HealthRegCallbackAidl regCallback, @NonNull ServiceManagerStub serviceManager) + throws RemoteException, NoSuchElementException { + + traceBegin("HealthInitGetServiceAidl"); + IHealth newService; + try { + newService = serviceManager.waitForDeclaredService(SERVICE_NAME); + } finally { + traceEnd(); + } + if (newService == null) { + throw new NoSuchElementException( + "IHealth service instance isn't available. Perhaps no permission?"); + } + mLastService.set(newService); + mRegCallback = regCallback; + if (mRegCallback != null) { + mRegCallback.onRegistration(null /* oldService */, newService); + } + + traceBegin("HealthInitRegisterNotificationAidl"); + mHandlerThread.start(); + try { + serviceManager.registerForNotifications(SERVICE_NAME, mServiceCallback); + } finally { + traceEnd(); + } + Slog.i(TAG, "health: HealthServiceWrapper listening to AIDL HAL"); + } + + @Override + @VisibleForTesting + public HandlerThread getHandlerThread() { + return mHandlerThread; + } + + @Override + public int getProperty(int id, BatteryProperty prop) throws RemoteException { + traceBegin("HealthGetPropertyAidl"); + try { + return getPropertyInternal(id, prop); + } finally { + traceEnd(); + } + } + + private int getPropertyInternal(int id, BatteryProperty prop) throws RemoteException { + IHealth service = mLastService.get(); + if (service == null) throw new RemoteException("no health service"); + try { + switch (id) { + case BatteryManager.BATTERY_PROPERTY_CHARGE_COUNTER: + prop.setLong(service.getChargeCounterUah()); + break; + case BatteryManager.BATTERY_PROPERTY_CURRENT_NOW: + prop.setLong(service.getCurrentNowMicroamps()); + break; + case BatteryManager.BATTERY_PROPERTY_CURRENT_AVERAGE: + prop.setLong(service.getCurrentAverageMicroamps()); + break; + case BatteryManager.BATTERY_PROPERTY_CAPACITY: + prop.setLong(service.getCapacity()); + break; + case BatteryManager.BATTERY_PROPERTY_STATUS: + prop.setLong(service.getChargeStatus()); + break; + case BatteryManager.BATTERY_PROPERTY_ENERGY_COUNTER: + prop.setLong(service.getEnergyCounterNwh()); + break; + } + } catch (UnsupportedOperationException e) { + // Leave prop untouched. + return -1; + } catch (ServiceSpecificException e) { + // Leave prop untouched. + return -2; + } + // throws RemoteException as-is. BatteryManager wraps it into a RuntimeException + // and throw it to apps. + + // If no error, return 0. + return 0; + } + + @Override + public void scheduleUpdate() throws RemoteException { + getHandlerThread() + .getThreadHandler() + .post( + () -> { + traceBegin("HealthScheduleUpdate"); + try { + IHealth service = mLastService.get(); + if (service == null) { + Slog.e(TAG, "no health service"); + return; + } + service.update(); + } catch (RemoteException | ServiceSpecificException ex) { + Slog.e(TAG, "Cannot call update on health AIDL HAL", ex); + } finally { + traceEnd(); + } + }); + } + + @Override + public HealthInfo getHealthInfo() throws RemoteException { + IHealth service = mLastService.get(); + if (service == null) return null; + try { + return service.getHealthInfo(); + } catch (UnsupportedOperationException | ServiceSpecificException ex) { + return null; + } + } + + private static void traceBegin(String name) { + Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, name); + } + + private static void traceEnd() { + Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); + } + + private class ServiceCallback extends IServiceCallback.Stub { + @Override + public void onRegistration(String name, @NonNull final IBinder newBinder) + throws RemoteException { + if (!SERVICE_NAME.equals(name)) return; + // This runnable only runs on mHandlerThread and ordering is ensured, hence + // no locking is needed inside the runnable. + getHandlerThread() + .getThreadHandler() + .post( + () -> { + IHealth newService = + IHealth.Stub.asInterface(Binder.allowBlocking(newBinder)); + IHealth oldService = mLastService.getAndSet(newService); + IBinder oldBinder = + oldService != null ? oldService.asBinder() : null; + if (Objects.equals(newBinder, oldBinder)) return; + + Slog.i(TAG, "New health AIDL HAL service registered"); + mRegCallback.onRegistration(oldService, newService); + }); + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/health/HealthServiceWrapperTest.java b/services/tests/servicestests/src/com/android/server/health/HealthServiceWrapperTest.java index c97a67bad590..16d97a454050 100644 --- a/services/tests/servicestests/src/com/android/server/health/HealthServiceWrapperTest.java +++ b/services/tests/servicestests/src/com/android/server/health/HealthServiceWrapperTest.java @@ -19,11 +19,12 @@ package com.android.server.health; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.fail; +import static org.mockito.AdditionalMatchers.not; import static org.mockito.Mockito.*; -import android.hardware.health.V2_0.IHealth; import android.hidl.manager.V1_0.IServiceManager; import android.hidl.manager.V1_0.IServiceNotification; +import android.os.IServiceCallback; import android.os.RemoteException; import androidx.test.filters.SmallTest; @@ -44,28 +45,47 @@ import java.util.NoSuchElementException; @RunWith(AndroidJUnit4.class) public class HealthServiceWrapperTest { - @Mock IServiceManager mMockedManager; - @Mock IHealth mMockedHal; - @Mock IHealth mMockedHal2; + @Mock android.hardware.health.V2_0.IHealth mMockedHal; + @Mock android.hardware.health.V2_0.IHealth mMockedHal2; @Mock HealthServiceWrapperHidl.Callback mCallback; @Mock HealthServiceWrapperHidl.IServiceManagerSupplier mManagerSupplier; @Mock HealthServiceWrapperHidl.IHealthSupplier mHealthServiceSupplier; + + @Mock android.hardware.health.IHealth.Stub mMockedAidlHal; + @Mock android.hardware.health.IHealth.Stub mMockedAidlHal2; + @Mock HealthServiceWrapperAidl.ServiceManagerStub mMockedAidlManager; + @Mock HealthRegCallbackAidl mRegCallbackAidl; + HealthServiceWrapper mWrapper; private static final String VENDOR = HealthServiceWrapperHidl.INSTANCE_VENDOR; + private static final String AIDL_SERVICE_NAME = HealthServiceWrapperAidl.SERVICE_NAME; @Before public void setUp() { MockitoAnnotations.initMocks(this); + + // Mocks the conversion between IHealth and IBinder. + when(mMockedAidlHal.asBinder()).thenCallRealMethod(); // returns mMockedAidlHal + when(mMockedAidlHal2.asBinder()).thenCallRealMethod(); // returns mMockedAidlHal2 + when(mMockedAidlHal.queryLocalInterface(android.hardware.health.IHealth.DESCRIPTOR)) + .thenReturn(mMockedAidlHal); + when(mMockedAidlHal2.queryLocalInterface(android.hardware.health.IHealth.DESCRIPTOR)) + .thenReturn(mMockedAidlHal2); } @After public void tearDown() { + validateMockitoUsage(); if (mWrapper != null) mWrapper.getHandlerThread().quitSafely(); } + public static <T> ArgumentMatcher<T> isOneOf(T[] collection) { + return isOneOf(Arrays.asList(collection)); + } + public static <T> ArgumentMatcher<T> isOneOf(Collection<T> collection) { return new ArgumentMatcher<T>() { @Override @@ -75,13 +95,39 @@ public class HealthServiceWrapperTest { @Override public String toString() { - return collection.toString(); + return "is one of " + collection.toString(); } }; } - private void initForInstances(String... instanceNamesArr) throws Exception { - final Collection<String> instanceNames = Arrays.asList(instanceNamesArr); + /** + * Set up mock objects to pretend that the given AIDL and HIDL instances exists. + * + * <p>Also, when registering service notifications, the mocked service managers immediately + * sends 3 registration notifications, including 2 referring to the original HAL and 1 referring + * to the new HAL. + * + * @param aidlInstances e.g. {"android.hardware.health.IHealth/default"} + * @param hidlInstances e.g. {"default", "backup"} + * @throws Exception + */ + private void initForInstances(String[] aidlInstances, String[] hidlInstances) throws Exception { + doAnswer( + (invocation) -> { + sendAidlRegCallback(invocation, mMockedAidlHal); + sendAidlRegCallback(invocation, mMockedAidlHal); + sendAidlRegCallback(invocation, mMockedAidlHal2); + return null; + }) + .when(mMockedAidlManager) + .registerForNotifications( + argThat(isOneOf(aidlInstances)), any(IServiceCallback.class)); + when(mMockedAidlManager.waitForDeclaredService(argThat(isOneOf(aidlInstances)))) + .thenReturn(mMockedAidlHal) + .thenThrow(new RuntimeException("waitForDeclaredService called more than once")); + when(mMockedAidlManager.waitForDeclaredService(not(argThat(isOneOf(aidlInstances))))) + .thenReturn(null); + doAnswer( (invocation) -> { // technically, preexisting is ignored by @@ -93,8 +139,8 @@ public class HealthServiceWrapperTest { }) .when(mMockedManager) .registerForNotifications( - eq(IHealth.kInterfaceName), - argThat(isOneOf(instanceNames)), + eq(android.hardware.health.V2_0.IHealth.kInterfaceName), + argThat(isOneOf(hidlInstances)), any(IServiceNotification.class)); doReturn(mMockedManager).when(mManagerSupplier).get(); @@ -104,7 +150,7 @@ public class HealthServiceWrapperTest { .doReturn(mMockedHal2) // notification 3 .doThrow(new RuntimeException("Should not call getService for more than 4 times")) .when(mHealthServiceSupplier) - .get(argThat(isOneOf(instanceNames))); + .get(argThat(isOneOf(hidlInstances))); } private void waitHandlerThreadFinish() throws Exception { @@ -121,19 +167,62 @@ public class HealthServiceWrapperTest { throws Exception { ((IServiceNotification) invocation.getArguments()[2]) .onRegistration( - IHealth.kInterfaceName, (String) invocation.getArguments()[1], preexisting); + android.hardware.health.V2_0.IHealth.kInterfaceName, + (String) invocation.getArguments()[1], + preexisting); + } + + private static void sendAidlRegCallback( + InvocationOnMock invocation, android.hardware.health.IHealth service) throws Exception { + ((IServiceCallback) invocation.getArguments()[1]) + .onRegistration((String) invocation.getArguments()[0], service.asBinder()); } private void createWrapper() throws RemoteException { - mWrapper = HealthServiceWrapper.create(mCallback, mManagerSupplier, mHealthServiceSupplier); + mWrapper = + HealthServiceWrapper.create( + mRegCallbackAidl, + mMockedAidlManager, + mCallback, + mManagerSupplier, + mHealthServiceSupplier); + } + + @SmallTest + @Test + public void testWrapAidlOnly() throws Exception { + initForInstances(new String[] {AIDL_SERVICE_NAME}, new String[0]); + createWrapper(); + waitHandlerThreadFinish(); + verify(mRegCallbackAidl, times(1)).onRegistration(same(null), same(mMockedAidlHal)); + verify(mRegCallbackAidl, never()) + .onRegistration(same(mMockedAidlHal), same(mMockedAidlHal)); + verify(mRegCallbackAidl, times(1)) + .onRegistration(same(mMockedAidlHal), same(mMockedAidlHal2)); + verify(mCallback, never()).onRegistration(any(), any(), anyString()); + } + + @SmallTest + @Test + public void testWrapPreferAidl() throws Exception { + initForInstances(new String[] {AIDL_SERVICE_NAME}, new String[] {VENDOR}); + createWrapper(); + waitHandlerThreadFinish(); + verify(mRegCallbackAidl, times(1)).onRegistration(same(null), same(mMockedAidlHal)); + verify(mRegCallbackAidl, never()) + .onRegistration(same(mMockedAidlHal), same(mMockedAidlHal)); + verify(mRegCallbackAidl, times(1)) + .onRegistration(same(mMockedAidlHal), same(mMockedAidlHal2)); + verify(mCallback, never()).onRegistration(any(), any(), anyString()); } @SmallTest @Test - public void testWrapPreferVendor() throws Exception { - initForInstances(VENDOR); + public void testWrapFallbackHidl() throws Exception { + initForInstances(new String[0], new String[] {VENDOR}); createWrapper(); waitHandlerThreadFinish(); + verify(mRegCallbackAidl, never()).onRegistration(any(), any()); verify(mCallback, times(1)).onRegistration(same(null), same(mMockedHal), eq(VENDOR)); verify(mCallback, never()).onRegistration(same(mMockedHal), same(mMockedHal), anyString()); verify(mCallback, times(1)).onRegistration(same(mMockedHal), same(mMockedHal2), eq(VENDOR)); @@ -142,7 +231,7 @@ public class HealthServiceWrapperTest { @SmallTest @Test public void testNoService() throws Exception { - initForInstances("unrelated"); + initForInstances(new String[0], new String[] {"unrelated"}); try { createWrapper(); fail("Expect NoSuchElementException"); |