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

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityThread;
import android.app.AppOpsManager;
import android.content.Context;
import android.os.IBinder;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.util.Log;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.app.IAppOpsCallback;
import com.android.internal.app.IAppOpsService;

import java.lang.ref.WeakReference;
import java.util.Objects;

/**
 * Class to encapsulate a number of common player operations:
 *   - AppOps for OP_PLAY_AUDIO
 *   - more to come (routing, transport control)
 * @hide
 */
public abstract class PlayerBase {

    private static final String TAG = "PlayerBase";
    /** Debug app ops */
    private static final boolean DEBUG_APP_OPS = false;
    private static final boolean DEBUG = DEBUG_APP_OPS || false;
    private static IAudioService sService; //lazy initialization, use getService()

    /** if true, only use OP_PLAY_AUDIO monitoring for logging, and rely on muting to happen
     *  in AudioFlinger */
    private static final boolean USE_AUDIOFLINGER_MUTING_FOR_OP = true;

    // parameters of the player that affect AppOps
    protected AudioAttributes mAttributes;

    // volumes of the subclass "player volumes", as seen by the client of the subclass
    //   (e.g. what was passed in AudioTrack.setVolume(float)). The actual volume applied is
    //   the combination of the player volume, and the PlayerBase pan and volume multipliers
    protected float mLeftVolume = 1.0f;
    protected float mRightVolume = 1.0f;
    protected float mAuxEffectSendLevel = 0.0f;

    // NEVER call into AudioService (see getService()) with mLock held: PlayerBase can run in
    // the same process as AudioService, which can synchronously call back into this class,
    // causing deadlocks between the two
    private final Object mLock = new Object();

    // for AppOps
    private @Nullable IAppOpsService mAppOps;
    private @Nullable IAppOpsCallback mAppOpsCallback;
    @GuardedBy("mLock")
    private boolean mHasAppOpsPlayAudio = true;

    private final int mImplType;
    // uniquely identifies the Player Interface throughout the system (P I Id)
    private int mPlayerIId = AudioPlaybackConfiguration.PLAYER_PIID_INVALID;

    @GuardedBy("mLock")
    private int mState;
    @GuardedBy("mLock")
    private int mStartDelayMs = 0;
    @GuardedBy("mLock")
    private float mPanMultiplierL = 1.0f;
    @GuardedBy("mLock")
    private float mPanMultiplierR = 1.0f;
    @GuardedBy("mLock")
    private float mVolMultiplier = 1.0f;

    /**
     * Constructor. Must be given audio attributes, as they are required for AppOps.
     * @param attr non-null audio attributes
     * @param class non-null class of the implementation of this abstract class
     */
    PlayerBase(@NonNull AudioAttributes attr, int implType) {
        if (attr == null) {
            throw new IllegalArgumentException("Illegal null AudioAttributes");
        }
        mAttributes = attr;
        mImplType = implType;
        mState = AudioPlaybackConfiguration.PLAYER_STATE_IDLE;
    };

    /**
     * Call from derived class when instantiation / initialization is successful
     */
    protected void baseRegisterPlayer() {
        if (!USE_AUDIOFLINGER_MUTING_FOR_OP) {
            IBinder b = ServiceManager.getService(Context.APP_OPS_SERVICE);
            mAppOps = IAppOpsService.Stub.asInterface(b);
            // initialize mHasAppOpsPlayAudio
            updateAppOpsPlayAudio();
            // register a callback to monitor whether the OP_PLAY_AUDIO is still allowed
            mAppOpsCallback = new IAppOpsCallbackWrapper(this);
            try {
                mAppOps.startWatchingMode(AppOpsManager.OP_PLAY_AUDIO,
                        ActivityThread.currentPackageName(), mAppOpsCallback);
            } catch (RemoteException e) {
                Log.e(TAG, "Error registering appOps callback", e);
                mHasAppOpsPlayAudio = false;
            }
        }
        try {
            mPlayerIId = getService().trackPlayer(
                    new PlayerIdCard(mImplType, mAttributes, new IPlayerWrapper(this)));
        } catch (RemoteException e) {
            Log.e(TAG, "Error talking to audio service, player will not be tracked", e);
        }
    }

