add ViewModel to start/stop recording

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
Marcel Hibbe 2022-12-05 18:20:25 +01:00
parent bb53982dd1
commit bafe9198eb
No known key found for this signature in database
GPG Key ID: C793F8B59F43CE7B
8 changed files with 240 additions and 48 deletions

View File

@ -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<PeerConnection.IceServer> 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 {

View File

@ -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
}

View File

@ -59,16 +59,22 @@ class CallRecordingRepositoryImpl(private val ncApi: NcApi, currentUserProvider:
override fun stopRecording(
roomToken: String
): Observable<StopCallRecordingModel> {
return ncApi.stopRecording(
credentials,
ApiUtils.getUrlForRecording(
apiVersion,
currentUser.baseUrl,
roomToken
)
).map { mapToStopCallRecordingModel(it.ocs?.meta!!) }
return Observable.just<StopCallRecordingModel>(StopCallRecordingModel(true))
}
// override fun stopRecording(
// roomToken: String
// ): Observable<StopCallRecordingModel> {
// return ncApi.stopRecording(
// credentials,
// ApiUtils.getUrlForRecording(
// apiVersion,
// currentUser.baseUrl,
// roomToken
// )
// ).map { mapToStopCallRecordingModel(it.ocs?.meta!!) }
// }
private fun mapToStartCallRecordingModel(
response: GenericMeta
): StartCallRecordingModel {

View File

@ -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<StartCallRecordingModel> {
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"
}

View File

@ -0,0 +1,131 @@
/*
* Nextcloud Talk application
*
* @author Marcel Hibbe
* Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
*
* 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.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<ViewState> = MutableLiveData(RecordingStoppedState)
val viewState: LiveData<ViewState>
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<StartCallRecordingModel> {
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<StopCallRecordingModel> {
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
}
}

View File

@ -105,6 +105,7 @@
android:src="@drawable/record_circle"
android:contentDescription="@null"
android:visibility="gone"
android:translationZ="2dp"
tools:visibility="visible">
</ImageView>
</LinearLayout>

View File

@ -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" />

View File

@ -557,8 +557,16 @@
<string name="audio_output_dialog_headline">Audio output</string>
<string name="audio_output_wired_headset">Wired headset</string>
<!-- Advanced call options -->
<string name="call_more_actions_dialog_headline">Advanced call options</string>
<string name="call_record_description">Record call</string>
<!-- Call recording -->
<string name="record_start_description">Start recording</string>
<string name="record_start_loading">starting…</string>
<string name="record_stop_description">Stop recording</string>
<string name="record_stop_loading">stopping…</string>
<string name="record_stop_confirm_title">Stop Call recording</string>
<string name="record_stop_confirm_message">"Do you really want to stop the recording?"</string>
<!-- Shared items -->
<string name="shared_items_media">Media</string>