Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 98852796 authored by Yifan Hong's avatar Yifan Hong
Browse files

BatteryService: implement HealthServiceWrapper

... which is a wrapper for @2.0::IHealth service that refreshes the
service if necessary. When new instances of IHealth are registered,
this class redirects its proxy to the new service.

Test: BatteryServiceTest
Bug: 63702641
Change-Id: I22f2aa1eb7d48a05dec5a7c747dabd6f832078e8
parent 990735ee
Loading
Loading
Loading
Loading
+124 −0
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import android.os.PowerManager;
import android.os.ResultReceiver;
import android.os.ShellCallback;
import android.os.ShellCommand;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.IBatteryStats;
import com.android.internal.util.DumpUtils;
import com.android.server.am.BatteryStatsService;
@@ -35,7 +36,10 @@ import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.hidl.manager.V1_0.IServiceManager;
import android.hidl.manager.V1_0.IServiceNotification;
import android.hardware.health.V2_0.HealthInfo;
import android.hardware.health.V2_0.IHealth;
import android.os.BatteryManager;
import android.os.BatteryManagerInternal;
import android.os.BatteryProperties;
@@ -63,6 +67,9 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;

import java.util.Arrays;
import java.util.List;
import java.util.NoSuchElementException;

/**
 * <p>BatteryService monitors the charging status, and charge level of the device
@@ -1020,4 +1027,121 @@ public final class BatteryService extends SystemService {
            }
        }
    }

    /**
     * HealthServiceWrapper wraps the internal IHealth service and refreshes the service when
     * necessary.
     *
     * On new registration of IHealth service, {@link #onRegistration onRegistration} is called and
     * the internal service is refreshed.
     * On death of an existing IHealth service, the internal service is NOT cleared to avoid
     * race condition between death notification and new service notification. Hence,
     * a caller must check for transaction errors when calling into the service.
     *
     * @hide Should only be used internally.
     */
    @VisibleForTesting
    static final class HealthServiceWrapper {
        private static final String TAG = "HealthServiceWrapper";
        public static final String INSTANCE_HEALTHD = "backup";
        public static final String INSTANCE_VENDOR = "default";
        // All interesting instances, sorted by priority high -> low.
        private static final List<String> sAllInstances =
                Arrays.asList(INSTANCE_VENDOR, INSTANCE_HEALTHD);

        private final IServiceNotification mNotification = new Notification();
        private Callback mCallback;
        private IHealthSupplier mHealthSupplier;

        /**
         * init should be called after constructor. For testing purposes, init is not called by
         * constructor.
         */
        HealthServiceWrapper() {
        }

        /**
         * Start monitoring registration of new IHealth services. Only instances that are in
         * {@code sAllInstances} and in device / framework manifest are used. This function should
         * only be called once.
         * @throws RemoteException transaction error when talking to IServiceManager
         * @throws NoSuchElementException if one of the following cases:
         *         - No service manager;
         *         - none of {@code sAllInstances} are in manifests (i.e. not
         *           available on this device), or none of these instances are available to current
         *           process.
         * @throws NullPointerException when callback is null or supplier is null
         */
        void init(Callback callback,
                  IServiceManagerSupplier managerSupplier,
                  IHealthSupplier healthSupplier)
                throws RemoteException, NoSuchElementException, NullPointerException {
            if (callback == null || managerSupplier == null || healthSupplier == null)
                throw new NullPointerException();

            mCallback = callback;
            mHealthSupplier = healthSupplier;

            IServiceManager manager = managerSupplier.get();
            for (String name : sAllInstances) {
                if (manager.getTransport(IHealth.kInterfaceName, name) ==
                        IServiceManager.Transport.EMPTY) {
                    continue;
                }

                manager.registerForNotifications(IHealth.kInterfaceName, name, mNotification);
                Slog.i(TAG, "health: HealthServiceWrapper listening to instance " + name);
                return;
            }

            throw new NoSuchElementException(String.format(
                    "No IHealth service instance among %s is available. Perhaps no permission?",
                    sAllInstances.toString()));
        }

        interface Callback {
            /**
             * This function is invoked asynchronously when a new and related IServiceNotification
             * is received.
             * @param service the recently retrieved service from IServiceManager.
             * Can be a dead service before service notification of a new service is delivered.
             * Implementation must handle cases for {@link RemoteException}s when calling
             * into service.
             * @param instance instance name.
             */
            void onRegistration(IHealth service, String instance);
        }

        /**
         * Supplier of services.
         * Must not return null; throw {@link NoSuchElementException} if a service is not available.
         */
        interface IServiceManagerSupplier {
            IServiceManager get() throws NoSuchElementException, RemoteException;
        }
        /**
         * Supplier of services.
         * Must not return null; throw {@link NoSuchElementException} if a service is not available.
         */
        interface IHealthSupplier {
            IHealth get(String instanceName) throws NoSuchElementException, RemoteException;
        }

        private class Notification extends IServiceNotification.Stub {
            @Override
            public final void onRegistration(String interfaceName, String instanceName,
                    boolean preexisting) {
                if (!IHealth.kInterfaceName.equals(interfaceName)) return;
                if (!sAllInstances.contains(instanceName)) return;
                try {
                    IHealth service = mHealthSupplier.get(instanceName);
                    Slog.i(TAG, "health: new instance registered " + instanceName);
                    mCallback.onRegistration(service, instanceName);
                } catch (NoSuchElementException | RemoteException ex) {
                    Slog.e(TAG, "health: Cannot get instance '" + instanceName + "': " +
                           ex.getMessage() + ". Perhaps no permission?");
                }
            }
        }
    }
}
+4 −1
Original line number Diff line number Diff line
@@ -33,7 +33,10 @@ LOCAL_SRC_FILES += aidl/com/android/servicestests/aidl/INetworkStateObserver.aid
    aidl/com/android/servicestests/aidl/ICmdReceiverService.aidl