    /**
     * To be called whenever the audio attributes of the player change
     * @param attr non-null audio attributes
     */
    void baseUpdateAudioAttributes(@NonNull AudioAttributes attr) {
        if (attr == null) {
            throw new IllegalArgumentException("Illegal null AudioAttributes");
        }
        try {
            getService().playerAttributes(mPlayerIId, attr);
        } catch (RemoteException e) {
            Log.e(TAG, "Error talking to audio service, STARTED state will not be tracked", e);
        }
        synchronized (mLock) {
            boolean attributesChanged = (mAttributes != attr);
            mAttributes = attr;
            updateAppOpsPlayAudio_sync(attributesChanged);
        }
    }

    private void updateState(int state) {
        final int piid;
        synchronized (mLock) {
            mState = state;
            piid = mPlayerIId;
        }
        try {
            getService().playerEvent(piid, state);
        } catch (RemoteException e) {
            Log.e(TAG, "Error talking to audio service, "
                    + AudioPlaybackConfiguration.toLogFriendlyPlayerState(state)
                    + " state will not be tracked for piid=" + piid, e);
        }
    }

    void baseStart() {
        if (DEBUG) { Log.v(TAG, "baseStart() piid=" + mPlayerIId); }
        updateState(AudioPlaybackConfiguration.PLAYER_STATE_STARTED);
        synchronized (mLock) {
            if (isRestricted_sync()) {
                playerSetVolume(true/*muting*/,0, 0);
            }
        }
    }

    void baseSetStartDelayMs(int delayMs) {
        synchronized(mLock) {
            mStartDelayMs = Math.max(delayMs, 0);
        }
    }

    protected int getStartDelayMs() {
        synchronized(mLock) {
            return mStartDelayMs;
        }
    }

    void basePause() {
        if (DEBUG) { Log.v(TAG, "basePause() piid=" + mPlayerIId); }
        updateState(AudioPlaybackConfiguration.PLAYER_STATE_PAUSED);
    }

    void baseStop() {
        if (DEBUG) { Log.v(TAG, "baseStop() piid=" + mPlayerIId); }
        updateState(AudioPlaybackConfiguration.PLAYER_STATE_STOPPED);
    }

    void baseSetPan(float pan) {
        final float p = Math.min(Math.max(-1.0f, pan), 1.0f);
        synchronized (mLock) {
            if (p >= 0.0f) {
                mPanMultiplierL = 1.0f - p;
                mPanMultiplierR = 1.0f;
            } else {
                mPanMultiplierL = 1.0f;
                mPanMultiplierR = 1.0f + p;
            }
        }
        updatePlayerVolume();
    }

    private void updatePlayerVolume() {
        final float finalLeftVol, finalRightVol;
        final boolean isRestricted;
        synchronized (mLock) {
            finalLeftVol = mVolMultiplier * mLeftVolume * mPanMultiplierL;
            finalRightVol = mVolMultiplier * mRightVolume * mPanMultiplierR;
            isRestricted = isRestricted_sync();
        }
        playerSetVolume(isRestricted /*muting*/, finalLeftVol, finalRightVol);
    }

    void setVolumeMultiplier(float vol) {
        synchronized (mLock) {
            this.mVolMultiplier = vol;
        }
        updatePlayerVolume();
    }

    void baseSetVolume(float leftVolume, float rightVolume) {
        synchronized (mLock) {
            mLeftVolume = leftVolume;
            mRightVolume = rightVolume;
        }
        updatePlayerVolume();
    }

    int baseSetAuxEffectSendLevel(float level) {
        synchronized (mLock) {
            mAuxEffectSendLevel = level;
            if (isRestricted_sync()) {
                return AudioSystem.SUCCESS;
            }
        }
        return playerSetAuxEffectSendLevel(false/*muting*/, level);
    }

