• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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