Skip to content

Add in-app camera #6763

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 35 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
51b905f
Add VideoCamera button for attachment
p1gp1g Jul 28, 2022
59546b0
Add internal Camera : take photo
p1gp1g Jul 30, 2022
8fcc9c2
Add internal Camera : record video
p1gp1g Aug 2, 2022
59e0c1e
Create tempfile in the fragment
p1gp1g Aug 2, 2022
6b4fef9
Change capture mode in camera view
p1gp1g Aug 5, 2022
764ef53
Change camera in camera view
p1gp1g Aug 5, 2022
a215fe2
Restore capture mode in camera view
p1gp1g Aug 5, 2022
e916709
Add gesture to zoom in camera view
p1gp1g Aug 5, 2022
f56a8fe
Remove intent action for camera activity
p1gp1g Aug 5, 2022
90e922b
Set the built-in camera as a lab preference
p1gp1g Aug 7, 2022
d534a7d
Control flash in camera view
p1gp1g Aug 7, 2022
9945231
Lint
p1gp1g Aug 7, 2022
0e3a8a1
Add progress indicator when taking a photo
p1gp1g Aug 7, 2022
8236a32
Add changelog detail
p1gp1g Aug 7, 2022
4856fc6
Add support for orientation
p1gp1g Aug 27, 2022
ec1e5e2
Camera: Follow the MVI pattern
p1gp1g Aug 29, 2022
3776305
Camera: Add UI tests
p1gp1g Aug 29, 2022
9bca9bd
Do not use "VectorCamera" for names
p1gp1g Aug 29, 2022
bbdfa6e
Fix dependencies.gradle for androidx.camera
p1gp1g Aug 29, 2022
08ae120
Enable orientationEventListener onStart
p1gp1g Sep 5, 2022
db5e582
Lint
p1gp1g Sep 5, 2022
d88371d
Add chronometer to camera view
p1gp1g Sep 5, 2022
ccf0c88
Use new UX for the camera
p1gp1g Sep 5, 2022
916cc2e
Import drawable and fix colors
p1gp1g Sep 5, 2022
772e97b
Fix UI test for the video camera
p1gp1g Sep 6, 2022
8cad95d
Better style/UX
p1gp1g Sep 6, 2022
23fa3c9
Use OnImageCapturedCallback to improve latency
p1gp1g Sep 6, 2022
d5039dc
Fix UI tests for the camera
p1gp1g Sep 6, 2022
d43b144
Add Camera Action description
p1gp1g Sep 6, 2022
64f3b2e
Fix chronometer visibility
p1gp1g Sep 12, 2022
4eac12d
Change text size and chrono margin
p1gp1g Sep 12, 2022
8114f5b
Use recording and done states
p1gp1g Sep 17, 2022
a28fb0d
Revert "Add VideoCamera button for attachment"
p1gp1g Sep 17, 2022
a5d87e6
Use common icon to flip camera
p1gp1g Sep 18, 2022
ae2a920
Fix image rotation
p1gp1g Oct 2, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/6763.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add an in-app camera
9 changes: 9 additions & 0 deletions dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -53,6 +54,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:$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",
'exifinterface' : "androidx.exifinterface:exifinterface:1.3.3",
Expand Down
2 changes: 2 additions & 0 deletions dependencies_groups.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@ ext.groups = [
],
mavenCentral: [
regex: [
'com\\.google\\.auto\\..*',
],
group: [
'com.google.auto',
'ch.qos.logback',
'com.adevinta.android',
'com.airbnb.android',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* 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 androidx.activity.result.ActivityResultLauncher
import im.vector.lib.multipicker.entity.MultiPickerImageType
import im.vector.lib.multipicker.entity.MultiPickerVideoType
import im.vector.lib.multipicker.utils.toMultiPickerImageType
import im.vector.lib.multipicker.utils.toMultiPickerVideoType

class BuiltInCameraPicker {

/**
* Start camera by using a ActivityResultLauncher.
* @return Uri of taken photo or null if the operation is cancelled.
*/
fun start(context: Context, activityResultLauncher: ActivityResultLauncher<Intent>, targetClass: Class<*>) {
val intent = Intent(context, targetClass)
activityResultLauncher.launch(intent)
}

/**
* 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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class MultiPicker<T> private constructor() {
val CONTACT by lazy { MultiPicker<ContactPicker>() }
val CAMERA by lazy { MultiPicker<CameraPicker>() }
val CAMERA_VIDEO by lazy { MultiPicker<CameraVideoPicker>() }
val BUILTIN_CAMERA by lazy { MultiPicker<BuiltInCameraPicker>() }

@Suppress("UNCHECKED_CAST")
fun <T> get(type: MultiPicker<T>): T {
Expand All @@ -39,6 +40,7 @@ class MultiPicker<T> private constructor() {
CONTACT -> ContactPicker() as T
CAMERA -> CameraPicker() as T
CAMERA_VIDEO -> CameraVideoPicker() as T
BUILTIN_CAMERA -> BuiltInCameraPicker() as T
else -> throw IllegalArgumentException("Unsupported type $type")
}
}
Expand Down
10 changes: 10 additions & 0 deletions library/ui-styles/src/main/res/values/styles_text_view.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,14 @@
<item name="lineHeight">16sp</item>
</style>

<style name="Widget.Vector.TextView.Floating">
<item name="android:textAppearance">@style/TextAppearance.Vector.Button</item>
<item name="android:textColor">?colorPrimary</item>
<item name="android:textSize">@dimen/text_size_body</item>
</style>

<style name="Widget.Vector.TextView.Floating.Bold">
<item name="android:textStyle">bold</item>
</style>

</resources>
9 changes: 9 additions & 0 deletions vector/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,15 @@ dependencies {
implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.11.0"

implementation libs.squareup.moshi

// Camera
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

// Lifecycle
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ class UiAllScreensSanityTest {
}
}

testCamera()
testThreadScreens()

val spaceName = UUID.randomUUID().toString()
Expand Down Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 -> {
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,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

Expand All @@ -64,6 +65,40 @@ 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.attachmentsCameraFlip)
veryLongClickOn(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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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())
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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
)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ enum class LabFeature {
LATEX_MATHEMATICS,
THREAD_MESSAGES,
AUTO_REPORT_ERRORS,
BUILTIN_CAMERA,
RENDER_USER_LOCATION
}
2 changes: 2 additions & 0 deletions vector/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,8 @@
<activity
android:name=".features.attachments.preview.AttachmentsPreviewActivity"
android:theme="@style/Theme.Vector.Black.AttachmentsPreview" />
<activity android:name=".features.attachments.camera.AttachmentsCameraActivity"
android:theme="@style/Theme.Vector.Black.AttachmentsPreview" />
<activity
android:name=".features.call.VectorCallActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Loading