Post-O. Sketch out CarDiagnosticManager API surface

Provide an implementation of all layers required to plumb CarDiagnosticManager through:
 * CarDiagnosticEvent
 * DiagnosticHalService
 * CarDiagnosticService
 * CarDiagnosticManager

If FutureFeatures are enabled, this is integrated end-to-end enough to run trivial tests of the API

Test: build with TARGET_USES_CAR_FUTURE_FEATURES=true then at a shell runtest -x packages/services/Car/tests/android_car_api_test/ -c android.car.apitest.CarDiagnosticManagerTest
Change-Id: I0f2aafd039d26fec15182dd7029cf8c7995ce85b
diff --git a/service/src/com/android/car/CarDiagnosticService.java b/service/src/com/android/car/CarDiagnosticService.java
new file mode 100644
index 0000000..3a02010
--- /dev/null
+++ b/service/src/com/android/car/CarDiagnosticService.java
@@ -0,0 +1,793 @@
+/*
+ * 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.car;
+
+import android.annotation.Nullable;
+import android.car.annotation.FutureFeature;
+import android.car.hardware.CarDiagnosticEvent;
+import android.car.hardware.CarDiagnosticManager;
+import android.car.hardware.ICarDiagnostic;
+import android.car.hardware.ICarDiagnosticEventListener;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.util.ArrayMap;
+import android.util.Log;
+import com.android.car.Listeners.ClientWithRate;
+import com.android.car.hal.DiagnosticHalService;
+import com.android.internal.annotations.GuardedBy;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.ConcurrentModificationException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.locks.ReentrantLock;
+
+@FutureFeature
+/** @hide */
+public class CarDiagnosticService extends ICarDiagnostic.Stub
+        implements CarServiceBase, DiagnosticHalService.DiagnosticListener {
+    /** {@link #mDiagnosticLock} is not waited forever for handling disconnection */
+    private static final long MAX_DIAGNOSTIC_LOCK_WAIT_MS = 1000;
+
+    /** lock to access diagnostic structures */
+    private final ReentrantLock mDiagnosticLock = new ReentrantLock();
+    /** hold clients callback */
+    @GuardedBy("mDiagnosticLock")
+    private final LinkedList<DiagnosticClient> mClients = new LinkedList<>();
+
+    /** key: diagnostic type. */
+    @GuardedBy("mDiagnosticLock")
+    private final HashMap<Integer, Listeners<DiagnosticClient>> mDiagnosticListeners =
+        new HashMap<>();
+
+    /** the latest live frame data. */
+    @GuardedBy("mDiagnosticLock")
+    private final LiveFrameRecord mLiveFrameDiagnosticRecord = new LiveFrameRecord(mDiagnosticLock);
+
+    /** the latest freeze frame data (key: DTC) */
+    @GuardedBy("mDiagnosticLock")
+    private final FreezeFrameRecord mFreezeFrameDiagnosticRecords = new FreezeFrameRecord(
+        mDiagnosticLock);
+
+    private final DiagnosticHalService mDiagnosticHal;
+
+    private final Context mContext;
+
+    public CarDiagnosticService(Context context, DiagnosticHalService diagnosticHal) {
+        mContext = context;
+        mDiagnosticHal = diagnosticHal;
+    }
+
+    @Override
+    public void init() {
+        mDiagnosticLock.lock();
+        try {
+            mDiagnosticHal.setDiagnosticListener(this);
+            setInitialLiveFrame();
+            setInitialFreezeFrames();
+        } finally {
+            mDiagnosticLock.unlock();
+        }
+    }
+
+    private CarDiagnosticEvent setInitialLiveFrame() {
+        CarDiagnosticEvent liveFrame = setRecentmostLiveFrame(mDiagnosticHal.getCurrentLiveFrame());
+        return liveFrame;
+    }
+
+    private void setInitialFreezeFrames() {
+        for (long timestamp: mDiagnosticHal.getFreezeFrameTimestamps()) {
+            setRecentmostFreezeFrame(mDiagnosticHal.getFreezeFrame(timestamp));
+        }
+    }
+
+    private CarDiagnosticEvent setRecentmostLiveFrame(final CarDiagnosticEvent event) {
+        return mLiveFrameDiagnosticRecord.update(Objects.requireNonNull(event).checkLiveFrame());
+    }
+
+    private CarDiagnosticEvent setRecentmostFreezeFrame(final CarDiagnosticEvent event) {
+        return mFreezeFrameDiagnosticRecords.update(
+                Objects.requireNonNull(event).checkFreezeFrame());
+    }
+
+    @Override
+    public void release() {
+        mDiagnosticLock.lock();
+        try {
+            mDiagnosticListeners.forEach(
+                    (Integer frameType, Listeners diagnosticListeners) ->
+                            diagnosticListeners.release());
+            mDiagnosticListeners.clear();
+            mLiveFrameDiagnosticRecord.disableIfNeeded();
+            mFreezeFrameDiagnosticRecords.disableIfNeeded();
+            mClients.clear();
+        } finally {
+            mDiagnosticLock.unlock();
+        }
+    }
+
+    private void processDiagnosticData(List<CarDiagnosticEvent> events) {
+        ArrayMap<CarDiagnosticService.DiagnosticClient, List<CarDiagnosticEvent>> eventsByClient =
+                new ArrayMap<>();
+
+        Listeners<DiagnosticClient> listeners = null;
+
+        mDiagnosticLock.lock();
+        for (CarDiagnosticEvent event : events) {
+            if (event.isLiveFrame()) {
+                // record recent-most live frame information
+                setRecentmostLiveFrame(event);
+                listeners = mDiagnosticListeners.get(CarDiagnosticManager.FRAME_TYPE_FLAG_LIVE);
+            } else if (event.isFreezeFrame()) {
+                setRecentmostFreezeFrame(event);
+                listeners = mDiagnosticListeners.get(CarDiagnosticManager.FRAME_TYPE_FLAG_FREEZE);
+            } else {
+                Log.w(
+                        CarLog.TAG_DIAGNOSTIC,
+                        String.format("received unknown diagnostic event: %s", event));
+                continue;
+            }
+
+            if (null != listeners) {
+                for (ClientWithRate<DiagnosticClient> clientWithRate : listeners.getClients()) {
+                    DiagnosticClient client = clientWithRate.getClient();
+                    List<CarDiagnosticEvent> clientEvents = eventsByClient.computeIfAbsent(client,
+                            (DiagnosticClient diagnosticClient) -> new LinkedList<>());
+                    clientEvents.add(event);
+                }
+            }
+        }
+        mDiagnosticLock.unlock();
+
+        for (ArrayMap.Entry<CarDiagnosticService.DiagnosticClient, List<CarDiagnosticEvent>> entry :
+                eventsByClient.entrySet()) {
+            CarDiagnosticService.DiagnosticClient client = entry.getKey();
+            List<CarDiagnosticEvent> clientEvents = entry.getValue();
+
+            client.dispatchDiagnosticUpdate(clientEvents);
+        }
+    }
+
+    /** Received diagnostic data from car. */
+    @Override
+    public void onDiagnosticEvents(List<CarDiagnosticEvent> events) {
+        processDiagnosticData(events);
+    }
+
+    private List<CarDiagnosticEvent> getCachedEventsLocked(int frameType) {
+        ArrayList<CarDiagnosticEvent> events = new ArrayList<>();
+        switch (frameType) {
+            case CarDiagnosticManager.FRAME_TYPE_FLAG_LIVE:
+                mLiveFrameDiagnosticRecord.lock();
+                events.add(mLiveFrameDiagnosticRecord.getLastEvent());
+                mLiveFrameDiagnosticRecord.unlock();
+                break;
+            case CarDiagnosticManager.FRAME_TYPE_FLAG_FREEZE:
+                mFreezeFrameDiagnosticRecords.lock();
+                mFreezeFrameDiagnosticRecords.getEvents().forEach(events::add);
+                mFreezeFrameDiagnosticRecords.unlock();
+                break;
+            default: break;
+        }
+        return events;
+    }
+
+    private void assertPermission(int frameType) {
+        if (Binder.getCallingUid() != Process.myUid()) {
+            switch (getDiagnosticPermission(frameType)) {
+                case PackageManager.PERMISSION_GRANTED:
+                    break;
+                default:
+                    throw new SecurityException(
+                        "client does not have permission:"
+                            + getPermissionName(frameType)
+                            + " pid:"
+                            + Binder.getCallingPid()
+                            + " uid:"
+                            + Binder.getCallingUid());
+            }
+        }
+    }
+
+    @Override
+    public boolean registerOrUpdateDiagnosticListener(int frameType, int rate,
+                ICarDiagnosticEventListener listener) {
+        boolean shouldStartDiagnostics = false;
+        CarDiagnosticService.DiagnosticClient diagnosticClient = null;
+        Integer oldRate = null;
+        Listeners<DiagnosticClient> diagnosticListeners = null;
+        mDiagnosticLock.lock();
+        try {
+            assertPermission(frameType);
+            diagnosticClient = findDiagnosticClientLocked(listener);
+            Listeners.ClientWithRate<DiagnosticClient> diagnosticClientWithRate = null;
+            if (diagnosticClient == null) {
+                diagnosticClient = new DiagnosticClient(listener);
+                try {
+                    listener.asBinder().linkToDeath(diagnosticClient, 0);
+                } catch (RemoteException e) {
+                    Log.w(
+                            CarLog.TAG_DIAGNOSTIC,
+                            String.format(
+                                    "received RemoteException trying to register listener for %s",
+                                    frameType));
+                    return false;
+                }
+                mClients.add(diagnosticClient);
+            }
+            // If we have a cached event for this diagnostic, send the event.
+            diagnosticClient.dispatchDiagnosticUpdate(getCachedEventsLocked(frameType));
+            diagnosticListeners = mDiagnosticListeners.get(frameType);
+            if (diagnosticListeners == null) {
+                diagnosticListeners = new Listeners<>(rate);
+                mDiagnosticListeners.put(frameType, diagnosticListeners);
+                shouldStartDiagnostics = true;
+            } else {
+                oldRate = diagnosticListeners.getRate();
+                diagnosticClientWithRate =
+                        diagnosticListeners.findClientWithRate(diagnosticClient);
+            }
+            if (diagnosticClientWithRate == null) {
+                diagnosticClientWithRate =
+                        new ClientWithRate<>(diagnosticClient, rate);
+                diagnosticListeners.addClientWithRate(diagnosticClientWithRate);
+            } else {
+                diagnosticClientWithRate.setRate(rate);
+            }
+            if (diagnosticListeners.getRate() > rate) {
+                diagnosticListeners.setRate(rate);
+                shouldStartDiagnostics = true;
+            }
+            diagnosticClient.addDiagnostic(frameType);
+        } finally {
+            mDiagnosticLock.unlock();
+        }
+        Log.i(
+                CarLog.TAG_DIAGNOSTIC,
+                String.format(
+                        "shouldStartDiagnostics = %s for %s at rate %d",
+                        shouldStartDiagnostics, frameType, rate));
+        // start diagnostic outside lock as it can take time.
+        if (shouldStartDiagnostics) {
+            if (!startDiagnostic(frameType, rate)) {
+                // failed. so remove from active diagnostic list.
+                Log.w(CarLog.TAG_DIAGNOSTIC, "startDiagnostic failed");
+                mDiagnosticLock.lock();
+                try {
+                    diagnosticClient.removeDiagnostic(frameType);
+                    if (oldRate != null) {
+                        diagnosticListeners.setRate(oldRate);
+                    } else {
+                        mDiagnosticListeners.remove(frameType);
+                    }
+                } finally {
+                    mDiagnosticLock.unlock();
+                }
+                return false;
+            }
+        }
+        return true;
+    }
+
+    //TODO(egranata): handle permissions correctly
+    private int getDiagnosticPermission(int frameType) {
+        String permission = getPermissionName(frameType);
+        int result = PackageManager.PERMISSION_GRANTED;
+        if (permission != null) {
+            return mContext.checkCallingOrSelfPermission(permission);
+        }
+        // If no permission is required, return granted.
+        return result;
+    }
+
+    private String getPermissionName(int frameType) {
+        return null;
+    }
+
+    private boolean startDiagnostic(int frameType, int rate) {
+        Log.i(CarLog.TAG_DIAGNOSTIC, String.format("starting diagnostic %s at rate %d",
+                frameType, rate));
+        DiagnosticHalService diagnosticHal = getDiagnosticHal();
+        if (diagnosticHal != null) {
+            if (!diagnosticHal.isReady()) {
+                Log.w(CarLog.TAG_DIAGNOSTIC, "diagnosticHal not ready");
+                return false;
+            }
+            switch (frameType) {
+                case CarDiagnosticManager.FRAME_TYPE_FLAG_LIVE:
+                    if (mLiveFrameDiagnosticRecord.isEnabled()) {
+                        return true;
+                    }
+                    if (diagnosticHal.requestSensorStart(CarDiagnosticManager.FRAME_TYPE_FLAG_LIVE,
+                            rate)) {
+                        mLiveFrameDiagnosticRecord.enable();
+                        return true;
+                    }
+                    break;
+                case CarDiagnosticManager.FRAME_TYPE_FLAG_FREEZE:
+                    if (mFreezeFrameDiagnosticRecords.isEnabled()) {
+                        return true;
+                    }
+                    if (diagnosticHal.requestSensorStart(CarDiagnosticManager.FRAME_TYPE_FLAG_FREEZE,
+                            rate)) {
+                        mFreezeFrameDiagnosticRecords.enable();
+                        return true;
+                    }
+                    break;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public void unregisterDiagnosticListener(
+            int frameType, ICarDiagnosticEventListener listener) {
+        boolean shouldStopDiagnostic = false;
+        boolean shouldRestartDiagnostic = false;
+        int newRate = 0;
+        mDiagnosticLock.lock();
+        try {
+            DiagnosticClient diagnosticClient = findDiagnosticClientLocked(listener);
+            if (diagnosticClient == null) {
+                Log.i(
+                        CarLog.TAG_DIAGNOSTIC,
+                        String.format(
+                                "trying to unregister diagnostic client %s for %s which is not registered",
+                                listener, frameType));
+                // never registered or already unregistered.
+                return;
+            }
+            diagnosticClient.removeDiagnostic(frameType);
+            if (diagnosticClient.getNumberOfActiveDiagnostic() == 0) {
+                diagnosticClient.release();
+                mClients.remove(diagnosticClient);
+            }
+            Listeners<DiagnosticClient> diagnosticListeners = mDiagnosticListeners.get(frameType);
+            if (diagnosticListeners == null) {
+                // diagnostic not active
+                return;
+            }
+            ClientWithRate<DiagnosticClient> clientWithRate =
+                    diagnosticListeners.findClientWithRate(diagnosticClient);
+            if (clientWithRate == null) {
+                return;
+            }
+            diagnosticListeners.removeClientWithRate(clientWithRate);
+            if (diagnosticListeners.getNumberOfClients() == 0) {
+                shouldStopDiagnostic = true;
+                mDiagnosticListeners.remove(frameType);
+            } else if (diagnosticListeners.updateRate()) { // rate changed
+                newRate = diagnosticListeners.getRate();
+                shouldRestartDiagnostic = true;
+            }
+        } finally {
+            mDiagnosticLock.unlock();
+        }
+        Log.i(
+                CarLog.TAG_DIAGNOSTIC,
+                String.format(
+                        "shouldStopDiagnostic = %s, shouldRestartDiagnostic = %s for type %s",
+                        shouldStopDiagnostic, shouldRestartDiagnostic, frameType));
+        if (shouldStopDiagnostic) {
+            stopDiagnostic(frameType);
+        } else if (shouldRestartDiagnostic) {
+            startDiagnostic(frameType, newRate);
+        }
+    }
+
+    private void stopDiagnostic(int frameType) {
+        DiagnosticHalService diagnosticHal = getDiagnosticHal();
+        if (diagnosticHal == null || !diagnosticHal.isReady()) {
+            Log.w(CarLog.TAG_DIAGNOSTIC, "diagnosticHal not ready");
+            return;
+        }
+        switch (frameType) {
+            case CarDiagnosticManager.FRAME_TYPE_FLAG_LIVE:
+                if (mLiveFrameDiagnosticRecord.disableIfNeeded())
+                    diagnosticHal.requestSensorStop(CarDiagnosticManager.FRAME_TYPE_FLAG_LIVE);
+                break;
+            case CarDiagnosticManager.FRAME_TYPE_FLAG_FREEZE:
+                if (mFreezeFrameDiagnosticRecords.disableIfNeeded())
+                    diagnosticHal.requestSensorStop(CarDiagnosticManager.FRAME_TYPE_FLAG_FREEZE);
+                break;
+        }
+    }
+
+    private DiagnosticHalService getDiagnosticHal() {
+        return mDiagnosticHal;
+    }
+
+    // ICarDiagnostic implementations
+
+    @Override
+    public CarDiagnosticEvent getLatestLiveFrame() {
+        mLiveFrameDiagnosticRecord.lock();
+        CarDiagnosticEvent liveFrame = mLiveFrameDiagnosticRecord.getLastEvent();
+        mLiveFrameDiagnosticRecord.unlock();
+        return liveFrame;
+    }
+
+    @Override
+    public long[] getFreezeFrameTimestamps() {
+        mFreezeFrameDiagnosticRecords.lock();
+        long[] timestamps = mFreezeFrameDiagnosticRecords.getFreezeFrameTimestamps();
+        mFreezeFrameDiagnosticRecords.unlock();
+        return timestamps;
+    }
+
+    @Override
+    @Nullable
+    public CarDiagnosticEvent getFreezeFrame(long timestamp) {
+        mFreezeFrameDiagnosticRecords.lock();
+        CarDiagnosticEvent freezeFrame = mFreezeFrameDiagnosticRecords.getEvent(timestamp);
+        mFreezeFrameDiagnosticRecords.unlock();
+        return freezeFrame;
+    }
+
+    @Override
+    public boolean clearFreezeFrames(long... timestamps) {
+        mFreezeFrameDiagnosticRecords.lock();
+        mDiagnosticHal.clearFreezeFrames(timestamps);
+        mFreezeFrameDiagnosticRecords.clearEvents();
+        mFreezeFrameDiagnosticRecords.unlock();
+        return true;
+    }
+
+    /**
+     * Find DiagnosticClient from client list and return it. This should be called with mClients
+     * locked.
+     *
+     * @param listener
+     * @return null if not found.
+     */
+    private CarDiagnosticService.DiagnosticClient findDiagnosticClientLocked(
+            ICarDiagnosticEventListener listener) {
+        IBinder binder = listener.asBinder();
+        for (DiagnosticClient diagnosticClient : mClients) {
+            if (diagnosticClient.isHoldingListenerBinder(binder)) {
+                return diagnosticClient;
+            }
+        }
+        return null;
+    }
+
+    private void removeClient(DiagnosticClient diagnosticClient) {
+        mDiagnosticLock.lock();
+        try {
+            for (int diagnostic : diagnosticClient.getDiagnosticArray()) {
+                unregisterDiagnosticListener(
+                        diagnostic, diagnosticClient.getICarDiagnosticEventListener());
+            }
+            mClients.remove(diagnosticClient);
+        } finally {
+            mDiagnosticLock.unlock();
+        }
+    }
+
+    private class DiagnosticDispatchHandler extends Handler {
+        private static final long DIAGNOSTIC_DISPATCH_MIN_INTERVAL_MS = 16; // over 60Hz
+
+        private static final int MSG_DIAGNOSTIC_DATA = 0;
+
+        private long mLastDiagnosticDispatchTime = -1;
+        private int mFreeListIndex = 0;
+        private final LinkedList<CarDiagnosticEvent>[] mDiagnosticDataList = new LinkedList[2];
+
+        private DiagnosticDispatchHandler(Looper looper) {
+            super(looper);
+            for (int i = 0; i < mDiagnosticDataList.length; i++) {
+                mDiagnosticDataList[i] = new LinkedList<CarDiagnosticEvent>();
+            }
+        }
+
+        private synchronized void handleDiagnosticEvents(List<CarDiagnosticEvent> data) {
+            LinkedList<CarDiagnosticEvent> list = mDiagnosticDataList[mFreeListIndex];
+            list.addAll(data);
+            requestDispatchLocked();
+        }
+
+        private synchronized void handleDiagnosticEvent(CarDiagnosticEvent event) {
+            LinkedList<CarDiagnosticEvent> list = mDiagnosticDataList[mFreeListIndex];
+            list.add(event);
+            requestDispatchLocked();
+        }
+
+        private void requestDispatchLocked() {
+            Message msg = obtainMessage(MSG_DIAGNOSTIC_DATA);
+            long now = SystemClock.uptimeMillis();
+            long delta = now - mLastDiagnosticDispatchTime;
+            if (delta > DIAGNOSTIC_DISPATCH_MIN_INTERVAL_MS) {
+                sendMessage(msg);
+            } else {
+                sendMessageDelayed(msg, DIAGNOSTIC_DISPATCH_MIN_INTERVAL_MS - delta);
+            }
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_DIAGNOSTIC_DATA:
+                    doHandleDiagnosticData();
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        private void doHandleDiagnosticData() {
+            List<CarDiagnosticEvent> listToDispatch = null;
+            synchronized (this) {
+                mLastDiagnosticDispatchTime = SystemClock.uptimeMillis();
+                int nonFreeListIndex = mFreeListIndex ^ 0x1;
+                List<CarDiagnosticEvent> nonFreeList = mDiagnosticDataList[nonFreeListIndex];
+                List<CarDiagnosticEvent> freeList = mDiagnosticDataList[mFreeListIndex];
+                if (nonFreeList.size() > 0) {
+                    // copy again, but this should not be normal case
+                    nonFreeList.addAll(freeList);
+                    listToDispatch = nonFreeList;
+                    freeList.clear();
+                } else if (freeList.size() > 0) {
+                    listToDispatch = freeList;
+                    mFreeListIndex = nonFreeListIndex;
+                }
+            }
+            // leave this part outside lock so that time-taking dispatching can be done without
+            // blocking diagnostic event notification.
+            if (listToDispatch != null) {
+                processDiagnosticData(listToDispatch);
+                listToDispatch.clear();
+            }
+        }
+    }
+
+    /** internal instance for pending client request */
+    private class DiagnosticClient implements Listeners.IListener {
+        /** callback for diagnostic events */
+        private final ICarDiagnosticEventListener mListener;
+
+        private final Set<Integer> mActiveDiagnostics = new HashSet<>();
+
+        /** when false, it is already released */
+        private volatile boolean mActive = true;
+
+        DiagnosticClient(ICarDiagnosticEventListener listener) {
+            this.mListener = listener;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o instanceof CarDiagnosticService.DiagnosticClient
+                    && mListener.asBinder()
+                            == ((CarDiagnosticService.DiagnosticClient) o).mListener.asBinder()) {
+                return true;
+            }
+            return false;
+        }
+
+        boolean isHoldingListenerBinder(IBinder listenerBinder) {
+            return mListener.asBinder() == listenerBinder;
+        }
+
+        void addDiagnostic(int frameType) {
+            mActiveDiagnostics.add(frameType);
+        }
+
+        void removeDiagnostic(int frameType) {
+            mActiveDiagnostics.remove(frameType);
+        }
+
+        int getNumberOfActiveDiagnostic() {
+            return mActiveDiagnostics.size();
+        }
+
+        int[] getDiagnosticArray() {
+            return mActiveDiagnostics.stream().mapToInt(Integer::intValue).toArray();
+        }
+
+        ICarDiagnosticEventListener getICarDiagnosticEventListener() {
+            return mListener;
+        }
+
+        /** Client dead. should remove all diagnostic requests from client */
+        @Override
+        public void binderDied() {
+            mListener.asBinder().unlinkToDeath(this, 0);
+            removeClient(this);
+        }
+
+        void dispatchDiagnosticUpdate(List<CarDiagnosticEvent> events) {
+            if (events.size() == 0) {
+                return;
+            }
+            if (mActive) {
+                try {
+                    mListener.onDiagnosticEvents(events);
+                } catch (RemoteException e) {
+                    //ignore. crash will be handled by death handler
+                }
+            } else {
+            }
+        }
+
+        @Override
+        public void release() {
+            if (mActive) {
+                mListener.asBinder().unlinkToDeath(this, 0);
+                mActiveDiagnostics.clear();
+                mActive = false;
+            }
+        }
+    }
+
+    private static abstract class DiagnosticRecord {
+        private final ReentrantLock mLock;
+        protected boolean mEnabled = false;
+
+        DiagnosticRecord(ReentrantLock lock) {
+            mLock = lock;
+        }
+
+        void lock() {
+            mLock.lock();
+        }
+
+        void unlock() {
+            mLock.unlock();
+        }
+
+        boolean isEnabled() {
+            return mEnabled;
+        }
+
+        void enable() {
+            mEnabled = true;
+        }
+
+        abstract boolean disableIfNeeded();
+        abstract CarDiagnosticEvent update(CarDiagnosticEvent newEvent);
+    }
+
+    private static class LiveFrameRecord extends DiagnosticRecord {
+        /** Store the most recent live-frame. */
+        CarDiagnosticEvent mLastEvent = null;
+
+        LiveFrameRecord(ReentrantLock lock) {
+            super(lock);
+        }
+
+        @Override
+        boolean disableIfNeeded() {
+            if (!mEnabled) return false;
+            mEnabled = false;
+            mLastEvent = null;
+            return true;
+        }
+
+        @Override
+        CarDiagnosticEvent update(CarDiagnosticEvent newEvent) {
+            newEvent = Objects.requireNonNull(newEvent);
+            if((null == mLastEvent) || mLastEvent.isEarlierThan(newEvent))
+                mLastEvent = newEvent;
+            return mLastEvent;
+        }
+
+        CarDiagnosticEvent getLastEvent() {
+            return mLastEvent;
+        }
+    }
+
+    private static class FreezeFrameRecord extends DiagnosticRecord {
+        /** Store the timestamp --> freeze frame mapping. */
+        HashMap<Long, CarDiagnosticEvent> mEvents = new HashMap<>();
+
+        FreezeFrameRecord(ReentrantLock lock) {
+            super(lock);
+        }
+
+        @Override
+        boolean disableIfNeeded() {
+            if (!mEnabled) return false;
+            mEnabled = false;
+            clearEvents();
+            return true;
+        }
+
+        void clearEvents() {
+            mEvents.clear();
+        }
+
+        @Override
+        CarDiagnosticEvent update(CarDiagnosticEvent newEvent) {
+            mEvents.put(newEvent.timestamp, newEvent);
+            return newEvent;
+        }
+
+        long[] getFreezeFrameTimestamps() {
+            return mEvents.keySet().stream().mapToLong(Long::longValue).toArray();
+        }
+
+        CarDiagnosticEvent getEvent(long timestamp) {
+            return mEvents.get(timestamp);
+        }
+
+        Iterable<CarDiagnosticEvent> getEvents() {
+            return mEvents.values();
+        }
+    }
+
+    @Override
+    public void dump(PrintWriter writer) {
+        writer.println("*CarDiagnosticService*");
+        writer.println("**last events for diagnostics**");
+        if (null != mLiveFrameDiagnosticRecord.getLastEvent()) {
+            writer.println("last live frame event: ");
+            writer.println(mLiveFrameDiagnosticRecord.getLastEvent());
+        }
+        writer.println("freeze frame events: ");
+        mFreezeFrameDiagnosticRecords.getEvents().forEach(writer::println);
+        writer.println("**clients**");
+        try {
+            for (DiagnosticClient client : mClients) {
+                if (client != null) {
+                    try {
+                        writer.println(
+                                "binder:"
+                                        + client.mListener
+                                        + " active diagnostics:"
+                                        + Arrays.toString(client.getDiagnosticArray()));
+                    } catch (ConcurrentModificationException e) {
+                        writer.println("concurrent modification happened");
+                    }
+                } else {
+                    writer.println("null client");
+                }
+            }
+        } catch (ConcurrentModificationException e) {
+            writer.println("concurrent modification happened");
+        }
+        writer.println("**diagnostic listeners**");
+        try {
+            for (int diagnostic : mDiagnosticListeners.keySet()) {
+                Listeners diagnosticListeners = mDiagnosticListeners.get(diagnostic);
+                if (diagnosticListeners != null) {
+                    writer.println(
+                            " Diagnostic:"
+                                    + diagnostic
+                                    + " num client:"
+                                    + diagnosticListeners.getNumberOfClients()
+                                    + " rate:"
+                                    + diagnosticListeners.getRate());
+                }
+            }
+        } catch (ConcurrentModificationException e) {
+            writer.println("concurrent modification happened");
+        }
+    }
+}