diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..c1a01418 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,3 @@ +[*.{kt,kts}] +# @Composable functions are intentionally PascalCase across this project. +ktlint_function_naming_ignore_when_annotated_with = Composable diff --git a/.github/scripts/copy_utils.sh b/.github/scripts/copy_utils.sh deleted file mode 100644 index c76310cf..00000000 --- a/.github/scripts/copy_utils.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash - -# Copyright (C) 2020 The Android Open Source Project -# -# 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 -# -# https://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. - -set -xe - -# Crawl all settings.gradle files which indicate an Android project -for GRADLE_FILE in `find . -name "settings.gradle"` ; do - SAMPLE=$(dirname "${GRADLE_FILE}") - # If the sample depends on "utils", copy it from the library project - if grep -q "include 'utils'" "$GRADLE_FILE"; then - rm -rf "$SAMPLE/utils" - cp -avR "$SAMPLE/../CameraUtils/lib" "$SAMPLE/utils" - - # Temporarily disable .gitignore file to add to utils to Git - mv "$SAMPLE/.gitignore" "$SAMPLE/.gitignore.bak" - git add "$SAMPLE/utils" - mv "$SAMPLE/.gitignore.bak" "$SAMPLE/.gitignore" - fi -done diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index dbb6ddab..42753cc7 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -22,213 +22,34 @@ on: branches: [ main ] jobs: - camerax_basic: - name: CameraXBasic + build: + name: Build & check runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: set up JDK 17 - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: '17' - - name: Build project - working-directory: CameraXBasic - run: ./gradlew assembleDebug - - name: Zip artifacts - working-directory: CameraXBasic - run: zip -r assemble.zip . -i '**/build/*.apk' '**/build/*.aab' '**/build/*.aar' '**/build/*.so' - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: camerax_basic_assemble - path: CameraXBasic/assemble.zip - camerax_extensions: - name: CameraXExtensions - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: set up JDK 17 - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: '17' - - name: Build project - working-directory: CameraXExtensions - run: ./gradlew assembleDebug - - name: Zip artifacts - working-directory: CameraXExtensions - run: zip -r assemble.zip . -i '**/build/*.apk' '**/build/*.aab' '**/build/*.aar' '**/build/*.so' - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: camerax_extensions_assemble - path: CameraXExtensions/assemble.zip - camerax_video: - name: CameraXVideo - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: set up JDK 17 - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: '17' - - name: Build project - working-directory: CameraXVideo - run: ./gradlew assembleDebug - - name: Zip artifacts - working-directory: CameraXVideo - run: zip -r assemble.zip . -i '**/build/*.apk' '**/build/*.aab' '**/build/*.aar' '**/build/*.so' - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: camerax_video_assemble - path: CameraXVideo/assemble.zip - camerax_advanced: - name: CameraXAdvanced - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: set up JDK 17 - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: '17' - - name: Build project - working-directory: CameraXAdvanced - run: ./gradlew assembleDebug - - name: Zip artifacts - working-directory: CameraXAdvanced - run: zip -r assemble.zip . -i '**/build/*.apk' '**/build/*.aab' '**/build/*.aar' '**/build/*.so' - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: camerax_advanced_assemble - path: CameraXAdvanced/assemble.zip - camerax_mlkit: - name: CameraX-MLKit - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: set up JDK 17 - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: '17' - - name: Build project - working-directory: CameraX-MLKit - run: ./gradlew assembleDebug - - name: Zip artifacts - working-directory: CameraX-MLKit - run: zip -r assemble.zip . -i '**/build/*.apk' '**/build/*.aab' '**/build/*.aar' '**/build/*.so' - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: camerax_mlkit_assemble - path: CameraX-MLKit/assemble.zip - camera2_basic: - name: Camera2Basic - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: set up JDK 17 - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: '17' - - name: Build project - working-directory: Camera2Basic - run: ./gradlew assembleDebug - - name: Zip artifacts - working-directory: Camera2Basic - run: zip -r assemble.zip . -i '**/build/*.apk' '**/build/*.aab' '**/build/*.aar' '**/build/*.so' - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: camera2_basic_assemble - path: Camera2Basic/assemble.zip - camera2_extensions: - name: Camera2Extensions - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: set up JDK 17 - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: '17' - - name: Build project - working-directory: Camera2Extensions - run: ./gradlew assembleDebug - - name: Zip artifacts - working-directory: Camera2Extensions - run: zip -r assemble.zip . -i '**/build/*.apk' '**/build/*.aab' '**/build/*.aar' '**/build/*.so' - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: camera2_extensions_assemble - path: Camera2Extensions/assemble.zip - camera2_video: - name: Camera2Video - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: set up JDK 17 + + - name: Set up JDK 17 uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: '17' - - name: Build project - working-directory: Camera2Video - run: ./gradlew assembleDebug - - name: Zip artifacts - working-directory: Camera2Video - run: zip -r assemble.zip . -i '**/build/*.apk' '**/build/*.aab' '**/build/*.aar' '**/build/*.so' - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: camera2_video_assemble - path: Camera2Video/assemble.zip - camera2_slowmotion: - name: Camera2SlowMotion - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: set up JDK 11 - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: '11' - - name: Build project - working-directory: Camera2SlowMotion - run: ./gradlew assembleDebug - - name: Zip artifacts - working-directory: Camera2SlowMotion - run: zip -r assemble.zip . -i '**/build/*.apk' '**/build/*.aab' '**/build/*.aar' '**/build/*.so' - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: camera2_slowmotion_assemble - path: Camera2SlowMotion/assemble.zip - camerautils: - name: CameraUtils - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: set up JDK 11 - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: '11' - - name: Build project - working-directory: CameraUtils + cache: gradle + + - name: Set up Android SDK + uses: android-actions/setup-android@v3 + + # The project compiles against API 37 (Android 17); install that platform. + - name: Install compileSdk 37 platform + run: yes | sdkmanager "platforms;android-37.0" > /dev/null + + - name: Check formatting (Spotless) + run: ./gradlew spotlessCheck + + - name: Assemble debug run: ./gradlew assembleDebug - - name: Zip artifacts - working-directory: CameraUtils - run: zip -r assemble.zip . -i '**/build/*.apk' '**/build/*.aab' '**/build/*.aar' '**/build/*.so' - - name: Upload artifacts + + - name: Upload debug APK uses: actions/upload-artifact@v4 with: - name: camerautils_assemble - path: CameraUtils/assemble.zip + name: app-debug + path: app/build/outputs/apk/debug/app-debug.apk diff --git a/CODEOWNERS b/CODEOWNERS index 9d9aa540..fd88988d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1 @@ -* @madebymozart \ No newline at end of file +* @lethargicpanda @calren @ksemenova @JolandaVerhoef diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 3e0558e0..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,36 +0,0 @@ -# How to become a contributor and submit your own code - -## Contributor License Agreements - -We'd love to accept your sample apps and patches! Before we can take them, we -have to jump a couple of legal hurdles. - -Please fill out either the individual or corporate Contributor License Agreement -(CLA). - - * If you are an individual writing original source code and you're sure you - own the intellectual property, then you'll need to sign an [individual CLA] - (https://developers.google.com/open-source/cla/individual). - * If you work for a company that wants to allow you to contribute your work, - then you'll need to sign a [corporate CLA] - (https://developers.google.com/open-source/cla/corporate). - -Follow either of the two links above to access the appropriate CLA and -instructions for how to sign and return it. Once we receive it, we'll be able to -accept your pull requests. - -## Contributing A Patch - -1. Submit an issue describing your proposed change to the repo in question. -1. The repo owner will respond to your issue promptly. -1. If your proposed change is accepted, and you haven't already done so, sign a - Contributor License Agreement (see details above). -1. Fork the desired repo, develop and test your code changes. -1. Ensure that your code adheres to the existing style in the sample to which - you are contributing. Refer to the - [Google Cloud Platform Samples Style Guide] - (https://github.com/GoogleCloudPlatform/Template/wiki/style.html) for the - recommended coding standards for this organization. -1. Ensure that your code has an appropriate set of unit tests which all pass. -1. Submit a pull request. - diff --git a/Camera2Basic/.gitignore b/Camera2Basic/.gitignore deleted file mode 100644 index 28b4859f..00000000 --- a/Camera2Basic/.gitignore +++ /dev/null @@ -1 +0,0 @@ -utils/ \ No newline at end of file diff --git a/Camera2Basic/.google/packaging.yaml b/Camera2Basic/.google/packaging.yaml deleted file mode 100644 index d29badae..00000000 --- a/Camera2Basic/.google/packaging.yaml +++ /dev/null @@ -1,17 +0,0 @@ - -# GOOGLE SAMPLE PACKAGING DATA -# -# This file is used by Google as part of our samples packaging process. -# End users may safely ignore this file. It has no relevance to other systems. ---- -status: PUBLISHED -technologies: [Android] -categories: [Camera] -languages: [Kotlin] -solutions: [Mobile] -github: android/camera -level: INTERMEDIATE -icon: screenshots/icon-web.png -apiRefs: - - android:android.hardware.camera2.DngCreator -license: apache2 diff --git a/Camera2Basic/.java-version b/Camera2Basic/.java-version deleted file mode 100644 index 8e2afd34..00000000 --- a/Camera2Basic/.java-version +++ /dev/null @@ -1 +0,0 @@ -17 \ No newline at end of file diff --git a/Camera2Basic/README.md b/Camera2Basic/README.md deleted file mode 100644 index 5d6c2fa9..00000000 --- a/Camera2Basic/README.md +++ /dev/null @@ -1,51 +0,0 @@ - -Android Camera2Basic Sample -=========================== - -This sample demonstrates using the Camera2 API to capture a JPEG, DEPTH or RAW frame. -Check the source code to see a simple example of how to display the camera preview -and capture a still image using the default configuration with the selected pixel -format. - -Introduction ------------- - -The [Camera2 API][1] allows users to capture RAW images, i.e. unprocessed pixel data -directly from the camera sensor that has not yet been converted into a format and -colorspace typically used for displaying and storing images viewed by humans. The -[DngCreator][2] class is provided as part of the Camera2 API as a utility for saving -RAW images as DNG files. - -This sample displays a live camera preview in a TextureView, and saves JPEG and DNG -file for each image captured. - -[1]: https://developer.android.com/reference/android/hardware/camera2/package-summary.html -[2]: https://developer.android.com/reference/android/hardware/camera2/DngCreator.html - -Pre-requisites --------------- - -- Android SDK 29+ -- Android Studio 3.5+ - -Screenshots -------------- - -Screenshot - -Getting Started ---------------- - -This sample uses the Gradle build system. To build this project, use the -"gradlew build" command or use "Import Project" in Android Studio. - -Support -------- - -- Stack Overflow: http://stackoverflow.com/questions/tagged/android - -If you've found an error in this sample, please file an issue: -https://github.com/android/camera-samples - -Patches are encouraged, and may be submitted by forking this project and -submitting a pull request through GitHub. Please see CONTRIBUTING.md for more details. diff --git a/Camera2Basic/app/build.gradle b/Camera2Basic/app/build.gradle deleted file mode 100644 index c800fd15..00000000 --- a/Camera2Basic/app/build.gradle +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * 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 - * - * https://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. - */ - -apply plugin: 'android' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-kapt' -apply plugin: "androidx.navigation.safeargs" - -android { - defaultConfig { - vectorDrawables.useSupportLibrary = true - } - buildFeatures { - viewBinding true - } - namespace "com.example.android.camera2.basic" - compileSdk 34 - defaultConfig { - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - applicationId "com.android.example.camera2.basic" - minSdkVersion 21 - targetSdk 34 - versionCode 1 - versionName "1.0.0" - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - - kotlinOptions { - jvmTarget = "17" - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } - - buildFeatures { - viewBinding true - dataBinding true - } -} - - - -dependencies { - implementation project(':utils') - - // Kotlin lang - implementation 'androidx.core:core-ktx:1.13.1' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' - - // App compat and UI things - implementation 'androidx.appcompat:appcompat:1.7.0' - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.3' - implementation "androidx.viewpager2:viewpager2:1.1.0" - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - - // Navigation library - def nav_version = '2.7.7' - implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" - implementation "androidx.navigation:navigation-ui-ktx:$nav_version" - - // EXIF Interface - implementation 'androidx.exifinterface:exifinterface:1.3.7' - - // Glide - implementation 'com.github.bumptech.glide:glide:4.11.0' - kapt 'com.github.bumptech.glide:compiler:4.11.0' - - // Unit testing - testImplementation 'androidx.test.ext:junit:1.2.1' - testImplementation 'androidx.test:rules:1.6.1' - testImplementation 'androidx.test:runner:1.6.1' - testImplementation 'androidx.test.espresso:espresso-core:3.6.1' - testImplementation 'org.robolectric:robolectric:4.3.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' -} diff --git a/Camera2Basic/app/src/main/AndroidManifest.xml b/Camera2Basic/app/src/main/AndroidManifest.xml deleted file mode 100644 index e27bcac8..00000000 --- a/Camera2Basic/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Camera2Basic/app/src/main/java/com/example/android/camera2/basic/CameraActivity.kt b/Camera2Basic/app/src/main/java/com/example/android/camera2/basic/CameraActivity.kt deleted file mode 100644 index 59c878c9..00000000 --- a/Camera2Basic/app/src/main/java/com/example/android/camera2/basic/CameraActivity.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * 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 - * - * https://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 com.example.android.camera2.basic - -import android.os.Bundle -import android.view.View -import androidx.appcompat.app.AppCompatActivity -import com.example.android.camera2.basic.databinding.ActivityCameraBinding - -class CameraActivity : AppCompatActivity() { - - private lateinit var activityCameraBinding: ActivityCameraBinding - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - activityCameraBinding = ActivityCameraBinding.inflate(layoutInflater) - setContentView(activityCameraBinding.root) - } - - override fun onResume() { - super.onResume() - // Before setting full screen flags, 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 - }, 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 - private const val IMMERSIVE_FLAG_TIMEOUT = 500L - } -} diff --git a/Camera2Basic/app/src/main/java/com/example/android/camera2/basic/fragments/CameraFragment.kt b/Camera2Basic/app/src/main/java/com/example/android/camera2/basic/fragments/CameraFragment.kt deleted file mode 100644 index 539f274f..00000000 --- a/Camera2Basic/app/src/main/java/com/example/android/camera2/basic/fragments/CameraFragment.kt +++ /dev/null @@ -1,495 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * 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 - * - * https://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 com.example.android.camera2.basic.fragments - -import android.annotation.SuppressLint -import android.content.Context -import android.graphics.Color -import android.graphics.ImageFormat -import android.hardware.camera2.CameraCaptureSession -import android.hardware.camera2.CameraCharacteristics -import android.hardware.camera2.CameraDevice -import android.hardware.camera2.CameraManager -import android.hardware.camera2.CaptureRequest -import android.hardware.camera2.CaptureResult -import android.hardware.camera2.DngCreator -import android.hardware.camera2.TotalCaptureResult -import android.media.Image -import android.media.ImageReader -import android.os.Build -import android.os.Bundle -import android.os.Handler -import android.os.HandlerThread -import android.util.Log -import android.view.LayoutInflater -import android.view.Surface -import android.view.SurfaceHolder -import android.view.View -import android.view.ViewGroup -import androidx.core.graphics.drawable.toDrawable -import androidx.exifinterface.media.ExifInterface -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.computeExifOrientation -import com.example.android.camera.utils.getPreviewOutputSize -import com.example.android.camera.utils.OrientationLiveData -import com.example.android.camera2.basic.CameraActivity -import com.example.android.camera2.basic.R -import com.example.android.camera2.basic.databinding.FragmentCameraBinding -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.suspendCancellableCoroutine -import java.io.Closeable -import java.io.File -import java.io.FileOutputStream -import java.io.IOException -import java.text.SimpleDateFormat -import java.util.concurrent.ArrayBlockingQueue -import java.util.concurrent.TimeoutException -import java.util.Date -import java.util.Locale -import kotlin.RuntimeException -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine - -class CameraFragment : Fragment() { - - /** Android ViewBinding */ - private var _fragmentCameraBinding: FragmentCameraBinding? = null - - private val fragmentCameraBinding get() = _fragmentCameraBinding!! - - /** 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 - context.getSystemService(Context.CAMERA_SERVICE) as CameraManager - } - - /** [CameraCharacteristics] corresponding to the provided Camera ID */ - private val characteristics: CameraCharacteristics by lazy { - cameraManager.getCameraCharacteristics(args.cameraId) - } - - /** Readers used as buffers for camera still shots */ - private lateinit var imageReader: ImageReader - - /** [HandlerThread] where all camera operations run */ - private val cameraThread = HandlerThread("CameraThread").apply { start() } - - /** [Handler] corresponding to [cameraThread] */ - private val cameraHandler = Handler(cameraThread.looper) - - /** Performs recording animation of flashing screen */ - private val animationTask: Runnable by lazy { - Runnable { - // Flash white animation - fragmentCameraBinding.overlay.background = Color.argb(150, 255, 255, 255).toDrawable() - // Wait for ANIMATION_FAST_MILLIS - fragmentCameraBinding.overlay.postDelayed({ - // Remove white flash animation - fragmentCameraBinding.overlay.background = null - }, CameraActivity.ANIMATION_FAST_MILLIS) - } - } - - /** [HandlerThread] where all buffer reading operations run */ - private val imageReaderThread = HandlerThread("imageReaderThread").apply { start() } - - /** [Handler] corresponding to [imageReaderThread] */ - private val imageReaderHandler = Handler(imageReaderThread.looper) - - /** The [CameraDevice] that will be opened in this fragment */ - private lateinit var camera: CameraDevice - - /** Internal reference to the ongoing [CameraCaptureSession] configured with our parameters */ - private lateinit var session: CameraCaptureSession - - /** Live data listener for changes in the device orientation relative to the camera */ - private lateinit var relativeOrientation: OrientationLiveData - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _fragmentCameraBinding = FragmentCameraBinding.inflate(inflater, container, false) - return fragmentCameraBinding.root - } - - @SuppressLint("MissingPermission") - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - fragmentCameraBinding.captureButton.setOnApplyWindowInsetsListener { v, insets -> - v.translationX = (-insets.systemWindowInsetRight).toFloat() - v.translationY = (-insets.systemWindowInsetBottom).toFloat() - insets.consumeSystemWindowInsets() - } - - 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 - - override fun surfaceCreated(holder: SurfaceHolder) { - // Selects appropriate preview size and configures view finder - val previewSize = getPreviewOutputSize( - 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 - ) - - // To ensure that size is set, initialize camera in the view's thread - view.post { initializeCamera() } - } - }) - - // 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") - }) - } - } - - /** - * Begin all camera operations in a coroutine in the main thread. This function: - * - Opens the camera - * - Configures the camera session - * - Starts the preview by dispatching a repeating capture request - * - Sets up the still image capture listeners - */ - private fun initializeCamera() = lifecycleScope.launch(Dispatchers.Main) { - // Open the selected camera - camera = openCamera(cameraManager, args.cameraId, cameraHandler) - - // Initialize an image reader which will be used to capture still photos - val size = characteristics.get( - CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! - .getOutputSizes(args.pixelFormat).maxByOrNull { it.height * it.width }!! - imageReader = ImageReader.newInstance( - size.width, size.height, args.pixelFormat, IMAGE_BUFFER_SIZE) - - // Creates list of Surfaces where the camera will output frames - val targets = listOf(fragmentCameraBinding.viewFinder.holder.surface, imageReader.surface) - - // Start a capture session using our open camera and list of Surfaces where frames will go - session = createCaptureSession(camera, targets, cameraHandler) - - val captureRequest = camera.createCaptureRequest( - CameraDevice.TEMPLATE_PREVIEW).apply { addTarget(fragmentCameraBinding.viewFinder.holder.surface) } - - // This will keep sending the capture request as frequently as possible until the - // session is torn down or session.stopRepeating() is called - session.setRepeatingRequest(captureRequest.build(), null, cameraHandler) - - // Listen to the capture button - fragmentCameraBinding.captureButton.setOnClickListener { - - // Disable click listener to prevent multiple requests simultaneously in flight - it.isEnabled = false - - // Perform I/O heavy operations in a different scope - lifecycleScope.launch(Dispatchers.IO) { - takePhoto().use { result -> - Log.d(TAG, "Result received: $result") - - // Save the result to disk - val output = saveResult(result) - Log.d(TAG, "Image saved: ${output.absolutePath}") - - // If the result is a JPEG file, update EXIF metadata with orientation info - if (output.extension == "jpg") { - val exif = ExifInterface(output.absolutePath) - exif.setAttribute( - ExifInterface.TAG_ORIENTATION, result.orientation.toString()) - exif.saveAttributes() - Log.d(TAG, "EXIF metadata saved: ${output.absolutePath}") - } - - // Display the photo taken to user - lifecycleScope.launch(Dispatchers.Main) { - navController.navigate(CameraFragmentDirections - .actionCameraToJpegViewer(output.absolutePath) - .setOrientation(result.orientation) - .setDepth(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && - result.format == ImageFormat.DEPTH_JPEG)) - } - } - - // Re-enable click listener after photo is taken - it.post { it.isEnabled = true } - } - } - } - - /** 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 - ): CameraDevice = suspendCancellableCoroutine { cont -> - manager.openCamera(cameraId, object : CameraDevice.StateCallback() { - override fun onOpened(device: CameraDevice) = cont.resume(device) - - override fun onDisconnected(device: CameraDevice) { - Log.w(TAG, "Camera $cameraId has been disconnected") - requireActivity().finish() - } - - override fun onError(device: CameraDevice, error: Int) { - val msg = when (error) { - ERROR_CAMERA_DEVICE -> "Fatal (device)" - ERROR_CAMERA_DISABLED -> "Device policy" - ERROR_CAMERA_IN_USE -> "Camera in use" - ERROR_CAMERA_SERVICE -> "Fatal (service)" - ERROR_MAX_CAMERAS_IN_USE -> "Maximum cameras in use" - else -> "Unknown" - } - val exc = RuntimeException("Camera $cameraId error: ($error) $msg") - Log.e(TAG, exc.message, exc) - if (cont.isActive) cont.resumeWithException(exc) - } - }, handler) - } - - /** - * Starts a [CameraCaptureSession] and returns the configured session (as the result of the - * suspend coroutine - */ - private suspend fun createCaptureSession( - device: CameraDevice, - targets: List, - handler: Handler? = null - ): CameraCaptureSession = suspendCoroutine { cont -> - - // Create a capture session using the predefined targets; this also involves defining the - // session state callback to be notified of when the session is ready - device.createCaptureSession(targets, object : CameraCaptureSession.StateCallback() { - - override fun onConfigured(session: CameraCaptureSession) = cont.resume(session) - - override fun onConfigureFailed(session: CameraCaptureSession) { - val exc = RuntimeException("Camera ${device.id} session configuration failed") - Log.e(TAG, exc.message, exc) - cont.resumeWithException(exc) - } - }, handler) - } - - /** - * Helper function used to capture a still image using the [CameraDevice.TEMPLATE_STILL_CAPTURE] - * template. It performs synchronization between the [CaptureResult] and the [Image] resulting - * from the single capture, and outputs a [CombinedCaptureResult] object. - */ - private suspend fun takePhoto(): - CombinedCaptureResult = suspendCoroutine { cont -> - - // Flush any images left in the image reader - @Suppress("ControlFlowWithEmptyBody") - while (imageReader.acquireNextImage() != null) { - } - - // Start a new image queue - val imageQueue = ArrayBlockingQueue(IMAGE_BUFFER_SIZE) - imageReader.setOnImageAvailableListener({ reader -> - val image = reader.acquireNextImage() - Log.d(TAG, "Image available in queue: ${image.timestamp}") - imageQueue.add(image) - }, imageReaderHandler) - - val captureRequest = session.device.createCaptureRequest( - CameraDevice.TEMPLATE_STILL_CAPTURE).apply { addTarget(imageReader.surface) } - session.capture(captureRequest.build(), object : CameraCaptureSession.CaptureCallback() { - - override fun onCaptureStarted( - session: CameraCaptureSession, - request: CaptureRequest, - timestamp: Long, - frameNumber: Long) { - super.onCaptureStarted(session, request, timestamp, frameNumber) - fragmentCameraBinding.viewFinder.post(animationTask) - } - - override fun onCaptureCompleted( - session: CameraCaptureSession, - request: CaptureRequest, - result: TotalCaptureResult) { - super.onCaptureCompleted(session, request, result) - val resultTimestamp = result.get(CaptureResult.SENSOR_TIMESTAMP) - Log.d(TAG, "Capture result received: $resultTimestamp") - - // Set a timeout in case image captured is dropped from the pipeline - val exc = TimeoutException("Image dequeuing took too long") - val timeoutRunnable = Runnable { cont.resumeWithException(exc) } - imageReaderHandler.postDelayed(timeoutRunnable, IMAGE_CAPTURE_TIMEOUT_MILLIS) - - // Loop in the coroutine's context until an image with matching timestamp comes - // We need to launch the coroutine context again because the callback is done in - // the handler provided to the `capture` method, not in our coroutine context - @Suppress("BlockingMethodInNonBlockingContext") - lifecycleScope.launch(cont.context) { - while (true) { - - // Dequeue images while timestamps don't match - val image = imageQueue.take() - // TODO(owahltinez): b/142011420 - // if (image.timestamp != resultTimestamp) continue - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && - image.format != ImageFormat.DEPTH_JPEG && - image.timestamp != resultTimestamp) continue - Log.d(TAG, "Matching image dequeued: ${image.timestamp}") - - // Unset the image reader listener - imageReaderHandler.removeCallbacks(timeoutRunnable) - imageReader.setOnImageAvailableListener(null, null) - - // Clear the queue of images, if there are left - while (imageQueue.size > 0) { - imageQueue.take().close() - } - - // Compute EXIF orientation metadata - val rotation = relativeOrientation.value ?: 0 - val mirrored = characteristics.get(CameraCharacteristics.LENS_FACING) == - CameraCharacteristics.LENS_FACING_FRONT - val exifOrientation = computeExifOrientation(rotation, mirrored) - - // Build the result and resume progress - cont.resume(CombinedCaptureResult( - image, result, exifOrientation, imageReader.imageFormat)) - - // There is no need to break out of the loop, this coroutine will suspend - } - } - } - }, cameraHandler) - } - - /** Helper function used to save a [CombinedCaptureResult] into a [File] */ - private suspend fun saveResult(result: CombinedCaptureResult): File = suspendCoroutine { cont -> - when (result.format) { - - // When the format is JPEG or DEPTH JPEG we can simply save the bytes as-is - ImageFormat.JPEG, ImageFormat.DEPTH_JPEG -> { - val buffer = result.image.planes[0].buffer - val bytes = ByteArray(buffer.remaining()).apply { buffer.get(this) } - try { - val output = createFile(requireContext(), "jpg") - FileOutputStream(output).use { it.write(bytes) } - cont.resume(output) - } catch (exc: IOException) { - Log.e(TAG, "Unable to write JPEG image to file", exc) - cont.resumeWithException(exc) - } - } - - // When the format is RAW we use the DngCreator utility library - ImageFormat.RAW_SENSOR -> { - val dngCreator = DngCreator(characteristics, result.metadata) - try { - val output = createFile(requireContext(), "dng") - FileOutputStream(output).use { dngCreator.writeImage(it, result.image) } - cont.resume(output) - } catch (exc: IOException) { - Log.e(TAG, "Unable to write DNG image to file", exc) - cont.resumeWithException(exc) - } - } - - // No other formats are supported by this sample - else -> { - val exc = RuntimeException("Unknown image format: ${result.image.format}") - Log.e(TAG, exc.message, exc) - cont.resumeWithException(exc) - } - } - } - - override fun onStop() { - super.onStop() - try { - camera.close() - } catch (exc: Throwable) { - Log.e(TAG, "Error closing camera", exc) - } - } - - override fun onDestroy() { - super.onDestroy() - cameraThread.quitSafely() - imageReaderThread.quitSafely() - } - - override fun onDestroyView() { - _fragmentCameraBinding = null - super.onDestroyView() - } - - companion object { - private val TAG = CameraFragment::class.java.simpleName - - /** Maximum number of images that will be held in the reader's buffer */ - private const val IMAGE_BUFFER_SIZE: Int = 3 - - /** Maximum time allowed to wait for the result of an image capture */ - private const val IMAGE_CAPTURE_TIMEOUT_MILLIS: Long = 5000 - - /** Helper data class used to hold capture metadata with their associated image */ - data class CombinedCaptureResult( - val image: Image, - val metadata: CaptureResult, - val orientation: Int, - val format: Int - ) : Closeable { - override fun close() = image.close() - } - - /** - * Create a [File] named a using formatted timestamp with the current date and time. - * - * @return [File] created. - */ - private fun createFile(context: Context, extension: String): File { - val sdf = SimpleDateFormat("yyyy_MM_dd_HH_mm_ss_SSS", Locale.US) - return File(context.filesDir, "IMG_${sdf.format(Date())}.$extension") - } - } -} diff --git a/Camera2Basic/app/src/main/java/com/example/android/camera2/basic/fragments/ImageViewerFragment.kt b/Camera2Basic/app/src/main/java/com/example/android/camera2/basic/fragments/ImageViewerFragment.kt deleted file mode 100644 index cfb4ca71..00000000 --- a/Camera2Basic/app/src/main/java/com/example/android/camera2/basic/fragments/ImageViewerFragment.kt +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * 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 - * - * https://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 com.example.android.camera2.basic.fragments - -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.graphics.Matrix -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.util.Log -import android.view.ViewGroup.LayoutParams.MATCH_PARENT -import android.widget.ImageView -import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope -import androidx.navigation.fragment.navArgs -import androidx.viewpager2.widget.ViewPager2 -import com.bumptech.glide.Glide -import com.example.android.camera.utils.GenericListAdapter -import com.example.android.camera.utils.decodeExifOrientation -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import java.io.BufferedInputStream -import java.io.File -import kotlin.math.max - - -class ImageViewerFragment : Fragment() { - - /** AndroidX navigation arguments */ - private val args: ImageViewerFragmentArgs by navArgs() - - /** Default Bitmap decoding options */ - private val bitmapOptions = BitmapFactory.Options().apply { - inJustDecodeBounds = false - // Keep Bitmaps at less than 1 MP - if (max(outHeight, outWidth) > DOWNSAMPLE_SIZE) { - val scaleFactorX = outWidth / DOWNSAMPLE_SIZE + 1 - val scaleFactorY = outHeight / DOWNSAMPLE_SIZE + 1 - inSampleSize = max(scaleFactorX, scaleFactorY) - } - } - - /** Bitmap transformation derived from passed arguments */ - private val bitmapTransformation: Matrix by lazy { decodeExifOrientation(args.orientation) } - - /** Flag indicating that there is depth data available for this image */ - private val isDepth: Boolean by lazy { args.depth } - - /** Data backing our Bitmap viewpager */ - private val bitmapList: MutableList = mutableListOf() - - private fun imageViewFactory() = ImageView(requireContext()).apply { - layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT) - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? = ViewPager2(requireContext()).apply { - // Populate the ViewPager and implement a cache of two media items - offscreenPageLimit = 2 - adapter = GenericListAdapter( - bitmapList, - itemViewFactory = { imageViewFactory() }) { view, item, _ -> - view as ImageView - Glide.with(view).load(item).into(view) - } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - view as ViewPager2 - lifecycleScope.launch(Dispatchers.IO) { - - // Load input image file - val inputBuffer = loadInputBuffer() - - // Load the main JPEG image - addItemToViewPager(view, decodeBitmap(inputBuffer, 0, inputBuffer.size)) - - // If we have depth data attached, attempt to load it - if (isDepth) { - try { - val depthStart = findNextJpegEndMarker(inputBuffer, 2) - addItemToViewPager(view, decodeBitmap( - inputBuffer, depthStart, inputBuffer.size - depthStart)) - - val confidenceStart = findNextJpegEndMarker(inputBuffer, depthStart) - addItemToViewPager(view, decodeBitmap( - inputBuffer, confidenceStart, inputBuffer.size - confidenceStart)) - - } catch (exc: RuntimeException) { - Log.e(TAG, "Invalid start marker for depth or confidence data") - } - } - } - } - - /** Utility function used to read input file into a byte array */ - private fun loadInputBuffer(): ByteArray { - val inputFile = File(args.filePath) - return BufferedInputStream(inputFile.inputStream()).let { stream -> - ByteArray(stream.available()).also { - stream.read(it) - stream.close() - } - } - } - - /** Utility function used to add an item to the viewpager and notify it, in the main thread */ - private fun addItemToViewPager(view: ViewPager2, item: Bitmap) = view.post { - bitmapList.add(item) - view.adapter!!.notifyDataSetChanged() - } - - /** Utility function used to decode a [Bitmap] from a byte array */ - private fun decodeBitmap(buffer: ByteArray, start: Int, length: Int): Bitmap { - - // Load bitmap from given buffer - val bitmap = BitmapFactory.decodeByteArray(buffer, start, length, bitmapOptions) - - // Transform bitmap orientation using provided metadata - return Bitmap.createBitmap( - bitmap, 0, 0, bitmap.width, bitmap.height, bitmapTransformation, true) - } - - companion object { - private val TAG = ImageViewerFragment::class.java.simpleName - - /** Maximum size of [Bitmap] decoded */ - private const val DOWNSAMPLE_SIZE: Int = 1024 // 1MP - - /** These are the magic numbers used to separate the different JPG data chunks */ - private val JPEG_DELIMITER_BYTES = arrayOf(-1, -39) - - /** - * Utility function used to find the markers indicating separation between JPEG data chunks - */ - private fun findNextJpegEndMarker(jpegBuffer: ByteArray, start: Int): Int { - - // Sanitize input arguments - assert(start >= 0) { "Invalid start marker: $start" } - assert(jpegBuffer.size > start) { - "Buffer size (${jpegBuffer.size}) smaller than start marker ($start)" } - - // Perform a linear search until the delimiter is found - for (i in start until jpegBuffer.size - 1) { - if (jpegBuffer[i].toInt() == JPEG_DELIMITER_BYTES[0] && - jpegBuffer[i + 1].toInt() == JPEG_DELIMITER_BYTES[1]) { - return i + 2 - } - } - - // If we reach this, it means that no marker was found - throw RuntimeException("Separator marker not found in buffer (${jpegBuffer.size})") - } - } -} diff --git a/Camera2Basic/app/src/main/java/com/example/android/camera2/basic/fragments/PermissionsFragment.kt b/Camera2Basic/app/src/main/java/com/example/android/camera2/basic/fragments/PermissionsFragment.kt deleted file mode 100644 index 1d6ceb53..00000000 --- a/Camera2Basic/app/src/main/java/com/example/android/camera2/basic/fragments/PermissionsFragment.kt +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * 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 - * - * https://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 com.example.android.camera2.basic.fragments - -import android.Manifest -import android.content.Context -import android.content.pm.PackageManager -import android.os.Bundle -import android.widget.Toast -import androidx.core.content.ContextCompat -import androidx.fragment.app.Fragment -import androidx.navigation.Navigation -import androidx.lifecycle.lifecycleScope -import com.example.android.camera2.basic.R - -private const val PERMISSIONS_REQUEST_CODE = 10 -private val PERMISSIONS_REQUIRED = arrayOf(Manifest.permission.CAMERA) - -/** - * This [Fragment] requests permissions and, once granted, it will navigate to the next fragment - */ -class PermissionsFragment : Fragment() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - if (hasPermissions(requireContext())) { - // If permissions have already been granted, proceed - nativateToCamera(); - } else { - // Request camera-related permissions - requestPermissions(PERMISSIONS_REQUIRED, PERMISSIONS_REQUEST_CODE) - } - } - - 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 - nativateToCamera(); - } else { - Toast.makeText(context, "Permission request denied", Toast.LENGTH_LONG).show() - } - } - } - - private fun nativateToCamera() - { - lifecycleScope.launchWhenStarted { - Navigation.findNavController(requireActivity(), R.id.fragment_container).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/Camera2Basic/app/src/main/java/com/example/android/camera2/basic/fragments/SelectorFragment.kt b/Camera2Basic/app/src/main/java/com/example/android/camera2/basic/fragments/SelectorFragment.kt deleted file mode 100644 index e338c8e9..00000000 --- a/Camera2Basic/app/src/main/java/com/example/android/camera2/basic/fragments/SelectorFragment.kt +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * 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 - * - * https://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 com.example.android.camera2.basic.fragments - -import android.annotation.SuppressLint -import android.content.Context -import android.graphics.ImageFormat -import android.hardware.camera2.CameraCharacteristics -import android.hardware.camera2.CameraManager -import android.hardware.camera2.CameraMetadata -import android.os.Bundle -import android.view.LayoutInflater -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.basic.R - -class SelectorFragment : Fragment() { - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? = RecyclerView(requireContext()) - - @SuppressLint("MissingPermission") - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - view as RecyclerView - view.apply { - layoutManager = LinearLayoutManager(requireContext()) - - val cameraManager = - requireContext().getSystemService(Context.CAMERA_SERVICE) as CameraManager - - val cameraList = enumerateCameras(cameraManager) - - val layoutId = android.R.layout.simple_list_item_1 - adapter = GenericListAdapter(cameraList, itemLayoutId = layoutId) { 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.format)) - } - } - } - - } - - companion object { - - /** Helper class used as a data holder for each selectable camera format item */ - private data class FormatItem(val title: String, val cameraId: String, val format: Int) - - /** Helper function used to convert a lens orientation enum into a human-readable string */ - private fun lensOrientationString(value: Int) = when(value) { - CameraCharacteristics.LENS_FACING_BACK -> "Back" - CameraCharacteristics.LENS_FACING_FRONT -> "Front" - CameraCharacteristics.LENS_FACING_EXTERNAL -> "External" - else -> "Unknown" - } - - /** Helper function used to list all compatible cameras and supported pixel formats */ - @SuppressLint("InlinedApi") - private fun enumerateCameras(cameraManager: CameraManager): List { - val availableCameras: MutableList = mutableListOf() - - // Get list of all compatible cameras - val cameraIds = cameraManager.cameraIdList.filter { - val characteristics = cameraManager.getCameraCharacteristics(it) - val capabilities = characteristics.get( - CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES) - capabilities?.contains( - CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE) ?: false - } - - - // Iterate over the list of cameras and return all the compatible ones - cameraIds.forEach { id -> - val characteristics = cameraManager.getCameraCharacteristics(id) - val orientation = lensOrientationString( - characteristics.get(CameraCharacteristics.LENS_FACING)!!) - - // Query the available capabilities and output formats - val capabilities = characteristics.get( - CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)!! - val outputFormats = characteristics.get( - CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!.outputFormats - - // All cameras *must* support JPEG output so we don't need to check characteristics - availableCameras.add(FormatItem( - "$orientation JPEG ($id)", id, ImageFormat.JPEG)) - - // Return cameras that support RAW capability - if (capabilities.contains( - CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW) && - outputFormats.contains(ImageFormat.RAW_SENSOR)) { - availableCameras.add(FormatItem( - "$orientation RAW ($id)", id, ImageFormat.RAW_SENSOR)) - } - - // Return cameras that support JPEG DEPTH capability - if (capabilities.contains( - CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT) && - outputFormats.contains(ImageFormat.DEPTH_JPEG)) { - availableCameras.add(FormatItem( - "$orientation DEPTH ($id)", id, ImageFormat.DEPTH_JPEG)) - } - } - - return availableCameras - } - } -} diff --git a/Camera2Basic/app/src/main/res/drawable-hdpi/ic_launcher.png b/Camera2Basic/app/src/main/res/drawable-hdpi/ic_launcher.png deleted file mode 100644 index bba1165c..00000000 Binary files a/Camera2Basic/app/src/main/res/drawable-hdpi/ic_launcher.png and /dev/null differ diff --git a/Camera2Basic/app/src/main/res/drawable-mdpi/ic_launcher.png b/Camera2Basic/app/src/main/res/drawable-mdpi/ic_launcher.png deleted file mode 100644 index 43045910..00000000 Binary files a/Camera2Basic/app/src/main/res/drawable-mdpi/ic_launcher.png and /dev/null differ diff --git a/Camera2Basic/app/src/main/res/drawable-xhdpi/ic_launcher.png b/Camera2Basic/app/src/main/res/drawable-xhdpi/ic_launcher.png deleted file mode 100644 index 80c5ebaa..00000000 Binary files a/Camera2Basic/app/src/main/res/drawable-xhdpi/ic_launcher.png and /dev/null differ diff --git a/Camera2Basic/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/Camera2Basic/app/src/main/res/drawable-xxhdpi/ic_launcher.png deleted file mode 100644 index 9baac9b6..00000000 Binary files a/Camera2Basic/app/src/main/res/drawable-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/Camera2Basic/app/src/main/res/drawable/ic_photo.xml b/Camera2Basic/app/src/main/res/drawable/ic_photo.xml deleted file mode 100644 index 7224d03c..00000000 --- a/Camera2Basic/app/src/main/res/drawable/ic_photo.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Camera2Basic/app/src/main/res/layout-land/fragment_camera.xml b/Camera2Basic/app/src/main/res/layout-land/fragment_camera.xml deleted file mode 100644 index 343a7fb4..00000000 --- a/Camera2Basic/app/src/main/res/layout-land/fragment_camera.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/Camera2Basic/app/src/main/res/layout/activity_camera.xml b/Camera2Basic/app/src/main/res/layout/activity_camera.xml deleted file mode 100644 index cc6e1b6e..00000000 --- a/Camera2Basic/app/src/main/res/layout/activity_camera.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - \ No newline at end of file diff --git a/Camera2Basic/app/src/main/res/layout/fragment_camera.xml b/Camera2Basic/app/src/main/res/layout/fragment_camera.xml deleted file mode 100644 index ba50a0ca..00000000 --- a/Camera2Basic/app/src/main/res/layout/fragment_camera.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/Camera2Basic/app/src/main/res/navigation/nav_graph.xml b/Camera2Basic/app/src/main/res/navigation/nav_graph.xml deleted file mode 100644 index fa67fc64..00000000 --- a/Camera2Basic/app/src/main/res/navigation/nav_graph.xml +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Camera2Basic/app/src/main/res/values/strings.xml b/Camera2Basic/app/src/main/res/values/strings.xml deleted file mode 100644 index feec0435..00000000 --- a/Camera2Basic/app/src/main/res/values/strings.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - Camera2Basic - Capture - diff --git a/Camera2Basic/app/src/main/res/values/styles.xml b/Camera2Basic/app/src/main/res/values/styles.xml deleted file mode 100644 index b9b5162e..00000000 --- a/Camera2Basic/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - diff --git a/Camera2Basic/app/src/test/java/com/example/android/camera2/basic/InstrumentedTests.kt b/Camera2Basic/app/src/test/java/com/example/android/camera2/basic/InstrumentedTests.kt deleted file mode 100644 index 46b66ad3..00000000 --- a/Camera2Basic/app/src/test/java/com/example/android/camera2/basic/InstrumentedTests.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * 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 - * - * https://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 com.example.android.camera2.basic - -import android.Manifest -import android.content.Context -import android.os.Build -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.rule.ActivityTestRule -import androidx.test.rule.GrantPermissionRule -import org.junit.Assert.assertEquals -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith - - -@RunWith(AndroidJUnit4::class) -class MainInstrumentedTest { - - @get:Rule - val permissionRule = GrantPermissionRule.grant(Manifest.permission.CAMERA) - - @get:Rule - val activityRule: ActivityTestRule = - ActivityTestRule(CameraActivity::class.java) - - @Test - fun useAppContext() { - // Context of the app under test - val context = ApplicationProvider.getApplicationContext() as Context - assertEquals("com.android.example.camera2.basic", context.packageName) - } -} \ No newline at end of file diff --git a/Camera2Basic/build.gradle b/Camera2Basic/build.gradle deleted file mode 100644 index 903b2713..00000000 --- a/Camera2Basic/build.gradle +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * 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 - * - * https://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. - */ - -// Top-level build file where you can add configuration options common to all sub-projects/modules. - -buildscript { - // Top-level variables used for versioning - ext.kotlin_version = '1.9.20' - ext.java_version = JavaVersion.VERSION_1_8 - - repositories { - google() - mavenCentral() - } - dependencies { - classpath 'com.android.tools.build:gradle:8.5.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.7.7" - - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle files - } -} - -allprojects { - repositories { - google() - mavenCentral() - } -} - -tasks.register('clean', Delete) { - delete rootProject.buildDir -} diff --git a/Camera2Basic/gradle.properties b/Camera2Basic/gradle.properties deleted file mode 100644 index 59169ec5..00000000 --- a/Camera2Basic/gradle.properties +++ /dev/null @@ -1,22 +0,0 @@ - -# Project-wide Gradle settings. - -# IDE (e.g. Android Studio) users: -# Settings specified in this file will override any Gradle settings -# configured through the IDE. - -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html - -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. -# Default value: -Xmx10248m -XX:MaxPermSize=256m -# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 - -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true - -android.enableJetifier=true -android.useAndroidX=true \ No newline at end of file diff --git a/Camera2Basic/gradle/wrapper/gradle-wrapper.jar b/Camera2Basic/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 94336fca..00000000 Binary files a/Camera2Basic/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/Camera2Basic/gradle/wrapper/gradle-wrapper.properties b/Camera2Basic/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index c2bd16ee..00000000 --- a/Camera2Basic/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Thu Jul 11 22:18:48 CEST 2024 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/Camera2Basic/gradlew b/Camera2Basic/gradlew deleted file mode 100755 index cccdd3d5..00000000 --- a/Camera2Basic/gradlew +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env sh - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - -exec "$JAVACMD" "$@" diff --git a/Camera2Basic/gradlew.bat b/Camera2Basic/gradlew.bat deleted file mode 100644 index e95643d6..00000000 --- a/Camera2Basic/gradlew.bat +++ /dev/null @@ -1,84 +0,0 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/Camera2Basic/screenshots/icon-web.png b/Camera2Basic/screenshots/icon-web.png deleted file mode 100644 index d9bd4c48..00000000 Binary files a/Camera2Basic/screenshots/icon-web.png and /dev/null differ diff --git a/Camera2Basic/screenshots/main.png b/Camera2Basic/screenshots/main.png deleted file mode 100644 index 76e44e9b..00000000 Binary files a/Camera2Basic/screenshots/main.png and /dev/null differ diff --git a/Camera2Basic/settings.gradle b/Camera2Basic/settings.gradle deleted file mode 100644 index 663c0482..00000000 --- a/Camera2Basic/settings.gradle +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * 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 - * - * https://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. - */ - -include 'app' -include 'utils' diff --git a/Camera2Basic/utils/.gitignore b/Camera2Basic/utils/.gitignore deleted file mode 100644 index 38c58757..00000000 --- a/Camera2Basic/utils/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/build -.idea \ No newline at end of file diff --git a/Camera2Basic/utils/README.md b/Camera2Basic/utils/README.md deleted file mode 100644 index 7a10722f..00000000 --- a/Camera2Basic/utils/README.md +++ /dev/null @@ -1,2 +0,0 @@ -Do not modify code under this folder outside of `CameraUtils`, it is copied -automatically by `.github/scripts/copy_utils.sh`. \ No newline at end of file diff --git a/Camera2Basic/utils/build.gradle b/Camera2Basic/utils/build.gradle deleted file mode 100644 index 86fcd555..00000000 --- a/Camera2Basic/utils/build.gradle +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * 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 - * - * https://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. - */ - -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' - -android { - namespace "com.android.example.camera.utils" - compileSdk 35 - - defaultConfig { - minSdkVersion 21 - targetSdk 35 - versionCode 1 - versionName "1.0" - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles 'consumer-rules.pro' - } - - compileOptions { - sourceCompatibility rootProject.ext.java_version - targetCompatibility rootProject.ext.java_version - } - - kotlinOptions { - jvmTarget = "$rootProject.ext.java_version" - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } -} - -dependencies { - - // Kotlin lang - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' - - // App compat and UI things - implementation 'androidx.appcompat:appcompat:1.7.0' - implementation 'androidx.recyclerview:recyclerview:1.3.2' - - // EXIF Interface - implementation 'androidx.exifinterface:exifinterface:1.3.7' - - // 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' - - // 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' -} diff --git a/Camera2Basic/utils/src/main/AndroidManifest.xml b/Camera2Basic/utils/src/main/AndroidManifest.xml deleted file mode 100644 index f0e666a5..00000000 --- a/Camera2Basic/utils/src/main/AndroidManifest.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/Camera2Basic/utils/src/main/java/com/example/android/camera/utils/AutoFitSurfaceView.kt b/Camera2Basic/utils/src/main/java/com/example/android/camera/utils/AutoFitSurfaceView.kt deleted file mode 100644 index 3d900d19..00000000 --- a/Camera2Basic/utils/src/main/java/com/example/android/camera/utils/AutoFitSurfaceView.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * 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 - * - * https://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 com.example.android.camera.utils - -import android.content.Context -import android.util.AttributeSet -import android.util.Log -import android.view.SurfaceView -import kotlin.math.roundToInt - -/** - * A [SurfaceView] that can be adjusted to a specified aspect ratio and - * performs center-crop transformation of input frames. - */ -class AutoFitSurfaceView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyle: Int = 0 -) : SurfaceView(context, attrs, defStyle) { - - private var aspectRatio = 0f - - /** - * Sets the aspect ratio for this view. The size of the view will be - * measured based on the ratio calculated from the parameters. - * - * @param width Camera resolution horizontal size - * @param height Camera resolution vertical size - */ - fun setAspectRatio(width: Int, height: Int) { - require(width > 0 && height > 0) { "Size cannot be negative" } - aspectRatio = width.toFloat() / height.toFloat() - holder.setFixedSize(width, height) - requestLayout() - } - - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec) - val width = MeasureSpec.getSize(widthMeasureSpec) - val height = MeasureSpec.getSize(heightMeasureSpec) - if (aspectRatio == 0f) { - setMeasuredDimension(width, height) - } else { - - // Performs center-crop transformation of the camera frames - val newWidth: Int - val newHeight: Int - val actualRatio = if (width > height) aspectRatio else 1f / aspectRatio - if (width < height * actualRatio) { - newHeight = height - newWidth = (height * actualRatio).roundToInt() - } else { - newWidth = width - newHeight = (width / actualRatio).roundToInt() - } - - Log.d(TAG, "Measured dimensions set: $newWidth x $newHeight") - setMeasuredDimension(newWidth, newHeight) - } - } - - companion object { - private val TAG = AutoFitSurfaceView::class.java.simpleName - } -} diff --git a/Camera2Basic/utils/src/main/java/com/example/android/camera/utils/CameraSizes.kt b/Camera2Basic/utils/src/main/java/com/example/android/camera/utils/CameraSizes.kt deleted file mode 100644 index 6db01d31..00000000 --- a/Camera2Basic/utils/src/main/java/com/example/android/camera/utils/CameraSizes.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * 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 - * - * https://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 com.example.android.camera.utils - -import android.graphics.Point -import android.hardware.camera2.CameraCharacteristics -import android.hardware.camera2.params.StreamConfigurationMap -import android.util.Size -import android.view.Display -import kotlin.math.max -import kotlin.math.min - -/** Helper class used to pre-compute shortest and longest sides of a [Size] */ -class SmartSize(width: Int, height: Int) { - var size = Size(width, height) - var long = max(size.width, size.height) - var short = min(size.width, size.height) - override fun toString() = "SmartSize(${long}x${short})" -} - -/** Standard High Definition size for pictures and video */ -val SIZE_1080P: SmartSize = SmartSize(1920, 1080) - -/** Returns a [SmartSize] object for the given [Display] */ -fun getDisplaySmartSize(display: Display): SmartSize { - val outPoint = Point() - display.getRealSize(outPoint) - return SmartSize(outPoint.x, outPoint.y) -} - -/** - * Returns the largest available PREVIEW size. For more information, see: - * https://d.android.com/reference/android/hardware/camera2/CameraDevice and - * https://developer.android.com/reference/android/hardware/camera2/params/StreamConfigurationMap - */ -fun getPreviewOutputSize( - 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)!! - if (format == null) - assert(StreamConfigurationMap.isOutputSupportedFor(targetClass)) - else - assert(config.isOutputSupportedFor(format)) - val allSizes = if (format == null) - config.getOutputSizes(targetClass) else config.getOutputSizes(format) - - // Get available sizes and sort them by area from largest to smallest - val validSizes = allSizes - .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 -} \ No newline at end of file diff --git a/Camera2Basic/utils/src/main/java/com/example/android/camera/utils/ExifUtils.kt b/Camera2Basic/utils/src/main/java/com/example/android/camera/utils/ExifUtils.kt deleted file mode 100644 index 561c14b3..00000000 --- a/Camera2Basic/utils/src/main/java/com/example/android/camera/utils/ExifUtils.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * 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 - * - * https://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 com.example.android.camera.utils - -import android.graphics.Bitmap -import android.graphics.Matrix -import android.util.Log -import androidx.exifinterface.media.ExifInterface - -private const val TAG: String = "ExifUtils" - -/** Transforms rotation and mirroring information into one of the [ExifInterface] constants */ -fun computeExifOrientation(rotationDegrees: Int, mirrored: Boolean) = when { - rotationDegrees == 0 && !mirrored -> ExifInterface.ORIENTATION_NORMAL - rotationDegrees == 0 && mirrored -> ExifInterface.ORIENTATION_FLIP_HORIZONTAL - rotationDegrees == 180 && !mirrored -> ExifInterface.ORIENTATION_ROTATE_180 - rotationDegrees == 180 && mirrored -> ExifInterface.ORIENTATION_FLIP_VERTICAL - rotationDegrees == 270 && mirrored -> ExifInterface.ORIENTATION_TRANSVERSE - rotationDegrees == 90 && !mirrored -> ExifInterface.ORIENTATION_ROTATE_90 - rotationDegrees == 90 && mirrored -> ExifInterface.ORIENTATION_TRANSPOSE - rotationDegrees == 270 && mirrored -> ExifInterface.ORIENTATION_ROTATE_270 - rotationDegrees == 270 && !mirrored -> ExifInterface.ORIENTATION_TRANSVERSE - else -> ExifInterface.ORIENTATION_UNDEFINED -} - -/** - * Helper function used to convert an EXIF orientation enum into a transformation matrix - * that can be applied to a bitmap. - * - * @return matrix - Transformation required to properly display [Bitmap] - */ -fun decodeExifOrientation(exifOrientation: Int): Matrix { - val matrix = Matrix() - - // Apply transformation corresponding to declared EXIF orientation - when (exifOrientation) { - ExifInterface.ORIENTATION_NORMAL -> Unit - ExifInterface.ORIENTATION_UNDEFINED -> Unit - ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90F) - ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180F) - ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270F) - ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.postScale(-1F, 1F) - ExifInterface.ORIENTATION_FLIP_VERTICAL -> matrix.postScale(1F, -1F) - ExifInterface.ORIENTATION_TRANSPOSE -> { - matrix.postScale(-1F, 1F) - matrix.postRotate(270F) - } - ExifInterface.ORIENTATION_TRANSVERSE -> { - matrix.postScale(-1F, 1F) - matrix.postRotate(90F) - } - - // Error out if the EXIF orientation is invalid - else -> Log.e(TAG, "Invalid orientation: $exifOrientation") - } - - // Return the resulting matrix - return matrix -} diff --git a/Camera2Basic/utils/src/main/java/com/example/android/camera/utils/GenericListAdapter.kt b/Camera2Basic/utils/src/main/java/com/example/android/camera/utils/GenericListAdapter.kt deleted file mode 100644 index a55af278..00000000 --- a/Camera2Basic/utils/src/main/java/com/example/android/camera/utils/GenericListAdapter.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * 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 - * - * https://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 com.example.android.camera.utils - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView - -/** Type helper used for the callback triggered once our view has been bound */ -typealias BindCallback = (view: View, data: T, position: Int) -> Unit - -/** List adapter for generic types, intended used for small-medium lists of data */ -class GenericListAdapter( - private val dataset: List, - private val itemLayoutId: Int? = null, - private val itemViewFactory: (() -> View)? = null, - private val onBind: BindCallback -) : RecyclerView.Adapter() { - - class GenericListViewHolder(val view: View) : RecyclerView.ViewHolder(view) - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = GenericListViewHolder(when { - itemViewFactory != null -> itemViewFactory.invoke() - itemLayoutId != null -> { - LayoutInflater.from(parent.context) - .inflate(itemLayoutId, parent, false) - } - else -> { - throw IllegalStateException( - "Either the layout ID or the view factory need to be non-null") - } - }) - - override fun onBindViewHolder(holder: GenericListViewHolder, position: Int) { - if (position < 0 || position > dataset.size) return - onBind(holder.view, dataset[position], position) - } - - override fun getItemCount() = dataset.size -} \ No newline at end of file diff --git a/Camera2Basic/utils/src/main/java/com/example/android/camera/utils/OrientationLiveData.kt b/Camera2Basic/utils/src/main/java/com/example/android/camera/utils/OrientationLiveData.kt deleted file mode 100644 index f9d9a470..00000000 --- a/Camera2Basic/utils/src/main/java/com/example/android/camera/utils/OrientationLiveData.kt +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * 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 - * - * https://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 com.example.android.camera.utils - -import android.content.Context -import android.hardware.camera2.CameraCharacteristics -import android.view.OrientationEventListener -import android.view.Surface -import androidx.lifecycle.LiveData - - -/** - * Calculates closest 90-degree orientation to compensate for the device - * rotation relative to sensor orientation, i.e., allows user to see camera - * frames with the expected orientation. - */ -class OrientationLiveData( - context: Context, - characteristics: CameraCharacteristics -): LiveData() { - - private val listener = object : OrientationEventListener(context.applicationContext) { - override fun onOrientationChanged(orientation: Int) { - val rotation = when { - orientation <= 45 -> Surface.ROTATION_0 - orientation <= 135 -> Surface.ROTATION_90 - orientation <= 225 -> Surface.ROTATION_180 - orientation <= 315 -> Surface.ROTATION_270 - else -> Surface.ROTATION_0 - } - val relative = computeRelativeRotation(characteristics, rotation) - if (relative != value) postValue(relative) - } - } - - override fun onActive() { - super.onActive() - listener.enable() - } - - override fun onInactive() { - super.onInactive() - listener.disable() - } - - companion object { - - /** - * Computes rotation required to transform from the camera sensor orientation to the - * device's current orientation in degrees. - * - * @param characteristics the [CameraCharacteristics] to query for the sensor orientation. - * @param surfaceRotation the current device orientation as a Surface constant - * @return the relative rotation from the camera sensor to the current device orientation. - */ - @JvmStatic - private fun computeRelativeRotation( - characteristics: CameraCharacteristics, - surfaceRotation: Int - ): Int { - val sensorOrientationDegrees = - characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!! - - val deviceOrientationDegrees = when (surfaceRotation) { - Surface.ROTATION_0 -> 0 - Surface.ROTATION_90 -> 90 - Surface.ROTATION_180 -> 180 - Surface.ROTATION_270 -> 270 - else -> 0 - } - - // Reverse device orientation for front-facing cameras - val sign = if (characteristics.get(CameraCharacteristics.LENS_FACING) == - CameraCharacteristics.LENS_FACING_FRONT) 1 else -1 - - // Calculate desired JPEG orientation relative to camera orientation to make - // the image upright relative to the device orientation - return (sensorOrientationDegrees - (deviceOrientationDegrees * sign) + 360) % 360 - } - } -} diff --git a/Camera2Basic/utils/src/main/java/com/example/android/camera/utils/Yuv.kt b/Camera2Basic/utils/src/main/java/com/example/android/camera/utils/Yuv.kt deleted file mode 100644 index c476ad0e..00000000 --- a/Camera2Basic/utils/src/main/java/com/example/android/camera/utils/Yuv.kt +++ /dev/null @@ -1,191 +0,0 @@ -package com.example.android.camera.utils - -import android.graphics.ImageFormat -import android.media.Image -import androidx.annotation.IntDef -import java.nio.ByteBuffer - -/* -This file is converted from part of https://github.com/gordinmitya/yuv2buf. -Follow the link to find demo app, performance benchmarks and unit tests. - -Intro to YUV image formats: -YUV_420_888 - is a generic format that can be represented as I420, YV12, NV21, and NV12. -420 means that for each 4 luminosity pixels we have 2 chroma pixels: U and V. - -* I420 format represents an image as Y plane followed by U then followed by V plane - without chroma channels interleaving. - For example: - Y Y Y Y - Y Y Y Y - U U V V - -* NV21 format represents an image as Y plane followed by V and U interleaved. First V then U. - For example: - Y Y Y Y - Y Y Y Y - V U V U - -* YV12 and NV12 are the same as previous formats but with swapped order of V and U. (U then V) - -Visualization of these 4 formats: -https://user-images.githubusercontent.com/9286092/89119601-4f6f8100-d4b8-11ea-9a51-2765f7e513c2.jpg - -It's guaranteed that image.getPlanes() always returns planes in order Y U V for YUV_420_888. -https://developer.android.com/reference/android/graphics/ImageFormat#YUV_420_888 - -Because I420 and NV21 are more widely supported (RenderScript, OpenCV, MNN) -the conversion is done into these formats. - -More about each format: https://www.fourcc.org/yuv.php -*/ - -@kotlin.annotation.Retention(AnnotationRetention.SOURCE) -@IntDef(ImageFormat.NV21, ImageFormat.YUV_420_888) -annotation class YuvType - -class YuvByteBuffer(image: Image, dstBuffer: ByteBuffer? = null) { - @YuvType - val type: Int - val buffer: ByteBuffer - - init { - val wrappedImage = ImageWrapper(image) - - type = if (wrappedImage.u.pixelStride == 1) { - ImageFormat.YUV_420_888 - } else { - ImageFormat.NV21 - } - val size = image.width * image.height * 3 / 2 - buffer = if ( - dstBuffer == null || dstBuffer.capacity() < size || - dstBuffer.isReadOnly || !dstBuffer.isDirect - ) { - ByteBuffer.allocateDirect(size) } - else { - dstBuffer - } - buffer.rewind() - - removePadding(wrappedImage) - } - - // Input buffers are always direct as described in - // https://developer.android.com/reference/android/media/Image.Plane#getBuffer() - private fun removePadding(image: ImageWrapper) { - val sizeLuma = image.y.width * image.y.height - val sizeChroma = image.u.width * image.u.height - if (image.y.rowStride > image.y.width) { - removePaddingCompact(image.y, buffer, 0) - } else { - buffer.position(0) - buffer.put(image.y.buffer) - } - if (type == ImageFormat.YUV_420_888) { - if (image.u.rowStride > image.u.width) { - removePaddingCompact(image.u, buffer, sizeLuma) - removePaddingCompact(image.v, buffer, sizeLuma + sizeChroma) - } else { - buffer.position(sizeLuma) - buffer.put(image.u.buffer) - buffer.position(sizeLuma + sizeChroma) - buffer.put(image.v.buffer) - } - } else { - if (image.u.rowStride > image.u.width * 2) { - removePaddingNotCompact(image, buffer, sizeLuma) - } else { - buffer.position(sizeLuma) - var uv = image.v.buffer - val properUVSize = image.v.height * image.v.rowStride - 1 - if (uv.capacity() > properUVSize) { - uv = clipBuffer(image.v.buffer, 0, properUVSize) - } - buffer.put(uv) - val lastOne = image.u.buffer[image.u.buffer.capacity() - 1] - buffer.put(buffer.capacity() - 1, lastOne) - } - } - buffer.rewind() - } - - private fun removePaddingCompact( - plane: PlaneWrapper, - dst: ByteBuffer, - offset: Int - ) { - require(plane.pixelStride == 1) { - "use removePaddingCompact with pixelStride == 1" - } - - val src = plane.buffer - val rowStride = plane.rowStride - var row: ByteBuffer - dst.position(offset) - for (i in 0 until plane.height) { - row = clipBuffer(src, i * rowStride, plane.width) - dst.put(row) - } - } - - private fun removePaddingNotCompact( - image: ImageWrapper, - dst: ByteBuffer, - offset: Int - ) { - require(image.u.pixelStride == 2) { - "use removePaddingNotCompact pixelStride == 2" - } - val width = image.u.width - val height = image.u.height - val rowStride = image.u.rowStride - var row: ByteBuffer - dst.position(offset) - for (i in 0 until height - 1) { - row = clipBuffer(image.v.buffer, i * rowStride, width * 2) - dst.put(row) - } - row = clipBuffer(image.u.buffer, (height - 1) * rowStride - 1, width * 2) - dst.put(row) - } - - private fun clipBuffer(buffer: ByteBuffer, start: Int, size: Int): ByteBuffer { - val duplicate = buffer.duplicate() - duplicate.position(start) - duplicate.limit(start + size) - return duplicate.slice() - } - - private class ImageWrapper(image:Image) { - val width= image.width - val height = image.height - val y = PlaneWrapper(width, height, image.planes[0]) - val u = PlaneWrapper(width / 2, height / 2, image.planes[1]) - val v = PlaneWrapper(width / 2, height / 2, image.planes[2]) - - // Check this is a supported image format - // https://developer.android.com/reference/android/graphics/ImageFormat#YUV_420_888 - init { - require(y.pixelStride == 1) { - "Pixel stride for Y plane must be 1 but got ${y.pixelStride} instead." - } - require(u.pixelStride == v.pixelStride && u.rowStride == v.rowStride) { - "U and V planes must have the same pixel and row strides " + - "but got pixel=${u.pixelStride} row=${u.rowStride} for U " + - "and pixel=${v.pixelStride} and row=${v.rowStride} for V" - } - require(u.pixelStride == 1 || u.pixelStride == 2) { - "Supported" + " pixel strides for U and V planes are 1 and 2" - } - } - } - - private class PlaneWrapper(width: Int, height: Int, plane: Image.Plane) { - val width = width - val height = height - val buffer: ByteBuffer = plane.buffer - val rowStride = plane.rowStride - val pixelStride = plane.pixelStride - } -} \ No newline at end of file diff --git a/Camera2Basic/utils/src/main/java/com/example/android/camera/utils/YuvToRgbConverter.kt b/Camera2Basic/utils/src/main/java/com/example/android/camera/utils/YuvToRgbConverter.kt deleted file mode 100644 index 8dcd5596..00000000 --- a/Camera2Basic/utils/src/main/java/com/example/android/camera/utils/YuvToRgbConverter.kt +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2020 The Android Open Source Project - * - * 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 - * - * https://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 com.example.android.camera.utils - -import android.content.Context -import android.graphics.Bitmap -import android.graphics.ImageFormat -import android.media.Image -import android.renderscript.Allocation -import android.renderscript.Element -import android.renderscript.RenderScript -import android.renderscript.ScriptIntrinsicYuvToRGB -import android.renderscript.Type -import java.nio.ByteBuffer - -/** - * Helper class used to convert a [Image] object from - * [ImageFormat.YUV_420_888] format to an RGB [Bitmap] object, it has equivalent - * functionality to https://github - * .com/androidx/androidx/blob/androidx-main/camera/camera-core/src/main/java/androidx/camera/core/ImageYuvToRgbConverter.java - * - * NOTE: This has been tested in a limited number of devices and is not - * considered production-ready code. It was created for illustration purposes, - * since this is not an efficient camera pipeline due to the multiple copies - * required to convert each frame. For example, this - * implementation - * (https://stackoverflow.com/questions/52726002/camera2-captured-picture-conversion-from-yuv-420-888-to-nv21/52740776#52740776) - * might have better performance. - */ -class YuvToRgbConverter(context: Context) { - private val rs = RenderScript.create(context) - private val scriptYuvToRgb = - ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs)) - - // Do not add getters/setters functions to these private variables - // because yuvToRgb() assume they won't be modified elsewhere - private var yuvBits: ByteBuffer? = null - private var bytes: ByteArray = ByteArray(0) - private var inputAllocation: Allocation? = null - private var outputAllocation: Allocation? = null - - @Synchronized - fun yuvToRgb(image: Image, output: Bitmap) { - val yuvBuffer = YuvByteBuffer(image, yuvBits) - yuvBits = yuvBuffer.buffer - - if (needCreateAllocations(image, yuvBuffer)) { - val yuvType = Type.Builder(rs, Element.U8(rs)) - .setX(image.width) - .setY(image.height) - .setYuvFormat(yuvBuffer.type) - inputAllocation = Allocation.createTyped( - rs, - yuvType.create(), - Allocation.USAGE_SCRIPT - ) - bytes = ByteArray(yuvBuffer.buffer.capacity()) - val rgbaType = Type.Builder(rs, Element.RGBA_8888(rs)) - .setX(image.width) - .setY(image.height) - outputAllocation = Allocation.createTyped( - rs, - rgbaType.create(), - Allocation.USAGE_SCRIPT - ) - } - - yuvBuffer.buffer.get(bytes) - inputAllocation!!.copyFrom(bytes) - - // Convert NV21 or YUV_420_888 format to RGB - inputAllocation!!.copyFrom(bytes) - scriptYuvToRgb.setInput(inputAllocation) - scriptYuvToRgb.forEach(outputAllocation) - outputAllocation!!.copyTo(output) - } - - private fun needCreateAllocations(image: Image, yuvBuffer: YuvByteBuffer): Boolean { - return (inputAllocation == null || // the very 1st call - inputAllocation!!.type.x != image.width || // image size changed - inputAllocation!!.type.y != image.height || - inputAllocation!!.type.yuv != yuvBuffer.type || // image format changed - bytes.size == yuvBuffer.buffer.capacity()) - } -} diff --git a/Camera2Basic/utils/src/main/res/drawable/ic_shutter.xml b/Camera2Basic/utils/src/main/res/drawable/ic_shutter.xml deleted file mode 100644 index 9bb91ab8..00000000 --- a/Camera2Basic/utils/src/main/res/drawable/ic_shutter.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/Camera2Basic/utils/src/main/res/drawable/ic_shutter_focused.xml b/Camera2Basic/utils/src/main/res/drawable/ic_shutter_focused.xml deleted file mode 100644 index 9bf521da..00000000 --- a/Camera2Basic/utils/src/main/res/drawable/ic_shutter_focused.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - diff --git a/Camera2Basic/utils/src/main/res/drawable/ic_shutter_normal.xml b/Camera2Basic/utils/src/main/res/drawable/ic_shutter_normal.xml deleted file mode 100644 index cb50026e..00000000 --- a/Camera2Basic/utils/src/main/res/drawable/ic_shutter_normal.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - diff --git a/Camera2Basic/utils/src/main/res/drawable/ic_shutter_pressed.xml b/Camera2Basic/utils/src/main/res/drawable/ic_shutter_pressed.xml deleted file mode 100644 index 9bf521da..00000000 --- a/Camera2Basic/utils/src/main/res/drawable/ic_shutter_pressed.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - diff --git a/Camera2Extensions/.google/packaging.yaml b/Camera2Extensions/.google/packaging.yaml deleted file mode 100644 index ce8a73e8..00000000 --- a/Camera2Extensions/.google/packaging.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (C) 2021 The Android Open Source Project -# -# 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 -# -# https://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. -# -# GOOGLE SAMPLE PACKAGING DATA -# -# This file is used by Google as part of our samples packaging process. -# End users may safely ignore this file. It has no relevance to other systems. ---- -status: PUBLISHED -technologies: [Android] -categories: [Camera] -languages: [Kotlin] -solutions: [Mobile] -github: android/camera-samples -level: INTERMEDIATE -apiRefs: - - android:android.hardware.camera2 -license: apache2 diff --git a/Camera2Extensions/README.md b/Camera2Extensions/README.md deleted file mode 100644 index 36dac63f..00000000 --- a/Camera2Extensions/README.md +++ /dev/null @@ -1,39 +0,0 @@ - -Android Camera2Extensions Sample -=========================== - -This sample captures [Camera2][1] extensions generated images via the Camera2 API -including displaying a camera preview and using repeating capture requests. -This sample application can only be built and used on Android SDK 31+. - -Introduction ------------- -This sample displays a live camera preview in TextureView and allows the user to -capture a still image using the Camera Extension [API][2] - -[1]: https://developer.android.com/reference/android/hardware/camera2/package-summary.html -[2]: https://developer.android.com/reference/android/hardware/camera2/CameraExtensionCharacteristics - -Pre-requisites --------------- - -- Android SDK 31+ -- Android Studio 3.6+ -- Device with CameraX/Camera2 extensions - -Getting Started ---------------- - -This sample uses the Gradle build system. To build this project, use the -"gradlew build" command or use "Import Project" in Android Studio. - -Support -------- - -- Stack Overflow: http://stackoverflow.com/questions/tagged/android - -If you've found an error in this sample, please file an issue: -https://github.com/android/camera-samples - -Patches are encouraged, and may be submitted by forking this project and -submitting a pull request through GitHub. Please see CONTRIBUTING.md for more details. diff --git a/Camera2Extensions/app/build.gradle b/Camera2Extensions/app/build.gradle deleted file mode 100644 index b3b9bc32..00000000 --- a/Camera2Extensions/app/build.gradle +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2021 The Android Open Source Project - * - * 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 - * - * https://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. - */ - -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-kapt' -apply plugin: "androidx.navigation.safeargs" - -android { - defaultConfig { - testInstrumentationRunner kotlin_version - applicationId "com.android.example.camera2.extensions" - compileSdk 34 - minSdkVersion 31 - targetSdkVersion 33 - versionCode 1 - versionName "1.0.0" - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8 - } - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } - - // Set the source of tests to same for both Unit and Instrumented tests - sourceSets { - String sharedTestDir = 'src/test/java' - test { - java.srcDir sharedTestDir - } - androidTest { - java.srcDir sharedTestDir - } - } - buildToolsVersion '30.0.3' - - buildFeatures { - viewBinding true - } -} - -dependencies { - implementation "androidx.activity:activity-ktx:1.2.3" - implementation "androidx.fragment:fragment-ktx:1.3.5" - implementation(project(":utils")) - - // Kotlin lang - implementation 'androidx.core:core-ktx:1.6.0' - //noinspection GradleDependency - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1' - - // App compat and UI things - implementation 'androidx.appcompat:appcompat:1.3.0' - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' - implementation 'androidx.constraintlayout:constraintlayout:2.0.4' - implementation 'com.google.android.material:material:1.6.0' - - // Navigation library - def nav_version = "2.3.5" - 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.3' - testImplementation 'androidx.test:rules:1.4.0' - testImplementation 'androidx.test:runner:1.4.0' - testImplementation 'androidx.test.espresso:espresso-core:3.4.0' - testImplementation "org.robolectric:robolectric:4.3.1" - - // Instrumented testing - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - androidTestImplementation 'androidx.test:rules:1.4.0' - androidTestImplementation 'androidx.test:runner:1.4.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' -} diff --git a/Camera2Extensions/app/src/main/AndroidManifest.xml b/Camera2Extensions/app/src/main/AndroidManifest.xml deleted file mode 100644 index d8918cc0..00000000 --- a/Camera2Extensions/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Camera2Extensions/app/src/main/java/com/example/android/camera2/extensions/CameraActivity.kt b/Camera2Extensions/app/src/main/java/com/example/android/camera2/extensions/CameraActivity.kt deleted file mode 100644 index a77a7386..00000000 --- a/Camera2Extensions/app/src/main/java/com/example/android/camera2/extensions/CameraActivity.kt +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * 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 com.example.android.camera2.extensions - -import android.Manifest -import android.content.pm.PackageManager -import android.os.Bundle -import android.view.View -import android.widget.FrameLayout -import android.widget.Toast -import androidx.appcompat.app.AppCompatActivity - -import androidx.activity.result.contract.ActivityResultContracts -import androidx.core.content.ContextCompat - -/* - * This is the launching point for the camera extension app where the camera fragment container - * is delayed to allow the UI to settle. All of the camera extension code can be found in - * "CameraFragment". - */ -class CameraActivity : AppCompatActivity() { - - private lateinit var container: FrameLayout - - private val requestPermissionLauncher = - registerForActivityResult( - ActivityResultContracts.RequestPermission() - ) { isGranted: Boolean -> - if (isGranted) { - // Before setting full screen flags, 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 - container.postDelayed({ - @Suppress("DEPRECATION") - container.systemUiVisibility = FLAGS_FULLSCREEN - }, - IMMERSIVE_FLAG_TIMEOUT) - } else { - Toast.makeText(this, R.string.permission_required, - Toast.LENGTH_SHORT).show() - finish() - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_camera) - container = findViewById(R.id.fragment_container) - } - - override fun onResume() { - super.onResume() - if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == - PackageManager.PERMISSION_GRANTED) { - container.postDelayed({ - @Suppress("DEPRECATION") - container.systemUiVisibility = FLAGS_FULLSCREEN - }, - IMMERSIVE_FLAG_TIMEOUT) - } else { - requestPermissionLauncher.launch(Manifest.permission.CAMERA) - } - } - - companion object { - /** Combination of all flags required to put activity into immersive mode */ - @Suppress("DEPRECATION") - 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 */ - private const val IMMERSIVE_FLAG_TIMEOUT = 500L - } -} diff --git a/Camera2Extensions/app/src/main/java/com/example/android/camera2/extensions/GenericListAdapter.kt b/Camera2Extensions/app/src/main/java/com/example/android/camera2/extensions/GenericListAdapter.kt deleted file mode 100644 index 4a345285..00000000 --- a/Camera2Extensions/app/src/main/java/com/example/android/camera2/extensions/GenericListAdapter.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * 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 com.example.android.camera2.extensions - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView - -/** List adapter for generic types, intended used for small-medium lists of data */ -class GenericListAdapter( - private val dataset: List, - private val itemLayoutId: Int? = null, - private val itemViewFactory: (() -> View)? = null, - private val onBind: (view: View, data: T, position: Int) -> Unit -) : RecyclerView.Adapter() { - - class GenericListViewHolder(val view: View) : RecyclerView.ViewHolder(view) - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = GenericListViewHolder( - when { - itemViewFactory != null -> itemViewFactory.invoke() - itemLayoutId != null -> { - LayoutInflater.from(parent.context) - .inflate(itemLayoutId, parent, false) - } - else -> { - throw IllegalStateException( - "Either the layout ID or the view factory need to be non-null") - } - }) - - override fun onBindViewHolder(holder: GenericListViewHolder, position: Int) { - if (position < 0 || position > dataset.size) return - onBind(holder.view, dataset[position], position) - } - - override fun getItemCount() = dataset.size -} \ No newline at end of file diff --git a/Camera2Extensions/app/src/main/java/com/example/android/camera2/extensions/ZoomUtil.kt b/Camera2Extensions/app/src/main/java/com/example/android/camera2/extensions/ZoomUtil.kt deleted file mode 100644 index 49f3c1af..00000000 --- a/Camera2Extensions/app/src/main/java/com/example/android/camera2/extensions/ZoomUtil.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * 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 com.example.android.camera2.extensions - -import android.graphics.Rect -import android.hardware.camera2.CameraCharacteristics - -object ZoomUtil { - - fun minZoom() = 1.0f - - fun maxZoom(characteristics: CameraCharacteristics): Float { - val maxZoom = characteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM) - ?: return minZoom() - - if (maxZoom < minZoom()) return minZoom() - - return maxZoom - } - - fun getZoomCropRect(zoomRatio: Float, characteristics: CameraCharacteristics): Rect { - val sensorRect = sensorRect(characteristics) - return cropRectByRatio(zoomRatio, sensorRect) - } - - private fun cropRectByRatio(zoomRatio: Float, sensorRect: Rect): Rect { - val cropWidth = sensorRect.width() / zoomRatio - val cropHeight = sensorRect.height() / zoomRatio - - val left = (sensorRect.width() - cropWidth) / 2.0f - val top = (sensorRect.height() - cropHeight) / 2.0f - - return Rect( - left.toInt(), - top.toInt(), - (left + cropWidth).toInt(), - (top + cropHeight).toInt() - ) - } - - private fun sensorRect(characteristics: CameraCharacteristics): Rect = - characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)!! -} \ No newline at end of file diff --git a/Camera2Extensions/app/src/main/java/com/example/android/camera2/extensions/fragments/CameraFragment.kt b/Camera2Extensions/app/src/main/java/com/example/android/camera2/extensions/fragments/CameraFragment.kt deleted file mode 100644 index 7c396b45..00000000 --- a/Camera2Extensions/app/src/main/java/com/example/android/camera2/extensions/fragments/CameraFragment.kt +++ /dev/null @@ -1,1319 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * 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 com.example.android.camera2.extensions.fragments - -import android.annotation.SuppressLint -import android.content.Context -import android.graphics.* -import android.hardware.camera2.* -import android.hardware.camera2.CameraExtensionSession.StillCaptureLatency -import android.hardware.camera2.params.ExtensionSessionConfiguration -import android.hardware.camera2.params.MeteringRectangle -import android.hardware.camera2.params.OutputConfiguration -import android.media.Image -import android.media.ImageReader -import android.os.* -import android.util.Log -import android.util.Size -import android.view.* -import android.widget.Toast -import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope -import androidx.navigation.fragment.navArgs -import com.example.android.camera.utils.YuvToRgbConverter -import com.example.android.camera2.extensions.R -import com.example.android.camera2.extensions.ZoomUtil -import com.example.android.camera2.extensions.databinding.FragmentCameraBinding -import com.google.android.material.slider.Slider -import java.io.File -import java.io.FileOutputStream -import java.io.OutputStream -import java.nio.ByteBuffer -import java.util.concurrent.Executors -import java.util.concurrent.ScheduledFuture -import java.util.concurrent.TimeUnit -import java.util.stream.Collectors -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlin.math.abs -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.asExecutor -import kotlinx.coroutines.launch -import kotlinx.coroutines.suspendCancellableCoroutine - -/** - * The still capture progress is in DONE state. - */ -private const val PROGRESS_STATE_DONE = 0 -/** - * The still capture progress is in HOLD_STILL state which should show a message to request the end - * users to hold still. - */ -private const val PROGRESS_STATE_HOLD_STILL = 1 -/** - * The still capture progress is in STILL_PROCESSING state which should show a message to let the - * end users know the still image is under processing. - */ -private const val PROGRESS_STATE_STILL_PROCESSING = 2 - -/* - * This is the main camera fragment where all camera extension logic can be found. - * The module demonstrates a typical camera extension use case with preview running in - * a TextureView and also corresponding still capture functionality. - * These features are only available on SDK 31 and higher. - */ -class CameraFragment : Fragment(), TextureView.SurfaceTextureListener { - - /** AndroidX navigation arguments */ - private val args: CameraFragmentArgs by navArgs() - - /** Detects, characterizes, and connects to a CameraDevice (used for all camera operations) */ - private val cameraManager: CameraManager by lazy { - val context = requireContext().applicationContext - context.getSystemService(Context.CAMERA_SERVICE) as CameraManager - } - - /** - * Still capture image reader - */ - private lateinit var stillImageReader: ImageReader - - /** - * Preview surface - */ - private lateinit var previewSurface: Surface - - /** - * Size of preview - */ - private lateinit var previewSize: Size - - /** - * Camera extension characteristics for the current camera device. - */ - private lateinit var extensionCharacteristics: CameraExtensionCharacteristics - - /** - * Camera characteristics for the current camera device. - */ - private lateinit var characteristics: CameraCharacteristics - - /** - * Flag whether we should restart preview after an extension switch. - */ - private var restartPreview = false - - /** - * Track current extension type and index. - */ - private var currentExtension = -1 - private var currentExtensionIdx = -1 - - /** - * The current camera extension session. - */ - private lateinit var cameraExtensionSession: CameraExtensionSession - - /** - * A reference to the opened [CameraDevice]. - */ - private lateinit var cameraDevice: CameraDevice - - /** - * A reference to the current view binding. - */ - private var _binding: FragmentCameraBinding? = null - private val binding get() = _binding!! - - private var zoomRatio: Float = ZoomUtil.minZoom() - - /** - * Track the extension strength support for current extension mode. - */ - private var isExtensionStrengthAvailable = false - - /** - * Track the extension strength setting. - */ - private var extensionStrength = -1 - /** - * Track the postview support for current extension mode. - */ - private var isPostviewAvailable = false - /** - * Track the capture process progress support for current extension mode. - */ - private var isCaptureProcessProgressAvailable = false - /** - * A reference to the image reader to receive the postview when it can be supported for current - * extension mode. - */ - private var postviewImageReader: ImageReader? = null - /** - * A ScheduledFuture for repeatedly updating the capture progress info. - */ - private var progressInfoScheduledFuture: ScheduledFuture<*>? = null - /** - * Track the process state of current still capture request. - */ - private var progressState = PROGRESS_STATE_DONE - /** - * Track the still capture latency of current still capture request. - */ - private var stillCaptureLatency: StillCaptureLatency? = null - /** - * Track the HOLD_STILL or STILL_PROCESSING start timestamp of current still capture request. - */ - private var progressStartTimestampMs: Long = 0 - /** - * Track the capture processing progress of current still capture request. - */ - private var captureProcessingProgress = -1 - - /** - * Calculates the remaining duration for the latency of current state. - * - * The duration is calculated against the start timestamp which was stored when current process - * state was begun. - */ - private fun calculateRemainingDurationInMs(): Long { - // Only supported when API level is 34 or above - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - return 0 - } - - // Only supported when capture latency info is provided - if (stillCaptureLatency == null) { - return 0 - } - - val currentTimestampMs = SystemClock.elapsedRealtime() - val pastTimeMs = currentTimestampMs - progressStartTimestampMs - val remainingTimeMs = if (progressState == PROGRESS_STATE_HOLD_STILL) { - stillCaptureLatency!!.captureLatency - pastTimeMs - } else { - stillCaptureLatency!!.processingLatency - pastTimeMs - } - - return if (remainingTimeMs > 0) remainingTimeMs else 0 - } - - /** - * Calculates the process progress for the latency of current state. - * - * The progress is calculated against the total latency duration of current state. - */ - private fun calculateProgressByRemainingDuration(): Int { - // Only supported when API level is 34 or above - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - return 0 - } - - // Only supported when capture latency info is provided - if (stillCaptureLatency == null) { - return 0 - } - - val currentTimestampMs = SystemClock.elapsedRealtime() - val pastTimeMs = currentTimestampMs - progressStartTimestampMs - return (if (progressState == PROGRESS_STATE_HOLD_STILL) { - pastTimeMs * 100 / stillCaptureLatency!!.captureLatency - } else { - pastTimeMs * 100 / stillCaptureLatency!!.processingLatency - }).toInt() - } - - /** - * Lens facing of the working camera - */ - private var lensFacing = CameraCharacteristics.LENS_FACING_BACK - - /** - * Gesture detector used for tap to focus - */ - private val tapToFocusListener = object : GestureDetector.SimpleOnGestureListener() { - override fun onSingleTapUp(event: MotionEvent): Boolean { - return tapToFocus(event) - } - } - - // Define a scale gesture detector to respond to pinch events and call - // setZoom on Camera.Parameters. - private val scaleGestureListener = object : ScaleGestureDetector.SimpleOnScaleGestureListener() { - override fun onScaleBegin(detector: ScaleGestureDetector): Boolean { - if (!hasZoomSupport()) { - return false - } - - // In case there is any focus happening, stop it. - cancelPendingAutoFocus() - return true - } - - override fun onScale(detector: ScaleGestureDetector): Boolean { - // Set the zoom level - startZoom(detector.scaleFactor) - return true - } - } - - /** - * Used to dispatch auto focus cancel after a timeout - */ - private val tapToFocusTimeoutHandler = Handler(Looper.getMainLooper()) - - /** - * Trivial capture callback implementation. - */ - private val captureCallbacks: CameraExtensionSession.ExtensionCaptureCallback = - object : CameraExtensionSession.ExtensionCaptureCallback() { - override fun onCaptureStarted( - session: CameraExtensionSession, request: CaptureRequest, - timestamp: Long - ) { - Log.v(TAG, "onCaptureStarted ts: $timestamp") - } - - override fun onCaptureProcessStarted( - session: CameraExtensionSession, - request: CaptureRequest - ) { - Log.v(TAG, "onCaptureProcessStarted") - // Turns to STILL_PROCESSING stage when the request tag is STILL_CAPTURE_TAG - if (request.tag == STILL_CAPTURE_TAG && progressState == PROGRESS_STATE_HOLD_STILL) { - progressState = PROGRESS_STATE_STILL_PROCESSING - progressStartTimestampMs = SystemClock.elapsedRealtime() - requireActivity().runOnUiThread { - binding.progressState?.text = getString(R.string.state_still_processing) - } - } - } - - override fun onCaptureResultAvailable( - session: CameraExtensionSession, - request: CaptureRequest, - result: TotalCaptureResult - ) { - Log.v(TAG, "onCaptureResultAvailable") - if (request.tag == AUTO_FOCUS_TAG) { - Log.v(TAG, "Auto focus region requested") - - // Consider listening for auto focus state such as auto focus locked - cameraExtensionSession.stopRepeating() - val autoFocusRegions = request.get(CaptureRequest.CONTROL_AF_REGIONS) - submitRequest( - CameraDevice.TEMPLATE_PREVIEW, - previewSurface, - true, - ) { builder -> - builder.apply { - set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO) - set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_IDLE) - set(CaptureRequest.CONTROL_AF_REGIONS, autoFocusRegions) - } - } - - queueAutoFocusReset() - } - - // The initial extension strength value will be provided by the capture results. Checks it - // to set and show the slider for end users to adjust their preferred strength setting. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && - isExtensionStrengthAvailable && extensionStrength == -1 && - result.keys.contains(CaptureResult.EXTENSION_STRENGTH) - ) { - result.get(CaptureResult.EXTENSION_STRENGTH)?.let { - extensionStrength = it - requireActivity().runOnUiThread { - binding.strengthSlider?.apply { - value = extensionStrength.toFloat() - visibility = View.VISIBLE - } - } - } - } - } - - override fun onCaptureFailed( - session: CameraExtensionSession, - request: CaptureRequest - ) { - Log.v(TAG, "onCaptureProcessFailed") - hideCaptureProgressUI() - } - - override fun onCaptureSequenceCompleted( - session: CameraExtensionSession, - sequenceId: Int - ) { - Log.v(TAG, "onCaptureProcessSequenceCompleted: $sequenceId") - } - - override fun onCaptureSequenceAborted( - session: CameraExtensionSession, - sequenceId: Int - ) { - Log.v(TAG, "onCaptureProcessSequenceAborted: $sequenceId") - hideCaptureProgressUI() - } - - override fun onCaptureProcessProgressed( - session: CameraExtensionSession, - request: CaptureRequest, - progress: Int, - ) { - // Caches current processing progress and updates the progress info - captureProcessingProgress = progress - updateProgressInfo() - } - } - - /** - * The slide OnChangeListener implementation for receiving the value change and submitting the - * request to change the extension strength setting. - */ - private val sliderOnChangeListener: Slider.OnChangeListener = - object : Slider.OnChangeListener { - override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) { - if (!fromUser || Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - return - } - - extensionStrength = value.toInt() - submitRequest( - CameraDevice.TEMPLATE_PREVIEW, - previewSurface, - true, - ) { builder -> - builder.apply { - set( - CaptureRequest.EXTENSION_STRENGTH, - extensionStrength - ) - Log.d(TAG, "submit request for extension strength: $extensionStrength") - } - } - } - } - - /** - * A list of supported extensions - */ - private val supportedExtensions = ArrayList() - - override fun onSurfaceTextureAvailable( - surfaceTexture: SurfaceTexture, - width: Int, height: Int - ) { - initializeCamera() - } - - override fun onSurfaceTextureSizeChanged( - surfaceTexture: SurfaceTexture, - width: Int, height: Int - ) {} - - override fun onSurfaceTextureDestroyed(surfaceTexture: SurfaceTexture): Boolean { - return true - } - - override fun onSurfaceTextureUpdated(surfaceTexture: SurfaceTexture) {} - - /** [HandlerThread] where all image store operations run */ - private val storeThread = HandlerThread("StoreThread").apply { start() } - - /** [Handler] corresponding to [storeThread] */ - private val storeHandler = Handler(storeThread.looper) - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentCameraBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - @SuppressLint("MissingPermission", "ClickableViewAccessibility") - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding.texture.surfaceTextureListener = this - - val tapToFocusGestureDetector = GestureDetector(requireContext(), tapToFocusListener) - val scaleGestureDetector = ScaleGestureDetector(requireContext(), scaleGestureListener) - binding.texture.setOnTouchListener { _, event -> - tapToFocusGestureDetector.onTouchEvent(event) - scaleGestureDetector.onTouchEvent(event) - true - } - - extensionCharacteristics = cameraManager.getCameraExtensionCharacteristics(args.cameraId) - characteristics = cameraManager.getCameraCharacteristics(args.cameraId) - lensFacing = characteristics[CameraCharacteristics.LENS_FACING]!! - - if (args.jpegR) { - for (extension in extensionCharacteristics.supportedExtensions) { - val jpegRSizes = extensionCharacteristics.getExtensionSupportedSizes( - extension, ImageFormat.JPEG_R - ) - - if (jpegRSizes.isNotEmpty()) { - supportedExtensions.add(extension) - } - } - } else { - supportedExtensions.addAll(extensionCharacteristics.supportedExtensions) - } - - if (currentExtension == -1) { - currentExtension = supportedExtensions[0] - currentExtensionIdx = 0 - refreshStrengthAndCaptureProgressAvailabilityInfo() - binding.strengthSlider?.visibility = View.GONE - extensionStrength = -1 - binding.switchButton.text = getExtensionLabel(currentExtension) - } - - binding.switchButton.setOnClickListener { v -> - if (v.id == R.id.switch_button) { - lifecycleScope.launch(Dispatchers.IO) { - currentExtensionIdx = (currentExtensionIdx + 1) % supportedExtensions.size - currentExtension = supportedExtensions[currentExtensionIdx] - refreshStrengthAndCaptureProgressAvailabilityInfo() - extensionStrength = -1 - requireActivity().runOnUiThread { - binding.strengthSlider?.visibility = View.GONE - binding.switchButton.text = getExtensionLabel(currentExtension) - restartPreview = true - } - try { - cameraExtensionSession.stopRepeating() - cameraExtensionSession.close() - } catch (e: Exception) { - Log.e(TAG, "Camera failure when closing camera extension") - } - } - } - } - // React to user touching the capture button - binding.captureButton.setOnTouchListener { _, event -> - when (event.action) { - MotionEvent.ACTION_DOWN -> { - lifecycleScope.launch(Dispatchers.IO) { - clearPendingAutoFocusReset() - takePicture() - } - } - } - - true - } - - // Sets strength slider change listener - binding.strengthSlider?.addOnChangeListener(sliderOnChangeListener) - } - - /** - * Refreshes the extension strength and capture progress related availability info. - */ - private fun refreshStrengthAndCaptureProgressAvailabilityInfo() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - isExtensionStrengthAvailable = - extensionCharacteristics.getAvailableCaptureRequestKeys(currentExtension) - .contains(CaptureRequest.EXTENSION_STRENGTH) - isPostviewAvailable = extensionCharacteristics.isPostviewAvailable(currentExtension) - isCaptureProcessProgressAvailable = - extensionCharacteristics.isCaptureProcessProgressAvailable(currentExtension) - } - } - - /** - * Begin all camera operations in a coroutine. This function: - * - Opens the camera - * - Configures the camera extension session - * - Starts the preview by dispatching a repeating request - */ - private fun initializeCamera() = lifecycleScope.launch(Dispatchers.IO) { - // Open the selected camera - cameraDevice = openCamera(cameraManager, args.cameraId) - - startPreview() - } - - /** 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, - ): CameraDevice = suspendCancellableCoroutine { cont -> - manager.openCamera(cameraId, Dispatchers.IO.asExecutor(), object : CameraDevice.StateCallback() { - override fun onOpened(device: CameraDevice) = cont.resume(device) - - override fun onDisconnected(device: CameraDevice) { - Log.w(TAG, "Camera $cameraId has been disconnected") - requireActivity().finish() - } - - override fun onError(device: CameraDevice, error: Int) { - val msg = when (error) { - ERROR_CAMERA_DEVICE -> "Fatal (device)" - ERROR_CAMERA_DISABLED -> "Device policy" - ERROR_CAMERA_IN_USE -> "Camera in use" - ERROR_CAMERA_SERVICE -> "Fatal (service)" - ERROR_MAX_CAMERAS_IN_USE -> "Maximum cameras in use" - else -> "Unknown" - } - val exc = RuntimeException("Camera $cameraId error: ($error) $msg") - Log.e(TAG, exc.message, exc) - if (cont.isActive) cont.resumeWithException(exc) - } - }) - } - - /** - * Pick a preview resolution that is both close/same as the display size and supported by camera - * and extensions. - */ - @Throws(CameraAccessException::class) - private fun pickPreviewResolution(manager: CameraManager, cameraId: String) : Size { - val characteristics = manager.getCameraCharacteristics(cameraId) - val map = characteristics.get( - CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP - ) - val textureSizes = map!!.getOutputSizes( - SurfaceTexture::class.java - ) - val displaySize = Point() - val displayMetrics = requireActivity().resources.displayMetrics - displaySize.x = displayMetrics.widthPixels - displaySize.y = displayMetrics.heightPixels - if (displaySize.x < displaySize.y) { - displaySize.x = displayMetrics.heightPixels - displaySize.y = displayMetrics.widthPixels - } - val displayArRatio = displaySize.x.toFloat() / displaySize.y - val previewSizes = ArrayList() - for (sz in textureSizes) { - val arRatio = sz.width.toFloat() / sz.height - if (abs(arRatio - displayArRatio) <= .2f) { - previewSizes.add(sz) - } - } - val extensionSizes = extensionCharacteristics.getExtensionSupportedSizes( - currentExtension, SurfaceTexture::class.java - ) - if (extensionSizes.isEmpty()) { - Toast.makeText( - requireActivity(), "Invalid preview extension sizes!.", - Toast.LENGTH_SHORT - ).show() - requireActivity().finish() - } - - var previewSize = extensionSizes[0] - val supportedPreviewSizes = - previewSizes.stream().distinct().filter { o: Size -> extensionSizes.contains(o) } - .collect(Collectors.toList()) - if (supportedPreviewSizes.isNotEmpty()) { - var currentDistance = Int.MAX_VALUE - for (sz in supportedPreviewSizes) { - val distance = abs(sz.width * sz.height - displaySize.x * displaySize.y) - if (currentDistance > distance) { - currentDistance = distance - previewSize = sz - } - } - } else { - Log.w( - TAG, "No overlap between supported camera and extensions preview sizes using " - + "first available!" - ) - } - - return previewSize - } - - /** - * Starts the camera preview. - */ - @Synchronized - private fun startPreview() { - if (!binding.texture.isAvailable) { - return - } - - previewSize = pickPreviewResolution(cameraManager, args.cameraId) - previewSurface = createPreviewSurface(previewSize) - stillImageReader = createStillImageReader() - postviewImageReader = createPostviewImageReader() - isPostviewAvailable = postviewImageReader != null - - val outputConfig = ArrayList() - outputConfig.add(OutputConfiguration(stillImageReader.surface)) - outputConfig.add(OutputConfiguration(previewSurface)) - val extensionConfiguration = ExtensionSessionConfiguration( - currentExtension, outputConfig, - Dispatchers.IO.asExecutor(), object : CameraExtensionSession.StateCallback() { - override fun onClosed(session: CameraExtensionSession) { - if (restartPreview) { - stillImageReader.close() - postviewImageReader?.close() - restartPreview = false - startPreview() - } else { - cameraDevice.close() - } - } - - override fun onConfigured(session: CameraExtensionSession) { - cameraExtensionSession = session - submitRequest( - CameraDevice.TEMPLATE_PREVIEW, - previewSurface, - true - ) { request -> - request.apply { - set(CaptureRequest.CONTROL_ZOOM_RATIO, zoomRatio) - } - } - } - - override fun onConfigureFailed(session: CameraExtensionSession) { - Toast.makeText( - requireActivity(), - "Failed to start camera extension preview.", - Toast.LENGTH_SHORT - ).show() - requireActivity().finish() - } - } - ) - // Adds postview image reader surface to extension session configuration if it is supported. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - postviewImageReader?.let { - extensionConfiguration.postviewOutputConfiguration = OutputConfiguration(it.surface) - } - } - try { - cameraDevice.createExtensionSession(extensionConfiguration) - } catch (e: CameraAccessException) { - Toast.makeText( - requireActivity(), "Failed during extension initialization!.", - Toast.LENGTH_SHORT - ).show() - requireActivity().finish() - } - } - - /** - * Creates the preview surface - */ - private fun createPreviewSurface(previewSize: Size): Surface { - val texture = binding.texture.surfaceTexture - texture?.setDefaultBufferSize(previewSize.width, previewSize.height) - return Surface(texture) - } - - /** - * Creates the still image reader and sets up OnImageAvailableListener - */ - private fun createStillImageReader(): ImageReader { - var stillFormat: Int - var stillCaptureSize: Size - - val yuvColorEncodingSystemSizes = extensionCharacteristics.getExtensionSupportedSizes( - currentExtension, ImageFormat.YUV_420_888 - ) - val jpegSizes = extensionCharacteristics.getExtensionSupportedSizes( - currentExtension, ImageFormat.JPEG - ) - stillFormat = if (jpegSizes.isEmpty()) ImageFormat.YUV_420_888 else ImageFormat.JPEG - stillCaptureSize = if (jpegSizes.isEmpty()) yuvColorEncodingSystemSizes[0] else jpegSizes[0] - - if (Build.VERSION.SDK_INT >= 35) { - val jpegRSizes = extensionCharacteristics.getExtensionSupportedSizes( - currentExtension, ImageFormat.JPEG_R - ) - if (args.jpegR && jpegRSizes.isNotEmpty()) { - stillFormat = ImageFormat.JPEG_R - stillCaptureSize = jpegRSizes[0] - } - } - val stillImageReader = ImageReader.newInstance( - stillCaptureSize.width, - stillCaptureSize.height, stillFormat, 1 - ) - stillImageReader.setOnImageAvailableListener( - { reader: ImageReader -> - var output: OutputStream - try { - reader.acquireLatestImage().use { image -> - hideCaptureProgressUI() - val file = File( - requireActivity().getExternalFilesDir(null), - if (image.format == ImageFormat.JPEG - || image.format == ImageFormat.JPEG_R) "frame.jpg" else "frame.yuv" - ) - output = FileOutputStream(file) - output.write(getDataFromImage(image)) - output.close() - Toast.makeText( - requireActivity(), "Frame saved at: " + file.path, - Toast.LENGTH_SHORT - ).show() - } - } catch (e: Exception) { - e.printStackTrace() - } - }, storeHandler - ) - return stillImageReader - } - - /** - * Creates postview image reader and sets up OnImageAvailableListener if current extension mode - * supports postview. - */ - private fun createPostviewImageReader(): ImageReader? { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE || !isPostviewAvailable) { - return null - } - - val jpegSupportedSizes = extensionCharacteristics.getPostviewSupportedSizes( - currentExtension, - Size( - stillImageReader.width, - stillImageReader.height - ), - ImageFormat.JPEG - ) - val yuvSupportedSizes = extensionCharacteristics.getPostviewSupportedSizes( - currentExtension, - Size( - stillImageReader.width, - stillImageReader.height - ), - ImageFormat.YUV_420_888 - ) - val postviewSize: Size - val postviewFormat: Int - if (jpegSupportedSizes.isNotEmpty()) { - postviewSize = jpegSupportedSizes[0] - postviewFormat = ImageFormat.JPEG - } else if (yuvSupportedSizes.isNotEmpty()){ - postviewSize = yuvSupportedSizes[0] - postviewFormat = ImageFormat.YUV_420_888 - } else { - return null - } - val postviewImageReader = - ImageReader.newInstance(postviewSize.width, postviewSize.height, postviewFormat, 1) - postviewImageReader.setOnImageAvailableListener( - { reader: ImageReader -> - try { - reader.acquireLatestImage().use { image -> - drawPostviewImage(image) - } - } catch (e: Exception) { - e.printStackTrace() - } - }, storeHandler - ) - - return postviewImageReader - } - - /** - * Draw postview image to the capture progress UI - */ - private fun drawPostviewImage(image: Image) { - createBitmapFromImage(image)?.let { bitmap -> - requireActivity().runOnUiThread { - binding.progressInfoImage?.apply { - // The following settings are for correctly displaying the postview image in portrait - // orientation which Camera2Extensions sample app currently supports for. - if (lensFacing == CameraCharacteristics.LENS_FACING_BACK) { - rotation = 90.0f - scaleY = 1.0f - } else { - rotation = 270.0f - scaleY = -1.0f - } - setImageBitmap(bitmap) - visibility = View.VISIBLE - } - } - } - } - - private fun createBitmapFromImage(image: Image): Bitmap? { - Log.d(TAG, "createBitmapFromImage from image of format ${image.format}") - when (image.format) { - ImageFormat.JPEG -> { - val data = getDataFromImage(image) - return BitmapFactory.decodeByteArray(data, 0, data.size, null) - } - - ImageFormat.YUV_420_888 -> { - val yuvToRgbConverter = YuvToRgbConverter(requireContext()) - val bitmap = Bitmap.createBitmap(image.width, image.height, Bitmap.Config.ARGB_8888) - yuvToRgbConverter.yuvToRgb(image, bitmap) - return bitmap - } - } - - return null - } - - /** - * Takes a picture. - */ - private fun takePicture() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - // Retrieves the still capture latency info - cameraExtensionSession.realtimeStillCaptureLatency?.let { - stillCaptureLatency = it - progressStartTimestampMs = SystemClock.elapsedRealtime() - } - captureProcessingProgress = -1 - showCaptureProgressUI() - } - submitRequest( - CameraDevice.TEMPLATE_STILL_CAPTURE, - if (isPostviewAvailable) { - listOf(stillImageReader.surface, postviewImageReader!!.surface) - } else { - listOf(stillImageReader.surface) - }, - false - ) { request -> - request.apply { - set(CaptureRequest.CONTROL_ZOOM_RATIO, zoomRatio) - setTag(STILL_CAPTURE_TAG) - } - } - } - - /** - * Shows the UI for capture progress info - */ - private fun showCaptureProgressUI() { - // Do not show the UI if none of still capture latency, process progress or postview is - // supported. - if (stillCaptureLatency == null && !isCaptureProcessProgressAvailable && !isPostviewAvailable) { - return - } - - progressState = PROGRESS_STATE_HOLD_STILL - requireActivity().runOnUiThread { - enableUiControls(false) - binding.progressInfoContainer?.visibility = View.VISIBLE - binding.progressState?.text = getString(R.string.state_hold_still) - binding.progressIndicator?.isIndeterminate = stillCaptureLatency == null - } - // Schedules to execute a runnable repeatedly to update the progress info - if (stillCaptureLatency != null) { - progressInfoScheduledFuture = Executors.newSingleThreadScheduledExecutor() - .scheduleAtFixedRate( - { updateProgressInfo() }, 0, 100, TimeUnit.MILLISECONDS - ) - } - } - - private fun enableUiControls(isEnabled: Boolean) { - requireActivity().runOnUiThread { - binding.switchButton.isEnabled = isEnabled - binding.captureButton.isEnabled = isEnabled - binding.strengthSlider?.isEnabled = isEnabled - } - } - - /** - * Updates the capture progress info - */ - private fun updateProgressInfo() { - requireActivity().runOnUiThread { - binding.progressState?.text = when (progressState) { - PROGRESS_STATE_HOLD_STILL -> resources.getString(R.string.state_hold_still) - PROGRESS_STATE_STILL_PROCESSING -> resources.getString(R.string.state_still_processing) - else -> "" - } - - binding.progressIndicator?.isIndeterminate = false - - if (progressState == PROGRESS_STATE_STILL_PROCESSING && isCaptureProcessProgressAvailable) { - binding.progressIndicator?.progress = captureProcessingProgress - - if (captureProcessingProgress == 100) { - hideCaptureProgressUI() - } - } - - stillCaptureLatency?.let { - val remainingDurationMs = calculateRemainingDurationInMs() - binding.progressLatencyDuration?.text = - resources.getString(R.string.latency_duration, (remainingDurationMs + 500) / 1000) - // Updates the progress indicator according to the remaining duration of latency time if - // capture process progress is not supported. - if (progressState == PROGRESS_STATE_HOLD_STILL || !isCaptureProcessProgressAvailable) { - binding.progressIndicator?.progress = calculateProgressByRemainingDuration() - } - // Automatically turns to still-processing state if capture process progress is not - // supported - if (remainingDurationMs.toInt() == 0 && progressState == PROGRESS_STATE_HOLD_STILL) { - progressState = PROGRESS_STATE_STILL_PROCESSING - progressStartTimestampMs = SystemClock.elapsedRealtime() - updateProgressInfo() - } - } - } - } - - /** - * Hides the UI for capture progress info - */ - private fun hideCaptureProgressUI() { - progressState = PROGRESS_STATE_DONE - requireActivity().runOnUiThread { - binding.progressInfoContainer?.apply { - visibility = View.GONE - binding.progressInfoImage?.visibility = View.GONE - binding.progressLatencyDuration?.text = "" - } - enableUiControls(true) - } - progressInfoScheduledFuture?.apply { - cancel(true) - progressInfoScheduledFuture = null - } - } - - private fun submitRequest( - templateType: Int, - target: Surface, - isRepeating: Boolean, - block: (captureRequest: CaptureRequest.Builder) -> CaptureRequest.Builder) { - return submitRequest(templateType, listOf(target), isRepeating, block) - } - - private fun submitRequest( - templateType: Int, - targets: List, - isRepeating: Boolean, - block: (captureRequest: CaptureRequest.Builder) -> CaptureRequest.Builder) { - try { - val captureBuilder = cameraDevice.createCaptureRequest(templateType) - .apply { - targets.forEach { - addTarget(it) - } - if (tag != null) { - setTag(tag) - } - block(this) - } - if (isRepeating) { - cameraExtensionSession.setRepeatingRequest( - captureBuilder.build(), - Dispatchers.IO.asExecutor(), - captureCallbacks - ) - } else { - cameraExtensionSession.capture( - captureBuilder.build(), - Dispatchers.IO.asExecutor(), - captureCallbacks - ) - } - } catch (e: CameraAccessException) { - Toast.makeText( - requireActivity(), "Camera failed to submit capture request!.", - Toast.LENGTH_SHORT - ).show() - } - } - - override fun onStop() { - super.onStop() - try { - clearPendingAutoFocusReset() - cameraDevice.close() - } catch (exc: Throwable) { - Log.e(TAG, "Error closing camera", exc) - } - } - - override fun onDestroy() { - super.onDestroy() - storeThread.quitSafely() - } - - override fun onResume() { - super.onResume() - if (binding.texture.isAvailable) { - initializeCamera() - } - } - - /** - * Removes any pending operation to restart auto focus in continuous picture mode. - */ - private fun clearPendingAutoFocusReset() { - tapToFocusTimeoutHandler.removeCallbacksAndMessages(null) - } - - /** - * Queue operation to restart auto focus in continuous picture mode. - */ - private fun queueAutoFocusReset() { - tapToFocusTimeoutHandler.postDelayed({ - Log.v(TAG, "Reset auto focus back to continuous picture") - cameraExtensionSession.stopRepeating() - - submitRequest( - CameraDevice.TEMPLATE_PREVIEW, - previewSurface, - true, - ) { builder -> - builder.apply { - set( - CaptureRequest.CONTROL_AF_MODE, - CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE - ) - } - } - }, AUTO_FOCUS_TIMEOUT_MILLIS) - } - - /** - * Handles the tap to focus event. - * This will cancel any existing focus operation and restart it at the new point. - * Note: If the device doesn't support auto focus then this operation will abort and return - * false. - */ - private fun tapToFocus(event: MotionEvent): Boolean { - if (!hasAutoFocusMeteringSupport()) { - return false - } - - // Reset zoom -- TODO(Support zoom and tap to focus) - zoomRatio = ZoomUtil.minZoom() - - cameraExtensionSession.stopRepeating() - cancelPendingAutoFocus() - startAutoFocus(meteringRectangle(event)) - - return true - } - - /** - * Not all camera extensions have auto focus metering support. - * Returns true if auto focus metering is supported otherwise false. - */ - private fun hasAutoFocusMeteringSupport(): Boolean { - if (characteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF) == 0) { - return false - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - val availableExtensionRequestKeys = - extensionCharacteristics.getAvailableCaptureRequestKeys(currentExtension) - return availableExtensionRequestKeys.contains(CaptureRequest.CONTROL_AF_TRIGGER) && - availableExtensionRequestKeys.contains(CaptureRequest.CONTROL_AF_MODE) && - availableExtensionRequestKeys.contains(CaptureRequest.CONTROL_AF_REGIONS) - } - - return false - } - - /** - * Not all camera extensions have zoom support. - * Returns true if zoom is supported otherwise false. - */ - private fun hasZoomSupport(): Boolean { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - val availableExtensionRequestKeys = - extensionCharacteristics.getAvailableCaptureRequestKeys(currentExtension) - return availableExtensionRequestKeys.contains(CaptureRequest.CONTROL_ZOOM_RATIO) - } - - return false - } - - - /** - * Translates a touch event relative to the preview surface to a region relative to the sensor. - * Note: This operation does not account for zoom / crop and should be handled otherwise the touch - * point won't correctly map to the sensor. - */ - private fun meteringRectangle(event: MotionEvent): MeteringRectangle { - val sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!! - val sensorSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)!! - - val halfMeteringRectWidth = (METERING_RECTANGLE_SIZE * sensorSize.width()) / 2 - val halfMeteringRectHeight = (METERING_RECTANGLE_SIZE * sensorSize.height()) / 2 - - // Normalize the [x,y] touch point in the view port to values in the range of [0,1] - val normalizedPoint = floatArrayOf(event.x / previewSize.height, event.y / previewSize.width) - - // Scale and rotate the normalized point such that it maps to the sensor region - Matrix().apply { - postRotate(-sensorOrientation.toFloat(), 0.5f, 0.5f) - postScale(sensorSize.width().toFloat(), sensorSize.height().toFloat()) - mapPoints(normalizedPoint) - } - - val meteringRegion = Rect( - (normalizedPoint[0] - halfMeteringRectWidth).toInt().coerceIn(0, sensorSize.width()), - (normalizedPoint[1] - halfMeteringRectHeight).toInt().coerceIn(0, sensorSize.height()), - (normalizedPoint[0] + halfMeteringRectWidth).toInt().coerceIn(0, sensorSize.width()), - (normalizedPoint[1] + halfMeteringRectHeight).toInt().coerceIn(0, sensorSize.height()) - ) - - return MeteringRectangle(meteringRegion, MeteringRectangle.METERING_WEIGHT_MAX) - } - - private fun startAutoFocus(meteringRectangle: MeteringRectangle) { - submitRequest( - CameraDevice.TEMPLATE_PREVIEW, - previewSurface, - false, - ) { request -> - request.apply { - set(CaptureRequest.CONTROL_AF_REGIONS, arrayOf(meteringRectangle)) - set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO) - set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_START) - setTag(AUTO_FOCUS_TAG) - } - } - } - - private fun cancelPendingAutoFocus() { - clearPendingAutoFocusReset() - submitRequest( - CameraDevice.TEMPLATE_PREVIEW, - previewSurface, - true - ) { request -> - request.apply { - set(CaptureRequest.CONTROL_ZOOM_RATIO, zoomRatio) - set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO) - set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_START) - } - } - } - - private fun startZoom(scaleFactor: Float) { - zoomRatio = - (zoomRatio * scaleFactor).coerceIn(ZoomUtil.minZoom(), ZoomUtil.maxZoom(characteristics)) - Log.d(TAG, "onScale: $zoomRatio") - submitRequest( - CameraDevice.TEMPLATE_PREVIEW, - previewSurface, - true - ) { request -> - request.apply { - set(CaptureRequest.CONTROL_ZOOM_RATIO, zoomRatio) - } - } - } - - companion object { - private val TAG = CameraFragment::class.java.simpleName - private const val STILL_CAPTURE_TAG = "still_capture_tag" - private const val AUTO_FOCUS_TAG = "auto_focus_tag" - private const val AUTO_FOCUS_TIMEOUT_MILLIS = 5_000L - private const val METERING_RECTANGLE_SIZE = 0.15f - - private fun getExtensionLabel(extension: Int): String { - return when (extension) { - CameraExtensionCharacteristics.EXTENSION_HDR -> "HDR" - CameraExtensionCharacteristics.EXTENSION_NIGHT -> "NIGHT" - CameraExtensionCharacteristics.EXTENSION_BOKEH -> "BOKEH" - CameraExtensionCharacteristics.EXTENSION_FACE_RETOUCH-> "FACE RETOUCH" - else -> "AUTO" - } - } - - private fun getDataFromImage(image: Image): ByteArray { - val format = image.format - val width = image.width - val height = image.height - var rowStride: Int - var pixelStride: Int - val data: ByteArray - - // Read image data - val planes = image.planes - var buffer: ByteBuffer - var offset = 0 - if (format == ImageFormat.JPEG || format == ImageFormat.JPEG_R) { - buffer = planes[0].buffer - data = ByteArray(buffer.limit()) - buffer.rewind() - buffer[data] - return data - } - data = ByteArray(width * height * ImageFormat.getBitsPerPixel(format) / 8) - var maxRowSize = planes[0].rowStride - for (plane in planes) { - if (maxRowSize < plane.rowStride) { - maxRowSize = plane.rowStride - } - } - val rowData = ByteArray(maxRowSize) - for (i in planes.indices) { - buffer = planes[i].buffer - rowStride = planes[i].rowStride - pixelStride = planes[i].pixelStride - // For multi-planar yuv images, assuming yuv420 with 2x2 chroma subsampling. - val w = if (i == 0) width else width / 2 - val h = if (i == 0) height else height / 2 - for (row in 0 until h) { - val bytesPerPixel = ImageFormat.getBitsPerPixel(format) / 8 - var length: Int - if (pixelStride == bytesPerPixel) { - // Special case: optimized read of the entire row - length = w * bytesPerPixel - buffer[data, offset, length] - offset += length - } else { - // Generic case: should work for any pixelStride but slower. - // Use intermediate buffer to avoid read byte-by-byte from - // DirectByteBuffer, which is very bad for performance - length = (w - 1) * pixelStride + bytesPerPixel - buffer[rowData, 0, length] - for (col in 0 until w) { - data[offset++] = rowData[col * pixelStride] - } - } - // Advance buffer the remainder of the row stride - if (row < h - 1) { - buffer.position(buffer.position() + rowStride - length) - } - } - buffer.rewind() - } - return data - } - } -} diff --git a/Camera2Extensions/app/src/main/java/com/example/android/camera2/extensions/fragments/SelectorFragment.kt b/Camera2Extensions/app/src/main/java/com/example/android/camera2/extensions/fragments/SelectorFragment.kt deleted file mode 100644 index 61c45b25..00000000 --- a/Camera2Extensions/app/src/main/java/com/example/android/camera2/extensions/fragments/SelectorFragment.kt +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * 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 com.example.android.camera2.extensions.fragments - -import android.annotation.SuppressLint -import android.content.Context -import android.graphics.ImageFormat; -import android.hardware.camera2.CameraCharacteristics -import android.hardware.camera2.CameraManager -import android.os.Build -import android.os.Bundle -import android.view.LayoutInflater -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.camera2.extensions.GenericListAdapter -import com.example.android.camera2.extensions.R - -/** - * In this [Fragment] we let users pick a camera - */ -class SelectorFragment : Fragment() { - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View = RecyclerView(requireContext()) - - @SuppressLint("MissingPermission") - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - view as RecyclerView - view.apply { - layoutManager = LinearLayoutManager(requireContext()) - - val cameraManager = - requireContext().getSystemService(Context.CAMERA_SERVICE) as CameraManager - - val cameraList = enumerateExtensionCameras(cameraManager) - - val layoutId = android.R.layout.simple_list_item_1 - adapter = GenericListAdapter(cameraList, itemLayoutId = layoutId) { view, item, _ -> - view.findViewById(android.R.id.text1).text = item.name - view.setOnClickListener { - Navigation.findNavController(requireActivity(), R.id.fragment_container) - .navigate(SelectorFragmentDirections.actionSelectorToCamera( - item.cameraId, item.jpegR)) - } - } - } - } - - companion object { - - private data class CameraInfo( - val name: String, - val cameraId: String, - val jpegR: Boolean = false) - - /** Converts a lens orientation enum into a human-readable string */ - private fun lensOrientationString(value: Int) = when (value) { - CameraCharacteristics.LENS_FACING_BACK -> "Back" - CameraCharacteristics.LENS_FACING_FRONT -> "Front" - CameraCharacteristics.LENS_FACING_EXTERNAL -> "External" - else -> "Unknown" - } - - /** Lists all extension-capable cameras*/ - @SuppressLint("InlinedApi") - private fun enumerateExtensionCameras(cameraManager: CameraManager): List { - val availableCameras: MutableList = mutableListOf() - - cameraManager.cameraIdList.forEach { id -> - val characteristics = cameraManager.getCameraCharacteristics(id) - val orientation = lensOrientationString( - characteristics.get(CameraCharacteristics.LENS_FACING)!!) - val extensionCharacteristics = cameraManager.getCameraExtensionCharacteristics(id) - val capabilities = characteristics.get( - CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)!! - - // Return cameras that declare to be backward compatible - if (capabilities.contains(CameraCharacteristics - .REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE) && - extensionCharacteristics.supportedExtensions.isNotEmpty()) { - availableCameras.add(CameraInfo("$orientation ($id)", id)) - - if (Build.VERSION.SDK_INT >= 35) { - for (extension in extensionCharacteristics.supportedExtensions) { - val jpegRSizes = extensionCharacteristics.getExtensionSupportedSizes( - extension, ImageFormat.JPEG_R - ) - - if (jpegRSizes.isNotEmpty()) { - availableCameras.add(CameraInfo("$orientation ($id) JPEG_R", id, true)) - break // Exit the loop since we found a suitable extension - } - } - } - } - } - return availableCameras - } - } -} diff --git a/Camera2Extensions/app/src/main/res/drawable-hdpi/ic_launcher.png b/Camera2Extensions/app/src/main/res/drawable-hdpi/ic_launcher.png deleted file mode 100644 index bba1165c..00000000 Binary files a/Camera2Extensions/app/src/main/res/drawable-hdpi/ic_launcher.png and /dev/null differ diff --git a/Camera2Extensions/app/src/main/res/drawable-mdpi/ic_launcher.png b/Camera2Extensions/app/src/main/res/drawable-mdpi/ic_launcher.png deleted file mode 100644 index 43045910..00000000 Binary files a/Camera2Extensions/app/src/main/res/drawable-mdpi/ic_launcher.png and /dev/null differ diff --git a/Camera2Extensions/app/src/main/res/drawable-xhdpi/ic_launcher.png b/Camera2Extensions/app/src/main/res/drawable-xhdpi/ic_launcher.png deleted file mode 100644 index 80c5ebaa..00000000 Binary files a/Camera2Extensions/app/src/main/res/drawable-xhdpi/ic_launcher.png and /dev/null differ diff --git a/Camera2Extensions/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/Camera2Extensions/app/src/main/res/drawable-xxhdpi/ic_launcher.png deleted file mode 100644 index 9baac9b6..00000000 Binary files a/Camera2Extensions/app/src/main/res/drawable-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/Camera2Extensions/app/src/main/res/drawable/ic_progress_info_bg.xml b/Camera2Extensions/app/src/main/res/drawable/ic_progress_info_bg.xml deleted file mode 100644 index 35296f0a..00000000 --- a/Camera2Extensions/app/src/main/res/drawable/ic_progress_info_bg.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/Camera2Extensions/app/src/main/res/drawable/ic_shutter.xml b/Camera2Extensions/app/src/main/res/drawable/ic_shutter.xml deleted file mode 100644 index e4df1cfe..00000000 --- a/Camera2Extensions/app/src/main/res/drawable/ic_shutter.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/Camera2Extensions/app/src/main/res/drawable/ic_shutter_focused.xml b/Camera2Extensions/app/src/main/res/drawable/ic_shutter_focused.xml deleted file mode 100644 index fb6f92de..00000000 --- a/Camera2Extensions/app/src/main/res/drawable/ic_shutter_focused.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - diff --git a/Camera2Extensions/app/src/main/res/drawable/ic_shutter_normal.xml b/Camera2Extensions/app/src/main/res/drawable/ic_shutter_normal.xml deleted file mode 100644 index 7c7efd5b..00000000 --- a/Camera2Extensions/app/src/main/res/drawable/ic_shutter_normal.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - diff --git a/Camera2Extensions/app/src/main/res/drawable/ic_shutter_pressed.xml b/Camera2Extensions/app/src/main/res/drawable/ic_shutter_pressed.xml deleted file mode 100644 index fb6f92de..00000000 --- a/Camera2Extensions/app/src/main/res/drawable/ic_shutter_pressed.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - diff --git a/Camera2Extensions/app/src/main/res/layout-land/fragment_camera.xml b/Camera2Extensions/app/src/main/res/layout-land/fragment_camera.xml deleted file mode 100644 index fca390cc..00000000 --- a/Camera2Extensions/app/src/main/res/layout-land/fragment_camera.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - -