Skip to content

Commit 482abce

Browse files
committed
Aperture: Configure camera after we have camera permissions
Won't hurt, especially on older SDK versions Change-Id: I6029039a32579d08e5c4cf8a534c732f96938b70
1 parent 90c6cd2 commit 482abce

File tree

3 files changed

+92
-80
lines changed

3 files changed

+92
-80
lines changed

app/src/main/java/org/lineageos/aperture/CameraActivity.kt

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -405,25 +405,6 @@ open class CameraActivity : AppCompatActivity(R.layout.activity_camera) {
405405
}
406406
}
407407

408-
if (viewModel.initialCameraMode == CameraMode.VIDEO && !viewModel.isVideoRecordingAvailable.value) {
409-
// If an app asked for a video we have to bail out
410-
if (viewModel.inSingleCaptureMode.value) {
411-
Toast.makeText(
412-
this, getString(R.string.camcorder_unsupported_toast), Toast.LENGTH_LONG
413-
).show()
414-
finish()
415-
return
416-
}
417-
// Fallback to photo mode
418-
viewModel.initialCameraMode = CameraMode.PHOTO
419-
}
420-
421-
// Initialize the camera configuration
422-
if (!viewModel.initializeCameraConfiguration()) {
423-
noCamera()
424-
return
425-
}
426-
427408
// Setup window insets
428409
ViewCompat.setOnApplyWindowInsetsListener(mainLayout) { _, windowInsets ->
429410
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
@@ -1407,6 +1388,26 @@ open class CameraActivity : AppCompatActivity(R.layout.activity_camera) {
14071388
screenFlashView.setController(viewModel.cameraController)
14081389
screenFlashView.setScreenFlashWindow(window)
14091390

1391+
val videoRecordingAvailable = viewModel.isVideoRecordingAvailable()
1392+
if (viewModel.initialCameraMode == CameraMode.VIDEO && !videoRecordingAvailable) {
1393+
// If an app asked for a video we have to bail out
1394+
if (viewModel.inSingleCaptureMode.value) {
1395+
Toast.makeText(
1396+
this@CameraActivity,
1397+
R.string.camcorder_unsupported_toast,
1398+
Toast.LENGTH_LONG,
1399+
).show()
1400+
finish()
1401+
return@collect
1402+
}
1403+
// Fallback to photo mode
1404+
viewModel.initialCameraMode = CameraMode.PHOTO
1405+
}
1406+
1407+
if (!viewModel.initializeCameraConfiguration()) {
1408+
noCamera()
1409+
}
1410+
14101411
initialized = true
14111412
}
14121413
}

app/src/main/java/org/lineageos/aperture/repositories/CameraRepository.kt

Lines changed: 40 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package org.lineageos.aperture.repositories
77

8+
import android.Manifest
89
import android.content.Context
910
import androidx.camera.camera2.interop.Camera2CameraInfo
1011
import androidx.camera.camera2.interop.ExperimentalCamera2Interop
@@ -17,79 +18,87 @@ import kotlinx.coroutines.CoroutineScope
1718
import kotlinx.coroutines.Dispatchers
1819
import kotlinx.coroutines.ExperimentalCoroutinesApi
1920
import kotlinx.coroutines.flow.SharingStarted
20-
import kotlinx.coroutines.flow.flowOf
21+
import kotlinx.coroutines.flow.asFlow
2122
import kotlinx.coroutines.flow.flowOn
2223
import kotlinx.coroutines.flow.mapLatest
23-
import kotlinx.coroutines.flow.stateIn
24+
import kotlinx.coroutines.flow.shareIn
25+
import org.lineageos.aperture.ext.permissionGranted
2426
import org.lineageos.aperture.models.Camera
2527

