Skip to content
This repository has been archived by the owner on Apr 12, 2022. It is now read-only.

Add internal recording of voice messages #3314

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ MatrixSdk:
- Changelog: https://github.com/matrix-org/matrix-android-sdk/releases/tag/v0.X.Y

Features:
-
- Add internal playback of audio files (#3207)
- Add internal recording of voice messages (#xxxx)

Improvements:
-
Expand Down
126 changes: 125 additions & 1 deletion vector/src/main/java/im/vector/activity/VectorRoomActivity.java
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,13 @@
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.Color;
import android.media.MediaRecorder;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Vibrator;
import android.text.InputType;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.TextWatcher;
Expand Down Expand Up @@ -92,6 +95,8 @@
import org.matrix.androidsdk.rest.model.User;
import org.matrix.androidsdk.rest.model.message.Message;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
Expand Down Expand Up @@ -139,6 +144,8 @@
import im.vector.widgets.WidgetManagerProvider;
import im.vector.widgets.WidgetsManager;

import static org.matrix.androidsdk.rest.model.message.Message.MSGTYPE_AUDIO;

/**
* Displays a single room with messages.
*/
Expand Down Expand Up @@ -207,6 +214,11 @@ public class VectorRoomActivity extends MXCActionBarActivity implements
private VectorMessageListFragment mVectorMessageListFragment;
private MXSession mSession;

// voice message recorder
private static final String VOICE_MESSAGE_FILE = "/voice_message.aac";
private MediaRecorder mRecorder;
private long mRecorderStartTime = 0L;

@Nullable
private Room mRoom;

Expand All @@ -227,6 +239,9 @@ public class VectorRoomActivity extends MXCActionBarActivity implements
@BindView(R.id.room_send_image_view)
ImageView mSendImageView;

@BindView(R.id.room_send_voice_view)
ImageView mSendVoiceView;

@BindView(R.id.editText_messageBox)
VectorAutoCompleteTextView mEditText;

Expand Down Expand Up @@ -1093,6 +1108,12 @@ public void onDestroy() {
protected void onPause() {
super.onPause();

if (mRecorder != null) {
mRecorder.stop();
mRecorder.release();
}
mRecorder = null;

if (mReadMarkerManager != null) {
mReadMarkerManager.onPause();
}
Expand Down Expand Up @@ -2427,6 +2448,12 @@ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permis
if (PermissionsToolsKt.onPermissionResultVideoIpCall(this, grantResults)) {
startIpCall(PreferencesManager.useJitsiConfCall(this), true);
}
} else if (requestCode == PermissionsToolsKt.PERMISSION_REQUEST_CODE_VOICE_MESSAGE) {
if (PermissionsToolsKt.allGranted(grantResults)) {
runOnUiThread(() -> startAudioRecording());
} else {
Toast.makeText(this, R.string.permissions_action_not_performed_missing_permissions, Toast.LENGTH_SHORT).show();
}
} else {
// Transmit to Fragment
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
Expand All @@ -2438,7 +2465,7 @@ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permis
*/
private void manageSendMoreButtons() {
int img = R.drawable.ic_material_file;
if (!PreferencesManager.sendMessageWithEnter(this) && mEditText.getText().length() > 0) {
if ((!PreferencesManager.sendMessageWithEnter(this) && mEditText.getText().length() > 0) || mRecorder != null) {
img = R.drawable.ic_material_send_green;
} else {
switch (PreferencesManager.getSelectedDefaultMediaSource(this)) {
Expand All @@ -2459,6 +2486,11 @@ private void manageSendMoreButtons() {
}
}
mSendImageView.setImageResource(img);
if (mEditText.getText().length() > 0) {
mSendVoiceView.setVisibility(View.GONE);
} else {
mSendVoiceView.setVisibility(View.VISIBLE);
}
}

/**
Expand Down Expand Up @@ -3889,6 +3921,25 @@ public void onClick(DialogInterface dialog, int which) {
void onSendClick() {
if (!TextUtils.isEmpty(mEditText.getText()) && !PreferencesManager.sendMessageWithEnter(this)) {
sendTextMessage();
} else if (mRecorder != null) {
stopAudioRecording();

File file = new File(getExternalCacheDir().getAbsolutePath() + VOICE_MESSAGE_FILE);
if (file.exists()) {
RoomMediaMessage mediaMessage = new RoomMediaMessage(Uri.fromFile(file), "Voice Message");
mediaMessage.setMessageType(MSGTYPE_AUDIO);
mVectorMessageListFragment.sendMediaMessage(mediaMessage);
// TODO file.delete() when sent
} else {
Toast.makeText(this.getApplicationContext(), getString(R.string.voice_recording_no_file), Toast.LENGTH_LONG);
}

// Restore UI
mEditText.setHint((mRoom.isEncrypted() && mSession.isCryptoEnabled()) ?
R.string.room_message_placeholder_encrypted : R.string.room_message_placeholder_not_encrypted);
mEditText.setInputType(InputType.TYPE_TEXT_FLAG_MULTI_LINE);
mSendVoiceView.setImageResource(R.drawable.vector_micro_green);
manageSendMoreButtons();
} else {
boolean useNativeCamera = PreferencesManager.useNativeCamera(this);
boolean isVoiceFeatureEnabled = PreferencesManager.isSendVoiceFeatureEnabled(this);
Expand Down Expand Up @@ -3941,6 +3992,79 @@ boolean onLongClick() {
return true;
}

@OnClick(R.id.room_send_voice_view)
void onSendVoiceClick() {
if (mRecorder == null) {
if (PermissionsToolsKt.checkPermissions(PermissionsToolsKt.PERMISSIONS_FOR_VOICE_MESSAGE,
VectorRoomActivity.this, PermissionsToolsKt.PERMISSION_REQUEST_CODE_VOICE_MESSAGE)) {
startAudioRecording();
}
} else { // Delete clicked
stopAndDeleteRecording();
}
}

private void startAudioRecording() {
stopAudioRecording();
mVectorMessageListFragment.pauseMediaPlayers();
try {
final String fileName = getExternalCacheDir().getAbsolutePath() + VOICE_MESSAGE_FILE;
mRecorder = new MediaRecorder();
mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mRecorder.setOutputFormat(MediaRecorder.OutputFormat.AAC_ADTS);
mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
mRecorder.setOutputFile(fileName);
mRecorder.prepare();
mRecorder.start();
mRecorderStartTime = System.currentTimeMillis();
refreshRecordingProgress();
mEditText.setHint(getString(R.string.room_message_placeholder_voice) + " 0:00");
mEditText.setInputType(InputType.TYPE_NULL);
mSendImageView.setImageResource(R.drawable.ic_material_send_green);
mSendVoiceView.setImageResource(R.drawable.ic_material_delete);
} catch (IOException e) {
Log.e(LOG_TAG, "AudioRecorder: " + e.getMessage());
}
}

public void stopAndDeleteRecording() {
stopAudioRecording();
File file = new File(getExternalCacheDir().getAbsolutePath() + VOICE_MESSAGE_FILE);
file.delete();
mEditText.setHint((mRoom.isEncrypted() && mSession.isCryptoEnabled()) ?
R.string.room_message_placeholder_encrypted : R.string.room_message_placeholder_not_encrypted);
mEditText.setInputType(InputType.TYPE_TEXT_FLAG_MULTI_LINE);
mSendVoiceView.setImageResource(R.drawable.vector_micro_green);
manageSendMoreButtons();
}

private void stopAudioRecording() {
if (mRecorder != null) {
mRecorder.stop();
mRecorder.release();
mRecorder = null;
}
}

private void refreshRecordingProgress() {
final Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
if (mRecorder == null || mEditText == null) return;

final long recordingTime = System.currentTimeMillis() - mRecorderStartTime;
int seconds = (int) (recordingTime / 1000);
int minutes = seconds / 60;
seconds %= 60;
final String durString = String.format("%d:%02d", minutes, seconds);
mEditText.setHint(getString(R.string.room_message_placeholder_voice) + " " + durString);

handler.postDelayed(this, 100);
}
}, 100);
}

private void onSendChoiceClicked(DialogListItem dialogListItem) {
if (dialogListItem instanceof DialogListItem.SendFile) {
launchFileSelectionIntent();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import android.graphics.Point;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.media.MediaPlayer;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
Expand Down Expand Up @@ -883,6 +884,7 @@ public void onBingRulesUpdate() {
*/
public void onPause() {
mEventFormattedTsMap.clear();
mMediasHelper.refreshPlayImageViews();
}

/**
Expand Down Expand Up @@ -952,9 +954,9 @@ public Event getCurrentSelectedEvent() {
*
* @param listener teh events listener
*/
public void setVectorMessagesAdapterActionsListener(IMessagesAdapterActionsListener listener) {
public void setVectorMessagesAdapterActionsListener(IMessagesAdapterActionsListener listener, Map<String, MediaPlayer> mediaPlayers) {
mVectorMessagesAdapterEventsListener = listener;
mMediasHelper.setVectorMessagesAdapterActionsListener(listener);
mMediasHelper.setVectorMessagesAdapterActionsListener(listener, mediaPlayers);
mHelper.setVectorMessagesAdapterActionsListener(listener);

if (null != mLinkMovementMethod) {
Expand Down Expand Up @@ -1590,6 +1592,7 @@ private View getFileView(final int position, View convertView, ViewGroup parent)

mMediasHelper.managePendingFileDownload(convertView, event, fileMessage, position);
mMediasHelper.managePendingUpload(convertView, event, ROW_TYPE_FILE, fileMessage.url);
mMediasHelper.managePlayback(convertView, event, ROW_TYPE_FILE, fileMessage.getUrl());

View fileLayout = convertView.findViewById(R.id.messagesAdapter_file_layout);
manageSubView(position, convertView, fileLayout, ROW_TYPE_FILE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import android.content.Context;
import android.graphics.Color;
import android.media.ExifInterface;
import android.media.MediaPlayer;
import android.os.Handler;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.view.View;
Expand Down Expand Up @@ -75,6 +77,10 @@ class VectorMessagesAdapterMediasHelper {
private final int mNotSentMessageTextColor;
private final int mDefaultMessageTextColor;

private final Handler handler = new Handler();
private Map<String, ImageView> mPlayImageViews = new HashMap<>();
private Map<String, MediaPlayer> mMediaPlayers;

VectorMessagesAdapterMediasHelper(Context context,
MXSession session,
int maxImageWidth,
Expand All @@ -96,8 +102,9 @@ class VectorMessagesAdapterMediasHelper {
*
* @param listener teh events listener
*/
void setVectorMessagesAdapterActionsListener(IMessagesAdapterActionsListener listener) {
void setVectorMessagesAdapterActionsListener(IMessagesAdapterActionsListener listener, Map<String, MediaPlayer> mediaPlayers) {
mVectorMessagesAdapterEventsListener = listener;
mMediaPlayers = mediaPlayers;
}

/**
Expand Down Expand Up @@ -180,6 +187,75 @@ public void onUploadComplete(final String uploadId, final String contentUri) {
refreshUploadViews(event, uploadStats, uploadProgressLayout);
}

void managePlayback(final View convertView, final Event event, final int type, final String mediaUrl) {
View playbackLayout = convertView.findViewById(R.id.content_media_playback_layout);
playbackLayout.setTag(mediaUrl);
final View playButton = playbackLayout.findViewById(R.id.media_playback);

if (null == playButton) return;

playButton.setTag(event);
playButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if ((event == playButton.getTag()) && (null != mVectorMessagesAdapterEventsListener)) {
ImageView imageView = playButton.findViewById(R.id.media_play_icon);
if ((null != imageView.getTag()) && (imageView.getTag().equals("playing"))) {
mVectorMessagesAdapterEventsListener.onEventAction(event, "", R.id.ic_action_pause_audio);
imageView.setImageResource(R.drawable.ic_baseline_play_arrow_24px);
imageView.setTag("paused");
} else {
mVectorMessagesAdapterEventsListener.onEventAction(event, "", R.id.ic_action_play_audio);
imageView.setTag("playing");
if (!mPlayImageViews.containsKey(mediaUrl)) {
mPlayImageViews.put(mediaUrl, imageView);
}
refreshPlayImageViews();
imageView.setImageResource(R.drawable.ic_baseline_pause_24px);
final ProgressBar progressBar = playbackLayout.findViewById(R.id.media_progress_view);
refreshPlaybackProgress(progressBar, mediaUrl);
}
}
}
});
}

void refreshPlayImageViews() {
for (ImageView iv : mPlayImageViews.values()) {
iv.setImageResource(R.drawable.ic_baseline_play_arrow_24px);
}
}

void refreshPlaybackProgress(ProgressBar progressBar, String mediaUrl) {
handler.postDelayed(new Runnable() {
@Override
public void run() {
if (null == mMediaPlayers) {
return;
}
try {
MediaPlayer mediaPlayer = mMediaPlayers.get(mediaUrl);

if (mediaPlayer == null || mediaUrl == null) {
mPlayImageViews.get(mediaUrl).setImageResource(R.drawable.ic_baseline_play_arrow_24px);
progressBar.setProgress(0);
return;
}


progressBar.setProgress((int) ((float) mediaPlayer.getCurrentPosition() / mediaPlayer.getDuration() * progressBar.getMax()));
if (mediaPlayer.isPlaying()) {
refreshPlaybackProgress(progressBar, mediaUrl);
} else {
mPlayImageViews.get(mediaUrl).setImageResource(R.drawable.ic_baseline_play_arrow_24px);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}, 1000);
}

// the image / video bitmaps are set to null if the matching URL is not the same
// to avoid flickering
private Map<String, String> mUrlByBitmapIndex = new HashMap<>();
Expand Down
Loading