diff --git a/Camera2SlowMotion/README.md b/Camera2SlowMotion/README.md index f2fa3d4f..71a03757 100644 --- a/Camera2SlowMotion/README.md +++ b/Camera2SlowMotion/README.md @@ -22,8 +22,8 @@ in an MP4 video file. Pre-requisites -------------- -- Android SDK 29+ -- Android Studio 3.5+ +- Android SDK 33+ +- Android Studio Panda 4 - Device with high-speed capture capability Screenshots diff --git a/Camera2SlowMotion/app/build.gradle b/Camera2SlowMotion/app/build.gradle index 017c2ed9..703ba89d 100644 --- a/Camera2SlowMotion/app/build.gradle +++ b/Camera2SlowMotion/app/build.gradle @@ -15,17 +15,15 @@ */ apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-kapt' apply plugin: "androidx.navigation.safeargs" android { - compileSdkVersion 29 + compileSdkVersion 37 defaultConfig { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" applicationId "com.android.example.camera2.slowmo" - minSdkVersion 23 - targetSdkVersion 29 + minSdkVersion 33 + targetSdkVersion 37 versionCode 1 versionName "1.0.0" } @@ -35,14 +33,10 @@ android { targetCompatibility rootProject.ext.java_version } - kotlinOptions { - jvmTarget = "$rootProject.ext.java_version" - } - buildTypes { release { minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } @@ -59,37 +53,39 @@ android { buildFeatures { viewBinding true + buildConfig true } + namespace 'com.example.android.camera2.slowmo' } dependencies { implementation project(':utils') // Kotlin lang - implementation 'androidx.core:core-ktx:1.3.0' + implementation 'androidx.core:core-ktx:1.18.0' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.4' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0' // App compat and UI things - implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0' - implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.appcompat:appcompat:1.7.1' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.10.0' + implementation 'androidx.constraintlayout:constraintlayout:2.2.1' // Navigation library - def nav_version = "2.2.2" + def nav_version = "2.9.8" implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" implementation "androidx.navigation:navigation-ui-ktx:$nav_version" // Unit testing - testImplementation 'androidx.test.ext:junit:1.1.1' - testImplementation 'androidx.test:rules:1.2.0' - testImplementation 'androidx.test:runner:1.2.0' - testImplementation 'androidx.test.espresso:espresso-core:3.2.0' - testImplementation "org.robolectric:robolectric:4.3.1" + testImplementation 'androidx.test.ext:junit:1.3.0' + testImplementation 'androidx.test:rules:1.7.0' + testImplementation 'androidx.test:runner:1.7.0' + testImplementation 'androidx.test.espresso:espresso-core:3.7.0' + testImplementation "org.robolectric:robolectric:4.16.1" // Instrumented testing - androidTestImplementation 'androidx.test.ext:junit:1.1.1' - androidTestImplementation 'androidx.test:rules:1.2.0' - androidTestImplementation 'androidx.test:runner:1.2.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + androidTestImplementation 'androidx.test.ext:junit:1.3.0' + androidTestImplementation 'androidx.test:rules:1.7.0' + androidTestImplementation 'androidx.test:runner:1.7.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0' } diff --git a/Camera2SlowMotion/app/src/main/AndroidManifest.xml b/Camera2SlowMotion/app/src/main/AndroidManifest.xml index 571499b6..94cea4f4 100644 --- a/Camera2SlowMotion/app/src/main/AndroidManifest.xml +++ b/Camera2SlowMotion/app/src/main/AndroidManifest.xml @@ -15,8 +15,7 @@ ~ limitations under the License. --> + xmlns:tools="http://schemas.android.com/tools"> @@ -26,7 +25,6 @@ @@ -34,6 +32,7 @@ diff --git a/Camera2SlowMotion/app/src/main/java/com/example/android/camera2/slowmo/CameraActivity.kt b/Camera2SlowMotion/app/src/main/java/com/example/android/camera2/slowmo/CameraActivity.kt index 5a56941c..ab45b501 100644 --- a/Camera2SlowMotion/app/src/main/java/com/example/android/camera2/slowmo/CameraActivity.kt +++ b/Camera2SlowMotion/app/src/main/java/com/example/android/camera2/slowmo/CameraActivity.kt @@ -17,12 +17,12 @@ package com.example.android.camera2.slowmo import android.os.Bundle -import android.view.View import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat import com.example.android.camera2.slowmo.databinding.ActivityCameraBinding class CameraActivity : AppCompatActivity() { - private lateinit var activityCameraBinding: ActivityCameraBinding override fun onCreate(savedInstanceState: Bundle?) { @@ -33,21 +33,16 @@ class CameraActivity : AppCompatActivity() { override fun onResume() { super.onResume() - // Before setting full screen flags, we must wait a bit to let UI settle; otherwise, we may + // Before setting full screen, we must wait a bit to let UI settle; otherwise, we may // be trying to set app to immersive mode before it's ready and the flags do not stick activityCameraBinding.fragmentContainer.postDelayed({ - activityCameraBinding.fragmentContainer.systemUiVisibility = FLAGS_FULLSCREEN + WindowCompat + .getInsetsController(window, window.decorView) + .hide(WindowInsetsCompat.Type.systemBars()) }, IMMERSIVE_FLAG_TIMEOUT) } companion object { - /** Combination of all flags required to put activity into immersive mode */ - const val FLAGS_FULLSCREEN = - View.SYSTEM_UI_FLAG_LOW_PROFILE or - View.SYSTEM_UI_FLAG_FULLSCREEN or - View.SYSTEM_UI_FLAG_LAYOUT_STABLE or - View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY - /** Milliseconds used for UI animations */ const val ANIMATION_FAST_MILLIS = 50L const val ANIMATION_SLOW_MILLIS = 100L diff --git a/Camera2SlowMotion/app/src/main/java/com/example/android/camera2/slowmo/fragments/CameraFragment.kt b/Camera2SlowMotion/app/src/main/java/com/example/android/camera2/slowmo/fragments/CameraFragment.kt index 547f5c8a..76cf1038 100644 --- a/Camera2SlowMotion/app/src/main/java/com/example/android/camera2/slowmo/fragments/CameraFragment.kt +++ b/Camera2SlowMotion/app/src/main/java/com/example/android/camera2/slowmo/fragments/CameraFragment.kt @@ -27,6 +27,8 @@ import android.hardware.camera2.CameraConstrainedHighSpeedCaptureSession import android.hardware.camera2.CameraDevice import android.hardware.camera2.CameraManager import android.hardware.camera2.CaptureRequest +import android.hardware.camera2.params.OutputConfiguration +import android.hardware.camera2.params.SessionConfiguration import android.hardware.camera2.params.StreamConfigurationMap import android.media.MediaCodec import android.media.MediaRecorder @@ -37,15 +39,19 @@ import android.os.HandlerThread import android.util.Log import android.util.Range import android.util.Size -import android.view.* +import android.view.Display +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.Surface +import android.view.SurfaceHolder +import android.view.View +import android.view.ViewGroup import android.webkit.MimeTypeMap import androidx.core.content.FileProvider import androidx.core.graphics.drawable.toDrawable import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.lifecycle.lifecycleScope -import androidx.navigation.NavController -import androidx.navigation.Navigation import androidx.navigation.fragment.navArgs import com.example.android.camera.utils.OrientationLiveData import com.example.android.camera.utils.SIZE_1080P @@ -53,7 +59,6 @@ import com.example.android.camera.utils.SmartSize import com.example.android.camera.utils.getDisplaySmartSize import com.example.android.camera2.slowmo.BuildConfig import com.example.android.camera2.slowmo.CameraActivity -import com.example.android.camera2.slowmo.R import com.example.android.camera2.slowmo.databinding.FragmentCameraBinding import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -66,10 +71,11 @@ import java.util.Locale import kotlin.RuntimeException import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine +import androidx.navigation.fragment.findNavController +import kotlinx.coroutines.asExecutor +import java.util.concurrent.Executor class CameraFragment : Fragment() { - /** Android ViewBinding */ private var _fragmentCameraBinding: FragmentCameraBinding? = null @@ -78,11 +84,6 @@ class CameraFragment : Fragment() { /** AndroidX navigation arguments */ private val args: CameraFragmentArgs by navArgs() - /** Host's navigation controller */ - private val navController: NavController by lazy { - Navigation.findNavController(requireActivity(), R.id.fragment_container) - } - /** Detects, characterizes, and connects to a CameraDevice (used for all camera operations) */ private val cameraManager: CameraManager by lazy { val context = requireContext().applicationContext @@ -95,14 +96,13 @@ class CameraFragment : Fragment() { } /** File where the recording will be saved */ - private val outputFile: File by lazy { createFile(requireContext(), "mp4") } + private val outputFile: File by lazy { createFile(requireContext()) } /** - * Setup a persistent [Surface] for the recorder so we can use it as an output target for the + * Set up a persistent [Surface] for the recorder so we can use it as an output target for the * camera session without preparing the recorder */ private val recorderSurface: Surface by lazy { - // Get a persistent Surface from MediaCodec, don't forget to release when done val surface = MediaCodec.createPersistentInputSurface() @@ -136,7 +136,10 @@ class CameraFragment : Fragment() { // Remove white flash animation fragmentCameraBinding.overlay.foreground = null // Restart animation recursively - fragmentCameraBinding.overlay.postDelayed(animationTask, CameraActivity.ANIMATION_FAST_MILLIS) + fragmentCameraBinding.overlay.postDelayed( + animationTask, + CameraActivity.ANIMATION_FAST_MILLIS + ) }, CameraActivity.ANIMATION_FAST_MILLIS) } } @@ -198,19 +201,28 @@ class CameraFragment : Fragment() { fragmentCameraBinding.viewFinder.holder.addCallback(object : SurfaceHolder.Callback { override fun surfaceDestroyed(holder: SurfaceHolder) = Unit override fun surfaceChanged( - holder: SurfaceHolder, - format: Int, - width: Int, - height: Int) = Unit + holder: SurfaceHolder, + format: Int, + width: Int, + height: Int + ) = Unit override fun surfaceCreated(holder: SurfaceHolder) { - // Selects appropriate preview size and configures view finder val previewSize = getConstrainedPreviewOutputSize( - fragmentCameraBinding.viewFinder.display, characteristics, SurfaceHolder::class.java) - Log.d(TAG, "View finder size: ${fragmentCameraBinding.viewFinder.width} x ${fragmentCameraBinding.viewFinder.height}") + fragmentCameraBinding.viewFinder.display, + characteristics, + SurfaceHolder::class.java + ) + Log.d( + TAG, + "View finder size: ${fragmentCameraBinding.viewFinder.width} x ${fragmentCameraBinding.viewFinder.height}" + ) Log.d(TAG, "Selected preview size: $previewSize") - fragmentCameraBinding.viewFinder.setAspectRatio(previewSize.width, previewSize.height) + fragmentCameraBinding.viewFinder.setAspectRatio( + previewSize.width, + previewSize.height + ) // To ensure that size is set, initialize camera in the view's thread fragmentCameraBinding.viewFinder.post { initializeCamera() } @@ -219,14 +231,14 @@ class CameraFragment : Fragment() { // Used to rotate the output media to match device orientation relativeOrientation = OrientationLiveData(requireContext(), characteristics).apply { - observe(viewLifecycleOwner, Observer { - orientation -> Log.d(TAG, "Orientation changed: $orientation") + observe(viewLifecycleOwner, Observer { orientation -> + Log.d(TAG, "Orientation changed: $orientation") }) } } /** Creates a [MediaRecorder] instance using the provided [Surface] as input */ - private fun createRecorder(surface: Surface) = MediaRecorder().apply { + private fun createRecorder(surface: Surface) = MediaRecorder(requireContext()).apply { setAudioSource(MediaRecorder.AudioSource.MIC) setVideoSource(MediaRecorder.VideoSource.SURFACE) setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) @@ -244,21 +256,19 @@ class CameraFragment : Fragment() { * additional constraint that the selected size must also be available as one of possible * constrained high-speed session sizes. */ - private fun getConstrainedPreviewOutputSize( - display: Display, - characteristics: CameraCharacteristics, - targetClass: Class, - format: Int? = null + private fun getConstrainedPreviewOutputSize( + display: Display, + characteristics: CameraCharacteristics, + targetClass: Class, + format: Int? = null ): Size { - // Find which is smaller: screen or 1080p val screenSize = getDisplaySmartSize(display) val hdScreen = screenSize.long >= SIZE_1080P.long || screenSize.short >= SIZE_1080P.short val maxSize = if (hdScreen) SIZE_1080P else screenSize // If image format is provided, use it to determine supported sizes; else use target class - val config = characteristics.get( - CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! + val config = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! if (format == null) { assert(StreamConfigurationMap.isOutputSupportedFor(targetClass)) } else { @@ -273,13 +283,14 @@ class CameraFragment : Fragment() { // Filter sizes which are part of the high speed constrained session val validSizes = allSizes - .filter { highSpeedSizes.contains(it) } - .sortedWith(compareBy { it.height * it.width }) - .map { SmartSize(it.width, it.height) }.reversed() + .filter { highSpeedSizes.contains(it) } + .sortedWith(compareBy { it.height * it.width }) + .map { SmartSize(it.width, it.height) }.reversed() // Then, get the largest output size that is smaller or equal than our max size return validSizes.first { it.long <= maxSize.long && it.short <= maxSize.short }.size } + /** * Begin all camera operations in a coroutine in the main thread. This function: * - Opens the camera @@ -288,20 +299,23 @@ class CameraFragment : Fragment() { */ @SuppressLint("ClickableViewAccessibility") private fun initializeCamera() = lifecycleScope.launch(Dispatchers.Main) { - // Open the selected camera camera = openCamera(cameraManager, args.cameraId, cameraHandler) // Creates list of Surfaces where the camera will output frames val targets = listOf(fragmentCameraBinding.viewFinder.holder.surface, recorderSurface) + val executor = Dispatchers.Default.asExecutor() + // Start a capture session using our open camera and list of Surfaces where frames will go - session = createCaptureSession(camera, targets, cameraHandler) + session = createCaptureSession(camera, targets, executor) // Ensures the requested size and FPS are compatible with this camera val fpsRange = Range(args.fps, args.fps) - assert(true == characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) - ?.getHighSpeedVideoFpsRangesFor(Size(args.width, args.height))?.contains(fpsRange)) + assert( + true == characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) + ?.getHighSpeedVideoFpsRangesFor(Size(args.width, args.height))?.contains(fpsRange) + ) // Sends the capture request as frequently as possible until the session is torn down or // session.stopRepeating() is called @@ -310,12 +324,9 @@ class CameraFragment : Fragment() { // Listen to the capture button fragmentCameraBinding.captureButton.setOnTouchListener { view, event -> when (event.action) { - MotionEvent.ACTION_DOWN -> lifecycleScope.launch(Dispatchers.IO) { - // Prevents screen rotation during the video recording - requireActivity().requestedOrientation = - ActivityInfo.SCREEN_ORIENTATION_LOCKED + requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED // Stops preview requests, and start record requests session.stopRepeating() @@ -336,10 +347,8 @@ class CameraFragment : Fragment() { } MotionEvent.ACTION_UP -> lifecycleScope.launch(Dispatchers.IO) { - // Unlocks screen rotation after recording finished - requireActivity().requestedOrientation = - ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED // Requires recording of at least MIN_REQUIRED_RECORDING_TIME_MILLIS val elapsedTimeMillis = System.currentTimeMillis() - recordingStartMillis @@ -355,22 +364,23 @@ class CameraFragment : Fragment() { // Broadcasts the media file to the rest of the system MediaScannerConnection.scanFile( - view.context, arrayOf(outputFile.absolutePath), null, null) + view.context, arrayOf(outputFile.absolutePath), null, null + ) // Launch external activity via intent to play video recorded using our provider - startActivity(Intent().apply { - action = Intent.ACTION_VIEW - type = MimeTypeMap.getSingleton() - .getMimeTypeFromExtension(outputFile.extension) + startActivity(Intent(Intent.ACTION_VIEW).apply { val authority = "${BuildConfig.APPLICATION_ID}.provider" - data = FileProvider.getUriForFile(view.context, authority, outputFile) - flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_ACTIVITY_CLEAR_TOP + setDataAndType( + FileProvider.getUriForFile(view.context, authority, outputFile), + MimeTypeMap.getSingleton().getMimeTypeFromExtension(outputFile.extension) + ) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) }) // Finishes our current camera screen delay(CameraActivity.ANIMATION_SLOW_MILLIS) - navController.popBackStack() + findNavController().popBackStack() } } @@ -381,9 +391,9 @@ class CameraFragment : Fragment() { /** Opens the camera and returns the opened device (as the result of the suspend coroutine) */ @SuppressLint("MissingPermission") private suspend fun openCamera( - manager: CameraManager, - cameraId: String, - handler: Handler? = null + manager: CameraManager, + cameraId: String, + handler: Handler? = null ): CameraDevice = suspendCancellableCoroutine { cont -> manager.openCamera(cameraId, object : CameraDevice.StateCallback() { override fun onOpened(device: CameraDevice) = cont.resume(device) @@ -394,7 +404,7 @@ class CameraFragment : Fragment() { } override fun onError(device: CameraDevice, error: Int) { - val msg = when(error) { + val msg = when (error) { ERROR_CAMERA_DEVICE -> "Fatal (device)" ERROR_CAMERA_DISABLED -> "Device policy" ERROR_CAMERA_IN_USE -> "Camera in use" @@ -414,25 +424,30 @@ class CameraFragment : Fragment() { * suspend coroutine) */ private suspend fun createCaptureSession( - device: CameraDevice, - targets: List, - handler: Handler? = null - ): CameraConstrainedHighSpeedCaptureSession = suspendCoroutine { cont -> - + device: CameraDevice, + targets: List, + executor: Executor + ): CameraConstrainedHighSpeedCaptureSession = suspendCancellableCoroutine { cont -> // Creates a capture session using the predefined targets, and defines a session state // callback which resumes the coroutine once the session is configured - device.createConstrainedHighSpeedCaptureSession( - targets, object: CameraCaptureSession.StateCallback() { - - override fun onConfigured(session: CameraCaptureSession) = - cont.resume(session as CameraConstrainedHighSpeedCaptureSession) - - override fun onConfigureFailed(session: CameraCaptureSession) { - val exc = RuntimeException("Camera ${device.id} session configuration failed") - Log.e(TAG, exc.message, exc) - cont.resumeWithException(exc) - } - }, handler) + device.createCaptureSession( + SessionConfiguration( + SessionConfiguration.SESSION_HIGH_SPEED, + targets.map(::OutputConfiguration), + executor, + object : CameraCaptureSession.StateCallback() { + override fun onConfigured(session: CameraCaptureSession) = + cont.resume(session as CameraConstrainedHighSpeedCaptureSession) + + override fun onConfigureFailed(session: CameraCaptureSession) { + val exc = RuntimeException("Camera ${device.id} session configuration failed") + Log.e(TAG, exc.message, exc) + if (cont.isActive) + cont.resumeWithException(exc) + } + } + ) + ) } override fun onStop() { @@ -469,9 +484,9 @@ class CameraFragment : Fragment() { private const val FPS_PREVIEW_ONLY: Int = 30 /** Creates a [File] named with the current date and time */ - private fun createFile(context: Context, extension: String): File { + private fun createFile(context: Context): File { val sdf = SimpleDateFormat("yyyy_MM_dd_HH_mm_ss_SSS", Locale.US) - return File(context.filesDir, "VID_${sdf.format(Date())}.$extension") + return File(context.filesDir, "VID_${sdf.format(Date())}.mp4") } } } diff --git a/Camera2SlowMotion/app/src/main/java/com/example/android/camera2/slowmo/fragments/PermissionsFragment.kt b/Camera2SlowMotion/app/src/main/java/com/example/android/camera2/slowmo/fragments/PermissionsFragment.kt index 220f8896..a9cf7ff4 100644 --- a/Camera2SlowMotion/app/src/main/java/com/example/android/camera2/slowmo/fragments/PermissionsFragment.kt +++ b/Camera2SlowMotion/app/src/main/java/com/example/android/camera2/slowmo/fragments/PermissionsFragment.kt @@ -21,50 +21,47 @@ import android.content.Context import android.content.pm.PackageManager import android.os.Bundle import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment -import androidx.navigation.Navigation -import com.example.android.camera2.slowmo.R +import androidx.navigation.fragment.findNavController -private const val PERMISSIONS_REQUEST_CODE = 10 private val PERMISSIONS_REQUIRED = arrayOf( - Manifest.permission.CAMERA, - Manifest.permission.RECORD_AUDIO) + Manifest.permission.CAMERA, + Manifest.permission.RECORD_AUDIO +) /** * This [Fragment] requests permissions and, once granted, it will navigate to the next fragment */ class PermissionsFragment : Fragment() { + private val requestPermissionsLauncher = registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { permissions -> + if (permissions.values.all { it }) { + navigateToSelectorFragment() + } else { + Toast.makeText(context, "Permission request denied", Toast.LENGTH_LONG).show() + } + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (hasPermissions(requireContext())) { // If permissions have already been granted, proceed - Navigation.findNavController(requireActivity(), R.id.fragment_container).navigate( - PermissionsFragmentDirections.actionPermissionsToSelector()) + navigateToSelectorFragment() } else { // Request camera-related permissions - requestPermissions(PERMISSIONS_REQUIRED, PERMISSIONS_REQUEST_CODE) + requestPermissionsLauncher.launch(PERMISSIONS_REQUIRED) } } - override fun onRequestPermissionsResult( - requestCode: Int, permissions: Array, grantResults: IntArray) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - if (requestCode == PERMISSIONS_REQUEST_CODE) { - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - // Takes the user to the success fragment when permission is granted - Navigation.findNavController(requireActivity(), R.id.fragment_container).navigate( - PermissionsFragmentDirections.actionPermissionsToSelector()) - } else { - Toast.makeText(context, "Permission request denied", Toast.LENGTH_LONG).show() - } - } + private fun navigateToSelectorFragment() { + findNavController().navigate(PermissionsFragmentDirections.actionPermissionsToSelector()) } companion object { - /** Convenience method used to check if all permissions required by this app are granted */ fun hasPermissions(context: Context) = PERMISSIONS_REQUIRED.all { ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED diff --git a/Camera2SlowMotion/app/src/main/java/com/example/android/camera2/slowmo/fragments/SelectorFragment.kt b/Camera2SlowMotion/app/src/main/java/com/example/android/camera2/slowmo/fragments/SelectorFragment.kt index 580a714b..d70d8c43 100644 --- a/Camera2SlowMotion/app/src/main/java/com/example/android/camera2/slowmo/fragments/SelectorFragment.kt +++ b/Camera2SlowMotion/app/src/main/java/com/example/android/camera2/slowmo/fragments/SelectorFragment.kt @@ -27,23 +27,21 @@ import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.fragment.app.Fragment -import androidx.navigation.Navigation import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.example.android.camera.utils.GenericListAdapter -import com.example.android.camera2.slowmo.R +import androidx.navigation.fragment.findNavController /** * In this [Fragment] we let users pick a camera, size and FPS to use for high * speed video recording */ class SelectorFragment : Fragment() { - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? = RecyclerView(requireContext()) + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = RecyclerView(requireContext()) @SuppressLint("MissingPermission") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -52,34 +50,40 @@ class SelectorFragment : Fragment() { view.apply { layoutManager = LinearLayoutManager(requireContext()) - val cameraManager = - requireContext().getSystemService(Context.CAMERA_SERVICE) as CameraManager + val cameraManager = requireContext() + .getSystemService(Context.CAMERA_SERVICE) as CameraManager val cameraList = enumerateHighSpeedCameras(cameraManager) - val layoutId = android.R.layout.simple_list_item_1 - adapter = GenericListAdapter(cameraList, itemLayoutId = layoutId) { view, item, _ -> + adapter = GenericListAdapter( + cameraList, + android.R.layout.simple_list_item_1 + ) { view, item, _ -> view.findViewById(android.R.id.text1).text = item.title view.setOnClickListener { - Navigation.findNavController(requireActivity(), R.id.fragment_container) - .navigate(SelectorFragmentDirections.actionSelectorToCamera( - item.cameraId, item.size.width, item.size.height, item.fps)) + findNavController().navigate( + SelectorFragmentDirections.actionSelectorToCamera( + item.cameraId, + item.size.width, + item.size.height, + item.fps + ) + ) } } } - } companion object { - private data class CameraInfo( - val title: String, - val cameraId: String, - val size: Size, - val fps: Int) + val title: String, + val cameraId: String, + val size: Size, + val fps: Int + ) /** Converts a lens orientation enum into a human-readable string */ - private fun lensOrientationString(value: Int) = when(value) { + private fun lensOrientationString(value: Int) = when (value) { CameraCharacteristics.LENS_FACING_BACK -> "Back" CameraCharacteristics.LENS_FACING_FRONT -> "Front" CameraCharacteristics.LENS_FACING_EXTERNAL -> "External" @@ -94,28 +98,30 @@ class SelectorFragment : Fragment() { // Iterate over the list of cameras and add those with high speed video recording // capability to our output. This function only returns those cameras that declare // constrained high speed video recording, but some cameras may be capable of doing - // unconstrained video recording with high enough FPS for some use cases and they will + // unconstrained video recording with high enough FPS for some use cases, and they will // not necessarily declare constrained high speed video capability. cameraManager.cameraIdList.forEach { id -> val characteristics = cameraManager.getCameraCharacteristics(id) val orientation = lensOrientationString( - characteristics.get(CameraCharacteristics.LENS_FACING)!!) + characteristics.get(CameraCharacteristics.LENS_FACING)!! + ) // Query the available capabilities and output formats - val capabilities = characteristics.get( - CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)!! - val cameraConfig = characteristics.get( - CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! + val capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)!! + val cameraConfig = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! // Return cameras that support constrained high video capability - if (capabilities.contains(CameraCharacteristics - .REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO)) { + if (capabilities.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO)) { // For each camera, list its compatible sizes and FPS ranges cameraConfig.highSpeedVideoSizes.forEach { size -> cameraConfig.getHighSpeedVideoFpsRangesFor(size).forEach { fpsRange -> val fps = fpsRange.upper val info = CameraInfo( - "$orientation ($id) $size $fps FPS", id, size, fps) + "$orientation ($id) $size $fps FPS", + id, + size, + fps + ) // Only report the highest FPS in the range, avoid duplicates if (!availableCameras.contains(info)) availableCameras.add(info) diff --git a/Camera2SlowMotion/app/src/main/res/navigation/nav_graph.xml b/Camera2SlowMotion/app/src/main/res/navigation/nav_graph.xml index 44cdd555..227a4280 100644 --- a/Camera2SlowMotion/app/src/main/res/navigation/nav_graph.xml +++ b/Camera2SlowMotion/app/src/main/res/navigation/nav_graph.xml @@ -68,8 +68,8 @@ app:argType="integer" /> diff --git a/Camera2SlowMotion/build.gradle b/Camera2SlowMotion/build.gradle index 60be841e..229eb498 100644 --- a/Camera2SlowMotion/build.gradle +++ b/Camera2SlowMotion/build.gradle @@ -18,7 +18,7 @@ buildscript { // Top-level variables used for versioning - ext.kotlin_version = '1.5.21' + ext.kotlin_version = '2.2.20' ext.java_version = JavaVersion.VERSION_1_8 repositories { @@ -26,9 +26,9 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.2.2' + classpath 'com.android.tools.build:gradle:9.2.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.4" + classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.9.8' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/Camera2SlowMotion/gradle.properties b/Camera2SlowMotion/gradle.properties index b448ae14..0e99b717 100644 --- a/Camera2SlowMotion/gradle.properties +++ b/Camera2SlowMotion/gradle.properties @@ -24,5 +24,7 @@ org.gradle.jvmargs=-Xmx1536m # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -android.enableJetifier=true android.useAndroidX=true +android.nonTransitiveRClass=false +android.uniquePackageNames=false +android.r8.strictFullModeForKeepRules=false diff --git a/Camera2SlowMotion/gradle/wrapper/gradle-wrapper.properties b/Camera2SlowMotion/gradle/wrapper/gradle-wrapper.properties index 9bc83a98..4e2ef5c1 100644 --- a/Camera2SlowMotion/gradle/wrapper/gradle-wrapper.properties +++ b/Camera2SlowMotion/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ -#Wed Mar 31 20:47:06 PDT 2021 +#Mon May 25 20:32:25 BRT 2026 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip diff --git a/Camera2SlowMotion/settings.gradle b/Camera2SlowMotion/settings.gradle index 663c0482..460a98c5 100644 --- a/Camera2SlowMotion/settings.gradle +++ b/Camera2SlowMotion/settings.gradle @@ -1,4 +1,6 @@ -/* +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' +}/* * Copyright 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/Camera2SlowMotion/utils/build.gradle b/Camera2SlowMotion/utils/build.gradle index 1ef65fa3..3d038586 100644 --- a/Camera2SlowMotion/utils/build.gradle +++ b/Camera2SlowMotion/utils/build.gradle @@ -15,16 +15,12 @@ */ apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' android { - compileSdkVersion 29 + compileSdkVersion 37 defaultConfig { minSdkVersion 21 - targetSdkVersion 29 - versionCode 1 - versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles 'consumer-rules.pro' @@ -35,41 +31,44 @@ android { targetCompatibility rootProject.ext.java_version } - kotlinOptions { - jvmTarget = "$rootProject.ext.java_version" - } - buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } + namespace 'com.example.android.camera.utils' + lint { + targetSdk 29 + } + testOptions { + targetSdk 29 + } } dependencies { // Kotlin lang implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.4' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0' // App compat and UI things - implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'androidx.recyclerview:recyclerview:1.1.0' + implementation 'androidx.appcompat:appcompat:1.7.1' + implementation 'androidx.recyclerview:recyclerview:1.4.0' // EXIF Interface - implementation 'androidx.exifinterface:exifinterface:1.2.0' + implementation 'androidx.exifinterface:exifinterface:1.4.2' // Unit testing - testImplementation 'androidx.test.ext:junit:1.1.1' - testImplementation 'androidx.test:rules:1.2.0' - testImplementation 'androidx.test:runner:1.2.0' - testImplementation 'androidx.test.espresso:espresso-core:3.2.0' - testImplementation 'org.robolectric:robolectric:4.3.1' + testImplementation 'androidx.test.ext:junit:1.3.0' + testImplementation 'androidx.test:rules:1.7.0' + testImplementation 'androidx.test:runner:1.7.0' + testImplementation 'androidx.test.espresso:espresso-core:3.7.0' + testImplementation 'org.robolectric:robolectric:4.16.1' // Instrumented testing - androidTestImplementation 'androidx.test.ext:junit:1.1.1' - androidTestImplementation 'androidx.test:rules:1.2.0' - androidTestImplementation 'androidx.test:runner:1.2.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + androidTestImplementation 'androidx.test.ext:junit:1.3.0' + androidTestImplementation 'androidx.test:rules:1.7.0' + androidTestImplementation 'androidx.test:runner:1.7.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0' } diff --git a/Camera2SlowMotion/utils/src/main/AndroidManifest.xml b/Camera2SlowMotion/utils/src/main/AndroidManifest.xml index 2e13c37a..b708cb2c 100644 --- a/Camera2SlowMotion/utils/src/main/AndroidManifest.xml +++ b/Camera2SlowMotion/utils/src/main/AndroidManifest.xml @@ -14,4 +14,4 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - +