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 android.widget.Toast;
import com.bluelinelabs.logansquare.LoganSquare; import com.bluelinelabs.logansquare.LoganSquare;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.nextcloud.talk.R; import com.nextcloud.talk.R;
import com.nextcloud.talk.adapters.ParticipantDisplayItem; import com.nextcloud.talk.adapters.ParticipantDisplayItem;
import com.nextcloud.talk.adapters.ParticipantsAdapter; 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.permissions.PlatformPermissionUtil;
import com.nextcloud.talk.utils.power.PowerManagerUtils; import com.nextcloud.talk.utils.power.PowerManagerUtils;
import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder; import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder;
import com.nextcloud.talk.viewmodels.CallRecordingViewModel;
import com.nextcloud.talk.webrtc.MagicWebRTCUtils; import com.nextcloud.talk.webrtc.MagicWebRTCUtils;
import com.nextcloud.talk.webrtc.MagicWebSocketInstance; import com.nextcloud.talk.webrtc.MagicWebSocketInstance;
import com.nextcloud.talk.webrtc.PeerConnectionWrapper; import com.nextcloud.talk.webrtc.PeerConnectionWrapper;
@ -146,9 +148,11 @@ import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat; import androidx.core.graphics.drawable.DrawableCompat;
import androidx.lifecycle.ViewModelProvider;
import autodagger.AutoInjector; import autodagger.AutoInjector;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.Observer; import io.reactivex.Observer;
@ -191,11 +195,15 @@ public class CallActivity extends CallBaseActivity {
@Inject @Inject
PlatformPermissionUtil permissionUtil; PlatformPermissionUtil permissionUtil;
@Inject
ViewModelProvider.Factory viewModelFactory;
public static final String TAG = "CallActivity"; public static final String TAG = "CallActivity";
public WebRtcAudioManager audioManager; public WebRtcAudioManager audioManager;
public CallRecordingViewModel callRecordingViewModel;
private static final String[] PERMISSIONS_CALL = { private static final String[] PERMISSIONS_CALL = {
Manifest.permission.CAMERA, Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO Manifest.permission.RECORD_AUDIO
@ -230,7 +238,7 @@ public class CallActivity extends CallBaseActivity {
private Disposable signalingDisposable; private Disposable signalingDisposable;
private List<PeerConnection.IceServer> iceServers; private List<PeerConnection.IceServer> iceServers;
private CameraEnumerator cameraEnumerator; private CameraEnumerator cameraEnumerator;
public String roomToken; private String roomToken;
private User conversationUser; private User conversationUser;
private String conversationName; private String conversationName;
private String callSession; private String callSession;
@ -375,6 +383,33 @@ public class CallActivity extends CallBaseActivity {
setCallState(CallStatus.CONNECTING); 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(); initClickListeners();
binding.microphoneButton.setOnTouchListener(new MicrophoneButtonTouchListener()); binding.microphoneButton.setOnTouchListener(new MicrophoneButtonTouchListener());
@ -484,6 +519,10 @@ public class CallActivity extends CallBaseActivity {
hangupNetworkCalls(false); hangupNetworkCalls(false);
} }
}); });
binding.callRecordingIndicator.setOnClickListener(l -> {
callRecordingViewModel.clickRecordButton();
});
} }
private void createCameraEnumerator() { private void createCameraEnumerator() {
@ -2881,8 +2920,13 @@ public class CallActivity extends CallBaseActivity {
eventBus.post(new ConfigurationChangeEvent()); eventBus.post(new ConfigurationChangeEvent());
} }
public void showCallRecordingIndicator(){ public void showCallRecordingIndicator() {
binding.callRecordingIndicator.setVisibility(View.VISIBLE); binding.callRecordingIndicator.setVisibility(View.VISIBLE);
}
public void hideCallRecordingIndicator() {
binding.callRecordingIndicator.setVisibility(View.GONE);
} }
private class SelfVideoTouchListener implements View.OnTouchListener { 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.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import com.nextcloud.talk.remotefilebrowser.viewmodels.RemoteFileBrowserItemsViewModel
import com.nextcloud.talk.messagesearch.MessageSearchViewModel import com.nextcloud.talk.messagesearch.MessageSearchViewModel
import com.nextcloud.talk.polls.viewmodels.PollCreateViewModel import com.nextcloud.talk.polls.viewmodels.PollCreateViewModel
import com.nextcloud.talk.polls.viewmodels.PollMainViewModel import com.nextcloud.talk.polls.viewmodels.PollMainViewModel
import com.nextcloud.talk.polls.viewmodels.PollResultsViewModel import com.nextcloud.talk.polls.viewmodels.PollResultsViewModel
import com.nextcloud.talk.polls.viewmodels.PollVoteViewModel 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.shareditems.viewmodels.SharedItemsViewModel
import com.nextcloud.talk.viewmodels.CallRecordingViewModel
import dagger.Binds import dagger.Binds
import dagger.MapKey import dagger.MapKey
import dagger.Module import dagger.Module
@ -89,4 +90,9 @@ abstract class ViewModelModule {
@IntoMap @IntoMap
@ViewModelKey(RemoteFileBrowserItemsViewModel::class) @ViewModelKey(RemoteFileBrowserItemsViewModel::class)
abstract fun remoteFileBrowserItemsViewModel(viewModel: RemoteFileBrowserItemsViewModel): ViewModel 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( override fun stopRecording(
roomToken: String roomToken: String
): Observable<StopCallRecordingModel> { ): Observable<StopCallRecordingModel> {
return ncApi.stopRecording( return Observable.just<StopCallRecordingModel>(StopCallRecordingModel(true))
credentials,
ApiUtils.getUrlForRecording(
apiVersion,
currentUser.baseUrl,
roomToken
)
).map { mapToStopCallRecordingModel(it.ocs?.meta!!) }
} }
// 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( private fun mapToStartCallRecordingModel(
response: GenericMeta response: GenericMeta
): StartCallRecordingModel { ): StartCallRecordingModel {

View File

@ -31,13 +31,8 @@ import com.nextcloud.talk.R
import com.nextcloud.talk.activities.CallActivity import com.nextcloud.talk.activities.CallActivity
import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.databinding.DialogMoreCallActionsBinding 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 com.nextcloud.talk.ui.theme.ViewThemeUtils
import io.reactivex.Observer import com.nextcloud.talk.viewmodels.CallRecordingViewModel
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import javax.inject.Inject import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class) @AutoInjector(NextcloudTalkApplication::class)
@ -46,9 +41,6 @@ class MoreCallActionsDialog(val callActivity: CallActivity) : BottomSheetDialog(
@Inject @Inject
lateinit var viewThemeUtils: ViewThemeUtils lateinit var viewThemeUtils: ViewThemeUtils
@Inject
lateinit var callRecordingRepository: CallRecordingRepository
private lateinit var binding: DialogMoreCallActionsBinding private lateinit var binding: DialogMoreCallActionsBinding
override fun onCreate(savedInstanceState: Bundle?) { 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) window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
viewThemeUtils.platform.themeDialogDark(binding.root) viewThemeUtils.platform.themeDialogDark(binding.root)
initClickListeners() initClickListeners()
initObservers()
} }
private fun initClickListeners() { private fun initClickListeners() {
binding.recordCall.setOnClickListener { binding.recordCall.setOnClickListener {
callRecordingRepository.startRecording(callActivity.roomToken) callActivity.callRecordingViewModel.clickRecordButton()
.subscribeOn(Schedulers.io()) }
?.observeOn(AndroidSchedulers.mainThread()) }
?.subscribe(CallStartRecordingObserver())
// dismiss() 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 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 { companion object {
private const val TAG = "MoreCallActionsDialog" 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:src="@drawable/record_circle"
android:contentDescription="@null" android:contentDescription="@null"
android:visibility="gone" android:visibility="gone"
android:translationZ="2dp"
tools:visibility="visible"> tools:visibility="visible">
</ImageView> </ImageView>
</LinearLayout> </LinearLayout>

View File

@ -63,7 +63,7 @@
android:layout_gravity="start|center_vertical" android:layout_gravity="start|center_vertical"
android:paddingStart="@dimen/standard_double_padding" android:paddingStart="@dimen/standard_double_padding"
android:paddingEnd="@dimen/zero" android:paddingEnd="@dimen/zero"
android:text="@string/call_record_description" android:text="@string/record_start_description"
android:textAlignment="viewStart" android:textAlignment="viewStart"
android:textColor="@color/high_emphasis_text_dark_background" android:textColor="@color/high_emphasis_text_dark_background"
android:textSize="@dimen/bottom_sheet_text_size" /> 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_dialog_headline">Audio output</string>
<string name="audio_output_wired_headset">Wired headset</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_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 --> <!-- Shared items -->
<string name="shared_items_media">Media</string> <string name="shared_items_media">Media</string>