package com.secureline.voice;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.pm.PackageManager;
import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.os.ServiceManager;
import android.util.Log;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

import com.android.secureline.ISecureLineVoice;
import com.secureline.ui.SecureLineStatusBadge;

import java.io.ByteArrayOutputStream;

public class MainActivity extends Activity {

    private static final String TAG =
            "SecureLineVoice";

    private static final int MICROPHONE_PERMISSION_REQUEST =
            1;

    private static final int TEST_SAMPLE_RATE =
            48000;

    private static final int TEST_MAX_SECONDS =
            5;

    private static final int PCM_BYTES_PER_SAMPLE =
            2;

    private ISecureLineVoice mService;

    private SecureLineStatusBadge voiceToggleButton;
    private SecureLineStatusBadge modeValueButton;
    private boolean voiceEnabled;
    private int selectedModePosition;
    private TextView modeDescription;
    private Button testButton;

    private volatile boolean testing = false;
    private volatile boolean playingTest = false;
    private volatile boolean activityStopped = false;
    private volatile boolean activityDestroyed = false;

    private volatile AudioRecord testRecorder;
    private volatile AudioTrack testPlayer;

    private boolean mInitializing = true;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(
                R.layout.activity_main
        );

        voiceToggleButton =
                findViewById(
                        R.id.voice_toggle_button
                );

        modeValueButton =
                findViewById(
                        R.id.mode_value_button
                );

        modeDescription =
                findViewById(
                        R.id.mode_description
                );

        testButton =
                findViewById(
                        R.id.test_voice
                );

        voiceToggleButton.setEnabled(false);
        modeValueButton.setEnabled(false);
        testButton.setEnabled(false);

        requestMicPermission();

        setupVoiceToggleButton();
        setupModeValueButton();
        setupTestButton();