    /**
     * To be called from a subclass release or finalize method.
     * Releases AppOps related resources.
     */
    void baseRelease() {
        if (DEBUG) { Log.v(TAG, "baseRelease() piid=" + mPlayerIId + " state=" + mState); }
        boolean releasePlayer = false;
        synchronized (mLock) {
            if (mState != AudioPlaybackConfiguration.PLAYER_STATE_RELEASED) {
                releasePlayer = true;
                mState = AudioPlaybackConfiguration.PLAYER_STATE_RELEASED;
            }
        }
        try {
            if (releasePlayer) {
                getService().releasePlayer(mPlayerIId);
            }
        } catch (RemoteException e) {
            Log.e(TAG, "Error talking to audio service, the player will still be tracked", e);
        }
        try {
            if (mAppOps != null) {
                mAppOps.stopWatchingMode(mAppOpsCallback);
            }
        } catch (Exception e) {
            // nothing to do here, the object is supposed to be released anyway
        }
    }

    private void updateAppOpsPlayAudio() {
        synchronized (mLock) {
            updateAppOpsPlayAudio_sync(false);
        }
    }

    /**
     * To be called whenever a condition that might affect audibility of this player is updated.
     * Must be called synchronized on mLock.
     */
    void updateAppOpsPlayAudio_sync(boolean attributesChanged) {
        if (USE_AUDIOFLINGER_MUTING_FOR_OP) {
            return;
        }
        boolean oldHasAppOpsPlayAudio = mHasAppOpsPlayAudio;
        try {
            int mode = AppOpsManager.MODE_IGNORED;
            if (mAppOps != null) {
                mode = mAppOps.checkAudioOperation(AppOpsManager.OP_PLAY_AUDIO,
                    mAttributes.getUsage(),
                    Process.myUid(), ActivityThread.currentPackageName());
            }
            mHasAppOpsPlayAudio = (mode == AppOpsManager.MODE_ALLOWED);
        } catch (RemoteException e) {
            mHasAppOpsPlayAudio = false;
        }

        // AppsOps alters a player's volume; when the restriction changes, reflect it on the actual
        // volume used by the player
        try {
            if (oldHasAppOpsPlayAudio != mHasAppOpsPlayAudio ||
                    attributesChanged) {
                getService().playerHasOpPlayAudio(mPlayerIId, mHasAppOpsPlayAudio);
                if (!isRestricted_sync()) {
                    if (DEBUG_APP_OPS) {
                        Log.v(TAG, "updateAppOpsPlayAudio: unmuting player, vol=" + mLeftVolume
                                + "/" + mRightVolume);
                    }
                    playerSetVolume(false/*muting*/,
                            mLeftVolume * mPanMultiplierL, mRightVolume * mPanMultiplierR);
                    playerSetAuxEffectSendLevel(false/*muting*/, mAuxEffectSendLevel);
                } else {
                    if (DEBUG_APP_OPS) {
                        Log.v(TAG, "updateAppOpsPlayAudio: muting player");
                    }
                    playerSetVolume(true/*muting*/, 0.0f, 0.0f);
                    playerSetAuxEffectSendLevel(true/*muting*/, 0.0f);
                }
            }
        } catch (Exception e) {
            // failing silently, player might not be in right state
        }
    }

    /**
     * To be called by the subclass whenever an operation is potentially restricted.
     * As the media player-common behavior are incorporated into this class, the subclass's need
     * to call this method should be removed, and this method could become private.
     * FIXME can this method be private so subclasses don't have to worry about when to check
     *    the restrictions.
     * @return
     */
    boolean isRestricted_sync() {
        if (USE_AUDIOFLINGER_MUTING_FOR_OP) {
            return false;
        }
        // check app ops
        if (mHasAppOpsPlayAudio) {
            return false;
        }
        // check bypass flag
        if ((mAttributes.getAllFlags() & AudioAttributes.FLAG_BYPASS_INTERRUPTION_POLICY) != 0) {
            return false;
        }
        // check force audibility flag and camera restriction
        if (((mAttributes.getAllFlags() & AudioAttributes.FLAG_AUDIBILITY_ENFORCED) != 0)
                && (mAttributes.getUsage() == AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)) {
            boolean cameraSoundForced = false;
            try {
                cameraSoundForced = getService().isCameraSoundForced();
            } catch (RemoteException e) {
                Log.e(TAG, "Cannot access AudioService in isRestricted_sync()");
            } catch (NullPointerException e) {
                Log.e(TAG, "Null AudioService in isRestricted_sync()");
            }
            if (cameraSoundForced) {
                return false;
            }
        }
        return true;
    }

