mirror of
https://gitlab.linphone.org/BC/public/linphone-android.git
synced 2026-01-20 21:18:07 +00:00
Added button to take photos in chat directly
This commit is contained in:
parent
62c23c248f
commit
eaa55ab068
6 changed files with 144 additions and 34 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -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"/>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue