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

import static android.telephony.PhoneStateListener.LISTEN_NONE;

import static com.android.cellbroadcastreceiver.CellBroadcastReceiver.DBG;
import static com.android.cellbroadcastservice.CellBroadcastMetrics.ERRSRC_CBR;
import static com.android.cellbroadcastservice.CellBroadcastMetrics.ERRTYPE_PLAYFLASH;
import static com.android.cellbroadcastservice.CellBroadcastMetrics.ERRTYPE_PLAYSOUND;
import static com.android.cellbroadcastservice.CellBroadcastMetrics.ERRTYPE_PLAYTTS;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.AssetFileDescriptor;
import android.content.res.Resources;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraManager;
import android.media.AudioAttributes;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.media.MediaPlayer.OnErrorListener;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.speech.tts.TextToSpeech;
import android.telephony.PhoneStateListener;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;

import com.android.cellbroadcastreceiver.CellBroadcastAlertService.AlertType;
import com.android.internal.annotations.VisibleForTesting;

import java.util.Locale;

/**
 * Manages alert audio and vibration and text-to-speech. Runs as a service so that
 * it can continue to play if another activity overrides the CellBroadcastListActivity.
 */
public class CellBroadcastAlertAudio extends Service implements TextToSpeech.OnInitListener,
        TextToSpeech.OnUtteranceCompletedListener, AudioManager.OnAudioFocusChangeListener {
    private static final String TAG = "CellBroadcastAlertAudio";

    /** Action to start playing alert audio/vibration/speech. */
    @VisibleForTesting
    public static final String ACTION_START_ALERT_AUDIO = "ACTION_START_ALERT_AUDIO";

    /** Extra for message body to speak (if speech enabled in settings). */
    public static final String ALERT_AUDIO_MESSAGE_BODY =
            "com.android.cellbroadcastreceiver.ALERT_AUDIO_MESSAGE_BODY";

    /** Extra for text-to-speech preferred language (if speech enabled in settings). */
    public static final String ALERT_AUDIO_MESSAGE_LANGUAGE =
            "com.android.cellbroadcastreceiver.ALERT_AUDIO_MESSAGE_LANGUAGE";

    /** Extra for alert tone type */
    public static final String ALERT_AUDIO_TONE_TYPE =
            "com.android.cellbroadcastreceiver.ALERT_AUDIO_TONE_TYPE";

    /** Extra for alert vibration pattern (unless main volume is silent). */
    public static final String ALERT_AUDIO_VIBRATION_PATTERN_EXTRA =
            "com.android.cellbroadcastreceiver.ALERT_AUDIO_VIBRATION_PATTERN";

    /** Extra for playing alert sound in full volume regardless Do Not Disturb is on. */
    public static final String ALERT_AUDIO_OVERRIDE_DND_EXTRA =
            "com.android.cellbroadcastreceiver.ALERT_OVERRIDE_DND_EXTRA";

    /** Extra for cutomized alert duration in ms. */
    public static final String ALERT_AUDIO_DURATION =
            "com.android.cellbroadcastreceiver.ALERT_AUDIO_DURATION";

    /** Extra for alert subscription index */
    public static final String ALERT_AUDIO_SUB_INDEX =
            "com.android.cellbroadcastreceiver.ALERT_AUDIO_SUB_INDEX";

    private static final String TTS_UTTERANCE_ID = "com.android.cellbroadcastreceiver.UTTERANCE_ID";

    /** Pause duration between alert sound and alert speech. */
    private static final long PAUSE_DURATION_BEFORE_SPEAKING_MSEC = 1000L;

    private static final int STATE_IDLE = 0;
    private static final int STATE_ALERTING = 1;
    private static final int STATE_PAUSING = 2;
    private static final int STATE_SPEAKING = 3;
    private static final int STATE_STOPPING = 4;

    /** Default LED flashing frequency is 250 milliseconds */
    private static final long DEFAULT_LED_FLASH_INTERVAL_MSEC = 250L;

    /** Default delay for resent alert audio intent */
    private static final long DEFAULT_RESENT_DELAY_MSEC = 200L;

    private int mState;

    @VisibleForTesting
    public TextToSpeech mTts;
    private boolean mTtsEngineReady;

    private AlertType mAlertType;
    private String mMessageBody;
    private String mMessageLanguage;
    private int mSubId;
    private boolean mTtsLanguageSupported;
    private boolean mEnableVibrate;
    private boolean mEnableAudio;
    private boolean mEnableLedFlash;
    private boolean mIsMediaPlayerStarted;
    private boolean mIsTextToSpeechSpeaking;
    private boolean mOverrideDnd;
    private boolean mResetAlarmVolumeNeeded;
    private int mUserSetAlarmVolume;
    private int[] mVibrationPattern;
    private int mAlertDuration = -1;

    private Vibrator mVibrator;
    private MediaPlayer mMediaPlayer;
    @VisibleForTesting
    public MediaPlayer mMediaPlayerInjected;
    private AudioManager mAudioManager;
    private TelephonyManager mTelephonyManager;
    private int mInitialCallState;
    private int mStartId;
    private ScreenOffReceiver mScreenOffReceiver;

    // Internal messages
    private static final int ALERT_SOUND_FINISHED = 1000;
    private static final int ALERT_PAUSE_FINISHED = 1001;
    private static final int ALERT_LED_FLASH_TOGGLE = 1002;

    @VisibleForTesting
    public Handler mHandler;

    private PhoneStateListener mPhoneStateListener;

    /**
     * Callback from TTS engine after initialization.
     *
     * @param status {@link TextToSpeech#SUCCESS} or {@link TextToSpeech#ERROR}.
     */
    @Override
    public void onInit(int status) {
        if (DBG) log("onInit() TTS engine status: " + status);
        if (status == TextToSpeech.SUCCESS) {
            mTtsEngineReady = true;
            mTts.setOnUtteranceCompletedListener(this);
            // try to set the TTS language to match the broadcast
            setTtsLanguage();
        } else {
            mTtsEngineReady = false;
            mTts = null;
            loge("onInit() TTS engine error: " + status);
        }
    }

    /**
     * Try to set the TTS engine language to the preferred language. If failed, set
     * it to the default language. mTtsLanguageSupported will be updated based on the response.
     */
    private void setTtsLanguage() {
        Locale locale = null;
        if (!TextUtils.isEmpty(mMessageLanguage)) {
            locale = new Locale(mMessageLanguage);
        }
        if (locale == null || locale.getLanguage().equalsIgnoreCase(
                Locale.getDefault().getLanguage())) {
            // If the cell broadcast message does not specify the language, use device's default
            // language.
            locale = Locale.getDefault();
        }

        if (DBG) log("Setting TTS language to '" + locale + '\'');
        int result = mTts.setLanguage(locale);
        if (DBG) log("TTS setLanguage() returned: " + result);
        mTtsLanguageSupported = (result >= TextToSpeech.LANG_AVAILABLE);
    }

    /**
     * Callback from TTS engine.
     *
     * @param utteranceId the identifier of the utterance.
     */
    @Override
    public void onUtteranceCompleted(String utteranceId) {
        if (utteranceId.equals(TTS_UTTERANCE_ID)) {
            // When we reach here, it could be TTS completed or TTS was cut due to another
            // new alert started playing. We don't want to stop the service in the later case.
            if (getState() == STATE_SPEAKING) {
                if (DBG) log("TTS completed. Stop CellBroadcastAlertAudio service");
                stopAlertAudioService();
            }
        }
    }

    @Override
    public void onCreate() {
        mVibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
        mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
        // Listen for incoming calls to kill the alarm.
        mTelephonyManager = ((TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE));
        mHandler = new Handler(Looper.getMainLooper()) {
            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case ALERT_SOUND_FINISHED:
                        if (DBG) log("ALERT_SOUND_FINISHED");
                        stop();     // stop alert sound
                        // if we can speak the message text
                        if (mMessageBody != null && mTtsEngineReady && mTtsLanguageSupported) {
                            sendMessageDelayed(mHandler.obtainMessage(ALERT_PAUSE_FINISHED),
                                    PAUSE_DURATION_BEFORE_SPEAKING_MSEC);
                            setState(STATE_PAUSING);
                        } else {
                            if (DBG) {
                                log("MessageEmpty = " + (mMessageBody == null)
                                        + ", mTtsEngineReady = " + mTtsEngineReady
                                        + ", mTtsLanguageSupported = " + mTtsLanguageSupported);
                            }
                            stopAlertAudioService();
                        }
                        // Set alert reminder depending on user preference
                        CellBroadcastAlertReminder.queueAlertReminder(getApplicationContext(),
                                mSubId,
                                true);
                        break;

                    case ALERT_PAUSE_FINISHED:
                        if (DBG) log("ALERT_PAUSE_FINISHED");
                        int res = TextToSpeech.ERROR;
                        if (mMessageBody != null && mTtsEngineReady && mTtsLanguageSupported) {
                            if (DBG) log("Speaking broadcast text: " + mMessageBody);

                            mTts.setAudioAttributes(getAlertAudioAttributes());
                            // Flush the text to speech queue
                            mTts.speak("", TextToSpeech.QUEUE_FLUSH, null, null);
                            res = mTts.speak(mMessageBody, 2, null, TTS_UTTERANCE_ID);
                            mIsTextToSpeechSpeaking = true;
                            setState(STATE_SPEAKING);
                        }
                        if (res != TextToSpeech.SUCCESS) {
                            loge("TTS engine not ready or language not supported or speak() "
                                    + "failed");
                            stopAlertAudioService();
                        }
                        break;

                    case ALERT_LED_FLASH_TOGGLE:
                        if (enableLedFlash(msg.arg1 != 0)) {
                            sendMessageDelayed(mHandler.obtainMessage(
                                    ALERT_LED_FLASH_TOGGLE, msg.arg1 != 0 ? 0 : 1, 0),
                                    DEFAULT_LED_FLASH_INTERVAL_MSEC);
                        }
                        break;

                    default:
                        loge("Handler received unknown message, what=" + msg.what);
                }
            }
        };
        mPhoneStateListener = new PhoneStateListener() {
            @Override
            public void onCallStateChanged(int state, String ignored) {
                // Stop the alert sound and speech if the call state changes.
                if (state != TelephonyManager.CALL_STATE_IDLE
                        && state != mInitialCallState) {
                    if (DBG) log("Call interrupted. Stop CellBroadcastAlertAudio service");
                    stopAlertAudioService();
                }
            }
        };
        mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
    }

    @Override
    public void onDestroy() {
        setState(STATE_STOPPING);
        // stop audio, vibration and TTS
        if (DBG) log("onDestroy");
        stop();
        // Stop listening for incoming calls.
        mTelephonyManager.listen(mPhoneStateListener, LISTEN_NONE);
        // shutdown TTS engine
        if (mTts != null) {
            try {
                mTts.shutdown();
            } catch (IllegalStateException e) {
                // catch "Unable to retrieve AudioTrack pointer for stop()" exception
                loge("exception trying to shutdown text-to-speech");
            }
        }
        if (mEnableAudio) {
            // Release the audio focus so other audio (e.g. music) can resume.
            // Do not do this in stop() because stop() is also called when we stop the tone (before
            // TTS is playing). We only want to release the focus when tone and TTS are played.
            mAudioManager.abandonAudioFocus(this);
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (DBG) log("onStartCommand");
        // No intent, tell the system not to restart us.
        if (intent == null) {
            if (DBG) log("Null intent. Stop CellBroadcastAlertAudio service");
            stopAlertAudioService();
            return START_NOT_STICKY;
        }

        // Check if service stop is in progress
        if (getState() == STATE_STOPPING) {
            if (DBG) log("stop is in progress");
            PendingIntent pi;
            pi = PendingIntent.getService(this, 1 /*REQUEST_CODE_CONTENT_INTENT*/, intent,
                    PendingIntent.FLAG_ONE_SHOT
                            | PendingIntent.FLAG_UPDATE_CURRENT
                            | PendingIntent.FLAG_IMMUTABLE);
            AlarmManager alarmManager = getSystemService(AlarmManager.class);
            if (alarmManager == null) {
                loge("can't get Alarm Service");
                return START_NOT_STICKY;
            }
            if (DBG) log("resent intent");
            // resent again
            long triggerTime = SystemClock.elapsedRealtime() + DEFAULT_RESENT_DELAY_MSEC;
            alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP,
                    triggerTime, pi);
            return START_STICKY;
        }

        mStartId = startId;
        return handleStartIntent(intent);
    }

    /**
     * Handle the start intent
     *
     * @param intent    the intent to start the service
     */
    @VisibleForTesting
    public int handleStartIntent(Intent intent) {
        // Get text to speak (if enabled by user)
        mMessageBody = intent.getStringExtra(ALERT_AUDIO_MESSAGE_BODY);
        mMessageLanguage = intent.getStringExtra(ALERT_AUDIO_MESSAGE_LANGUAGE);
        mSubId = intent.getIntExtra(ALERT_AUDIO_SUB_INDEX,
                SubscriptionManager.INVALID_SUBSCRIPTION_ID);

        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);

        // retrieve whether to play alert sound in full volume regardless Do Not Disturb is on.
        mOverrideDnd = intent.getBooleanExtra(ALERT_AUDIO_OVERRIDE_DND_EXTRA, false);
        // retrieve the vibrate settings from cellbroadcast receiver settings.
        mEnableVibrate = prefs.getBoolean(CellBroadcastSettings.KEY_ENABLE_ALERT_VIBRATE, true)
                || mOverrideDnd;
        // retrieve the vibration patterns.
        mVibrationPattern = intent.getIntArrayExtra(ALERT_AUDIO_VIBRATION_PATTERN_EXTRA);

        Resources res = CellBroadcastSettings.getResourcesByOperator(getApplicationContext(),
                mSubId, CellBroadcastReceiver.getRoamingOperatorSupported(getApplicationContext()));
        mEnableLedFlash = res.getBoolean(R.bool.enable_led_flash);

        // retrieve the customized alert duration. -1 means play the alert with the tone's duration.
        mAlertDuration = intent.getIntExtra(ALERT_AUDIO_DURATION, -1);
        // retrieve the alert type
        mAlertType = AlertType.DEFAULT;
        if (intent.getSerializableExtra(ALERT_AUDIO_TONE_TYPE) != null) {
            mAlertType = (AlertType) intent.getSerializableExtra(ALERT_AUDIO_TONE_TYPE);
        }

        switch (mAudioManager.getRingerMode()) {
            case AudioManager.RINGER_MODE_SILENT:
                if (DBG) log("Ringer mode: silent");
                if (!mOverrideDnd) {
                    mEnableVibrate = false;
                }
                // If the phone is in silent mode, we only enable the audio when override dnd
                // setting is turned on.
                mEnableAudio = mOverrideDnd;
                break;
            case AudioManager.RINGER_MODE_VIBRATE:
                if (DBG) log("Ringer mode: vibrate");
                // If the phone is in vibration mode, we only enable the audio when override dnd
                // setting is turned on.
                mEnableAudio = mOverrideDnd;
                break;
            case AudioManager.RINGER_MODE_NORMAL:
            default:
                if (DBG) log("Ringer mode: normal");
                mEnableAudio = true;
                break;
        }

        if (mMessageBody != null && mEnableAudio) {
            if (mTts == null) {
                mTts = new TextToSpeech(this, this);
            } else if (mTtsEngineReady) {
                setTtsLanguage();
            }
        }

        if ((mEnableAudio || mEnableVibrate) && (mAlertType != AlertType.MUTE)) {
            playAlertTone(mAlertType, mVibrationPattern);
        } else {
            if (DBG) log("No audio/vibrate playing. Stop CellBroadcastAlertAudio service");
            stopAlertAudioService();
            return START_NOT_STICKY;
        }

        // Record the initial call state here so that the new alarm has the
        // newest state.
        mInitialCallState = mTelephonyManager.getCallState();

        return START_STICKY;
    }

    // Volume suggested by media team for in-call alarms.
    private static final float IN_CALL_VOLUME_LEFT = 0.125f;
    private static final float IN_CALL_VOLUME_RIGHT = 0.125f;

    /**
     * Start playing the alert sound.
     *
     * @param alertType    the alert type (e.g. default, earthquake, tsunami, etc..)
     * @param patternArray the alert vibration pattern
     */
    private void playAlertTone(AlertType alertType, int[] patternArray) {
        // stop() checks to see if we are already playing.
        stop();

        log("playAlertTone: alertType=" + alertType + ", mEnableVibrate=" + mEnableVibrate
                + ", mEnableAudio=" + mEnableAudio + ", mOverrideDnd=" + mOverrideDnd
                + ", mSubId=" + mSubId);
        Resources res = CellBroadcastSettings.getResourcesByOperator(getApplicationContext(),
                mSubId, CellBroadcastReceiver.getRoamingOperatorSupported(getApplicationContext()));

        // Vibration duration in milliseconds
        long vibrateDuration = 0;

        // Get the alert tone duration. Negative tone duration value means we only play the tone
        // once, not repeat it.
        int customAlertDuration = mAlertDuration;

        // Start the vibration first.
        if (mEnableVibrate) {
            long[] vibrationPattern = new long[patternArray.length];

            for (int i = 0; i < patternArray.length; i++) {
                vibrationPattern[i] = patternArray[i];
                vibrateDuration += patternArray[i];
            }

            AudioAttributes.Builder attrBuilder = new AudioAttributes.Builder();
            attrBuilder.setUsage(AudioAttributes.USAGE_ALARM);
            if (mOverrideDnd) {
                // Set the flags to bypass DnD mode if override dnd is turned on.
                attrBuilder.setFlags(AudioAttributes.FLAG_BYPASS_INTERRUPTION_POLICY
                        | AudioAttributes.FLAG_BYPASS_MUTE);
            }
            AudioAttributes attr = attrBuilder.build();
            // If we only play the tone once, then we also play the vibration pattern once.
            int repeatIndex = (customAlertDuration < 0)
                    ? -1 /* not repeat */ : 0 /* index to repeat */;
            VibrationEffect effect = VibrationEffect.createWaveform(vibrationPattern, repeatIndex);
            log("vibrate: effect=" + effect + ", attr=" + attr + ", duration="
                    + customAlertDuration);
            mVibrator.vibrate(effect, attr);
            // Android default behavior will stop vibration when screen turns off.
            // if mute by physical button is not allowed, press power key should not turn off
            // vibration.
            if (!res.getBoolean(R.bool.mute_by_physical_button)) {
                mScreenOffReceiver = new ScreenOffReceiver(effect, attr);
                registerReceiver(mScreenOffReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF));
            }
        }

        if (mEnableLedFlash) {
            log("Start LED flashing");
            mHandler.sendMessage(mHandler.obtainMessage(ALERT_LED_FLASH_TOGGLE, 1, 0));
        }

        if (mEnableAudio) {
            // future optimization: reuse media player object
            mMediaPlayer = mMediaPlayerInjected != null ? mMediaPlayerInjected : new MediaPlayer();
            mMediaPlayer.setOnErrorListener(new OnErrorListener() {
                public boolean onError(MediaPlayer mp, int what, int extra) {
                    loge("Error occurred while playing audio.");
                    mHandler.sendMessage(mHandler.obtainMessage(ALERT_SOUND_FINISHED));
                    return true;
                }
            });

            // If the duration is not specified by the config, just play the alert tone
            // with the tone's duration.
            if (customAlertDuration < 0) {
                mMediaPlayer.setOnCompletionListener(new OnCompletionListener() {
                    public void onCompletion(MediaPlayer mp) {
                        if (DBG) log("Audio playback complete.");
                        mHandler.sendMessage(mHandler.obtainMessage(ALERT_SOUND_FINISHED));
                        return;
                    }
                });
            }

            try {
                log("Locale=" + res.getConfiguration().getLocales() + ", alertType=" + alertType);

                // Load the tones based on type
                switch (alertType) {
                    case ETWS_EARTHQUAKE:
                        setDataSourceFromResource(res, mMediaPlayer, R.raw.etws_earthquake);
                        break;
                    case ETWS_TSUNAMI:
                        setDataSourceFromResource(res, mMediaPlayer, R.raw.etws_tsunami);
                        break;
                    case OTHER:
                        setDataSourceFromResource(res, mMediaPlayer, R.raw.etws_other_disaster);
                        break;
                    case ETWS_DEFAULT:
                        setDataSourceFromResource(res, mMediaPlayer, R.raw.etws_default);
                        break;
                    case INFO:
                    case AREA:
                        if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)) {
                            //TODO(b/279183006): remove watch workaround when URI supported.
                            setDataSourceFromResource(res, mMediaPlayer, R.raw.watch_info);
                        } else {
                            mMediaPlayer.setDataSource(this,
                                    Settings.System.DEFAULT_NOTIFICATION_URI);
                        }
                        break;
                    case TEST:
                    case DEFAULT:
                    default:
                        setDataSourceFromResource(res, mMediaPlayer, R.raw.default_tone);
                }

                // Request audio focus (though we're going to play even if we don't get it). The
                // only scenario we are not getting focus immediately is a voice call is holding
                // focus, since we are passing AUDIOFOCUS_FLAG_DELAY_OK, the focus will be granted
                // once voice call ends.
                mAudioManager.requestAudioFocus(this,
                        new AudioAttributes.Builder().setLegacyStreamType(
                                (alertType == AlertType.INFO || alertType == AlertType.AREA) ?
                                        AudioManager.STREAM_NOTIFICATION
                                        : AudioManager.STREAM_ALARM).build(),
                        AudioManager.AUDIOFOCUS_GAIN_TRANSIENT,
                        AudioManager.AUDIOFOCUS_FLAG_DELAY_OK);
                mMediaPlayer.setAudioAttributes(getAlertAudioAttributes());
                setAlertVolume();

                // If we are using the custom alert duration, set looping to true so we can repeat
                // the alert. The tone playing will stop when ALERT_SOUND_FINISHED arrives.
                // Otherwise we just play the alert tone once.
                mMediaPlayer.setLooping(customAlertDuration >= 0);
                mMediaPlayer.prepare();
                // If the duration is specified by the config, stop playing the alert after
                // the specified duration.
                if (customAlertDuration >= 0) {
                    mHandler.sendMessageDelayed(mHandler.obtainMessage(ALERT_SOUND_FINISHED),
                            customAlertDuration);
                }
                mMediaPlayer.start();
                mIsMediaPlayerStarted = true;

            } catch (Exception ex) {
                loge("Failed to play alert sound: " + ex);
                CellBroadcastReceiverMetrics.getInstance().logModuleError(
                        ERRSRC_CBR, ERRTYPE_PLAYSOUND);
                // Immediately move into the next state ALERT_SOUND_FINISHED.
                mHandler.sendMessage(mHandler.obtainMessage(ALERT_SOUND_FINISHED));
            }
        } else {
            // In normal mode (playing tone + vibration), this service will stop after audio
            // playback is done. However, if the device is in vibrate only mode, we need to stop
            // the service right after vibration because there won't be any audio complete callback
            // to stop the service. Unfortunately it's not like MediaPlayer has onCompletion()
            // callback that we can use, we'll have to use our own timer to stop the service.
            mHandler.sendMessageDelayed(mHandler.obtainMessage(ALERT_SOUND_FINISHED),
                    customAlertDuration >= 0 ? customAlertDuration : vibrateDuration);
        }
        setState(STATE_ALERTING);
    }

    private static void setDataSourceFromResource(Resources resources,
            MediaPlayer player, int res) throws java.io.IOException {
        AssetFileDescriptor afd = resources.openRawResourceFd(res);
        if (afd != null) {
            player.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(),
                    afd.getLength());
            afd.close();
        }
    }

    /**
     * Turn on camera's LED
     *
     * @param on {@code true} if turned on, otherwise turned off.
     * @return {@code true} if successful, otherwise false.
     */
    private boolean enableLedFlash(boolean on) {
        log("enbleLedFlash=" + on);
        CameraManager cameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
        if (cameraManager == null) return false;
        final String[] ids;
        try {
            ids = cameraManager.getCameraIdList();
        } catch (CameraAccessException e) {
            CellBroadcastReceiverMetrics.getInstance()
                    .logModuleError(ERRSRC_CBR, ERRTYPE_PLAYFLASH);
            log("Can't get camera id");
            return false;
        }

        boolean success = false;
        for (String id : ids) {
            try {
                CameraCharacteristics c = cameraManager.getCameraCharacteristics(id);
                Boolean flashAvailable = c.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
                if (flashAvailable != null && flashAvailable) {
                    cameraManager.setTorchMode(id, on);
                    success = true;
                }
            } catch (CameraAccessException e) {
                CellBroadcastReceiverMetrics.getInstance().logModuleError(
                        ERRSRC_CBR, ERRTYPE_PLAYFLASH);
                log("Can't flash. e=" + e);
                // continue with the next available camera
            }
        }
        return success;
    }

    /**
     * Stops alert audio and speech.
     */
    public void stop() {
        if (DBG) log("stop()");

        mHandler.removeMessages(ALERT_SOUND_FINISHED);
        mHandler.removeMessages(ALERT_PAUSE_FINISHED);
        mHandler.removeMessages(ALERT_LED_FLASH_TOGGLE);

        resetAlarmStreamVolume();

        // Stop audio playing
        if (mMediaPlayer != null && mIsMediaPlayerStarted) {
            try {
                mMediaPlayer.stop();
                mMediaPlayer.release();
            } catch (IllegalStateException e) {
                // catch "Unable to retrieve AudioTrack pointer for stop()" exception
                loge("exception trying to stop media player");
            }
            mIsMediaPlayerStarted = false;
            mMediaPlayer = null;
        }

        // Stop vibrator
        mVibrator.cancel();
        if (mScreenOffReceiver != null) {
            try {
                unregisterReceiver(mScreenOffReceiver);
            } catch (Exception e) {
                // already unregistered
            }
            mScreenOffReceiver = null;
        }

        if (mEnableLedFlash) {
            enableLedFlash(false);
        }

        if (mTts != null && mIsTextToSpeechSpeaking) {
            try {
                mTts.stop();
            } catch (IllegalStateException e) {
                // catch "Unable to retrieve AudioTrack pointer for stop()" exception
                loge("exception trying to stop text-to-speech");
                CellBroadcastReceiverMetrics.getInstance()
                        .logModuleError(ERRSRC_CBR, ERRTYPE_PLAYTTS);
            }
            mIsTextToSpeechSpeaking = false;
        }

        // Service will be destroyed if the state is STATE_STOPPING,
        // so it should not be changed to another state.
        if (getState() != STATE_STOPPING) {
            setState(STATE_IDLE);
        }
    }

    @Override
    public void onAudioFocusChange(int focusChange) {
        log("onAudioFocusChanged: " + focusChange);
        // Do nothing, as we don't care if focus was steal from other apps, as emergency alerts will
        // play anyway.
    }

    /**
     * Get audio attribute for the alarm.
     */
    private AudioAttributes getAlertAudioAttributes() {
        AudioAttributes.Builder builder = new AudioAttributes.Builder();

        builder.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION);
        builder.setUsage((mAlertType == AlertType.INFO || mAlertType == AlertType.AREA) ?
                AudioAttributes.USAGE_NOTIFICATION : AudioAttributes.USAGE_ALARM);
        if (mOverrideDnd) {
            // Set FLAG_BYPASS_INTERRUPTION_POLICY and FLAG_BYPASS_MUTE so that it enables
            // audio in any DnD mode, even in total silence DnD mode (requires MODIFY_PHONE_STATE).

            // Note: this only works when the audio attributes usage is set to USAGE_ALARM. If
            // regulatory concerns mean that we need to bypass DnD for AlertType.INFO or
            // AlertType.AREA as well, we'll need to add a config flag to have INFO go over the
            // alarm stream as well for those jurisdictions in which those regulatory concerns apply
            builder.setFlags(AudioAttributes.FLAG_BYPASS_INTERRUPTION_POLICY
                    | AudioAttributes.FLAG_BYPASS_MUTE);
        }

        return builder.build();
    }

    /**
     * Set volume for alerts.
     */
    private void setAlertVolume() {
        if (mTelephonyManager.getCallState() != TelephonyManager.CALL_STATE_IDLE
                || isOnEarphone()) {
            // If we are in a call, play the alert
            // sound at a low volume to not disrupt the call.
            log("in call: reducing volume");
            mMediaPlayer.setVolume(IN_CALL_VOLUME_LEFT, IN_CALL_VOLUME_RIGHT);
        } else if (mOverrideDnd) {
            // If override DnD is turned on,
            // we overwrite volume setting of STREAM_ALARM to full, play at
            // max possible volume, and reset it after it's finished.
            setAlarmStreamVolumeToFull();
        }
    }

    private boolean isOnEarphone() {
        AudioDeviceInfo[] deviceList = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);

        for (AudioDeviceInfo devInfo : deviceList) {
            int type = devInfo.getType();
            if (type == AudioDeviceInfo.TYPE_WIRED_HEADSET
                    || type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES
                    || type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO
                    || type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP) {
                return true;
            }
        }

        return false;
    }

    /**
     * Set volume of STREAM_ALARM to full.
     */
    private void setAlarmStreamVolumeToFull() {
        if (mAlertType != AlertType.INFO && mAlertType != AlertType.AREA) {
            log("setting alarm volume to full for cell broadcast alerts.");
            int streamType = AudioManager.STREAM_ALARM;
            mUserSetAlarmVolume = mAudioManager.getStreamVolume(streamType);
            mResetAlarmVolumeNeeded = true;
            mAudioManager.setStreamVolume(streamType,
                    mAudioManager.getStreamMaxVolume(streamType), 0);
        } else {
            log("Skipping setting alarm volume to full for alert type INFO and AREA");
        }
    }

    /**
     * Reset volume of STREAM_ALARM, if needed.
     */
    private void resetAlarmStreamVolume() {
        if (mResetAlarmVolumeNeeded) {
            log("resetting alarm volume to back to " + mUserSetAlarmVolume);
            mAudioManager.setStreamVolume(AudioManager.STREAM_ALARM, mUserSetAlarmVolume, 0);
            mResetAlarmVolumeNeeded = false;
        }
    }

    /**
     * Stop CellBroadcastAlertAudio Service and set state to STATE_STOPPING
     */
    private boolean stopAlertAudioService() {
        if (DBG) log("stopAlertAudioService, current state is " + getState());
        boolean result = false;
        if (getState() != STATE_STOPPING) {
            setState(STATE_STOPPING);
            result = stopSelfResult(mStartId);
            if (DBG) log((result ? "Successful" : "Failed")
                    + " to stop AlertAudioService[" + mStartId + "]");
        }
        return result;
    }

    /**
     * Set AlertAudioService state
     *
     * @param state service status
     */
    private synchronized void setState(int state) {
        if (DBG) log("Set state from " + mState + " to " + state);
        mState = state;
    }

    /**
     * Get AlertAudioService status
     * @return service status
     */
    @VisibleForTesting
    public synchronized int getState() {
        return mState;
    }

    /*
     * BroadcastReceiver for screen off events. Used for Latam.
     * CMAS requirements to make sure vibration continues when screen goes off
     */
    private class ScreenOffReceiver extends BroadcastReceiver {
        VibrationEffect mVibrationEffect;
        AudioAttributes mAudioAttr;

        ScreenOffReceiver(VibrationEffect effect, AudioAttributes attributes) {
            this.mVibrationEffect = effect;
            this.mAudioAttr = attributes;
        }

        @Override
        public void onReceive(Context context, Intent intent) {
            // Restart the vibration after screen off
            if (mState == STATE_ALERTING) {
                mVibrator.vibrate(mVibrationEffect, mAudioAttr);
            }
        }
    }

    private static void log(String msg) {
        Log.d(TAG, msg);
    }

    private static void loge(String msg) {
        Log.e(TAG, msg);
    }
}
