Add option to record a video from the camera

Replace #2411
This commit is contained in:
Benoit Marty 2021-05-03 16:59:32 +02:00 committed by Benoit Marty
parent 30a54cfdbc
commit d9ffce7e0d
11 changed files with 404 additions and 12 deletions

View file

@ -10,6 +10,7 @@ Improvements 🙌:
- Compress video before sending (#442)
- Improve file too big error detection (#3245)
- User can now select video when selecting Gallery to send attachments to a room
- Add option to record a video from the camera
Bugfix 🐛:
- Message states cosmetic changes (#3007)

View file

@ -0,0 +1,126 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.lib.multipicker
import android.content.Context
import android.content.Intent
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.provider.MediaStore
import androidx.activity.result.ActivityResultLauncher
import androidx.core.content.FileProvider
import im.vector.lib.multipicker.entity.MultiPickerVideoType
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* Implementation of taking a video with Camera
*/
class CameraVideoPicker {
/**
* Start camera by using a ActivityResultLauncher
* @return Uri of taken photo or null if the operation is cancelled.
*/
fun startWithExpectingFile(context: Context, activityResultLauncher: ActivityResultLauncher<Intent>): Uri? {
val videoUri = createVideoUri(context)
val intent = createIntent().apply {
putExtra(MediaStore.EXTRA_OUTPUT, videoUri)
}
activityResultLauncher.launch(intent)
return videoUri
}
/**
* Call this function from onActivityResult(int, int, Intent).
* @return Taken photo or null if request code is wrong
* or result code is not Activity.RESULT_OK
* or user cancelled the operation.
*/
fun getTakenVideo(context: Context, videoUri: Uri): MultiPickerVideoType? {
val projection = arrayOf(
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.SIZE,
MediaStore.Images.Media.MIME_TYPE
)
context.contentResolver.query(
videoUri,
projection,
null,
null,
null
)?.use { cursor ->
val nameColumn = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)
val sizeColumn = cursor.getColumnIndex(MediaStore.Images.Media.SIZE)
if (cursor.moveToNext()) {
val name = cursor.getString(nameColumn)
val size = cursor.getLong(sizeColumn)
var duration = 0L
var width = 0
var height = 0
var orientation = 0
context.contentResolver.openFileDescriptor(videoUri, "r")?.use { pfd ->
val mediaMetadataRetriever = MediaMetadataRetriever()
mediaMetadataRetriever.setDataSource(pfd.fileDescriptor)
duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0L
width = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toInt() ?: 0
height = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toInt() ?: 0
orientation = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)?.toInt() ?: 0
}
return MultiPickerVideoType(
name,
size,
context.contentResolver.getType(videoUri),
videoUri,
width,
height,
orientation,
duration
)
}
}
return null
}
private fun createIntent(): Intent {
return Intent(MediaStore.ACTION_VIDEO_CAPTURE)
}
companion object {
fun createVideoUri(context: Context): Uri {
val file = createVideoFile(context)
val authority = context.packageName + ".multipicker.fileprovider"
return FileProvider.getUriForFile(context, authority, file)
}
private fun createVideoFile(context: Context): File {
val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
val storageDir: File = context.filesDir
return File.createTempFile(
"${timeStamp}_", /* prefix */
".mp4", /* suffix */
storageDir /* directory */
)
}
}
}

View file

@ -26,6 +26,7 @@ class MultiPicker<T> {
val AUDIO by lazy { MultiPicker<AudioPicker>() }
val CONTACT by lazy { MultiPicker<ContactPicker>() }
val CAMERA by lazy { MultiPicker<CameraPicker>() }
val CAMERA_VIDEO by lazy { MultiPicker<CameraVideoPicker>() }
@Suppress("UNCHECKED_CAST")
fun <T> get(type: MultiPicker<T>): T {
@ -37,6 +38,7 @@ class MultiPicker<T> {
AUDIO -> AudioPicker() as T
CONTACT -> ContactPicker() as T
CAMERA -> CameraPicker() as T
CAMERA_VIDEO -> CameraVideoPicker() as T
else -> throw IllegalArgumentException("Unsupported type $type")
}
}

View file

@ -0,0 +1,118 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.dialogs
import android.app.Activity
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import im.vector.app.R
import im.vector.app.databinding.DialogPhotoOrVideoBinding
import im.vector.app.features.settings.VectorPreferences
class PhotoOrVideoDialog(
private val activity: Activity,
private val vectorPreferences: VectorPreferences
) {
interface PhotoOrVideoDialogListener {
fun takePhoto()
fun takeVideo()
}
interface PhotoOrVideoDialogSettingsListener {
fun onUpdated()
}
fun show(listener: PhotoOrVideoDialogListener) {
when (vectorPreferences.getTakePhotoVideoMode()) {
VectorPreferences.TAKE_PHOTO_VIDEO_MODE_PHOTO -> listener.takePhoto()
VectorPreferences.TAKE_PHOTO_VIDEO_MODE_VIDEO -> listener.takeVideo()
/* VectorPreferences.TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK */
else -> {
val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_photo_or_video, null)
val views = DialogPhotoOrVideoBinding.bind(dialogLayout)
// Show option to set as default in this case
views.dialogPhotoOrVideoAsDefault.isVisible = true
// Always default to photo
views.dialogPhotoOrVideoPhoto.isChecked = true
AlertDialog.Builder(activity)
.setTitle(R.string.option_take_photo_video)
.setView(dialogLayout)
.setPositiveButton(R.string._continue) { _, _ ->
submit(views, vectorPreferences, listener)
}
.setNegativeButton(R.string.cancel, null)
.show()
}
}
}
private fun submit(views: DialogPhotoOrVideoBinding,
vectorPreferences: VectorPreferences,
listener: PhotoOrVideoDialogListener) {
val mode = if (views.dialogPhotoOrVideoPhoto.isChecked) {
VectorPreferences.TAKE_PHOTO_VIDEO_MODE_PHOTO
} else {
VectorPreferences.TAKE_PHOTO_VIDEO_MODE_VIDEO
}
if (views.dialogPhotoOrVideoAsDefault.isChecked) {
vectorPreferences.setTakePhotoVideoMode(mode)
}
when (mode) {
VectorPreferences.TAKE_PHOTO_VIDEO_MODE_PHOTO -> listener.takePhoto()
VectorPreferences.TAKE_PHOTO_VIDEO_MODE_VIDEO -> listener.takeVideo()
}
}
fun showForSettings(listener: PhotoOrVideoDialogSettingsListener) {
val currentMode = vectorPreferences.getTakePhotoVideoMode()
val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_photo_or_video, null)
val views = DialogPhotoOrVideoBinding.bind(dialogLayout)
// Show option for always ask in this case
views.dialogPhotoOrVideoAlwaysAsk.isVisible = true
// Always default to photo
views.dialogPhotoOrVideoPhoto.isChecked = currentMode == VectorPreferences.TAKE_PHOTO_VIDEO_MODE_PHOTO
views.dialogPhotoOrVideoVideo.isChecked = currentMode == VectorPreferences.TAKE_PHOTO_VIDEO_MODE_VIDEO
views.dialogPhotoOrVideoAlwaysAsk.isChecked = currentMode == VectorPreferences.TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK
AlertDialog.Builder(activity)
.setTitle(R.string.option_take_photo_video)
.setView(dialogLayout)
.setPositiveButton(R.string.save) { _, _ ->
submitSettings(views)
listener.onUpdated()
}
.setNegativeButton(R.string.cancel, null)
.show()
}
private fun submitSettings(views: DialogPhotoOrVideoBinding) {
vectorPreferences.setTakePhotoVideoMode(
when {
views.dialogPhotoOrVideoPhoto.isChecked -> VectorPreferences.TAKE_PHOTO_VIDEO_MODE_PHOTO
views.dialogPhotoOrVideoVideo.isChecked -> VectorPreferences.TAKE_PHOTO_VIDEO_MODE_VIDEO
else -> VectorPreferences.TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK
}
)
}
}

