From eaa55ab06899f99f442dc9b9f7c42bc5c9b8e514 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 14 Dec 2023 12:00:03 +0100 Subject: [PATCH] Added button to take photos in chat directly --- .../chat/fragment/ConversationFragment.kt | 96 ++++++++++++++++--- .../SendMessageInConversationViewModel.kt | 20 ++-- .../java/org/linphone/utils/TimestampUtils.kt | 8 ++ .../res/layout/chat_conversation_fragment.xml | 4 + ...conversation_record_voice_message_area.xml | 8 +- .../layout/chat_conversation_send_area.xml | 42 ++++++-- 6 files changed, 144 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt index 7d15a1ffc..f0c7719a3 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt @@ -25,6 +25,7 @@ import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.net.Uri @@ -39,6 +40,8 @@ import android.view.WindowManager import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.UiThread +import androidx.core.app.ActivityCompat +import androidx.core.content.FileProvider import androidx.core.view.doOnPreDraw import androidx.databinding.DataBindingUtil import androidx.lifecycle.ViewModelProvider @@ -53,6 +56,7 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout.OnTabSelectedListener +import java.io.File import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -80,6 +84,7 @@ import org.linphone.utils.FileUtils import org.linphone.utils.LinphoneUtils import org.linphone.utils.RecyclerViewSwipeUtils import org.linphone.utils.RecyclerViewSwipeUtilsCallback +import org.linphone.utils.TimestampUtils import org.linphone.utils.addCharacterAtPosition import org.linphone.utils.hideKeyboard import org.linphone.utils.setKeyboardInsetListener @@ -125,6 +130,49 @@ class ConversationFragment : SlidingPaneChildFragment() { } } + private var pendingImageCaptureFile: File? = null + + private val startCameraCapture = registerForActivityResult( + ActivityResultContracts.TakePicture() + ) { captured -> + val path = pendingImageCaptureFile?.absolutePath + if (path != null) { + if (captured) { + Log.i("$TAG Image was captured and saved in [$path]") + sendMessageViewModel.addAttachment(path) + } else { + Log.w("$TAG Image capture was aborted") + lifecycleScope.launch { + FileUtils.deleteFile(path) + } + } + pendingImageCaptureFile = null + } else { + Log.e("$TAG No pending captured image file!") + } + } + + private val requestCameraPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted -> + if (isGranted) { + Log.i("$TAG CAMERA permission has been granted") + } else { + Log.e("$TAG CAMERA permission has been denied") + } + } + + private val requestRecordAudioPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted -> + if (isGranted) { + Log.i("$TAG RECORD_AUDIO permission has been granted, starting voice message recording") + sendMessageViewModel.startVoiceMessageRecording() + } else { + Log.e("$TAG RECORD_AUDIO permission has been denied") + } + } + private val dataObserver = object : AdapterDataObserver() { override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { if (positionStart == 0 && adapter.itemCount == itemCount) { @@ -174,17 +222,6 @@ class ConversationFragment : SlidingPaneChildFragment() { override fun onSlide(bottomSheet: View, slideOffset: Float) { } } - private val requestPermissionLauncher = registerForActivityResult( - ActivityResultContracts.RequestPermission() - ) { isGranted -> - if (isGranted) { - Log.i("$TAG RECORD_AUDIO permission has been granted, starting voice message recording") - sendMessageViewModel.startVoiceMessageRecording() - } else { - Log.e("$TAG RECORD_AUDIO permission has been denied") - } - } - private var bottomSheetDeliveryModel: MessageDeliveryModel? = null private var bottomSheetReactionsModel: MessageReactionsModel? = null @@ -354,6 +391,40 @@ class ConversationFragment : SlidingPaneChildFragment() { ) } + binding.setOpenCameraClickListener { + if (ActivityCompat.checkSelfPermission( + requireContext(), + Manifest.permission.CAMERA + ) != PackageManager.PERMISSION_GRANTED + ) { + Log.w("$TAG Asking for CAMERA permission") + requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA) + } else { + val timeStamp = TimestampUtils.toFullString( + System.currentTimeMillis(), + timestampInSecs = false + ) + val tempFileName = "$timeStamp.jpg" + Log.i( + "$TAG Opening camera to take a picture, will be stored in file [$tempFileName]" + ) + val file = FileUtils.getFileStoragePath(tempFileName) + try { + val publicUri = FileProvider.getUriForFile( + requireContext(), + requireContext().getString(R.string.file_provider), + file + ) + pendingImageCaptureFile = file + startCameraCapture.launch(publicUri) + } catch (e: Exception) { + Log.e( + "$TAG Failed to get public URI for file in which to store captured image: $e" + ) + } + } + } + binding.setGoToInfoClickListener { if (findNavController().currentDestination?.id == R.id.conversationFragment) { val action = @@ -392,7 +463,7 @@ class ConversationFragment : SlidingPaneChildFragment() { sendMessageViewModel.askRecordAudioPermissionEvent.observe(viewLifecycleOwner) { it.consume { Log.w("$TAG Asking for RECORD_AUDIO permission") - requestPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO) + requestRecordAudioPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO) } } @@ -516,6 +587,7 @@ class ConversationFragment : SlidingPaneChildFragment() { }) binding.root.setKeyboardInsetListener { keyboardVisible -> + sendMessageViewModel.isKeyboardOpen.value = keyboardVisible if (keyboardVisible) { sendMessageViewModel.isEmojiPickerOpen.value = false diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt index 7fd2ba08f..3abb0b55d 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt @@ -79,9 +79,11 @@ class SendMessageInConversationViewModel @UiThread constructor() : ViewModel() { val isReplyingToMessage = MutableLiveData() - val voiceRecording = MutableLiveData() + val isKeyboardOpen = MutableLiveData() - val voiceRecordingInProgress = MutableLiveData() + val isVoiceRecording = MutableLiveData() + + val isVoiceRecordingInProgress = MutableLiveData() val voiceRecordingDuration = MutableLiveData() @@ -227,7 +229,7 @@ class SendMessageInConversationViewModel @UiThread constructor() : ViewModel() { message.addUtf8TextContent(toSend) } - if (voiceRecording.value == true && voiceMessageRecorder.file != null) { + if (isVoiceRecording.value == true && voiceMessageRecorder.file != null) { stopVoiceRecorder() val content = voiceMessageRecorder.createContent() if (content != null) { @@ -278,7 +280,7 @@ class SendMessageInConversationViewModel @UiThread constructor() : ViewModel() { if (::voiceMessageRecorder.isInitialized) { stopVoiceRecorder() } - voiceRecording.postValue(false) + isVoiceRecording.postValue(false) // Warning: do not delete files val attachmentsList = arrayListOf() @@ -375,10 +377,10 @@ class SendMessageInConversationViewModel @UiThread constructor() : ViewModel() { } coreContext.postOnCoreThread { - voiceRecording.postValue(true) + isVoiceRecording.postValue(true) initVoiceRecorder() - voiceRecordingInProgress.postValue(true) + isVoiceRecordingInProgress.postValue(true) startVoiceRecorder() } } @@ -403,7 +405,7 @@ class SendMessageInConversationViewModel @UiThread constructor() : ViewModel() { } } - voiceRecording.postValue(false) + isVoiceRecording.postValue(false) } } @@ -531,7 +533,7 @@ class SendMessageInConversationViewModel @UiThread constructor() : ViewModel() { voiceRecordAudioFocusRequest = null } - voiceRecordingInProgress.postValue(false) + isVoiceRecordingInProgress.postValue(false) } @WorkerThread @@ -642,7 +644,7 @@ class SendMessageInConversationViewModel @UiThread constructor() : ViewModel() { } private fun recorderTickerFlow() = flow { - while (voiceRecordingInProgress.value == true) { + while (isVoiceRecordingInProgress.value == true) { emit(Unit) delay(500) } diff --git a/app/src/main/java/org/linphone/utils/TimestampUtils.kt b/app/src/main/java/org/linphone/utils/TimestampUtils.kt index 5604fb781..8b15a7648 100644 --- a/app/src/main/java/org/linphone/utils/TimestampUtils.kt +++ b/app/src/main/java/org/linphone/utils/TimestampUtils.kt @@ -145,6 +145,14 @@ class TimestampUtils { return dateFormat.format(cal.time) } + @AnyThread + fun toFullString(time: Long, timestampInSecs: Boolean = true): String { + val calendar = Calendar.getInstance() + calendar.timeInMillis = if (timestampInSecs) time * 1000 else time + + return SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(calendar.time) + } + @AnyThread fun toString( timestamp: Long, diff --git a/app/src/main/res/layout/chat_conversation_fragment.xml b/app/src/main/res/layout/chat_conversation_fragment.xml index 544a764b8..05c40ac03 100644 --- a/app/src/main/res/layout/chat_conversation_fragment.xml +++ b/app/src/main/res/layout/chat_conversation_fragment.xml @@ -22,6 +22,9 @@ + @@ -240,6 +243,7 @@ layout="@layout/chat_conversation_send_area" app:layout_constraintBottom_toBottomOf="parent" bind:openFilePickerClickListener="@{openFilePickerClickListener}" + bind:openCameraClickListener="@{openCameraClickListener}" bind:viewModel="@{sendMessageViewModel}"/> diff --git a/app/src/main/res/layout/chat_conversation_send_area.xml b/app/src/main/res/layout/chat_conversation_send_area.xml index 95c646e5b..3ae871925 100644 --- a/app/src/main/res/layout/chat_conversation_send_area.xml +++ b/app/src/main/res/layout/chat_conversation_send_area.xml @@ -8,6 +8,9 @@ + @@ -23,8 +26,15 @@ android:id="@+id/standard_messages" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:visibility="@{viewModel.voiceRecording ? View.INVISIBLE : View.VISIBLE}" - app:constraint_referenced_ids="emoji_picker_toggle, attach_file, message_area_background, message_to_send" /> + android:visibility="@{viewModel.isVoiceRecording ? View.INVISIBLE : View.VISIBLE}" + app:constraint_referenced_ids="emoji_picker_toggle, message_area_background, message_to_send" /> + + + +