        connectService();
    }

    @Override
    protected void onStart() {

        super.onStart();

        activityStopped = false;

        if (!testing && !playingTest) {

            resetVoiceTestUi();
        }
    }

    @Override
    protected void onStop() {

        activityStopped = true;

        /*
         * Only the local Voice Test is stopped here.
         * SecureLine Voice protection remains enabled through
         * the system service and continues working in supported apps.
         */
        stopVoiceTestRecording();
        stopVoiceTestPlayback();

        super.onStop();
    }

    @Override
    protected void onDestroy() {

        activityDestroyed = true;

        stopVoiceTestRecording();
        stopVoiceTestPlayback();

        super.onDestroy();
    }

    private void requestMicPermission() {

        if (checkSelfPermission(
                android.Manifest.permission.RECORD_AUDIO)
                != PackageManager.PERMISSION_GRANTED) {

            requestPermissions(
                    new String[]{
                            android.Manifest.permission.RECORD_AUDIO
                    },
                    MICROPHONE_PERMISSION_REQUEST
            );
        }
    }

    private void connectService() {

        new Thread(() -> {

            for (int i = 0; i < 40; i++) {

                try {

                    mService =
                            ISecureLineVoice.Stub.asInterface(
                                    ServiceManager.getService(
                                            "secureline_voice"
                                    )
                            );

                    if (mService != null) {

                        Log.i(
                                TAG,
                                "service connected"
                        );

                        syncUiState();

                        return;
                    }

                    Thread.sleep(250);

                } catch (Throwable ignored) {
                }
            }

            Log.e(
                    TAG,
                    "secureline_voice service not found"
            );

        }, "SecureLineVoiceServiceConnection").start();
    }

    private void syncUiState() {

        try {

            if (mService == null)
                return;

            boolean enabled =
                    mService.isVoiceEnabled();

            String mode =
                    mService.getVoiceMode();

            runOnUiThread(() -> {

                voiceEnabled = enabled;

                String[] values =
                        getResources()
                                .getStringArray(
                                        R.array.voice_mode_values
                                );

                selectedModePosition = 0;

                for (int i = 0;
                     i < values.length;
                     i++) {

                    if (values[i]
                            .equalsIgnoreCase(mode)) {

                        selectedModePosition = i;
                        break;
                    }
                }

                updateModeDescription(selectedModePosition);
                updateVoiceControls();

                mInitializing = false;

                voiceToggleButton.setEnabled(true);
                testButton.setEnabled(true);
            });

        } catch (Throwable e) {

            Log.e(
                    TAG,
                    "syncUiState failed",
                    e
            );
        }
    }

    private void setupVoiceToggleButton() {

        voiceToggleButton.setOnClickListener(view -> {

            if (mInitializing || mService == null) {
                return;
            }

            final boolean enabling = !voiceEnabled;
            final int title = enabling
                    ? R.string.enable_voice_protection_title
                    : R.string.disable_voice_protection_title;
            final int message = enabling
                    ? R.string.enable_voice_protection_message
                    : R.string.disable_voice_protection_message;

            new AlertDialog.Builder(this, R.style.SecureLineUI_Dialog)
                    .setTitle(title)
                    .setMessage(message)
                    .setNegativeButton(R.string.cancel, null)
                    .setPositiveButton(
                            enabling ? R.string.turn_on : R.string.turn_off,
                            (dialog, which) -> applyVoiceEnabled(enabling))
                    .show();
        });
    }

    private void applyVoiceEnabled(boolean enabled) {

        try {

            if (mService == null) {
                return;
            }

            mService.setVoiceEnabled(enabled);
            voiceEnabled = enabled;
            updateVoiceControls();

        } catch (Throwable error) {

            Log.e(TAG, "setVoiceEnabled failed", error);

            Toast.makeText(
                    this,
                    R.string.voice_service_unavailable,
                    Toast.LENGTH_SHORT
            ).show();
        }
    }

    private void setupModeValueButton() {

        modeValueButton.setOnClickListener(view -> {

            if (mInitializing || mService == null || !voiceEnabled) {
                return;
            }

            CharSequence[] modes =
                    getResources().getTextArray(
                            R.array.voice_modes
                    );

            new AlertDialog.Builder(this, R.style.SecureLineUI_Dialog)
                    .setTitle(R.string.select_voice_mode)
                    .setSingleChoiceItems(
                            modes,
                            selectedModePosition,
                            (dialog, position) -> {
                                dialog.dismiss();
                                confirmVoiceModeChange(position);
                            })
                    .setNegativeButton(R.string.cancel, null)
                    .show();
        });
    }

    private void confirmVoiceModeChange(int position) {

        CharSequence[] modes =
                getResources().getTextArray(
                        R.array.voice_modes
                );

        if (position < 0 || position >= modes.length) {
            return;
        }

        new AlertDialog.Builder(this, R.style.SecureLineUI_Dialog)
                .setTitle(R.string.change_voice_mode_title)
                .setMessage(getString(
                        R.string.change_voice_mode_message,
                        modes[position]))
                .setNegativeButton(R.string.cancel, null)
                .setPositiveButton(
                        R.string.change,
                        (dialog, which) -> applyVoiceMode(position))
                .show();
    }

    private void applyVoiceMode(int position) {

        try {

            if (mService == null) {
                return;
            }

            String[] values =
                    getResources()
                            .getStringArray(
                                    R.array.voice_mode_values
                            );

            if (position < 0 || position >= values.length) {
                return;
            }

            mService.setVoiceMode(values[position]);
            selectedModePosition = position;
            updateModeDescription(position);
            updateVoiceControls();

        } catch (Throwable error) {

            Log.e(TAG, "setVoiceMode failed", error);

            Toast.makeText(
                    this,
                    R.string.voice_service_unavailable,
                    Toast.LENGTH_SHORT
            ).show();
        }
    }

    private void updateVoiceControls() {

        voiceToggleButton.setText(
                voiceEnabled
                        ? R.string.on
                        : R.string.off
        );

        voiceToggleButton.setBadgeType(
                voiceEnabled
                        ? SecureLineStatusBadge.TYPE_ACTIVE
                        : SecureLineStatusBadge.TYPE_NEUTRAL
        );

        CharSequence[] modes =
                getResources().getTextArray(
                        R.array.voice_modes
                );

        if (selectedModePosition < 0
                || selectedModePosition >= modes.length) {

            selectedModePosition = 0;
        }

        modeValueButton.setText(
                modes[selectedModePosition]
        );

        modeValueButton.setEnabled(
                voiceEnabled
        );

        modeValueButton.setClickable(
                voiceEnabled
        );

        modeValueButton.setFocusable(
                voiceEnabled
        );

        modeValueButton.setAlpha(
                voiceEnabled ? 1f : 0.72f
        );

        modeValueButton.setBadgeType(
                voiceEnabled
                        ? SecureLineStatusBadge.TYPE_ACTIVE
                        : SecureLineStatusBadge.TYPE_NEUTRAL
        );
    }

    private void updateModeDescription(
            int position) {

        switch (position) {

            case 0:
                modeDescription.setText(
                        R.string.mode_deep_voice_desc
                );
                break;

            case 1:
                modeDescription.setText(
                        R.string.mode_documentary_voice_desc
                );
                break;

            case 2:
                modeDescription.setText(
                        R.string.mode_private_voice_desc
                );
                break;

            case 3:
                modeDescription.setText(
                        R.string.mode_clear_voice_desc
                );
                break;

            case 4:
                modeDescription.setText(
                        R.string.mode_bright_voice_desc
                );
                break;

            case 5:
                modeDescription.setText(
                        R.string.mode_ghost_voice_desc
                );
                break;

            default:
                modeDescription.setText(
                        R.string.mode_deep_voice_desc
                );
                break;
        }
    }

    private void setupTestButton() {

        testButton.setOnClickListener(view -> {

            try {

                if (testing) {

                    stopVoiceTestRecording();

                    return;
                }

                if (playingTest) {

                    stopVoiceTestPlayback();

                    return;
                }

		if (mService == null) {

		    Toast.makeText(
		            MainActivity.this,
		            R.string.voice_service_unavailable,
		            Toast.LENGTH_SHORT
		    ).show();

		    return;
		}

                if (!voiceEnabled) {

                    Toast.makeText(
                            MainActivity.this,
                            R.string.voice_test_requires_enabled,
                            Toast.LENGTH_SHORT
                    ).show();

                    return;
                }

                if (checkSelfPermission(
                        android.Manifest.permission.RECORD_AUDIO)
                        != PackageManager.PERMISSION_GRANTED) {

                    requestMicPermission();

                    Toast.makeText(
                            MainActivity.this,
                            R.string.voice_test_requires_microphone,
                            Toast.LENGTH_SHORT
                    ).show();

                    return;
                }

                startVoiceTest();

            } catch (Throwable e) {

                Log.e(
                        TAG,
                        "Voice test failed",
                        e
                );

                Toast.makeText(
                        MainActivity.this,
                        R.string.voice_test_failed,
                        Toast.LENGTH_LONG
                ).show();

                stopVoiceTestRecording();
                stopVoiceTestPlayback();
                resetVoiceTestUi();
            }
        });
    }

    private void startVoiceTest() {

	if (testing
	        || playingTest
	        || activityDestroyed
	        || activityStopped) {

            return;
        }

        testing = true;

        runOnUiThread(() -> {

            testButton.setEnabled(true);

            testButton.setText(
                    R.string.stop_voice_test
            );
        });

        new Thread(() -> {

            AudioRecord recorder = null;

            ByteArrayOutputStream recordedAudio =
                    new ByteArrayOutputStream();

            boolean recordingStarted = false;
	    boolean recordingFailed = false;

            try {

                int minimumBuffer =
                        AudioRecord.getMinBufferSize(
                                TEST_SAMPLE_RATE,
                                AudioFormat.CHANNEL_IN_MONO,
                                AudioFormat.ENCODING_PCM_16BIT
                        );

                if (minimumBuffer <= 0) {

                    throw new IllegalStateException(
                            "Invalid AudioRecord buffer: "
                                    + minimumBuffer
                    );
                }

                int recordBufferSize =
                        Math.max(
                                minimumBuffer * 2,
                                4096
                        );

                recorder =
                        new AudioRecord.Builder()
                                .setAudioSource(
                                        MediaRecorder.AudioSource.MIC
                                )
                                .setAudioFormat(
                                        new AudioFormat.Builder()
                                                .setEncoding(
                                                        AudioFormat
                                                                .ENCODING_PCM_16BIT
                                                )
                                                .setSampleRate(
                                                        TEST_SAMPLE_RATE
                                                )
                                                .setChannelMask(
                                                        AudioFormat
                                                                .CHANNEL_IN_MONO
                                                )
                                                .build()
                                )
                                .setBufferSizeInBytes(
                                        recordBufferSize
                                )
                                .build();

                if (recorder.getState()
                        != AudioRecord.STATE_INITIALIZED) {

                    throw new IllegalStateException(
                            "AudioRecord initialization failed"
                    );
                }

                testRecorder = recorder;

                byte[] buffer =
                        new byte[Math.min(
                                recordBufferSize,
                                4096
                        )];

                int maximumBytes =
                        TEST_SAMPLE_RATE
                                * PCM_BYTES_PER_SAMPLE
                                * TEST_MAX_SECONDS;

                recorder.startRecording();

                recordingStarted = true;

                Log.i(
                        TAG,
                        "Voice test recording started"
                );

		while (testing
		        && !activityDestroyed
		        && !activityStopped
		        && recordedAudio.size()
		        < maximumBytes) {

                    int remainingBytes =
                            maximumBytes
                                    - recordedAudio.size();

                    int requestedBytes =
                            Math.min(
                                    buffer.length,
                                    remainingBytes
                            );

                    int read =
                            recorder.read(
                                    buffer,
                                    0,
                                    requestedBytes,
                                    AudioRecord.READ_BLOCKING
                            );

		    if (read > 0) {

		        recordedAudio.write(
		                buffer,
		                0,
		                read
		        );

		    } else if (!testing
		            || activityDestroyed
		            || activityStopped) {

		        /*
		         * Recorder was intentionally stopped by the user
		         * or because the activity is closing.
		         */
		        break;

		    } else if (read
		            == AudioRecord.ERROR_DEAD_OBJECT
		            || read
		            == AudioRecord.ERROR_INVALID_OPERATION
		            || read
		            == AudioRecord.ERROR_BAD_VALUE) {

		        throw new IllegalStateException(
		                "AudioRecord read failed: "
		                        + read
		        );
		    }
                }

	    } catch (Throwable e) {

	        recordingFailed = true;

	        Log.e(
	                TAG,
	                "Voice test recording failed",
	                e
	        );

	        if (!activityDestroyed
	                && !activityStopped) {

	            runOnUiThread(() ->
	                    Toast.makeText(
	                            MainActivity.this,
	                            R.string.voice_test_failed,
	                            Toast.LENGTH_LONG
	                    ).show()
	            );
	        }

	    } finally {

                testing = false;

                if (recorder != null) {

                    if (recordingStarted) {

                        try {

                            if (recorder.getRecordingState()
                                    == AudioRecord
                                    .RECORDSTATE_RECORDING) {

                                recorder.stop();
                            }

                        } catch (Throwable ignored) {
                        }
                    }

                    try {
                        recorder.release();
                    } catch (Throwable ignored) {
                    }
                }

                testRecorder = null;

                Log.i(
                        TAG,
                        "Voice test recording stopped"
                );
            }

	    if (activityDestroyed || activityStopped) {

	        resetVoiceTestUi();

	        return;
	    }

	    if (recordingFailed) {

	        resetVoiceTestUi();

	        return;
	    }

	    byte[] audio =
	            recordedAudio.toByteArray();

            if (audio.length
                    < TEST_SAMPLE_RATE
                    * PCM_BYTES_PER_SAMPLE
                    / 4) {

                runOnUiThread(() ->
                        Toast.makeText(
                                MainActivity.this,
                                R.string.voice_test_too_short,
                                Toast.LENGTH_SHORT
                        ).show()
                );

                resetVoiceTestUi();

                return;
            }

            playVoiceTest(
                    audio
            );

        }, "SecureLineVoiceRecorder").start();
    }

    private void playVoiceTest(
            byte[] audio) {

	if (audio == null
	        || audio.length == 0
	        || activityDestroyed
	        || activityStopped) {

            resetVoiceTestUi();

            return;
        }

        playingTest = true;

        runOnUiThread(() -> {

            testButton.setEnabled(true);

            testButton.setText(
                    R.string.stop_voice_playback
            );
        });

        AudioTrack player = null;

        try {

            int minimumBuffer =
                    AudioTrack.getMinBufferSize(
                            TEST_SAMPLE_RATE,
                            AudioFormat.CHANNEL_OUT_MONO,
                            AudioFormat.ENCODING_PCM_16BIT
                    );

            if (minimumBuffer <= 0) {

                throw new IllegalStateException(
                        "Invalid AudioTrack buffer: "
                                + minimumBuffer
                );
            }

            int trackBufferSize =
                    Math.max(
                            minimumBuffer * 2,
                            4096
                    );

            player =
                    new AudioTrack.Builder()
                            .setAudioAttributes(
                                    new AudioAttributes.Builder()
                                            .setUsage(
                                                    AudioAttributes
                                                            .USAGE_MEDIA
                                            )
                                            .setContentType(
                                                    AudioAttributes
                                                            .CONTENT_TYPE_SPEECH
                                            )
                                            .build()
                            )
                            .setAudioFormat(
                                    new AudioFormat.Builder()
                                            .setEncoding(
                                                    AudioFormat
                                                            .ENCODING_PCM_16BIT
                                            )
                                            .setSampleRate(
                                                    TEST_SAMPLE_RATE
                                            )
                                            .setChannelMask(
                                                    AudioFormat
                                                            .CHANNEL_OUT_MONO
                                            )
                                            .build()
                            )
                            .setBufferSizeInBytes(
                                    trackBufferSize
                            )
                            .setTransferMode(
                                    AudioTrack.MODE_STREAM
                            )
                            .build();

            if (player.getState()
                    != AudioTrack.STATE_INITIALIZED) {

                throw new IllegalStateException(
                        "AudioTrack initialization failed"
                );
            }

            testPlayer = player;

            /*
             * Playback uses normal media volume.
             * The microphone is already fully stopped, so
             * playback cannot feed back into the recording.
             */
            player.setVolume(0.85f);

            player.play();

            Log.i(
                    TAG,
                    "Voice test playback started"
            );

            int offset = 0;

	    while (playingTest
	            && !activityDestroyed
	            && !activityStopped
	            && offset < audio.length) {

                int bytesToWrite =
                        Math.min(
                                4096,
                                audio.length - offset
                        );

                int written =
                        player.write(
                                audio,
                                offset,
                                bytesToWrite,
                                AudioTrack.WRITE_BLOCKING
                        );

		if (written > 0) {

		    offset += written;

		} else if (!playingTest
		        || activityDestroyed
		        || activityStopped) {

		    /*
		     * Playback was intentionally stopped by the user
		     * or because the activity is closing.
		     */
		    break;

		} else {

		    throw new IllegalStateException(
		            "AudioTrack write failed: "
		                    + written
		    );
		}
            }

	    if (playingTest
	            && !activityDestroyed
	            && !activityStopped) {

                int totalFrames =
                        audio.length
                                / PCM_BYTES_PER_SAMPLE;

		while (playingTest
		        && !activityDestroyed
		        && !activityStopped
		        && player
		        .getPlaybackHeadPosition()
		        < totalFrames) {

                    Thread.sleep(20);
                }
            }

	} catch (Throwable e) {

	    if (playingTest
	            && !activityDestroyed
	            && !activityStopped) {

	        Log.e(
	                TAG,
	                "Voice test playback failed",
	                e
	        );

	        runOnUiThread(() ->
	                Toast.makeText(
	                        MainActivity.this,
	                        R.string.voice_test_failed,
	                        Toast.LENGTH_LONG
	                ).show()
	        );

	    } else {

	        Log.i(
	                TAG,
	                "Voice test playback intentionally stopped"
	        );
	    }

        } finally {

            playingTest = false;

            if (player != null) {

                try {

                    if (player.getPlayState()
                            == AudioTrack.PLAYSTATE_PLAYING) {

                        player.pause();
                    }

                } catch (Throwable ignored) {
                }

                try {
                    player.flush();
                } catch (Throwable ignored) {
                }

                try {
                    player.stop();
                } catch (Throwable ignored) {
                }

                try {
                    player.release();
                } catch (Throwable ignored) {
                }
            }

            testPlayer = null;

            Log.i(
                    TAG,
                    "Voice test playback stopped"
            );

            resetVoiceTestUi();
        }
    }

    private void stopVoiceTestRecording() {

        testing = false;

        AudioRecord recorder =
                testRecorder;

        if (recorder != null) {

            try {

                if (recorder.getRecordingState()
                        == AudioRecord
                        .RECORDSTATE_RECORDING) {

                    recorder.stop();
                }

            } catch (Throwable ignored) {
            }
        }
    }

    private void stopVoiceTestPlayback() {

        playingTest = false;

        AudioTrack player =
                testPlayer;

        if (player != null) {

            try {

                if (player.getPlayState()
                        == AudioTrack.PLAYSTATE_PLAYING) {

                    player.pause();
                }

            } catch (Throwable ignored) {
            }

            try {
                player.flush();
            } catch (Throwable ignored) {
            }
        }
    }

    private void resetVoiceTestUi() {

        if (activityDestroyed || activityStopped)
            return;

        runOnUiThread(() -> {

            testButton.setEnabled(
                    mService != null
            );

            testButton.setText(
                    R.string.start_voice_test
             );
        });
    }
}