    private static IAudioService getService()
    {
        if (sService != null) {
            return sService;
        }
        IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
        sService = IAudioService.Stub.asInterface(b);
        return sService;
    }

    /**
     * @hide
     * @param delayMs
     */
    public void setStartDelayMs(int delayMs) {
        baseSetStartDelayMs(delayMs);
    }

    //=====================================================================
    // Abstract methods a subclass needs to implement
    /**
     * Abstract method for the subclass behavior's for volume and muting commands
     * @param muting if true, the player is to be muted, and the volume values can be ignored
     * @param leftVolume the left volume to use if muting is false
     * @param rightVolume the right volume to use if muting is false
     */
    abstract void playerSetVolume(boolean muting, float leftVolume, float rightVolume);

    /**
     * Abstract method to apply a {@link VolumeShaper.Configuration}
     * and a {@link VolumeShaper.Operation} to the Player.
     * This should be overridden by the Player to call into the native
     * VolumeShaper implementation. Multiple {@code VolumeShapers} may be
     * concurrently active for a given Player, each accessible by the
     * {@code VolumeShaper} id.
     *
     * The {@code VolumeShaper} implementation caches the id returned
     * when applying a fully specified configuration
     * from {VolumeShaper.Configuration.Builder} to track later
     * operation changes requested on it.
     *
     * @param configuration a {@code VolumeShaper.Configuration} object
     *        created by {@link VolumeShaper.Configuration.Builder} or
     *        an created from a {@code VolumeShaper} id
     *        by the {@link VolumeShaper.Configuration} constructor.
     * @param operation a {@code VolumeShaper.Operation}.
     * @return a negative error status or a
     *         non-negative {@code VolumeShaper} id on success.
     */
    /* package */ abstract int playerApplyVolumeShaper(
            @NonNull VolumeShaper.Configuration configuration,
            @NonNull VolumeShaper.Operation operation);

    /**
     * Abstract method to get the current VolumeShaper state.
     * @param id the {@code VolumeShaper} id returned from
     *           sending a fully specified {@code VolumeShaper.Configuration}
     *           through {@link #playerApplyVolumeShaper}
     * @return a {@code VolumeShaper.State} object or null if
     *         there is no {@code VolumeShaper} for the id.
     */
    /* package */ abstract @Nullable VolumeShaper.State playerGetVolumeShaperState(int id);

    abstract int playerSetAuxEffectSendLevel(boolean muting, float level);
    abstract void playerStart();
    abstract void playerPause();
    abstract void playerStop();

    //=====================================================================
    private static class IAppOpsCallbackWrapper extends IAppOpsCallback.Stub {
        private final WeakReference<PlayerBase> mWeakPB;

        public IAppOpsCallbackWrapper(PlayerBase pb) {
            mWeakPB = new WeakReference<PlayerBase>(pb);
        }

        @Override
        public void opChanged(int op, int uid, String packageName) {
            if (op == AppOpsManager.OP_PLAY_AUDIO) {
                if (DEBUG_APP_OPS) { Log.v(TAG, "opChanged: op=PLAY_AUDIO pack=" + packageName); }
                final PlayerBase pb = mWeakPB.get();
                if (pb != null) {
                    pb.updateAppOpsPlayAudio();
                }
            }
        }
    }

    //=====================================================================
    /**
     * Wrapper around an implementation of IPlayer for all subclasses of PlayerBase
     * that doesn't keep a strong reference on PlayerBase
     */
    private static class IPlayerWrapper extends IPlayer.Stub {
        private final WeakReference<PlayerBase> mWeakPB;

        public IPlayerWrapper(PlayerBase pb) {
            mWeakPB = new WeakReference<PlayerBase>(pb);
        }

        @Override
        public void start() {
            final PlayerBase pb = mWeakPB.get();
            if (pb != null) {
                pb.playerStart();
            }
        }

        @Override
        public void pause() {
            final PlayerBase pb = mWeakPB.get();
            if (pb != null) {
                pb.playerPause();
            }
        }