LOCAL_SRC_FILES += $(call all-java-files-under, test-apps/JobTestApp/src)

LOCAL_JAVA_LIBRARIES := android.test.mock legacy-android-test
LOCAL_JAVA_LIBRARIES := \
    android.hidl.manager-V1.0-java \
    android.test.mock \
    legacy-android-test \

LOCAL_PACKAGE_NAME := FrameworksServicesTests
LOCAL_COMPATIBILITY_SUITE := device-tests
+121 −0
Original line number Diff line number Diff line
/*
 * 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;

import static junit.framework.Assert.*;
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.RemoteException;
import android.support.test.filters.SmallTest;
import android.test.AndroidTestCase;
import android.util.Slog;

import java.util.Arrays;
import java.util.Collection;
import java.util.NoSuchElementException;

import org.junit.runner.RunWith;
import org.mockito.ArgumentMatcher;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;


public class BatteryServiceTest extends AndroidTestCase {

    @Mock IServiceManager mMockedManager;
    @Mock IHealth mMockedHal;

    @Mock BatteryService.HealthServiceWrapper.Callback mCallback;
    @Mock BatteryService.HealthServiceWrapper.IServiceManagerSupplier mManagerSupplier;
    @Mock BatteryService.HealthServiceWrapper.IHealthSupplier mHealthServiceSupplier;
    BatteryService.HealthServiceWrapper mWrapper;

    private static final String HEALTHD = BatteryService.HealthServiceWrapper.INSTANCE_HEALTHD;
    private static final String VENDOR = BatteryService.HealthServiceWrapper.INSTANCE_VENDOR;

    @Override
    public void setUp() {
        MockitoAnnotations.initMocks(this);
    }

    public static <T> ArgumentMatcher<T> isOneOf(Collection<T> collection) {
        return new ArgumentMatcher<T>() {
            @Override public boolean matches(T e) {
                return collection.contains(e);
            }
            @Override public String toString() {
                return collection.toString();
            }
        };
    }

    private void initForInstances(String... instanceNamesArr) throws Exception {
        final Collection<String> instanceNames = Arrays.asList(instanceNamesArr);
        doAnswer((invocation) -> {
                Slog.e("BatteryServiceTest", "health: onRegistration " + invocation.getArguments()[2]);
                ((IServiceNotification)invocation.getArguments()[2]).onRegistration(
                        IHealth.kInterfaceName,
                        (String)invocation.getArguments()[1],
                        true /* preexisting */);
                return null;
            }).when(mMockedManager).registerForNotifications(
                eq(IHealth.kInterfaceName),
                argThat(isOneOf(instanceNames)),
                any(IServiceNotification.class));

        doReturn(mMockedHal).when(mMockedManager)
            .get(eq(IHealth.kInterfaceName), argThat(isOneOf(instanceNames)));

        doReturn(IServiceManager.Transport.HWBINDER).when(mMockedManager)
            .getTransport(eq(IHealth.kInterfaceName), argThat(isOneOf(instanceNames)));

        doReturn(mMockedManager).when(mManagerSupplier).get();
        doReturn(mMockedHal).when(mHealthServiceSupplier)
            .get(argThat(isOneOf(instanceNames)));

        mWrapper = new BatteryService.HealthServiceWrapper();
    }

    @SmallTest
    public void testWrapPreferVendor() throws Exception {
        initForInstances(VENDOR, HEALTHD);
        mWrapper.init(mCallback, mManagerSupplier, mHealthServiceSupplier);
        verify(mCallback).onRegistration(same(mMockedHal), eq(VENDOR));
    }

    @SmallTest
    public void testUseHealthd() throws Exception {
        initForInstances(HEALTHD);
        mWrapper.init(mCallback, mManagerSupplier, mHealthServiceSupplier);
        verify(mCallback).onRegistration(same(mMockedHal), eq(HEALTHD));
    }

    @SmallTest
    public void testNoService() throws Exception {
        initForInstances("unrelated");
        try {
            mWrapper.init(mCallback, mManagerSupplier, mHealthServiceSupplier);
            fail("Expect NoSuchElementException");
        } catch (NoSuchElementException ex) {
            // expected
        }
    }
}