View file

@ -15,12 +15,15 @@
*/
package im.vector.app.features.attachments
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.activity.result.ActivityResultLauncher
import im.vector.app.core.dialogs.PhotoOrVideoDialog
import im.vector.app.core.platform.Restorable
import im.vector.app.features.settings.VectorPreferences
import im.vector.lib.multipicker.MultiPicker
import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
@ -91,10 +94,21 @@ class AttachmentsHelper(val context: Context, val callback: Callback) : Restorab
}
/**
* Starts the process for handling capture image picking
* Starts the process for handling image/video capture. Can open a dialog
*/
fun openCamera(context: Context, activityResultLauncher: ActivityResultLauncher<Intent>) {
captureUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(context, activityResultLauncher)
fun openCamera(activity: Activity,
vectorPreferences: VectorPreferences,
cameraActivityResultLauncher: ActivityResultLauncher<Intent>,
cameraVideoActivityResultLauncher: ActivityResultLauncher<Intent>) {
PhotoOrVideoDialog(activity, vectorPreferences).show(object : PhotoOrVideoDialog.PhotoOrVideoDialogListener {
override fun takePhoto() {
captureUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(context, cameraActivityResultLauncher)
}
override fun takeVideo() {
captureUri = MultiPicker.get(MultiPicker.CAMERA_VIDEO).startWithExpectingFile(context, cameraVideoActivityResultLauncher)
}
})
}
/**
@ -141,7 +155,7 @@ class AttachmentsHelper(val context: Context, val callback: Callback) : Restorab
)
}
fun onPhotoResult() {
fun onCameraResult() {
captureUri?.let { captureUri ->
MultiPicker.get(MultiPicker.CAMERA)
.getTakenPhoto(context, captureUri)
@ -153,6 +167,18 @@ class AttachmentsHelper(val context: Context, val callback: Callback) : Restorab
}
}
fun onCameraVideoResult() {
captureUri?.let { captureUri ->
MultiPicker.get(MultiPicker.CAMERA_VIDEO)
.getTakenVideo(context, captureUri)
?.let {
callback.onContentAttachmentsReady(
listOf(it).map { it.toContentAttachmentData() }
)
}
}
}
fun onVideoResult(data: Intent?) {
callback.onContentAttachmentsReady(
MultiPicker.get(MultiPicker.VIDEO)

View file

@ -994,9 +994,15 @@ class RoomDetailFragment @Inject constructor(
}
}
private val attachmentPhotoActivityResultLauncher = registerStartForActivityResult {
private val attachmentCameraActivityResultLauncher = registerStartForActivityResult {
if (it.resultCode == Activity.RESULT_OK) {
attachmentsHelper.onPhotoResult()
attachmentsHelper.onCameraResult()
}
}
private val attachmentCameraVideoActivityResultLauncher = registerStartForActivityResult {
if (it.resultCode == Activity.RESULT_OK) {
attachmentsHelper.onCameraVideoResult()
}
}
@ -1989,7 +1995,12 @@ class RoomDetailFragment @Inject constructor(
private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) {
when (type) {
AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera(requireContext(), attachmentPhotoActivityResultLauncher)
AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera(
activity = requireActivity(),
vectorPreferences = vectorPreferences,
cameraActivityResultLauncher = attachmentCameraActivityResultLauncher,
cameraVideoActivityResultLauncher = attachmentCameraVideoActivityResultLauncher
)
AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile(attachmentFileActivityResultLauncher)
AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery(attachmentMediaActivityResultLauncher)
AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio(attachmentAudioActivityResultLauncher)

View file

@ -193,6 +193,13 @@ class VectorPreferences @Inject constructor(private val context: Context) {
private const val SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST = "SETTINGS_UNKNWON_DEVICE_DISMISSED_LIST"
private const val TAKE_PHOTO_VIDEO_MODE = "TAKE_PHOTO_VIDEO_MODE"
// Possible values for TAKE_PHOTO_VIDEO_MODE
const val TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK = 0
const val TAKE_PHOTO_VIDEO_MODE_PHOTO = 1
const val TAKE_PHOTO_VIDEO_MODE_VIDEO = 2
// Background sync modes
// some preferences keys must be kept after a logout
@ -948,4 +955,17 @@ class VectorPreferences @Inject constructor(private val context: Context) {
fun labsUseExperimentalRestricted(): Boolean {
return defaultPrefs.getBoolean(SETTINGS_LABS_USE_RESTRICTED_JOIN_RULE, false)
}
/*
* Photo / video picker
*/
fun getTakePhotoVideoMode(): Int {
return defaultPrefs.getInt(TAKE_PHOTO_VIDEO_MODE, TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK)
}
fun setTakePhotoVideoMode(mode: Int) {
return defaultPrefs.edit {
putInt(TAKE_PHOTO_VIDEO_MODE, mode)
}
}
}

