From bafe9198eb9f948fdfc3d2007a1e2b2ed3162718 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Mon, 5 Dec 2022 18:20:25 +0100 Subject: [PATCH] add ViewModel to start/stop recording Signed-off-by: Marcel Hibbe --- .../talk/activities/CallActivity.java | 48 ++++++- .../talk/dagger/modules/ViewModelModule.kt | 8 +- .../CallRecordingRepositoryImpl.kt | 22 +-- .../talk/ui/dialog/MoreCallActionsDialog.kt | 66 +++++---- .../talk/viewmodels/CallRecordingViewModel.kt | 131 ++++++++++++++++++ app/src/main/res/layout/call_activity.xml | 1 + .../res/layout/dialog_more_call_actions.xml | 2 +- app/src/main/res/values/strings.xml | 10 +- 8 files changed, 240 insertions(+), 48 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/talk/viewmodels/CallRecordingViewModel.kt diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java index d9031041f..22235eba5 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java @@ -57,6 +57,7 @@ import android.widget.RelativeLayout; import android.widget.Toast; import com.bluelinelabs.logansquare.LoganSquare; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.nextcloud.talk.R; import com.nextcloud.talk.adapters.ParticipantDisplayItem; import com.nextcloud.talk.adapters.ParticipantsAdapter; @@ -97,6 +98,7 @@ import com.nextcloud.talk.utils.animations.PulseAnimation; import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil; import com.nextcloud.talk.utils.power.PowerManagerUtils; import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder; +import com.nextcloud.talk.viewmodels.CallRecordingViewModel; import com.nextcloud.talk.webrtc.MagicWebRTCUtils; import com.nextcloud.talk.webrtc.MagicWebSocketInstance; import com.nextcloud.talk.webrtc.PeerConnectionWrapper; @@ -146,9 +148,11 @@ import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; +import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; +import androidx.lifecycle.ViewModelProvider; import autodagger.AutoInjector; import io.reactivex.Observable; import io.reactivex.Observer; @@ -191,11 +195,15 @@ public class CallActivity extends CallBaseActivity { @Inject PlatformPermissionUtil permissionUtil; + @Inject + ViewModelProvider.Factory viewModelFactory; public static final String TAG = "CallActivity"; public WebRtcAudioManager audioManager; + public CallRecordingViewModel callRecordingViewModel; + private static final String[] PERMISSIONS_CALL = { Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO @@ -230,7 +238,7 @@ public class CallActivity extends CallBaseActivity { private Disposable signalingDisposable; private List iceServers; private CameraEnumerator cameraEnumerator; - public String roomToken; + private String roomToken; private User conversationUser; private String conversationName; private String callSession; @@ -375,6 +383,33 @@ public class CallActivity extends CallBaseActivity { setCallState(CallStatus.CONNECTING); } + callRecordingViewModel = new ViewModelProvider(this, viewModelFactory).get((CallRecordingViewModel.class)); + callRecordingViewModel.setData(roomToken); + + callRecordingViewModel.getViewState().observe(this, viewState -> { + if (viewState instanceof CallRecordingViewModel.RecordingStartedState) { + showCallRecordingIndicator(); + } else if (viewState instanceof CallRecordingViewModel.RecordingConfirmStopState) { + MaterialAlertDialogBuilder dialogBuilder = new MaterialAlertDialogBuilder(this) + .setTitle(R.string.record_stop_confirm_title) + .setMessage(R.string.record_stop_confirm_message) + .setPositiveButton(R.string.record_stop_description, + (dialog, which) -> callRecordingViewModel.stopRecording()) + .setNegativeButton(R.string.nc_common_dismiss, + (dialog, which) -> callRecordingViewModel.dismissStopRecording()); + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this, dialogBuilder); + AlertDialog dialog = dialogBuilder.show(); + + viewThemeUtils.platform.colorTextButtons( + dialog.getButton(AlertDialog.BUTTON_POSITIVE), + dialog.getButton(AlertDialog.BUTTON_NEGATIVE) + ); + + } else { + hideCallRecordingIndicator(); + } + }); + initClickListeners(); binding.microphoneButton.setOnTouchListener(new MicrophoneButtonTouchListener()); @@ -484,6 +519,10 @@ public class CallActivity extends CallBaseActivity { hangupNetworkCalls(false); } }); + + binding.callRecordingIndicator.setOnClickListener(l -> { + callRecordingViewModel.clickRecordButton(); + }); } private void createCameraEnumerator() { @@ -2881,8 +2920,13 @@ public class CallActivity extends CallBaseActivity { eventBus.post(new ConfigurationChangeEvent()); } - public void showCallRecordingIndicator(){ + public void showCallRecordingIndicator() { binding.callRecordingIndicator.setVisibility(View.VISIBLE); + + } + + public void hideCallRecordingIndicator() { + binding.callRecordingIndicator.setVisibility(View.GONE); } private class SelfVideoTouchListener implements View.OnTouchListener { diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt index 3d8ee7535..990bdc32d 100644 --- a/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/ViewModelModule.kt @@ -23,13 +23,14 @@ package com.nextcloud.talk.dagger.modules import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import com.nextcloud.talk.remotefilebrowser.viewmodels.RemoteFileBrowserItemsViewModel import com.nextcloud.talk.messagesearch.MessageSearchViewModel import com.nextcloud.talk.polls.viewmodels.PollCreateViewModel import com.nextcloud.talk.polls.viewmodels.PollMainViewModel import com.nextcloud.talk.polls.viewmodels.PollResultsViewModel import com.nextcloud.talk.polls.viewmodels.PollVoteViewModel +import com.nextcloud.talk.remotefilebrowser.viewmodels.RemoteFileBrowserItemsViewModel import com.nextcloud.talk.shareditems.viewmodels.SharedItemsViewModel +import com.nextcloud.talk.viewmodels.CallRecordingViewModel import dagger.Binds import dagger.MapKey import dagger.Module @@ -89,4 +90,9 @@ abstract class ViewModelModule { @IntoMap @ViewModelKey(RemoteFileBrowserItemsViewModel::class) abstract fun remoteFileBrowserItemsViewModel(viewModel: RemoteFileBrowserItemsViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(CallRecordingViewModel::class) + abstract fun callRecordingViewModel(viewModel: CallRecordingViewModel): ViewModel } diff --git a/app/src/main/java/com/nextcloud/talk/repositories/callrecording/CallRecordingRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/repositories/callrecording/CallRecordingRepositoryImpl.kt index 2f31c3f8c..72e6cf62e 100644 --- a/app/src/main/java/com/nextcloud/talk/repositories/callrecording/CallRecordingRepositoryImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/repositories/callrecording/CallRecordingRepositoryImpl.kt @@ -59,16 +59,22 @@ class CallRecordingRepositoryImpl(private val ncApi: NcApi, currentUserProvider: override fun stopRecording( roomToken: String ): Observable { - return ncApi.stopRecording( - credentials, - ApiUtils.getUrlForRecording( - apiVersion, - currentUser.baseUrl, - roomToken - ) - ).map { mapToStopCallRecordingModel(it.ocs?.meta!!) } + return Observable.just(StopCallRecordingModel(true)) } + // override fun stopRecording( + // roomToken: String + // ): Observable { + // return ncApi.stopRecording( + // credentials, + // ApiUtils.getUrlForRecording( + // apiVersion, + // currentUser.baseUrl, + // roomToken + // ) + // ).map { mapToStopCallRecordingModel(it.ocs?.meta!!) } + // } + private fun mapToStartCallRecordingModel( response: GenericMeta ): StartCallRecordingModel { diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt index b416259b9..f7bdfe4fe 100644 --- a/app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt +++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/MoreCallActionsDialog.kt @@ -31,13 +31,8 @@ import com.nextcloud.talk.R import com.nextcloud.talk.activities.CallActivity import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.databinding.DialogMoreCallActionsBinding -import com.nextcloud.talk.models.domain.StartCallRecordingModel -import com.nextcloud.talk.repositories.callrecording.CallRecordingRepository import com.nextcloud.talk.ui.theme.ViewThemeUtils -import io.reactivex.Observer -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.Disposable -import io.reactivex.schedulers.Schedulers +import com.nextcloud.talk.viewmodels.CallRecordingViewModel import javax.inject.Inject @AutoInjector(NextcloudTalkApplication::class) @@ -46,9 +41,6 @@ class MoreCallActionsDialog(val callActivity: CallActivity) : BottomSheetDialog( @Inject lateinit var viewThemeUtils: ViewThemeUtils - @Inject - lateinit var callRecordingRepository: CallRecordingRepository - private lateinit var binding: DialogMoreCallActionsBinding override fun onCreate(savedInstanceState: Bundle?) { @@ -60,16 +52,41 @@ class MoreCallActionsDialog(val callActivity: CallActivity) : BottomSheetDialog( window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) viewThemeUtils.platform.themeDialogDark(binding.root) + initClickListeners() + initObservers() } private fun initClickListeners() { binding.recordCall.setOnClickListener { - callRecordingRepository.startRecording(callActivity.roomToken) - .subscribeOn(Schedulers.io()) - ?.observeOn(AndroidSchedulers.mainThread()) - ?.subscribe(CallStartRecordingObserver()) - // dismiss() + callActivity.callRecordingViewModel.clickRecordButton() + } + } + + private fun initObservers() { + callActivity.callRecordingViewModel.viewState.observe(this) { state -> + when (state) { + is CallRecordingViewModel.RecordingStartedState -> { + binding.recordCallText.text = context.getText(R.string.record_stop_description) + dismiss() + } + is CallRecordingViewModel.RecordingStoppedState -> { + binding.recordCallText.text = context.getText(R.string.record_start_description) + dismiss() + } + is CallRecordingViewModel.RecordingStartLoadingState -> { + binding.recordCallText.text = context.getText(R.string.record_start_loading) + } + is CallRecordingViewModel.RecordingStopLoadingState -> { + binding.recordCallText.text = context.getText(R.string.record_stop_loading) + } + is CallRecordingViewModel.RecordingConfirmStopState -> { + binding.recordCallText.text = context.getText(R.string.record_stop_description) + } + else -> { + Log.e(TAG, "unknown viewState for callRecordingViewModel") + } + } } } @@ -80,27 +97,6 @@ class MoreCallActionsDialog(val callActivity: CallActivity) : BottomSheetDialog( behavior.state = BottomSheetBehavior.STATE_COLLAPSED } - inner class CallStartRecordingObserver : Observer { - override fun onSubscribe(d: Disposable) { - // unused atm - } - - override fun onNext(startCallRecordingModel: StartCallRecordingModel) { - if (startCallRecordingModel.success) { - binding.recordCallText.text = "started" - callActivity.showCallRecordingIndicator() - } - } - - override fun onError(e: Throwable) { - Log.e(TAG, "failure in CallStartRecordingObserver", e) - } - - override fun onComplete() { - // dismiss() - } - } - companion object { private const val TAG = "MoreCallActionsDialog" } diff --git a/app/src/main/java/com/nextcloud/talk/viewmodels/CallRecordingViewModel.kt b/app/src/main/java/com/nextcloud/talk/viewmodels/CallRecordingViewModel.kt new file mode 100644 index 000000000..8ecae40a7 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/viewmodels/CallRecordingViewModel.kt @@ -0,0 +1,131 @@ +/* + * Nextcloud Talk application + * + * @author Marcel Hibbe + * Copyright (C) 2022 Marcel Hibbe + * + * 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 . + */ + +package com.nextcloud.talk.viewmodels + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.nextcloud.talk.models.domain.StartCallRecordingModel +import com.nextcloud.talk.models.domain.StopCallRecordingModel +import com.nextcloud.talk.repositories.callrecording.CallRecordingRepository +import com.nextcloud.talk.users.UserManager +import io.reactivex.Observer +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject + +class CallRecordingViewModel @Inject constructor(private val repository: CallRecordingRepository) : ViewModel() { + + @Inject + lateinit var userManager: UserManager + + lateinit var roomToken: String + + sealed interface ViewState + object RecordingStartedState : ViewState + object RecordingStoppedState : ViewState + object RecordingStartLoadingState : ViewState + object RecordingStopLoadingState : ViewState + object RecordingConfirmStopState : ViewState + + private val _viewState: MutableLiveData = MutableLiveData(RecordingStoppedState) + val viewState: LiveData + get() = _viewState + + private var disposable: Disposable? = null + + fun clickRecordButton() { + if (viewState.value == RecordingStartedState) { + _viewState.value = RecordingConfirmStopState + } else if (viewState.value == RecordingStoppedState) { + _viewState.value = RecordingStartLoadingState + repository.startRecording(roomToken) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(CallStartRecordingObserver()) + } + } + + fun stopRecording() { + _viewState.value = RecordingStopLoadingState + repository.stopRecording(roomToken) + .subscribeOn(Schedulers.io()) + ?.observeOn(AndroidSchedulers.mainThread()) + ?.subscribe(CallStopRecordingObserver()) + } + + fun dismissStopRecording() { + _viewState.value = RecordingStartedState + } + + override fun onCleared() { + super.onCleared() + disposable?.dispose() + } + + fun setData(roomToken: String) { + this.roomToken = roomToken + } + + inner class CallStartRecordingObserver : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(startCallRecordingModel: StartCallRecordingModel) { + if (startCallRecordingModel.success) { + _viewState.value = RecordingStartedState + } + } + + override fun onError(e: Throwable) { + Log.e(TAG, "failure in CallStartRecordingObserver", e) + } + + override fun onComplete() { + // dismiss() + } + } + + inner class CallStopRecordingObserver : Observer { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onNext(stopCallRecordingModel: StopCallRecordingModel) { + _viewState.value = RecordingStoppedState + } + + override fun onError(e: Throwable) { + Log.e(TAG, "failure in CallStopRecordingObserver", e) + } + + override fun onComplete() { + // dismiss() + } + } + + companion object { + private val TAG = CallRecordingViewModel::class.java.simpleName + } +} diff --git a/app/src/main/res/layout/call_activity.xml b/app/src/main/res/layout/call_activity.xml index ad3c7d998..a1905e4e8 100644 --- a/app/src/main/res/layout/call_activity.xml +++ b/app/src/main/res/layout/call_activity.xml @@ -105,6 +105,7 @@ android:src="@drawable/record_circle" android:contentDescription="@null" android:visibility="gone" + android:translationZ="2dp" tools:visibility="visible"> diff --git a/app/src/main/res/layout/dialog_more_call_actions.xml b/app/src/main/res/layout/dialog_more_call_actions.xml index 17d843c35..e33ef1181 100644 --- a/app/src/main/res/layout/dialog_more_call_actions.xml +++ b/app/src/main/res/layout/dialog_more_call_actions.xml @@ -63,7 +63,7 @@ android:layout_gravity="start|center_vertical" android:paddingStart="@dimen/standard_double_padding" android:paddingEnd="@dimen/zero" - android:text="@string/call_record_description" + android:text="@string/record_start_description" android:textAlignment="viewStart" android:textColor="@color/high_emphasis_text_dark_background" android:textSize="@dimen/bottom_sheet_text_size" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1ccf3a82f..25f513e20 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -557,8 +557,16 @@ Audio output Wired headset + Advanced call options - Record call + + + Start recording + starting… + Stop recording + stopping… + Stop Call recording + "Do you really want to stop the recording?" Media