inital commit

This commit is contained in:
2021-04-18 19:37:00 +02:00
commit 51b2c91d24
44 changed files with 2782 additions and 0 deletions

36
.gitignore vendored Normal file
View File

@@ -0,0 +1,36 @@
# built application files
*.apk
*.ap_
# Mac files
.DS_Store
# files for the dex VM
*.dex
# Java class files
*.class
# generated files
bin/
gen/
# Ignore gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
proguard-project.txt
# Eclipse files
.project
.classpath
.settings/
# Android Studio/IDEA
*.iml
.idea

191
LICENSE Normal file
View File

@@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
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
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.

36
README.md Normal file
View File

@@ -0,0 +1,36 @@
Android CameraLive
==================
This app is using the Camera2 API to display the camera preview with a JPEG, DEPTH
or RAW frame.
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.
This app displays a live camera preview in a TextureView and is based on the
[Camera2Basic][2] example.
[1]: https://developer.android.com/reference/android/hardware/camera2/package-summary.html
[2]: https://github.com/android/camera-samples/tree/main/Camera2Basic
Pre-requisites
--------------
- Android SDK 29+
- Android Studio 3.5+
Screenshots
-----------
<img src="screenshots/main.png" height="400" alt="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.

89
app/build.gradle Normal file
View 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'
}

View 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>

View 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
}
}

View File

@@ -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")
}
}
}

View File

@@ -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})")
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

View 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>

View 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>

View 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" />

View 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>

View 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>

View 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>

View 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>

View File

@@ -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)
}
}

48
build.gradle Normal file
View File

@@ -0,0 +1,48 @@
/*
* 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.4.31'
ext.java_version = JavaVersion.VERSION_1_8
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.4"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
mavenLocal()
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

22
gradle.properties Normal file
View File

@@ -0,0 +1,22 @@
# 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

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,6 @@
#Wed Mar 31 20:44:51 PDT 2021
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip

172
gradlew vendored Executable file
View File

@@ -0,0 +1,172 @@
#!/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" "$@"

84
gradlew.bat vendored Normal file
View File

@@ -0,0 +1,84 @@
@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

BIN
screenshots/icon-web.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

BIN
screenshots/main.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

18
settings.gradle Normal file
View File

@@ -0,0 +1,18 @@
/*
* 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'

2
utils/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/build
.idea

75
utils/build.gradle Normal file
View File

@@ -0,0 +1,75 @@
/*
* 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'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 29
defaultConfig {
minSdkVersion 21
targetSdkVersion 29
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.3.4'
// App compat and UI things
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
// EXIF Interface
implementation 'androidx.exifinterface:exifinterface:1.2.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'
}

View File

@@ -0,0 +1,17 @@
<?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 package="de.weseng.camera.utils" />

View File

@@ -0,0 +1,79 @@
/*
* 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.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
}
}

View File

@@ -0,0 +1,79 @@
/*
* 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.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 <T>getPreviewOutputSize(
display: Display,
characteristics: CameraCharacteristics,
targetClass: Class<T>,
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
}

View File

@@ -0,0 +1,73 @@
/*
* 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.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
}

View File

@@ -0,0 +1,55 @@
/*
* 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.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<T> = (view: View, data: T, position: Int) -> Unit
/** List adapter for generic types, intended used for small-medium lists of data */
class GenericListAdapter<T>(
private val dataset: List<T>,
private val itemLayoutId: Int? = null,
private val itemViewFactory: (() -> View)? = null,
private val onBind: BindCallback<T>
) : RecyclerView.Adapter<GenericListAdapter.GenericListViewHolder>() {
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
}

View File