View file

@ -23,6 +23,7 @@ import androidx.appcompat.app.AlertDialog
import androidx.core.view.children
import androidx.preference.Preference
import im.vector.app.R
import im.vector.app.core.dialogs.PhotoOrVideoDialog
import im.vector.app.core.extensions.restart
import im.vector.app.core.preference.VectorListPreference
import im.vector.app.core.preference.VectorPreference
@ -45,6 +46,9 @@ class VectorSettingsPreferencesFragment @Inject constructor(
private val textSizePreference by lazy {
findPreference<VectorPreference>(VectorPreferences.SETTINGS_INTERFACE_TEXT_SIZE_KEY)!!
}
private val takePhotoOrVideoPreference by lazy {
findPreference<VectorPreference>("SETTINGS_INTERFACE_TAKE_PHOTO_VIDEO")!!
}
override fun bindPref() {
// user interface preferences
@ -123,6 +127,28 @@ class VectorSettingsPreferencesFragment @Inject constructor(
false
}
}
// Take photo or video
updateTakePhotoOrVideoPreferenceSummary()
takePhotoOrVideoPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
PhotoOrVideoDialog(requireActivity(), vectorPreferences).showForSettings(object: PhotoOrVideoDialog.PhotoOrVideoDialogSettingsListener {
override fun onUpdated() {
updateTakePhotoOrVideoPreferenceSummary()
}
})
true
}
}
private fun updateTakePhotoOrVideoPreferenceSummary() {
takePhotoOrVideoPreference.summary = getString(
when (vectorPreferences.getTakePhotoVideoMode()) {
VectorPreferences.TAKE_PHOTO_VIDEO_MODE_PHOTO -> R.string.option_take_photo
VectorPreferences.TAKE_PHOTO_VIDEO_MODE_VIDEO -> R.string.option_take_video
/* VectorPreferences.TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK */
else -> R.string.option_always_ask
}
)
}
// ==============================================================================================================

