inital commit
This commit is contained in:
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal 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
191
LICENSE
Normal 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
36
README.md
Normal 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
89
app/build.gradle
Normal file
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: "androidx.navigation.safeargs"
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
defaultConfig {
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
applicationId "de.weseng.camera.basic"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode 1
|
||||
versionName "1.0.0"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility rootProject.ext.java_version
|
||||
targetCompatibility rootProject.ext.java_version
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "$rootProject.ext.java_version"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':utils')
|
||||
|
||||
// Kotlin lang
|
||||
implementation 'androidx.core:core-ktx:1.2.0'
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.4'
|
||||
|
||||
// App compat and UI things
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
|
||||
implementation "androidx.viewpager2:viewpager2:1.0.0"
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
|
||||
// Navigation library
|
||||
def nav_version = '2.2.2'
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
|
||||
|
||||
// EXIF Interface
|
||||
implementation 'androidx.exifinterface:exifinterface:1.2.0'
|
||||
|
||||
// Glide
|
||||
implementation 'com.github.bumptech.glide:glide:4.11.0'
|
||||
kapt 'com.github.bumptech.glide:compiler:4.11.0'
|
||||
|
||||
// Unit testing
|
||||
testImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
testImplementation 'androidx.test:rules:1.2.0'
|
||||
testImplementation 'androidx.test:runner:1.2.0'
|
||||
testImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
testImplementation 'org.robolectric:robolectric:4.3.1'
|
||||
|
||||
// Instrumented testing
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test:rules:1.2.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
}
|
||||
49
app/src/main/AndroidManifest.xml
Normal file
49
app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright 2020 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ https://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="de.weseng.camera.basic">
|
||||
|
||||
<!-- Permission declarations -->
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
<!-- A camera with (optional) RAW capability is required to use this application -->
|
||||
<uses-feature android:name="android.hardware.camera.any" />
|
||||
<uses-feature android:name="android.hardware.camera.raw" android:required="false" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="true"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
|
||||
<activity
|
||||
android:name="de.weseng.camera.basic.CameraActivity"
|
||||
android:clearTaskOnLaunch="true"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<!-- Main app intent filter -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
58
app/src/main/java/de/weseng/camera/basic/CameraActivity.kt
Normal file
58
app/src/main/java/de/weseng/camera/basic/CameraActivity.kt
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package de.weseng.camera.basic
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageButton
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
||||
class CameraActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var container: FrameLayout
|
||||
private lateinit var captureButton: ImageButton
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_camera)
|
||||
container = findViewById(R.id.fragment_container)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// Before setting full screen flags, we must wait a bit to let UI settle; otherwise, we may
|
||||
// be trying to set app to immersive mode before it's ready and the flags do not stick
|
||||
container.postDelayed({
|
||||
container.systemUiVisibility = FLAGS_FULLSCREEN
|
||||
}, IMMERSIVE_FLAG_TIMEOUT)
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** Combination of all flags required to put activity into immersive mode */
|
||||
const val FLAGS_FULLSCREEN =
|
||||
View.SYSTEM_UI_FLAG_LOW_PROFILE or
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
|
||||
/** Milliseconds used for UI animations */
|
||||
const val ANIMATION_FAST_MILLIS = 50L
|
||||
const val ANIMATION_SLOW_MILLIS = 100L
|
||||
private const val IMMERSIVE_FLAG_TIMEOUT = 500L
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,487 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package de.weseng.camera.basic.fragments
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.ImageFormat
|
||||
import android.hardware.camera2.CameraCaptureSession
|
||||
import android.hardware.camera2.CameraCharacteristics
|
||||
import android.hardware.camera2.CameraDevice
|
||||
import android.hardware.camera2.CameraManager
|
||||
import android.hardware.camera2.CaptureRequest
|
||||
import android.hardware.camera2.CaptureResult
|
||||
import android.hardware.camera2.DngCreator
|
||||
import android.hardware.camera2.TotalCaptureResult
|
||||
import android.media.Image
|
||||
import android.media.ImageReader
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Surface
|
||||
import android.view.SurfaceHolder
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import androidx.exifinterface.media.ExifInterface
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import de.weseng.camera.utils.computeExifOrientation
|
||||
import de.weseng.camera.utils.getPreviewOutputSize
|
||||
import de.weseng.camera.utils.AutoFitSurfaceView
|
||||
import de.weseng.camera.utils.OrientationLiveData
|
||||
import de.weseng.camera.basic.R
|
||||
import de.weseng.camera.basic.CameraActivity
|
||||
import kotlinx.android.synthetic.main.fragment_camera.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.concurrent.ArrayBlockingQueue
|
||||
import java.util.concurrent.TimeoutException
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import kotlin.RuntimeException
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
class CameraFragment : Fragment() {
|
||||
|
||||
/** AndroidX navigation arguments */
|
||||
private val args: CameraFragmentArgs by navArgs()
|
||||
|
||||
/** Host's navigation controller */
|
||||
private val navController: NavController by lazy {
|
||||
Navigation.findNavController(requireActivity(), R.id.fragment_container)
|
||||
}
|
||||
|
||||
/** Detects, characterizes, and connects to a CameraDevice (used for all camera operations) */
|
||||
private val cameraManager: CameraManager by lazy {
|
||||
val context = requireContext().applicationContext
|
||||
context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
|
||||
}
|
||||
|
||||
/** [CameraCharacteristics] corresponding to the provided Camera ID */
|
||||
private val characteristics: CameraCharacteristics by lazy {
|
||||
cameraManager.getCameraCharacteristics(args.cameraId)
|
||||
}
|
||||
|
||||
/** Readers used as buffers for camera still shots */
|
||||
private lateinit var imageReader: ImageReader
|
||||
|
||||
/** [HandlerThread] where all camera operations run */
|
||||
private val cameraThread = HandlerThread("CameraThread").apply { start() }
|
||||
|
||||
/** [Handler] corresponding to [cameraThread] */
|
||||
private val cameraHandler = Handler(cameraThread.looper)
|
||||
|
||||
/** Performs recording animation of flashing screen */
|
||||
private val animationTask: Runnable by lazy {
|
||||
Runnable {
|
||||
// Flash white animation
|
||||
overlay.background = Color.argb(150, 255, 255, 255).toDrawable()
|
||||
// Wait for ANIMATION_FAST_MILLIS
|
||||
overlay.postDelayed({
|
||||
// Remove white flash animation
|
||||
overlay.background = null
|
||||
}, CameraActivity.ANIMATION_FAST_MILLIS)
|
||||
}
|
||||
}
|
||||
|
||||
/** [HandlerThread] where all buffer reading operations run */
|
||||
private val imageReaderThread = HandlerThread("imageReaderThread").apply { start() }
|
||||
|
||||
/** [Handler] corresponding to [imageReaderThread] */
|
||||
private val imageReaderHandler = Handler(imageReaderThread.looper)
|
||||
|
||||
/** Where the camera preview is displayed */
|
||||
private lateinit var viewFinder: AutoFitSurfaceView
|
||||
|
||||
/** Overlay on top of the camera preview */
|
||||
private lateinit var overlay: View
|
||||
|
||||
/** The [CameraDevice] that will be opened in this fragment */
|
||||
private lateinit var camera: CameraDevice
|
||||
|
||||
/** Internal reference to the ongoing [CameraCaptureSession] configured with our parameters */
|
||||
private lateinit var session: CameraCaptureSession
|
||||
|
||||
/** Live data listener for changes in the device orientation relative to the camera */
|
||||
private lateinit var relativeOrientation: OrientationLiveData
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? = inflater.inflate(R.layout.fragment_camera, container, false)
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
overlay = view.findViewById(R.id.overlay)
|
||||
viewFinder = view.findViewById(R.id.view_finder)
|
||||
capture_button.setOnApplyWindowInsetsListener { v, insets ->
|
||||
v.translationX = (-insets.systemWindowInsetRight).toFloat()
|
||||
v.translationY = (-insets.systemWindowInsetBottom).toFloat()
|
||||
insets.consumeSystemWindowInsets()
|
||||
}
|
||||
capture_button.setVisibility(View.GONE)
|
||||
//captureButton = findViewById(R.id.capture_button)
|
||||
//captureButton.setVisibility(View.GONE))
|
||||
|
||||
viewFinder.holder.addCallback(object : SurfaceHolder.Callback {
|
||||
override fun surfaceDestroyed(holder: SurfaceHolder) = Unit
|
||||
|
||||
override fun surfaceChanged(
|
||||
holder: SurfaceHolder,
|
||||
format: Int,
|
||||
width: Int,
|
||||
height: Int) = Unit
|
||||
|
||||
override fun surfaceCreated(holder: SurfaceHolder) {
|
||||
|
||||
// Selects appropriate preview size and configures view finder
|
||||
val previewSize = getPreviewOutputSize(
|
||||
viewFinder.display, characteristics, SurfaceHolder::class.java)
|
||||
Log.d(TAG, "View finder size: ${viewFinder.width} x ${viewFinder.height}")
|
||||
Log.d(TAG, "Selected preview size: $previewSize")
|
||||
viewFinder.setAspectRatio(previewSize.width, previewSize.height)
|
||||
|
||||
// To ensure that size is set, initialize camera in the view's thread
|
||||
view.post { initializeCamera() }
|
||||
}
|
||||
})
|
||||
|
||||
// Used to rotate the output media to match device orientation
|
||||
relativeOrientation = OrientationLiveData(requireContext(), characteristics).apply {
|
||||
observe(viewLifecycleOwner, Observer {
|
||||
orientation -> Log.d(TAG, "Orientation changed: $orientation")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin all camera operations in a coroutine in the main thread. This function:
|
||||
* - Opens the camera
|
||||
* - Configures the camera session
|
||||
* - Starts the preview by dispatching a repeating capture request
|
||||
* - Sets up the still image capture listeners
|
||||
*/
|
||||
private fun initializeCamera() = lifecycleScope.launch(Dispatchers.Main) {
|
||||
// Open the selected camera
|
||||
camera = openCamera(cameraManager, args.cameraId, cameraHandler)
|
||||
|
||||
// Initialize an image reader which will be used to capture still photos
|
||||
val size = characteristics.get(
|
||||
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
|
||||
.getOutputSizes(args.pixelFormat).maxBy { it.height * it.width }!!
|
||||
imageReader = ImageReader.newInstance(
|
||||
size.width, size.height, args.pixelFormat, IMAGE_BUFFER_SIZE)
|
||||
|
||||
// Creates list of Surfaces where the camera will output frames
|
||||
val targets = listOf(viewFinder.holder.surface, imageReader.surface)
|
||||
|
||||
// Start a capture session using our open camera and list of Surfaces where frames will go
|
||||
session = createCaptureSession(camera, targets, cameraHandler)
|
||||
|
||||
val captureRequest = camera.createCaptureRequest(
|
||||
CameraDevice.TEMPLATE_PREVIEW).apply { addTarget(viewFinder.holder.surface) }
|
||||
|
||||
// This will keep sending the capture request as frequently as possible until the
|
||||
// session is torn down or session.stopRepeating() is called
|
||||
session.setRepeatingRequest(captureRequest.build(), null, cameraHandler)
|
||||
|
||||
// Listen to the capture button
|
||||
capture_button.setOnClickListener {
|
||||
|
||||
// Disable click listener to prevent multiple requests simultaneously in flight
|
||||
it.isEnabled = false
|
||||
|
||||
// Perform I/O heavy operations in a different scope
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
takePhoto().use { result ->
|
||||
Log.d(TAG, "Result received: $result")
|
||||
|
||||
// Save the result to disk
|
||||
val output = saveResult(result)
|
||||
Log.d(TAG, "Image saved: ${output.absolutePath}")
|
||||
|
||||
// If the result is a JPEG file, update EXIF metadata with orientation info
|
||||
if (output.extension == "jpg") {
|
||||
val exif = ExifInterface(output.absolutePath)
|
||||
exif.setAttribute(
|
||||
ExifInterface.TAG_ORIENTATION, result.orientation.toString())
|
||||
exif.saveAttributes()
|
||||
Log.d(TAG, "EXIF metadata saved: ${output.absolutePath}")
|
||||
}
|
||||
|
||||
// Display the photo taken to user
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
navController.navigate(CameraFragmentDirections.actionCameraToJpegViewer(output.absolutePath)
|
||||
.setOrientation(result.orientation)
|
||||
.setDepth(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
|
||||
result.format == ImageFormat.DEPTH_JPEG))
|
||||
}
|
||||
}
|
||||
|
||||
// Re-enable click listener after photo is taken
|
||||
it.post { it.isEnabled = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Opens the camera and returns the opened device (as the result of the suspend coroutine) */
|
||||
@SuppressLint("MissingPermission")
|
||||
private suspend fun openCamera(
|
||||
manager: CameraManager,
|
||||
cameraId: String,
|
||||
handler: Handler? = null
|
||||
): CameraDevice = suspendCancellableCoroutine { cont ->
|
||||
manager.openCamera(cameraId, object : CameraDevice.StateCallback() {
|
||||
override fun onOpened(device: CameraDevice) = cont.resume(device)
|
||||
|
||||
override fun onDisconnected(device: CameraDevice) {
|
||||
Log.w(TAG, "Camera $cameraId has been disconnected")
|
||||
requireActivity().finish()
|
||||
}
|
||||
|
||||
override fun onError(device: CameraDevice, error: Int) {
|
||||
val msg = when(error) {
|
||||
ERROR_CAMERA_DEVICE -> "Fatal (device)"
|
||||
ERROR_CAMERA_DISABLED -> "Device policy"
|
||||
ERROR_CAMERA_IN_USE -> "Camera in use"
|
||||
ERROR_CAMERA_SERVICE -> "Fatal (service)"
|
||||
ERROR_MAX_CAMERAS_IN_USE -> "Maximum cameras in use"
|
||||
else -> "Unknown"
|
||||
}
|
||||
val exc = RuntimeException("Camera $cameraId error: ($error) $msg")
|
||||
Log.e(TAG, exc.message, exc)
|
||||
if (cont.isActive) cont.resumeWithException(exc)
|
||||
}
|
||||
}, handler)
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a [CameraCaptureSession] and returns the configured session (as the result of the
|
||||
* suspend coroutine
|
||||
*/
|
||||
private suspend fun createCaptureSession(
|
||||
device: CameraDevice,
|
||||
targets: List<Surface>,
|
||||
handler: Handler? = null
|
||||
): CameraCaptureSession = suspendCoroutine { cont ->
|
||||
|
||||
// Create a capture session using the predefined targets; this also involves defining the
|
||||
// session state callback to be notified of when the session is ready
|
||||
device.createCaptureSession(targets, object: CameraCaptureSession.StateCallback() {
|
||||
|
||||
override fun onConfigured(session: CameraCaptureSession) = cont.resume(session)
|
||||
|
||||
override fun onConfigureFailed(session: CameraCaptureSession) {
|
||||
val exc = RuntimeException("Camera ${device.id} session configuration failed")
|
||||
Log.e(TAG, exc.message, exc)
|
||||
cont.resumeWithException(exc)
|
||||
}
|
||||
}, handler)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function used to capture a still image using the [CameraDevice.TEMPLATE_STILL_CAPTURE]
|
||||
* template. It performs synchronization between the [CaptureResult] and the [Image] resulting
|
||||
* from the single capture, and outputs a [CombinedCaptureResult] object.
|
||||
*/
|
||||
private suspend fun takePhoto():
|
||||
CombinedCaptureResult = suspendCoroutine { cont ->
|
||||
|
||||
// Flush any images left in the image reader
|
||||
@Suppress("ControlFlowWithEmptyBody")
|
||||
while (imageReader.acquireNextImage() != null) {}
|
||||
|
||||
// Start a new image queue
|
||||
val imageQueue = ArrayBlockingQueue<Image>(IMAGE_BUFFER_SIZE)
|
||||
imageReader.setOnImageAvailableListener({ reader ->
|
||||
val image = reader.acquireNextImage()
|
||||
Log.d(TAG, "Image available in queue: ${image.timestamp}")
|
||||
imageQueue.add(image)
|
||||
}, imageReaderHandler)
|
||||
|
||||
val captureRequest = session.device.createCaptureRequest(
|
||||
CameraDevice.TEMPLATE_STILL_CAPTURE).apply { addTarget(imageReader.surface) }
|
||||
session.capture(captureRequest.build(), object : CameraCaptureSession.CaptureCallback() {
|
||||
|
||||
override fun onCaptureStarted(
|
||||
session: CameraCaptureSession,
|
||||
request: CaptureRequest,
|
||||
timestamp: Long,
|
||||
frameNumber: Long) {
|
||||
super.onCaptureStarted(session, request, timestamp, frameNumber)
|
||||
viewFinder.post(animationTask)
|
||||
}
|
||||
|
||||
override fun onCaptureCompleted(
|
||||
session: CameraCaptureSession,
|
||||
request: CaptureRequest,
|
||||
result: TotalCaptureResult) {
|
||||
super.onCaptureCompleted(session, request, result)
|
||||
val resultTimestamp = result.get(CaptureResult.SENSOR_TIMESTAMP)
|
||||
Log.d(TAG, "Capture result received: $resultTimestamp")
|
||||
|
||||
// Set a timeout in case image captured is dropped from the pipeline
|
||||
val exc = TimeoutException("Image dequeuing took too long")
|
||||
val timeoutRunnable = Runnable { cont.resumeWithException(exc) }
|
||||
imageReaderHandler.postDelayed(timeoutRunnable, IMAGE_CAPTURE_TIMEOUT_MILLIS)
|
||||
|
||||
// Loop in the coroutine's context until an image with matching timestamp comes
|
||||
// We need to launch the coroutine context again because the callback is done in
|
||||
// the handler provided to the `capture` method, not in our coroutine context
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
lifecycleScope.launch(cont.context) {
|
||||
while (true) {
|
||||
|
||||
// Dequeue images while timestamps don't match
|
||||
val image = imageQueue.take()
|
||||
// TODO(owahltinez): b/142011420
|
||||
// if (image.timestamp != resultTimestamp) continue
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
|
||||
image.format != ImageFormat.DEPTH_JPEG &&
|
||||
image.timestamp != resultTimestamp) continue
|
||||
Log.d(TAG, "Matching image dequeued: ${image.timestamp}")
|
||||
|
||||
// Unset the image reader listener
|
||||
imageReaderHandler.removeCallbacks(timeoutRunnable)
|
||||
imageReader.setOnImageAvailableListener(null, null)
|
||||
|
||||
// Clear the queue of images, if there are left
|
||||
while (imageQueue.size > 0) {
|
||||
imageQueue.take().close()
|
||||
}
|
||||
|
||||
// Compute EXIF orientation metadata
|
||||
val rotation = relativeOrientation.value ?: 0
|
||||
val mirrored = characteristics.get(CameraCharacteristics.LENS_FACING) ==
|
||||
CameraCharacteristics.LENS_FACING_FRONT
|
||||
val exifOrientation = computeExifOrientation(rotation, mirrored)
|
||||
|
||||
// Build the result and resume progress
|
||||
cont.resume(CombinedCaptureResult(
|
||||
image, result, exifOrientation, imageReader.imageFormat))
|
||||
|
||||
// There is no need to break out of the loop, this coroutine will suspend
|
||||
}
|
||||
}
|
||||
}
|
||||
}, cameraHandler)
|
||||
}
|
||||
|
||||
/** Helper function used to save a [CombinedCaptureResult] into a [File] */
|
||||
private suspend fun saveResult(result: CombinedCaptureResult): File = suspendCoroutine { cont ->
|
||||
when (result.format) {
|
||||
|
||||
// When the format is JPEG or DEPTH JPEG we can simply save the bytes as-is
|
||||
ImageFormat.JPEG, ImageFormat.DEPTH_JPEG -> {
|
||||
val buffer = result.image.planes[0].buffer
|
||||
val bytes = ByteArray(buffer.remaining()).apply { buffer.get(this) }
|
||||
try {
|
||||
val output = createFile(requireContext(), "jpg")
|
||||
FileOutputStream(output).use { it.write(bytes) }
|
||||
cont.resume(output)
|
||||
} catch (exc: IOException) {
|
||||
Log.e(TAG, "Unable to write JPEG image to file", exc)
|
||||
cont.resumeWithException(exc)
|
||||
}
|
||||
}
|
||||
|
||||
// When the format is RAW we use the DngCreator utility library
|
||||
ImageFormat.RAW_SENSOR -> {
|
||||
val dngCreator = DngCreator(characteristics, result.metadata)
|
||||
try {
|
||||
val output = createFile(requireContext(), "dng")
|
||||
FileOutputStream(output).use { dngCreator.writeImage(it, result.image) }
|
||||
cont.resume(output)
|
||||
} catch (exc: IOException) {
|
||||
Log.e(TAG, "Unable to write DNG image to file", exc)
|
||||
cont.resumeWithException(exc)
|
||||
}
|
||||
}
|
||||
|
||||
// No other formats are supported by this sample
|
||||
else -> {
|
||||
val exc = RuntimeException("Unknown image format: ${result.image.format}")
|
||||
Log.e(TAG, exc.message, exc)
|
||||
cont.resumeWithException(exc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
try {
|
||||
camera.close()
|
||||
} catch (exc: Throwable) {
|
||||
Log.e(TAG, "Error closing camera", exc)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
cameraThread.quitSafely()
|
||||
imageReaderThread.quitSafely()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = CameraFragment::class.java.simpleName
|
||||
|
||||
/** Maximum number of images that will be held in the reader's buffer */
|
||||
private const val IMAGE_BUFFER_SIZE: Int = 3
|
||||
|
||||
/** Maximum time allowed to wait for the result of an image capture */
|
||||
private const val IMAGE_CAPTURE_TIMEOUT_MILLIS: Long = 5000
|
||||
|
||||
/** Helper data class used to hold capture metadata with their associated image */
|
||||
data class CombinedCaptureResult(
|
||||
val image: Image,
|
||||
val metadata: CaptureResult,
|
||||
val orientation: Int,
|
||||
val format: Int
|
||||
) : Closeable {
|
||||
override fun close() = image.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a [File] named a using formatted timestamp with the current date and time.
|
||||
*
|
||||
* @return [File] created.
|
||||
*/
|
||||
private fun createFile(context: Context, extension: String): File {
|
||||
val sdf = SimpleDateFormat("yyyy_MM_dd_HH_mm_ss_SSS", Locale.US)
|
||||
return File(context.filesDir, "IMG_${sdf.format(Date())}.$extension")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package de.weseng.camera.basic.fragments
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Matrix
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.util.Log
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.widget.ImageView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.bumptech.glide.Glide
|
||||
import de.weseng.camera.utils.GenericListAdapter
|
||||
import de.weseng.camera.utils.decodeExifOrientation
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.File
|
||||
import kotlin.math.max
|
||||
|
||||
|
||||
class ImageViewerFragment : Fragment() {
|
||||
|
||||
/** AndroidX navigation arguments */
|
||||
private val args: ImageViewerFragmentArgs by navArgs()
|
||||
|
||||
/** Default Bitmap decoding options */
|
||||
private val bitmapOptions = BitmapFactory.Options().apply {
|
||||
inJustDecodeBounds = false
|
||||
// Keep Bitmaps at less than 1 MP
|
||||
if (max(outHeight, outWidth) > DOWNSAMPLE_SIZE) {
|
||||
val scaleFactorX = outWidth / DOWNSAMPLE_SIZE + 1
|
||||
val scaleFactorY = outHeight / DOWNSAMPLE_SIZE + 1
|
||||
inSampleSize = max(scaleFactorX, scaleFactorY)
|
||||
}
|
||||
}
|
||||
|
||||
/** Bitmap transformation derived from passed arguments */
|
||||
private val bitmapTransformation: Matrix by lazy { decodeExifOrientation(args.orientation) }
|
||||
|
||||
/** Flag indicating that there is depth data available for this image */
|
||||
private val isDepth: Boolean by lazy { args.depth }
|
||||
|
||||
/** Data backing our Bitmap viewpager */
|
||||
private val bitmapList: MutableList<Bitmap> = mutableListOf()
|
||||
|
||||
private fun imageViewFactory() = ImageView(requireContext()).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? = ViewPager2(requireContext()).apply {
|
||||
// Populate the ViewPager and implement a cache of two media items
|
||||
offscreenPageLimit = 2
|
||||
adapter = GenericListAdapter(
|
||||
bitmapList,
|
||||
itemViewFactory = { imageViewFactory() }) { view, item, _ ->
|
||||
view as ImageView
|
||||
Glide.with(view).load(item).into(view)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
view as ViewPager2
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
|
||||
// Load input image file
|
||||
val inputBuffer = loadInputBuffer()
|
||||
|
||||
// Load the main JPEG image
|
||||
addItemToViewPager(view, decodeBitmap(inputBuffer, 0, inputBuffer.size))
|
||||
|
||||
// If we have depth data attached, attempt to load it
|
||||
if (isDepth) {
|
||||
try {
|
||||
val depthStart = findNextJpegEndMarker(inputBuffer, 2)
|
||||
addItemToViewPager(view, decodeBitmap(
|
||||
inputBuffer, depthStart, inputBuffer.size - depthStart))
|
||||
|
||||
val confidenceStart = findNextJpegEndMarker(inputBuffer, depthStart)
|
||||
addItemToViewPager(view, decodeBitmap(
|
||||
inputBuffer, confidenceStart, inputBuffer.size - confidenceStart))
|
||||
|
||||
} catch (exc: RuntimeException) {
|
||||
Log.e(TAG, "Invalid start marker for depth or confidence data")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Utility function used to read input file into a byte array */
|
||||
private fun loadInputBuffer(): ByteArray {
|
||||
val inputFile = File(args.filePath)
|
||||
return BufferedInputStream(inputFile.inputStream()).let { stream ->
|
||||
ByteArray(stream.available()).also {
|
||||
stream.read(it)
|
||||
stream.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Utility function used to add an item to the viewpager and notify it, in the main thread */
|
||||
private fun addItemToViewPager(view: ViewPager2, item: Bitmap) = view.post {
|
||||
bitmapList.add(item)
|
||||
view.adapter!!.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
/** Utility function used to decode a [Bitmap] from a byte array */
|
||||
private fun decodeBitmap(buffer: ByteArray, start: Int, length: Int): Bitmap {
|
||||
|
||||
// Load bitmap from given buffer
|
||||
val bitmap = BitmapFactory.decodeByteArray(buffer, start, length, bitmapOptions)
|
||||
|
||||
// Transform bitmap orientation using provided metadata
|
||||
return Bitmap.createBitmap(
|
||||
bitmap, 0, 0, bitmap.width, bitmap.height, bitmapTransformation, true)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = ImageViewerFragment::class.java.simpleName
|
||||
|
||||
/** Maximum size of [Bitmap] decoded */
|
||||
private const val DOWNSAMPLE_SIZE: Int = 1024 // 1MP
|
||||
|
||||
/** These are the magic numbers used to separate the different JPG data chunks */
|
||||
private val JPEG_DELIMITER_BYTES = arrayOf(-1, -39)
|
||||
|
||||
/**
|
||||
* Utility function used to find the markers indicating separation between JPEG data chunks
|
||||
*/
|
||||
private fun findNextJpegEndMarker(jpegBuffer: ByteArray, start: Int): Int {
|
||||
|
||||
// Sanitize input arguments
|
||||
assert(start >= 0) { "Invalid start marker: $start" }
|
||||
assert(jpegBuffer.size > start) {
|
||||
"Buffer size (${jpegBuffer.size}) smaller than start marker ($start)" }
|
||||
|
||||
// Perform a linear search until the delimiter is found
|
||||
for (i in start until jpegBuffer.size - 1) {
|
||||
if (jpegBuffer[i].toInt() == JPEG_DELIMITER_BYTES[0] &&
|
||||
jpegBuffer[i + 1].toInt() == JPEG_DELIMITER_BYTES[1]) {
|
||||
return i + 2
|
||||
}
|
||||
}
|
||||
|
||||
// If we reach this, it means that no marker was found
|
||||
throw RuntimeException("Separator marker not found in buffer (${jpegBuffer.size})")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package de.weseng.camera.basic.fragments
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.Navigation
|
||||
import de.weseng.camera.basic.R
|
||||
|
||||
private const val PERMISSIONS_REQUEST_CODE = 10
|
||||
private val PERMISSIONS_REQUIRED = arrayOf(Manifest.permission.CAMERA)
|
||||
|
||||
/**
|
||||
* This [Fragment] requests permissions and, once granted, it will navigate to the next fragment
|
||||
*/
|
||||
class PermissionsFragment : Fragment() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (hasPermissions(requireContext())) {
|
||||
// If permissions have already been granted, proceed
|
||||
Navigation.findNavController(requireActivity(), R.id.fragment_container).navigate(
|
||||
PermissionsFragmentDirections.actionPermissionsToSelector())
|
||||
} else {
|
||||
// Request camera-related permissions
|
||||
requestPermissions(PERMISSIONS_REQUIRED, PERMISSIONS_REQUEST_CODE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
if (requestCode == PERMISSIONS_REQUEST_CODE) {
|
||||
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
// Takes the user to the success fragment when permission is granted
|
||||
Navigation.findNavController(requireActivity(), R.id.fragment_container).navigate(
|
||||
PermissionsFragmentDirections.actionPermissionsToSelector())
|
||||
} else {
|
||||
Toast.makeText(context, "Permission request denied", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
/** Convenience method used to check if all permissions required by this app are granted */
|
||||
fun hasPermissions(context: Context) = PERMISSIONS_REQUIRED.all {
|
||||
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package de.weseng.camera.basic.fragments
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.ImageFormat
|
||||
import android.hardware.camera2.CameraCharacteristics
|
||||
import android.hardware.camera2.CameraManager
|
||||
import android.hardware.camera2.CameraMetadata
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import de.weseng.camera.basic.R
|
||||
import de.weseng.camera.utils.GenericListAdapter
|
||||
|
||||
class SelectorFragment : Fragment() {
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? = RecyclerView(requireContext())
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
view as RecyclerView
|
||||
view.apply {
|
||||
layoutManager = LinearLayoutManager(requireContext())
|
||||
|
||||
val cameraManager =
|
||||
requireContext().getSystemService(Context.CAMERA_SERVICE) as CameraManager
|
||||
|
||||
val cameraList = enumerateCameras(cameraManager)
|
||||
|
||||
val layoutId = android.R.layout.simple_list_item_1
|
||||
adapter = GenericListAdapter(cameraList, itemLayoutId = layoutId) { view, item, _ ->
|
||||
view.findViewById<TextView>(android.R.id.text1).text = item.title
|
||||
view.setOnClickListener {
|
||||
Navigation.findNavController(requireActivity(), R.id.fragment_container)
|
||||
.navigate(SelectorFragmentDirections.actionSelectorToCamera(
|
||||
item.cameraId, item.format))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
/** Helper class used as a data holder for each selectable camera format item */
|
||||
private data class FormatItem(val title: String, val cameraId: String, val format: Int)
|
||||
|
||||
/** Helper function used to convert a lens orientation enum into a human-readable string */
|
||||
private fun lensOrientationString(value: Int) = when(value) {
|
||||
CameraCharacteristics.LENS_FACING_BACK -> "Back"
|
||||
CameraCharacteristics.LENS_FACING_FRONT -> "Front"
|
||||
CameraCharacteristics.LENS_FACING_EXTERNAL -> "External"
|
||||
else -> "Unknown"
|
||||
}
|
||||
|
||||
/** Helper function used to list all compatible cameras and supported pixel formats */
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun enumerateCameras(cameraManager: CameraManager): List<FormatItem> {
|
||||
val availableCameras: MutableList<FormatItem> = mutableListOf()
|
||||
|
||||
// Get list of all compatible cameras
|
||||
val cameraIds = cameraManager.cameraIdList.filter {
|
||||
val characteristics = cameraManager.getCameraCharacteristics(it)
|
||||
val capabilities = characteristics.get(
|
||||
CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)
|
||||
capabilities?.contains(
|
||||
CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE) ?: false
|
||||
}
|
||||
|
||||
|
||||
// Iterate over the list of cameras and return all the compatible ones
|
||||
cameraIds.forEach { id ->
|
||||
val characteristics = cameraManager.getCameraCharacteristics(id)
|
||||
val orientation = lensOrientationString(
|
||||
characteristics.get(CameraCharacteristics.LENS_FACING)!!)
|
||||
|
||||
// Query the available capabilities and output formats
|
||||
val capabilities = characteristics.get(
|
||||
CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)!!
|
||||
val outputFormats = characteristics.get(
|
||||
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!.outputFormats
|
||||
|
||||
// All cameras *must* support JPEG output so we don't need to check characteristics
|
||||
availableCameras.add(FormatItem(
|
||||
"$orientation JPEG ($id)", id, ImageFormat.JPEG))
|
||||
|
||||
// Return cameras that support RAW capability
|
||||
if (capabilities.contains(
|
||||
CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW) &&
|
||||
outputFormats.contains(ImageFormat.RAW_SENSOR)) {
|
||||
availableCameras.add(FormatItem(
|
||||
"$orientation RAW ($id)", id, ImageFormat.RAW_SENSOR))
|
||||
}
|
||||
|
||||
// Return cameras that support JPEG DEPTH capability
|
||||
if (capabilities.contains(
|
||||
CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT) &&
|
||||
outputFormats.contains(ImageFormat.DEPTH_JPEG)) {
|
||||
availableCameras.add(FormatItem(
|
||||
"$orientation DEPTH ($id)", id, ImageFormat.DEPTH_JPEG))
|
||||
}
|
||||
}
|
||||
|
||||
return availableCameras
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
app/src/main/res/drawable-hdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/drawable-hdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
BIN
app/src/main/res/drawable-mdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/drawable-mdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
BIN
app/src/main/res/drawable-xhdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/drawable-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
BIN
app/src/main/res/drawable-xxhdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/drawable-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
25
app/src/main/res/drawable/ic_photo.xml
Normal file
25
app/src/main/res/drawable/ic_photo.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright 2020 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ https://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
|
||||
</vector>
|
||||
42
app/src/main/res/layout-land/fragment_camera.xml
Normal file
42
app/src/main/res/layout-land/fragment_camera.xml
Normal file
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright 2020 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ https://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<de.weseng.camera.utils.AutoFitSurfaceView
|
||||
android:id="@+id/view_finder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<View
|
||||
android:id="@+id/overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:foreground="@android:color/transparent" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/capture_button"
|
||||
android:layout_width="96dp"
|
||||
android:layout_height="96dp"
|
||||
android:layout_gravity="center|right"
|
||||
android:scaleType="fitCenter"
|
||||
android:background="@drawable/ic_shutter"
|
||||
android:contentDescription="@string/capture" />
|
||||
|
||||
</FrameLayout>
|
||||
28
app/src/main/res/layout/activity_camera.xml
Normal file
28
app/src/main/res/layout/activity_camera.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright 2020 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ https://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/fragment_container"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true"
|
||||
app:defaultNavHost="true"
|
||||
app:navGraph="@navigation/nav_graph"
|
||||
tools:context="de.weseng.camera.basic.CameraActivity" />
|
||||
42
app/src/main/res/layout/fragment_camera.xml
Normal file
42
app/src/main/res/layout/fragment_camera.xml
Normal file
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright 2020 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ https://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<de.weseng.camera.utils.AutoFitSurfaceView
|
||||
android:id="@+id/view_finder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<View
|
||||
android:id="@+id/overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:foreground="@android:color/transparent" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/capture_button"
|
||||
android:layout_width="96dp"
|
||||
android:layout_height="96dp"
|
||||
android:layout_gravity="bottom|center"
|
||||
android:scaleType="fitCenter"
|
||||
android:background="@drawable/ic_shutter"
|
||||
android:contentDescription="@string/capture" />
|
||||
|
||||
</FrameLayout>
|
||||
97
app/src/main/res/navigation/nav_graph.xml
Normal file
97
app/src/main/res/navigation/nav_graph.xml
Normal file
@@ -0,0 +1,97 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright 2020 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ https://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<navigation
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/nav_graph"
|
||||
app:startDestination="@id/permissions_fragment">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/permissions_fragment"
|
||||
android:name="de.weseng.camera.basic.fragments.PermissionsFragment"
|
||||
android:label="Permissions" >
|
||||
|
||||
<action
|
||||
android:id="@+id/action_permissions_to_selector"
|
||||
app:destination="@id/selector_fragment"
|
||||
app:popUpTo="@id/permissions_fragment"
|
||||
app:popUpToInclusive="true" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/selector_fragment"
|
||||
android:name="de.weseng.camera.basic.fragments.SelectorFragment"
|
||||
android:label="Selector" >
|
||||
|
||||
<action
|
||||
android:id="@+id/action_selector_to_camera"
|
||||
app:launchSingleTop="true"
|
||||
app:destination="@id/camera_fragment" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/camera_fragment"
|
||||
android:name="de.weseng.camera.basic.fragments.CameraFragment"
|
||||
android:label="Camera" >
|
||||
|
||||
<argument
|
||||
android:name="camera_id"
|
||||
app:argType="string"
|
||||
app:nullable="false"/>
|
||||
|
||||
<argument
|
||||
android:name="pixel_format"
|
||||
app:argType="integer"
|
||||
app:nullable="false"/>
|
||||
|
||||
<action
|
||||
android:id="@+id/action_camera_to_permissions"
|
||||
app:destination="@id/permissions_fragment"
|
||||
app:popUpTo="@id/camera_fragment"
|
||||
app:popUpToInclusive="true"/>
|
||||
|
||||
<action
|
||||
android:id="@+id/action_camera_to_jpeg_viewer"
|
||||
app:launchSingleTop="true"
|
||||
app:destination="@id/image_viewer_fragment" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/image_viewer_fragment"
|
||||
android:name="de.weseng.camera.basic.fragments.ImageViewerFragment"
|
||||
android:label="Image Viewer" >
|
||||
|
||||
<argument
|
||||
android:name="file_path"
|
||||
app:argType="string"
|
||||
app:nullable="false"/>
|
||||
|
||||
<argument
|
||||
android:name="orientation"
|
||||
app:argType="integer"
|
||||
android:defaultValue="0" />
|
||||
|
||||
<argument
|
||||
android:name="depth"
|
||||
app:argType="boolean"
|
||||
android:defaultValue="false" />
|
||||
</fragment>
|
||||
|
||||
</navigation>
|
||||
19
app/src/main/res/values/strings.xml
Normal file
19
app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Copyright 2015 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<string name="app_name">CameraLive</string>
|
||||
<string name="capture">Capture</string>
|
||||
</resources>
|
||||
27
app/src/main/res/values/styles.xml
Normal file
27
app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright 2020 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ https://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<item name="android:immersive">true</item>
|
||||
<item name="android:windowFullscreen">true</item>
|
||||
<item name="android:windowTranslucentStatus">true</item>
|
||||
<item name="android:windowTranslucentNavigation">true</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package de.weseng.camera.basic
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
import androidx.test.rule.GrantPermissionRule
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MainInstrumentedTest {
|
||||
|
||||
@get:Rule
|
||||
val permissionRule = GrantPermissionRule.grant(Manifest.permission.CAMERA)
|
||||
|
||||
@get:Rule
|
||||
val activityRule: ActivityTestRule<CameraActivity> =
|
||||
ActivityTestRule(CameraActivity::class.java)
|
||||
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test
|
||||
val context = ApplicationProvider.getApplicationContext() as Context
|
||||
assertEquals("de.weseng.camera.basic", context.packageName)
|
||||
}
|
||||
}
|
||||
48
build.gradle
Normal file
48
build.gradle
Normal 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
22
gradle.properties
Normal 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
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
172
gradlew
vendored
Executable 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
84
gradlew.bat
vendored
Normal 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
BIN
screenshots/icon-web.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
BIN
screenshots/main.png
Normal file
BIN
screenshots/main.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
18
settings.gradle
Normal file
18
settings.gradle
Normal 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
2
utils/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/build
|
||||
.idea
|
||||
75
utils/build.gradle
Normal file
75
utils/build.gradle
Normal 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'
|
||||
}
|
||||
17
utils/src/main/AndroidManifest.xml
Normal file
17
utils/src/main/AndroidManifest.xml
Normal 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" />
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
79
utils/src/main/java/de/weseng/camera/utils/CameraSizes.kt
Normal file
79
utils/src/main/java/de/weseng/camera/utils/CameraSizes.kt
Normal 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
|
||||
}
|
||||
73
utils/src/main/java/de/weseng/camera/utils/ExifUtils.kt
Normal file
73
utils/src/main/java/de/weseng/camera/utils/ExifUtils.kt
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
198
utils/src/main/java/de/weseng/camera/utils/YuvToRgbConverter.kt
Normal file
198
utils/src/main/java/de/weseng/camera/utils/YuvToRgbConverter.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
utils/src/main/res/drawable/ic_shutter.xml
Normal file
21
utils/src/main/res/drawable/ic_shutter.xml
Normal 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>
|
||||
28
utils/src/main/res/drawable/ic_shutter_focused.xml
Normal file
28
utils/src/main/res/drawable/ic_shutter_focused.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright 2020 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ https://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<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>
|
||||
28
utils/src/main/res/drawable/ic_shutter_normal.xml
Normal file
28
utils/src/main/res/drawable/ic_shutter_normal.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright 2020 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ https://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<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>
|
||||
28
utils/src/main/res/drawable/ic_shutter_pressed.xml
Normal file
28
utils/src/main/res/drawable/ic_shutter_pressed.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright 2020 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ https://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<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>
|
||||
Reference in New Issue
Block a user