浏览代码

add ViewModel to start/stop recording

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Marcel Hibbe 2 年之前
父节点
当前提交
bafe9198eb

+ 46 - 2
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<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 {

+ 7 - 1
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
 }

+ 14 - 8
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<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 {

+ 31 - 35
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<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"
     }

+ 131 - 0
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 <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
+    }
+}

+ 1 - 0
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">
                 </ImageView>
             </LinearLayout>

+ 1 - 1
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" />

+ 9 - 1
app/src/main/res/values/strings.xml

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