@@ -0,0 +1,95 @@
/*
* 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.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<Int>() {
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
}
}
}

View File

@@ -0,0 +1,198 @@
/*
* 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.utils
import android.content.Context
import android.graphics.Bitmap
import android.graphics.ImageFormat
import android.graphics.Rect
import android.media.Image
import android.renderscript.Allocation
import android.renderscript.Element
import android.renderscript.RenderScript
import android.renderscript.ScriptIntrinsicYuvToRGB
import android.renderscript.Type
/**
* Helper class used to efficiently convert a [Media.Image] object from
* [ImageFormat.YUV_420_888] format to an RGB [Bitmap] object.
*
* The [yuvToRgb] method is able to achieve the same FPS as the CameraX image
* analysis use case on a Pixel 3 XL device at the default analyzer resolution,
* which is 30 FPS with 640x480.
*
* 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.
*/
class YuvToRgbConverter(context: Context) {
private val rs = RenderScript.create(context)
private val scriptYuvToRgb = ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs))
private var pixelCount: Int = -1
private lateinit var yuvBuffer: ByteArray
private lateinit var inputAllocation: Allocation
private lateinit var outputAllocation: Allocation
@Synchronized
fun yuvToRgb(image: Image, output: Bitmap) {
// Ensure that the intermediate output byte buffer is allocated
if (!::yuvBuffer.isInitialized) {
pixelCount = image.cropRect.width() * image.cropRect.height()
// Bits per pixel is an average for the whole image, so it's useful to compute the size
// of the full buffer but should not be used to determine pixel offsets
val pixelSizeBits = ImageFormat.getBitsPerPixel(ImageFormat.YUV_420_888)
yuvBuffer = ByteArray(pixelCount * pixelSizeBits / 8)
}
// Get the YUV data in byte array form using NV21 format
imageToByteArray(image, yuvBuffer)
// Ensure that the RenderScript inputs and outputs are allocated
if (!::inputAllocation.isInitialized) {
// Explicitly create an element with type NV21, since that's the pixel format we use
val elemType = Type.Builder(rs, Element.YUV(rs)).setYuvFormat(ImageFormat.NV21).create()
inputAllocation = Allocation.createSized(rs, elemType.element, yuvBuffer.size)
}
if (!::outputAllocation.isInitialized) {
outputAllocation = Allocation.createFromBitmap(rs, output)
}
// Convert NV21 format YUV to RGB
inputAllocation.copyFrom(yuvBuffer)
scriptYuvToRgb.setInput(inputAllocation)
scriptYuvToRgb.forEach(outputAllocation)
outputAllocation.copyTo(output)
}
private fun imageToByteArray(image: Image, outputBuffer: ByteArray) {
assert(image.format == ImageFormat.YUV_420_888)
val imageCrop = image.cropRect
val imagePlanes = image.planes
imagePlanes.forEachIndexed { planeIndex, plane ->
// How many values are read in input for each output value written
// Only the Y plane has a value for every pixel, U and V have half the resolution i.e.
//
// Y Plane U Plane V Plane
// =============== ======= =======
// Y Y Y Y Y Y Y Y U U U U V V V V
// Y Y Y Y Y Y Y Y U U U U V V V V
// Y Y Y Y Y Y Y Y U U U U V V V V
// Y Y Y Y Y Y Y Y U U U U V V V V
// Y Y Y Y Y Y Y Y
// Y Y Y Y Y Y Y Y
// Y Y Y Y Y Y Y Y
val outputStride: Int
// The index in the output buffer the next value will be written at
// For Y it's zero, for U and V we start at the end of Y and interleave them i.e.
//
// First chunk Second chunk
// =============== ===============
// Y Y Y Y Y Y Y Y V U V U V U V U
// Y Y Y Y Y Y Y Y V U V U V U V U
// Y Y Y Y Y Y Y Y V U V U V U V U
// Y Y Y Y Y Y Y Y V U V U V U V U
// Y Y Y Y Y Y Y Y
// Y Y Y Y Y Y Y Y
// Y Y Y Y Y Y Y Y
var outputOffset: Int
when (planeIndex) {
0 -> {
outputStride = 1
outputOffset = 0
}
1 -> {
outputStride = 2
// For NV21 format, U is in odd-numbered indices
outputOffset = pixelCount + 1
}
2 -> {
outputStride = 2
// For NV21 format, V is in even-numbered indices
outputOffset = pixelCount
}
else -> {
// Image contains more than 3 planes, something strange is going on
return@forEachIndexed
}
}
val planeBuffer = plane.buffer
val rowStride = plane.rowStride
val pixelStride = plane.pixelStride
// We have to divide the width and height by two if it's not the Y plane
val planeCrop = if (planeIndex == 0) {
imageCrop
} else {
Rect(
imageCrop.left / 2,
imageCrop.top / 2,
imageCrop.right / 2,
imageCrop.bottom / 2
)
}
val planeWidth = planeCrop.width()
val planeHeight = planeCrop.height()
// Intermediate buffer used to store the bytes of each row
val rowBuffer = ByteArray(plane.rowStride)
// Size of each row in bytes
val rowLength = if (pixelStride == 1 && outputStride == 1) {
planeWidth
} else {
// Take into account that the stride may include data from pixels other than this
// particular plane and row, and that could be between pixels and not after every
// pixel:
//
// |---- Pixel stride ----| Row ends here --> |
// | Pixel 1 | Other Data | Pixel 2 | Other Data | ... | Pixel N |
//
// We need to get (N-1) * (pixel stride bytes) per row + 1 byte for the last pixel
(planeWidth - 1) * pixelStride + 1
}
for (row in 0 until planeHeight) {
// Move buffer position to the beginning of this row
planeBuffer.position(
(row + planeCrop.top) * rowStride + planeCrop.left * pixelStride)
if (pixelStride == 1 && outputStride == 1) {
// When there is a single stride value for pixel and output, we can just copy
// the entire row in a single step
planeBuffer.get(outputBuffer, outputOffset, rowLength)
outputOffset += rowLength
} else {
// When either pixel or output have a stride > 1 we must copy pixel by pixel
planeBuffer.get(rowBuffer, 0, rowLength)
for (col in 0 until planeWidth) {
outputBuffer[outputOffset] = rowBuffer[col * pixelStride]
outputOffset += outputStride
}
}
}
}
}
}

