1/*
2 * Copyright (C) 2017 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
17package android.car.diagnostic;
18
19import android.annotation.IntDef;
20import android.annotation.Nullable;
21import android.annotation.SystemApi;
22import android.car.Car;
23import android.car.CarApiUtil;
24import android.car.CarLibLog;
25import android.car.CarManagerBase;
26import android.car.CarNotConnectedException;
27import android.car.diagnostic.ICarDiagnosticEventListener.Stub;
28import android.content.Context;
29import android.os.Handler;
30import android.os.IBinder;
31import android.os.RemoteException;
32import android.util.Log;
33import android.util.SparseArray;
34
35import com.android.car.internal.CarPermission;
36import com.android.car.internal.CarRatedListeners;
37import com.android.car.internal.SingleMessageHandler;
38
39import java.lang.annotation.Retention;
40import java.lang.annotation.RetentionPolicy;
41import java.lang.ref.WeakReference;
42import java.util.ArrayList;
43import java.util.List;
44import java.util.function.Consumer;
45
46/**
47 * API for monitoring car diagnostic data.
48 *
49 * @hide
50 */
51@SystemApi
52public final class CarDiagnosticManager implements CarManagerBase {
53    public static final int FRAME_TYPE_LIVE = 0;
54    public static final int FRAME_TYPE_FREEZE = 1;
55
56    @Retention(RetentionPolicy.SOURCE)
57    @IntDef({FRAME_TYPE_LIVE, FRAME_TYPE_FREEZE})
58    /** @hide */
59    public @interface FrameType {}
60
61    /** @hide */
62    public static final @FrameType int FRAME_TYPES[] = {
63        FRAME_TYPE_LIVE,
64        FRAME_TYPE_FREEZE
65    };
66
67    private static final int MSG_DIAGNOSTIC_EVENTS = 0;
68
69    private final ICarDiagnostic mService;
70    private final SparseArray<CarDiagnosticListeners> mActiveListeners = new SparseArray<>();
71
72    /** Handles call back into clients. */
73    private final SingleMessageHandler<CarDiagnosticEvent> mHandlerCallback;
74
75    private CarDiagnosticEventListenerToService mListenerToService;
76
77    private final CarPermission mVendorExtensionPermission;
78
79    /** @hide */
80    public CarDiagnosticManager(IBinder service, Context context, Handler handler) {
81        mService = ICarDiagnostic.Stub.asInterface(service);
82        mHandlerCallback = new SingleMessageHandler<CarDiagnosticEvent>(handler.getLooper(),
83            MSG_DIAGNOSTIC_EVENTS) {
84            @Override
85            protected void handleEvent(CarDiagnosticEvent event) {
86                CarDiagnosticListeners listeners;
87                synchronized (mActiveListeners) {
88                    listeners = mActiveListeners.get(event.frameType);
89                }
90                if (listeners != null) {
91                    listeners.onDiagnosticEvent(event);
92                }
93            }
94        };
95        mVendorExtensionPermission = new CarPermission(context, Car.PERMISSION_VENDOR_EXTENSION);
96    }
97
98    @Override
99    /** @hide */
100    public void onCarDisconnected() {
101        synchronized(mActiveListeners) {
102            mActiveListeners.clear();
103            mListenerToService = null;
104        }
105    }
106
107    /** Listener for diagnostic events. Callbacks are called in the Looper context. */
108    public interface OnDiagnosticEventListener {
109        /**
110         * Called when there is a diagnostic event from the car.
111         *
112         * @param carDiagnosticEvent
113         */
114        void onDiagnosticEvent(final CarDiagnosticEvent carDiagnosticEvent);
115    }
116
117    // OnDiagnosticEventListener registration
118
119    private void assertFrameType(@FrameType int frameType) {
120        switch(frameType) {
121            case FRAME_TYPE_FREEZE:
122            case FRAME_TYPE_LIVE:
123                return;
124            default:
125                throw new IllegalArgumentException(String.format(
126                            "%d is not a valid diagnostic frame type", frameType));
127        }
128    }
129
130    /**
131     * Register a new listener for events of a given frame type and rate.
132     * @param listener
133     * @param frameType
134     * @param rate
135     * @return true if the registration was successful; false otherwise
136     * @throws CarNotConnectedException
137     * @throws IllegalArgumentException
138     */
139    public boolean registerListener(OnDiagnosticEventListener listener,
140            @FrameType int frameType,
141            int rate)
142                throws CarNotConnectedException, IllegalArgumentException {
143        assertFrameType(frameType);
144        synchronized(mActiveListeners) {
145            if (null == mListenerToService) {
146                mListenerToService = new CarDiagnosticEventListenerToService(this);
147            }
148            boolean needsServerUpdate = false;
149            CarDiagnosticListeners listeners = mActiveListeners.get(frameType);
150            if (listeners == null) {
151                listeners = new CarDiagnosticListeners(rate);
152                mActiveListeners.put(frameType, listeners);
153                needsServerUpdate = true;
154            }
155            if (listeners.addAndUpdateRate(listener, rate)) {
156                needsServerUpdate = true;
157            }
158            if (needsServerUpdate) {
159                if (!registerOrUpdateDiagnosticListener(frameType, rate)) {
160                    return false;
161                }
162            }
163        }
164        return true;
165    }
166
167    /**
168     * Unregister a listener, causing it to stop receiving all diagnostic events.
169     * @param listener
170     */
171    public void unregisterListener(OnDiagnosticEventListener listener) {
172        synchronized(mActiveListeners) {
173            for(@FrameType int frameType : FRAME_TYPES) {
174                doUnregisterListenerLocked(listener, frameType);
175            }
176        }
177    }
178
179    private void doUnregisterListenerLocked(OnDiagnosticEventListener listener,
180            @FrameType int frameType) {
181        CarDiagnosticListeners listeners = mActiveListeners.get(frameType);
182        if (listeners != null) {
183            boolean needsServerUpdate = false;
184            if (listeners.contains(listener)) {
185                needsServerUpdate = listeners.remove(listener);
186            }
187            if (listeners.isEmpty()) {
188                try {
189                    mService.unregisterDiagnosticListener(frameType,
190                        mListenerToService);
191                } catch (RemoteException e) {
192                    //ignore
193                }
194                mActiveListeners.remove(frameType);
195            } else if (needsServerUpdate) {
196                try {
197                    registerOrUpdateDiagnosticListener(frameType, listeners.getRate());
198                } catch (CarNotConnectedException e) {
199                    // ignore
200                }
201            }
202        }
203    }
204
205    private boolean registerOrUpdateDiagnosticListener(@FrameType int frameType, int rate)
206        throws CarNotConnectedException {
207        try {
208            return mService.registerOrUpdateDiagnosticListener(frameType, rate, mListenerToService);
209        } catch (IllegalStateException e) {
210            CarApiUtil.checkCarNotConnectedExceptionFromCarService(e);
211        } catch (RemoteException e) {
212            throw new CarNotConnectedException();
213        }
214        return false;
215    }
216
217    // ICarDiagnostic forwards
218
219    /**
220     * Retrieve the most-recently acquired live frame data from the car.
221     * @return A CarDiagnostic event for the most recently known live frame if one is present.
222     *         null if no live frame has been recorded by the vehicle.
223     * @throws CarNotConnectedException
224     */
225    public @Nullable
226    CarDiagnosticEvent getLatestLiveFrame() throws CarNotConnectedException {
227        try {
228            return mService.getLatestLiveFrame();
229        } catch (IllegalStateException e) {
230            CarApiUtil.checkCarNotConnectedExceptionFromCarService(e);
231        } catch (RemoteException e) {
232            throw new CarNotConnectedException();
233        }
234        return null;
235    }
236
237    /**
238     * Return the list of the timestamps for which a freeze frame is currently stored.
239     * @return An array containing timestamps at which, at the current time, the vehicle has
240     *         a freeze frame stored. If no freeze frames are currently stored, an empty
241     *         array will be returned.
242     * Because vehicles might have a limited amount of storage for frames, clients cannot
243     * assume that a timestamp obtained via this call will be indefinitely valid for retrieval
244     * of the actual diagnostic data, and must be prepared to handle a missing frame.
245     * @throws CarNotConnectedException
246     */
247    public long[] getFreezeFrameTimestamps() throws CarNotConnectedException {
248        try {
249            return mService.getFreezeFrameTimestamps();
250        } catch (IllegalStateException e) {
251            CarApiUtil.checkCarNotConnectedExceptionFromCarService(e);
252        } catch (RemoteException e) {
253            throw new CarNotConnectedException();
254        }
255        return new long[]{};
256    }
257
258    /**
259     * Retrieve the freeze frame event data for a given timestamp, if available.
260     * @param timestamp
261     * @return A CarDiagnostic event for the frame at the given timestamp, if one is
262     *         available. null is returned otherwise.
263     * Storage constraints might cause frames to be deleted from vehicle memory.
264     * For this reason it cannot be assumed that a timestamp will yield a valid frame,
265     * even if it was initially obtained via a call to getFreezeFrameTimestamps().
266     * @throws CarNotConnectedException
267     */
268    public @Nullable
269    CarDiagnosticEvent getFreezeFrame(long timestamp)
270        throws CarNotConnectedException {
271        try {
272            return mService.getFreezeFrame(timestamp);
273        } catch (IllegalStateException e) {
274            CarApiUtil.checkCarNotConnectedExceptionFromCarService(e);
275        } catch (RemoteException e) {
276            throw new CarNotConnectedException();
277        }
278        return null;
279    }
280
281    /**
282     * Clear the freeze frame information from vehicle memory at the given timestamps.
283     * @param timestamps A list of timestamps to delete freeze frames at, or an empty array
284     *                   to delete all freeze frames from vehicle memory.
285     * @return true if all the required frames were deleted (including if no timestamps are
286     *         provided and all frames were cleared); false otherwise.
287     * Due to storage constraints, timestamps cannot be assumed to be indefinitely valid, and
288     * a false return from this method should be used by the client as cause for invalidating
289     * its local knowledge of the vehicle diagnostic state.
290     * @throws CarNotConnectedException
291     */
292    public boolean clearFreezeFrames(long... timestamps) throws CarNotConnectedException {
293        try {
294            return mService.clearFreezeFrames(timestamps);
295        } catch (IllegalStateException e) {
296            CarApiUtil.checkCarNotConnectedExceptionFromCarService(e);
297        } catch (RemoteException e) {
298            throw new CarNotConnectedException();
299        }
300        return false;
301    }
302
303    /**
304     * Returns true if this vehicle supports sending live frame information.
305     * @return
306     * @throws CarNotConnectedException
307     */
308    public boolean isLiveFrameSupported() throws CarNotConnectedException {
309        try {
310            return mService.isLiveFrameSupported();
311        } catch (IllegalStateException e) {
312            CarApiUtil.checkCarNotConnectedExceptionFromCarService(e);
313        } catch (RemoteException e) {
314            throw new CarNotConnectedException();
315        }
316        return false;
317    }
318
319    /**
320     * Returns true if this vehicle supports supports sending notifications to
321     * registered listeners when new freeze frames happen.
322     * @throws CarNotConnectedException
323     */
324    public boolean isFreezeFrameNotificationSupported() throws CarNotConnectedException {
325        try {
326            return mService.isFreezeFrameNotificationSupported();
327        } catch (IllegalStateException e) {
328            CarApiUtil.checkCarNotConnectedExceptionFromCarService(e);
329        } catch (RemoteException e) {
330            throw new CarNotConnectedException();
331        }
332        return false;
333    }
334
335    /**
336     * Returns whether the underlying HAL supports retrieving freeze frames
337     * stored in vehicle memory using timestamp.
338     * @throws CarNotConnectedException
339     */
340    public boolean isGetFreezeFrameSupported() throws CarNotConnectedException {
341        try {
342            return mService.isGetFreezeFrameSupported();
343        } catch (IllegalStateException e) {
344            CarApiUtil.checkCarNotConnectedExceptionFromCarService(e);
345        } catch (RemoteException e) {
346            throw new CarNotConnectedException();
347        }
348        return false;
349    }
350
351    /**
352     * Returns true if this vehicle supports clearing all freeze frames.
353     * This is only meaningful if freeze frame data is also supported.
354     *
355     * A return value of true for this method indicates that it is supported to call
356     * carDiagnosticManager.clearFreezeFrames()
357     * to delete all freeze frames stored in vehicle memory.
358     *
359     * @return
360     * @throws CarNotConnectedException
361     */
362    public boolean isClearFreezeFramesSupported() throws CarNotConnectedException {
363        try {
364            return mService.isClearFreezeFramesSupported();
365        } catch (IllegalStateException e) {
366            CarApiUtil.checkCarNotConnectedExceptionFromCarService(e);
367        } catch (RemoteException e) {
368            throw new CarNotConnectedException();
369        }
370        return false;
371    }
372
373    /**
374     * Returns true if this vehicle supports clearing specific freeze frames by timestamp.
375     * This is only meaningful if freeze frame data is also supported.
376     *
377     * A return value of true for this method indicates that it is supported to call
378     * carDiagnosticManager.clearFreezeFrames(timestamp1, timestamp2, ...)
379     * to delete the freeze frames stored for the provided input timestamps, provided any exist.
380     *
381     * @return
382     * @throws CarNotConnectedException
383     */
384    public boolean isSelectiveClearFreezeFramesSupported() throws CarNotConnectedException {
385        try {
386            return mService.isSelectiveClearFreezeFramesSupported();
387        } catch (IllegalStateException e) {
388            CarApiUtil.checkCarNotConnectedExceptionFromCarService(e);
389        } catch (RemoteException e) {
390            throw new CarNotConnectedException();
391        }
392        return false;
393    }
394
395    private static class CarDiagnosticEventListenerToService
396            extends Stub {
397        private final WeakReference<CarDiagnosticManager> mManager;
398
399        public CarDiagnosticEventListenerToService(CarDiagnosticManager manager) {
400            mManager = new WeakReference<>(manager);
401        }
402
403        private void handleOnDiagnosticEvents(CarDiagnosticManager manager,
404            List<CarDiagnosticEvent> events) {
405            manager.mHandlerCallback.sendEvents(events);
406        }
407
408        @Override
409        public void onDiagnosticEvents(List<CarDiagnosticEvent> events) {
410            CarDiagnosticManager manager = mManager.get();
411            if (manager != null) {
412                handleOnDiagnosticEvents(manager, events);
413            }
414        }
415    }
416
417    private class CarDiagnosticListeners extends CarRatedListeners<OnDiagnosticEventListener> {
418        CarDiagnosticListeners(int rate) {
419            super(rate);
420        }
421
422        void onDiagnosticEvent(final CarDiagnosticEvent event) {
423            // throw away old data as oneway binder call can change order.
424            long updateTime = event.timestamp;
425            if (updateTime < mLastUpdateTime) {
426                Log.w(CarLibLog.TAG_DIAGNOSTIC, "dropping old data");
427                return;
428            }
429            mLastUpdateTime = updateTime;
430            final boolean hasVendorExtensionPermission = mVendorExtensionPermission.checkGranted();
431            final CarDiagnosticEvent eventToDispatch = hasVendorExtensionPermission ?
432                    event :
433                    event.withVendorSensorsRemoved();
434            List<OnDiagnosticEventListener> listeners;
435            synchronized (mActiveListeners) {
436                listeners = new ArrayList<>(getListeners());
437            }
438            listeners.forEach(new Consumer<OnDiagnosticEventListener>() {
439
440                @Override
441                public void accept(OnDiagnosticEventListener listener) {
442                    listener.onDiagnosticEvent(eventToDispatch);
443                }
444            });
445        }
446    }
447}
448