/*
 * 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.audio;

import android.annotation.NonNull;
import android.media.AudioDeviceAttributes;
import android.media.AudioManager;
import android.media.AudioSystem;
import android.media.MediaMetrics;

import com.android.server.audio.AudioDeviceInventory.WiredDeviceConnectionState;
import com.android.server.utils.EventLogger;


public class AudioServiceEvents {

    final static class PhoneStateEvent extends EventLogger.Event {
        static final int MODE_SET = 0;
        static final int MODE_IN_COMMUNICATION_TIMEOUT = 1;

        final int mOp;
        final String mPackage;
        final int mOwnerPid;
        final int mRequesterPid;
        final int mRequestedMode;
        final int mActualMode;

        /** used for MODE_SET */
        PhoneStateEvent(String callingPackage, int requesterPid, int requestedMode,
                        int ownerPid, int actualMode) {
            mOp = MODE_SET;
            mPackage = callingPackage;
            mRequesterPid = requesterPid;
            mRequestedMode = requestedMode;
            mOwnerPid = ownerPid;
            mActualMode = actualMode;
            logMetricEvent();
        }

        /** used for MODE_IN_COMMUNICATION_TIMEOUT */
        PhoneStateEvent(String callingPackage, int ownerPid) {
            mOp = MODE_IN_COMMUNICATION_TIMEOUT;
            mPackage = callingPackage;
            mOwnerPid = ownerPid;
            mRequesterPid = 0;
            mRequestedMode = 0;
            mActualMode = 0;
            logMetricEvent();
        }

        @Override
        public String eventToString() {
            switch (mOp) {
                case MODE_SET:
                    return new StringBuilder("setMode(")
                            .append(AudioSystem.modeToString(mRequestedMode))
                            .append(") from package=").append(mPackage)
                            .append(" pid=").append(mRequesterPid)
                            .append(" selected mode=")
                            .append(AudioSystem.modeToString(mActualMode))
                            .append(" by pid=").append(mOwnerPid).toString();
                case MODE_IN_COMMUNICATION_TIMEOUT:
                    return new StringBuilder("mode IN COMMUNICATION timeout")
                            .append(" for package=").append(mPackage)
                            .append(" pid=").append(mOwnerPid).toString();
                default: return new StringBuilder("FIXME invalid op:").append(mOp).toString();
            }
        }

        /**
         * Audio Analytics unique Id.
         */
        private static final String mMetricsId = MediaMetrics.Name.AUDIO_MODE;

        private void logMetricEvent() {
            switch (mOp) {
                case MODE_SET:
                    new MediaMetrics.Item(mMetricsId)
                            .set(MediaMetrics.Property.EVENT, "set")
                            .set(MediaMetrics.Property.REQUESTED_MODE,
                                    AudioSystem.modeToString(mRequestedMode))
                            .set(MediaMetrics.Property.MODE, AudioSystem.modeToString(mActualMode))
                            .set(MediaMetrics.Property.CALLING_PACKAGE, mPackage)
                            .record();
                    return;
                case MODE_IN_COMMUNICATION_TIMEOUT:
                    new MediaMetrics.Item(mMetricsId)
                            .set(MediaMetrics.Property.EVENT, "inCommunicationTimeout")
                            .set(MediaMetrics.Property.CALLING_PACKAGE, mPackage)
                            .record();
                    return;
                default: return;
            }
        }
    }

    final static class WiredDevConnectEvent extends EventLogger.Event {
        final WiredDeviceConnectionState mState;

        WiredDevConnectEvent(WiredDeviceConnectionState state) {
            mState = state;
        }

        @Override
        public String eventToString() {
            return new StringBuilder("setWiredDeviceConnectionState(")
                    .append(" type:").append(
                            Integer.toHexString(mState.mAttributes.getInternalType()))
                    .append(" (").append(AudioSystem.isInputDevice(
                            mState.mAttributes.getInternalType()) ? "source" : "sink").append(") ")
                    .append(" state:").append(AudioSystem.deviceStateToString(mState.mState))
                    .append(" addr:").append(mState.mAttributes.getAddress())
                    .append(" name:").append(mState.mAttributes.getName())
                    .append(") from ").append(mState.mCaller).toString();
        }
    }

    final static class ForceUseEvent extends EventLogger.Event {
        final int mUsage;
        final int mConfig;
        final String mReason;

        ForceUseEvent(int usage, int config, String reason) {
            mUsage = usage;
            mConfig = config;
            mReason = reason;
        }

        @Override
        public String eventToString() {
            return new StringBuilder("setForceUse(")
                    .append(AudioSystem.forceUseUsageToString(mUsage))
                    .append(", ").append(AudioSystem.forceUseConfigToString(mConfig))
                    .append(") due to ").append(mReason).toString();
        }
    }

    static final class VolChangedBroadcastEvent extends EventLogger.Event {
        final int mStreamType;
        final String mAliasStreamIndexes;
        final int mIndex;
        final int mOldIndex;

        VolChangedBroadcastEvent(int stream, String aliasIndexes, int index, int oldIndex) {
            mStreamType = stream;
            mAliasStreamIndexes = aliasIndexes;
            mIndex = index;
            mOldIndex = oldIndex;
        }

        @Override
        public String eventToString() {
            return new StringBuilder("sending VOLUME_CHANGED stream:")
                    .append(AudioSystem.streamToString(mStreamType))
                    .append(" index:").append(mIndex)
                    .append(" (was:").append(mOldIndex).append(")")
                    .append(mAliasStreamIndexes)
                    .toString();
        }
    }

    static final class DeviceVolumeEvent extends EventLogger.Event {
        static final int DEV_VOL_SET_DEVICE_VOLUME = 0;
        static final int DEV_VOL_NOTIFY_ABSOLUTE_VOLUME_CHANGED = 1;

        final int mEvent;
        final int mStream;
        final int mVolIndex;
        final String mDeviceNativeType;
        final String mDeviceAddress;
        final String mCaller;
        int mDeviceForStream;
        boolean mSkipped;
        boolean mIsCurDevice;

        DeviceVolumeEvent(int streamType, int index, @NonNull AudioDeviceAttributes device,
                int deviceForStream, String callingPackage, boolean skipped, String event) {
            mEvent = DEV_VOL_SET_DEVICE_VOLUME;
            mStream = streamType;
            mVolIndex = index;
            mDeviceNativeType = "0x" + Integer.toHexString(device.getInternalType());
            mDeviceAddress = device.getAddress();
            mDeviceForStream = deviceForStream;
            mCaller = callingPackage;
            mSkipped = skipped;
            // log metrics
            new MediaMetrics.Item(MediaMetrics.Name.AUDIO_VOLUME_EVENT)
                    .set(MediaMetrics.Property.EVENT, event)
                    .set(MediaMetrics.Property.STREAM_TYPE,
                            AudioSystem.streamToString(mStream))
                    .set(MediaMetrics.Property.INDEX, mVolIndex)
                    .set(MediaMetrics.Property.DEVICE, mDeviceNativeType)
                    .set(MediaMetrics.Property.ADDRESS, mDeviceAddress)
                    .set(MediaMetrics.Property.CALLING_PACKAGE, mCaller)
                    .record();
        }

        DeviceVolumeEvent(int streamType, int index, @NonNull AudioDeviceAttributes device,
                String callingPackage, boolean isCurDevice) {
            mEvent = DEV_VOL_NOTIFY_ABSOLUTE_VOLUME_CHANGED;
            mStream = streamType;
            mVolIndex = index;
            mDeviceNativeType = "0x" + Integer.toHexString(device.getInternalType());
            mDeviceAddress = device.getAddress();
            mCaller = callingPackage;
            mIsCurDevice = isCurDevice;
            // log metrics
            new MediaMetrics.Item(MediaMetrics.Name.AUDIO_VOLUME_EVENT)
                    .set(MediaMetrics.Property.EVENT, "notifyAbsoluteVolumeChanged")
                    .set(MediaMetrics.Property.STREAM_TYPE,
                            AudioSystem.streamToString(mStream))
                    .set(MediaMetrics.Property.INDEX, mVolIndex)
                    .set(MediaMetrics.Property.DEVICE, mDeviceNativeType)
                    .set(MediaMetrics.Property.ADDRESS, mDeviceAddress)
                    .set(MediaMetrics.Property.CALLING_PACKAGE, mCaller)
                    .record();
        }

        @Override
        public String eventToString() {
            switch(mEvent) {
                case DEV_VOL_SET_DEVICE_VOLUME: {
                    final StringBuilder sb = new StringBuilder("setDeviceVolume")
                            .append("(stream:").append(AudioSystem.streamToString(mStream))
                            .append(" index:").append(mVolIndex)
                            .append(" device:").append(mDeviceNativeType)
                            .append(" addr:").append(mDeviceAddress)
                            .append(") from ").append(mCaller);
                    if (mSkipped) {
                        sb.append(" skipped [device in use]");
                    } else if (mDeviceForStream != AudioSystem.DEVICE_NONE) {
                        sb.append(" currDevForStream:Ox").append(
                                Integer.toHexString(mDeviceForStream));
                    }
                    return sb.toString();
                }
                case DEV_VOL_NOTIFY_ABSOLUTE_VOLUME_CHANGED: {
                    final StringBuilder sb = new StringBuilder("notifyAbsoluteVolumeChanged")
                            .append("(stream:").append(AudioSystem.streamToString(mStream))
                            .append(" index:").append(mVolIndex)
                            .append(" device:").append(mDeviceNativeType)
                            .append(" addr:").append(mDeviceAddress)
                            .append("[").append(mIsCurDevice ? "curr dev]" : "not curr dev]")
                            .append(") from ").append(mCaller);
                    return sb.toString();
                }
                default: return "wrong event";
            }
        }
    }

    final static class VolumeEvent extends EventLogger.Event {
        static final int VOL_ADJUST_SUGG_VOL = 0;
        static final int VOL_ADJUST_STREAM_VOL = 1;
        static final int VOL_SET_STREAM_VOL = 2;
        static final int VOL_SET_HEARING_AID_VOL = 3;
        static final int VOL_SET_AVRCP_VOL = 4;
        static final int VOL_ADJUST_VOL_UID = 5;
        static final int VOL_VOICE_ACTIVITY_CONTEXTUAL_VOLUME = 6;
        static final int VOL_MODE_CHANGE_HEARING_AID = 7;
        static final int VOL_SET_GROUP_VOL = 8;
        static final int VOL_MUTE_STREAM_INT = 9;
        static final int VOL_SET_LE_AUDIO_VOL = 10;
        static final int VOL_ADJUST_GROUP_VOL = 11;
        static final int VOL_MASTER_MUTE = 12;
        static final int VOL_ABS_DEVICE_ENABLED_ERROR = 13;
        static final int VOL_SET_ABS_VOL = 14;

        final int mOp;
        final int mStream;
        final int mVal1;
        final int mVal2;
        final int mVal3;
        final String mCaller;
        final String mGroupName;

        /** used for VOL_SET_STREAM_VOL */
        VolumeEvent(int op, int stream, int val1, int val2, int val3, String caller) {
            mOp = op;
            mStream = stream;
            mVal1 = val1;
            mVal2 = val2;
            mVal3 = val3;
            mCaller = caller;
            // unused
            mGroupName = null;
            logMetricEvent();
        }

        /** used for VOL_ADJUST_VOL_UID,
         *           VOL_ADJUST_SUGG_VOL,
         *           VOL_ADJUST_STREAM_VOL,
         *           VOL_SET_LE_AUDIO_VOL
         */
        VolumeEvent(int op, int stream, int val1, int val2, String caller) {
            mOp = op;
            mStream = stream;
            mVal1 = val1;
            mVal2 = val2;
            mCaller = caller;
            // unused
            mVal3 = -1;
            mGroupName = null;
            logMetricEvent();
        }

        /** used for VOL_SET_HEARING_AID_VOL*/
        VolumeEvent(int op, int index, int gainDb) {
            mOp = op;
            mVal1 = index;
            mVal2 = gainDb;
            // unused
            mVal3 = -1;
            mStream = -1;
            mCaller = null;
            mGroupName = null;
            logMetricEvent();
        }

        /** used for VOL_SET_AVRCP_VOL */
        VolumeEvent(int op, int index) {
            mOp = op;
            mVal1 = index;
            // unused
            mVal2 = 0;
            mVal3 = -1;
            mStream = -1;
            mCaller = null;
            mGroupName = null;
            logMetricEvent();
        }

        /** used for VOL_SET_ABS_VOL */
        VolumeEvent(int op, int stream, int index, int device, boolean muted) {
            mOp = op;
            mVal1 = index;
            // unused
            mVal2 = device;
            mVal3 = muted ? 1 : 0;
            mStream = stream;
            mCaller = null;
            mGroupName = null;
            logMetricEvent();
        }

        /** used for VOL_VOICE_ACTIVITY_CONTEXTUAL_VOLUME */
        VolumeEvent(int op, boolean voiceActive, int stream, int index, int device) {
            mOp = op;
            mStream = stream;
            mVal1 = index;
            mVal2 = voiceActive ? 1 : 0;
            // unused
            mVal3 = device;
            mCaller = null;
            mGroupName = null;
            logMetricEvent();
        }

        /** used for VOL_MODE_CHANGE_HEARING_AID */
        VolumeEvent(int op, int mode, int stream, int index) {
            mOp = op;
            mStream = stream;
            mVal1 = index;
            mVal2 = mode;
            // unused
            mVal3 = -1;
            mCaller = null;
            mGroupName = null;
            logMetricEvent();
        }

        /** used for VOL_SET_GROUP_VOL,
         *           VOL_ADJUST_GROUP_VOL */
        VolumeEvent(int op, String group, int index, int flags, String caller) {
            mOp = op;
            mStream = -1;
            mVal1 = index;
            mVal2 = flags;
            mCaller = caller;
            mGroupName = group;
            // unused
            mVal3 = -1;
            logMetricEvent();
        }

        /** used for VOL_MUTE_STREAM_INT */
        VolumeEvent(int op, int stream, boolean state) {
            mOp = op;
            mStream = stream;
            mVal1 = state ? 1 : 0;
            mVal2 = 0;
            // unused
            mCaller = null;
            mGroupName = null;
            mVal3 = -1;
            logMetricEvent();
        }

        /** used for VOL_MASTER_MUTE */
        VolumeEvent(int op, boolean state) {
            mOp = op;
            mStream = -1;
            mVal1 = state ? 1 : 0;
            mVal2 = 0;
            // unused
            mVal3 = -1;
            mCaller = null;
            mGroupName = null;
            logMetricEvent();
        }

        /** used for VOL_ABS_DEVICE_ENABLED_ERROR */
        VolumeEvent(int op, int result, int device, boolean enabled, int streamType) {
            mOp = op;
            mStream = streamType;
            mVal1 = device;
            mVal2 = enabled ? 1 : 0;
            mVal3 = result;
            // unused
            mCaller = null;
            mGroupName = null;
            logMetricEvent();
        }


        /**
         * Audio Analytics unique Id.
         */
        private static final String mMetricsId = MediaMetrics.Name.AUDIO_VOLUME_EVENT;

        /**
         * Log mediametrics event
         */
        private void logMetricEvent() {
            switch (mOp) {
                case VOL_ADJUST_SUGG_VOL:
                case VOL_ADJUST_VOL_UID:
                case VOL_ADJUST_STREAM_VOL: {
                    String eventName;
                    switch (mOp) {
                        case VOL_ADJUST_SUGG_VOL:
                            eventName = "adjustSuggestedStreamVolume";
                            break;
                        case VOL_ADJUST_STREAM_VOL:
                            eventName = "adjustStreamVolume";
                            break;
                        case VOL_ADJUST_VOL_UID:
                            eventName = "adjustStreamVolumeForUid";
                            break;
                        default:
                            return; // not possible, just return here
                    }
                    new MediaMetrics.Item(mMetricsId)
                            .set(MediaMetrics.Property.CALLING_PACKAGE, mCaller)
                            .set(MediaMetrics.Property.DIRECTION, mVal1 > 0 ? "up" : "down")
                            .set(MediaMetrics.Property.EVENT, eventName)
                            .set(MediaMetrics.Property.FLAGS, mVal2)
                            .set(MediaMetrics.Property.STREAM_TYPE,
                                    AudioSystem.streamToString(mStream))
                            .record();
                    return;
                }
                case VOL_ADJUST_GROUP_VOL:
                    new MediaMetrics.Item(mMetricsId)
                            .set(MediaMetrics.Property.CALLING_PACKAGE, mCaller)
                            .set(MediaMetrics.Property.DIRECTION, mVal1 > 0 ? "up" : "down")
                            .set(MediaMetrics.Property.EVENT, "adjustVolumeGroupVolume")
                            .set(MediaMetrics.Property.FLAGS, mVal2)
                            .set(MediaMetrics.Property.GROUP, mGroupName)
                            .record();
                    return;
                case VOL_SET_STREAM_VOL:
                    new MediaMetrics.Item(mMetricsId)
                            .set(MediaMetrics.Property.CALLING_PACKAGE, mCaller)
                            .set(MediaMetrics.Property.EVENT, "setStreamVolume")
                            .set(MediaMetrics.Property.FLAGS, mVal2)
                            .set(MediaMetrics.Property.INDEX, mVal1)
                            .set(MediaMetrics.Property.OLD_INDEX, mVal3)
                            .set(MediaMetrics.Property.STREAM_TYPE,
                                    AudioSystem.streamToString(mStream))
                            .record();
                    return;
                case VOL_SET_HEARING_AID_VOL:
                    new MediaMetrics.Item(mMetricsId)
                            .set(MediaMetrics.Property.EVENT, "setHearingAidVolume")
                            .set(MediaMetrics.Property.GAIN_DB, (double) mVal2)
                            .set(MediaMetrics.Property.INDEX, mVal1)
                            .record();
                    return;
                case VOL_SET_LE_AUDIO_VOL:
                    new MediaMetrics.Item(mMetricsId)
                            .set(MediaMetrics.Property.EVENT, "setLeAudioVolume")
                            .set(MediaMetrics.Property.INDEX, mVal1)
                            .set(MediaMetrics.Property.MAX_INDEX, mVal2)
                            .set(MediaMetrics.Property.STREAM_TYPE,
                                    AudioSystem.streamToString(mStream))
                            .record();
                    return;
                case VOL_SET_AVRCP_VOL:
                    new MediaMetrics.Item(mMetricsId)
                            .set(MediaMetrics.Property.EVENT, "setAvrcpVolume")
                            .set(MediaMetrics.Property.INDEX, mVal1)
                            .record();
                    return;
                case VOL_SET_ABS_VOL:
                    new MediaMetrics.Item(mMetricsId)
                            .set(MediaMetrics.Property.EVENT, "setAbsoluteVolume")
                            .set(MediaMetrics.Property.STREAM_TYPE,
                                    AudioSystem.streamToString(mStream))
                            .set(MediaMetrics.Property.INDEX, mVal3 == 1 ? 0 : mVal1)
                            .set(MediaMetrics.Property.DEVICE,
                                    AudioSystem.getOutputDeviceName(mVal2))
                            .record();
                    return;
                case VOL_VOICE_ACTIVITY_CONTEXTUAL_VOLUME:
                    new MediaMetrics.Item(mMetricsId)
                            .set(MediaMetrics.Property.EVENT, "voiceActivityContextualVolume")
                            .set(MediaMetrics.Property.INDEX, mVal1)
                            .set(MediaMetrics.Property.STATE,
                                    mVal2 == 1 ? "active" : "inactive")
                            .set(MediaMetrics.Property.STREAM_TYPE,
                                    AudioSystem.streamToString(mStream))
                            .set(MediaMetrics.Property.DEVICE,
                                    AudioSystem.getOutputDeviceName(mVal3))
                            .record();
                    return;
                case VOL_MODE_CHANGE_HEARING_AID:
                    new MediaMetrics.Item(mMetricsId)
                            .set(MediaMetrics.Property.EVENT, "modeChangeHearingAid")
                            .set(MediaMetrics.Property.INDEX, mVal1)
                            .set(MediaMetrics.Property.MODE, AudioSystem.modeToString(mVal2))
                            .set(MediaMetrics.Property.STREAM_TYPE,
                                    AudioSystem.streamToString(mStream))
                            .record();
                    return;
                case VOL_SET_GROUP_VOL:
                    new MediaMetrics.Item(mMetricsId)
                            .set(MediaMetrics.Property.CALLING_PACKAGE, mCaller)
                            .set(MediaMetrics.Property.EVENT, "setVolumeIndexForAttributes")
                            .set(MediaMetrics.Property.FLAGS, mVal2)
                            .set(MediaMetrics.Property.GROUP, mGroupName)
                            .set(MediaMetrics.Property.INDEX, mVal1)
                            .record();
                    return;
                case VOL_MUTE_STREAM_INT:
                    // No value in logging metrics for this internal event
                    return;
                case VOL_MASTER_MUTE:
                    // No value in logging metrics for this internal event
                    return;
                case VOL_ABS_DEVICE_ENABLED_ERROR:
                    // No value in logging metrics for this internal event
                    return;
                default:
                    return;
            }
        }

        @Override
        public String eventToString() {
            switch (mOp) {
                case VOL_ADJUST_SUGG_VOL:
                    return new StringBuilder("adjustSuggestedStreamVolume(sugg:")
                            .append(AudioSystem.streamToString(mStream))
                            .append(" dir:").append(AudioManager.adjustToString(mVal1))
                            .append(" flags:0x").append(Integer.toHexString(mVal2))
                            .append(") from ").append(mCaller)
                            .toString();
                case VOL_ADJUST_GROUP_VOL:
                    return new StringBuilder("adjustVolumeGroupVolume(group:")
                            .append(mGroupName)
                            .append(" dir:").append(AudioManager.adjustToString(mVal1))
                            .append(" flags:0x").append(Integer.toHexString(mVal2))
                            .append(") from ").append(mCaller)
                            .toString();
                case VOL_ADJUST_STREAM_VOL:
                    return new StringBuilder("adjustStreamVolume(stream:")
                            .append(AudioSystem.streamToString(mStream))
                            .append(" dir:").append(AudioManager.adjustToString(mVal1))
                            .append(" flags:0x").append(Integer.toHexString(mVal2))
                            .append(") from ").append(mCaller)
                            .toString();
                case VOL_SET_STREAM_VOL:
                    return new StringBuilder("setStreamVolume(stream:")
                            .append(AudioSystem.streamToString(mStream))
                            .append(" index:").append(mVal1)
                            .append(" flags:0x").append(Integer.toHexString(mVal2))
                            .append(" oldIndex:").append(mVal3)
                            .append(") from ").append(mCaller)
                            .toString();
                case VOL_SET_HEARING_AID_VOL:
                    return new StringBuilder("setHearingAidVolume:")
                            .append(" index:").append(mVal1)
                            .append(" gain dB:").append(mVal2)
                            .toString();
                case VOL_SET_LE_AUDIO_VOL:
                    return new StringBuilder("setLeAudioVolume(stream:")
                            .append(AudioSystem.streamToString(mStream))
                            .append(" index:").append(mVal1)
                            .append(" maxIndex:").append(mVal2)
                            .toString();
                case VOL_SET_AVRCP_VOL:
                    return new StringBuilder("setAvrcpVolume:")
                            .append(" index:").append(mVal1)
                            .toString();
                case VOL_SET_ABS_VOL:
                    return new StringBuilder("setAbsoluteVolume:")
                            .append(" stream:").append(AudioSystem.streamToString(mStream))
                            .append(" index:").append(mVal1)
                            .append(" muted:").append(mVal3 == 1 ? "true" : "false")
                            .append(" device:").append(AudioSystem.getOutputDeviceName(mVal2))
                            .toString();
                case VOL_ADJUST_VOL_UID:
                    return new StringBuilder("adjustStreamVolumeForUid(stream:")
                            .append(AudioSystem.streamToString(mStream))
                            .append(" dir:").append(AudioManager.adjustToString(mVal1))
                            .append(" flags:0x").append(Integer.toHexString(mVal2))
                            .append(") from ").append(mCaller)
                            .toString();
                case VOL_VOICE_ACTIVITY_CONTEXTUAL_VOLUME:
                    return new StringBuilder("Voice activity change (")
                            .append(mVal2 == 1 ? "active" : "inactive")
                            .append(") causes setting volume to idx:").append(mVal1)
                            .append(" stream:").append(AudioSystem.streamToString(mStream))
                            .append(" device:").append(AudioSystem.getOutputDeviceName(mVal3))
                            .toString();
                case VOL_MODE_CHANGE_HEARING_AID:
                    return new StringBuilder("setMode(")
                            .append(AudioSystem.modeToString(mVal2))
                            .append(") causes setting HEARING_AID volume to idx:").append(mVal1)
                            .append(" stream:").append(AudioSystem.streamToString(mStream))
                            .toString();
                case VOL_SET_GROUP_VOL:
                    return new StringBuilder("setVolumeIndexForAttributes(group:")
                            .append(" group: ").append(mGroupName)
                            .append(" index:").append(mVal1)
                            .append(" flags:0x").append(Integer.toHexString(mVal2))
                            .append(") from ").append(mCaller)
                            .toString();
                case VOL_MUTE_STREAM_INT:
                    return new StringBuilder("VolumeStreamState.muteInternally(stream:")
                            .append(AudioSystem.streamToString(mStream))
                            .append(mVal1 == 1 ? ", muted)" : ", unmuted)")
                            .toString();
                case VOL_MASTER_MUTE:
                    return new StringBuilder("Master mute:")
                            .append(mVal1 == 1 ? " muted)" : " unmuted)")
                            .toString();
                case VOL_ABS_DEVICE_ENABLED_ERROR:
                    return new StringBuilder("setDeviceAbsoluteVolumeEnabled failed with ")
                            .append(mVal3)
                            .append(" for dev: 0x").append(Integer.toHexString(mVal1))
                            .append(" enabled: ").append(mVal2)
                            .append(" streamType: ").append(mStream)
                            .toString();
                default: return new StringBuilder("FIXME invalid op:").append(mOp).toString();
            }
        }
    }

    static final class SoundDoseEvent extends EventLogger.Event {
        static final int MOMENTARY_EXPOSURE = 0;
        static final int DOSE_UPDATE = 1;
        static final int DOSE_REPEAT_5X = 2;
        static final int DOSE_ACCUMULATION_START = 3;
        static final int LOWER_VOLUME_TO_RS1 = 4;
        static final int UPDATE_ABS_VOLUME_ATTENUATION = 5;

        final int mEventType;
        final float mFloatValue;
        final long mLongValue;

        private SoundDoseEvent(int event, float f, long l) {
            mEventType = event;
            mFloatValue = f;
            mLongValue = l;
        }

        static SoundDoseEvent getMomentaryExposureEvent(float mel) {
            return new SoundDoseEvent(MOMENTARY_EXPOSURE, mel, 0 /*ignored*/);
        }

        static SoundDoseEvent getDoseUpdateEvent(float csd, long totalDuration) {
            return new SoundDoseEvent(DOSE_UPDATE, csd, totalDuration);
        }

        static SoundDoseEvent getDoseRepeat5xEvent() {
            return new SoundDoseEvent(DOSE_REPEAT_5X, 0 /*ignored*/, 0 /*ignored*/);
        }

        static SoundDoseEvent getDoseAccumulationStartEvent() {
            return new SoundDoseEvent(DOSE_ACCUMULATION_START, 0 /*ignored*/, 0 /*ignored*/);
        }

        static SoundDoseEvent getLowerVolumeToRs1Event() {
            return new SoundDoseEvent(LOWER_VOLUME_TO_RS1, 0 /*ignored*/, 0 /*ignored*/);
        }

        static SoundDoseEvent getAbsVolumeAttenuationEvent(float attenuation, int device) {
            return new SoundDoseEvent(UPDATE_ABS_VOLUME_ATTENUATION, attenuation, device);
        }

        @Override
        public String eventToString() {
            switch (mEventType) {
                case MOMENTARY_EXPOSURE:
                    return String.format("momentary exposure MEL=%.2f", mFloatValue);
                case DOSE_UPDATE:
                    return String.format(java.util.Locale.US,
                            "dose update CSD=%.1f%% total duration=%d",
                            mFloatValue * 100.0f, mLongValue);
                case DOSE_REPEAT_5X:
                    return "CSD reached 500%";
                case DOSE_ACCUMULATION_START:
                    return "CSD accumulating: RS2 entered";
                case LOWER_VOLUME_TO_RS1:
                    return "CSD lowering volume to RS1";
                case UPDATE_ABS_VOLUME_ATTENUATION:
                    return String.format(java.util.Locale.US,
                            "Updating CSD absolute volume attenuation on device 0x%s with %.2f dB ",
                            Long.toHexString(mLongValue), mFloatValue);
            }
            return new StringBuilder("FIXME invalid event type:").append(mEventType).toString();
        }
    }

    static final class LoudnessEvent extends EventLogger.Event {
        static final int START_PIID = 0;

        static final int STOP_PIID = 1;

        static final int CLIENT_DIED = 2;

        final int mEventType;
        final int mIntValue1;
        final int mIntValue2;

        private LoudnessEvent(int event, int i1, int i2) {
            mEventType = event;
            mIntValue1 = i1;
            mIntValue2 = i2;
        }

        static LoudnessEvent getStartPiid(int piid, int pid) {
            return new LoudnessEvent(START_PIID, piid, pid);
        }

        static LoudnessEvent getStopPiid(int piid, int pid) {
            return new LoudnessEvent(STOP_PIID, piid, pid);
        }

        static LoudnessEvent getClientDied(int pid) {
            return new LoudnessEvent(CLIENT_DIED, 0 /* ignored */, pid);
        }


        @Override
        public String eventToString() {
            switch (mEventType) {
                case START_PIID:
                    return String.format(
                            "Start loudness updates for piid %d for client pid %d",
                            mIntValue1, mIntValue2);
                case STOP_PIID:
                    return String.format(
                            "Stop loudness updates for piid %d for client pid %d",
                            mIntValue1, mIntValue2);
                case CLIENT_DIED:
                    return String.format("Loudness client with pid %d died", mIntValue2);

            }
            return new StringBuilder("FIXME invalid event type:").append(mEventType).toString();
        }
    }

    /**
     * Class to log stream type mute/unmute events
     */
    static final class StreamMuteEvent extends EventLogger.Event {
        final int mStreamType;
        final boolean mMuted;
        final String mSource;

        StreamMuteEvent(int streamType, boolean muted, String source) {
            mStreamType = streamType;
            mMuted = muted;
            mSource = source;
        }

        @Override
        public String eventToString() {
            final String streamName =
                    (mStreamType <= AudioSystem.getNumStreamTypes() && mStreamType >= 0)
                    ? AudioSystem.STREAM_NAMES[mStreamType]
                    : ("stream " + mStreamType);
            return new StringBuilder(streamName)
                    .append(mMuted ? " muting by " : " unmuting by ")
                    .append(mSource)
                    .toString();
        }
    }

    /**
     * Class to log unmute errors that contradict the ringer/zen mode muted streams
     */
    static final class StreamUnmuteErrorEvent extends EventLogger.Event {
        final int mStreamType;
        final int mRingerZenMutedStreams;

        StreamUnmuteErrorEvent(int streamType, int ringerZenMutedStreams) {
            mStreamType = streamType;
            mRingerZenMutedStreams = ringerZenMutedStreams;
        }

        @Override
        public String eventToString() {
            final String streamName =
                    (mStreamType <= AudioSystem.getNumStreamTypes() && mStreamType >= 0)
                            ? AudioSystem.STREAM_NAMES[mStreamType]
                            : ("stream " + mStreamType);
            return new StringBuilder("Invalid call to unmute ")
                    .append(streamName)
                    .append(" despite muted streams 0x")
                    .append(Integer.toHexString(mRingerZenMutedStreams))
                    .toString();
        }
    }

    static final class RingerZenMutedStreamsEvent extends EventLogger.Event {
        final int mRingerZenMutedStreams;
        final String mSource;

        RingerZenMutedStreamsEvent(int ringerZenMutedStreams, String source) {
            mRingerZenMutedStreams = ringerZenMutedStreams;
            mSource = source;
        }

        @Override
        public String eventToString() {
            return new StringBuilder("RingerZenMutedStreams 0x")
                    .append(Integer.toHexString(mRingerZenMutedStreams))
                    .append(" from ").append(mSource)
                    .toString();
        }
    }
}
