/*
 * Copyright (C) 2016 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.content.Context;
import android.content.pm.PackageManager;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecordingConfiguration;
import android.media.AudioSystem;
import android.media.IRecordingConfigDispatcher;
import android.media.MediaRecorder;
import android.media.audiofx.AudioEffect;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;

import java.io.PrintWriter;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

/**
 * Class to receive and dispatch updates from AudioSystem about recording configurations.
 */
public final class RecordingActivityMonitor implements AudioSystem.AudioRecordingCallback {

    public final static String TAG = "AudioService.RecordingActivityMonitor";

    private ArrayList<RecMonitorClient> mClients = new ArrayList<RecMonitorClient>();
    // a public client is one that needs an anonymized version of the playback configurations, we
    // keep track of whether there is at least one to know when we need to create the list of
    // playback configurations that do not contain uid/package name information.
    private boolean mHasPublicClients = false;

    static final class RecordingState {
        private final int mRiid;
        private final RecorderDeathHandler mDeathHandler;
        private boolean mIsActive;
        private AudioRecordingConfiguration mConfig;

        RecordingState(int riid, RecorderDeathHandler handler) {
            mRiid = riid;
            mDeathHandler = handler;
        }

        RecordingState(AudioRecordingConfiguration config) {
            mRiid = AudioManager.RECORD_RIID_INVALID;
            mDeathHandler = null;
            mConfig = config;
        }

        int getRiid() {
            return mRiid;
        }

        int getPortId() {
            return mConfig != null ? mConfig.getClientPortId() : -1;
        }

        AudioRecordingConfiguration getConfig() {
            return mConfig;
        }

        boolean hasDeathHandler() {
            return mDeathHandler != null;
        }

        boolean isActiveConfiguration() {
            return mIsActive && mConfig != null;
        }

        // returns true if status of an active recording has changed
        boolean setActive(boolean active) {
            if (mIsActive == active) return false;
            mIsActive = active;
            return mConfig != null;
        }

        // returns true if an active recording has been updated
        boolean setConfig(AudioRecordingConfiguration config) {
            if (config.equals(mConfig)) return false;
            mConfig = config;
            return mIsActive;
        }

        void dump(PrintWriter pw) {
            pw.println("riid " + mRiid + "; active? " + mIsActive);
            if (mConfig != null) {
                mConfig.dump(pw);
            } else {
                pw.println("  no config");
            }
        }
    }
    private List<RecordingState> mRecordStates = new ArrayList<RecordingState>();

    private final PackageManager mPackMan;

    RecordingActivityMonitor(Context ctxt) {
        RecMonitorClient.sMonitor = this;
        RecorderDeathHandler.sMonitor = this;
        mPackMan = ctxt.getPackageManager();
    }

    /**
     * Implementation of android.media.AudioSystem.AudioRecordingCallback
     */
    public void onRecordingConfigurationChanged(int event, int riid, int uid, int session,
                                                int source, int portId, boolean silenced,
                                                int[] recordingInfo,
                                                AudioEffect.Descriptor[] clientEffects,
                                                AudioEffect.Descriptor[] effects,
                                                int activeSource, String packName) {
        final AudioRecordingConfiguration config = createRecordingConfiguration(
                uid, session, source, recordingInfo,
                portId, silenced, activeSource, clientEffects, effects);
        if (MediaRecorder.isSystemOnlyAudioSource(source)) {
            // still want to log event, it just won't appear in recording configurations;
            sEventLogger.log(new RecordingEvent(event, riid, config).printLog(TAG));
            return;
        }
        dispatchCallbacks(updateSnapshot(event, riid, config));
    }

    /**
     * Track a recorder provided by the client
     */
    public int trackRecorder(IBinder recorder) {
        if (recorder == null) {
            Log.e(TAG, "trackRecorder called with null token");
            return AudioManager.RECORD_RIID_INVALID;
        }
        final int newRiid = AudioSystem.newAudioRecorderId();
        RecorderDeathHandler handler = new RecorderDeathHandler(newRiid, recorder);
        if (!handler.init()) {
            // probably means that the AudioRecord has already died
            return AudioManager.RECORD_RIID_INVALID;
        }
        synchronized (mRecordStates) {
            mRecordStates.add(new RecordingState(newRiid, handler));
        }
        // a newly added record is inactive, no change in active configs is possible.
        return newRiid;
    }

