/** * 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; } return motionEvent.getY() <= actionUpMinY; } 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(); if (callback != null) { callback.onStartedRecordingAudioMessage(); } 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(); void onStartedRecordingAudioMessage(); } }