1 /*
<lambda>null2 * Copyright (C) 2023 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package com.google.jetpackcamera
17
18 import android.app.Activity
19 import android.content.Intent
20 import android.content.pm.ActivityInfo
21 import android.net.Uri
22 import android.os.Build
23 import android.os.Bundle
24 import android.provider.MediaStore
25 import android.provider.Settings
26 import android.util.Log
27 import androidx.activity.ComponentActivity
28 import androidx.activity.compose.setContent
29 import androidx.activity.viewModels
30 import androidx.annotation.RequiresApi
31 import androidx.compose.foundation.background
32 import androidx.compose.foundation.isSystemInDarkTheme
33 import androidx.compose.foundation.layout.Arrangement
34 import androidx.compose.foundation.layout.Column
35 import androidx.compose.foundation.layout.fillMaxSize
36 import androidx.compose.foundation.layout.size
37 import androidx.compose.material3.CircularProgressIndicator
38 import androidx.compose.material3.MaterialTheme
39 import androidx.compose.material3.Surface
40 import androidx.compose.material3.Text
41 import androidx.compose.runtime.Composable
42 import androidx.compose.runtime.getValue
43 import androidx.compose.runtime.mutableStateOf
44 import androidx.compose.runtime.setValue
45 import androidx.compose.ui.Alignment
46 import androidx.compose.ui.ExperimentalComposeUiApi
47 import androidx.compose.ui.Modifier
48 import androidx.compose.ui.graphics.Color
49 import androidx.compose.ui.res.stringResource
50 import androidx.compose.ui.semantics.semantics
51 import androidx.compose.ui.semantics.testTagsAsResourceId
52 import androidx.compose.ui.unit.dp
53 import androidx.core.content.IntentCompat
54 import androidx.lifecycle.Lifecycle
55 import androidx.lifecycle.lifecycleScope
56 import androidx.lifecycle.repeatOnLifecycle
57 import androidx.tracing.Trace
58 import com.google.jetpackcamera.MainActivityUiState.Loading
59 import com.google.jetpackcamera.MainActivityUiState.Success
60 import com.google.jetpackcamera.core.common.traceFirstFrameMainActivity
61 import com.google.jetpackcamera.feature.preview.PreviewMode
62 import com.google.jetpackcamera.feature.preview.PreviewViewModel
63 import com.google.jetpackcamera.settings.model.DarkMode
64 import com.google.jetpackcamera.ui.JcaApp
65 import com.google.jetpackcamera.ui.theme.JetpackCameraTheme
66 import dagger.hilt.android.AndroidEntryPoint
67 import kotlinx.coroutines.CompletableDeferred
68 import kotlinx.coroutines.flow.collect
69 import kotlinx.coroutines.flow.onEach
70 import kotlinx.coroutines.launch
71
72 private const val TAG = "MainActivity"
73 private const val KEY_DEBUG_MODE = "KEY_DEBUG_MODE"
74
75 /**
76 * Activity for the JetpackCameraApp.
77 */
78 @AndroidEntryPoint(ComponentActivity::class)
79 class MainActivity : Hilt_MainActivity() {
80 private val viewModel: MainActivityViewModel by viewModels()
81
82 @RequiresApi(Build.VERSION_CODES.M)
83 @OptIn(ExperimentalComposeUiApi::class)
84 override fun onCreate(savedInstanceState: Bundle?) {
85 super.onCreate(savedInstanceState)
86 var uiState: MainActivityUiState by mutableStateOf(Loading)
87
88 lifecycleScope.launch {
89 lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
90 viewModel.uiState
91 .onEach {
92 uiState = it
93 }
94 .collect()
95 }
96 }
97
98 var firstFrameComplete: CompletableDeferred<Unit>? = null
99 if (Trace.isEnabled()) {
100 firstFrameComplete = CompletableDeferred()
101 // start trace between app starting and the earliest possible completed capture
102 lifecycleScope.launch {
103 traceFirstFrameMainActivity(cookie = 0) {
104 firstFrameComplete.await()
105 }
106 }
107 }
108
109 setContent {
110 when (uiState) {
111 Loading -> {
112 Column(
113 modifier = Modifier
114 .fillMaxSize()
115 .background(Color.Black),
116 verticalArrangement = Arrangement.Center,
117 horizontalAlignment = Alignment.CenterHorizontally
118 ) {
119 CircularProgressIndicator(modifier = Modifier.size(50.dp))
120 Text(text = stringResource(R.string.jca_loading), color = Color.White)
121 }
122 }
123
124 is Success -> {
125 // TODO(kimblebee@): add app setting to enable/disable dynamic color
126 JetpackCameraTheme(
127 darkTheme = isInDarkMode(uiState = uiState),
128 dynamicColor = false
129 ) {
130 Surface(
131 modifier = Modifier
132 .fillMaxSize()
133 .semantics {
134 testTagsAsResourceId = true
135 },
136 color = MaterialTheme.colorScheme.background
137 ) {
138 JcaApp(
139 previewMode = getPreviewMode(),
140 isDebugMode = isDebugMode,
141 openAppSettings = ::openAppSettings,
142 onRequestWindowColorMode = { colorMode ->
143 // Window color mode APIs require API level 26+
144 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
145 Log.d(
146 TAG,
147 "Setting window color mode to:" +
148 " ${colorMode.toColorModeString()}"
149 )
150 window?.colorMode = colorMode
151 }
152 },
153 onFirstFrameCaptureCompleted = {
154 firstFrameComplete?.complete(Unit)
155 }
156 )
157 }
158 }
159 }
160 }
161 }
162 }
163
164 private val isDebugMode: Boolean
165 get() = intent?.getBooleanExtra(KEY_DEBUG_MODE, false) ?: false
166
167 private fun getStandardMode(): PreviewMode.StandardMode {
168 return PreviewMode.StandardMode { event ->
169 if (event is PreviewViewModel.ImageCaptureEvent.ImageSaved) {
170 @Suppress("DEPRECATION")
171 val intent = Intent(android.hardware.Camera.ACTION_NEW_PICTURE)
172 intent.setData(event.savedUri)
173 sendBroadcast(intent)
174 }
175 }
176 }
177
178 private fun getExternalCaptureUri(): Uri? {
179 return IntentCompat.getParcelableExtra(
180 intent,
181 MediaStore.EXTRA_OUTPUT,
182 Uri::class.java
183 ) ?: intent?.clipData?.getItemAt(0)?.uri
184 }
185
186 private fun getMultipleExternalCaptureUri(): List<Uri>? {
187 val stringUris = intent.getStringArrayListExtra(MediaStore.EXTRA_OUTPUT)
188 if (stringUris.isNullOrEmpty()) {
189 return null
190 } else {
191 val result = mutableListOf<Uri>()
192 for (string in stringUris) {
193 result.add(Uri.parse(string))
194 }
195 return result
196 }
197 }
198
199 private fun getPreviewMode(): PreviewMode {
200 return intent?.action?.let { action ->
201 when (action) {
202 MediaStore.ACTION_IMAGE_CAPTURE ->
203 PreviewMode.ExternalImageCaptureMode(getExternalCaptureUri()) { event ->
204 Log.d(TAG, "onImageCapture, event: $event")
205 if (event is PreviewViewModel.ImageCaptureEvent.ImageSaved) {
206 val resultIntent = Intent()
207 resultIntent.putExtra(MediaStore.EXTRA_OUTPUT, event.savedUri)
208 setResult(RESULT_OK, resultIntent)
209 Log.d(TAG, "onImageCapture, finish()")
210 finish()
211 }
212 }
213
214 MediaStore.ACTION_VIDEO_CAPTURE ->
215 PreviewMode.ExternalVideoCaptureMode(getExternalCaptureUri()) { event ->
216 Log.d(TAG, "onVideoCapture, event: $event")
217 if (event is PreviewViewModel.VideoCaptureEvent.VideoSaved) {
218 val resultIntent = Intent()
219 resultIntent.putExtra(MediaStore.EXTRA_OUTPUT, event.savedUri)
220 setResult(RESULT_OK, resultIntent)
221 Log.d(TAG, "onVideoCapture, finish()")
222 finish()
223 }
224 }
225
226 MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA -> {
227 val uriList: List<Uri>? = getMultipleExternalCaptureUri()
228 val pictureTakenUriList: ArrayList<String?> = arrayListOf()
229 PreviewMode.ExternalMultipleImageCaptureMode(
230 uriList
231 ) { event: PreviewViewModel.ImageCaptureEvent, uriIndex: Int ->
232 Log.d(TAG, "onMultipleImageCapture, event: $event")
233 if (uriList == null) {
234 when (event) {
235 is PreviewViewModel.ImageCaptureEvent.ImageSaved ->
236 pictureTakenUriList.add(event.savedUri.toString())
237 is PreviewViewModel.ImageCaptureEvent.ImageCaptureError ->
238 pictureTakenUriList.add(event.exception.toString())
239 }
240 val resultIntent = Intent()
241 resultIntent.putStringArrayListExtra(
242 MediaStore.EXTRA_OUTPUT,
243 pictureTakenUriList
244 )
245 setResult(RESULT_OK, resultIntent)
246 } else if (uriIndex == uriList.size - 1) {
247 setResult(RESULT_OK, Intent())
248 Log.d(TAG, "onMultipleImageCapture, finish()")
249 finish()
250 }
251 }
252 }
253
254 else -> {
255 Log.w(TAG, "Ignoring external intent with unknown action.")
256 getStandardMode()
257 }
258 }
259 } ?: getStandardMode()
260 }
261 }
262
263 /**
264 * Determines whether the Theme should be in dark, light, or follow system theme
265 */
266 @Composable
isInDarkModenull267 private fun isInDarkMode(uiState: MainActivityUiState): Boolean = when (uiState) {
268 Loading -> isSystemInDarkTheme()
269 is Success -> when (uiState.cameraAppSettings.darkMode) {
270 DarkMode.DARK -> true
271 DarkMode.LIGHT -> false
272 DarkMode.SYSTEM -> isSystemInDarkTheme()
273 }
274 }
275
276 @RequiresApi(Build.VERSION_CODES.O)
toColorModeStringnull277 private fun Int.toColorModeString(): String {
278 return when (this) {
279 ActivityInfo.COLOR_MODE_DEFAULT -> "COLOR_MODE_DEFAULT"
280 ActivityInfo.COLOR_MODE_HDR -> "COLOR_MODE_HDR"
281 ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT -> "COLOR_MODE_WIDE_COLOR_GAMUT"
282 else -> "<Unknown>"
283 }
284 }
285
286 /**
287 * Open the app settings when necessary. I.e. to enable permissions that have been denied by a user
288 */
openAppSettingsnull289 private fun Activity.openAppSettings() {
290 Intent(
291 Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
292 Uri.fromParts("package", packageName, null)
293 ).also(::startActivity)
294 }
295