From 5be7b1b7280c053ea8712c4c5bbe66a06d26d9f6 Mon Sep 17 00:00:00 2001 From: dkanada Date: Sat, 9 Jan 2021 23:23:17 +0900 Subject: [PATCH 001/434] store cache for WidgetWebView between loads --- .../im/vector/app/features/widgets/webview/WidgetWebView.kt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt b/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt index 446bc1663f..dc59f2694f 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt @@ -36,10 +36,6 @@ fun WebView.setupForWidget(webViewEventListener: WebViewEventListener) { // clear caches clearHistory() clearFormData() - clearCache(true) - - // does not cache the data - settings.cacheMode = WebSettings.LOAD_NO_CACHE // Enable Javascript settings.javaScriptEnabled = true @@ -77,8 +73,6 @@ fun WebView.clearAfterWidget() { webChromeClient = null webViewClient = null clearHistory() - // NOTE: clears RAM cache, if you pass true, it will also clear the disk cache. - clearCache(true) // Loading a blank page is optional, but will ensure that the WebView isn't doing anything when you destroy it. loadUrl("about:blank") removeAllViews() From b411e8f0093e3de47108d95c1b3227421ae40b1a Mon Sep 17 00:00:00 2001 From: dkanada Date: Tue, 12 Jan 2021 11:42:45 +0900 Subject: [PATCH 002/434] update changelog --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 1315d006e0..5e8ae6ab77 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ Features ✨: Improvements 🙌: - Add System theme option and set as default (#904, #2387) + - Use WebView cache for widgets to avoid excessive data use (#2648) - Warn user when they are leaving a not public room (#1460) Bugfix 🐛: From bc6eb565a3b14c94f4f93d13ccf3c346b67463ac Mon Sep 17 00:00:00 2001 From: dkanada Date: Tue, 12 Jan 2021 23:19:33 +0900 Subject: [PATCH 003/434] remove unused import --- .../java/im/vector/app/features/widgets/webview/WidgetWebView.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt b/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt index dc59f2694f..b1908d87de 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt @@ -21,7 +21,6 @@ import android.view.ViewGroup import android.webkit.CookieManager import android.webkit.PermissionRequest import android.webkit.WebChromeClient -import android.webkit.WebSettings import android.webkit.WebView import im.vector.app.R import im.vector.app.features.themes.ThemeUtils From 69350ef514400a28deb0bc9ceeaf2899ad312c62 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 17 Jun 2021 16:17:38 +0300 Subject: [PATCH 004/434] Voice message UI initial implementation. --- build.gradle | 3 + vector/build.gradle | 3 + .../home/room/detail/RoomDetailAction.kt | 4 + .../home/room/detail/RoomDetailFragment.kt | 13 +++ .../home/room/detail/RoomDetailViewModel.kt | 23 +++++ .../room/detail/composer/TextComposerView.kt | 39 +++++++- .../composer/VoiceMessageRecorderView.kt | 92 +++++++++++++++++++ .../composer/VoiceMessageRecordingHelper.kt | 87 ++++++++++++++++++ .../res/drawable/bg_voice_message_lock.xml | 14 +++ .../main/res/drawable/ic_voice_lock_arrow.xml | 13 +++ .../src/main/res/drawable/ic_voice_locked.xml | 5 + .../drawable/ic_voice_message_unlocked.xml | 5 + vector/src/main/res/drawable/ic_voice_mic.xml | 12 +++ .../res/drawable/ic_voice_mic_recording.xml | 18 ++++ .../ic_voice_slide_to_cancel_arrow.xml | 8 ++ .../src/main/res/layout/composer_layout.xml | 6 ++ ...composer_layout_constraint_set_compact.xml | 8 ++ .../layout/view_voice_message_recorder.xml | 69 ++++++++++++++ vector/src/main/res/values/colors.xml | 5 + vector/src/main/res/values/strings.xml | 4 + 20 files changed, 428 insertions(+), 3 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecordingHelper.kt create mode 100644 vector/src/main/res/drawable/bg_voice_message_lock.xml create mode 100644 vector/src/main/res/drawable/ic_voice_lock_arrow.xml create mode 100644 vector/src/main/res/drawable/ic_voice_locked.xml create mode 100644 vector/src/main/res/drawable/ic_voice_message_unlocked.xml create mode 100644 vector/src/main/res/drawable/ic_voice_mic.xml create mode 100644 vector/src/main/res/drawable/ic_voice_mic_recording.xml create mode 100644 vector/src/main/res/drawable/ic_voice_slide_to_cancel_arrow.xml create mode 100644 vector/src/main/res/layout/view_voice_message_recorder.xml diff --git a/build.gradle b/build.gradle index 881cd340f1..a7acc1c124 100644 --- a/build.gradle +++ b/build.gradle @@ -48,6 +48,9 @@ allprojects { // Chat effects includeGroupByRegex 'com\\.github\\.jetradarmobile' includeGroupByRegex 'nl\\.dionsegijn' + + // Voice RecordView + includeGroupByRegex 'com\\.github\\.3llomi' } } maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } diff --git a/vector/build.gradle b/vector/build.gradle index a94f796a90..991e483e98 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -144,6 +144,8 @@ android { buildConfigField "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy", "outboundSessionKeySharingStrategy", "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy.WhenTyping" + buildConfigField "Long", "VOICE_MESSAGE_DURATION_LIMIT_MS", "120000L" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" // Keep abiFilter for the universalApk @@ -391,6 +393,7 @@ dependencies { implementation "androidx.autofill:autofill:$autofill_version" implementation 'jp.wasabeef:glide-transformations:4.3.0' implementation 'com.github.vector-im:PFLockScreen-Android:1.0.0-beta12' + implementation 'com.github.3llomi:RecordView:3.0.1' // Custom Tab implementation 'androidx.browser:browser:1.3.0' diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index 72e614c18c..e63fc9e17b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -109,4 +109,8 @@ sealed class RoomDetailAction : VectorViewModelAction { // Failed messages object RemoveAllFailedMessages : RoomDetailAction() + + // Voice Message + object StartRecordingVoiceMessage : RoomDetailAction() + data class EndRecordingVoiceMessage(val recordTime: Long) : RoomDetailAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 9ed4feebc4..024a37067a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -100,6 +100,7 @@ import im.vector.app.core.ui.views.NotificationAreaView import im.vector.app.core.utils.Debouncer import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.KeyboardStateUtils +import im.vector.app.core.utils.PERMISSIONS_FOR_AUDIO_IP_CALL import im.vector.app.core.utils.PERMISSIONS_FOR_WRITING_FILES import im.vector.app.core.utils.checkPermissions import im.vector.app.core.utils.colorizeMatchingText @@ -1192,6 +1193,18 @@ class RoomDetailFragment @Inject constructor( override fun onTextEmptyStateChanged(isEmpty: Boolean) { // No op } + + override fun onVoiceRecordingStarted() { + roomDetailViewModel.handle(RoomDetailAction.StartRecordingVoiceMessage) + } + + override fun onVoiceRecordingEnded(recordTime: Long) { + roomDetailViewModel.handle(RoomDetailAction.EndRecordingVoiceMessage(recordTime)) + } + + override fun checkVoiceRecordingPermission(): Boolean { + return checkPermissions(PERMISSIONS_FOR_AUDIO_IP_CALL, requireActivity(), 0) + } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index a2041c0a80..55ee63091e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail import android.net.Uri import androidx.annotation.IdRes +import androidx.core.net.toUri import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.Async import com.airbnb.mvrx.Fail @@ -38,6 +39,7 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.mvrx.runCatchingToAsync import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider +import im.vector.app.features.attachments.toContentAttachmentData import im.vector.app.features.call.conference.JitsiService import im.vector.app.features.call.dialpad.DialPadLookup import im.vector.app.features.call.lookup.CallProtocolsChecker @@ -47,6 +49,7 @@ import im.vector.app.features.command.ParsedCommand import im.vector.app.features.createdirect.DirectRoomHelper import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider +import im.vector.app.features.home.room.detail.composer.VoiceMessageRecordingHelper import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder @@ -56,6 +59,7 @@ import im.vector.app.features.home.room.typing.TypingHelper import im.vector.app.features.powerlevel.PowerLevelsObservableFactory import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorPreferences +import im.vector.lib.multipicker.utils.toMultiPickerAudioType import io.reactivex.Observable import io.reactivex.rxkotlin.subscribeBy import io.reactivex.schedulers.Schedulers @@ -119,6 +123,7 @@ class RoomDetailViewModel @AssistedInject constructor( private val chatEffectManager: ChatEffectManager, private val directRoomHelper: DirectRoomHelper, private val jitsiService: JitsiService, + private val voiceMessageRecordingHelper: VoiceMessageRecordingHelper, timelineSettingsFactory: TimelineSettingsFactory ) : VectorViewModel(initialState), Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener { @@ -324,6 +329,8 @@ class RoomDetailViewModel @AssistedInject constructor( is RoomDetailAction.DoNotShowPreviewUrlFor -> handleDoNotShowPreviewUrlFor(action) RoomDetailAction.RemoveAllFailedMessages -> handleRemoveAllFailedMessages() RoomDetailAction.ResendAll -> handleResendAll() + RoomDetailAction.StartRecordingVoiceMessage -> handleStartRecordingVoiceMessage() + is RoomDetailAction.EndRecordingVoiceMessage -> handleEndRecordingVoiceMessage(action.recordTime) }.exhaustive } @@ -611,6 +618,22 @@ class RoomDetailViewModel @AssistedInject constructor( } } + private fun handleStartRecordingVoiceMessage() { + voiceMessageRecordingHelper.startRecording() + } + + private fun handleEndRecordingVoiceMessage(recordTime: Long) { + if (recordTime == 0L) { + voiceMessageRecordingHelper.deleteRecording() + return + } + voiceMessageRecordingHelper.stopRecording(recordTime)?.let { audioType -> + room.sendMedia(audioType.toContentAttachmentData(), false, emptySet()) + room + //voiceMessageRecordingHelper.deleteRecording() + } + } + private fun isIntegrationEnabled() = session.integrationManagerService().isIntegrationEnabled() fun isMenuItemVisible(@IdRes itemId: Int): Boolean = com.airbnb.mvrx.withState(this) { state -> diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt index d5e24dbb6b..6672027133 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/TextComposerView.kt @@ -24,6 +24,7 @@ import android.view.ViewGroup import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.core.text.toSpannable +import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.transition.ChangeBounds import androidx.transition.Fade @@ -32,6 +33,7 @@ import androidx.transition.TransitionManager import androidx.transition.TransitionSet import im.vector.app.R import im.vector.app.databinding.ComposerLayoutBinding +import org.matrix.android.sdk.api.extensions.orFalse /** * Encapsulate the timeline composer UX. @@ -46,6 +48,9 @@ class TextComposerView @JvmOverloads constructor( fun onCloseRelatedMessage() fun onSendMessage(text: CharSequence) fun onAddAttachment() + fun onVoiceRecordingStarted() + fun onVoiceRecordingEnded(recordTime: Long) + fun checkVoiceRecordingPermission(): Boolean } val views: ComposerLayoutBinding @@ -71,7 +76,9 @@ class TextComposerView @JvmOverloads constructor( } override fun onTextEmptyStateChanged(isEmpty: Boolean) { - views.sendButton.isVisible = currentConstraintSetId == R.layout.composer_layout_constraint_set_expanded || !isEmpty + val shouldShowSendButton = currentConstraintSetId == R.layout.composer_layout_constraint_set_expanded || !isEmpty + views.sendButton.isInvisible = !shouldShowSendButton + views.voiceMessageRecorderView.isVisible = !shouldShowSendButton } } views.composerRelatedMessageCloseButton.setOnClickListener { @@ -87,6 +94,28 @@ class TextComposerView @JvmOverloads constructor( views.attachmentButton.setOnClickListener { callback?.onAddAttachment() } + + views.voiceMessageRecorderView.callback = object : VoiceMessageRecorderView.Callback { + override fun onVoiceRecordingStarted() { + views.attachmentButton.isVisible = false + views.composerEditText.isVisible = false + views.composerEmojiButton.isVisible = false + views.composerEditTextOuterBorder.isVisible = false + callback?.onVoiceRecordingStarted() + } + + override fun onVoiceRecordingEnded(recordTime: Long) { + views.attachmentButton.isVisible = true + views.composerEditText.isVisible = true + views.composerEmojiButton.isVisible = true + views.composerEditTextOuterBorder.isVisible = true + callback?.onVoiceRecordingEnded(recordTime) + } + + override fun checkVoiceRecordingPermission(): Boolean { + return callback?.checkVoiceRecordingPermission().orFalse() + } + } } fun collapse(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) { @@ -96,7 +125,10 @@ class TextComposerView @JvmOverloads constructor( } currentConstraintSetId = R.layout.composer_layout_constraint_set_compact applyNewConstraintSet(animate, transitionComplete) - views.sendButton.isVisible = !views.composerEditText.text.isNullOrEmpty() + + val shouldShowSendButton = !views.composerEditText.text.isNullOrEmpty() + views.sendButton.isInvisible = !shouldShowSendButton + views.voiceMessageRecorderView.isVisible = !shouldShowSendButton } fun expand(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) { @@ -106,7 +138,8 @@ class TextComposerView @JvmOverloads constructor( } currentConstraintSetId = R.layout.composer_layout_constraint_set_expanded applyNewConstraintSet(animate, transitionComplete) - views.sendButton.isVisible = true + views.sendButton.isInvisible = false + views.voiceMessageRecorderView.isVisible = false } private fun applyNewConstraintSet(animate: Boolean, transitionComplete: (() -> Unit)?) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt new file mode 100644 index 0000000000..8ae32fcc69 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecorderView.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * 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 im.vector.app.features.home.room.detail.composer + +import android.content.Context +import android.util.AttributeSet +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import com.devlomi.record_view.OnRecordListener +import im.vector.app.BuildConfig +import im.vector.app.R +import im.vector.app.databinding.ViewVoiceMessageRecorderBinding +import org.matrix.android.sdk.api.extensions.orFalse + +/** + * Encapsulates the voice message recording view and animations. + */ +class VoiceMessageRecorderView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { + + interface Callback { + fun onVoiceRecordingStarted() + fun onVoiceRecordingEnded(recordTime: Long) + fun checkVoiceRecordingPermission(): Boolean + } + + private val views: ViewVoiceMessageRecorderBinding + + var callback: Callback? = null + + init { + inflate(context, R.layout.view_voice_message_recorder, this) + views = ViewVoiceMessageRecorderBinding.bind(this) + + views.voiceMessageButton.setRecordView(views.voiceMessageRecordView) + views.voiceMessageRecordView.timeLimit = BuildConfig.VOICE_MESSAGE_DURATION_LIMIT_MS + + views.voiceMessageRecordView.setRecordPermissionHandler { callback?.checkVoiceRecordingPermission().orFalse() } + + views.voiceMessageRecordView.setOnRecordListener(object : OnRecordListener { + override fun onStart() { + onVoiceRecordingStarted() + } + + override fun onCancel() { + onVoiceRecordingEnded(0) + } + + override fun onFinish(recordTime: Long, limitReached: Boolean) { + onVoiceRecordingEnded(recordTime) + } + + override fun onLessThanSecond() { + onVoiceRecordingEnded(0) + } + }) + } + + private fun onVoiceRecordingStarted() { + views.voiceMessageLockBackground.isVisible = true + views.voiceMessageLockArrow.isVisible = true + views.voiceMessageLockImage.isVisible = true + views.voiceMessageButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_voice_mic_recording)) + callback?.onVoiceRecordingStarted() + } + + private fun onVoiceRecordingEnded(recordTime: Long) { + views.voiceMessageLockBackground.isVisible = false + views.voiceMessageLockArrow.isVisible = false + views.voiceMessageLockImage.isVisible = false + views.voiceMessageButton.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_voice_mic)) + callback?.onVoiceRecordingEnded(recordTime) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecordingHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecordingHelper.kt new file mode 100644 index 0000000000..63cbbe6e79 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/VoiceMessageRecordingHelper.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * 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 im.vector.app.features.home.room.detail.composer + +import android.content.Context +import android.media.MediaRecorder +import androidx.core.content.FileProvider +import androidx.core.net.toUri +import im.vector.app.BuildConfig +import im.vector.lib.multipicker.entity.MultiPickerAudioType +import im.vector.lib.multipicker.utils.toMultiPickerAudioType +import timber.log.Timber +import java.io.File +import java.io.FileOutputStream +import java.lang.RuntimeException +import java.util.UUID +import javax.inject.Inject + +/** + * Helper class to record audio for voice messages. + */ +class VoiceMessageRecordingHelper @Inject constructor( + private val context: Context +) { + + private lateinit var mediaRecorder: MediaRecorder + private val outputDirectory = File(context.cacheDir, "downloads") + private var outputFile: File? = null + + init { + if (!outputDirectory.exists()) { + outputDirectory.mkdirs() + } + } + + private fun refreshMediaRecorder() { + mediaRecorder = MediaRecorder() + mediaRecorder.setAudioSource(MediaRecorder.AudioSource.DEFAULT) + mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.OGG) + mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.OPUS) + mediaRecorder.setAudioEncodingBitRate(24000) + mediaRecorder.setAudioSamplingRate(48000) + } + + fun startRecording() { + outputFile = File(outputDirectory, UUID.randomUUID().toString() + ".ogg") + FileOutputStream(outputFile).use { fos -> + refreshMediaRecorder() + mediaRecorder.setOutputFile(fos.fd) + mediaRecorder.prepare() + mediaRecorder.start() + } + } + + fun stopRecording(recordTime: Long): MultiPickerAudioType? { + try { + mediaRecorder.stop() + mediaRecorder.reset() + mediaRecorder.release() + outputFile?.let { + val outputFileUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileProvider", it) + return outputFileUri?.toMultiPickerAudioType(context) + } ?: return null + } catch (e: RuntimeException) { // Usually thrown when the record is less than 1 second. + Timber.e(e, "Voice message is not valid. Record time: %s", recordTime) + return null + } + } + + fun deleteRecording() { + outputFile?.delete() + } +} diff --git a/vector/src/main/res/drawable/bg_voice_message_lock.xml b/vector/src/main/res/drawable/bg_voice_message_lock.xml new file mode 100644 index 0000000000..672d7bf80f --- /dev/null +++ b/vector/src/main/res/drawable/bg_voice_message_lock.xml @@ -0,0 +1,14 @@ + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/drawable/ic_voice_lock_arrow.xml b/vector/src/main/res/drawable/ic_voice_lock_arrow.xml new file mode 100644 index 0000000000..7f9f2403ad --- /dev/null +++ b/vector/src/main/res/drawable/ic_voice_lock_arrow.xml @@ -0,0 +1,13 @@ + + + diff --git a/vector/src/main/res/drawable/ic_voice_locked.xml b/vector/src/main/res/drawable/ic_voice_locked.xml new file mode 100644 index 0000000000..2b92d9d5e0 --- /dev/null +++ b/vector/src/main/res/drawable/ic_voice_locked.xml @@ -0,0 +1,5 @@ + + + diff --git a/vector/src/main/res/drawable/ic_voice_message_unlocked.xml b/vector/src/main/res/drawable/ic_voice_message_unlocked.xml new file mode 100644 index 0000000000..007de349ec --- /dev/null +++ b/vector/src/main/res/drawable/ic_voice_message_unlocked.xml @@ -0,0 +1,5 @@ + + + diff --git a/vector/src/main/res/drawable/ic_voice_mic.xml b/vector/src/main/res/drawable/ic_voice_mic.xml new file mode 100644 index 0000000000..7cb091afaa --- /dev/null +++ b/vector/src/main/res/drawable/ic_voice_mic.xml @@ -0,0 +1,12 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_voice_mic_recording.xml b/vector/src/main/res/drawable/ic_voice_mic_recording.xml new file mode 100644 index 0000000000..eb6cea39a5 --- /dev/null +++ b/vector/src/main/res/drawable/ic_voice_mic_recording.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/vector/src/main/res/drawable/ic_voice_slide_to_cancel_arrow.xml b/vector/src/main/res/drawable/ic_voice_slide_to_cancel_arrow.xml new file mode 100644 index 0000000000..1299e82530 --- /dev/null +++ b/vector/src/main/res/drawable/ic_voice_slide_to_cancel_arrow.xml @@ -0,0 +1,8 @@ + + + diff --git a/vector/src/main/res/layout/composer_layout.xml b/vector/src/main/res/layout/composer_layout.xml index 3816c206b4..7c9c23645d 100644 --- a/vector/src/main/res/layout/composer_layout.xml +++ b/vector/src/main/res/layout/composer_layout.xml @@ -131,4 +131,10 @@ android:src="@drawable/ic_send" tools:ignore="MissingConstraints" /> + + diff --git a/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml b/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml index 079bc9705a..b51f69302a 100644 --- a/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml +++ b/vector/src/main/res/layout/composer_layout_constraint_set_compact.xml @@ -178,4 +178,12 @@ tools:ignore="MissingPrefix" tools:visibility="visible" /> + + \ No newline at end of file diff --git a/vector/src/main/res/layout/view_voice_message_recorder.xml b/vector/src/main/res/layout/view_voice_message_recorder.xml new file mode 100644 index 0000000000..3b124ae7ef --- /dev/null +++ b/vector/src/main/res/layout/view_voice_message_recorder.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/colors.xml b/vector/src/main/res/values/colors.xml index 7158554833..f66476a795 100644 --- a/vector/src/main/res/values/colors.xml +++ b/vector/src/main/res/values/colors.xml @@ -132,4 +132,9 @@ @color/black_alpha @android:color/transparent + + #FFF3F8FD + #22252B + #22252B + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 80120b51bf..b2519f60b2 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3399,4 +3399,8 @@ Some rooms may be hidden because they’re private and you need an invite. Unnamed Room + + Start Voice Message + Slide to cancel + Voice Message Lock From cb96886568e1969fdbe43f069a08ad792951ef32 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 17 Jun 2021 16:18:20 +0300 Subject: [PATCH 005/434] Send voice message. --- .../java/org/matrix/android/sdk/rx/RxRoom.kt | 5 ++++ .../room/model/message/MessageAudioContent.kt | 13 +++++++- .../crypto/model/rest/AudioWaveformInfo.kt | 30 +++++++++++++++++++ .../room/send/LocalEchoEventFactory.kt | 9 +++++- .../multipicker/utils/ContentResolverUtil.kt | 2 +- 5 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/AudioWaveformInfo.kt diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt index 21db4e1893..b3495c4493 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt @@ -23,6 +23,7 @@ import io.reactivex.Single import kotlinx.coroutines.rx2.rxCompletable import kotlinx.coroutines.rx2.rxSingle import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.room.Room @@ -146,6 +147,10 @@ class RxRoom(private val room: Room) { fun deleteAvatar(): Completable = rxCompletable { room.deleteAvatar() } + + fun sendMedia(attachment: ContentAttachmentData, compressBeforeSending: Boolean, roomIds: Set): Completable = rxCompletable { + room.sendMedia(attachment, compressBeforeSending, roomIds) + } } fun Room.rx(): RxRoom { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioContent.kt index fcf3d73cbe..3ce4cd6932 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioContent.kt @@ -20,6 +20,7 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.internal.crypto.model.rest.AudioWaveformInfo import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo @JsonClass(generateAdapter = true) @@ -50,7 +51,17 @@ data class MessageAudioContent( /** * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption. */ - @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null + @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null, + + /** + * Encapsulates waveform and duration of the audio. + */ + @Json(name = "org.matrix.msc1767.audio") val audioWaveformInfo: AudioWaveformInfo? = null, + + /** + * Indicates that is a voice message. + */ + @Json(name = "org.matrix.msc2516.voice") val voiceMessageIndicator: Any? = null ) : MessageWithAttachmentContent { override val mimeType: String? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/AudioWaveformInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/AudioWaveformInfo.kt new file mode 100644 index 0000000000..31a356e164 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/AudioWaveformInfo.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * 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 org.matrix.android.sdk.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class AudioWaveformInfo( + @Json(name = "duration") + val duration: Long? = null, + + @Json(name = "waveform") + val waveform: List? = null + +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index f505b13b33..03afa4beb6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -53,6 +53,7 @@ import org.matrix.android.sdk.api.session.room.model.relation.ReplyToContent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent import org.matrix.android.sdk.api.session.room.timeline.isReply +import org.matrix.android.sdk.internal.crypto.model.rest.AudioWaveformInfo import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.content.ThumbnailExtractor import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory @@ -289,6 +290,7 @@ internal class LocalEchoEventFactory @Inject constructor( } private fun createAudioEvent(roomId: String, attachment: ContentAttachmentData): Event { + val isVoiceMessage = attachment.mimeType == "audio/ogg" val content = MessageAudioContent( msgType = MessageType.MSGTYPE_AUDIO, body = attachment.name ?: "audio", @@ -296,7 +298,12 @@ internal class LocalEchoEventFactory @Inject constructor( mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() }, size = attachment.size ), - url = attachment.queryUri.toString() + url = attachment.queryUri.toString(), + audioWaveformInfo = if (!isVoiceMessage) null else AudioWaveformInfo( + duration = attachment.duration, + waveform = null // TODO. + ), + voiceMessageIndicator = if (!isVoiceMessage) null else Any() ) return createMessageEvent(roomId, content) } diff --git a/multipicker/src/main/java/im/vector/lib/multipicker/utils/ContentResolverUtil.kt b/multipicker/src/main/java/im/vector/lib/multipicker/utils/ContentResolverUtil.kt index a1982b0bbc..6cadfa6ce7 100644 --- a/multipicker/src/main/java/im/vector/lib/multipicker/utils/ContentResolverUtil.kt +++ b/multipicker/src/main/java/im/vector/lib/multipicker/utils/ContentResolverUtil.kt @@ -111,7 +111,7 @@ internal fun Uri.toMultiPickerVideoType(context: Context): MultiPickerVideoType? } } -internal fun Uri.toMultiPickerAudioType(context: Context): MultiPickerAudioType? { +fun Uri.toMultiPickerAudioType(context: Context): MultiPickerAudioType? { val projection = arrayOf( MediaStore.Audio.Media.DISPLAY_NAME, MediaStore.Audio.Media.SIZE From e888f2b15a42c2563450cd102740153b0fd67acf Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 29 Jun 2021 11:09:18 +0200 Subject: [PATCH 006/434] Call controls: move choose sound device out of overflow menu --- .../features/call/CallControlsBottomSheet.kt | 66 ----------------- .../app/features/call/CallControlsView.kt | 12 +-- .../call/CallSoundDeviceChooserBottomSheet.kt | 74 +++++++++++++++++++ .../app/features/call/VectorCallActivity.kt | 9 ++- .../features/call/audio/CallAudioManager.kt | 13 ++-- .../res/drawable/ic_call_audio_settings.xml | 15 ++++ .../res/drawable/ic_call_speaker_active.xml | 11 --- .../res/drawable/ic_call_speaker_default.xml | 7 -- .../drawable/ic_sound_device_headphone.xml | 9 +++ .../res/drawable/ic_sound_device_phone.xml | 9 +++ .../res/drawable/ic_sound_device_speaker.xml | 15 ++++ .../res/layout/bottom_sheet_call_controls.xml | 10 --- ...bottom_sheet_call_sound_device_chooser.xml | 60 +++++++++++++++ .../res/layout/bottom_sheet_generic_list.xml | 2 +- .../main/res/layout/view_call_controls.xml | 49 ++++++------ 15 files changed, 233 insertions(+), 128 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/call/CallSoundDeviceChooserBottomSheet.kt create mode 100644 vector/src/main/res/drawable/ic_call_audio_settings.xml delete mode 100644 vector/src/main/res/drawable/ic_call_speaker_active.xml delete mode 100644 vector/src/main/res/drawable/ic_call_speaker_default.xml create mode 100644 vector/src/main/res/drawable/ic_sound_device_headphone.xml create mode 100644 vector/src/main/res/drawable/ic_sound_device_phone.xml create mode 100644 vector/src/main/res/drawable/ic_sound_device_speaker.xml create mode 100644 vector/src/main/res/layout/bottom_sheet_call_sound_device_chooser.xml diff --git a/vector/src/main/java/im/vector/app/features/call/CallControlsBottomSheet.kt b/vector/src/main/java/im/vector/app/features/call/CallControlsBottomSheet.kt index f23b26883a..a3e3a5d71d 100644 --- a/vector/src/main/java/im/vector/app/features/call/CallControlsBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/call/CallControlsBottomSheet.kt @@ -45,10 +45,6 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment { - showSoundDeviceChooser(it.available, it.current) - } - else -> { - } - } - } - } - - private fun showSoundDeviceChooser(available: Set, current: CallAudioManager.Device) { - val soundDevices = available.map { - when (it) { - CallAudioManager.Device.WIRELESS_HEADSET -> span { - text = getString(R.string.sound_device_wireless_headset) - textStyle = if (current == it) "bold" else "normal" - } - CallAudioManager.Device.PHONE -> span { - text = getString(R.string.sound_device_phone) - textStyle = if (current == it) "bold" else "normal" - } - CallAudioManager.Device.SPEAKER -> span { - text = getString(R.string.sound_device_speaker) - textStyle = if (current == it) "bold" else "normal" - } - CallAudioManager.Device.HEADSET -> span { - text = getString(R.string.sound_device_headset) - textStyle = if (current == it) "bold" else "normal" - } - } - } - MaterialAlertDialogBuilder(requireContext()) - .setItems(soundDevices.toTypedArray()) { d, n -> - d.cancel() - when (soundDevices[n].toString()) { - // TODO Make an adapter and handle multiple Bluetooth headsets. Also do not use translations. - getString(R.string.sound_device_phone) -> { - callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.Device.PHONE)) - } - getString(R.string.sound_device_speaker) -> { - callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.Device.SPEAKER)) - } - getString(R.string.sound_device_headset) -> { - callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.Device.HEADSET)) - } - getString(R.string.sound_device_wireless_headset) -> { - callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.Device.WIRELESS_HEADSET)) - } - } - } - .setNegativeButton(R.string.cancel, null) - .show() } private fun renderState(state: VectorCallViewState) { - views.callControlsSoundDevice.title = getString(R.string.call_select_sound_device) - views.callControlsSoundDevice.subTitle = when (state.device) { - CallAudioManager.Device.PHONE -> getString(R.string.sound_device_phone) - CallAudioManager.Device.SPEAKER -> getString(R.string.sound_device_speaker) - CallAudioManager.Device.HEADSET -> getString(R.string.sound_device_headset) - CallAudioManager.Device.WIRELESS_HEADSET -> getString(R.string.sound_device_wireless_headset) - } - views.callControlsSwitchCamera.isVisible = state.isVideoCall && state.canSwitchCamera views.callControlsSwitchCamera.subTitle = getString(if (state.isFrontCamera) R.string.call_camera_front else R.string.call_camera_back) diff --git a/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt b/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt index 1a54551072..2f771b3a8d 100644 --- a/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt +++ b/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt @@ -36,16 +36,19 @@ class CallControlsView @JvmOverloads constructor( init { inflate(context, R.layout.view_call_controls, this) views = ViewCallControlsBinding.bind(this) - + views.audioSettingsIcon.setOnClickListener { didTapAudioSettings() } views.ringingControlAccept.setOnClickListener { acceptIncomingCall() } views.ringingControlDecline.setOnClickListener { declineIncomingCall() } views.endCallIcon.setOnClickListener { endOngoingCall() } views.muteIcon.setOnClickListener { toggleMute() } views.videoToggleIcon.setOnClickListener { toggleVideo() } - views.openChatIcon.setOnClickListener { returnToChat() } views.moreIcon.setOnClickListener { moreControlOption() } } + private fun didTapAudioSettings() { + interactionListener?.didTapAudioSettings() + } + private fun acceptIncomingCall() { interactionListener?.didAcceptIncomingCall() } @@ -66,9 +69,6 @@ class CallControlsView @JvmOverloads constructor( interactionListener?.didTapToggleVideo() } - private fun returnToChat() { - interactionListener?.returnToChat() - } private fun moreControlOption() { interactionListener?.didTapMore() @@ -127,12 +127,12 @@ class CallControlsView @JvmOverloads constructor( } interface InteractionListener { + fun didTapAudioSettings() fun didAcceptIncomingCall() fun didDeclineIncomingCall() fun didEndCall() fun didTapToggleMute() fun didTapToggleVideo() - fun returnToChat() fun didTapMore() } } diff --git a/vector/src/main/java/im/vector/app/features/call/CallSoundDeviceChooserBottomSheet.kt b/vector/src/main/java/im/vector/app/features/call/CallSoundDeviceChooserBottomSheet.kt new file mode 100644 index 0000000000..a011952549 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/CallSoundDeviceChooserBottomSheet.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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 im.vector.app.features.call + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.airbnb.mvrx.activityViewModel +import im.vector.app.R +import im.vector.app.core.epoxy.bottomsheet.bottomSheetActionItem +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.app.databinding.BottomSheetGenericListBinding +import im.vector.app.features.call.audio.CallAudioManager +import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet + +class CallSoundDeviceChooserBottomSheet : VectorBaseBottomSheetDialogFragment() { + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetGenericListBinding { + return BottomSheetGenericListBinding.inflate(inflater, container, false) + } + + private val callViewModel: VectorCallViewModel by activityViewModel() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + callViewModel.observeViewEvents { + when (it) { + is VectorCallViewEvents.ShowSoundDeviceChooser -> { + render(it.available, it.current) + } + else -> { + } + } + } + callViewModel.handle(VectorCallViewActions.SwitchSoundDevice) + } + + private fun render(available: Set, current: CallAudioManager.Device) { + views.bottomSheetRecyclerView.withModels { + available.forEach { device -> + bottomSheetActionItem { + id(device.ordinal) + textRes(device.titleRes) + iconRes(device.drawableRes) + selected(current == device) + listener { + callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(device)) + dismiss() + } + } + } + } + } + + companion object { + fun newInstance(): RoomListQuickActionsBottomSheet { + return RoomListQuickActionsBottomSheet() + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt index 21939bd42b..b933e1e69d 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt @@ -26,6 +26,7 @@ import android.os.Bundle import android.os.Parcelable import android.view.View import android.view.WindowManager +import androidx.core.content.ContentProviderCompat.requireContext import androidx.core.content.ContextCompat import androidx.core.content.getSystemService import androidx.core.view.isInvisible @@ -43,6 +44,7 @@ import im.vector.app.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL import im.vector.app.core.utils.allGranted import im.vector.app.core.utils.checkPermissions import im.vector.app.databinding.ActivityCallBinding +import im.vector.app.features.call.audio.CallAudioManager import im.vector.app.features.call.dialpad.CallDialPadBottomSheet import im.vector.app.features.call.dialpad.DialPadFragment import im.vector.app.features.call.utils.EglUtils @@ -53,6 +55,7 @@ import im.vector.app.features.home.room.detail.RoomDetailActivity import im.vector.app.features.home.room.detail.RoomDetailArgs import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.parcelize.Parcelize +import me.gujun.android.span.span import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.MxPeerConnectionState @@ -404,6 +407,10 @@ class VectorCallActivity : VectorBaseActivity(), CallContro } } + override fun didTapAudioSettings() { + CallSoundDeviceChooserBottomSheet().show(supportFragmentManager, "SoundDeviceChooser") + } + override fun didAcceptIncomingCall() { callViewModel.handle(VectorCallViewActions.AcceptCall) } @@ -424,7 +431,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro callViewModel.handle(VectorCallViewActions.ToggleVideo) } - override fun returnToChat() { + private fun returnToChat() { val args = RoomDetailArgs(callArgs.signalingRoomId) val intent = RoomDetailActivity.newIntent(this, args).apply { flags = FLAG_ACTIVITY_CLEAR_TOP diff --git a/vector/src/main/java/im/vector/app/features/call/audio/CallAudioManager.kt b/vector/src/main/java/im/vector/app/features/call/audio/CallAudioManager.kt index 36a11b5923..7797cec929 100644 --- a/vector/src/main/java/im/vector/app/features/call/audio/CallAudioManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/audio/CallAudioManager.kt @@ -19,7 +19,10 @@ package im.vector.app.features.call.audio import android.content.Context import android.media.AudioManager import android.os.Build +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes import androidx.core.content.getSystemService +import im.vector.app.R import org.matrix.android.sdk.api.extensions.orFalse import timber.log.Timber import java.util.HashSet @@ -31,11 +34,11 @@ class CallAudioManager(private val context: Context, val configChange: (() -> Un private var audioDeviceDetector: AudioDeviceDetector? = null private var audioDeviceRouter: AudioDeviceRouter? = null - enum class Device { - PHONE, - SPEAKER, - HEADSET, - WIRELESS_HEADSET + enum class Device(@StringRes val titleRes: Int, @DrawableRes val drawableRes: Int) { + PHONE(R.string.sound_device_phone,R.drawable.ic_sound_device_phone), + SPEAKER(R.string.sound_device_speaker,R.drawable.ic_sound_device_speaker), + HEADSET(R.string.sound_device_headset,R.drawable.ic_sound_device_headphone), + WIRELESS_HEADSET(R.string.sound_device_wireless_headset,R.drawable.ic_sound_device_headphone) } enum class Mode { diff --git a/vector/src/main/res/drawable/ic_call_audio_settings.xml b/vector/src/main/res/drawable/ic_call_audio_settings.xml new file mode 100644 index 0000000000..8a47d27d9a --- /dev/null +++ b/vector/src/main/res/drawable/ic_call_audio_settings.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/vector/src/main/res/drawable/ic_call_speaker_active.xml b/vector/src/main/res/drawable/ic_call_speaker_active.xml deleted file mode 100644 index 97035b1915..0000000000 --- a/vector/src/main/res/drawable/ic_call_speaker_active.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - diff --git a/vector/src/main/res/drawable/ic_call_speaker_default.xml b/vector/src/main/res/drawable/ic_call_speaker_default.xml deleted file mode 100644 index 2fc06a5795..0000000000 --- a/vector/src/main/res/drawable/ic_call_speaker_default.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/vector/src/main/res/drawable/ic_sound_device_headphone.xml b/vector/src/main/res/drawable/ic_sound_device_headphone.xml new file mode 100644 index 0000000000..4648465356 --- /dev/null +++ b/vector/src/main/res/drawable/ic_sound_device_headphone.xml @@ -0,0 +1,9 @@ + + + diff --git a/vector/src/main/res/drawable/ic_sound_device_phone.xml b/vector/src/main/res/drawable/ic_sound_device_phone.xml new file mode 100644 index 0000000000..fd825c77f0 --- /dev/null +++ b/vector/src/main/res/drawable/ic_sound_device_phone.xml @@ -0,0 +1,9 @@ + + + diff --git a/vector/src/main/res/drawable/ic_sound_device_speaker.xml b/vector/src/main/res/drawable/ic_sound_device_speaker.xml new file mode 100644 index 0000000000..eb786d3fe1 --- /dev/null +++ b/vector/src/main/res/drawable/ic_sound_device_speaker.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/vector/src/main/res/layout/bottom_sheet_call_controls.xml b/vector/src/main/res/layout/bottom_sheet_call_controls.xml index bb4a91ea72..e751ac412a 100644 --- a/vector/src/main/res/layout/bottom_sheet_call_controls.xml +++ b/vector/src/main/res/layout/bottom_sheet_call_controls.xml @@ -7,16 +7,6 @@ android:background="?colorSurface" android:orientation="vertical"> - - + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/bottom_sheet_generic_list.xml b/vector/src/main/res/layout/bottom_sheet_generic_list.xml index 87a2cb54fc..144e291bb9 100644 --- a/vector/src/main/res/layout/bottom_sheet_generic_list.xml +++ b/vector/src/main/res/layout/bottom_sheet_generic_list.xml @@ -1,5 +1,5 @@ - + + + - - + /> From 7c033b4090228b624eb7dbea357949b807d7f1ac Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 30 Jun 2021 18:08:44 +0200 Subject: [PATCH 007/434] Call UI: add toolbar --- .../app/features/call/VectorCallActivity.kt | 50 +++++++++-------- .../res/drawable/ic_call_back_to_chat.xml | 10 ++++ vector/src/main/res/layout/activity_call.xml | 56 +++++++++---------- .../main/res/layout/view_call_controls.xml | 18 +++--- vector/src/main/res/menu/vector_call.xml | 11 ++++ vector/src/main/res/values/strings.xml | 2 + 6 files changed, 83 insertions(+), 64 deletions(-) create mode 100644 vector/src/main/res/drawable/ic_call_back_to_chat.xml create mode 100644 vector/src/main/res/menu/vector_call.xml diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt index b933e1e69d..602a68ea74 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt @@ -24,9 +24,9 @@ import android.graphics.Color import android.os.Build import android.os.Bundle import android.os.Parcelable +import android.view.MenuItem import android.view.View import android.view.WindowManager -import androidx.core.content.ContentProviderCompat.requireContext import androidx.core.content.ContextCompat import androidx.core.content.getSystemService import androidx.core.view.isInvisible @@ -38,13 +38,13 @@ import com.airbnb.mvrx.withState import com.google.android.material.dialog.MaterialAlertDialogBuilder import im.vector.app.R import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.utils.PERMISSIONS_FOR_AUDIO_IP_CALL import im.vector.app.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL import im.vector.app.core.utils.allGranted import im.vector.app.core.utils.checkPermissions import im.vector.app.databinding.ActivityCallBinding -import im.vector.app.features.call.audio.CallAudioManager import im.vector.app.features.call.dialpad.CallDialPadBottomSheet import im.vector.app.features.call.dialpad.DialPadFragment import im.vector.app.features.call.utils.EglUtils @@ -55,7 +55,6 @@ import im.vector.app.features.home.room.detail.RoomDetailActivity import im.vector.app.features.home.room.detail.RoomDetailArgs import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.parcelize.Parcelize -import me.gujun.android.span.span import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.MxPeerConnectionState @@ -127,6 +126,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro if (savedInstanceState != null) { (supportFragmentManager.findFragmentByTag(FRAGMENT_DIAL_PAD_TAG) as? CallDialPadBottomSheet)?.callback = dialPadCallback } + setSupportActionBar(views.callToolbar) configureCallViews() callViewModel.subscribe(this) { @@ -152,6 +152,16 @@ class VectorCallActivity : VectorBaseActivity(), CallContro } } + override fun getMenuRes() = R.menu.vector_call + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == R.id.menu_call_open_chat) { + returnToChat() + return true + } + return super.onOptionsItemSelected(item) + } + override fun onDestroy() { callManager.getCallById(callArgs.callId)?.detachRenderers(listOf(views.pipRenderer, views.fullscreenRenderer)) if (surfaceRenderersAreInitialized) { @@ -171,35 +181,27 @@ class VectorCallActivity : VectorBaseActivity(), CallContro views.callControlsView.updateForState(state) val callState = state.callState.invoke() - views.callConnectingProgress.isVisible = false views.callActionText.setOnClickListener(null) views.callActionText.isVisible = false views.smallIsHeldIcon.isVisible = false when (callState) { is CallState.Idle, is CallState.CreateOffer, + is CallState.LocalRinging, is CallState.Dialing -> { views.callVideoGroup.isInvisible = true views.callInfoGroup.isVisible = true - views.callStatusText.setText(R.string.call_ring) + views.callToolbar.setSubtitle(R.string.call_ring) configureCallInfo(state) } - - is CallState.LocalRinging -> { - views.callVideoGroup.isInvisible = true - views.callInfoGroup.isVisible = true - views.callStatusText.text = null - configureCallInfo(state) - } - is CallState.Answering -> { views.callVideoGroup.isInvisible = true views.callInfoGroup.isVisible = true - views.callStatusText.setText(R.string.call_connecting) - views.callConnectingProgress.isVisible = true + views.callToolbar.setSubtitle(R.string.call_connecting) configureCallInfo(state) } is CallState.Connected -> { + views.callToolbar.subtitle = state.formattedDuration if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { if (state.isLocalOnHold || state.isRemoteOnHold) { views.smallIsHeldIcon.isVisible = true @@ -210,11 +212,11 @@ class VectorCallActivity : VectorBaseActivity(), CallContro views.callActionText.setText(R.string.call_resume_action) views.callActionText.isVisible = true views.callActionText.setOnClickListener { callViewModel.handle(VectorCallViewActions.ToggleHoldResume) } - views.callStatusText.setText(R.string.call_held_by_you) + views.callToolbar.setSubtitle(R.string.call_held_by_you) } else { views.callActionText.isInvisible = true state.callInfo?.opponentUserItem?.let { - views.callStatusText.text = getString(R.string.call_held_by_user, it.getBestName()) + views.callToolbar.subtitle = getString(R.string.call_held_by_user, it.getBestName()) } } } else if (state.transferee !is VectorCallViewState.TransfereeState.NoTransferee) { @@ -226,10 +228,8 @@ class VectorCallActivity : VectorBaseActivity(), CallContro views.callActionText.text = getString(R.string.call_transfer_transfer_to_title, transfereeName) views.callActionText.isVisible = true views.callActionText.setOnClickListener { callViewModel.handle(VectorCallViewActions.TransferCall) } - views.callStatusText.text = state.formattedDuration configureCallInfo(state) } else { - views.callStatusText.text = state.formattedDuration configureCallInfo(state) if (callArgs.isVideoCall) { views.callVideoGroup.isVisible = true @@ -245,8 +245,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro views.callVideoGroup.isInvisible = true views.callInfoGroup.isVisible = true configureCallInfo(state) - views.callStatusText.setText(R.string.call_connecting) - views.callConnectingProgress.isVisible = true + views.callToolbar.setSubtitle(R.string.call_connecting) } } is CallState.Terminated -> { @@ -262,9 +261,14 @@ class VectorCallActivity : VectorBaseActivity(), CallContro val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen_blur) avatarRenderer.renderBlur(it, views.bgCallView, sampling = 20, rounded = false, colorFilter = colorFilter, addPlaceholder = false) if (state.transferee is VectorCallViewState.TransfereeState.NoTransferee) { - views.participantNameText.text = it.getBestName() + views.participantNameText.setTextOrHide(null) + views.callToolbar.title = if (state.isVideoCall) { + getString(R.string.video_call_with_participant, it.getBestName()) + } else { + getString(R.string.audio_call_with_participant, it.getBestName()) + } } else { - views.participantNameText.text = getString(R.string.call_transfer_consulting_with, it.getBestName()) + views.participantNameText.setTextOrHide(getString(R.string.call_transfer_consulting_with, it.getBestName())) } if (blurAvatar) { avatarRenderer.renderBlur(it, views.otherMemberAvatar, sampling = 2, rounded = true, colorFilter = colorFilter, addPlaceholder = true) diff --git a/vector/src/main/res/drawable/ic_call_back_to_chat.xml b/vector/src/main/res/drawable/ic_call_back_to_chat.xml new file mode 100644 index 0000000000..b1c532f17d --- /dev/null +++ b/vector/src/main/res/drawable/ic_call_back_to_chat.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/layout/activity_call.xml b/vector/src/main/res/layout/activity_call.xml index 0df28cf16a..94794799c4 100644 --- a/vector/src/main/res/layout/activity_call.xml +++ b/vector/src/main/res/layout/activity_call.xml @@ -22,9 +22,11 @@ + + - -