Skip to content

Commit

Permalink
Add option to record a video from the camera
Browse files Browse the repository at this point in the history
Replace #2411
  • Loading branch information
bmarty committed May 3, 2021
1 parent 30a54cf commit d9ffce7
Show file tree
Hide file tree
Showing 11 changed files with 404 additions and 12 deletions.
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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 */
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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")
}
}
Expand Down
118 changes: 118 additions & 0 deletions vector/src/main/java/im/vector/app/core/dialogs/PhotoOrVideoDialog.kt
Original file line number Diff line number Diff line change
@@ -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
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
})
}

/**
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}

Expand Down Expand Up @@ -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)
Expand Down
Loading

0 comments on commit d9ffce7

Please sign in to comment.