View file

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/layout_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="?dialogPreferredPadding"
android:paddingTop="12dp"
android:paddingEnd="?dialogPreferredPadding"
android:paddingBottom="12dp">
<RadioGroup
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/dialog_photo_or_video_photo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/option_take_photo"
tools:checked="true" />
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/dialog_photo_or_video_video"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="180dp"
android:text="@string/option_take_video" />
<!-- Displayed only form the settings -->
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/dialog_photo_or_video_always_ask"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="180dp"
android:text="@string/option_always_ask"
android:visibility="gone"
tools:visibility="visible" />
</RadioGroup>
<!-- Displayed only form the timeline -->
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/dialog_photo_or_video_as_default"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/use_as_default_and_do_not_ask_again"
android:visibility="gone"
tools:visibility="visible" />
</LinearLayout>

View file

@ -575,6 +575,9 @@
<string name="option_take_photo_video">Take photo or video</string>
<string name="option_take_photo">Take photo</string>
<string name="option_take_video">Take video</string>
<string name="option_always_ask">Always ask</string>
<string name="use_as_default_and_do_not_ask_again">Use as default and do not ask again</string>
<!-- No sticker application dialog -->
<string name="no_sticker_application_dialog_content">You dont currently have any stickerpacks enabled.\n\nAdd some now?</string>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<im.vector.app.core.preference.VectorPreferenceCategory
android:key="SETTINGS_USER_INTERFACE_KEY"
@ -55,6 +56,12 @@
android:summary="@string/settings_show_emoji_keyboard_summary"
android:title="@string/settings_show_emoji_keyboard" />
<im.vector.app.core.preference.VectorPreference
android:key="SETTINGS_INTERFACE_TAKE_PHOTO_VIDEO"
android:persistent="false"
android:title="@string/option_take_photo_video"
tools:summary="@string/option_always_ask" />
</im.vector.app.core.preference.VectorPreferenceCategory>
<im.vector.app.core.preference.VectorPreferenceCategory android:title="@string/settings_category_timeline">