Added button to take photos in chat directly

This commit is contained in:
Sylvain Berfini 2023-12-14 12:00:03 +01:00
parent 62c23c248f
commit eaa55ab068
6 changed files with 144 additions and 34 deletions

View file

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

View file

@ -79,9 +79,11 @@ class SendMessageInConversationViewModel @UiThread constructor() : ViewModel() {
val isReplyingToMessage = MutableLiveData<String>()
val voiceRecording = MutableLiveData<Boolean>()
val isKeyboardOpen = MutableLiveData<Boolean>()
val voiceRecordingInProgress = MutableLiveData<Boolean>()
val isVoiceRecording = MutableLiveData<Boolean>()
val isVoiceRecordingInProgress = MutableLiveData<Boolean>()
val voiceRecordingDuration = MutableLiveData<Int>()
@ -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<FileModel>()
@ -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)
}

View file

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

View file

@ -22,6 +22,9 @@
<variable
name="openFilePickerClickListener"
type="View.OnClickListener" />
<variable
name="openCameraClickListener"
type="View.OnClickListener" />
<variable
name="scrollToBottomClickListener"
type="View.OnClickListener" />
@ -240,6 +243,7 @@
layout="@layout/chat_conversation_send_area"
app:layout_constraintBottom_toBottomOf="parent"
bind:openFilePickerClickListener="@{openFilePickerClickListener}"
bind:openCameraClickListener="@{openCameraClickListener}"
bind:viewModel="@{sendMessageViewModel}"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton

View file

@ -53,7 +53,7 @@
android:padding="8dp"
android:src="@drawable/stop_fill"
android:background="@drawable/circle_white_button_background"
android:visibility="@{viewModel.voiceRecordingInProgress ? View.VISIBLE : View.GONE}"
android:visibility="@{viewModel.isVoiceRecordingInProgress ? View.VISIBLE : View.GONE}"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintBottom_toBottomOf="@id/voice_recording_duration"
app:layout_constraintStart_toStartOf="@id/voice_record_progress"
@ -69,7 +69,7 @@
android:padding="8dp"
android:src="@{viewModel.isPlayingVoiceRecord ? @drawable/pause_fill : @drawable/play_fill, default=@drawable/play_fill}"
android:background="@drawable/circle_white_button_background"
android:visibility="@{!viewModel.voiceRecordingInProgress ? View.VISIBLE : View.GONE}"
android:visibility="@{!viewModel.isVoiceRecordingInProgress ? View.VISIBLE : View.GONE}"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintBottom_toBottomOf="@id/voice_record_duration"
app:layout_constraintStart_toStartOf="@id/voice_record_progress"
@ -90,7 +90,7 @@
android:textSize="14sp"
android:textColor="?attr/color_main2_600"
android:background="@drawable/shape_squircle_white_r50_background"
android:visibility="@{viewModel.voiceRecordingInProgress ? View.VISIBLE : View.GONE}"
android:visibility="@{viewModel.isVoiceRecordingInProgress ? View.VISIBLE : View.GONE}"
android:drawableStart="@drawable/record_fill"
android:drawablePadding="8dp"
app:drawableTint="@color/red_danger_500"
@ -112,7 +112,7 @@
android:textSize="14sp"
android:textColor="?attr/color_main2_600"
android:background="@drawable/shape_squircle_white_r50_background"
android:visibility="@{!viewModel.voiceRecordingInProgress ? View.VISIBLE : View.GONE}"
android:visibility="@{!viewModel.isVoiceRecordingInProgress ? View.VISIBLE : View.GONE}"
app:layout_constraintBottom_toBottomOf="@id/voice_record_progress"
app:layout_constraintEnd_toEndOf="@id/voice_record_progress"
app:layout_constraintTop_toTopOf="@id/voice_record_progress"/>

View file

@ -8,6 +8,9 @@
<variable
name="openFilePickerClickListener"
type="View.OnClickListener" />
<variable
name="openCameraClickListener"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.main.chat.viewmodel.SendMessageInConversationViewModel" />
@ -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" />
<androidx.constraintlayout.widget.Group
android:id="@+id/extra_actions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{viewModel.isVoiceRecording ? View.INVISIBLE : viewModel.isKeyboardOpen ? View.GONE : View.VISIBLE}"
app:constraint_referenced_ids="attach_file, capture_image" />
<include
android:id="@+id/reply_area"
@ -71,7 +81,7 @@
android:id="@+id/emoji_picker_toggle"
android:layout_width="40dp"
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:layout_marginStart="5dp"
android:onClick="@{() -> viewModel.toggleEmojiPickerVisibility()}"
android:padding="8dp"
android:src="@{viewModel.isEmojiPickerOpen ? @drawable/x : @drawable/smiley, default=@drawable/smiley}"
@ -84,26 +94,40 @@
android:id="@+id/attach_file"
android:layout_width="40dp"
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="5dp"
android:onClick="@{openFilePickerClickListener}"
android:padding="8dp"
android:src="@drawable/paperclip"
app:layout_constraintBottom_toBottomOf="@id/message_area_background"
app:layout_constraintEnd_toStartOf="@id/message_area_background"
app:layout_constraintEnd_toStartOf="@id/capture_image"
app:layout_constraintStart_toEndOf="@id/emoji_picker_toggle"
app:layout_constraintTop_toTopOf="@id/message_area_background"
app:tint="@color/icon_color_selector" />
<ImageView
android:id="@+id/capture_image"
android:layout_width="40dp"
android:layout_height="0dp"
android:layout_marginStart="5dp"
android:onClick="@{openCameraClickListener}"
android:padding="8dp"
android:src="@drawable/camera"
app:layout_constraintBottom_toBottomOf="@id/message_area_background"
app:layout_constraintEnd_toStartOf="@id/message_area_background"
app:layout_constraintStart_toEndOf="@id/attach_file"
app:layout_constraintTop_toTopOf="@id/message_area_background"
app:tint="@color/icon_color_selector" />
<ImageView
android:id="@+id/message_area_background"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="5dp"
android:layout_marginEnd="16dp"
android:src="@drawable/edit_text_background"
app:layout_constraintBottom_toBottomOf="@id/message_to_send"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/attach_file"
app:layout_constraintStart_toEndOf="@id/capture_image"
app:layout_constraintTop_toTopOf="@id/message_to_send" />
<org.linphone.ui.main.chat.view.RichEditText
@ -155,7 +179,7 @@
android:layout_width="40dp"
android:layout_height="0dp"
android:layout_marginEnd="4dp"
android:visibility="@{viewModel.textToSend.length() > 0 || viewModel.attachments.size() > 0 || viewModel.voiceRecording ? View.GONE : View.VISIBLE}"
android:visibility="@{viewModel.textToSend.length() > 0 || viewModel.attachments.size() > 0 || viewModel.isVoiceRecording ? View.GONE : View.VISIBLE}"
android:onClick="@{() -> viewModel.startVoiceMessageRecording()}"
android:padding="8dp"
android:src="@drawable/microphone"
@ -170,7 +194,7 @@
android:layout_height="wrap_content"
layout="@layout/chat_conversation_record_voice_message_area"
bind:viewModel="@{viewModel}"
android:visibility="@{viewModel.voiceRecording ? View.VISIBLE : View.GONE, default=gone}"
android:visibility="@{viewModel.isVoiceRecording ? View.VISIBLE : View.GONE, default=gone}"
app:layout_constraintTop_toTopOf="@id/message_area_background"
app:layout_constraintBottom_toBottomOf="@id/message_area_background"/>