1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.health;
18 
19 import static android.hardware.health.Translate.h2aTranslate;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.hardware.health.HealthInfo;
24 import android.hardware.health.V2_0.IHealth;
25 import android.hardware.health.V2_0.Result;
26 import android.hidl.manager.V1_0.IServiceManager;
27 import android.hidl.manager.V1_0.IServiceNotification;
28 import android.os.BatteryManager;
29 import android.os.BatteryProperty;
30 import android.os.HandlerThread;
31 import android.os.RemoteException;
32 import android.os.Trace;
33 import android.util.MutableInt;
34 import android.util.Slog;
35 
36 import com.android.internal.annotations.VisibleForTesting;
37 
38 import java.util.NoSuchElementException;
39 import java.util.Objects;
40 import java.util.concurrent.atomic.AtomicReference;
41 
42 /**
43  * Implement {@link HealthServiceWrapper} backed by the HIDL HAL.
44  *
45  * @hide
46  */
47 final class HealthServiceWrapperHidl extends HealthServiceWrapper {
48     private static final String TAG = "HealthServiceWrapperHidl";
49     public static final String INSTANCE_VENDOR = "default";
50 
51     private final IServiceNotification mNotification = new Notification();
52     private final HandlerThread mHandlerThread = new HandlerThread("HealthServiceHwbinder");
53     // These variables are fixed after init.
54     private Callback mCallback;
55     private IHealthSupplier mHealthSupplier;
56     private String mInstanceName;
57 
58     // Last IHealth service received.
59     private final AtomicReference<IHealth> mLastService = new AtomicReference<>();
60 
traceBegin(String name)61     private static void traceBegin(String name) {
62         Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, name);
63     }
64 
traceEnd()65     private static void traceEnd() {
66         Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
67     }
68 
69     @Override
getProperty(int id, final BatteryProperty prop)70     public int getProperty(int id, final BatteryProperty prop) throws RemoteException {
71         traceBegin("HealthGetProperty");
72         try {
73             IHealth service = mLastService.get();
74             if (service == null) throw new RemoteException("no health service");
75             final MutableInt outResult = new MutableInt(Result.NOT_SUPPORTED);
76             switch (id) {
77                 case BatteryManager.BATTERY_PROPERTY_CHARGE_COUNTER:
78                     service.getChargeCounter(
79                             (int result, int value) -> {
80                                 outResult.value = result;
81                                 if (result == Result.SUCCESS) prop.setLong(value);
82                             });
83                     break;
84                 case BatteryManager.BATTERY_PROPERTY_CURRENT_NOW:
85                     service.getCurrentNow(
86                             (int result, int value) -> {
87                                 outResult.value = result;
88                                 if (result == Result.SUCCESS) prop.setLong(value);
89                             });
90                     break;
91                 case BatteryManager.BATTERY_PROPERTY_CURRENT_AVERAGE:
92                     service.getCurrentAverage(
93                             (int result, int value) -> {
94                                 outResult.value = result;
95                                 if (result == Result.SUCCESS) prop.setLong(value);
96                             });
97                     break;
98                 case BatteryManager.BATTERY_PROPERTY_CAPACITY:
99                     service.getCapacity(
100                             (int result, int value) -> {
101                                 outResult.value = result;
102                                 if (result == Result.SUCCESS) prop.setLong(value);
103                             });
104                     break;
105                 case BatteryManager.BATTERY_PROPERTY_STATUS:
106                     service.getChargeStatus(
107                             (int result, int value) -> {
108                                 outResult.value = result;
109                                 if (result == Result.SUCCESS) prop.setLong(value);
110                             });
111                     break;
112                 case BatteryManager.BATTERY_PROPERTY_ENERGY_COUNTER:
113                     service.getEnergyCounter(
114                             (int result, long value) -> {
115                                 outResult.value = result;
116                                 if (result == Result.SUCCESS) prop.setLong(value);
117                             });
118                     break;
119             }
120             return outResult.value;
121         } finally {
122             traceEnd();
123         }
124     }
125 
126     @Override
scheduleUpdate()127     public void scheduleUpdate() throws RemoteException {
128         getHandlerThread()
129                 .getThreadHandler()
130                 .post(
131                         () -> {
132                             traceBegin("HealthScheduleUpdate");
133                             try {
134                                 IHealth service = mLastService.get();
135                                 if (service == null) {
136                                     Slog.e(TAG, "no health service");
137                                     return;
138                                 }
139                                 service.update();
140                             } catch (RemoteException ex) {
141                                 Slog.e(TAG, "Cannot call update on health HAL", ex);
142                             } finally {
143                                 traceEnd();
144                             }
145                         });
146     }
147 
148     private static class Mutable<T> {
149         public T value;
150     }
151 
152     @Override
getHealthInfo()153     public HealthInfo getHealthInfo() throws RemoteException {
154         IHealth service = mLastService.get();
155         if (service == null) return null;
156         final Mutable<HealthInfo> ret = new Mutable<>();
157         service.getHealthInfo(
158                 (result, value) -> {
159                     if (result == Result.SUCCESS) {
160                         ret.value = h2aTranslate(value.legacy);
161                     }
162                 });
163         return ret.value;
164     }
165 
166     /**
167      * Start monitoring registration of new IHealth services. Only instance {@link #INSTANCE_VENDOR}
168      * and in device / framework manifest are used. This function should only be called once.
169      *
170      * <p>mCallback.onRegistration() is called synchronously (aka in init thread) before this method
171      * returns if callback is not null.
172      *
173      * @throws RemoteException transaction error when talking to IServiceManager
174      * @throws NoSuchElementException if one of the following cases: - No service manager; - {@link
175      *     #INSTANCE_VENDOR} is not in manifests (i.e. not available on this device), or none of
176      *     these instances are available to current process.
177      * @throws NullPointerException when supplier is null
178      */
179     @VisibleForTesting
HealthServiceWrapperHidl( @ullable Callback callback, @NonNull IServiceManagerSupplier managerSupplier, @NonNull IHealthSupplier healthSupplier)180     HealthServiceWrapperHidl(
181             @Nullable Callback callback,
182             @NonNull IServiceManagerSupplier managerSupplier,
183             @NonNull IHealthSupplier healthSupplier)
184             throws RemoteException, NoSuchElementException, NullPointerException {
185         if (managerSupplier == null || healthSupplier == null) {
186             throw new NullPointerException();
187         }
188         mHealthSupplier = healthSupplier;
189 
190         // Initialize mLastService and call callback for the first time (in init thread)
191         IHealth newService = null;
192         traceBegin("HealthInitGetService_" + INSTANCE_VENDOR);
193         try {
194             newService = healthSupplier.get(INSTANCE_VENDOR);
195         } catch (NoSuchElementException ex) {
196             /* ignored, handled below */
197         } finally {
198             traceEnd();
199         }
200         if (newService != null) {
201             mInstanceName = INSTANCE_VENDOR;
202             mLastService.set(newService);
203         }
204 
205         if (mInstanceName == null || newService == null) {
206             throw new NoSuchElementException(
207                     String.format(
208                             "IHealth service instance %s isn't available. Perhaps no permission?",
209                             INSTANCE_VENDOR));
210         }
211 
212         if (callback != null) {
213             mCallback = callback;
214             mCallback.onRegistration(null, newService, mInstanceName);
215         }
216 
217         // Register for future service registrations
218         traceBegin("HealthInitRegisterNotification");
219         mHandlerThread.start();
220         try {
221             managerSupplier
222                     .get()
223                     .registerForNotifications(IHealth.kInterfaceName, mInstanceName, mNotification);
224         } finally {
225             traceEnd();
226         }
227         Slog.i(TAG, "health: HealthServiceWrapper listening to instance " + mInstanceName);
228     }
229 
230     @VisibleForTesting
getHandlerThread()231     public HandlerThread getHandlerThread() {
232         return mHandlerThread;
233     }
234 
235     /** Service registration callback. */
236     interface Callback {
237         /**
238          * This function is invoked asynchronously when a new and related IServiceNotification is
239          * received.
240          *
241          * @param service the recently retrieved service from IServiceManager. Can be a dead service
242          *     before service notification of a new service is delivered. Implementation must handle
243          *     cases for {@link RemoteException}s when calling into service.
244          * @param instance instance name.
245          */
onRegistration(IHealth oldService, IHealth newService, String instance)246         void onRegistration(IHealth oldService, IHealth newService, String instance);
247     }
248 
249     /**
250      * Supplier of services. Must not return null; throw {@link NoSuchElementException} if a service
251      * is not available.
252      */
253     interface IServiceManagerSupplier {
get()254         default IServiceManager get() throws NoSuchElementException, RemoteException {
255             return IServiceManager.getService();
256         }
257     }
258 
259     /**
260      * Supplier of services. Must not return null; throw {@link NoSuchElementException} if a service
261      * is not available.
262      */
263     interface IHealthSupplier {
get(String name)264         default IHealth get(String name) throws NoSuchElementException, RemoteException {
265             return IHealth.getService(name, true /* retry */);
266         }
267     }
268 
269     private class Notification extends IServiceNotification.Stub {
270         @Override
onRegistration( String interfaceName, String instanceName, boolean preexisting)271         public final void onRegistration(
272                 String interfaceName, String instanceName, boolean preexisting) {
273             if (!IHealth.kInterfaceName.equals(interfaceName)) return;
274             if (!mInstanceName.equals(instanceName)) return;
275 
276             // This runnable only runs on mHandlerThread and ordering is ensured, hence
277             // no locking is needed inside the runnable.
278             mHandlerThread
279                     .getThreadHandler()
280                     .post(
281                             new Runnable() {
282                                 @Override
283                                 public void run() {
284                                     try {
285                                         IHealth newService = mHealthSupplier.get(mInstanceName);
286                                         IHealth oldService = mLastService.getAndSet(newService);
287 
288                                         // preexisting may be inaccurate (race). Check for equality
289                                         // here.
290                                         if (Objects.equals(newService, oldService)) return;
291 
292                                         Slog.i(
293                                                 TAG,
294                                                 "health: new instance registered " + mInstanceName);
295                                         // #init() may be called with null callback. Skip null
296                                         // callbacks.
297                                         if (mCallback == null) return;
298                                         mCallback.onRegistration(
299                                                 oldService, newService, mInstanceName);
300                                     } catch (NoSuchElementException | RemoteException ex) {
301                                         Slog.e(
302                                                 TAG,
303                                                 "health: Cannot get instance '"
304                                                         + mInstanceName
305                                                         + "': "
306                                                         + ex.getMessage()
307                                                         + ". Perhaps no permission?");
308                                     }
309                                 }
310                             });
311         }
312     }
313 }
314