From 51b905fbba2cc9bdbf4595de4c414a2680050b2d Mon Sep 17 00:00:00 2001 From: sim Date: Thu, 28 Jul 2022 21:30:39 +0200 Subject: [PATCH 01/35] Add VideoCamera button for attachment --- .../app/core/dialogs/PhotoOrVideoDialog.kt | 120 ------------------ .../attachments/AttachmentTypeSelectorView.kt | 9 +- .../features/attachments/AttachmentsHelper.kt | 26 +--- .../home/room/detail/TimelineFragment.kt | 8 +- .../features/settings/VectorPreferences.kt | 29 ----- .../VectorSettingsPreferencesFragment.kt | 25 ---- .../drawable/ic_attachment_video_camera.xml | 13 ++ .../layout/view_attachment_type_selector.xml | 10 ++ vector/src/main/res/values/strings.xml | 1 + .../res/xml/vector_settings_preferences.xml | 6 - 10 files changed, 39 insertions(+), 208 deletions(-) delete mode 100644 vector/src/main/java/im/vector/app/core/dialogs/PhotoOrVideoDialog.kt create mode 100644 vector/src/main/res/drawable/ic_attachment_video_camera.xml diff --git a/vector/src/main/java/im/vector/app/core/dialogs/PhotoOrVideoDialog.kt b/vector/src/main/java/im/vector/app/core/dialogs/PhotoOrVideoDialog.kt deleted file mode 100644 index 9effa4c489f..00000000000 --- a/vector/src/main/java/im/vector/app/core/dialogs/PhotoOrVideoDialog.kt +++ /dev/null @@ -1,120 +0,0 @@ -/* - * 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.core.view.isVisible -import com.google.android.material.dialog.MaterialAlertDialogBuilder -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 - - MaterialAlertDialogBuilder(activity) - .setTitle(R.string.option_take_photo_video) - .setView(dialogLayout) - .setPositiveButton(R.string._continue) { _, _ -> - submit(views, vectorPreferences, listener) - } - .setNegativeButton(R.string.action_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 - - MaterialAlertDialogBuilder(activity) - .setTitle(R.string.option_take_photo_video) - .setView(dialogLayout) - .setPositiveButton(R.string.action_save) { _, _ -> - submitSettings(views) - listener.onUpdated() - } - .setNegativeButton(R.string.action_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 - } - ) - } -} diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt index c85c3aa6b5d..cb179ac4bea 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentTypeSelectorView.kt @@ -69,7 +69,8 @@ class AttachmentTypeSelectorView( contentView = inflater.inflate(R.layout.view_attachment_type_selector, null, false) views = ViewAttachmentTypeSelectorBinding.bind(contentView) views.attachmentGalleryButton.configure(Type.GALLERY) - views.attachmentCameraButton.configure(Type.CAMERA) + views.attachmentCameraButton.configure(Type.PHOTO_CAMERA) + views.attachmentVideoCameraButton.configure(Type.VIDEO_CAMERA) views.attachmentFileButton.configure(Type.FILE) views.attachmentStickersButton.configure(Type.STICKER) views.attachmentContactButton.configure(Type.CONTACT) @@ -127,7 +128,8 @@ class AttachmentTypeSelectorView( fun setAttachmentVisibility(type: Type, isVisible: Boolean) { when (type) { - Type.CAMERA -> views.attachmentCameraButton + Type.PHOTO_CAMERA -> views.attachmentCameraButton + Type.VIDEO_CAMERA -> views.attachmentVideoCameraButton Type.GALLERY -> views.attachmentGalleryButton Type.FILE -> views.attachmentFileButton Type.STICKER -> views.attachmentStickersButton @@ -215,7 +217,8 @@ class AttachmentTypeSelectorView( * The all possible types to pick with their required permissions and tooltip resource. */ enum class Type(val permissions: List, @StringRes val tooltipRes: Int) { - CAMERA(PERMISSIONS_FOR_TAKING_PHOTO, R.string.tooltip_attachment_photo), + PHOTO_CAMERA(PERMISSIONS_FOR_TAKING_PHOTO, R.string.tooltip_attachment_photo), + VIDEO_CAMERA(PERMISSIONS_FOR_TAKING_PHOTO, R.string.tooltip_attachment_video), GALLERY(PERMISSIONS_EMPTY, R.string.tooltip_attachment_gallery), FILE(PERMISSIONS_EMPTY, R.string.tooltip_attachment_file), STICKER(PERMISSIONS_EMPTY, R.string.tooltip_attachment_sticker), diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt index f8aa22f4181..bd8e1397664 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt @@ -15,16 +15,13 @@ */ 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.core.resources.BuildMeta -import im.vector.app.features.settings.VectorPreferences import im.vector.lib.multipicker.MultiPicker import org.matrix.android.sdk.api.session.content.ContentAttachmentData import timber.log.Timber @@ -92,23 +89,14 @@ class AttachmentsHelper( } /** - * Starts the process for handling image/video capture. Can open a dialog + * Starts the process for handling image/video capture. */ - fun openCamera( - activity: Activity, - vectorPreferences: VectorPreferences, - cameraActivityResultLauncher: ActivityResultLauncher, - cameraVideoActivityResultLauncher: ActivityResultLauncher - ) { - 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) - } - }) + fun openPhotoCamera(cameraActivityResultLauncher: ActivityResultLauncher) { + captureUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(context, cameraActivityResultLauncher) + } + + fun openVideoCamera(cameraVideoActivityResultLauncher: ActivityResultLauncher) { + captureUri = MultiPicker.get(MultiPicker.CAMERA_VIDEO).startWithExpectingFile(context, cameraVideoActivityResultLauncher) } /** diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index b5eb0608d49..e47958848f4 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -2616,12 +2616,8 @@ class TimelineFragment : private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) { when (type) { - AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera( - activity = requireActivity(), - vectorPreferences = vectorPreferences, - cameraActivityResultLauncher = attachmentCameraActivityResultLauncher, - cameraVideoActivityResultLauncher = attachmentCameraVideoActivityResultLauncher - ) + AttachmentTypeSelectorView.Type.PHOTO_CAMERA -> attachmentsHelper.openPhotoCamera(attachmentCameraActivityResultLauncher) + AttachmentTypeSelectorView.Type.VIDEO_CAMERA -> attachmentsHelper.openVideoCamera(attachmentCameraVideoActivityResultLauncher) AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile(attachmentFileActivityResultLauncher) AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery(attachmentMediaActivityResultLauncher) AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact(attachmentContactActivityResultLauncher) diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index cefbe64d9df..5e6fd852f96 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -207,8 +207,6 @@ class VectorPreferences @Inject constructor( private const val SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST = "SETTINGS_UNKNWON_DEVICE_DISMISSED_LIST" - private const val TAKE_PHOTO_VIDEO_MODE = "TAKE_PHOTO_VIDEO_MODE" - private const val SETTINGS_LABS_ENABLE_LIVE_LOCATION = "SETTINGS_LABS_ENABLE_LIVE_LOCATION" private const val SETTINGS_LABS_ENABLE_ELEMENT_CALL_PERMISSION_SHORTCUTS = "SETTINGS_LABS_ENABLE_ELEMENT_CALL_PERMISSION_SHORTCUTS" @@ -223,11 +221,6 @@ class VectorPreferences @Inject constructor( // This key will be used to enable user for displaying live user info or not. const val SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO = "SETTINGS_TIMELINE_SHOW_LIVE_SENDER_INFO" - // 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 @@ -447,15 +440,6 @@ class VectorPreferences @Inject constructor( return defaultPrefs.getBoolean(SETTINGS_SHOW_AVATAR_DISPLAY_NAME_CHANGES_MESSAGES_KEY, true) } - /** - * Tells the native camera to take a photo or record a video. - * - * @return true to use the native camera app to record video or take photo. - */ - fun useNativeCamera(): Boolean { - return defaultPrefs.getBoolean(SETTINGS_USE_NATIVE_CAMERA_PREFERENCE_KEY, false) - } - /** * Tells if the send voice feature is enabled. * @@ -1047,19 +1031,6 @@ class VectorPreferences @Inject constructor( return defaultPrefs.getBoolean(SETTINGS_PREF_SPACE_SHOW_ALL_ROOM_IN_HOME, 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) - } - } - fun labsEnableLiveLocation(): Boolean { return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_LIVE_LOCATION, false) } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt index 0bd5316b8f5..1ccc6d6f559 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt @@ -24,7 +24,6 @@ import androidx.preference.Preference import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint 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 @@ -54,9 +53,6 @@ class VectorSettingsPreferencesFragment : private val textSizePreference by lazy { findPreference(VectorPreferences.SETTINGS_INTERFACE_TEXT_SIZE_KEY)!! } - private val takePhotoOrVideoPreference by lazy { - findPreference("SETTINGS_INTERFACE_TAKE_PHOTO_VIDEO")!! - } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -162,27 +158,6 @@ class VectorSettingsPreferencesFragment : } } - // 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 - } - ) } // ============================================================================================================== diff --git a/vector/src/main/res/drawable/ic_attachment_video_camera.xml b/vector/src/main/res/drawable/ic_attachment_video_camera.xml new file mode 100644 index 00000000000..c8c33946789 --- /dev/null +++ b/vector/src/main/res/drawable/ic_attachment_video_camera.xml @@ -0,0 +1,13 @@ + + + + diff --git a/vector/src/main/res/layout/view_attachment_type_selector.xml b/vector/src/main/res/layout/view_attachment_type_selector.xml index c108dfd96e8..b2b68c1a395 100644 --- a/vector/src/main/res/layout/view_attachment_type_selector.xml +++ b/vector/src/main/res/layout/view_attachment_type_selector.xml @@ -93,6 +93,16 @@ android:src="@drawable/ic_attachment_camera" app:tint="?colorPrimary" /> + + Live location sharing Please note: this is a labs feature using a temporary implementation. This means you will not be able to delete your location history, and advanced users will be able to see your location history even after you stop sharing your live location with this room. Enable location sharing + Open camera for a video %d message removed diff --git a/vector/src/main/res/xml/vector_settings_preferences.xml b/vector/src/main/res/xml/vector_settings_preferences.xml index a3b8a3476cc..f7412486a62 100644 --- a/vector/src/main/res/xml/vector_settings_preferences.xml +++ b/vector/src/main/res/xml/vector_settings_preferences.xml @@ -66,12 +66,6 @@ android:summary="@string/settings_show_emoji_keyboard_summary" android:title="@string/settings_show_emoji_keyboard" /> - - From 59546b04d04eae88d4f61a978965132f7d8b22ed Mon Sep 17 00:00:00 2001 From: sim Date: Sat, 30 Jul 2022 16:36:29 +0200 Subject: [PATCH 02/35] Add internal Camera : take photo --- dependencies.gradle | 8 + dependencies_groups.gradle | 2 + .../im/vector/lib/multipicker/MultiPicker.kt | 2 + .../lib/multipicker/VectorCameraPicker.kt | 94 ++++++++ vector/build.gradle | 10 + vector/src/main/AndroidManifest.xml | 10 +- .../features/attachments/AttachmentsHelper.kt | 22 ++ .../camera/AttachmentsCameraActivity.kt | 56 +++++ .../camera/AttachmentsCameraFragment.kt | 204 ++++++++++++++++++ .../home/room/detail/TimelineFragment.kt | 8 +- .../layout/fragment_attachments_camera.xml | 58 +++++ .../res/menu/vector_attachments_camera.xml | 4 + vector/src/main/res/values/strings.xml | 2 + 13 files changed, 478 insertions(+), 2 deletions(-) create mode 100644 library/multipicker/src/main/java/im/vector/lib/multipicker/VectorCameraPicker.kt create mode 100644 vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraActivity.kt create mode 100644 vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt create mode 100644 vector/src/main/res/layout/fragment_attachments_camera.xml create mode 100644 vector/src/main/res/menu/vector_attachments_camera.xml diff --git a/dependencies.gradle b/dependencies.gradle index 7a9ed3f9315..4e63f646b97 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -53,6 +53,14 @@ ext.libs = [ 'activity' : "androidx.activity:activity:1.5.1", 'appCompat' : "androidx.appcompat:appcompat:1.4.2", 'biometric' : "androidx.biometric:biometric:1.1.0", + 'camera': [ + 'core' : "androidx.camera.camera-core:1.1.0", + 'camera2' : "androidx.camera.camera-camera2:1.1.0", + 'lifecycle' : "androidx.camera.camera-lifecycle:1.1.0", + 'video' : "androidx.camera.camera-video:1.1.0", + 'view' : "androidx.camera.camera-view:1.1.0", + 'extensions' : "androidx.camera.camera-extensions:1.1.0", + ], 'core' : "androidx.core:core-ktx:1.8.0", 'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1", 'exifinterface' : "androidx.exifinterface:exifinterface:1.3.3", diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index bcd737acc97..c5d22c24c95 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -40,8 +40,10 @@ ext.groups = [ ], mavenCentral: [ regex: [ + 'com\\.google\\.auto\\..*', ], group: [ + 'com.google.auto', 'ch.qos.logback', 'com.adevinta.android', 'com.airbnb.android', diff --git a/library/multipicker/src/main/java/im/vector/lib/multipicker/MultiPicker.kt b/library/multipicker/src/main/java/im/vector/lib/multipicker/MultiPicker.kt index 93773458869..726aab7aa3c 100644 --- a/library/multipicker/src/main/java/im/vector/lib/multipicker/MultiPicker.kt +++ b/library/multipicker/src/main/java/im/vector/lib/multipicker/MultiPicker.kt @@ -27,6 +27,7 @@ class MultiPicker private constructor() { val CONTACT by lazy { MultiPicker() } val CAMERA by lazy { MultiPicker() } val CAMERA_VIDEO by lazy { MultiPicker() } + val VECTOR_CAMERA by lazy { MultiPicker()} @Suppress("UNCHECKED_CAST") fun get(type: MultiPicker): T { @@ -39,6 +40,7 @@ class MultiPicker private constructor() { CONTACT -> ContactPicker() as T CAMERA -> CameraPicker() as T CAMERA_VIDEO -> CameraVideoPicker() as T + VECTOR_CAMERA -> VectorCameraPicker() as T else -> throw IllegalArgumentException("Unsupported type $type") } } diff --git a/library/multipicker/src/main/java/im/vector/lib/multipicker/VectorCameraPicker.kt b/library/multipicker/src/main/java/im/vector/lib/multipicker/VectorCameraPicker.kt new file mode 100644 index 00000000000..6a46bb8ef3c --- /dev/null +++ b/library/multipicker/src/main/java/im/vector/lib/multipicker/VectorCameraPicker.kt @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2022 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.net.Uri +import android.os.Parcelable +import android.provider.MediaStore +import androidx.activity.result.ActivityResultLauncher +import androidx.core.content.FileProvider +import im.vector.lib.multipicker.entity.MultiPickerImageType +import im.vector.lib.multipicker.entity.MultiPickerVideoType +import im.vector.lib.multipicker.utils.MediaType +import im.vector.lib.multipicker.utils.createTemporaryMediaFile +import im.vector.lib.multipicker.utils.toMultiPickerImageType +import im.vector.lib.multipicker.utils.toMultiPickerVideoType +import kotlinx.parcelize.Parcelize + +@Parcelize +data class CameraUris( + var photoUri: Uri?, + var videoUri: Uri?, +) : Parcelable + +class VectorCameraPicker { + + /** + * Start camera by using a ActivityResultLauncher. + * @return Uri of taken photo or null if the operation is cancelled. + */ + fun startWithExpectingFile(context: Context, activityResultLauncher: ActivityResultLauncher): CameraUris { + val uris = CameraUris( + photoUri = createMediaUri(context, MediaType.IMAGE), + videoUri = createMediaUri(context, MediaType.VIDEO), + ) + val intent = createIntent().apply { + putExtra(MediaStore.EXTRA_OUTPUT, uris) + } + activityResultLauncher.launch(intent) + return uris + } + + /** + * 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 getTakenPhoto(context: Context, photoUri: Uri): MultiPickerImageType? { + return photoUri.toMultiPickerImageType(context) + } + + /** + * Call this function from onActivityResult(int, int, Intent). + * @return Taken video 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? { + return videoUri.toMultiPickerVideoType(context) + } + + + private fun createIntent(): Intent { + return Intent(ACTION_VECTOR_CAMERA).apply { + action = ACTION_VECTOR_CAMERA + } + } + + private fun createMediaUri(context: Context, mediaType: MediaType): Uri { + val file = createTemporaryMediaFile(context, mediaType) + val authority = context.packageName + ".multipicker.fileprovider" + return FileProvider.getUriForFile(context, authority, file) + } + + companion object { + const val ACTION_VECTOR_CAMERA = "im.vector.app.action.VECTOR_CAMERA" + } +} diff --git a/vector/build.gradle b/vector/build.gradle index 83d322946ba..e702cdaf694 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -394,6 +394,16 @@ dependencies { implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.11.0" implementation libs.squareup.moshi + + // Camera + def camerax_version = "1.1.0" + implementation "androidx.camera:camera-core:${camerax_version}" + implementation "androidx.camera:camera-camera2:${camerax_version}" + implementation "androidx.camera:camera-lifecycle:${camerax_version}" + implementation "androidx.camera:camera-video:${camerax_version}" + implementation "androidx.camera:camera-view:${camerax_version}" + implementation "androidx.camera:camera-extensions:${camerax_version}" + kapt libs.squareup.moshiKotlin // Lifecycle diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index ea62aa1b58c..7c48e4a54f7 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -52,7 +52,7 @@ + + + + + + ) { + captureCameraUris = MultiPicker.get(MultiPicker.VECTOR_CAMERA).startWithExpectingFile(context, vectorCameraActivityResultLauncher) + } + /** * Starts the process for handling contact picking. */ @@ -158,6 +164,22 @@ class AttachmentsHelper( } } + fun onVectorCameraResult() { + captureCameraUris?.let { cameraUris -> + val multiPicker = MultiPicker.get(MultiPicker.VECTOR_CAMERA) + val media = cameraUris.photoUri?.let { photoUri -> + multiPicker.getTakenPhoto(context, photoUri) + } ?: run { + cameraUris.videoUri?.let { videoUri -> + multiPicker.getTakenVideo(context, videoUri) + } + } ?: return + callback.onContentAttachmentsReady( + listOf(media).map { it.toContentAttachmentData() } + ) + } + } + fun onCameraVideoResult() { captureUri?.let { captureUri -> MultiPicker.get(MultiPicker.CAMERA_VIDEO) diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraActivity.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraActivity.kt new file mode 100644 index 00000000000..b687cd2bcdf --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraActivity.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2022 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.features.attachments.camera + +import android.content.Intent +import android.provider.MediaStore +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.core.extensions.addFragment +import im.vector.app.core.platform.VectorBaseActivity +import im.vector.app.databinding.ActivitySimpleBinding +import im.vector.app.features.themes.ActivityOtherThemes +import im.vector.lib.multipicker.CameraUris + +@AndroidEntryPoint +class AttachmentsCameraActivity : VectorBaseActivity() { + override fun getOtherThemes() = ActivityOtherThemes.AttachmentsPreview + + override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater) + + override fun getCoordinatorLayout() = views.coordinatorLayout + + override fun initUiAndData() { + if (isFirstCreation()) { + val fragmentArgs: CameraUris = intent?.extras?.getParcelable(MediaStore.EXTRA_OUTPUT) ?: return + addFragment(views.simpleFragmentContainer, AttachmentsCameraFragment::class.java, fragmentArgs) + } + } + + fun setResultAndFinish(data: CameraUris) { + val resultIntent = Intent().apply { + putExtra(MediaStore.EXTRA_OUTPUT, data) + } + setResult(RESULT_OK, resultIntent) + finish() + } + + fun setErrorAndFinish() { + val resultIntent = Intent() + setResult(RESULT_CANCELED, resultIntent) + finish() + } +} diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt new file mode 100644 index 00000000000..6ee738af4d7 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2022 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.features.attachments.camera + +import android.Manifest +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.camera.core.CameraSelector +import androidx.camera.core.ImageCapture +import androidx.camera.core.ImageCaptureException +import androidx.camera.core.Preview +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.camera.video.Recorder +import androidx.camera.video.Recording +import androidx.camera.video.VideoCapture +import androidx.core.content.ContextCompat +import com.airbnb.mvrx.args +import dagger.hilt.android.AndroidEntryPoint +import im.vector.app.R +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.platform.VectorMenuProvider +import im.vector.app.databinding.FragmentAttachmentsCameraBinding +import im.vector.lib.multipicker.CameraUris +import timber.log.Timber +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +@AndroidEntryPoint +class AttachmentsCameraFragment : + VectorBaseFragment(), + VectorMenuProvider { + + private val cameraUris: CameraUris by args() + + private var imageCapture: ImageCapture? = null + + private var videoCapture: VideoCapture? = null + private var recording: Recording? = null + + private lateinit var cameraExecutor: ExecutorService + + private val requestPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { + if (allPermissionsGranted()) { + startCamera() + } else { + Toast.makeText(context, + "Permissions not granted by the user.", + Toast.LENGTH_SHORT).show() + activity?.finish() + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + // Request camera permissions + if (allPermissionsGranted()) { + startCamera() + } else { + requestPermissionLauncher.launch(REQUIRED_PERMISSIONS) + } + + views.attachmentsCameraImageAction.debouncedClicks { + takePhoto() + } + + views.attachmentsCameraVideoAction.debouncedClicks { + captureVideo() + } + + cameraExecutor = Executors.newSingleThreadExecutor() + } + + private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all { permission -> + context?.let { context -> + ContextCompat.checkSelfPermission(context, permission) + } == PackageManager.PERMISSION_GRANTED + } + + + private fun takePhoto() { + Timber.d("Taking a photo") + context?.let { context -> + // Get a stable reference of the modifiable image capture use case + val imageCapture = imageCapture ?: return + val uri = cameraUris.photoUri ?: return + val file = context.contentResolver.openOutputStream(uri) ?: return + + // Create output options object which contains file + metadata + val outputOptions = ImageCapture.OutputFileOptions + .Builder(file) + .build() + + // Set up image capture listener, which is triggered after photo has + // been taken + imageCapture.takePicture( + outputOptions, + ContextCompat.getMainExecutor(context), + object : ImageCapture.OnImageSavedCallback { + override fun onError(exc: ImageCaptureException) { + Timber.e("Photo capture failed: ${exc.message}", exc) + Toast.makeText(context, "An error occurred", Toast.LENGTH_SHORT).show() + (activity as? AttachmentsCameraActivity)?.setErrorAndFinish() + } + + override fun onImageSaved(output: ImageCapture.OutputFileResults){ + (activity as? AttachmentsCameraActivity)?.setResultAndFinish(cameraUris.apply { videoUri = null }) + } + } + ) + } + } + + + private fun captureVideo() { + Timber.d("Capturing Video")} + + private fun startCamera() { + Timber.d("Starting Camera") + context?.let { context -> + val cameraProviderFuture = ProcessCameraProvider.getInstance(context) + + cameraProviderFuture.addListener({ + // Used to bind the lifecycle of cameras to the lifecycle owner + val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() + + // Preview + val preview = Preview.Builder() + .build() + .also { + it.setSurfaceProvider(views.viewFinder.surfaceProvider) + } + + imageCapture = ImageCapture.Builder().build() + + // Select back camera as a default + val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA + + try { + // Unbind use cases before rebinding + cameraProvider.unbindAll() + + // Bind use cases to camera + cameraProvider.bindToLifecycle( + this, cameraSelector, preview, imageCapture + ) + } catch (exc: Exception) { + Timber.e("Use case binding failed", exc) + } + }, ContextCompat.getMainExecutor(context)) + } + } + + override fun onDestroyView() { + cameraExecutor.shutdown() + super.onDestroyView() + } + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentAttachmentsCameraBinding { + return FragmentAttachmentsCameraBinding.inflate(inflater, container, false) + } + + override fun getMenuRes() = R.menu.vector_attachments_camera + + override fun handleMenuItemSelected(item: MenuItem): Boolean { + return true + } + + companion object { + private val REQUIRED_PERMISSIONS = + mutableListOf ( + Manifest.permission.CAMERA, + Manifest.permission.RECORD_AUDIO + ).apply { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { + add(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + }.toTypedArray() + + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index e47958848f4..e9add9f05c2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -1381,6 +1381,12 @@ class TimelineFragment : } } + private val attachmentVectorCameraActivityResultLauncher = registerStartForActivityResult { + if (it.resultCode == Activity.RESULT_OK) { + attachmentsHelper.onVectorCameraResult() + } + } + private val contentAttachmentActivityResultLauncher = registerStartForActivityResult { activityResult -> val data = activityResult.data ?: return@registerStartForActivityResult if (activityResult.resultCode == Activity.RESULT_OK) { @@ -2616,7 +2622,7 @@ class TimelineFragment : private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) { when (type) { - AttachmentTypeSelectorView.Type.PHOTO_CAMERA -> attachmentsHelper.openPhotoCamera(attachmentCameraActivityResultLauncher) + AttachmentTypeSelectorView.Type.PHOTO_CAMERA -> attachmentsHelper.openVectorCamera(attachmentVectorCameraActivityResultLauncher) AttachmentTypeSelectorView.Type.VIDEO_CAMERA -> attachmentsHelper.openVideoCamera(attachmentCameraVideoActivityResultLauncher) AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile(attachmentFileActivityResultLauncher) AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery(attachmentMediaActivityResultLauncher) diff --git a/vector/src/main/res/layout/fragment_attachments_camera.xml b/vector/src/main/res/layout/fragment_attachments_camera.xml new file mode 100644 index 00000000000..e20f8b938d8 --- /dev/null +++ b/vector/src/main/res/layout/fragment_attachments_camera.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/menu/vector_attachments_camera.xml b/vector/src/main/res/menu/vector_attachments_camera.xml new file mode 100644 index 00000000000..b1313affa1c --- /dev/null +++ b/vector/src/main/res/menu/vector_attachments_camera.xml @@ -0,0 +1,4 @@ + + + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index c14c74ea352..346f07d49a1 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3197,6 +3197,8 @@ Please note: this is a labs feature using a temporary implementation. This means you will not be able to delete your location history, and advanced users will be able to see your location history even after you stop sharing your live location with this room. Enable location sharing Open camera for a video + Take a photo + Take a video %d message removed From 8fcc9c2386920267d0504f50eae321b3e2e8471c Mon Sep 17 00:00:00 2001 From: sim Date: Tue, 2 Aug 2022 12:41:47 +0200 Subject: [PATCH 03/35] Add internal Camera : record video --- .../features/attachments/AttachmentsHelper.kt | 29 +++--- .../camera/AttachmentsCameraFragment.kt | 98 ++++++++++++++++++- .../home/room/detail/TimelineFragment.kt | 6 +- 3 files changed, 112 insertions(+), 21 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt index 8321547f8ed..e06c46fd409 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt @@ -46,7 +46,6 @@ class AttachmentsHelper( // Capture path allows to handle camera image picking. It must be restored if the activity gets killed. private var captureUri: Uri? = null - private var captureCameraUris: CameraUris? = null // The pending type is set if we have to handle permission request. It must be restored if the activity gets killed. var pendingType: AttachmentTypeSelectorView.Type? = null @@ -102,7 +101,7 @@ class AttachmentsHelper( } fun openVectorCamera(vectorCameraActivityResultLauncher: ActivityResultLauncher) { - captureCameraUris = MultiPicker.get(MultiPicker.VECTOR_CAMERA).startWithExpectingFile(context, vectorCameraActivityResultLauncher) + MultiPicker.get(MultiPicker.VECTOR_CAMERA).startWithExpectingFile(context, vectorCameraActivityResultLauncher) } /** @@ -164,20 +163,18 @@ class AttachmentsHelper( } } - fun onVectorCameraResult() { - captureCameraUris?.let { cameraUris -> - val multiPicker = MultiPicker.get(MultiPicker.VECTOR_CAMERA) - val media = cameraUris.photoUri?.let { photoUri -> - multiPicker.getTakenPhoto(context, photoUri) - } ?: run { - cameraUris.videoUri?.let { videoUri -> - multiPicker.getTakenVideo(context, videoUri) - } - } ?: return - callback.onContentAttachmentsReady( - listOf(media).map { it.toContentAttachmentData() } - ) - } + fun onVectorCameraResult(cameraUris: CameraUris) { + val multiPicker = MultiPicker.get(MultiPicker.VECTOR_CAMERA) + val media = cameraUris.photoUri?.let { photoUri -> + multiPicker.getTakenPhoto(context, photoUri) + } ?: run { + cameraUris.videoUri?.let { videoUri -> + multiPicker.getTakenVideo(context, videoUri) + } + } ?: return + callback.onContentAttachmentsReady( + listOf(media).map { it.toContentAttachmentData() } + ) } fun onCameraVideoResult() { diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt index 6ee738af4d7..e7e4ecf8790 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt @@ -17,6 +17,7 @@ package im.vector.app.features.attachments.camera import android.Manifest +import android.annotation.SuppressLint import android.content.pm.PackageManager import android.os.Build import android.os.Bundle @@ -31,33 +32,48 @@ import androidx.camera.core.ImageCapture import androidx.camera.core.ImageCaptureException import androidx.camera.core.Preview import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.camera.video.FileOutputOptions +import androidx.camera.video.Quality +import androidx.camera.video.QualitySelector import androidx.camera.video.Recorder import androidx.camera.video.Recording import androidx.camera.video.VideoCapture +import androidx.camera.video.VideoRecordEvent import androidx.core.content.ContextCompat +import androidx.core.content.FileProvider +import androidx.core.content.PermissionChecker import com.airbnb.mvrx.args import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorMenuProvider +import im.vector.app.core.time.Clock import im.vector.app.databinding.FragmentAttachmentsCameraBinding import im.vector.lib.multipicker.CameraUris import timber.log.Timber +import java.io.File import java.util.concurrent.ExecutorService import java.util.concurrent.Executors +import javax.inject.Inject @AndroidEntryPoint class AttachmentsCameraFragment : VectorBaseFragment(), VectorMenuProvider { + @Inject lateinit var clock: Clock + private val cameraUris: CameraUris by args() + private lateinit var authority : String + private lateinit var storageDir : File + private var imageCapture: ImageCapture? = null private var videoCapture: VideoCapture? = null private var recording: Recording? = null + private lateinit var cameraExecutor: ExecutorService private val requestPermissionLauncher = registerForActivityResult( @@ -75,6 +91,8 @@ class AttachmentsCameraFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + authority = context?.packageName + ".fileProvider" + storageDir = context?.cacheDir.also { it?.mkdirs() }!! // Request camera permissions if (allPermissionsGranted()) { @@ -100,7 +118,6 @@ class AttachmentsCameraFragment : } == PackageManager.PERMISSION_GRANTED } - private fun takePhoto() { Timber.d("Taking a photo") context?.let { context -> @@ -134,10 +151,78 @@ class AttachmentsCameraFragment : } } - + @SuppressLint("UseCompatLoadingForDrawables") private fun captureVideo() { - Timber.d("Capturing Video")} + Timber.d("Capturing Video") + context?.let { context -> + val videoCapture = this.videoCapture ?: return + + views.attachmentsCameraImageAction.isEnabled = false + + val curRecording = recording + if (curRecording != null) { + // Stop the current recording session. + curRecording.stop() + recording = null + return + } + + val file = File.createTempFile( + "VID_${clock.epochMillis()}", + ".mp4", + storageDir + ) + + val outputUri = FileProvider.getUriForFile( + context, + authority, + file + ) + + val options = FileOutputOptions + .Builder(file) + .build() + recording = videoCapture.output + .prepareRecording(context, options) + .apply { + if (PermissionChecker.checkSelfPermission(context, + Manifest.permission.RECORD_AUDIO) == + PermissionChecker.PERMISSION_GRANTED) + { + withAudioEnabled() + } + } + .start(ContextCompat.getMainExecutor(context)) { recordEvent -> + when(recordEvent) { + is VideoRecordEvent.Start -> { + views.attachmentsCameraVideoAction.setImageDrawable( + context.getDrawable(R.drawable.ic_video_off) + ) + } + is VideoRecordEvent.Finalize -> { + if (!recordEvent.hasError()) { + Timber.d("Video capture succeeded: " + + "${recordEvent.outputResults.outputUri}") + (activity as? AttachmentsCameraActivity)?.setResultAndFinish( + cameraUris.apply { + videoUri = outputUri + photoUri = null + } + ) + } else { + recording?.close() + recording = null + Timber.e("Video capture ends with error: " + + "${recordEvent.error}") + (activity as? AttachmentsCameraActivity)?.setErrorAndFinish() + } + } + } + } + } + } + @SuppressLint("RestrictedApi") private fun startCamera() { Timber.d("Starting Camera") context?.let { context -> @@ -156,6 +241,11 @@ class AttachmentsCameraFragment : imageCapture = ImageCapture.Builder().build() + val recorder = Recorder.Builder() + .setQualitySelector(QualitySelector.from(Quality.HIGHEST)) + .build() + videoCapture = VideoCapture.withOutput(recorder) + // Select back camera as a default val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA @@ -165,7 +255,7 @@ class AttachmentsCameraFragment : // Bind use cases to camera cameraProvider.bindToLifecycle( - this, cameraSelector, preview, imageCapture + this, cameraSelector, preview, imageCapture, videoCapture ) } catch (exc: Exception) { Timber.e("Use case binding failed", exc) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index e9add9f05c2..c2d2b07c3ab 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -23,6 +23,7 @@ import android.content.res.Configuration import android.net.Uri import android.os.Build import android.os.Bundle +import android.provider.MediaStore import android.text.Spannable import android.text.format.DateUtils import android.text.method.LinkMovementMethod @@ -212,6 +213,7 @@ import im.vector.app.features.widgets.WidgetActivity import im.vector.app.features.widgets.WidgetArgs import im.vector.app.features.widgets.WidgetKind import im.vector.app.features.widgets.permissions.RoomWidgetPermissionBottomSheet +import im.vector.lib.multipicker.CameraUris import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.launchIn @@ -1383,7 +1385,9 @@ class TimelineFragment : private val attachmentVectorCameraActivityResultLauncher = registerStartForActivityResult { if (it.resultCode == Activity.RESULT_OK) { - attachmentsHelper.onVectorCameraResult() + it.data?.getParcelableExtra(MediaStore.EXTRA_OUTPUT)?.let { cameraUris -> + attachmentsHelper.onVectorCameraResult(cameraUris) + } } } From 59e0c1ec10fbd3fa61b3df8c77b7ad1ac70e8181 Mon Sep 17 00:00:00 2001 From: sim Date: Tue, 2 Aug 2022 13:14:40 +0200 Subject: [PATCH 04/35] Create tempfile in the fragment Use VectorCameraOutput --- .../lib/multipicker/VectorCameraPicker.kt | 30 +-------- .../features/attachments/AttachmentsHelper.kt | 17 +++-- .../camera/AttachmentsCameraActivity.kt | 6 +- .../camera/AttachmentsCameraFragment.kt | 67 +++++++++++++------ .../attachments/camera/VectorCameraOutput.kt | 32 +++++++++ .../home/room/detail/TimelineFragment.kt | 6 +- 6 files changed, 92 insertions(+), 66 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/attachments/camera/VectorCameraOutput.kt diff --git a/library/multipicker/src/main/java/im/vector/lib/multipicker/VectorCameraPicker.kt b/library/multipicker/src/main/java/im/vector/lib/multipicker/VectorCameraPicker.kt index 6a46bb8ef3c..20a131c9419 100644 --- a/library/multipicker/src/main/java/im/vector/lib/multipicker/VectorCameraPicker.kt +++ b/library/multipicker/src/main/java/im/vector/lib/multipicker/VectorCameraPicker.kt @@ -19,23 +19,11 @@ package im.vector.lib.multipicker import android.content.Context import android.content.Intent import android.net.Uri -import android.os.Parcelable -import android.provider.MediaStore import androidx.activity.result.ActivityResultLauncher -import androidx.core.content.FileProvider import im.vector.lib.multipicker.entity.MultiPickerImageType import im.vector.lib.multipicker.entity.MultiPickerVideoType -import im.vector.lib.multipicker.utils.MediaType -import im.vector.lib.multipicker.utils.createTemporaryMediaFile import im.vector.lib.multipicker.utils.toMultiPickerImageType import im.vector.lib.multipicker.utils.toMultiPickerVideoType -import kotlinx.parcelize.Parcelize - -@Parcelize -data class CameraUris( - var photoUri: Uri?, - var videoUri: Uri?, -) : Parcelable class VectorCameraPicker { @@ -43,16 +31,8 @@ class VectorCameraPicker { * Start camera by using a ActivityResultLauncher. * @return Uri of taken photo or null if the operation is cancelled. */ - fun startWithExpectingFile(context: Context, activityResultLauncher: ActivityResultLauncher): CameraUris { - val uris = CameraUris( - photoUri = createMediaUri(context, MediaType.IMAGE), - videoUri = createMediaUri(context, MediaType.VIDEO), - ) - val intent = createIntent().apply { - putExtra(MediaStore.EXTRA_OUTPUT, uris) - } - activityResultLauncher.launch(intent) - return uris + fun start(activityResultLauncher: ActivityResultLauncher) { + activityResultLauncher.launch(createIntent()) } /** @@ -82,12 +62,6 @@ class VectorCameraPicker { } } - private fun createMediaUri(context: Context, mediaType: MediaType): Uri { - val file = createTemporaryMediaFile(context, mediaType) - val authority = context.packageName + ".multipicker.fileprovider" - return FileProvider.getUriForFile(context, authority, file) - } - companion object { const val ACTION_VECTOR_CAMERA = "im.vector.app.action.VECTOR_CAMERA" } diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt index e06c46fd409..2abc0049a22 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt @@ -22,7 +22,8 @@ import android.os.Bundle import androidx.activity.result.ActivityResultLauncher import im.vector.app.core.platform.Restorable import im.vector.app.core.resources.BuildMeta -import im.vector.lib.multipicker.CameraUris +import im.vector.app.features.attachments.camera.MediaType +import im.vector.app.features.attachments.camera.VectorCameraOutput import im.vector.lib.multipicker.MultiPicker import org.matrix.android.sdk.api.session.content.ContentAttachmentData import timber.log.Timber @@ -101,7 +102,7 @@ class AttachmentsHelper( } fun openVectorCamera(vectorCameraActivityResultLauncher: ActivityResultLauncher) { - MultiPicker.get(MultiPicker.VECTOR_CAMERA).startWithExpectingFile(context, vectorCameraActivityResultLauncher) + MultiPicker.get(MultiPicker.VECTOR_CAMERA).start(vectorCameraActivityResultLauncher) } /** @@ -163,14 +164,12 @@ class AttachmentsHelper( } } - fun onVectorCameraResult(cameraUris: CameraUris) { + fun onVectorCameraResult(cameraOutput: VectorCameraOutput) { val multiPicker = MultiPicker.get(MultiPicker.VECTOR_CAMERA) - val media = cameraUris.photoUri?.let { photoUri -> - multiPicker.getTakenPhoto(context, photoUri) - } ?: run { - cameraUris.videoUri?.let { videoUri -> - multiPicker.getTakenVideo(context, videoUri) - } + val media = if (cameraOutput.type == MediaType.IMAGE) { + multiPicker.getTakenPhoto(context, cameraOutput.uri) + } else { + multiPicker.getTakenVideo(context, cameraOutput.uri) } ?: return callback.onContentAttachmentsReady( listOf(media).map { it.toContentAttachmentData() } diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraActivity.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraActivity.kt index b687cd2bcdf..ca3613b3704 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraActivity.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraActivity.kt @@ -23,7 +23,6 @@ import im.vector.app.core.extensions.addFragment import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.databinding.ActivitySimpleBinding import im.vector.app.features.themes.ActivityOtherThemes -import im.vector.lib.multipicker.CameraUris @AndroidEntryPoint class AttachmentsCameraActivity : VectorBaseActivity() { @@ -35,12 +34,11 @@ class AttachmentsCameraActivity : VectorBaseActivity() { override fun initUiAndData() { if (isFirstCreation()) { - val fragmentArgs: CameraUris = intent?.extras?.getParcelable(MediaStore.EXTRA_OUTPUT) ?: return - addFragment(views.simpleFragmentContainer, AttachmentsCameraFragment::class.java, fragmentArgs) + addFragment(views.simpleFragmentContainer, AttachmentsCameraFragment::class.java) } } - fun setResultAndFinish(data: CameraUris) { + fun setResultAndFinish(data: VectorCameraOutput) { val resultIntent = Intent().apply { putExtra(MediaStore.EXTRA_OUTPUT, data) } diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt index e7e4ecf8790..ceb03224d9e 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt @@ -18,7 +18,9 @@ package im.vector.app.features.attachments.camera import android.Manifest import android.annotation.SuppressLint +import android.content.Context import android.content.pm.PackageManager +import android.net.Uri import android.os.Build import android.os.Bundle import android.view.LayoutInflater @@ -42,14 +44,12 @@ import androidx.camera.video.VideoRecordEvent import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import androidx.core.content.PermissionChecker -import com.airbnb.mvrx.args import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.core.time.Clock import im.vector.app.databinding.FragmentAttachmentsCameraBinding -import im.vector.lib.multipicker.CameraUris import timber.log.Timber import java.io.File import java.util.concurrent.ExecutorService @@ -63,8 +63,6 @@ class AttachmentsCameraFragment : @Inject lateinit var clock: Clock - private val cameraUris: CameraUris by args() - private lateinit var authority : String private lateinit var storageDir : File @@ -123,8 +121,9 @@ class AttachmentsCameraFragment : context?.let { context -> // Get a stable reference of the modifiable image capture use case val imageCapture = imageCapture ?: return - val uri = cameraUris.photoUri ?: return - val file = context.contentResolver.openOutputStream(uri) ?: return + + val file = createTempFile(MediaType.IMAGE) + val outputUri = getUri(context, file) // Create output options object which contains file + metadata val outputOptions = ImageCapture.OutputFileOptions @@ -144,7 +143,12 @@ class AttachmentsCameraFragment : } override fun onImageSaved(output: ImageCapture.OutputFileResults){ - (activity as? AttachmentsCameraActivity)?.setResultAndFinish(cameraUris.apply { videoUri = null }) + (activity as? AttachmentsCameraActivity)?.setResultAndFinish( + VectorCameraOutput( + type = MediaType.IMAGE, + uri = outputUri + ) + ) } } ) @@ -167,17 +171,8 @@ class AttachmentsCameraFragment : return } - val file = File.createTempFile( - "VID_${clock.epochMillis()}", - ".mp4", - storageDir - ) - - val outputUri = FileProvider.getUriForFile( - context, - authority, - file - ) + val file = createTempFile(MediaType.VIDEO) + val outputUri = getUri(context, file) val options = FileOutputOptions .Builder(file) @@ -204,10 +199,10 @@ class AttachmentsCameraFragment : Timber.d("Video capture succeeded: " + "${recordEvent.outputResults.outputUri}") (activity as? AttachmentsCameraActivity)?.setResultAndFinish( - cameraUris.apply { - videoUri = outputUri - photoUri = null - } + VectorCameraOutput( + type = MediaType.VIDEO, + uri = outputUri + ) ) } else { recording?.close() @@ -279,6 +274,34 @@ class AttachmentsCameraFragment : return true } + private fun createTempFile(type: MediaType): File { + var prefix = "" + var suffix = "" + when (type) { + MediaType.IMAGE -> { + prefix = "IMG_" + suffix = ".jpg" + } + MediaType.VIDEO -> { + prefix = "VID_" + suffix = ".mp4" + } + } + return File.createTempFile( + "$prefix${clock.epochMillis()}", + suffix, + storageDir + ) + } + + private fun getUri(context: Context, file: File): Uri { + return FileProvider.getUriForFile( + context, + authority, + file + ) + } + companion object { private val REQUIRED_PERMISSIONS = mutableListOf ( diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/VectorCameraOutput.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/VectorCameraOutput.kt new file mode 100644 index 00000000000..5512e4212d3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/VectorCameraOutput.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022 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.features.attachments.camera + +import android.net.Uri +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +enum class MediaType{ + IMAGE, + VIDEO +} + +@Parcelize +data class VectorCameraOutput ( + val type: MediaType, + val uri: Uri, +) : Parcelable diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index c2d2b07c3ab..8d1e9fc31ae 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -136,6 +136,7 @@ import im.vector.app.features.attachments.AttachmentTypeSelectorView import im.vector.app.features.attachments.AttachmentsHelper import im.vector.app.features.attachments.ContactAttachment import im.vector.app.features.attachments.ShareIntentHandler +import im.vector.app.features.attachments.camera.VectorCameraOutput import im.vector.app.features.attachments.preview.AttachmentsPreviewActivity import im.vector.app.features.attachments.preview.AttachmentsPreviewArgs import im.vector.app.features.attachments.toGroupedContentAttachmentData @@ -213,7 +214,6 @@ import im.vector.app.features.widgets.WidgetActivity import im.vector.app.features.widgets.WidgetArgs import im.vector.app.features.widgets.WidgetKind import im.vector.app.features.widgets.permissions.RoomWidgetPermissionBottomSheet -import im.vector.lib.multipicker.CameraUris import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.launchIn @@ -1385,8 +1385,8 @@ class TimelineFragment : private val attachmentVectorCameraActivityResultLauncher = registerStartForActivityResult { if (it.resultCode == Activity.RESULT_OK) { - it.data?.getParcelableExtra(MediaStore.EXTRA_OUTPUT)?.let { cameraUris -> - attachmentsHelper.onVectorCameraResult(cameraUris) + it.data?.getParcelableExtra(MediaStore.EXTRA_OUTPUT)?.let { cameraOutput -> + attachmentsHelper.onVectorCameraResult(cameraOutput) } } } From 6b4fef9b53a0ae7f35a345a25d52e144f0c77716 Mon Sep 17 00:00:00 2001 From: sim Date: Fri, 5 Aug 2022 10:14:59 +0200 Subject: [PATCH 05/35] Change capture mode in camera view --- .../camera/AttachmentsCameraFragment.kt | 52 ++++++++++++++++--- .../layout/fragment_attachments_camera.xml | 34 +++++++++--- vector/src/main/res/values/strings.xml | 2 + 3 files changed, 73 insertions(+), 15 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt index ceb03224d9e..b3614da66b0 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt @@ -67,7 +67,6 @@ class AttachmentsCameraFragment : private lateinit var storageDir : File private var imageCapture: ImageCapture? = null - private var videoCapture: VideoCapture? = null private var recording: Recording? = null @@ -99,12 +98,12 @@ class AttachmentsCameraFragment : requestPermissionLauncher.launch(REQUIRED_PERMISSIONS) } - views.attachmentsCameraImageAction.debouncedClicks { - takePhoto() + views.attachmentsCameraCaptureAction.debouncedClicks { + capture() } - views.attachmentsCameraVideoAction.debouncedClicks { - captureVideo() + views.attachmentsCameraChangeAction.debouncedClicks { + changeCaptureMode() } cameraExecutor = Executors.newSingleThreadExecutor() @@ -116,6 +115,43 @@ class AttachmentsCameraFragment : } == PackageManager.PERMISSION_GRANTED } + private fun changeCaptureMode() { + when (captureMode) { + MediaType.IMAGE -> { + captureMode = MediaType.VIDEO + views.attachmentsCameraCaptureAction.setImageDrawable( + context?.getDrawable(R.drawable.ic_video) + ) + views.attachmentsCameraChangeAction.apply { + setImageDrawable( + context?.getDrawable(R.drawable.ic_camera_plain) + ) + contentDescription = getString(R.string.attachment_camera_photo) + } + } + MediaType.VIDEO -> { + captureMode = MediaType.IMAGE + views.attachmentsCameraCaptureAction.setImageDrawable( + context?.getDrawable(R.drawable.ic_camera_plain) + ) + views.attachmentsCameraChangeAction.apply { + setImageDrawable( + context?.getDrawable(R.drawable.ic_video) + ) + contentDescription = getString(R.string.attachment_camera_video) + } + } + } + } + + private fun capture() { + when (captureMode) { + MediaType.IMAGE -> takePhoto() + MediaType.VIDEO -> captureVideo() + } + + } + private fun takePhoto() { Timber.d("Taking a photo") context?.let { context -> @@ -161,7 +197,8 @@ class AttachmentsCameraFragment : context?.let { context -> val videoCapture = this.videoCapture ?: return - views.attachmentsCameraImageAction.isEnabled = false + views.attachmentsCameraChangeAction.isEnabled = false + views.attachmentsCameraFlip.isEnabled = false val curRecording = recording if (curRecording != null) { @@ -190,7 +227,7 @@ class AttachmentsCameraFragment : .start(ContextCompat.getMainExecutor(context)) { recordEvent -> when(recordEvent) { is VideoRecordEvent.Start -> { - views.attachmentsCameraVideoAction.setImageDrawable( + views.attachmentsCameraCaptureAction.setImageDrawable( context.getDrawable(R.drawable.ic_video_off) ) } @@ -303,6 +340,7 @@ class AttachmentsCameraFragment : } companion object { + private var captureMode = MediaType.IMAGE private val REQUIRED_PERMISSIONS = mutableListOf ( Manifest.permission.CAMERA, diff --git a/vector/src/main/res/layout/fragment_attachments_camera.xml b/vector/src/main/res/layout/fragment_attachments_camera.xml index e20f8b938d8..c0db3653db0 100644 --- a/vector/src/main/res/layout/fragment_attachments_camera.xml +++ b/vector/src/main/res/layout/fragment_attachments_camera.xml @@ -11,35 +11,53 @@ android:layout_height="match_parent" /> + + + android:contentDescription="@string/attachment_camera_flip" /> + app:layout_constraintGuide_percent=".2" /> + app:layout_constraintGuide_percent=".8" /> diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 346f07d49a1..d4f4c8a4f27 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3199,6 +3199,8 @@ Open camera for a video Take a photo Take a video + Capture + Change camera %d message removed From 764ef5315e02d10e0b57cdc58f4a500fa7ab6269 Mon Sep 17 00:00:00 2001 From: sim Date: Fri, 5 Aug 2022 10:48:37 +0200 Subject: [PATCH 06/35] Change camera in camera view --- .../camera/AttachmentsCameraFragment.kt | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt index b3614da66b0..681c947dab8 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt @@ -106,6 +106,10 @@ class AttachmentsCameraFragment : changeCaptureMode() } + views.attachmentsCameraFlip.debouncedClicks { + changeLensFacing() + } + cameraExecutor = Executors.newSingleThreadExecutor() } @@ -115,6 +119,7 @@ class AttachmentsCameraFragment : } == PackageManager.PERMISSION_GRANTED } + @SuppressLint("UseCompatLoadingForDrawables") private fun changeCaptureMode() { when (captureMode) { MediaType.IMAGE -> { @@ -144,6 +149,15 @@ class AttachmentsCameraFragment : } } + private fun changeLensFacing() { + cameraSelector = if (cameraSelector == CameraSelector.DEFAULT_BACK_CAMERA) { + CameraSelector.DEFAULT_FRONT_CAMERA + } else { + CameraSelector.DEFAULT_BACK_CAMERA + } + startCamera() + } + private fun capture() { when (captureMode) { MediaType.IMAGE -> takePhoto() @@ -254,7 +268,6 @@ class AttachmentsCameraFragment : } } - @SuppressLint("RestrictedApi") private fun startCamera() { Timber.d("Starting Camera") context?.let { context -> @@ -278,9 +291,6 @@ class AttachmentsCameraFragment : .build() videoCapture = VideoCapture.withOutput(recorder) - // Select back camera as a default - val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA - try { // Unbind use cases before rebinding cameraProvider.unbindAll() @@ -341,6 +351,7 @@ class AttachmentsCameraFragment : companion object { private var captureMode = MediaType.IMAGE + private var cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA private val REQUIRED_PERMISSIONS = mutableListOf ( Manifest.permission.CAMERA, From a215fe239563bf36f0c30f3d85ad674511316b6c Mon Sep 17 00:00:00 2001 From: sim Date: Fri, 5 Aug 2022 10:54:13 +0200 Subject: [PATCH 07/35] Restore capture mode in camera view --- .../camera/AttachmentsCameraFragment.kt | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt index 681c947dab8..ae550fdc1b4 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt @@ -98,6 +98,8 @@ class AttachmentsCameraFragment : requestPermissionLauncher.launch(REQUIRED_PERMISSIONS) } + setButtons() + views.attachmentsCameraCaptureAction.debouncedClicks { capture() } @@ -119,11 +121,18 @@ class AttachmentsCameraFragment : } == PackageManager.PERMISSION_GRANTED } - @SuppressLint("UseCompatLoadingForDrawables") private fun changeCaptureMode() { + captureMode = when (captureMode) { + MediaType.IMAGE -> MediaType.VIDEO + MediaType.VIDEO -> MediaType.IMAGE + } + setButtons() + } + + @SuppressLint("UseCompatLoadingForDrawables") + private fun setButtons() { when (captureMode) { - MediaType.IMAGE -> { - captureMode = MediaType.VIDEO + MediaType.VIDEO -> { views.attachmentsCameraCaptureAction.setImageDrawable( context?.getDrawable(R.drawable.ic_video) ) @@ -134,8 +143,7 @@ class AttachmentsCameraFragment : contentDescription = getString(R.string.attachment_camera_photo) } } - MediaType.VIDEO -> { - captureMode = MediaType.IMAGE + MediaType.IMAGE -> { views.attachmentsCameraCaptureAction.setImageDrawable( context?.getDrawable(R.drawable.ic_camera_plain) ) From e9167093a7e39b7320603cd0af7388541eb97e30 Mon Sep 17 00:00:00 2001 From: sim Date: Fri, 5 Aug 2022 11:39:22 +0200 Subject: [PATCH 08/35] Add gesture to zoom in camera view --- .../camera/AttachmentsCameraFragment.kt | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt index ae550fdc1b4..b359290d2f1 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt @@ -25,10 +25,12 @@ import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.MenuItem +import android.view.ScaleGestureDetector import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts +import androidx.camera.core.Camera import androidx.camera.core.CameraSelector import androidx.camera.core.ImageCapture import androidx.camera.core.ImageCaptureException @@ -70,9 +72,17 @@ class AttachmentsCameraFragment : private var videoCapture: VideoCapture? = null private var recording: Recording? = null - + private lateinit var camera: Camera private lateinit var cameraExecutor: ExecutorService + private val gestureListener = object : ScaleGestureDetector.SimpleOnScaleGestureListener() { + override fun onScale(detector: ScaleGestureDetector): Boolean { + val scale = camera.cameraInfo.zoomState.value!!.zoomRatio * detector.scaleFactor + camera.cameraControl.setZoomRatio(scale) + return true + } + } + private val requestPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions() ) { @@ -86,6 +96,7 @@ class AttachmentsCameraFragment : } } + @SuppressLint("ClickableViewAccessibility") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) authority = context?.packageName + ".fileProvider" @@ -111,7 +122,12 @@ class AttachmentsCameraFragment : views.attachmentsCameraFlip.debouncedClicks { changeLensFacing() } + val scaleGestureDetector = ScaleGestureDetector(context, gestureListener) + views.root.setOnTouchListener { _, event -> + scaleGestureDetector.onTouchEvent(event) + return@setOnTouchListener true + } cameraExecutor = Executors.newSingleThreadExecutor() } @@ -292,7 +308,9 @@ class AttachmentsCameraFragment : it.setSurfaceProvider(views.viewFinder.surfaceProvider) } - imageCapture = ImageCapture.Builder().build() + imageCapture = ImageCapture.Builder() + .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) + .build() val recorder = Recorder.Builder() .setQualitySelector(QualitySelector.from(Quality.HIGHEST)) @@ -304,7 +322,7 @@ class AttachmentsCameraFragment : cameraProvider.unbindAll() // Bind use cases to camera - cameraProvider.bindToLifecycle( + camera = cameraProvider.bindToLifecycle( this, cameraSelector, preview, imageCapture, videoCapture ) } catch (exc: Exception) { From f56a8fefefeb11d14262792d562333953b7fe5c4 Mon Sep 17 00:00:00 2001 From: sim Date: Fri, 5 Aug 2022 12:04:08 +0200 Subject: [PATCH 09/35] Remove intent action for camera activity --- .../vector/lib/multipicker/VectorCameraPicker.kt | 16 +++------------- vector/src/main/AndroidManifest.xml | 8 +------- .../features/attachments/AttachmentsHelper.kt | 7 ++++++- 3 files changed, 10 insertions(+), 21 deletions(-) diff --git a/library/multipicker/src/main/java/im/vector/lib/multipicker/VectorCameraPicker.kt b/library/multipicker/src/main/java/im/vector/lib/multipicker/VectorCameraPicker.kt index 20a131c9419..25af37b20ed 100644 --- a/library/multipicker/src/main/java/im/vector/lib/multipicker/VectorCameraPicker.kt +++ b/library/multipicker/src/main/java/im/vector/lib/multipicker/VectorCameraPicker.kt @@ -31,8 +31,9 @@ class VectorCameraPicker { * Start camera by using a ActivityResultLauncher. * @return Uri of taken photo or null if the operation is cancelled. */ - fun start(activityResultLauncher: ActivityResultLauncher) { - activityResultLauncher.launch(createIntent()) + fun start(context: Context, activityResultLauncher: ActivityResultLauncher, targetClass: Class<*>) { + val intent = Intent(context, targetClass) + activityResultLauncher.launch(intent) } /** @@ -54,15 +55,4 @@ class VectorCameraPicker { fun getTakenVideo(context: Context, videoUri: Uri): MultiPickerVideoType? { return videoUri.toMultiPickerVideoType(context) } - - - private fun createIntent(): Intent { - return Intent(ACTION_VECTOR_CAMERA).apply { - action = ACTION_VECTOR_CAMERA - } - } - - companion object { - const val ACTION_VECTOR_CAMERA = "im.vector.app.action.VECTOR_CAMERA" - } } diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 7c48e4a54f7..c9dff2beb95 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -291,13 +291,7 @@ android:name=".features.attachments.preview.AttachmentsPreviewActivity" android:theme="@style/Theme.Vector.Black.AttachmentsPreview" /> - - - - - + android:theme="@style/Theme.Vector.Black.AttachmentsPreview" /> ) { - MultiPicker.get(MultiPicker.VECTOR_CAMERA).start(vectorCameraActivityResultLauncher) + MultiPicker.get(MultiPicker.VECTOR_CAMERA).start( + context, + vectorCameraActivityResultLauncher, + AttachmentsCameraActivity::class.java + ) } /** From 90e922b075585b0edb6c5598ce7fd656081e6c56 Mon Sep 17 00:00:00 2001 From: sim Date: Sun, 7 Aug 2022 11:20:26 +0200 Subject: [PATCH 10/35] Set the built-in camera as a lab preference --- .../app/features/home/room/detail/TimelineFragment.kt | 11 ++++++++++- .../vector/app/features/settings/VectorPreferences.kt | 6 +++++- vector/src/main/res/values/strings.xml | 1 + vector/src/main/res/xml/vector_settings_labs.xml | 5 +++++ 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 8d1e9fc31ae..564a22ce014 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -1582,6 +1582,9 @@ class TimelineFragment : attachmentTypeSelector.setAttachmentVisibility( AttachmentTypeSelectorView.Type.POLL, !isThreadTimeLine() ) + attachmentTypeSelector.setAttachmentVisibility( + AttachmentTypeSelectorView.Type.VIDEO_CAMERA, !vectorPreferences.builtinCameraIsEnabled() + ) } attachmentTypeSelector.show(views.composerLayout.views.attachmentButton) } @@ -2626,7 +2629,13 @@ class TimelineFragment : private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) { when (type) { - AttachmentTypeSelectorView.Type.PHOTO_CAMERA -> attachmentsHelper.openVectorCamera(attachmentVectorCameraActivityResultLauncher) + AttachmentTypeSelectorView.Type.PHOTO_CAMERA -> { + if (vectorPreferences.builtinCameraIsEnabled()) { + attachmentsHelper.openVectorCamera(attachmentVectorCameraActivityResultLauncher) + } else { + attachmentsHelper.openPhotoCamera(attachmentCameraActivityResultLauncher) + } + } AttachmentTypeSelectorView.Type.VIDEO_CAMERA -> attachmentsHelper.openVideoCamera(attachmentCameraVideoActivityResultLauncher) AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile(attachmentFileActivityResultLauncher) AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery(attachmentMediaActivityResultLauncher) diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index 5e6fd852f96..6339b1b61e0 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -158,11 +158,11 @@ class VectorPreferences @Inject constructor( const val SETTINGS_USER_REFUSED_LAZY_LOADING_PREFERENCE_KEY = "SETTINGS_USER_REFUSED_LAZY_LOADING_PREFERENCE_KEY" const val SETTINGS_DATA_SAVE_MODE_PREFERENCE_KEY = "SETTINGS_DATA_SAVE_MODE_PREFERENCE_KEY" private const val SETTINGS_USE_JITSI_CONF_PREFERENCE_KEY = "SETTINGS_USE_JITSI_CONF_PREFERENCE_KEY" - private const val SETTINGS_USE_NATIVE_CAMERA_PREFERENCE_KEY = "SETTINGS_USE_NATIVE_CAMERA_PREFERENCE_KEY" private const val SETTINGS_ENABLE_SEND_VOICE_FEATURE_PREFERENCE_KEY = "SETTINGS_ENABLE_SEND_VOICE_FEATURE_PREFERENCE_KEY" const val SETTINGS_LABS_ALLOW_EXTENDED_LOGS = "SETTINGS_LABS_ALLOW_EXTENDED_LOGS" const val SETTINGS_LABS_AUTO_REPORT_UISI = "SETTINGS_LABS_AUTO_REPORT_UISI" + const val SETTINGS_LABS_ENABLE_BUILTIN_CAMERA = "SETTINGS_LABS_ENABLE_BUILTIN_CAMERA" const val SETTINGS_PREF_SPACE_SHOW_ALL_ROOM_IN_HOME = "SETTINGS_PREF_SPACE_SHOW_ALL_ROOM_IN_HOME" private const val SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY = "SETTINGS_DEVELOPER_MODE_PREFERENCE_KEY" @@ -361,6 +361,10 @@ class VectorPreferences @Inject constructor( return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_LATEX_MATHS, false) } + fun builtinCameraIsEnabled(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_BUILTIN_CAMERA, false) + } + fun failFast(): Boolean { return buildMeta.isDebug || (developerMode() && defaultPrefs.getBoolean(SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY, false)) } diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index d4f4c8a4f27..84a6589b205 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3201,6 +3201,7 @@ Take a video Capture Change camera + Enable Built-in Camera %d message removed diff --git a/vector/src/main/res/xml/vector_settings_labs.xml b/vector/src/main/res/xml/vector_settings_labs.xml index 80b71a1f753..3f081487466 100644 --- a/vector/src/main/res/xml/vector_settings_labs.xml +++ b/vector/src/main/res/xml/vector_settings_labs.xml @@ -59,6 +59,11 @@ android:title="@string/labs_enable_latex_maths" /> + + Date: Sun, 7 Aug 2022 12:51:57 +0200 Subject: [PATCH 11/35] Control flash in camera view --- .../camera/AttachmentsCameraFragment.kt | 56 ++++++++++++++++++- .../src/main/res/drawable/ic_flash_auto.xml | 5 ++ vector/src/main/res/drawable/ic_flash_off.xml | 5 ++ vector/src/main/res/drawable/ic_flash_on.xml | 5 ++ .../layout/fragment_attachments_camera.xml | 14 +++++ vector/src/main/res/values/strings.xml | 3 + 6 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 vector/src/main/res/drawable/ic_flash_auto.xml create mode 100644 vector/src/main/res/drawable/ic_flash_off.xml create mode 100644 vector/src/main/res/drawable/ic_flash_on.xml diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt index b359290d2f1..aa03f8e3add 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt @@ -47,6 +47,7 @@ import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import androidx.core.content.PermissionChecker import dagger.hilt.android.AndroidEntryPoint +import androidx.core.view.isVisible import im.vector.app.R import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorMenuProvider @@ -109,7 +110,8 @@ class AttachmentsCameraFragment : requestPermissionLauncher.launch(REQUIRED_PERMISSIONS) } - setButtons() + setCaptureModeButtons() + setFlashButton() views.attachmentsCameraCaptureAction.debouncedClicks { capture() @@ -122,6 +124,11 @@ class AttachmentsCameraFragment : views.attachmentsCameraFlip.debouncedClicks { changeLensFacing() } + + views.attachmentsCameraFlash.debouncedClicks { + rotateFlashMode() + } + val scaleGestureDetector = ScaleGestureDetector(context, gestureListener) views.root.setOnTouchListener { _, event -> @@ -142,11 +149,11 @@ class AttachmentsCameraFragment : MediaType.IMAGE -> MediaType.VIDEO MediaType.VIDEO -> MediaType.IMAGE } - setButtons() + setCaptureModeButtons() } @SuppressLint("UseCompatLoadingForDrawables") - private fun setButtons() { + private fun setCaptureModeButtons() { when (captureMode) { MediaType.VIDEO -> { views.attachmentsCameraCaptureAction.setImageDrawable( @@ -171,6 +178,36 @@ class AttachmentsCameraFragment : } } } + setFlashButton() + } + + @SuppressLint("UseCompatLoadingForDrawables") + private fun setFlashButton() { + if (captureMode == MediaType.VIDEO || cameraSelector != CameraSelector.DEFAULT_BACK_CAMERA) { + views.attachmentsCameraFlash.isVisible = false + } else { + views.attachmentsCameraFlash.apply { + isVisible = true + setImageDrawable( + context?.getDrawable( + when (flashMode) { + ImageCapture.FLASH_MODE_AUTO -> R.drawable.ic_flash_auto + ImageCapture.FLASH_MODE_OFF -> R.drawable.ic_flash_off + ImageCapture.FLASH_MODE_ON -> R.drawable.ic_flash_on + else -> R.drawable.ic_flash_auto + } + ) + ) + contentDescription = context?.getString( + when (flashMode) { + ImageCapture.FLASH_MODE_AUTO -> R.string.attachment_camera_disable_flash + ImageCapture.FLASH_MODE_OFF -> R.string.attachment_camera_enable_flash + ImageCapture.FLASH_MODE_ON -> R.string.attachment_camera_auto_flash + else -> R.string.attachment_camera_disable_flash + } + ) + } + } } private fun changeLensFacing() { @@ -179,6 +216,7 @@ class AttachmentsCameraFragment : } else { CameraSelector.DEFAULT_BACK_CAMERA } + setFlashButton() startCamera() } @@ -187,7 +225,16 @@ class AttachmentsCameraFragment : MediaType.IMAGE -> takePhoto() MediaType.VIDEO -> captureVideo() } + } + private fun rotateFlashMode() { + flashMode = when (flashMode) { + ImageCapture.FLASH_MODE_AUTO -> ImageCapture.FLASH_MODE_OFF + ImageCapture.FLASH_MODE_OFF -> ImageCapture.FLASH_MODE_ON + ImageCapture.FLASH_MODE_ON -> ImageCapture.FLASH_MODE_AUTO + else -> ImageCapture.FLASH_MODE_OFF + } + setFlashButton() } private fun takePhoto() { @@ -196,6 +243,8 @@ class AttachmentsCameraFragment : // Get a stable reference of the modifiable image capture use case val imageCapture = imageCapture ?: return + imageCapture.flashMode = flashMode + val file = createTempFile(MediaType.IMAGE) val outputUri = getUri(context, file) @@ -378,6 +427,7 @@ class AttachmentsCameraFragment : companion object { private var captureMode = MediaType.IMAGE private var cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA + private var flashMode = ImageCapture.FLASH_MODE_AUTO private val REQUIRED_PERMISSIONS = mutableListOf ( Manifest.permission.CAMERA, diff --git a/vector/src/main/res/drawable/ic_flash_auto.xml b/vector/src/main/res/drawable/ic_flash_auto.xml new file mode 100644 index 00000000000..4a74797d0d3 --- /dev/null +++ b/vector/src/main/res/drawable/ic_flash_auto.xml @@ -0,0 +1,5 @@ + + + diff --git a/vector/src/main/res/drawable/ic_flash_off.xml b/vector/src/main/res/drawable/ic_flash_off.xml new file mode 100644 index 00000000000..b940fd93834 --- /dev/null +++ b/vector/src/main/res/drawable/ic_flash_off.xml @@ -0,0 +1,5 @@ + + + diff --git a/vector/src/main/res/drawable/ic_flash_on.xml b/vector/src/main/res/drawable/ic_flash_on.xml new file mode 100644 index 00000000000..a6c0c44b308 --- /dev/null +++ b/vector/src/main/res/drawable/ic_flash_on.xml @@ -0,0 +1,5 @@ + + + diff --git a/vector/src/main/res/layout/fragment_attachments_camera.xml b/vector/src/main/res/layout/fragment_attachments_camera.xml index c0db3653db0..7de46972588 100644 --- a/vector/src/main/res/layout/fragment_attachments_camera.xml +++ b/vector/src/main/res/layout/fragment_attachments_camera.xml @@ -52,6 +52,20 @@ app:layout_constraintEnd_toStartOf="@id/vertical_rightline" android:contentDescription="@string/attachment_camera_flip" /> + + Capture Change camera Enable Built-in Camera + Disable flash + Enable flash + Set flash automatically %d message removed From 994523118138cfadf0afb6b80456738d77456c50 Mon Sep 17 00:00:00 2001 From: sim Date: Sun, 7 Aug 2022 14:43:45 +0200 Subject: [PATCH 12/35] Lint --- .../im/vector/lib/multipicker/MultiPicker.kt | 2 +- vector/src/main/AndroidManifest.xml | 2 +- .../camera/AttachmentsCameraFragment.kt | 83 +++++++++---------- .../attachments/camera/VectorCameraOutput.kt | 4 +- .../VectorSettingsPreferencesFragment.kt | 1 - .../main/res/layout/dialog_photo_or_video.xml | 52 ------------ .../layout/fragment_attachments_camera.xml | 34 ++------ vector/src/main/res/values/strings.xml | 9 +- 8 files changed, 57 insertions(+), 130 deletions(-) delete mode 100644 vector/src/main/res/layout/dialog_photo_or_video.xml diff --git a/library/multipicker/src/main/java/im/vector/lib/multipicker/MultiPicker.kt b/library/multipicker/src/main/java/im/vector/lib/multipicker/MultiPicker.kt index 726aab7aa3c..2ee5cb0793b 100644 --- a/library/multipicker/src/main/java/im/vector/lib/multipicker/MultiPicker.kt +++ b/library/multipicker/src/main/java/im/vector/lib/multipicker/MultiPicker.kt @@ -27,7 +27,7 @@ class MultiPicker private constructor() { val CONTACT by lazy { MultiPicker() } val CAMERA by lazy { MultiPicker() } val CAMERA_VIDEO by lazy { MultiPicker() } - val VECTOR_CAMERA by lazy { MultiPicker()} + val VECTOR_CAMERA by lazy { MultiPicker() } @Suppress("UNCHECKED_CAST") fun get(type: MultiPicker): T { diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index c9dff2beb95..d1d29be8d48 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -52,7 +52,7 @@ ? = null @@ -240,41 +240,41 @@ class AttachmentsCameraFragment : private fun takePhoto() { Timber.d("Taking a photo") context?.let { context -> - // Get a stable reference of the modifiable image capture use case - val imageCapture = imageCapture ?: return - - imageCapture.flashMode = flashMode - - val file = createTempFile(MediaType.IMAGE) - val outputUri = getUri(context, file) - - // Create output options object which contains file + metadata - val outputOptions = ImageCapture.OutputFileOptions - .Builder(file) - .build() - - // Set up image capture listener, which is triggered after photo has - // been taken - imageCapture.takePicture( - outputOptions, - ContextCompat.getMainExecutor(context), - object : ImageCapture.OnImageSavedCallback { - override fun onError(exc: ImageCaptureException) { - Timber.e("Photo capture failed: ${exc.message}", exc) - Toast.makeText(context, "An error occurred", Toast.LENGTH_SHORT).show() - (activity as? AttachmentsCameraActivity)?.setErrorAndFinish() - } + // Get a stable reference of the modifiable image capture use case + val imageCapture = imageCapture ?: return - override fun onImageSaved(output: ImageCapture.OutputFileResults){ - (activity as? AttachmentsCameraActivity)?.setResultAndFinish( - VectorCameraOutput( - type = MediaType.IMAGE, - uri = outputUri - ) - ) + imageCapture.flashMode = flashMode + + val file = createTempFile(MediaType.IMAGE) + val outputUri = getUri(context, file) + + // Create output options object which contains file + metadata + val outputOptions = ImageCapture.OutputFileOptions + .Builder(file) + .build() + + // Set up image capture listener, which is triggered after photo has + // been taken + imageCapture.takePicture( + outputOptions, + ContextCompat.getMainExecutor(context), + object : ImageCapture.OnImageSavedCallback { + override fun onError(exc: ImageCaptureException) { + Timber.e("Photo capture failed: ${exc.message}", exc) + Toast.makeText(context, "An error occurred", Toast.LENGTH_SHORT).show() + (activity as? AttachmentsCameraActivity)?.setErrorAndFinish() + } + + override fun onImageSaved(output: ImageCapture.OutputFileResults) { + (activity as? AttachmentsCameraActivity)?.setResultAndFinish( + VectorCameraOutput( + type = MediaType.IMAGE, + uri = outputUri + ) + ) + } } - } - ) + ) } } @@ -304,15 +304,15 @@ class AttachmentsCameraFragment : recording = videoCapture.output .prepareRecording(context, options) .apply { - if (PermissionChecker.checkSelfPermission(context, - Manifest.permission.RECORD_AUDIO) == - PermissionChecker.PERMISSION_GRANTED) - { + if (PermissionChecker.checkSelfPermission( + context, + Manifest.permission.RECORD_AUDIO + ) == PermissionChecker.PERMISSION_GRANTED) { withAudioEnabled() } } .start(ContextCompat.getMainExecutor(context)) { recordEvent -> - when(recordEvent) { + when (recordEvent) { is VideoRecordEvent.Start -> { views.attachmentsCameraCaptureAction.setImageDrawable( context.getDrawable(R.drawable.ic_video_off) @@ -429,7 +429,7 @@ class AttachmentsCameraFragment : private var cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA private var flashMode = ImageCapture.FLASH_MODE_AUTO private val REQUIRED_PERMISSIONS = - mutableListOf ( + mutableListOf( Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO ).apply { @@ -437,6 +437,5 @@ class AttachmentsCameraFragment : add(Manifest.permission.WRITE_EXTERNAL_STORAGE) } }.toTypedArray() - } } diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/VectorCameraOutput.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/VectorCameraOutput.kt index 5512e4212d3..300a7da77d7 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/camera/VectorCameraOutput.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/VectorCameraOutput.kt @@ -20,13 +20,13 @@ import android.net.Uri import android.os.Parcelable import kotlinx.parcelize.Parcelize -enum class MediaType{ +enum class MediaType { IMAGE, VIDEO } @Parcelize -data class VectorCameraOutput ( +data class VectorCameraOutput( val type: MediaType, val uri: Uri, ) : Parcelable diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt index 1ccc6d6f559..ac1182944db 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt @@ -157,7 +157,6 @@ class VectorSettingsPreferencesFragment : false } } - } // ============================================================================================================== diff --git a/vector/src/main/res/layout/dialog_photo_or_video.xml b/vector/src/main/res/layout/dialog_photo_or_video.xml deleted file mode 100644 index 31b71142377..00000000000 --- a/vector/src/main/res/layout/dialog_photo_or_video.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_attachments_camera.xml b/vector/src/main/res/layout/fragment_attachments_camera.xml index 7de46972588..db794e856e7 100644 --- a/vector/src/main/res/layout/fragment_attachments_camera.xml +++ b/vector/src/main/res/layout/fragment_attachments_camera.xml @@ -1,7 +1,6 @@ @@ -21,8 +20,8 @@ android:scaleY="0.7" android:src="@drawable/ic_video" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toStartOf="@+id/vertical_leftline" - app:layout_constraintStart_toStartOf="@+id/vertical_leftline" /> + app:layout_constraintEnd_toStartOf="@id/attachmentsCameraCaptureAction" + app:layout_constraintStart_toStartOf="parent" /> - - - - - - diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 1b52215b365..2ed29549d37 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -511,12 +511,15 @@ Send files Send sticker - Take photo or video + + Take photo or video Take photo Take video - Always ask + + Always ask - Use as default and do not ask again + + Use as default and do not ask again You don’t currently have any stickerpacks enabled.\n\nAdd some now? From 0e3a8a190e48ce741da132d46e9b7fb946342330 Mon Sep 17 00:00:00 2001 From: sim Date: Sun, 7 Aug 2022 15:21:39 +0200 Subject: [PATCH 13/35] Add progress indicator when taking a photo --- .../attachments/camera/AttachmentsCameraFragment.kt | 1 + .../main/res/layout/fragment_attachments_camera.xml | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt index 8ed9bba10ae..a524e215930 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt @@ -240,6 +240,7 @@ class AttachmentsCameraFragment : private fun takePhoto() { Timber.d("Taking a photo") context?.let { context -> + views.attachmentsCameraLoading.isVisible = true // Get a stable reference of the modifiable image capture use case val imageCapture = imageCapture ?: return diff --git a/vector/src/main/res/layout/fragment_attachments_camera.xml b/vector/src/main/res/layout/fragment_attachments_camera.xml index db794e856e7..d53628b126a 100644 --- a/vector/src/main/res/layout/fragment_attachments_camera.xml +++ b/vector/src/main/res/layout/fragment_attachments_camera.xml @@ -9,6 +9,19 @@ android:layout_width="match_parent" android:layout_height="match_parent" /> + + Date: Sun, 7 Aug 2022 15:45:08 +0200 Subject: [PATCH 14/35] Add changelog detail Signed-off-by: sim --- changelog.d/6763.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6763.feature diff --git a/changelog.d/6763.feature b/changelog.d/6763.feature new file mode 100644 index 00000000000..826c8f516c1 --- /dev/null +++ b/changelog.d/6763.feature @@ -0,0 +1 @@ +Add an in-app camera From 4856fc616383f6b557fd009bfdd2eb3f809b00f6 Mon Sep 17 00:00:00 2001 From: sim Date: Sat, 27 Aug 2022 17:12:38 +0200 Subject: [PATCH 15/35] Add support for orientation --- .../camera/AttachmentsCameraFragment.kt | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt index a524e215930..f5d5b2d69d9 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt @@ -25,7 +25,9 @@ import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.MenuItem +import android.view.OrientationEventListener import android.view.ScaleGestureDetector +import android.view.Surface import android.view.View import android.view.ViewGroup import android.widget.Toast @@ -72,6 +74,7 @@ class AttachmentsCameraFragment : private var imageCapture: ImageCapture? = null private var videoCapture: VideoCapture? = null private var recording: Recording? = null + private var rotation = Surface.ROTATION_0 private lateinit var camera: Camera private lateinit var cameraExecutor: ExecutorService @@ -97,6 +100,34 @@ class AttachmentsCameraFragment : } } + private val orientationEventListener by lazy { + object : OrientationEventListener(context) { + @SuppressLint("RestrictedApi") + override fun onOrientationChanged(orientation: Int) { + val rotation = when (orientation) { + in 45 until 135 -> Surface.ROTATION_270 + in 135 until 225 -> Surface.ROTATION_180 + in 225 until 315 -> Surface.ROTATION_90 + else -> Surface.ROTATION_0 + } + if (rotation == this@AttachmentsCameraFragment.rotation) return + this@AttachmentsCameraFragment.rotation = rotation + + rotateButtons( + when (rotation) { + Surface.ROTATION_270 -> 270F + Surface.ROTATION_180 -> 180F + Surface.ROTATION_90 -> 90F + else -> 0F + } + ) + imageCapture?.targetRotation = rotation + videoCapture?.targetRotation = rotation + } + } + } + + @SuppressLint("ClickableViewAccessibility") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -112,6 +143,7 @@ class AttachmentsCameraFragment : setCaptureModeButtons() setFlashButton() + orientationEventListener.enable() views.attachmentsCameraCaptureAction.debouncedClicks { capture() @@ -138,6 +170,11 @@ class AttachmentsCameraFragment : cameraExecutor = Executors.newSingleThreadExecutor() } + override fun onStop() { + super.onStop() + orientationEventListener.disable() + } + private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all { permission -> context?.let { context -> ContextCompat.checkSelfPermission(context, permission) @@ -181,6 +218,17 @@ class AttachmentsCameraFragment : setFlashButton() } + private fun rotateButtons(rotation: Float) { + arrayOf( + views.attachmentsCameraFlip, + views.attachmentsCameraFlash, + views.attachmentsCameraChangeAction, + views.attachmentsCameraCaptureAction, + ).forEach { + it.rotation = rotation + } + } + @SuppressLint("UseCompatLoadingForDrawables") private fun setFlashButton() { if (captureMode == MediaType.VIDEO || cameraSelector != CameraSelector.DEFAULT_BACK_CAMERA) { From ec1e5e2cc5e160ce41445364ad7951340371aeaf Mon Sep 17 00:00:00 2001 From: sim Date: Mon, 29 Aug 2022 20:56:54 +0200 Subject: [PATCH 16/35] Camera: Follow the MVI pattern --- .../app/core/di/MavericksViewModelModule.kt | 6 + .../camera/AttachmentsCameraAction.kt | 35 ++ .../camera/AttachmentsCameraFragment.kt | 381 ++++++------------ .../camera/AttachmentsCameraModel.kt | 259 ++++++++++++ .../camera/AttachmentsCameraState.kt | 29 ++ .../camera/AttachmentsCameraViewEvents.kt | 26 ++ 6 files changed, 471 insertions(+), 265 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraAction.kt create mode 100644 vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraModel.kt create mode 100644 vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraState.kt create mode 100644 vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraViewEvents.kt diff --git a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt index b21b4778e36..39ae516fc3f 100644 --- a/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/MavericksViewModelModule.kt @@ -22,6 +22,7 @@ import dagger.hilt.InstallIn import dagger.multibindings.IntoMap import im.vector.app.features.analytics.accountdata.AnalyticsAccountDataViewModel import im.vector.app.features.analytics.ui.consent.AnalyticsConsentViewModel +import im.vector.app.features.attachments.camera.AttachmentsCameraModel import im.vector.app.features.auth.ReAuthViewModel import im.vector.app.features.call.VectorCallViewModel import im.vector.app.features.call.conference.JitsiCallViewModel @@ -605,6 +606,11 @@ interface MavericksViewModelModule { @MavericksViewModelKey(VectorAttachmentViewerViewModel::class) fun vectorAttachmentViewerViewModelFactory(factory: VectorAttachmentViewerViewModel.Factory): MavericksAssistedViewModelFactory<*, *> + @Binds + @IntoMap + @MavericksViewModelKey(AttachmentsCameraModel::class) + fun vectorAttachmentsCameraModelFactory(factory: AttachmentsCameraModel.Factory): MavericksAssistedViewModelFactory<*, *> + @Binds @IntoMap @MavericksViewModelKey(LiveLocationMapViewModel::class) diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraAction.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraAction.kt new file mode 100644 index 00000000000..49c315d8ccf --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraAction.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2022 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.features.attachments.camera + +import android.content.Context +import androidx.camera.core.ImageCapture +import androidx.camera.video.Recorder +import androidx.camera.video.VideoCapture +import im.vector.app.core.platform.VectorViewModelAction + +sealed class AttachmentsCameraAction : VectorViewModelAction { + object ChangeLensFacing : AttachmentsCameraAction() + object ChangeCaptureMode : AttachmentsCameraAction() + object RotateFlashMode : AttachmentsCameraAction() + data class SetRotation(val rotation: Int) : AttachmentsCameraAction() + data class Capture( + val context: Context, + val imageCapture: ImageCapture? = null, + val videoCapture: VideoCapture? = null + ) : AttachmentsCameraAction() +} diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt index f5d5b2d69d9..b6315db818e 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt @@ -18,9 +18,7 @@ package im.vector.app.features.attachments.camera import android.Manifest import android.annotation.SuppressLint -import android.content.Context import android.content.pm.PackageManager -import android.net.Uri import android.os.Build import android.os.Bundle import android.view.LayoutInflater @@ -35,28 +33,23 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.camera.core.Camera import androidx.camera.core.CameraSelector import androidx.camera.core.ImageCapture -import androidx.camera.core.ImageCaptureException import androidx.camera.core.Preview import androidx.camera.lifecycle.ProcessCameraProvider -import androidx.camera.video.FileOutputOptions import androidx.camera.video.Quality import androidx.camera.video.QualitySelector import androidx.camera.video.Recorder -import androidx.camera.video.Recording import androidx.camera.video.VideoCapture -import androidx.camera.video.VideoRecordEvent import androidx.core.content.ContextCompat -import androidx.core.content.FileProvider -import androidx.core.content.PermissionChecker import dagger.hilt.android.AndroidEntryPoint import androidx.core.view.isVisible +import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.withState import im.vector.app.R import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.core.time.Clock import im.vector.app.databinding.FragmentAttachmentsCameraBinding import timber.log.Timber -import java.io.File import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import javax.inject.Inject @@ -67,14 +60,11 @@ class AttachmentsCameraFragment : VectorMenuProvider { @Inject lateinit var clock: Clock - - private lateinit var authority: String - private lateinit var storageDir: File + private val viewModel: AttachmentsCameraModel by activityViewModel() private var imageCapture: ImageCapture? = null private var videoCapture: VideoCapture? = null - private var recording: Recording? = null - private var rotation = Surface.ROTATION_0 + private var currentLens: Int? = null private lateinit var camera: Camera private lateinit var cameraExecutor: ExecutorService @@ -102,37 +92,39 @@ class AttachmentsCameraFragment : private val orientationEventListener by lazy { object : OrientationEventListener(context) { - @SuppressLint("RestrictedApi") override fun onOrientationChanged(orientation: Int) { - val rotation = when (orientation) { - in 45 until 135 -> Surface.ROTATION_270 - in 135 until 225 -> Surface.ROTATION_180 - in 225 until 315 -> Surface.ROTATION_90 - else -> Surface.ROTATION_0 - } - if (rotation == this@AttachmentsCameraFragment.rotation) return - this@AttachmentsCameraFragment.rotation = rotation - - rotateButtons( - when (rotation) { - Surface.ROTATION_270 -> 270F - Surface.ROTATION_180 -> 180F - Surface.ROTATION_90 -> 90F - else -> 0F + withState(viewModel) { state -> + val rotation = when (orientation) { + in 45 until 135 -> Surface.ROTATION_270 + in 135 until 225 -> Surface.ROTATION_180 + in 225 until 315 -> Surface.ROTATION_90 + else -> Surface.ROTATION_0 + } + if (rotation != state.rotation) { + viewModel.handle(AttachmentsCameraAction.SetRotation(rotation)) } - ) - imageCapture?.targetRotation = rotation - videoCapture?.targetRotation = rotation + } } } } - - @SuppressLint("ClickableViewAccessibility") + @SuppressLint("ClickableViewAccessibility", "UseCompatLoadingForDrawables") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - authority = context?.packageName + ".fileProvider" - storageDir = context?.cacheDir.also { it?.mkdirs() }!! + viewModel.observeViewEvents { + when (it) { + AttachmentsCameraViewEvents.StartRecording -> { + views.attachmentsCameraCaptureAction.setImageDrawable( + context?.getDrawable(R.drawable.ic_video_off) + ) + views.attachmentsCameraChangeAction.isEnabled = false + views.attachmentsCameraFlip.isEnabled = false + } + AttachmentsCameraViewEvents.TakePhoto -> views.attachmentsCameraLoading.isVisible = true + AttachmentsCameraViewEvents.SetErrorAndFinish -> (activity as AttachmentsCameraActivity).setErrorAndFinish() + is AttachmentsCameraViewEvents.SetResultAndFinish -> (activity as AttachmentsCameraActivity).setResultAndFinish(it.vectorCameraOutput) + } + } // Request camera permissions if (allPermissionsGranted()) { @@ -141,24 +133,24 @@ class AttachmentsCameraFragment : requestPermissionLauncher.launch(REQUIRED_PERMISSIONS) } - setCaptureModeButtons() - setFlashButton() orientationEventListener.enable() views.attachmentsCameraCaptureAction.debouncedClicks { - capture() + context?.let { + viewModel.handle(AttachmentsCameraAction.Capture(it, imageCapture, videoCapture)) + } } views.attachmentsCameraChangeAction.debouncedClicks { - changeCaptureMode() + viewModel.handle(AttachmentsCameraAction.ChangeCaptureMode) } views.attachmentsCameraFlip.debouncedClicks { - changeLensFacing() + viewModel.handle(AttachmentsCameraAction.ChangeLensFacing) } views.attachmentsCameraFlash.debouncedClicks { - rotateFlashMode() + viewModel.handle(AttachmentsCameraAction.RotateFlashMode) } val scaleGestureDetector = ScaleGestureDetector(context, gestureListener) @@ -170,6 +162,16 @@ class AttachmentsCameraFragment : cameraExecutor = Executors.newSingleThreadExecutor() } + override fun invalidate() { + // Request camera permissions + if (allPermissionsGranted()) { + startCamera() + } + setCaptureModeButtons() + setFlashButton() + setRotation() + } + override fun onStop() { super.onStop() orientationEventListener.disable() @@ -181,218 +183,96 @@ class AttachmentsCameraFragment : } == PackageManager.PERMISSION_GRANTED } - private fun changeCaptureMode() { - captureMode = when (captureMode) { - MediaType.IMAGE -> MediaType.VIDEO - MediaType.VIDEO -> MediaType.IMAGE - } - setCaptureModeButtons() - } - @SuppressLint("UseCompatLoadingForDrawables") private fun setCaptureModeButtons() { - when (captureMode) { - MediaType.VIDEO -> { - views.attachmentsCameraCaptureAction.setImageDrawable( - context?.getDrawable(R.drawable.ic_video) - ) - views.attachmentsCameraChangeAction.apply { - setImageDrawable( - context?.getDrawable(R.drawable.ic_camera_plain) + withState(viewModel) { state -> + when (state.captureMode) { + MediaType.VIDEO -> { + views.attachmentsCameraCaptureAction.setImageDrawable( + context?.getDrawable(R.drawable.ic_video) ) - contentDescription = getString(R.string.attachment_camera_photo) + views.attachmentsCameraChangeAction.apply { + setImageDrawable( + context?.getDrawable(R.drawable.ic_camera_plain) + ) + contentDescription = getString(R.string.attachment_camera_photo) + } } - } - MediaType.IMAGE -> { - views.attachmentsCameraCaptureAction.setImageDrawable( - context?.getDrawable(R.drawable.ic_camera_plain) - ) - views.attachmentsCameraChangeAction.apply { - setImageDrawable( - context?.getDrawable(R.drawable.ic_video) + MediaType.IMAGE -> { + views.attachmentsCameraCaptureAction.setImageDrawable( + context?.getDrawable(R.drawable.ic_camera_plain) ) - contentDescription = getString(R.string.attachment_camera_video) + views.attachmentsCameraChangeAction.apply { + setImageDrawable( + context?.getDrawable(R.drawable.ic_video) + ) + contentDescription = getString(R.string.attachment_camera_video) + } } } } - setFlashButton() } - private fun rotateButtons(rotation: Float) { - arrayOf( - views.attachmentsCameraFlip, - views.attachmentsCameraFlash, - views.attachmentsCameraChangeAction, - views.attachmentsCameraCaptureAction, - ).forEach { - it.rotation = rotation + @SuppressLint("RestrictedApi") + private fun setRotation() { + withState(viewModel) { state -> + arrayOf( + views.attachmentsCameraFlip, + views.attachmentsCameraFlash, + views.attachmentsCameraChangeAction, + views.attachmentsCameraCaptureAction, + ).forEach { + it.rotation = when (state.rotation) { + Surface.ROTATION_270 -> 270F + Surface.ROTATION_180 -> 180F + Surface.ROTATION_90 -> 90F + else -> 0F + } + } + imageCapture?.targetRotation = state.rotation + videoCapture?.targetRotation = state.rotation } } @SuppressLint("UseCompatLoadingForDrawables") private fun setFlashButton() { - if (captureMode == MediaType.VIDEO || cameraSelector != CameraSelector.DEFAULT_BACK_CAMERA) { - views.attachmentsCameraFlash.isVisible = false - } else { - views.attachmentsCameraFlash.apply { - isVisible = true - setImageDrawable( - context?.getDrawable( - when (flashMode) { - ImageCapture.FLASH_MODE_AUTO -> R.drawable.ic_flash_auto - ImageCapture.FLASH_MODE_OFF -> R.drawable.ic_flash_off - ImageCapture.FLASH_MODE_ON -> R.drawable.ic_flash_on - else -> R.drawable.ic_flash_auto - } - ) - ) - contentDescription = context?.getString( - when (flashMode) { - ImageCapture.FLASH_MODE_AUTO -> R.string.attachment_camera_disable_flash - ImageCapture.FLASH_MODE_OFF -> R.string.attachment_camera_enable_flash - ImageCapture.FLASH_MODE_ON -> R.string.attachment_camera_auto_flash - else -> R.string.attachment_camera_disable_flash - } - ) - } - } - } - - private fun changeLensFacing() { - cameraSelector = if (cameraSelector == CameraSelector.DEFAULT_BACK_CAMERA) { - CameraSelector.DEFAULT_FRONT_CAMERA - } else { - CameraSelector.DEFAULT_BACK_CAMERA - } - setFlashButton() - startCamera() - } - - private fun capture() { - when (captureMode) { - MediaType.IMAGE -> takePhoto() - MediaType.VIDEO -> captureVideo() - } - } - - private fun rotateFlashMode() { - flashMode = when (flashMode) { - ImageCapture.FLASH_MODE_AUTO -> ImageCapture.FLASH_MODE_OFF - ImageCapture.FLASH_MODE_OFF -> ImageCapture.FLASH_MODE_ON - ImageCapture.FLASH_MODE_ON -> ImageCapture.FLASH_MODE_AUTO - else -> ImageCapture.FLASH_MODE_OFF - } - setFlashButton() - } - - private fun takePhoto() { - Timber.d("Taking a photo") - context?.let { context -> - views.attachmentsCameraLoading.isVisible = true - // Get a stable reference of the modifiable image capture use case - val imageCapture = imageCapture ?: return - - imageCapture.flashMode = flashMode - - val file = createTempFile(MediaType.IMAGE) - val outputUri = getUri(context, file) - - // Create output options object which contains file + metadata - val outputOptions = ImageCapture.OutputFileOptions - .Builder(file) - .build() - - // Set up image capture listener, which is triggered after photo has - // been taken - imageCapture.takePicture( - outputOptions, - ContextCompat.getMainExecutor(context), - object : ImageCapture.OnImageSavedCallback { - override fun onError(exc: ImageCaptureException) { - Timber.e("Photo capture failed: ${exc.message}", exc) - Toast.makeText(context, "An error occurred", Toast.LENGTH_SHORT).show() - (activity as? AttachmentsCameraActivity)?.setErrorAndFinish() - } - - override fun onImageSaved(output: ImageCapture.OutputFileResults) { - (activity as? AttachmentsCameraActivity)?.setResultAndFinish( - VectorCameraOutput( - type = MediaType.IMAGE, - uri = outputUri - ) + withState(viewModel) { state -> + if (state.captureMode == MediaType.VIDEO || state.cameraSelector != CameraSelector.DEFAULT_BACK_CAMERA) { + views.attachmentsCameraFlash.isVisible = false + } else { + views.attachmentsCameraFlash.apply { + isVisible = true + setImageDrawable( + context?.getDrawable( + when (state.flashMode) { + ImageCapture.FLASH_MODE_AUTO -> R.drawable.ic_flash_auto + ImageCapture.FLASH_MODE_OFF -> R.drawable.ic_flash_off + ImageCapture.FLASH_MODE_ON -> R.drawable.ic_flash_on + else -> R.drawable.ic_flash_auto + } ) - } - } - ) - } - } - - @SuppressLint("UseCompatLoadingForDrawables") - private fun captureVideo() { - Timber.d("Capturing Video") - context?.let { context -> - val videoCapture = this.videoCapture ?: return - - views.attachmentsCameraChangeAction.isEnabled = false - views.attachmentsCameraFlip.isEnabled = false - - val curRecording = recording - if (curRecording != null) { - // Stop the current recording session. - curRecording.stop() - recording = null - return - } - - val file = createTempFile(MediaType.VIDEO) - val outputUri = getUri(context, file) - - val options = FileOutputOptions - .Builder(file) - .build() - recording = videoCapture.output - .prepareRecording(context, options) - .apply { - if (PermissionChecker.checkSelfPermission( - context, - Manifest.permission.RECORD_AUDIO - ) == PermissionChecker.PERMISSION_GRANTED) { - withAudioEnabled() - } - } - .start(ContextCompat.getMainExecutor(context)) { recordEvent -> - when (recordEvent) { - is VideoRecordEvent.Start -> { - views.attachmentsCameraCaptureAction.setImageDrawable( - context.getDrawable(R.drawable.ic_video_off) - ) - } - is VideoRecordEvent.Finalize -> { - if (!recordEvent.hasError()) { - Timber.d("Video capture succeeded: " + - "${recordEvent.outputResults.outputUri}") - (activity as? AttachmentsCameraActivity)?.setResultAndFinish( - VectorCameraOutput( - type = MediaType.VIDEO, - uri = outputUri - ) - ) - } else { - recording?.close() - recording = null - Timber.e("Video capture ends with error: " + - "${recordEvent.error}") - (activity as? AttachmentsCameraActivity)?.setErrorAndFinish() - } + ) + contentDescription = context?.getString( + when (state.flashMode) { + ImageCapture.FLASH_MODE_AUTO -> R.string.attachment_camera_disable_flash + ImageCapture.FLASH_MODE_OFF -> R.string.attachment_camera_enable_flash + ImageCapture.FLASH_MODE_ON -> R.string.attachment_camera_auto_flash + else -> R.string.attachment_camera_disable_flash } - } - } + ) + } + } } } + @SuppressLint("RestrictedApi") private fun startCamera() { Timber.d("Starting Camera") - context?.let { context -> + val context = this.context ?: return + withState(viewModel) { state -> + if (currentLens == state.cameraSelector.lensFacing) return@withState + currentLens = state.cameraSelector.lensFacing + val cameraProviderFuture = ProcessCameraProvider.getInstance(context) cameraProviderFuture.addListener({ @@ -421,8 +301,10 @@ class AttachmentsCameraFragment : // Bind use cases to camera camera = cameraProvider.bindToLifecycle( - this, cameraSelector, preview, imageCapture, videoCapture + this, state.cameraSelector, preview, imageCapture, videoCapture ) + + Timber.d("Lensfacing: ${camera.cameraInfo.cameraSelector}") } catch (exc: Exception) { Timber.e("Use case binding failed", exc) } @@ -445,38 +327,7 @@ class AttachmentsCameraFragment : return true } - private fun createTempFile(type: MediaType): File { - var prefix = "" - var suffix = "" - when (type) { - MediaType.IMAGE -> { - prefix = "IMG_" - suffix = ".jpg" - } - MediaType.VIDEO -> { - prefix = "VID_" - suffix = ".mp4" - } - } - return File.createTempFile( - "$prefix${clock.epochMillis()}", - suffix, - storageDir - ) - } - - private fun getUri(context: Context, file: File): Uri { - return FileProvider.getUriForFile( - context, - authority, - file - ) - } - companion object { - private var captureMode = MediaType.IMAGE - private var cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA - private var flashMode = ImageCapture.FLASH_MODE_AUTO private val REQUIRED_PERMISSIONS = mutableListOf( Manifest.permission.CAMERA, diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraModel.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraModel.kt new file mode 100644 index 00000000000..350d190a62b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraModel.kt @@ -0,0 +1,259 @@ +/* + * Copyright (c) 2022 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.features.attachments.camera + +import android.Manifest +import android.annotation.SuppressLint +import android.content.Context +import android.net.Uri +import android.widget.Toast +import androidx.camera.core.CameraSelector +import androidx.camera.core.ImageCapture +import androidx.camera.core.ImageCaptureException +import androidx.camera.video.FileOutputOptions +import androidx.camera.video.Recorder +import androidx.camera.video.Recording +import androidx.camera.video.VideoCapture +import androidx.camera.video.VideoRecordEvent +import androidx.core.content.ContextCompat +import androidx.core.content.FileProvider +import androidx.core.content.PermissionChecker +import com.airbnb.mvrx.MavericksViewModelFactory +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.core.di.MavericksAssistedViewModelFactory +import im.vector.app.core.di.hiltMavericksViewModelFactory +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.core.time.Clock +import timber.log.Timber +import java.io.File + +class AttachmentsCameraModel @AssistedInject constructor( + @Assisted val initialState: AttachmentsCameraState, + val clock: Clock +) : VectorViewModel(initialState) { + + private var recording: Recording? = null + + @AssistedFactory + interface Factory : MavericksAssistedViewModelFactory { + override fun create(initialState: AttachmentsCameraState): AttachmentsCameraModel + } + + companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() + + override fun handle(action: AttachmentsCameraAction) { + when(action) { + AttachmentsCameraAction.ChangeLensFacing -> changeLensFacing() + AttachmentsCameraAction.ChangeCaptureMode -> changeCaptureMode() + AttachmentsCameraAction.RotateFlashMode -> rotateFlashMode() + is AttachmentsCameraAction.SetRotation -> setRotation(action.rotation) + is AttachmentsCameraAction.Capture -> capture(action) + } + } + + private fun changeLensFacing() { + setState { + copy ( + cameraSelector = if (cameraSelector == CameraSelector.DEFAULT_BACK_CAMERA) { + CameraSelector.DEFAULT_FRONT_CAMERA + } else { + CameraSelector.DEFAULT_BACK_CAMERA + } + ) + } + } + + private fun changeCaptureMode() { + setState { + copy( + captureMode = when (captureMode) { + MediaType.IMAGE -> MediaType.VIDEO + MediaType.VIDEO -> MediaType.IMAGE + } + ) + } + } + + private fun rotateFlashMode() { + setState { + copy( + flashMode = when (flashMode) { + ImageCapture.FLASH_MODE_AUTO -> ImageCapture.FLASH_MODE_OFF + ImageCapture.FLASH_MODE_OFF -> ImageCapture.FLASH_MODE_ON + ImageCapture.FLASH_MODE_ON -> ImageCapture.FLASH_MODE_AUTO + else -> ImageCapture.FLASH_MODE_OFF + } + ) + } + } + + private fun setRotation(newRotation: Int) { + setState { + copy ( + rotation = newRotation + ) + } + } + + private fun capture(action: AttachmentsCameraAction.Capture) = withState { state -> + when(state.captureMode) { + MediaType.IMAGE -> { + action.imageCapture?.let { + capture(action.context, action.imageCapture) + true + } + } + MediaType.VIDEO -> { + action.videoCapture?.let { + capture(action.context, action.videoCapture) + true + } + } + } ?: _viewEvents.post(AttachmentsCameraViewEvents.SetErrorAndFinish) + } + + private fun capture(context: Context, imageCapture: ImageCapture) { + _viewEvents.post(AttachmentsCameraViewEvents.TakePhoto) + withState { state -> + imageCapture.flashMode = state.flashMode + + val file = createTempFile(context, MediaType.IMAGE) + val outputUri = getUri(context, file) + + // Create output options object which contains file + metadata + val outputOptions = ImageCapture.OutputFileOptions + .Builder(file) + .build() + + // Set up image capture listener, which is triggered after photo has + // been taken + imageCapture.takePicture( + outputOptions, + ContextCompat.getMainExecutor(context), + object : ImageCapture.OnImageSavedCallback { + override fun onError(exc: ImageCaptureException) { + Timber.e("Photo capture failed: ${exc.message}", exc) + Toast.makeText(context, "An error occurred", Toast.LENGTH_SHORT).show() + _viewEvents.post(AttachmentsCameraViewEvents.SetErrorAndFinish) + } + + override fun onImageSaved(output: ImageCapture.OutputFileResults) { + _viewEvents.post( + AttachmentsCameraViewEvents.SetResultAndFinish( + VectorCameraOutput( + type = MediaType.IMAGE, + uri = outputUri + ) + ) + ) + } + } + ) + } + } + + + @SuppressLint("UseCompatLoadingForDrawables") + private fun capture(context: Context, videoCapture: VideoCapture) { + Timber.d("Capturing Video") + recording?.let { + recording?.stop() + recording = null + return + } + + val file = createTempFile(context, MediaType.VIDEO) + val outputUri = getUri(context, file) + + val options = FileOutputOptions + .Builder(file) + .build() + + recording = videoCapture.output + .prepareRecording(context, options) + .apply { + if (PermissionChecker.checkSelfPermission( + context, + Manifest.permission.RECORD_AUDIO + ) == PermissionChecker.PERMISSION_GRANTED) { + withAudioEnabled() + } + } + .start(ContextCompat.getMainExecutor(context)) { recordEvent -> + when (recordEvent) { + is VideoRecordEvent.Start -> { + _viewEvents.post(AttachmentsCameraViewEvents.StartRecording) + } + is VideoRecordEvent.Finalize -> { + if (!recordEvent.hasError()) { + Timber.d( + "Video capture succeeded: " + + "${recordEvent.outputResults.outputUri}" + ) + _viewEvents.post( + AttachmentsCameraViewEvents.SetResultAndFinish( + VectorCameraOutput( + type = MediaType.VIDEO, + uri = outputUri + ) + ) + ) + } else { + recording?.close() + recording = null + Timber.e( + "Video capture ends with error: " + + "${recordEvent.error}" + ) + _viewEvents.post(AttachmentsCameraViewEvents.SetErrorAndFinish) + } + } + } + } + } + + private fun createTempFile(context: Context, type: MediaType): File { + var prefix = "" + var suffix = "" + when (type) { + MediaType.IMAGE -> { + prefix = "IMG_" + suffix = ".jpg" + } + MediaType.VIDEO -> { + prefix = "VID_" + suffix = ".mp4" + } + } + return File.createTempFile( + "$prefix${clock.epochMillis()}", + suffix, + context.cacheDir.also { it?.mkdirs() }!! + ) + } + + + private fun getUri(context: Context, file: File): Uri { + return FileProvider.getUriForFile( + context, + context.packageName + ".fileProvider", + file + ) + } +} diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraState.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraState.kt new file mode 100644 index 00000000000..541651db37f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraState.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022 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.features.attachments.camera + +import android.view.Surface +import androidx.camera.core.CameraSelector +import androidx.camera.core.ImageCapture +import com.airbnb.mvrx.MavericksState + +data class AttachmentsCameraState( + val captureMode: MediaType = MediaType.IMAGE, + val cameraSelector: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA, + val flashMode: Int = ImageCapture.FLASH_MODE_AUTO, + val rotation: Int = Surface.ROTATION_0, +) : MavericksState diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraViewEvents.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraViewEvents.kt new file mode 100644 index 00000000000..25f4de7d70a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraViewEvents.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2022 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.features.attachments.camera + +import im.vector.app.core.platform.VectorViewEvents + +sealed class AttachmentsCameraViewEvents : VectorViewEvents { + object StartRecording: AttachmentsCameraViewEvents() + object TakePhoto: AttachmentsCameraViewEvents() + object SetErrorAndFinish: AttachmentsCameraViewEvents() + data class SetResultAndFinish(val vectorCameraOutput: VectorCameraOutput): AttachmentsCameraViewEvents() +} From 37763051f437269e1627d5fbb4ffae686cfaa8d4 Mon Sep 17 00:00:00 2001 From: sim Date: Mon, 29 Aug 2022 21:41:48 +0200 Subject: [PATCH 17/35] Camera: Add UI tests --- .../vector/app/ui/UiAllScreensSanityTest.kt | 15 ++++++++ .../im/vector/app/ui/robot/ElementRobot.kt | 9 +++++ .../im/vector/app/ui/robot/RoomDetailRobot.kt | 37 +++++++++++++++++++ .../app/ui/robot/settings/labs/LabFeature.kt | 1 + 4 files changed, 62 insertions(+) diff --git a/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt b/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt index 94340060605..8321a56abf1 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/UiAllScreensSanityTest.kt @@ -97,6 +97,7 @@ class UiAllScreensSanityTest { } } + testCamera() testThreadScreens() val spaceName = UUID.randomUUID().toString() @@ -174,4 +175,18 @@ class UiAllScreensSanityTest { } elementRobot.toggleLabFeature(LabFeature.THREAD_MESSAGES) } + + private fun testCamera() { + elementRobot.toggleLabFeature(LabFeature.BUILTIN_CAMERA) + elementRobot.newRoom { + createNewRoom { + createRoom { + cameraPhotoBack() + cameraPhotoFront() + cameraVideoFront() + } + } + } + elementRobot.toggleLabFeature(LabFeature.BUILTIN_CAMERA) + } } diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt index b6fbfc23ab6..8243cf503fe 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/ElementRobot.kt @@ -140,6 +140,15 @@ class ElementRobot { // I hereby cheat and write: Thread.sleep(30_000) } + LabFeature.BUILTIN_CAMERA -> { + settings { + labs { + onView(withText(R.string.labs_enable_builtin_camera)) + .check(ViewAssertions.matches(isDisplayed())) + .perform(ViewActions.closeSoftKeyboard(), click()) + } + } + } else -> { } } diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt index 64360805993..6defc71922e 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt @@ -64,6 +64,43 @@ class RoomDetailRobot { pressBack() } + fun cameraPhotoBack() { + clickOn(R.id.attachmentButton) + clickOn(R.id.attachmentCameraButton) + clickOn(R.id.attachmentsCameraFlash) + clickOn(R.id.attachmentsCameraFlash) + clickOn(R.id.attachmentsCameraCaptureAction) + waitAttachmentPreviewer() + clickOn(R.id.attachmentPreviewerSendButton) + } + + fun cameraPhotoFront() { + clickOn(R.id.attachmentButton) + clickOn(R.id.attachmentCameraButton) + clickOn(R.id.attachmentsCameraFlip) + clickOn(R.id.attachmentsCameraCaptureAction) + waitAttachmentPreviewer() + clickOn(R.id.attachmentPreviewerSendButton) + } + + fun cameraVideoFront() { + clickOn(R.id.attachmentButton) + clickOn(R.id.attachmentCameraButton) + clickOn(R.id.attachmentsCameraChangeAction) + clickOn(R.id.attachmentsCameraFlip) + clickOn(R.id.attachmentsCameraCaptureAction) + sleep(1_000) + clickOn(R.id.attachmentsCameraCaptureAction) + waitAttachmentPreviewer() + clickOn(R.id.attachmentPreviewerSendButton) + } + + private fun waitAttachmentPreviewer() { + // haven't find a way to use waitUntilViewVisible + // for the attachmentPreviewer View + sleep(4_000) + } + fun replyToThread(message: String) { openMessageMenu(message) { replyInThread() diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/labs/LabFeature.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/labs/LabFeature.kt index 656201d8128..97e266ef194 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/settings/labs/LabFeature.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/settings/labs/LabFeature.kt @@ -22,5 +22,6 @@ enum class LabFeature { LATEX_MATHEMATICS, THREAD_MESSAGES, AUTO_REPORT_ERRORS, + BUILTIN_CAMERA, RENDER_USER_LOCATION } From 9bca9bda947348a83353bcdbc0e62266b689fd4f Mon Sep 17 00:00:00 2001 From: sim Date: Mon, 29 Aug 2022 21:57:56 +0200 Subject: [PATCH 18/35] Do not use "VectorCamera" for names --- ...{VectorCameraPicker.kt => BuiltInCameraPicker.kt} | 2 +- .../java/im/vector/lib/multipicker/MultiPicker.kt | 4 ++-- .../app/features/attachments/AttachmentsHelper.kt | 12 ++++++------ .../attachments/camera/AttachmentsCameraActivity.kt | 2 +- .../attachments/camera/AttachmentsCameraFragment.kt | 2 +- .../attachments/camera/AttachmentsCameraModel.kt | 4 ++-- ...torCameraOutput.kt => AttachmentsCameraOutput.kt} | 2 +- .../camera/AttachmentsCameraViewEvents.kt | 2 +- .../features/home/room/detail/TimelineFragment.kt | 10 +++++----- 9 files changed, 20 insertions(+), 20 deletions(-) rename library/multipicker/src/main/java/im/vector/lib/multipicker/{VectorCameraPicker.kt => BuiltInCameraPicker.kt} (98%) rename vector/src/main/java/im/vector/app/features/attachments/camera/{VectorCameraOutput.kt => AttachmentsCameraOutput.kt} (96%) diff --git a/library/multipicker/src/main/java/im/vector/lib/multipicker/VectorCameraPicker.kt b/library/multipicker/src/main/java/im/vector/lib/multipicker/BuiltInCameraPicker.kt similarity index 98% rename from library/multipicker/src/main/java/im/vector/lib/multipicker/VectorCameraPicker.kt rename to library/multipicker/src/main/java/im/vector/lib/multipicker/BuiltInCameraPicker.kt index 25af37b20ed..5b77781c570 100644 --- a/library/multipicker/src/main/java/im/vector/lib/multipicker/VectorCameraPicker.kt +++ b/library/multipicker/src/main/java/im/vector/lib/multipicker/BuiltInCameraPicker.kt @@ -25,7 +25,7 @@ import im.vector.lib.multipicker.entity.MultiPickerVideoType import im.vector.lib.multipicker.utils.toMultiPickerImageType import im.vector.lib.multipicker.utils.toMultiPickerVideoType -class VectorCameraPicker { +class BuiltInCameraPicker { /** * Start camera by using a ActivityResultLauncher. diff --git a/library/multipicker/src/main/java/im/vector/lib/multipicker/MultiPicker.kt b/library/multipicker/src/main/java/im/vector/lib/multipicker/MultiPicker.kt index 2ee5cb0793b..6a5cc7775af 100644 --- a/library/multipicker/src/main/java/im/vector/lib/multipicker/MultiPicker.kt +++ b/library/multipicker/src/main/java/im/vector/lib/multipicker/MultiPicker.kt @@ -27,7 +27,7 @@ class MultiPicker private constructor() { val CONTACT by lazy { MultiPicker() } val CAMERA by lazy { MultiPicker() } val CAMERA_VIDEO by lazy { MultiPicker() } - val VECTOR_CAMERA by lazy { MultiPicker() } + val BUILTIN_CAMERA by lazy { MultiPicker() } @Suppress("UNCHECKED_CAST") fun get(type: MultiPicker): T { @@ -40,7 +40,7 @@ class MultiPicker private constructor() { CONTACT -> ContactPicker() as T CAMERA -> CameraPicker() as T CAMERA_VIDEO -> CameraVideoPicker() as T - VECTOR_CAMERA -> VectorCameraPicker() as T + BUILTIN_CAMERA -> BuiltInCameraPicker() as T else -> throw IllegalArgumentException("Unsupported type $type") } } diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt index 53a49ee65a1..10f20a61cb6 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt @@ -24,7 +24,7 @@ import im.vector.app.core.platform.Restorable import im.vector.app.core.resources.BuildMeta import im.vector.app.features.attachments.camera.AttachmentsCameraActivity import im.vector.app.features.attachments.camera.MediaType -import im.vector.app.features.attachments.camera.VectorCameraOutput +import im.vector.app.features.attachments.camera.AttachmentsCameraOutput import im.vector.lib.multipicker.MultiPicker import org.matrix.android.sdk.api.session.content.ContentAttachmentData import timber.log.Timber @@ -102,10 +102,10 @@ class AttachmentsHelper( captureUri = MultiPicker.get(MultiPicker.CAMERA_VIDEO).startWithExpectingFile(context, cameraVideoActivityResultLauncher) } - fun openVectorCamera(vectorCameraActivityResultLauncher: ActivityResultLauncher) { - MultiPicker.get(MultiPicker.VECTOR_CAMERA).start( + fun openBuiltInCamera(attachmentsCameraActivityResultLauncher: ActivityResultLauncher) { + MultiPicker.get(MultiPicker.BUILTIN_CAMERA).start( context, - vectorCameraActivityResultLauncher, + attachmentsCameraActivityResultLauncher, AttachmentsCameraActivity::class.java ) } @@ -169,8 +169,8 @@ class AttachmentsHelper( } } - fun onVectorCameraResult(cameraOutput: VectorCameraOutput) { - val multiPicker = MultiPicker.get(MultiPicker.VECTOR_CAMERA) + fun onAttachmentsCameraResult(cameraOutput: AttachmentsCameraOutput) { + val multiPicker = MultiPicker.get(MultiPicker.BUILTIN_CAMERA) val media = if (cameraOutput.type == MediaType.IMAGE) { multiPicker.getTakenPhoto(context, cameraOutput.uri) } else { diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraActivity.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraActivity.kt index ca3613b3704..c7907120989 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraActivity.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraActivity.kt @@ -38,7 +38,7 @@ class AttachmentsCameraActivity : VectorBaseActivity() { } } - fun setResultAndFinish(data: VectorCameraOutput) { + fun setResultAndFinish(data: AttachmentsCameraOutput) { val resultIntent = Intent().apply { putExtra(MediaStore.EXTRA_OUTPUT, data) } diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt index b6315db818e..815537992e2 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt @@ -122,7 +122,7 @@ class AttachmentsCameraFragment : } AttachmentsCameraViewEvents.TakePhoto -> views.attachmentsCameraLoading.isVisible = true AttachmentsCameraViewEvents.SetErrorAndFinish -> (activity as AttachmentsCameraActivity).setErrorAndFinish() - is AttachmentsCameraViewEvents.SetResultAndFinish -> (activity as AttachmentsCameraActivity).setResultAndFinish(it.vectorCameraOutput) + is AttachmentsCameraViewEvents.SetResultAndFinish -> (activity as AttachmentsCameraActivity).setResultAndFinish(it.attachmentsCameraOutput) } } diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraModel.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraModel.kt index 350d190a62b..d5f62c0f60e 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraModel.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraModel.kt @@ -156,7 +156,7 @@ class AttachmentsCameraModel @AssistedInject constructor( override fun onImageSaved(output: ImageCapture.OutputFileResults) { _viewEvents.post( AttachmentsCameraViewEvents.SetResultAndFinish( - VectorCameraOutput( + AttachmentsCameraOutput( type = MediaType.IMAGE, uri = outputUri ) @@ -208,7 +208,7 @@ class AttachmentsCameraModel @AssistedInject constructor( ) _viewEvents.post( AttachmentsCameraViewEvents.SetResultAndFinish( - VectorCameraOutput( + AttachmentsCameraOutput( type = MediaType.VIDEO, uri = outputUri ) diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/VectorCameraOutput.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraOutput.kt similarity index 96% rename from vector/src/main/java/im/vector/app/features/attachments/camera/VectorCameraOutput.kt rename to vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraOutput.kt index 300a7da77d7..eacacab108c 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/camera/VectorCameraOutput.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraOutput.kt @@ -26,7 +26,7 @@ enum class MediaType { } @Parcelize -data class VectorCameraOutput( +data class AttachmentsCameraOutput( val type: MediaType, val uri: Uri, ) : Parcelable diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraViewEvents.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraViewEvents.kt index 25f4de7d70a..454916c4cde 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraViewEvents.kt @@ -22,5 +22,5 @@ sealed class AttachmentsCameraViewEvents : VectorViewEvents { object StartRecording: AttachmentsCameraViewEvents() object TakePhoto: AttachmentsCameraViewEvents() object SetErrorAndFinish: AttachmentsCameraViewEvents() - data class SetResultAndFinish(val vectorCameraOutput: VectorCameraOutput): AttachmentsCameraViewEvents() + data class SetResultAndFinish(val attachmentsCameraOutput: AttachmentsCameraOutput): AttachmentsCameraViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 564a22ce014..984cd5a096b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -136,7 +136,7 @@ import im.vector.app.features.attachments.AttachmentTypeSelectorView import im.vector.app.features.attachments.AttachmentsHelper import im.vector.app.features.attachments.ContactAttachment import im.vector.app.features.attachments.ShareIntentHandler -import im.vector.app.features.attachments.camera.VectorCameraOutput +import im.vector.app.features.attachments.camera.AttachmentsCameraOutput import im.vector.app.features.attachments.preview.AttachmentsPreviewActivity import im.vector.app.features.attachments.preview.AttachmentsPreviewArgs import im.vector.app.features.attachments.toGroupedContentAttachmentData @@ -1383,10 +1383,10 @@ class TimelineFragment : } } - private val attachmentVectorCameraActivityResultLauncher = registerStartForActivityResult { + private val attachmentBuiltInCameraActivityResultLauncher = registerStartForActivityResult { if (it.resultCode == Activity.RESULT_OK) { - it.data?.getParcelableExtra(MediaStore.EXTRA_OUTPUT)?.let { cameraOutput -> - attachmentsHelper.onVectorCameraResult(cameraOutput) + it.data?.getParcelableExtra(MediaStore.EXTRA_OUTPUT)?.let { cameraOutput -> + attachmentsHelper.onAttachmentsCameraResult(cameraOutput) } } } @@ -2631,7 +2631,7 @@ class TimelineFragment : when (type) { AttachmentTypeSelectorView.Type.PHOTO_CAMERA -> { if (vectorPreferences.builtinCameraIsEnabled()) { - attachmentsHelper.openVectorCamera(attachmentVectorCameraActivityResultLauncher) + attachmentsHelper.openBuiltInCamera(attachmentBuiltInCameraActivityResultLauncher) } else { attachmentsHelper.openPhotoCamera(attachmentCameraActivityResultLauncher) } From bbdfa6ebc8f90a99c633809560a648c19f42bdf3 Mon Sep 17 00:00:00 2001 From: sim Date: Mon, 29 Aug 2022 22:34:46 +0200 Subject: [PATCH 19/35] Fix dependencies.gradle for androidx.camera --- dependencies.gradle | 13 +++++++------ vector/build.gradle | 13 ++++++------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/dependencies.gradle b/dependencies.gradle index 4e63f646b97..63a7141aec5 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -31,6 +31,7 @@ def jjwt = "0.11.5" def vanniktechEmoji = "0.15.0" def fragment = "1.5.2" +def cameraxVersion = "1.1.0" // Testing def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819 @@ -54,12 +55,12 @@ ext.libs = [ 'appCompat' : "androidx.appcompat:appcompat:1.4.2", 'biometric' : "androidx.biometric:biometric:1.1.0", 'camera': [ - 'core' : "androidx.camera.camera-core:1.1.0", - 'camera2' : "androidx.camera.camera-camera2:1.1.0", - 'lifecycle' : "androidx.camera.camera-lifecycle:1.1.0", - 'video' : "androidx.camera.camera-video:1.1.0", - 'view' : "androidx.camera.camera-view:1.1.0", - 'extensions' : "androidx.camera.camera-extensions:1.1.0", + 'core' : "androidx.camera:camera-core:$cameraxVersion", + 'camera2' : "androidx.camera:camera-camera2:$cameraxVersion", + 'lifecycle' : "androidx.camera:camera-lifecycle:$cameraxVersion", + 'video' : "androidx.camera:camera-video:$cameraxVersion", + 'view' : "androidx.camera:camera-view:$cameraxVersion", + 'extensions' : "androidx.camera:camera-extensions:$cameraxVersion", ], 'core' : "androidx.core:core-ktx:1.8.0", 'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1", diff --git a/vector/build.gradle b/vector/build.gradle index e702cdaf694..f8246255efc 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -396,13 +396,12 @@ dependencies { implementation libs.squareup.moshi // Camera - def camerax_version = "1.1.0" - implementation "androidx.camera:camera-core:${camerax_version}" - implementation "androidx.camera:camera-camera2:${camerax_version}" - implementation "androidx.camera:camera-lifecycle:${camerax_version}" - implementation "androidx.camera:camera-video:${camerax_version}" - implementation "androidx.camera:camera-view:${camerax_version}" - implementation "androidx.camera:camera-extensions:${camerax_version}" + implementation libs.androidx.camera.core + implementation libs.androidx.camera.camera2 + implementation libs.androidx.camera.lifecycle + implementation libs.androidx.camera.video + implementation libs.androidx.camera.view + implementation libs.androidx.camera.extensions kapt libs.squareup.moshiKotlin From 08ae1202965bfdf764624493244944284cd99b38 Mon Sep 17 00:00:00 2001 From: sim Date: Mon, 5 Sep 2022 18:53:27 +0200 Subject: [PATCH 20/35] Enable orientationEventListener onStart --- .../attachments/camera/AttachmentsCameraFragment.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt index 815537992e2..711150674b4 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt @@ -133,8 +133,6 @@ class AttachmentsCameraFragment : requestPermissionLauncher.launch(REQUIRED_PERMISSIONS) } - orientationEventListener.enable() - views.attachmentsCameraCaptureAction.debouncedClicks { context?.let { viewModel.handle(AttachmentsCameraAction.Capture(it, imageCapture, videoCapture)) @@ -172,6 +170,11 @@ class AttachmentsCameraFragment : setRotation() } + override fun onStart() { + super.onStart() + orientationEventListener.enable() + } + override fun onStop() { super.onStop() orientationEventListener.disable() From db5e58273f1001bc370dd574911aa13263f982ee Mon Sep 17 00:00:00 2001 From: sim Date: Mon, 5 Sep 2022 18:55:05 +0200 Subject: [PATCH 21/35] Lint --- .../app/features/attachments/AttachmentsHelper.kt | 2 +- .../attachments/camera/AttachmentsCameraFragment.kt | 2 +- .../attachments/camera/AttachmentsCameraModel.kt | 10 ++++------ .../attachments/camera/AttachmentsCameraViewEvents.kt | 8 ++++---- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt index 10f20a61cb6..8ae114afab0 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt @@ -23,8 +23,8 @@ import androidx.activity.result.ActivityResultLauncher import im.vector.app.core.platform.Restorable import im.vector.app.core.resources.BuildMeta import im.vector.app.features.attachments.camera.AttachmentsCameraActivity -import im.vector.app.features.attachments.camera.MediaType import im.vector.app.features.attachments.camera.AttachmentsCameraOutput +import im.vector.app.features.attachments.camera.MediaType import im.vector.lib.multipicker.MultiPicker import org.matrix.android.sdk.api.session.content.ContentAttachmentData import timber.log.Timber diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt index 711150674b4..5290c26b3c2 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt @@ -40,10 +40,10 @@ import androidx.camera.video.QualitySelector import androidx.camera.video.Recorder import androidx.camera.video.VideoCapture import androidx.core.content.ContextCompat -import dagger.hilt.android.AndroidEntryPoint import androidx.core.view.isVisible import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState +import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorMenuProvider diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraModel.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraModel.kt index d5f62c0f60e..804cc82fe04 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraModel.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraModel.kt @@ -58,7 +58,7 @@ class AttachmentsCameraModel @AssistedInject constructor( companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() override fun handle(action: AttachmentsCameraAction) { - when(action) { + when (action) { AttachmentsCameraAction.ChangeLensFacing -> changeLensFacing() AttachmentsCameraAction.ChangeCaptureMode -> changeCaptureMode() AttachmentsCameraAction.RotateFlashMode -> rotateFlashMode() @@ -69,7 +69,7 @@ class AttachmentsCameraModel @AssistedInject constructor( private fun changeLensFacing() { setState { - copy ( + copy( cameraSelector = if (cameraSelector == CameraSelector.DEFAULT_BACK_CAMERA) { CameraSelector.DEFAULT_FRONT_CAMERA } else { @@ -105,14 +105,14 @@ class AttachmentsCameraModel @AssistedInject constructor( private fun setRotation(newRotation: Int) { setState { - copy ( + copy( rotation = newRotation ) } } private fun capture(action: AttachmentsCameraAction.Capture) = withState { state -> - when(state.captureMode) { + when (state.captureMode) { MediaType.IMAGE -> { action.imageCapture?.let { capture(action.context, action.imageCapture) @@ -168,7 +168,6 @@ class AttachmentsCameraModel @AssistedInject constructor( } } - @SuppressLint("UseCompatLoadingForDrawables") private fun capture(context: Context, videoCapture: VideoCapture) { Timber.d("Capturing Video") @@ -248,7 +247,6 @@ class AttachmentsCameraModel @AssistedInject constructor( ) } - private fun getUri(context: Context, file: File): Uri { return FileProvider.getUriForFile( context, diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraViewEvents.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraViewEvents.kt index 454916c4cde..bca72dfd481 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraViewEvents.kt @@ -19,8 +19,8 @@ package im.vector.app.features.attachments.camera import im.vector.app.core.platform.VectorViewEvents sealed class AttachmentsCameraViewEvents : VectorViewEvents { - object StartRecording: AttachmentsCameraViewEvents() - object TakePhoto: AttachmentsCameraViewEvents() - object SetErrorAndFinish: AttachmentsCameraViewEvents() - data class SetResultAndFinish(val attachmentsCameraOutput: AttachmentsCameraOutput): AttachmentsCameraViewEvents() + object StartRecording : AttachmentsCameraViewEvents() + object TakePhoto : AttachmentsCameraViewEvents() + object SetErrorAndFinish : AttachmentsCameraViewEvents() + data class SetResultAndFinish(val attachmentsCameraOutput: AttachmentsCameraOutput) : AttachmentsCameraViewEvents() } From d88371d9037bf9734f2d2fa6464c29df1c0f545d Mon Sep 17 00:00:00 2001 From: sim Date: Mon, 5 Sep 2022 20:01:16 +0200 Subject: [PATCH 22/35] Add chronometer to camera view --- .../camera/AttachmentsCameraFragment.kt | 14 ++++++++++++-- .../res/layout/fragment_attachments_camera.xml | 14 ++++++++++++++ vector/src/main/res/values/strings.xml | 1 + 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt index 5290c26b3c2..daf3e55dcca 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt @@ -21,6 +21,7 @@ import android.annotation.SuppressLint import android.content.pm.PackageManager import android.os.Build import android.os.Bundle +import android.os.SystemClock import android.view.LayoutInflater import android.view.MenuItem import android.view.OrientationEventListener @@ -119,10 +120,19 @@ class AttachmentsCameraFragment : ) views.attachmentsCameraChangeAction.isEnabled = false views.attachmentsCameraFlip.isEnabled = false + views.attachmentsCameraChronometer.isVisible = true + views.attachmentsCameraChronometer.base = SystemClock.elapsedRealtime() + views.attachmentsCameraChronometer.start() } AttachmentsCameraViewEvents.TakePhoto -> views.attachmentsCameraLoading.isVisible = true - AttachmentsCameraViewEvents.SetErrorAndFinish -> (activity as AttachmentsCameraActivity).setErrorAndFinish() - is AttachmentsCameraViewEvents.SetResultAndFinish -> (activity as AttachmentsCameraActivity).setResultAndFinish(it.attachmentsCameraOutput) + AttachmentsCameraViewEvents.SetErrorAndFinish -> { + views.attachmentsCameraChronometer.stop() + (activity as AttachmentsCameraActivity).setErrorAndFinish() + } + is AttachmentsCameraViewEvents.SetResultAndFinish -> { + views.attachmentsCameraChronometer.stop() + (activity as AttachmentsCameraActivity).setResultAndFinish(it.attachmentsCameraOutput) + } } } diff --git a/vector/src/main/res/layout/fragment_attachments_camera.xml b/vector/src/main/res/layout/fragment_attachments_camera.xml index d53628b126a..6c87727f6e5 100644 --- a/vector/src/main/res/layout/fragment_attachments_camera.xml +++ b/vector/src/main/res/layout/fragment_attachments_camera.xml @@ -64,6 +64,20 @@ app:layout_constraintEnd_toEndOf="parent" android:contentDescription="@string/attachment_camera_flip" /> + + Disable flash Enable flash Set flash automatically + Chronometer %d message removed From ccf0c88e68bbe5fa8503c67a48cb64d1fa54f5fa Mon Sep 17 00:00:00 2001 From: sim Date: Mon, 5 Sep 2022 21:42:03 +0200 Subject: [PATCH 23/35] Use new UX for the camera Start recording a video onLongClick --- .../camera/AttachmentsCameraAction.kt | 1 - .../camera/AttachmentsCameraFragment.kt | 53 +++++-------------- .../camera/AttachmentsCameraModel.kt | 53 +++++++------------ .../camera/AttachmentsCameraState.kt | 1 - .../layout/fragment_attachments_camera.xml | 22 ++------ 5 files changed, 37 insertions(+), 93 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraAction.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraAction.kt index 49c315d8ccf..008c0c4a8a5 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraAction.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraAction.kt @@ -24,7 +24,6 @@ import im.vector.app.core.platform.VectorViewModelAction sealed class AttachmentsCameraAction : VectorViewModelAction { object ChangeLensFacing : AttachmentsCameraAction() - object ChangeCaptureMode : AttachmentsCameraAction() object RotateFlashMode : AttachmentsCameraAction() data class SetRotation(val rotation: Int) : AttachmentsCameraAction() data class Capture( diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt index daf3e55dcca..ddd873c2ecd 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt @@ -24,6 +24,7 @@ import android.os.Bundle import android.os.SystemClock import android.view.LayoutInflater import android.view.MenuItem +import android.view.MotionEvent import android.view.OrientationEventListener import android.view.ScaleGestureDetector import android.view.Surface @@ -118,8 +119,8 @@ class AttachmentsCameraFragment : views.attachmentsCameraCaptureAction.setImageDrawable( context?.getDrawable(R.drawable.ic_video_off) ) - views.attachmentsCameraChangeAction.isEnabled = false views.attachmentsCameraFlip.isEnabled = false + views.attachmentsCameraFlash.isEnabled = false views.attachmentsCameraChronometer.isVisible = true views.attachmentsCameraChronometer.base = SystemClock.elapsedRealtime() views.attachmentsCameraChronometer.start() @@ -143,14 +144,20 @@ class AttachmentsCameraFragment : requestPermissionLauncher.launch(REQUIRED_PERMISSIONS) } - views.attachmentsCameraCaptureAction.debouncedClicks { - context?.let { - viewModel.handle(AttachmentsCameraAction.Capture(it, imageCapture, videoCapture)) + views.attachmentsCameraCaptureAction.setOnTouchListener { _, motionEvent -> + if (motionEvent.action == MotionEvent.ACTION_UP) { + context?.let { + viewModel.handle(AttachmentsCameraAction.Capture(it, imageCapture)) + } } + false } - views.attachmentsCameraChangeAction.debouncedClicks { - viewModel.handle(AttachmentsCameraAction.ChangeCaptureMode) + views.attachmentsCameraCaptureAction.setOnLongClickListener { + context?.let { + viewModel.handle(AttachmentsCameraAction.Capture(it, videoCapture = videoCapture)) + } + true } views.attachmentsCameraFlip.debouncedClicks { @@ -175,7 +182,6 @@ class AttachmentsCameraFragment : if (allPermissionsGranted()) { startCamera() } - setCaptureModeButtons() setFlashButton() setRotation() } @@ -196,43 +202,12 @@ class AttachmentsCameraFragment : } == PackageManager.PERMISSION_GRANTED } - @SuppressLint("UseCompatLoadingForDrawables") - private fun setCaptureModeButtons() { - withState(viewModel) { state -> - when (state.captureMode) { - MediaType.VIDEO -> { - views.attachmentsCameraCaptureAction.setImageDrawable( - context?.getDrawable(R.drawable.ic_video) - ) - views.attachmentsCameraChangeAction.apply { - setImageDrawable( - context?.getDrawable(R.drawable.ic_camera_plain) - ) - contentDescription = getString(R.string.attachment_camera_photo) - } - } - MediaType.IMAGE -> { - views.attachmentsCameraCaptureAction.setImageDrawable( - context?.getDrawable(R.drawable.ic_camera_plain) - ) - views.attachmentsCameraChangeAction.apply { - setImageDrawable( - context?.getDrawable(R.drawable.ic_video) - ) - contentDescription = getString(R.string.attachment_camera_video) - } - } - } - } - } - @SuppressLint("RestrictedApi") private fun setRotation() { withState(viewModel) { state -> arrayOf( views.attachmentsCameraFlip, views.attachmentsCameraFlash, - views.attachmentsCameraChangeAction, views.attachmentsCameraCaptureAction, ).forEach { it.rotation = when (state.rotation) { @@ -250,7 +225,7 @@ class AttachmentsCameraFragment : @SuppressLint("UseCompatLoadingForDrawables") private fun setFlashButton() { withState(viewModel) { state -> - if (state.captureMode == MediaType.VIDEO || state.cameraSelector != CameraSelector.DEFAULT_BACK_CAMERA) { + if (state.cameraSelector != CameraSelector.DEFAULT_BACK_CAMERA) { views.attachmentsCameraFlash.isVisible = false } else { views.attachmentsCameraFlash.apply { diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraModel.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraModel.kt index 804cc82fe04..8ba9f51f411 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraModel.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraModel.kt @@ -60,7 +60,6 @@ class AttachmentsCameraModel @AssistedInject constructor( override fun handle(action: AttachmentsCameraAction) { when (action) { AttachmentsCameraAction.ChangeLensFacing -> changeLensFacing() - AttachmentsCameraAction.ChangeCaptureMode -> changeCaptureMode() AttachmentsCameraAction.RotateFlashMode -> rotateFlashMode() is AttachmentsCameraAction.SetRotation -> setRotation(action.rotation) is AttachmentsCameraAction.Capture -> capture(action) @@ -79,17 +78,6 @@ class AttachmentsCameraModel @AssistedInject constructor( } } - private fun changeCaptureMode() { - setState { - copy( - captureMode = when (captureMode) { - MediaType.IMAGE -> MediaType.VIDEO - MediaType.VIDEO -> MediaType.IMAGE - } - ) - } - } - private fun rotateFlashMode() { setState { copy( @@ -111,21 +99,21 @@ class AttachmentsCameraModel @AssistedInject constructor( } } - private fun capture(action: AttachmentsCameraAction.Capture) = withState { state -> - when (state.captureMode) { - MediaType.IMAGE -> { - action.imageCapture?.let { - capture(action.context, action.imageCapture) - true - } - } - MediaType.VIDEO -> { - action.videoCapture?.let { - capture(action.context, action.videoCapture) - true - } - } - } ?: _viewEvents.post(AttachmentsCameraViewEvents.SetErrorAndFinish) + private fun capture(action: AttachmentsCameraAction.Capture) { + recording?.let { + recording?.stop() + recording = null + return + } + action.videoCapture?.let { + capture(action.context, action.videoCapture) + return + } + action.imageCapture?.let { + capture(action.context, action.imageCapture) + return + } + _viewEvents.post(AttachmentsCameraViewEvents.SetErrorAndFinish) } private fun capture(context: Context, imageCapture: ImageCapture) { @@ -168,14 +156,11 @@ class AttachmentsCameraModel @AssistedInject constructor( } } - @SuppressLint("UseCompatLoadingForDrawables") - private fun capture(context: Context, videoCapture: VideoCapture) { + @SuppressLint("UseCompatLoadingForDrawables", "RestrictedApi") + private fun capture(context: Context, videoCapture: VideoCapture) = withState { state -> Timber.d("Capturing Video") - recording?.let { - recording?.stop() - recording = null - return - } + + videoCapture.camera?.cameraControl?.enableTorch(state.flashMode == ImageCapture.FLASH_MODE_ON) val file = createTempFile(context, MediaType.VIDEO) val outputUri = getUri(context, file) diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraState.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraState.kt index 541651db37f..9b3ceaa5fdc 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraState.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraState.kt @@ -22,7 +22,6 @@ import androidx.camera.core.ImageCapture import com.airbnb.mvrx.MavericksState data class AttachmentsCameraState( - val captureMode: MediaType = MediaType.IMAGE, val cameraSelector: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA, val flashMode: Int = ImageCapture.FLASH_MODE_AUTO, val rotation: Int = Surface.ROTATION_0, diff --git a/vector/src/main/res/layout/fragment_attachments_camera.xml b/vector/src/main/res/layout/fragment_attachments_camera.xml index 6c87727f6e5..49818a2108c 100644 --- a/vector/src/main/res/layout/fragment_attachments_camera.xml +++ b/vector/src/main/res/layout/fragment_attachments_camera.xml @@ -23,15 +23,15 @@ /> @@ -44,7 +44,7 @@ android:elevation="2dp" android:scaleX="1.3" android:scaleY="1.3" - android:src="@drawable/ic_camera_plain" + android:src="@drawable/bg_rounded_button" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -78,18 +78,4 @@ android:visibility="gone" android:contentDescription="@string/attachment_camera_chronometer" /> - - From 916cc2ec4229c8fc494aadf3338d23f4a76c0367 Mon Sep 17 00:00:00 2001 From: sim Date: Mon, 5 Sep 2022 22:49:24 +0200 Subject: [PATCH 24/35] Import drawable and fix colors --- .../attachments/camera/AttachmentsCameraFragment.kt | 2 +- vector/src/main/res/drawable/ic_material_record.xml | 5 +++++ vector/src/main/res/drawable/ic_material_stop.xml | 5 +++++ .../main/res/layout/fragment_attachments_camera.xml | 12 +++++++----- 4 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 vector/src/main/res/drawable/ic_material_record.xml create mode 100644 vector/src/main/res/drawable/ic_material_stop.xml diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt index ddd873c2ecd..e42387903dc 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt @@ -117,7 +117,7 @@ class AttachmentsCameraFragment : when (it) { AttachmentsCameraViewEvents.StartRecording -> { views.attachmentsCameraCaptureAction.setImageDrawable( - context?.getDrawable(R.drawable.ic_video_off) + context?.getDrawable(R.drawable.ic_material_stop) ) views.attachmentsCameraFlip.isEnabled = false views.attachmentsCameraFlash.isEnabled = false diff --git a/vector/src/main/res/drawable/ic_material_record.xml b/vector/src/main/res/drawable/ic_material_record.xml new file mode 100644 index 00000000000..57fc55c15d9 --- /dev/null +++ b/vector/src/main/res/drawable/ic_material_record.xml @@ -0,0 +1,5 @@ + + + diff --git a/vector/src/main/res/drawable/ic_material_stop.xml b/vector/src/main/res/drawable/ic_material_stop.xml new file mode 100644 index 00000000000..19bcbee7993 --- /dev/null +++ b/vector/src/main/res/drawable/ic_material_stop.xml @@ -0,0 +1,5 @@ + + + diff --git a/vector/src/main/res/layout/fragment_attachments_camera.xml b/vector/src/main/res/layout/fragment_attachments_camera.xml index 49818a2108c..eb223ced110 100644 --- a/vector/src/main/res/layout/fragment_attachments_camera.xml +++ b/vector/src/main/res/layout/fragment_attachments_camera.xml @@ -15,7 +15,8 @@ android:layout_height="wrap_content" android:indeterminate="true" android:visibility="invisible" - app:trackColor="@color/alert_default_icon_color" + app:trackColor="?colorBackgroundFloating" + app:indicatorColor="?colorPrimary" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" @@ -42,12 +43,13 @@ android:layout_height="wrap_content" android:layout_marginBottom="50dp" android:elevation="2dp" - android:scaleX="1.3" - android:scaleY="1.3" - android:src="@drawable/bg_rounded_button" + android:scaleX="1.7" + android:scaleY="1.7" + android:src="@drawable/ic_material_record" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" + app:fabSize="mini" android:contentDescription="@string/attachment_camera_capture" /> Date: Tue, 6 Sep 2022 09:13:01 +0200 Subject: [PATCH 25/35] Fix UI test for the video camera --- .../java/im/vector/app/ui/robot/RoomDetailRobot.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt index 6defc71922e..99891f3f599 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt @@ -28,6 +28,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import com.adevinta.android.barista.interaction.BaristaClickInteractions import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn +import com.adevinta.android.barista.interaction.BaristaClickInteractions.longClickOn import com.adevinta.android.barista.interaction.BaristaEditTextInteractions.writeTo import com.adevinta.android.barista.interaction.BaristaMenuClickInteractions.clickMenu import com.adevinta.android.barista.interaction.BaristaMenuClickInteractions.openMenu @@ -86,11 +87,8 @@ class RoomDetailRobot { fun cameraVideoFront() { clickOn(R.id.attachmentButton) clickOn(R.id.attachmentCameraButton) - clickOn(R.id.attachmentsCameraChangeAction) clickOn(R.id.attachmentsCameraFlip) - clickOn(R.id.attachmentsCameraCaptureAction) - sleep(1_000) - clickOn(R.id.attachmentsCameraCaptureAction) + longClickOn(R.id.attachmentsCameraCaptureAction) waitAttachmentPreviewer() clickOn(R.id.attachmentPreviewerSendButton) } From 8cad95d002c9d09421dee81f03604c85ded26357 Mon Sep 17 00:00:00 2001 From: sim Date: Tue, 6 Sep 2022 20:21:37 +0200 Subject: [PATCH 26/35] Better style/UX Use simpleActivityWaitingView, stop camera preview Remove unused executer --- .../src/main/res/values/styles_text_view.xml | 6 ++++++ .../camera/AttachmentsCameraActivity.kt | 6 ++++++ .../camera/AttachmentsCameraFragment.kt | 19 ++++++++----------- .../layout/fragment_attachments_camera.xml | 19 ++----------------- vector/src/main/res/values/strings.xml | 2 -- 5 files changed, 22 insertions(+), 30 deletions(-) diff --git a/library/ui-styles/src/main/res/values/styles_text_view.xml b/library/ui-styles/src/main/res/values/styles_text_view.xml index 0dcaf30f483..4336c342035 100644 --- a/library/ui-styles/src/main/res/values/styles_text_view.xml +++ b/library/ui-styles/src/main/res/values/styles_text_view.xml @@ -53,4 +53,10 @@ 16sp + + diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraActivity.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraActivity.kt index c7907120989..3266d309552 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraActivity.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraActivity.kt @@ -18,6 +18,7 @@ package im.vector.app.features.attachments.camera import android.content.Intent import android.provider.MediaStore +import androidx.core.view.isVisible import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.extensions.addFragment import im.vector.app.core.platform.VectorBaseActivity @@ -26,6 +27,7 @@ import im.vector.app.features.themes.ActivityOtherThemes @AndroidEntryPoint class AttachmentsCameraActivity : VectorBaseActivity() { + override fun getOtherThemes() = ActivityOtherThemes.AttachmentsPreview override fun getBinding() = ActivitySimpleBinding.inflate(layoutInflater) @@ -51,4 +53,8 @@ class AttachmentsCameraActivity : VectorBaseActivity() { setResult(RESULT_CANCELED, resultIntent) finish() } + + fun showWaitingView() { + views.simpleActivityWaitingView.isVisible = true + } } diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt index e42387903dc..a53093b4e91 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt @@ -52,8 +52,6 @@ import im.vector.app.core.platform.VectorMenuProvider import im.vector.app.core.time.Clock import im.vector.app.databinding.FragmentAttachmentsCameraBinding import timber.log.Timber -import java.util.concurrent.ExecutorService -import java.util.concurrent.Executors import javax.inject.Inject @AndroidEntryPoint @@ -66,10 +64,10 @@ class AttachmentsCameraFragment : private var imageCapture: ImageCapture? = null private var videoCapture: VideoCapture? = null + private var preview: Preview? = null private var currentLens: Int? = null private lateinit var camera: Camera - private lateinit var cameraExecutor: ExecutorService private val gestureListener = object : ScaleGestureDetector.SimpleOnScaleGestureListener() { override fun onScale(detector: ScaleGestureDetector): Boolean { @@ -125,7 +123,12 @@ class AttachmentsCameraFragment : views.attachmentsCameraChronometer.base = SystemClock.elapsedRealtime() views.attachmentsCameraChronometer.start() } - AttachmentsCameraViewEvents.TakePhoto -> views.attachmentsCameraLoading.isVisible = true + AttachmentsCameraViewEvents.TakePhoto -> { + (activity as AttachmentsCameraActivity?)?.showWaitingView() + context?.let { context -> + ProcessCameraProvider.getInstance(context).get().unbind(preview) + } + } AttachmentsCameraViewEvents.SetErrorAndFinish -> { views.attachmentsCameraChronometer.stop() (activity as AttachmentsCameraActivity).setErrorAndFinish() @@ -174,7 +177,6 @@ class AttachmentsCameraFragment : scaleGestureDetector.onTouchEvent(event) return@setOnTouchListener true } - cameraExecutor = Executors.newSingleThreadExecutor() } override fun invalidate() { @@ -268,7 +270,7 @@ class AttachmentsCameraFragment : val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() // Preview - val preview = Preview.Builder() + preview = Preview.Builder() .build() .also { it.setSurfaceProvider(views.viewFinder.surfaceProvider) @@ -300,11 +302,6 @@ class AttachmentsCameraFragment : } } - override fun onDestroyView() { - cameraExecutor.shutdown() - super.onDestroyView() - } - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentAttachmentsCameraBinding { return FragmentAttachmentsCameraBinding.inflate(inflater, container, false) } diff --git a/vector/src/main/res/layout/fragment_attachments_camera.xml b/vector/src/main/res/layout/fragment_attachments_camera.xml index eb223ced110..1c9b7b17a98 100644 --- a/vector/src/main/res/layout/fragment_attachments_camera.xml +++ b/vector/src/main/res/layout/fragment_attachments_camera.xml @@ -9,20 +9,6 @@ android:layout_width="match_parent" android:layout_height="match_parent" /> - - diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index f8c67fd2a0d..11fb3c83d82 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3200,8 +3200,6 @@ Please note: this is a labs feature using a temporary implementation. This means you will not be able to delete your location history, and advanced users will be able to see your location history even after you stop sharing your live location with this room. Enable location sharing Open camera for a video - Take a photo - Take a video Capture Change camera Enable Built-in Camera From 23fa3c91147e1acd01c776459f16d84857dfb78e Mon Sep 17 00:00:00 2001 From: sim Date: Tue, 6 Sep 2022 21:19:15 +0200 Subject: [PATCH 27/35] Use OnImageCapturedCallback to improve latency --- .../camera/AttachmentsCameraModel.kt | 61 ++++++++++++------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraModel.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraModel.kt index 8ba9f51f411..cc660d3feb6 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraModel.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraModel.kt @@ -19,11 +19,14 @@ package im.vector.app.features.attachments.camera import android.Manifest import android.annotation.SuppressLint import android.content.Context +import android.graphics.Bitmap.CompressFormat +import android.graphics.BitmapFactory import android.net.Uri import android.widget.Toast import androidx.camera.core.CameraSelector import androidx.camera.core.ImageCapture import androidx.camera.core.ImageCaptureException +import androidx.camera.core.ImageProxy import androidx.camera.video.FileOutputOptions import androidx.camera.video.Recorder import androidx.camera.video.Recording @@ -41,7 +44,10 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.time.Clock import timber.log.Timber +import java.io.ByteArrayOutputStream import java.io.File +import java.io.FileOutputStream +import java.nio.ByteBuffer class AttachmentsCameraModel @AssistedInject constructor( @Assisted val initialState: AttachmentsCameraState, @@ -117,45 +123,54 @@ class AttachmentsCameraModel @AssistedInject constructor( } private fun capture(context: Context, imageCapture: ImageCapture) { - _viewEvents.post(AttachmentsCameraViewEvents.TakePhoto) withState { state -> imageCapture.flashMode = state.flashMode val file = createTempFile(context, MediaType.IMAGE) val outputUri = getUri(context, file) - // Create output options object which contains file + metadata - val outputOptions = ImageCapture.OutputFileOptions - .Builder(file) - .build() - - // Set up image capture listener, which is triggered after photo has - // been taken imageCapture.takePicture( - outputOptions, ContextCompat.getMainExecutor(context), - object : ImageCapture.OnImageSavedCallback { - override fun onError(exc: ImageCaptureException) { - Timber.e("Photo capture failed: ${exc.message}", exc) - Toast.makeText(context, "An error occurred", Toast.LENGTH_SHORT).show() - _viewEvents.post(AttachmentsCameraViewEvents.SetErrorAndFinish) + object : ImageCapture.OnImageCapturedCallback() { + override fun onCaptureSuccess(image: ImageProxy) { + _viewEvents.post(AttachmentsCameraViewEvents.TakePhoto) + saveImageProxyToFile(image, file)?.let { + _viewEvents.post( + AttachmentsCameraViewEvents.SetResultAndFinish( + AttachmentsCameraOutput( + type = MediaType.IMAGE, + uri = outputUri + ) + ) + ) + } ?: _viewEvents.post(AttachmentsCameraViewEvents.SetErrorAndFinish) } - override fun onImageSaved(output: ImageCapture.OutputFileResults) { - _viewEvents.post( - AttachmentsCameraViewEvents.SetResultAndFinish( - AttachmentsCameraOutput( - type = MediaType.IMAGE, - uri = outputUri - ) - ) - ) + override fun onError(exception: ImageCaptureException) { + Timber.e("Photo capture failed: ${exception.message}", exception) + Toast.makeText(context, "An error occurred", Toast.LENGTH_SHORT).show() + _viewEvents.post(AttachmentsCameraViewEvents.SetErrorAndFinish) } } ) } } + private fun saveImageProxyToFile(image: ImageProxy, file: File): Boolean? { + val buffer: ByteBuffer = image.planes[0].buffer + val bytes = ByteArray(buffer.capacity()) + buffer.get(bytes) + return BitmapFactory.decodeByteArray(bytes, 0, bytes.size, null)?.let { bitmap -> + val bos = ByteArrayOutputStream() + bitmap.compress(CompressFormat.JPEG, 90, bos) + val fd = FileOutputStream(file) + fd.write(bos.toByteArray()) + fd.flush() + fd.close() + true + } + } + @SuppressLint("UseCompatLoadingForDrawables", "RestrictedApi") private fun capture(context: Context, videoCapture: VideoCapture) = withState { state -> Timber.d("Capturing Video") From d5039dc00f3cb3cf4e01b0fa86e12f06e957e3fb Mon Sep 17 00:00:00 2001 From: sim Date: Tue, 6 Sep 2022 22:04:59 +0200 Subject: [PATCH 28/35] Fix UI tests for the camera --- .../im/vector/app/ui/robot/RoomDetailRobot.kt | 4 +- .../ui/robot/interaction/ClickInteractions.kt | 28 ++++++++++ .../app/ui/robot/interaction/VeryLongClick.kt | 55 +++++++++++++++++++ .../app/ui/robot/interaction/ViewActions.kt | 39 +++++++++++++ 4 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 vector/src/androidTest/java/im/vector/app/ui/robot/interaction/ClickInteractions.kt create mode 100644 vector/src/androidTest/java/im/vector/app/ui/robot/interaction/VeryLongClick.kt create mode 100644 vector/src/androidTest/java/im/vector/app/ui/robot/interaction/ViewActions.kt diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt index 99891f3f599..68b858ac02f 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/RoomDetailRobot.kt @@ -28,7 +28,6 @@ import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import com.adevinta.android.barista.interaction.BaristaClickInteractions import com.adevinta.android.barista.interaction.BaristaClickInteractions.clickOn -import com.adevinta.android.barista.interaction.BaristaClickInteractions.longClickOn import com.adevinta.android.barista.interaction.BaristaEditTextInteractions.writeTo import com.adevinta.android.barista.interaction.BaristaMenuClickInteractions.clickMenu import com.adevinta.android.barista.interaction.BaristaMenuClickInteractions.openMenu @@ -39,6 +38,7 @@ import im.vector.app.features.home.room.detail.timeline.action.MessageActionsBot import im.vector.app.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet import im.vector.app.features.reactions.data.EmojiDataSource import im.vector.app.interactWithSheet +import im.vector.app.ui.robot.interaction.ClickInteractions.veryLongClickOn import im.vector.app.withRetry import java.lang.Thread.sleep @@ -88,7 +88,7 @@ class RoomDetailRobot { clickOn(R.id.attachmentButton) clickOn(R.id.attachmentCameraButton) clickOn(R.id.attachmentsCameraFlip) - longClickOn(R.id.attachmentsCameraCaptureAction) + veryLongClickOn(R.id.attachmentsCameraCaptureAction) waitAttachmentPreviewer() clickOn(R.id.attachmentPreviewerSendButton) } diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/interaction/ClickInteractions.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/interaction/ClickInteractions.kt new file mode 100644 index 00000000000..bdb53521b4d --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/interaction/ClickInteractions.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2022 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.ui.robot.interaction + +import com.adevinta.android.barista.internal.performAction +import com.adevinta.android.barista.internal.util.resourceMatcher +import im.vector.app.ui.robot.interaction.ViewActions.veryLongClick + +object ClickInteractions { + @JvmStatic + fun veryLongClickOn(resId: Int) { + resId.resourceMatcher().performAction(veryLongClick()) + } +} diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/interaction/VeryLongClick.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/interaction/VeryLongClick.kt new file mode 100644 index 00000000000..3b34a56a752 --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/interaction/VeryLongClick.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2022 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.ui.robot.interaction + +import android.view.ViewConfiguration +import androidx.test.espresso.UiController +import androidx.test.espresso.action.MotionEvents +import androidx.test.espresso.action.Tapper + +object VeryLongClick : Tapper { + override fun sendTap(uiController: UiController?, + coordinates: FloatArray?, + precision: FloatArray?, + inputDevice: Int, + buttonState: Int): Tapper.Status { + checkNotNull(uiController) + checkNotNull(coordinates) + checkNotNull(precision) + + val downEvent = MotionEvents.sendDown(uiController, coordinates, precision, inputDevice, buttonState).down + try { + // Duration before a press turns into a long press. + // Factor 1.5 is needed, otherwise a long press is not safely detected. + // See android.test.TouchUtils longClickView + // Factor 10 is needed to simulate user still pressing after the longClick is detected + val longPressTimeout = (ViewConfiguration.getLongPressTimeout() * 10f).toLong() + uiController.loopMainThreadForAtLeast(longPressTimeout) + if (!MotionEvents.sendUp(uiController, downEvent)) { + MotionEvents.sendCancel(uiController, downEvent) + return Tapper.Status.FAILURE + } + } finally { + downEvent!!.recycle() + } + return Tapper.Status.SUCCESS + } + + override fun sendTap(uiController: UiController?, coordinates: FloatArray?, precision: FloatArray?): Tapper.Status { + return sendTap(uiController, coordinates, precision, 0, 0) + } +} diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/interaction/ViewActions.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/interaction/ViewActions.kt new file mode 100644 index 00000000000..e393da61fd0 --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/interaction/ViewActions.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2022 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.ui.robot.interaction + +import android.view.InputDevice +import android.view.MotionEvent +import androidx.test.espresso.ViewAction +import androidx.test.espresso.action.GeneralClickAction +import androidx.test.espresso.action.GeneralLocation +import androidx.test.espresso.action.Press +import androidx.test.espresso.action.ViewActions + +object ViewActions { + fun veryLongClick(): ViewAction { + return ViewActions.actionWithAssertions( + GeneralClickAction( + VeryLongClick, + GeneralLocation.CENTER, + Press.FINGER, + InputDevice.SOURCE_UNKNOWN, + MotionEvent.ACTION_DOWN + ) + ) + } +} From d43b14447cb32f6f1664a5f7d7f00899829a8fa5 Mon Sep 17 00:00:00 2001 From: sim Date: Tue, 6 Sep 2022 22:35:24 +0200 Subject: [PATCH 29/35] Add Camera Action description --- .../src/main/res/values/styles_text_view.xml | 4 +++ .../camera/AttachmentsCameraFragment.kt | 2 ++ .../layout/fragment_attachments_camera.xml | 27 +++++++++++++------ vector/src/main/res/values/strings.xml | 1 + 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/library/ui-styles/src/main/res/values/styles_text_view.xml b/library/ui-styles/src/main/res/values/styles_text_view.xml index 4336c342035..a92a4f94d0c 100644 --- a/library/ui-styles/src/main/res/values/styles_text_view.xml +++ b/library/ui-styles/src/main/res/values/styles_text_view.xml @@ -56,6 +56,10 @@ + + diff --git a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt index a53093b4e91..dfa5e154c65 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/camera/AttachmentsCameraFragment.kt @@ -19,6 +19,7 @@ package im.vector.app.features.attachments.camera import android.Manifest import android.annotation.SuppressLint import android.content.pm.PackageManager +import android.opengl.Visibility import android.os.Build import android.os.Bundle import android.os.SystemClock @@ -148,6 +149,7 @@ class AttachmentsCameraFragment : } views.attachmentsCameraCaptureAction.setOnTouchListener { _, motionEvent -> + views.attachmentCameraActionDescription.visibility = View.INVISIBLE if (motionEvent.action == MotionEvent.ACTION_UP) { context?.let { viewModel.handle(AttachmentsCameraAction.Capture(it, imageCapture)) diff --git a/vector/src/main/res/layout/fragment_attachments_camera.xml b/vector/src/main/res/layout/fragment_attachments_camera.xml index 1c9b7b17a98..5fba7dcaa86 100644 --- a/vector/src/main/res/layout/fragment_attachments_camera.xml +++ b/vector/src/main/res/layout/fragment_attachments_camera.xml @@ -9,17 +9,28 @@ android:layout_width="match_parent" android:layout_height="match_parent" /> + + @@ -27,12 +38,12 @@ android:id="@+id/attachmentsCameraCaptureAction" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginBottom="50dp" + android:layout_marginBottom="24dp" android:elevation="2dp" android:scaleX="1.7" android:scaleY="1.7" android:src="@drawable/ic_material_record" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@id/attachmentCameraActionDescription" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:fabSize="mini" @@ -42,12 +53,12 @@ android:id="@+id/attachmentsCameraFlip" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginBottom="50dp" + android:layout_marginBottom="24dp" android:elevation="2dp" android:scaleX="0.7" android:scaleY="0.7" android:src="@drawable/ic_video_flip" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@id/attachmentCameraActionDescription" app:layout_constraintStart_toEndOf="@id/attachmentsCameraCaptureAction" app:layout_constraintEnd_toEndOf="parent" android:contentDescription="@string/attachment_camera_flip" /> @@ -56,13 +67,13 @@ android:id="@+id/attachmentsCameraChronometer" android:layout_width="wrap_content" android:layout_height="wrap_content" - style="@style/Widget.Vector.TextView.Floating" + style="@style/Widget.Vector.TextView.Floating.Bold" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toTopOf="@id/attachmentsCameraCaptureAction" android:layout_marginBottom="32dp" android:enabled="true" - android:visibility="invisible" + android:visibility="visible" android:contentDescription="@string/attachment_camera_chronometer" /> diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 11fb3c83d82..d7ee28bf879 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3207,6 +3207,7 @@ Enable flash Set flash automatically Chronometer + Tap for photo, hold for video %d message removed From 64f3b2e29a3894453b3474e17c853004a7d5ff6a Mon Sep 17 00:00:00 2001 From: sim Date: Mon, 12 Sep 2022 23:05:56 +0200 Subject: [PATCH 30/35] Fix chronometer visibility --- vector/src/main/res/layout/fragment_attachments_camera.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/res/layout/fragment_attachments_camera.xml b/vector/src/main/res/layout/fragment_attachments_camera.xml index 5fba7dcaa86..a43b45ebbb1 100644 --- a/vector/src/main/res/layout/fragment_attachments_camera.xml +++ b/vector/src/main/res/layout/fragment_attachments_camera.xml @@ -73,7 +73,7 @@ app:layout_constraintBottom_toTopOf="@id/attachmentsCameraCaptureAction" android:layout_marginBottom="32dp" android:enabled="true" - android:visibility="visible" + android:visibility="invisible" android:contentDescription="@string/attachment_camera_chronometer" /> From 4eac12d37d6d700755accfb62350cea726d55237 Mon Sep 17 00:00:00 2001 From: sim Date: Mon, 12 Sep 2022 23:22:14 +0200 Subject: [PATCH 31/35] Change text size and chrono margin --- library/ui-styles/src/main/res/values/styles_text_view.xml | 2 +- vector/src/main/res/layout/fragment_attachments_camera.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/ui-styles/src/main/res/values/styles_text_view.xml b/library/ui-styles/src/main/res/values/styles_text_view.xml index a92a4f94d0c..0d51e218bda 100644 --- a/library/ui-styles/src/main/res/values/styles_text_view.xml +++ b/library/ui-styles/src/main/res/values/styles_text_view.xml @@ -56,7 +56,7 @@