View File

@@ -0,0 +1,21 @@
<?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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/ic_shutter_pressed" />
<item android:state_focused="true" android:drawable="@drawable/ic_shutter_focused" />
<item android:drawable="@drawable/ic_shutter_normal" />
</selector>

View 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="74"
android:viewportHeight="74">
<path android:fillColor="#FFFFFF" android:fillType="evenOdd"
android:pathData="M73.1,37C73.1,17.0637 56.9373,0.9 37,0.9C17.0627,0.9 0.9,17.0637 0.9,37C0.9,56.9373 17.0627,73.1 37,73.1C56.9373,73.1 73.1,56.9373 73.1,37"
android:strokeColor="#00000000" android:strokeWidth="1"/>
<path android:fillColor="#58A0C4" android:fillType="evenOdd"
android:pathData="M67.4,37C67.4,53.7895 53.7895,67.4 37,67.4C20.2105,67.4 6.6,53.7895 6.6,37C6.6,20.2105 20.2105,6.6 37,6.6C53.7895,6.6 67.4,20.2105 67.4,37"
android:strokeColor="#00000000" android:strokeWidth="1"/>
</vector>

View 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="74"
android:viewportHeight="74">
<path android:fillColor="#FFFFFF" android:fillType="evenOdd"
android:pathData="M73.1,37C73.1,17.0637 56.9373,0.9 37,0.9C17.0627,0.9 0.9,17.0637 0.9,37C0.9,56.9373 17.0627,73.1 37,73.1C56.9373,73.1 73.1,56.9373 73.1,37"
android:strokeColor="#00000000" android:strokeWidth="1"/>
<path android:fillColor="#CFD7DB" android:fillType="evenOdd"
android:pathData="M67.4,37C67.4,53.7895 53.7895,67.4 37,67.4C20.2105,67.4 6.6,53.7895 6.6,37C6.6,20.2105 20.2105,6.6 37,6.6C53.7895,6.6 67.4,20.2105 67.4,37"
android:strokeColor="#00000000" android:strokeWidth="1"/>
</vector>

View 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="74"
android:viewportHeight="74">
<path android:fillColor="#FFFFFF" android:fillType="evenOdd"
android:pathData="M73.1,37C73.1,17.0637 56.9373,0.9 37,0.9C17.0627,0.9 0.9,17.0637 0.9,37C0.9,56.9373 17.0627,73.1 37,73.1C56.9373,73.1 73.1,56.9373 73.1,37"
android:strokeColor="#00000000" android:strokeWidth="1"/>
<path android:fillColor="#58A0C4" android:fillType="evenOdd"
android:pathData="M67.4,37C67.4,53.7895 53.7895,67.4 37,67.4C20.2105,67.4 6.6,53.7895 6.6,37C6.6,20.2105 20.2105,6.6 37,6.6C53.7895,6.6 67.4,20.2105 67.4,37"
android:strokeColor="#00000000" android:strokeWidth="1"/>
</vector>