2628
/**
2729
* Repository that provides camera devices.
2830
*/
2931
@androidx.annotation.OptIn(ExperimentalCamera2Interop::class, ExperimentalLensFacing::class)
3032
class CameraRepository(
31-
context: Context,
33+
private val context: Context,
3234
coroutineScope: CoroutineScope,
3335
private val overlaysRepository: OverlaysRepository,
3436
) {
3537
/**
3638
* CameraX's [ProcessCameraProvider].
3739
*/
38-
private val cameraProvider = ProcessCameraProvider.getInstance(
39-
context
40-
).get()
40+
private val cameraProvider by lazy {
41+
requireCameraPermission()
42+
ProcessCameraProvider.getInstance(context).get()
43+
}
4144

4245
/**
4346
* CameraX's [ExtensionsManager].
4447
*/
45-
val extensionsManager: ExtensionsManager = ExtensionsManager.getInstanceAsync(
46-
context, cameraProvider
47-
).get()
48+
private val extensionsManager: ExtensionsManager by lazy {
49+
ExtensionsManager.getInstanceAsync(context, cameraProvider).get()
50+
}
4851

4952
/**
5053
* List of internal cameras. These should never change.
5154
*/
52-
val internalCameras = cameraProvider.availableCameraInfos
53-
.filter { cameraXCameraInfo ->
54-
cameraXCameraInfo.lensFacing != CameraSelector.LENS_FACING_EXTERNAL
55-
&& cameraXCameraInfo.isInternalCameraAllowed()
56-
}
57-
.mapToCamera()
55+
private val internalCameras by lazy {
56+
cameraProvider.availableCameraInfos
57+
.filter { cameraXCameraInfo ->
58+
cameraXCameraInfo.lensFacing != CameraSelector.LENS_FACING_EXTERNAL
59+
&& cameraXCameraInfo.isInternalCameraAllowed()
60+
}
61+
.mapToCamera()
62+
}
5863

59-
val mainBackCamera = internalCameras.firstOrNull { camera ->
60-
camera.cameraId == DEFAULT_BACK_CAMERA_ID
64+
val mainBackCamera by lazy {
65+
internalCameras.firstOrNull { camera ->
66+
camera.cameraId == DEFAULT_BACK_CAMERA_ID
67+
}
6168
}
6269

63-
val mainFrontCamera = internalCameras.firstOrNull { camera ->
64-
camera.cameraId == DEFAULT_FRONT_CAMERA_ID
70+
val mainFrontCamera by lazy {
71+
internalCameras.firstOrNull { camera ->
72+
camera.cameraId == DEFAULT_FRONT_CAMERA_ID
73+
}
6574
}
6675

6776
/**
6877
* List of external cameras. These will change once the user connects or disconnects a camera.
6978
*/
70-
val externalCameras = flowOf(
79+
private val externalCameras = suspend {
7180
cameraProvider.availableCameraInfos
7281
.filter { cameraXCameraInfo ->
7382
cameraXCameraInfo.lensFacing == CameraSelector.LENS_FACING_EXTERNAL
7483
}
7584
.mapToCamera()
76-
)
85+
}.asFlow()
7786
.flowOn(Dispatchers.IO)
78-
.stateIn(
87+
.shareIn(
7988
scope = coroutineScope,
80-
started = SharingStarted.Eagerly,
81-
initialValue = listOf(),
89+
started = SharingStarted.WhileSubscribed(),
90+
replay = 1,
8291
)
8392

8493
@OptIn(ExperimentalCoroutinesApi::class)
8594
val cameras = externalCameras.mapLatest { externalCameras ->
8695
internalCameras + externalCameras
8796
}
8897
.flowOn(Dispatchers.IO)
89-
.stateIn(
98+
.shareIn(
9099
scope = coroutineScope,
91-
started = SharingStarted.Eagerly,
92-
initialValue = internalCameras,
100+
started = SharingStarted.WhileSubscribed(),
101+
replay = 1,
93102
)
94103

95104
/**
@@ -140,6 +149,10 @@ class CameraRepository(
140149
overlaysRepository,
141150
)
142151

152+
private fun requireCameraPermission() = require(
153+
context.permissionGranted(Manifest.permission.CAMERA)
154+
) { "Camera permission not granted" }
155+
143156
companion object {
144157
private const val DEFAULT_BACK_CAMERA_ID = "0"
145158
private const val DEFAULT_FRONT_CAMERA_ID = "1"

app/src/main/java/org/lineageos/aperture/viewmodels/CameraViewModel.kt

Lines changed: 32 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import kotlinx.coroutines.flow.combine
4747
import kotlinx.coroutines.flow.drop
4848
import kotlinx.coroutines.flow.filter
4949
import kotlinx.coroutines.flow.filterNotNull
50+
import kotlinx.coroutines.flow.first
5051
import kotlinx.coroutines.flow.flatMapLatest
5152
import kotlinx.coroutines.flow.flowOf
5253
import kotlinx.coroutines.flow.flowOn
@@ -165,18 +166,6 @@ class CameraViewModel(application: Application) : ApertureViewModel(application)
165166
*/
166167
val event = _event.asSharedFlow()
167168

168-
@OptIn(ExperimentalCoroutinesApi::class)
169-
val isVideoRecordingAvailable = cameraRepository.cameras
170-
.mapLatest { cameras ->
171-
cameras.any { it.supportsCameraMode(CameraMode.VIDEO) }
172-
}
173-
.flowOn(Dispatchers.IO)
174-
.stateIn(
175-
scope = viewModelScope,
176-
started = SharingStarted.Eagerly,
177-
initialValue = true,
178-
)
179-
180169
/**
181170
* The initial camera mode.
182171
*/
@@ -250,7 +239,7 @@ class CameraViewModel(application: Application) : ApertureViewModel(application)
250239
.flowOn(Dispatchers.IO)
251240
.stateIn(
252241
scope = viewModelScope,
253-
started = SharingStarted.Eagerly,
242+
started = SharingStarted.WhileSubscribed(),
254243
initialValue = mapOf(),
255244
)
256245

@@ -989,7 +978,7 @@ class CameraViewModel(application: Application) : ApertureViewModel(application)
989978
* Initialize [cameraConfiguration].
990979
* [initialCameraMode] and [initialCameraFacing] must be initialized.
991980
*/
992-
fun initializeCameraConfiguration(): Boolean {
981+
suspend fun initializeCameraConfiguration(): Boolean {
993982
val cameraMode = initialCameraMode
994983
val cameraFacing = initialCameraFacing
995984

@@ -1282,27 +1271,29 @@ class CameraViewModel(application: Application) : ApertureViewModel(application)
12821271

12831272
fun setCameraMode(
12841273
cameraMode: CameraMode,
1285-
) = updateConfiguration<CameraConfiguration> { cameraConfiguration ->
1286-
if (cameraConfiguration.cameraMode == cameraMode) {
1287-
return@updateConfiguration cameraConfiguration
1288-
}
1274+
) = viewModelScope.launch {
1275+
updateConfiguration<CameraConfiguration> { cameraConfiguration ->
1276+
if (cameraConfiguration.cameraMode == cameraMode) {
1277+
return@updateConfiguration cameraConfiguration
1278+
}
12891279

1290-
val camera = cameraConfiguration.camera.takeIf {
1291-
it.supportsCameraMode(cameraMode)
1292-
} ?: getSuitableCamera(
1293-
cameraMode,
1294-
cameraConfiguration.camera.cameraFacing,
1295-
) ?: run {
1296-
Log.e(LOG_TAG, "No camera supports the requested camera mode $cameraMode")
1297-
return@updateConfiguration cameraConfiguration
1298-
}
1280+
val camera = cameraConfiguration.camera.takeIf {
1281+
it.supportsCameraMode(cameraMode)
1282+
} ?: getSuitableCamera(
1283+
cameraMode,
1284+
cameraConfiguration.camera.cameraFacing,
1285+
) ?: run {
1286+
Log.e(LOG_TAG, "No camera supports the requested camera mode $cameraMode")
1287+
return@updateConfiguration cameraConfiguration
1288+
}
12991289

1300-
preferencesRepository.lastCameraMode.value = cameraMode
1290+
preferencesRepository.lastCameraMode.value = cameraMode
13011291

1302-
createInitialCameraConfiguration(
1303-
camera = camera,
1304-
cameraMode = cameraMode,
1305-
)
1292+
createInitialCameraConfiguration(
1293+
camera = camera,
1294+
cameraMode = cameraMode,
1295+
)
1296+
}
13061297
}
13071298

13081299
/**
@@ -1615,6 +1606,13 @@ class CameraViewModel(application: Application) : ApertureViewModel(application)
16151606

16161607
fun fileExists(uri: Uri) = mediaRepository.fileExists(uri)
16171608

1609+
/**
1610+
* Get whether or not any camera currently supports video mode.
1611+
*/
1612+
suspend fun isVideoRecordingAvailable() = cameraRepository.cameras.first().any {
1613+
it.supportsCameraMode(CameraMode.VIDEO)
1614+
}
1615+
16181616
/**
16191617
* Emit a new event to let the activity handle it.
16201618
*/
@@ -1629,11 +1627,11 @@ class CameraViewModel(application: Application) : ApertureViewModel(application)
16291627
* @param cameraFacing The requested [CameraFacing]
16301628
* @return A [Camera] that is compatible with the provided configuration or null
16311629
*/
1632-
private fun getSuitableCamera(
1630+
private suspend fun getSuitableCamera(
16331631
cameraMode: CameraMode,
16341632
cameraFacing: CameraFacing,
16351633
): Camera? {
1636-
val compatibleCameras = cameraRepository.cameras.value
1634+
val compatibleCameras = cameraRepository.cameras.first()
16371635
.filter { camera ->
16381636
camera.supportsCameraMode(cameraMode)
16391637
}

0 commit comments

Comments
 (0)