inital commit
This commit is contained in:
89
app/build.gradle
Normal file
89
app/build.gradle
Normal file
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: "androidx.navigation.safeargs"
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
defaultConfig {
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
applicationId "de.weseng.camera.basic"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode 1
|
||||
versionName "1.0.0"
|
||||
}
|
||||
|
||||
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.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':utils')
|
||||
|
||||
// Kotlin lang
|
||||
implementation 'androidx.core:core-ktx:1.2.0'
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.4'
|
||||
|
||||
// App compat and UI things
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
|
||||
implementation "androidx.viewpager2:viewpager2:1.0.0"
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
|
||||
// Navigation library
|
||||
def nav_version = '2.2.2'
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
|
||||
|
||||
// EXIF Interface
|
||||
implementation 'androidx.exifinterface:exifinterface:1.2.0'
|
||||
|
||||
// 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.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'
|
||||
}
|
||||
49
app/src/main/AndroidManifest.xml
Normal file
49
app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="de.weseng.camera.basic">
|
||||
|
||||
<!-- Permission declarations -->
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
<!-- A camera with (optional) RAW capability is required to use this application -->
|
||||
<uses-feature android:name="android.hardware.camera.any" />
|
||||
<uses-feature android:name="android.hardware.camera.raw" android:required="false" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="true"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
|
||||
<activity
|
||||
android:name="de.weseng.camera.basic.CameraActivity"
|
||||
android:clearTaskOnLaunch="true"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<!-- Main app intent filter -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
58
app/src/main/java/de/weseng/camera/basic/CameraActivity.kt
Normal file
58
app/src/main/java/de/weseng/camera/basic/CameraActivity.kt
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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 de.weseng.camera.basic
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageButton
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
||||
class CameraActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var container: FrameLayout
|
||||
private lateinit var captureButton: ImageButton
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_camera)
|
||||
container = findViewById(R.id.fragment_container)
|
||||
}
|
||||
|
||||
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
|
||||
container.postDelayed({
|
||||
container.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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,487 @@
|
||||
/*
|
||||
* 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 de.weseng.camera.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 de.weseng.camera.utils.computeExifOrientation
|
||||
import de.weseng.camera.utils.getPreviewOutputSize
|
||||
import de.weseng.camera.utils.AutoFitSurfaceView
|
||||
import de.weseng.camera.utils.OrientationLiveData
|
||||
import de.weseng.camera.basic.R
|
||||
import de.weseng.camera.basic.CameraActivity
|
||||
import kotlinx.android.synthetic.main.fragment_camera.*
|
||||
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() {
|
||||
|
||||
/** 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
|
||||
overlay.background = Color.argb(150, 255, 255, 255).toDrawable()
|
||||
// Wait for ANIMATION_FAST_MILLIS
|
||||
overlay.postDelayed({
|
||||
// Remove white flash animation
|
||||
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)
|
||||
|
||||
/** Where the camera preview is displayed */
|
||||
private lateinit var viewFinder: AutoFitSurfaceView
|
||||
|
||||
/** Overlay on top of the camera preview */
|
||||
private lateinit var overlay: View
|
||||
|
||||
/** 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? = inflater.inflate(R.layout.fragment_camera, container, false)
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
overlay = view.findViewById(R.id.overlay)
|
||||
viewFinder = view.findViewById(R.id.view_finder)
|
||||
capture_button.setOnApplyWindowInsetsListener { v, insets ->
|
||||
v.translationX = (-insets.systemWindowInsetRight).toFloat()
|
||||
v.translationY = (-insets.systemWindowInsetBottom).toFloat()
|
||||
insets.consumeSystemWindowInsets()
|
||||
}
|
||||
capture_button.setVisibility(View.GONE)
|
||||
//captureButton = findViewById(R.id.capture_button)
|
||||
//captureButton.setVisibility(View.GONE))
|
||||
|
||||
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(
|
||||
viewFinder.display, characteristics, SurfaceHolder::class.java)
|
||||
Log.d(TAG, "View finder size: ${viewFinder.width} x ${viewFinder.height}")
|
||||
Log.d(TAG, "Selected preview size: $previewSize")
|
||||
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).maxBy { 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(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(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
|
||||
capture_button.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<Surface>,
|
||||
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>(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)
|
||||
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()
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* 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 de.weseng.camera.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 de.weseng.camera.utils.GenericListAdapter
|
||||
import de.weseng.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<Bitmap> = 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})")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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 de.weseng.camera.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 de.weseng.camera.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
|
||||
Navigation.findNavController(requireActivity(), R.id.fragment_container).navigate(
|
||||
PermissionsFragmentDirections.actionPermissionsToSelector())
|
||||
} else {
|
||||
// Request camera-related permissions
|
||||
requestPermissions(PERMISSIONS_REQUIRED, PERMISSIONS_REQUEST_CODE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
if (requestCode == PERMISSIONS_REQUEST_CODE) {
|
||||
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
// Takes the user to the success fragment when permission is granted
|
||||
Navigation.findNavController(requireActivity(), R.id.fragment_container).navigate(
|
||||
PermissionsFragmentDirections.actionPermissionsToSelector())
|
||||
} else {
|
||||
Toast.makeText(context, "Permission request denied", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* 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 de.weseng.camera.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 de.weseng.camera.basic.R
|
||||
import de.weseng.camera.utils.GenericListAdapter
|
||||
|
||||
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<TextView>(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<FormatItem> {
|
||||
val availableCameras: MutableList<FormatItem> = 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
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
app/src/main/res/drawable-hdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/drawable-hdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
BIN
app/src/main/res/drawable-mdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/drawable-mdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/drawable-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/drawable-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
25
app/src/main/res/drawable/ic_photo.xml
Normal file
25
app/src/main/res/drawable/ic_photo.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
|
||||
</vector>
|
||||
42
app/src/main/res/layout-land/fragment_camera.xml
Normal file
42
app/src/main/res/layout-land/fragment_camera.xml
Normal file
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<de.weseng.camera.utils.AutoFitSurfaceView
|
||||
android:id="@+id/view_finder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<View
|
||||
android:id="@+id/overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:foreground="@android:color/transparent" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/capture_button"
|
||||
android:layout_width="96dp"
|
||||
android:layout_height="96dp"
|
||||
android:layout_gravity="center|right"
|
||||
android:scaleType="fitCenter"
|
||||
android:background="@drawable/ic_shutter"
|
||||
android:contentDescription="@string/capture" />
|
||||
|
||||
</FrameLayout>
|
||||
28
app/src/main/res/layout/activity_camera.xml
Normal file
28
app/src/main/res/layout/activity_camera.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/fragment_container"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true"
|
||||
app:defaultNavHost="true"
|
||||
app:navGraph="@navigation/nav_graph"
|
||||
tools:context="de.weseng.camera.basic.CameraActivity" />
|
||||
42
app/src/main/res/layout/fragment_camera.xml
Normal file
42
app/src/main/res/layout/fragment_camera.xml
Normal file
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<de.weseng.camera.utils.AutoFitSurfaceView
|
||||
android:id="@+id/view_finder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<View
|
||||
android:id="@+id/overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:foreground="@android:color/transparent" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/capture_button"
|
||||
android:layout_width="96dp"
|
||||
android:layout_height="96dp"
|
||||
android:layout_gravity="bottom|center"
|
||||
android:scaleType="fitCenter"
|
||||
android:background="@drawable/ic_shutter"
|
||||
android:contentDescription="@string/capture" />
|
||||
|
||||
</FrameLayout>
|
||||
97
app/src/main/res/navigation/nav_graph.xml
Normal file
97
app/src/main/res/navigation/nav_graph.xml
Normal file
@@ -0,0 +1,97 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<navigation
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/nav_graph"
|
||||
app:startDestination="@id/permissions_fragment">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/permissions_fragment"
|
||||
android:name="de.weseng.camera.basic.fragments.PermissionsFragment"
|
||||
android:label="Permissions" >
|
||||
|
||||
<action
|
||||
android:id="@+id/action_permissions_to_selector"
|
||||
app:destination="@id/selector_fragment"
|
||||
app:popUpTo="@id/permissions_fragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/selector_fragment"
|
||||
android:name="de.weseng.camera.basic.fragments.SelectorFragment"
|
||||
android:label="Selector" >
|
||||
|
||||
<action
|
||||
android:id="@+id/action_selector_to_camera"
|
||||
app:launchSingleTop="true"
|
||||
app:destination="@id/camera_fragment" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/camera_fragment"
|
||||
android:name="de.weseng.camera.basic.fragments.CameraFragment"
|
||||
android:label="Camera" >
|
||||
|
||||
<argument
|
||||
android:name="camera_id"
|
||||
app:argType="string"
|
||||
app:nullable="false"/>
|
||||
|
||||
<argument
|
||||
android:name="pixel_format"
|
||||
app:argType="integer"
|
||||
app:nullable="false"/>
|
||||
|
||||
<action
|
||||
android:id="@+id/action_camera_to_permissions"
|
||||
app:destination="@id/permissions_fragment"
|
||||
app:popUpTo="@id/camera_fragment"
|
||||
app:popUpToInclusive="true"/>
|
||||
|
||||
<action
|
||||
android:id="@+id/action_camera_to_jpeg_viewer"
|
||||
app:launchSingleTop="true"
|
||||
app:destination="@id/image_viewer_fragment" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/image_viewer_fragment"
|
||||
android:name="de.weseng.camera.basic.fragments.ImageViewerFragment"
|
||||
android:label="Image Viewer" >
|
||||
|
||||
<argument
|
||||
android:name="file_path"
|
||||
app:argType="string"
|
||||
app:nullable="false"/>
|
||||
|
||||
<argument
|
||||
android:name="orientation"
|
||||
app:argType="integer"
|
||||
android:defaultValue="0" />
|
||||
|
||||
<argument
|
||||
android:name="depth"
|
||||
app:argType="boolean"
|
||||
android:defaultValue="false" />
|
||||
</fragment>
|
||||
|
||||
</navigation>
|
||||
19
app/src/main/res/values/strings.xml
Normal file
19
app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Copyright 2015 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.
|
||||
-->
|
||||
<resources>
|
||||
<string name="app_name">CameraLive</string>
|
||||
<string name="capture">Capture</string>
|
||||
</resources>
|
||||
27
app/src/main/res/values/styles.xml
Normal file
27
app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
<resources>
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<item name="android:immersive">true</item>
|
||||
<item name="android:windowFullscreen">true</item>
|
||||
<item name="android:windowTranslucentStatus">true</item>
|
||||
<item name="android:windowTranslucentNavigation">true</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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 de.weseng.camera.basic
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
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<CameraActivity> =
|
||||
ActivityTestRule(CameraActivity::class.java)
|
||||
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test
|
||||
val context = ApplicationProvider.getApplicationContext() as Context
|
||||
assertEquals("de.weseng.camera.basic", context.packageName)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user