    /**
     * Receive an event from the client about a tracked recorder
     */
    public void recorderEvent(int riid, int event) {
        int configEvent = event == AudioManager.RECORDER_STATE_STARTED
                ? AudioManager.RECORD_CONFIG_EVENT_START :
                event == AudioManager.RECORDER_STATE_STOPPED
                ? AudioManager.RECORD_CONFIG_EVENT_STOP : AudioManager.RECORD_CONFIG_EVENT_NONE;
        if (riid == AudioManager.RECORD_RIID_INVALID
                || configEvent == AudioManager.RECORD_CONFIG_EVENT_NONE) {
            sEventLogger.log(new RecordingEvent(event, riid, null).printLog(TAG));
            return;
        }
        dispatchCallbacks(updateSnapshot(configEvent, riid, null));
    }

    /**
     * Stop tracking the recorder
     */
    public void releaseRecorder(int riid) {
        dispatchCallbacks(updateSnapshot(AudioManager.RECORD_CONFIG_EVENT_RELEASE, riid, null));
    }

    private void dispatchCallbacks(List<AudioRecordingConfiguration> configs) {
        if (configs == null) { // null means "no changes"
            return;
        }
        synchronized (mClients) {
            // list of recording configurations for "public consumption". It is only computed if
            // there are non-system recording activity listeners.
            final List<AudioRecordingConfiguration> configsPublic = mHasPublicClients
                    ? anonymizeForPublicConsumption(configs) :
                      new ArrayList<AudioRecordingConfiguration>();
            for (RecMonitorClient rmc : mClients) {
                try {
                    if (rmc.mIsPrivileged) {
                        rmc.mDispatcherCb.dispatchRecordingConfigChange(configs);
                    } else {
                        rmc.mDispatcherCb.dispatchRecordingConfigChange(configsPublic);
                    }
                } catch (RemoteException e) {
                    Log.w(TAG, "Could not call dispatchRecordingConfigChange() on client", e);
                }
            }
        }
    }

    protected void dump(PrintWriter pw) {
        // recorders
        pw.println("\nRecordActivityMonitor dump time: "
                + DateFormat.getTimeInstance().format(new Date()));
        synchronized (mRecordStates) {
            for (RecordingState state : mRecordStates) {
                state.dump(pw);
            }
        }
        pw.println("\n");
        // log
        sEventLogger.dump(pw);
    }

    private static ArrayList<AudioRecordingConfiguration> anonymizeForPublicConsumption(
            List<AudioRecordingConfiguration> sysConfigs) {
        ArrayList<AudioRecordingConfiguration> publicConfigs =
                new ArrayList<AudioRecordingConfiguration>();
        // only add active anonymized configurations,
        for (AudioRecordingConfiguration config : sysConfigs) {
            publicConfigs.add(AudioRecordingConfiguration.anonymizedCopy(config));
        }
        return publicConfigs;
    }

    void initMonitor() {
        AudioSystem.setRecordingCallback(this);
    }

    void onAudioServerDied() {
        // Remove all RecordingState entries that do not have a death handler (that means
        // they are tracked by the Audio Server). If there were active entries among removed,
        // dispatch active configuration changes.
        List<AudioRecordingConfiguration> configs = null;
        synchronized (mRecordStates) {
            boolean configChanged = false;
            for (Iterator<RecordingState> it = mRecordStates.iterator(); it.hasNext(); ) {
                RecordingState state = it.next();
                if (!state.hasDeathHandler()) {
                    if (state.isActiveConfiguration()) {
                        configChanged = true;
                        sEventLogger.log(new RecordingEvent(
                                        AudioManager.RECORD_CONFIG_EVENT_RELEASE,
                                        state.getRiid(), state.getConfig()));
                    }
                    it.remove();
                }
            }
            if (configChanged) {
                configs = getActiveRecordingConfigurations(true /*isPrivileged*/);
            }
        }
        dispatchCallbacks(configs);
    }

    void registerRecordingCallback(IRecordingConfigDispatcher rcdb, boolean isPrivileged) {
        if (rcdb == null) {
            return;
        }
        synchronized (mClients) {
            final RecMonitorClient rmc = new RecMonitorClient(rcdb, isPrivileged);
            if (rmc.init()) {
                if (!isPrivileged) {
                    mHasPublicClients = true;
                }
                mClients.add(rmc);
            }
        }
    }

    void unregisterRecordingCallback(IRecordingConfigDispatcher rcdb) {
        if (rcdb == null) {
            return;
        }
        synchronized (mClients) {
            final Iterator<RecMonitorClient> clientIterator = mClients.iterator();
            boolean hasPublicClients = false;
            while (clientIterator.hasNext()) {
                RecMonitorClient rmc = clientIterator.next();
                if (rcdb.equals(rmc.mDispatcherCb)) {
                    rmc.release();
                    clientIterator.remove();
                } else {
                    if (!rmc.mIsPrivileged) {
                        hasPublicClients = true;
                    }
                }
            }
            mHasPublicClients = hasPublicClients;
        }
    }

    List<AudioRecordingConfiguration> getActiveRecordingConfigurations(boolean isPrivileged) {
        List<AudioRecordingConfiguration> configs = new ArrayList<AudioRecordingConfiguration>();
        synchronized (mRecordStates) {
            for (RecordingState state : mRecordStates) {
                if (state.isActiveConfiguration()) {
                    configs.add(state.getConfig());
                }
            }
        }
        // AudioRecordingConfiguration objects never get updated. If config changes,
        // the reference to the config is set in RecordingState.
        if (!isPrivileged) {
            configs = anonymizeForPublicConsumption(configs);
        }
        return configs;
    }

    /**
     * Create a recording configuration from the provided parameters
     * @param uid
     * @param session
     * @param source
     * @param recordingFormat see
     *     {@link AudioSystem.AudioRecordingCallback#onRecordingConfigurationChanged(int, int, int,\
     int, int, boolean, int[], AudioEffect.Descriptor[], AudioEffect.Descriptor[], int, String)}
     *     for the definition of the contents of the array
     * @param portId
     * @param silenced
     * @param activeSource
     * @param clientEffects
     * @param effects
     * @return null a configuration object.
     */
    private AudioRecordingConfiguration createRecordingConfiguration(int uid,
            int session, int source, int[] recordingInfo, int portId, boolean silenced,
            int activeSource, AudioEffect.Descriptor[] clientEffects,
            AudioEffect.Descriptor[] effects) {
        final AudioFormat clientFormat = new AudioFormat.Builder()
                .setEncoding(recordingInfo[0])
                // FIXME this doesn't support index-based masks
                .setChannelMask(recordingInfo[1])
                .setSampleRate(recordingInfo[2])
                .build();
        final AudioFormat deviceFormat = new AudioFormat.Builder()
                .setEncoding(recordingInfo[3])
                // FIXME this doesn't support index-based masks
                .setChannelMask(recordingInfo[4])
                .setSampleRate(recordingInfo[5])
                .build();
        final int patchHandle = recordingInfo[6];
        final String[] packages = mPackMan.getPackagesForUid(uid);
        final String packageName;
        if (packages != null && packages.length > 0) {
            packageName = packages[0];
        } else {
            packageName = "";
        }
        return new AudioRecordingConfiguration(uid, session, source,
                clientFormat, deviceFormat, patchHandle, packageName,
                portId, silenced, activeSource, clientEffects, effects);
    }

    /**
     * Update the internal "view" of the active recording sessions
     * @param event RECORD_CONFIG_EVENT_...
     * @param riid
     * @param config
     * @return null if the list of active recording sessions has not been modified, a list
     *     with the current active configurations otherwise.
     */
    private List<AudioRecordingConfiguration> updateSnapshot(
            int event, int riid, AudioRecordingConfiguration config) {
        List<AudioRecordingConfiguration> configs = null;
        synchronized (mRecordStates) {
            int stateIndex = -1;
            if (riid != AudioManager.RECORD_RIID_INVALID) {
                stateIndex = findStateByRiid(riid);
            } else if (config != null) {
                stateIndex = findStateByPortId(config.getClientPortId());
            }
            if (stateIndex == -1) {
                if (event == AudioManager.RECORD_CONFIG_EVENT_START && config != null) {
                    // First time registration for a recorder tracked by AudioServer.
                    mRecordStates.add(new RecordingState(config));
                    stateIndex = mRecordStates.size() - 1;
                } else {
                    if (config == null) {
                        // Records tracked by clients must be registered first via trackRecorder.
                        Log.e(TAG, String.format(
                                        "Unexpected event %d for riid %d", event, riid));
                    }
                    return configs;
                }
            }
            final RecordingState state = mRecordStates.get(stateIndex);

            boolean configChanged;
            switch (event) {
                case AudioManager.RECORD_CONFIG_EVENT_START:
                    configChanged = state.setActive(true);
                    if (config != null) {
                        configChanged = state.setConfig(config) || configChanged;
                    }
                    break;
                case AudioManager.RECORD_CONFIG_EVENT_UPDATE:
                    // For this event config != null
                    configChanged = state.setConfig(config);
                    break;
                case AudioManager.RECORD_CONFIG_EVENT_STOP:
                    configChanged = state.setActive(false);
                    if (!state.hasDeathHandler()) {
                        // A recorder tracked by AudioServer has to be removed now so it
                        // does not leak. It will be re-registered if recording starts again.
                        mRecordStates.remove(stateIndex);
                    }
                    break;
                case AudioManager.RECORD_CONFIG_EVENT_RELEASE:
                    configChanged = state.isActiveConfiguration();
                    mRecordStates.remove(stateIndex);
                    break;
                default:
                    Log.e(TAG, String.format("Unknown event %d for riid %d / portid %d",
                                    event, riid, state.getPortId()));
                    configChanged = false;
            }
            if (configChanged) {
                sEventLogger.log(new RecordingEvent(event, riid, state.getConfig()));
                configs = getActiveRecordingConfigurations(true /*isPrivileged*/);
            }
        }
        return configs;
    }

    // riid is assumed to be valid
    private int findStateByRiid(int riid) {
        synchronized (mRecordStates) {
            for (int i = 0; i < mRecordStates.size(); i++) {
                if (mRecordStates.get(i).getRiid() == riid) {
                    return i;
                }
            }
        }
        return -1;
    }

    private int findStateByPortId(int portId) {
        // Lookup by portId is unambiguous only for recordings managed by the Audio Server.
        synchronized (mRecordStates) {
            for (int i = 0; i < mRecordStates.size(); i++) {
                if (!mRecordStates.get(i).hasDeathHandler()
                        && mRecordStates.get(i).getPortId() == portId) {
                    return i;
                }
            }
        }
        return -1;
    }

    /**
     * Inner class to track clients that want to be notified of recording updates
     */
    private final static class RecMonitorClient implements IBinder.DeathRecipient {

        // can afford to be static because only one RecordingActivityMonitor ever instantiated
        static RecordingActivityMonitor sMonitor;

        final IRecordingConfigDispatcher mDispatcherCb;
        final boolean mIsPrivileged;

        RecMonitorClient(IRecordingConfigDispatcher rcdb, boolean isPrivileged) {
            mDispatcherCb = rcdb;
            mIsPrivileged = isPrivileged;
        }

        public void binderDied() {
            Log.w(TAG, "client died");
            sMonitor.unregisterRecordingCallback(mDispatcherCb);
        }

        boolean init() {
            try {
                mDispatcherCb.asBinder().linkToDeath(this, 0);
                return true;
            } catch (RemoteException e) {
                Log.w(TAG, "Could not link to client death", e);
                return false;
            }
        }

        void release() {
            mDispatcherCb.asBinder().unlinkToDeath(this, 0);
        }
    }

    private static final class RecorderDeathHandler implements IBinder.DeathRecipient {

        // can afford to be static because only one RecordingActivityMonitor ever instantiated
        static RecordingActivityMonitor sMonitor;

        final int mRiid;
        private final IBinder mRecorderToken;

        RecorderDeathHandler(int riid, IBinder recorderToken) {
            mRiid = riid;
            mRecorderToken = recorderToken;
        }

        public void binderDied() {
            sMonitor.releaseRecorder(mRiid);
        }

        boolean init() {
            try {
                mRecorderToken.linkToDeath(this, 0);
                return true;
            } catch (RemoteException e) {
                Log.w(TAG, "Could not link to recorder death", e);
                return false;
            }
        }
    }

    /**
     * Inner class for recording event logging
     */
    private static final class RecordingEvent extends AudioEventLogger.Event {
        private final int mRecEvent;
        private final int mRIId;
        private final int mClientUid;
        private final int mSession;
        private final int mSource;
        private final String mPackName;

        RecordingEvent(int event, int riid, AudioRecordingConfiguration config) {
            mRecEvent = event;
            mRIId = riid;
            if (config != null) {
                mClientUid = config.getClientUid();
                mSession = config.getClientAudioSessionId();
                mSource = config.getClientAudioSource();
                mPackName = config.getClientPackageName();
            } else {
                mClientUid = -1;
                mSession = -1;
                mSource = -1;
                mPackName = null;
            }
        }

        private static String recordEventToString(int recEvent) {
            switch (recEvent) {
                case AudioManager.RECORD_CONFIG_EVENT_START:
                    return "start";
                case AudioManager.RECORD_CONFIG_EVENT_UPDATE:
                    return "update";
                case AudioManager.RECORD_CONFIG_EVENT_STOP:
                    return "stop";
                case AudioManager.RECORD_CONFIG_EVENT_RELEASE:
                    return "release";
                default:
                    return "unknown (" + recEvent + ")";
            }
        }

        @Override
        public String eventToString() {
            return new StringBuilder("rec ").append(recordEventToString(mRecEvent))
                    .append(" riid:").append(mRIId)
                    .append(" uid:").append(mClientUid)
                    .append(" session:").append(mSession)
                    .append(" src:").append(MediaRecorder.toLogFriendlyAudioSource(mSource))
                    .append(mPackName == null ? "" : " pack:" + mPackName).toString();
        }
    }

    private static final AudioEventLogger sEventLogger = new AudioEventLogger(50,
            "recording activity received by AudioService");
}
