com.waz.zclient.ui.audiomessage.AudioMessageRecordingView.java Source code

Java tutorial

Introduction

Here is the source code for com.waz.zclient.ui.audiomessage.AudioMessageRecordingView.java

Source

/**
 * Wire
 * Copyright (C) 2016 Wire Swiss GmbH
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.waz.zclient.ui.audiomessage;

import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.LightingColorFilter;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Handler;
import android.support.v4.content.ContextCompat;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.SeekBar;
import android.widget.TextView;
import com.waz.api.AssetFactory;
import com.waz.api.AudioAssetForUpload;
import com.waz.api.AudioEffect;
import com.waz.api.AudioOverview;
import com.waz.api.PlaybackControls;
import com.waz.api.RecordingCallback;
import com.waz.api.RecordingControls;
import com.waz.zclient.core.api.scala.ModelObserver;
import com.waz.zclient.ui.R;
import com.waz.zclient.ui.utils.CursorUtils;
import com.waz.zclient.utils.StringUtils;
import com.waz.zclient.utils.ViewUtils;
import org.threeten.bp.Duration;
import org.threeten.bp.Instant;

public class AudioMessageRecordingView extends FrameLayout implements View.OnClickListener {

    private static final int RECORDING_TIMER_INTERVAL = 100;
    private static final int RECORDING_INDICATOR_VISIBLE_INTERVAL = 750;
    private static final int RECORDING_INDICATOR_HIDDEN_INTERVAL = 350;
    private PlaybackControls playbackControls;

    private enum SlideControlState {
        RECORDING, SEND_FROM_RECORDING, PREVIEW
    }

    private View closeButtonContainer;
    private TextView hintTextView;
    private View recordingIndicatorDotView;
    private View recordingIndicatorContainerView;
    private View cancelButton;
    private View bottomButton;
    private TextView bottomButtonTextView;
    private View sendButton;
    private TextView sendButtonTextView;
    private View slideControl;
    private View slideControlContainer;
    private TextView timerTextView;
    private SeekBar recordingSeekBar;
    private boolean darkTheme;

    private int actionUpMinY;
    private boolean sendImmediately;
    private AudioAssetForUpload audioAssetForUpload;

    private SlideControlState slideControlState;
    private RecordingControls recordingControls;
    private Callback callback;

    private Drawable defaultSlideControlBackground;
    private Drawable colorSlideControlBackground;

    private long recordingStartEpochMillis;
    private Handler recordingTimerHandler;
    private final Runnable recordingTimerRunnable = new Runnable() {
        @Override
        public void run() {
            int durationMillis = (int) (System.currentTimeMillis() - recordingStartEpochMillis);
            timerTextView.setText(StringUtils.formatTimeMilliSeconds(durationMillis));
            recordingTimerHandler.postDelayed(recordingTimerRunnable, RECORDING_TIMER_INTERVAL);
        }
    };

    private final ModelObserver<PlaybackControls> playbackControlsModelObserver = new ModelObserver<PlaybackControls>() {
        @Override
        public void updated(PlaybackControls model) {
            if (model.isPlaying()) {
                bottomButtonTextView.setText(R.string.glyph__pause);
            } else {
                bottomButtonTextView.setText(R.string.glyph__play);
            }
            recordingSeekBar.setMax((int) model.getDuration().toMillis());
            recordingSeekBar.setProgress((int) model.getPlayhead().toMillis());
        }
    };

    private final RecordingCallback recordingCallback = new RecordingCallback() {
        @Override
        public void onStart(Instant instant) {
            setRecordingStart(instant.toEpochMilli());
        }

        @Override
        public void onComplete(final AudioAssetForUpload audioAssetForUpload, boolean fileSizeLimitReached,
                AudioOverview overview) {
            if (fileSizeLimitReached) {
                ViewUtils.showAlertDialog(getContext(), R.string.audio_message__recording__limit_reached__title,
                        R.string.audio_message__recording__limit_reached__message,
                        R.string.audio_message__recording__limit_reached__confirmation,
                        R.string.confirmation_menu__cancel, new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialogInterface, int i) {
                                if (callback != null) {
                                    callback.onSendAudioMessage(audioAssetForUpload, null, true);
                                }
                            }
                        }, null);
                return;
            }

            if (sendImmediately) {
                if (callback != null) {
                    callback.onSendAudioMessage(audioAssetForUpload, null, true);
                }
            } else {
                AudioMessageRecordingView.this.audioAssetForUpload = audioAssetForUpload;
                playbackControls = audioAssetForUpload.getPlaybackControls();
                playbackControlsModelObserver.setAndUpdate(playbackControls);
            }
        }

        @Override
        public void onCancel() {
            playbackControlsModelObserver.clear();
            playbackControls = null;
        }
    };

    private final SeekBar.OnSeekBarChangeListener seekBarChangeListener = new SeekBar.OnSeekBarChangeListener() {
        @Override
        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
            if (!fromUser || playbackControls == null) {
                return;
            }
            playbackControls.setPlayhead(Duration.ofMillis(progress));
        }

        @Override
        public void onStartTrackingTouch(SeekBar seekBar) {

        }

        @Override
        public void onStopTrackingTouch(SeekBar seekBar) {

        }
    };

    private ObjectAnimator recordingIndicatorDotAnimator;

    public AudioMessageRecordingView(Context context) {
        this(context, null);
    }

    public AudioMessageRecordingView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public AudioMessageRecordingView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        LayoutInflater.from(getContext()).inflate(R.layout.voice_message_recording, this, true);

        slideControlContainer = ViewUtils.getView(this, R.id.fl__audio_message__recording__slide_control_container);
        closeButtonContainer = ViewUtils.getView(this, R.id.ll__audio_message_recording__close_button_container);

        hintTextView = ViewUtils.getView(this, R.id.ttv__audio_message_recording__hint);
        recordingIndicatorDotView = ViewUtils.getView(this, R.id.fl__audio_message__recording__indicator_dot);
        recordingIndicatorContainerView = ViewUtils.getView(this,
                R.id.fl__audio_message__recording__indicator_container);
        cancelButton = ViewUtils.getView(this, R.id.fl__audio_message__recording__cancel_button_container);
        bottomButton = ViewUtils.getView(this, R.id.fl__audio_message__recording__bottom_button_container);
        bottomButtonTextView = ViewUtils.getView(this, R.id.gtv__audido_message__recording__bottom_button);
        sendButton = ViewUtils.getView(this, R.id.fl__audio_message__recording__send_button_container);
        sendButtonTextView = ViewUtils.getView(this, R.id.gtv__audio_message__recording__send_button);
        slideControl = ViewUtils.getView(this, R.id.fl__audio_message__recording__slide_control);
        timerTextView = ViewUtils.getView(this, R.id.ttv__audio_message__recording__duration);
        recordingSeekBar = ViewUtils.getView(this, R.id.sb__voice_message__recording__seekbar);
        recordingSeekBar.setVisibility(GONE);
        recordingSeekBar.setOnSeekBarChangeListener(seekBarChangeListener);

        defaultSlideControlBackground = ContextCompat.getDrawable(getContext(),
                R.drawable.audio_message__slide_control__background);
        colorSlideControlBackground = ContextCompat.getDrawable(getContext(),
                R.drawable.audio_message__slide_control__background_accent__green);

        sendButtonTextView.setTextColor(ContextCompat.getColor(getContext(), R.color.accent_green));

        actionUpMinY = getResources()
                .getDimensionPixelOffset(R.dimen.audio_message_recording__slide_control__height)
                - 2 * getResources().getDimensionPixelOffset(R.dimen.audio_message_recording__slide_control__width)
                - getContext().getResources().getDimensionPixelSize(R.dimen.wire__padding__8);

        cancelButton.setOnClickListener(this);
        sendButton.setOnClickListener(this);
        bottomButton.setOnClickListener(this);

        recordingTimerHandler = new Handler();

        setSlideControlState(SlideControlState.RECORDING);
        sendImmediately = false;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int width = MeasureSpec.getSize(widthMeasureSpec);
        int cancelContainerWidth = CursorUtils.getDistanceOfAudioMessageIconToLeftScreenEdge(getContext(), width);
        ViewUtils.setWidth(closeButtonContainer, cancelContainerWidth);
        ViewUtils.setMarginLeft(cancelButton, CursorUtils.getMarginBetweenCursorButtons(getContext()));
    }

    @Override
    public void onClick(View view) {
        if (view.getId() == R.id.fl__audio_message__recording__cancel_button_container) {
            // CANCEL BUTTON
            if (slideControlState == SlideControlState.RECORDING || callback == null) {
                return;
            }
            if (playbackControls != null && playbackControls.isPlaying()) {
                playbackControls.stop();
            }
            callback.onCancelledAudioMessageRecording();
        } else if (view.getId() == R.id.fl__audio_message__recording__send_button_container) {
            // SEND BUTTON
            if (recordingControls == null || callback == null || playbackControls == null) {
                return;
            }
            if (playbackControls.isPlaying()) {
                playbackControls.stop();
            }
            callback.onSendAudioMessage(audioAssetForUpload, null, false);
        } else if (view.getId() == R.id.fl__audio_message__recording__bottom_button_container) {
            if (playbackControls == null) {
                return;
            }
            if (playbackControls.isPlaying()) {
                playbackControls.stop();
            } else {
                playbackControls.play();
                if (callback != null) {
                    callback.onPreviewedAudioMessage();
                }
            }
        }
    }

    public void setCallback(Callback callback) {
        this.callback = callback;
    }

    public void prepareForRecording() {

        recordingControls = AssetFactory.recordAudioAsset(recordingCallback);
        setSlideControlState(SlideControlState.RECORDING);
    }

    public void setRecordingStart(long recordingStartEpochMillis) {
        this.recordingStartEpochMillis = recordingStartEpochMillis;
        recordingTimerHandler.post(recordingTimerRunnable);
    }

    public void onMotionEventFromAudioMessageButton(MotionEvent motionEvent) {
        switch (motionEvent.getAction()) {
        case MotionEvent.ACTION_MOVE:
            if (slidedUpToSend(motionEvent)) {
                setSlideControlState(SlideControlState.SEND_FROM_RECORDING);
            } else {
                setSlideControlState(SlideControlState.RECORDING);
            }
            break;
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_OUTSIDE:
        case MotionEvent.ACTION_UP:
            if (recordingControls == null) {
                // recordingControls might be gone while user is long pressing e.g. when cancel is clicked at same time
                break;
            }
            if (slideControlState == SlideControlState.SEND_FROM_RECORDING) {
                // SLIDE UP TO INSTANT SEND
                sendImmediately = true;
            } else if (slideControlState == SlideControlState.RECORDING) {
                sendImmediately = false;
                setSlideControlState(SlideControlState.PREVIEW);
            }
            if (recordingControls != null) {
                recordingControls.stop();
            }
            break;
        }
    }

    public void reset() {
        playbackControlsModelObserver.clear();
        playbackControls = null;
        audioAssetForUpload = null;
        sendImmediately = false;
        if (recordingControls != null) {
            recordingControls.cancel();
        }
        recordingControls = null;
        timerTextView.setText("");
        stopRecordingTimer();
        stopRecordingIndicator();
    }

    public void setDarkTheme(boolean dark) {
        darkTheme = dark;
    }

    public void setAccentColor(int color) {
        Drawable drawable = recordingSeekBar.getProgressDrawable();
        if (drawable == null) {
            return;
        }
        if (drawable instanceof LayerDrawable) {
            LayerDrawable layerDrawable = (LayerDrawable) drawable;
            Drawable progress = layerDrawable.findDrawableByLayerId(android.R.id.progress);
            if (progress != null) {
                drawable = progress;
            }
        }
        drawable.setColorFilter(new LightingColorFilter(0xFF000000, color));
        drawable = recordingSeekBar.getThumb();
        drawable.setColorFilter(new LightingColorFilter(0xFF000000, color));
    }

    private boolean slidedUpToSend(MotionEvent motionEvent) {
        if (slideControlState != SlideControlState.RECORDING
                && slideControlState != SlideControlState.SEND_FROM_RECORDING) {
            return false;
        }

        if (motionEvent.getY() <= actionUpMinY) {
            return true;
        }

        return false;
    }

    private void setSlideControlState(SlideControlState state) {
        if (slideControlState == state) {
            return;
        }
        slideControlState = state;
        switch (state) {
        case RECORDING:
            slideControl.setBackground(defaultSlideControlBackground);
            hintTextView.setText(
                    getResources().getString(R.string.audio_message__recording__slide_control__slide_hint));
            sendButtonTextView.setTextColor(ContextCompat.getColor(getContext(), R.color.accent_green));
            if (darkTheme) {
                bottomButtonTextView.setTextColor(
                        ContextCompat.getColor(getContext(), R.color.wire__text_color_primary_dark_selector));
            } else {
                bottomButtonTextView.setTextColor(
                        ContextCompat.getColor(getContext(), R.color.wire__text_color_primary_light_selector));
            }
            bottomButtonTextView.setText(getContext().getResources().getText(R.string.glyph__microphone_on));
            recordingIndicatorContainerView.setVisibility(VISIBLE);
            recordingSeekBar.setVisibility(GONE);
            startRecordingIndicator();
            break;
        case SEND_FROM_RECORDING:
            slideControl.setBackground(colorSlideControlBackground);
            sendButtonTextView.setTextColor(
                    ContextCompat.getColor(getContext(), R.color.wire__text_color_primary_dark_selector));
            bottomButtonTextView.setTextColor(
                    ContextCompat.getColor(getContext(), R.color.wire__text_color_primary_dark_selector));
            break;
        case PREVIEW:
            if (darkTheme) {
                bottomButtonTextView.setTextColor(
                        ContextCompat.getColor(getContext(), R.color.wire__text_color_primary_dark_selector));
            } else {
                bottomButtonTextView.setTextColor(
                        ContextCompat.getColor(getContext(), R.color.wire__text_color_primary_light_selector));
            }
            slideControl.setBackground(defaultSlideControlBackground);
            sendButtonTextView.setTextColor(ContextCompat.getColor(getContext(), R.color.accent_green));
            bottomButtonTextView.setText(getContext().getResources().getText(R.string.glyph__play));
            recordingSeekBar.setVisibility(VISIBLE);
            hintTextView
                    .setText(getResources().getString(R.string.audio_message__recording__slide_control__tap_hint));
            recordingIndicatorContainerView.setVisibility(GONE);
            stopRecordingIndicator();
            stopRecordingTimer();
            break;
        }
    }

    private void startRecordingIndicator() {
        if (recordingIndicatorDotAnimator == null) {
            recordingIndicatorDotAnimator = ObjectAnimator.ofFloat(recordingIndicatorDotView, View.ALPHA, 0f);
            recordingIndicatorDotAnimator.setRepeatCount(ValueAnimator.INFINITE);
            recordingIndicatorDotAnimator.setRepeatMode(ValueAnimator.REVERSE);
            recordingIndicatorDotAnimator.setDuration(RECORDING_INDICATOR_HIDDEN_INTERVAL);
            recordingIndicatorDotAnimator.setStartDelay(RECORDING_INDICATOR_VISIBLE_INTERVAL);
        }
        recordingIndicatorDotAnimator.start();
    }

    private void stopRecordingIndicator() {
        recordingIndicatorDotAnimator.cancel();
    }

    private void stopRecordingTimer() {
        recordingTimerHandler.removeCallbacks(recordingTimerRunnable);
    }

    public interface Callback {
        void onSendAudioMessage(AudioAssetForUpload audioAssetForUpload, AudioEffect appliedAudioEffect,
                boolean sentWithQuickAction);

        void onCancelledAudioMessageRecording();

        void onPreviewedAudioMessage();
    }
}