        @Override
        public void stop() {
            final PlayerBase pb = mWeakPB.get();
            if (pb != null) {
                pb.playerStop();
            }
        }

        @Override
        public void setVolume(float vol) {
            final PlayerBase pb = mWeakPB.get();
            if (pb != null) {
                pb.setVolumeMultiplier(vol);
            }
        }

        @Override
        public void setPan(float pan) {
            final PlayerBase pb = mWeakPB.get();
            if (pb != null) {
                pb.baseSetPan(pan);
            }
        }

        @Override
        public void setStartDelayMs(int delayMs) {
            final PlayerBase pb = mWeakPB.get();
            if (pb != null) {
                pb.baseSetStartDelayMs(delayMs);
            }
        }

        @Override
        public void applyVolumeShaper(
                @NonNull VolumeShaper.Configuration configuration,
                @NonNull VolumeShaper.Operation operation) {
            final PlayerBase pb = mWeakPB.get();
            if (pb != null) {
                pb.playerApplyVolumeShaper(configuration, operation);
            }
        }
    }

    //=====================================================================
    /**
     * Class holding all the information about a player that needs to be known at registration time
     */
    public static class PlayerIdCard implements Parcelable {
        public final int mPlayerType;

        public static final int AUDIO_ATTRIBUTES_NONE = 0;
        public static final int AUDIO_ATTRIBUTES_DEFINED = 1;
        public final AudioAttributes mAttributes;
        public final IPlayer mIPlayer;

        PlayerIdCard(int type, @NonNull AudioAttributes attr, @NonNull IPlayer iplayer) {
            mPlayerType = type;
            mAttributes = attr;
            mIPlayer = iplayer;
        }

        @Override
        public int hashCode() {
            return Objects.hash(mPlayerType);
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeInt(mPlayerType);
            mAttributes.writeToParcel(dest, 0);
            dest.writeStrongBinder(mIPlayer == null ? null : mIPlayer.asBinder());
        }

        public static final @android.annotation.NonNull Parcelable.Creator<PlayerIdCard> CREATOR
        = new Parcelable.Creator<PlayerIdCard>() {
            /**
             * Rebuilds an PlayerIdCard previously stored with writeToParcel().
             * @param p Parcel object to read the PlayerIdCard from
             * @return a new PlayerIdCard created from the data in the parcel
             */
            public PlayerIdCard createFromParcel(Parcel p) {
                return new PlayerIdCard(p);
            }
            public PlayerIdCard[] newArray(int size) {
                return new PlayerIdCard[size];
            }
        };

        private PlayerIdCard(Parcel in) {
            mPlayerType = in.readInt();
            mAttributes = AudioAttributes.CREATOR.createFromParcel(in);
            // IPlayer can be null if unmarshalling a Parcel coming from who knows where
            final IBinder b = in.readStrongBinder();
            mIPlayer = (b == null ? null : IPlayer.Stub.asInterface(b));
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || !(o instanceof PlayerIdCard)) return false;

            PlayerIdCard that = (PlayerIdCard) o;

            // FIXME change to the binder player interface once supported as a member
            return ((mPlayerType == that.mPlayerType) && mAttributes.equals(that.mAttributes));
        }
    }

    //=====================================================================
    // Utilities

    /**
     * @hide
     * Use to generate warning or exception in legacy code paths that allowed passing stream types
     * to qualify audio playback.
     * @param streamType the stream type to check
     * @throws IllegalArgumentException
     */
    public static void deprecateStreamTypeForPlayback(int streamType, @NonNull String className,
            @NonNull String opName) throws IllegalArgumentException {
        // STREAM_ACCESSIBILITY was introduced at the same time the use of stream types
        // for audio playback was deprecated, so it is not allowed at all to qualify a playback
        // use case
        if (streamType == AudioManager.STREAM_ACCESSIBILITY) {
            throw new IllegalArgumentException("Use of STREAM_ACCESSIBILITY is reserved for "
                    + "volume control");
        }
        Log.w(className, "Use of stream types is deprecated for operations other than " +
                "volume control");
        Log.w(className, "See the documentation of " + opName + " for what to use instead with " +
                "android.media.AudioAttributes to qualify your playback use case");
    }
}
