/*
 * Copyright (C) 2019 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.notification;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.media.AudioAttributes;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.UserHandle;
import android.telephony.TelephonyManager;
import android.util.Log;

import androidx.annotation.MainThread;
import androidx.annotation.Nullable;

import java.util.HashMap;

/**
 * Helper class for playing notification beeps. For Feature_automotive the sounds for notification
 * will be disabled at the server level and notification center will handle playing all the sounds
 * using this class.
 */
class Beeper {
    private static final String TAG = "Beeper";
    private static final long ALLOWED_ALERT_INTERVAL = 1000;
    private static final boolean DEBUG = Build.IS_ENG || Build.IS_USERDEBUG;

    private final Context mContext;
    private final AudioManager mAudioManager;
    private final Uri mInCallSoundToPlayUri;
    private AudioAttributes mPlaybackAttributes;

    private boolean mInCall;

    private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (action.equals(TelephonyManager.ACTION_PHONE_STATE_CHANGED)) {
                mInCall = TelephonyManager.EXTRA_STATE_OFFHOOK
                        .equals(intent.getStringExtra(TelephonyManager.EXTRA_STATE));
            }
        }
    };

    /**
     * Map that contains all the package name as the key for which the notifications made
     * noise. The value will be the last notification post time from the package.
     */
    private final HashMap<String, Long> packageLastPostedTime;

    @Nullable
    private BeepRecord currentBeep;

    public Beeper(Context context) {
        mContext = context;
        mAudioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE));
        mInCallSoundToPlayUri = Uri.parse("file://" + context.getResources().getString(
                com.android.internal.R.string.config_inCallNotificationSound));
        packageLastPostedTime = new HashMap<>();
        IntentFilter filter = new IntentFilter();
        filter.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED);
        context.registerReceiver(mIntentReceiver, filter);
    }

    /**
     * Beep with a provided sound.
     *
     * @param packageName of which {@link AlertEntry} belongs to.
     * @param soundToPlay {@link Uri} from where the sound will be played.
     */
    @MainThread
    public void beep(String packageName, Uri soundToPlay) {
        if (!canAlert(packageName)) {
            if (DEBUG) {
                Log.d(TAG, "Package recently made noise: " + packageName);
            }
            return;
        }

        packageLastPostedTime.put(packageName, System.currentTimeMillis());
        stopBeeping();
        if (mInCall) {
            currentBeep = new BeepRecord(mInCallSoundToPlayUri);
        } else {
            currentBeep = new BeepRecord(soundToPlay);
        }
        currentBeep.play();
    }

    /**
     * Checks if the package is allowed to make noise or not.
     */
    private boolean canAlert(String packageName) {
        if (packageLastPostedTime.containsKey(packageName)) {
            long lastPostedTime = packageLastPostedTime.get(packageName);
            return System.currentTimeMillis() - lastPostedTime > ALLOWED_ALERT_INTERVAL;
        }
        return true;
    }

    @MainThread
    void stopBeeping() {
        if (currentBeep != null) {
            currentBeep.stop();
            currentBeep = null;
        }
    }

    /** A class that represents a beep through its lifecycle. */
    private final class BeepRecord implements MediaPlayer.OnPreparedListener,
            MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener,
            AudioManager.OnAudioFocusChangeListener {

        private final Uri mBeepUri;
        private final int mBeepStream;
        private final MediaPlayer mPlayer;

        /** Only set in case of an error. See {@link #playViaRingtoneManager}. */
        @Nullable
        private Ringtone mRingtone;

        private int mAudiofocusRequestFailed = AudioManager.AUDIOFOCUS_REQUEST_FAILED;
        private boolean mCleanedUp;

        /**
         * Create a new {@link BeepRecord} that will play the given sound.
         *
         * @param beepUri The sound to play.
         */
        public BeepRecord(Uri beepUri) {
            this.mBeepUri = beepUri;
            this.mBeepStream = AudioManager.STREAM_MUSIC;
            mPlayer = new MediaPlayer();
            mPlayer.setOnPreparedListener(this);
            mPlayer.setOnCompletionListener(this);
            mPlayer.setOnErrorListener(this);
        }

        /** Start playing the sound. */
        @MainThread
        public void play() {
            if (DEBUG) {
                Log.d(TAG, "playing sound: ");
            }
            try {
                mPlayer.setDataSource(getContextForForegroundUser(), mBeepUri, /* headers= */null);
                mPlaybackAttributes = new AudioAttributes.Builder()
                        .setUsage(AudioAttributes.USAGE_NOTIFICATION)
                        .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                        .build();
                mPlayer.setAudioAttributes(mPlaybackAttributes);
                mPlayer.prepareAsync();
            } catch (Exception e) {
                Log.d(TAG, "playing via ringtone manager: " + e);
                handleError();
            }
        }

        /** Stop the currently playing sound, if it's playing. If it isn't, do nothing. */
        @MainThread
        public void stop() {
            if (!mCleanedUp && mPlayer.isPlaying()) {
                mPlayer.stop();
            }

            if (mRingtone != null) {
                mRingtone.stop();
                mRingtone = null;
            }
            cleanUp();
        }

        /** Handle MediaPlayer preparation completing - gain audio focus and play the sound. */
        @Override // MediaPlayer.OnPreparedListener
        public void onPrepared(MediaPlayer mediaPlayer) {
            if (mCleanedUp) {
                return;
            }
            AudioFocusRequest focusRequest = new AudioFocusRequest.Builder(
                    AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)
                    .setAudioAttributes(mPlaybackAttributes)
                    .setOnAudioFocusChangeListener(this, new Handler())
                    .build();

            mAudiofocusRequestFailed = mAudioManager.requestAudioFocus(focusRequest);
            if (mAudiofocusRequestFailed == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
                // Only play the sound if we actually gained audio focus.
                mPlayer.start();
            } else {
                cleanUp();
            }
        }

        /** Handle completion by cleaning up our state. */
        @Override // MediaPlayer.OnCompletionListener
        public void onCompletion(MediaPlayer mediaPlayer) {
            cleanUp();
        }

        /** Handle errors that come from MediaPlayer. */
        @Override // MediaPlayer.OnErrorListener
        public boolean onError(MediaPlayer mediaPlayer, int what, int extra) {
            handleError();
            return true;
        }

        /**
         * Not actually used for anything, but allows us to pass {@code this} to {@link
         * AudioManager#requestAudioFocus}, so that different audio focus requests from different
         * {@link BeepRecord}s don't collide.
         */
        @Override // AudioManager.OnAudioFocusChangeListener
        public void onAudioFocusChange(int i) {
        }

        /**
         * Notifications is running in the system process, so we want to make sure we lookup sounds
         * in the foreground user's space.
         */
        private Context getContextForForegroundUser() {
            try {
                return mContext.createPackageContextAsUser(mContext.getPackageName(), /* flags= */
                        0, UserHandle.of(NotificationUtils.getCurrentUser(mContext)));
            } catch (PackageManager.NameNotFoundException e) {
                throw new RuntimeException(e);
            }
        }

        /** Handle an error by trying to play the sound through {@link RingtoneManager}. */
        private void handleError() {
            cleanUp();
            playViaRingtoneManager();
        }

        /** Clean up and release our state. */
        private void cleanUp() {
            if (mAudiofocusRequestFailed == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
                mAudioManager.abandonAudioFocus(this);
                mAudiofocusRequestFailed = AudioManager.AUDIOFOCUS_REQUEST_FAILED;
            }
            mPlayer.release();
            mCleanedUp = true;
        }

        /**
         * Handle a failure to play the sound directly, by playing through {@link RingtoneManager}.
         *
         * <p>RingtoneManager is equipped to play sounds that require READ_EXTERNAL_STORAGE
         * permission (see b/30572189), but can't handle requesting and releasing audio focus.
         * Since we want audio focus in the common case, try playing the sound ourselves through
         * MediaPlayer before we give up and hand over to RingtoneManager.
         */
        private void playViaRingtoneManager() {
            mRingtone = RingtoneManager.getRingtone(getContextForForegroundUser(), mBeepUri);
            if (mRingtone != null) {
                mRingtone.setStreamType(mBeepStream);
                mRingtone.play();
            }
        }
    }
}
