checkpoint: camera works for qr scan
This commit is contained in:
parent
e27e10315a
commit
e26d66cc3b
|
@ -6,6 +6,7 @@ apply plugin: 'kotlin-android-extensions'
|
|||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'deploygate'
|
||||
apply plugin: 'com.github.ben-manes.versions'
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
|
||||
android {
|
||||
compileSdkVersion buildConfig.compileSdkVersion
|
||||
|
@ -19,19 +20,43 @@ android {
|
|||
vectorDrawables.useSupportLibrary = true
|
||||
multiDexEnabled true
|
||||
}
|
||||
|
||||
dataBinding {
|
||||
enabled true
|
||||
}
|
||||
|
||||
flavorDimensions 'network'
|
||||
productFlavors {
|
||||
// product flavor names cannot start with the word "test" because they would clash with other targets
|
||||
ztestnet {
|
||||
dimension 'network'
|
||||
applicationId 'cash.z.android.wallet.testnet'
|
||||
matchingFallbacks = ['debug']
|
||||
}
|
||||
|
||||
zmainnet {
|
||||
dimension 'network'
|
||||
applicationId 'cash.z.android.wallet.mainnet'
|
||||
matchingFallbacks = ['release']
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
mock {
|
||||
initWith debug
|
||||
matchingFallbacks = ['debug', 'release']
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
lintConfig file("zcash-lint-options.xml")
|
||||
}
|
||||
|
@ -59,7 +84,7 @@ dependencies {
|
|||
|
||||
// Zcash
|
||||
implementation deps.zcash.walletSdk
|
||||
compile project(path: ':qrecycler')
|
||||
implementation project(path: ':qrecycler')
|
||||
|
||||
// TODO: get the AAR to provide these
|
||||
implementation "io.grpc:grpc-okhttp:1.17.1"
|
||||
|
@ -79,6 +104,7 @@ dependencies {
|
|||
implementation deps.speeddial
|
||||
implementation deps.lottie
|
||||
debugImplementation deps.stetho
|
||||
mockImplementation deps.stetho
|
||||
|
||||
testImplementation deps.junit
|
||||
androidTestImplementation deps.androidx.test.runner
|
||||
|
|
Binary file not shown.
|
@ -0,0 +1,85 @@
|
|||
package cash.z.android.wallet.di.module
|
||||
|
||||
import cash.z.android.wallet.BuildConfig
|
||||
import cash.z.android.wallet.ZcashWalletApplication
|
||||
import cash.z.android.wallet.sample.SampleProperties
|
||||
import cash.z.android.wallet.sample.SampleProperties.COMPACT_BLOCK_PORT
|
||||
import cash.z.android.wallet.sample.SampleProperties.COMPACT_BLOCK_SERVER
|
||||
import cash.z.wallet.sdk.data.*
|
||||
import cash.z.wallet.sdk.jni.JniConverter
|
||||
import cash.z.wallet.sdk.secure.Wallet
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Module that contributes all the objects necessary for the synchronizer, which is basically everything that has
|
||||
* application scope.
|
||||
*/
|
||||
@Module
|
||||
internal object SynchronizerModule {
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTwig(): Twig = TroubleshootingTwig() // troubleshoot on debug, silent on release
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDownloader(twigger: Twig): CompactBlockStream {
|
||||
return CompactBlockStream(COMPACT_BLOCK_SERVER, COMPACT_BLOCK_PORT, twigger)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideProcessor(application: ZcashWalletApplication, converter: JniConverter, twigger: Twig): CompactBlockProcessor {
|
||||
return CompactBlockProcessor(application, converter, SampleProperties.wallet.cacheDbName, SampleProperties.wallet.dataDbName, logger = twigger)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRepository(application: ZcashWalletApplication, converter: JniConverter, twigger: Twig): TransactionRepository {
|
||||
return PollingTransactionRepository(application, SampleProperties.wallet.dataDbName, 10_000L, converter, twigger)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideWallet(application: ZcashWalletApplication, converter: JniConverter, twigger: Twig): Wallet {
|
||||
return Wallet(converter, application.getDatabasePath(SampleProperties.wallet.dataDbName).absolutePath, "${application.cacheDir.absolutePath}/params", seedProvider = SampleProperties.wallet.seedProvider, spendingKeyProvider = SampleProperties.wallet.spendingKeyProvider, logger = twigger)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideManager(wallet: Wallet, repository: TransactionRepository, downloader: CompactBlockStream, twigger: Twig): ActiveTransactionManager {
|
||||
return ActiveTransactionManager(repository, downloader.connection, wallet, twigger)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideJniConverter(): JniConverter {
|
||||
return JniConverter().also {
|
||||
if (BuildConfig.DEBUG) it.initLogs()
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSynchronizer(
|
||||
downloader: CompactBlockStream,
|
||||
processor: CompactBlockProcessor,
|
||||
repository: TransactionRepository,
|
||||
manager: ActiveTransactionManager,
|
||||
wallet: Wallet,
|
||||
twigger: Twig
|
||||
): Synchronizer {
|
||||
return SdkSynchronizer(downloader, processor, repository, manager, wallet, blockPollFrequency = 500_000L, logger = twigger)
|
||||
}
|
||||
|
||||
}
|
|
@ -4,6 +4,9 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="cash.z.android.wallet">
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
||||
<dist:module dist:instant="true" />
|
||||
|
||||
<application
|
||||
|
@ -19,6 +22,7 @@
|
|||
android:name=".ui.activity.MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/ZcashTheme.NoActionBar"
|
||||
android:windowSoftInputMode="adjustPan"
|
||||
android:screenOrientation="portrait">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
@ -27,6 +31,8 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".ui.util.CameraQrScanner" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -30,6 +30,7 @@ import javax.inject.Singleton
|
|||
ReceiveFragmentModule::class,
|
||||
RequestFragmentModule::class,
|
||||
SendFragmentModule::class,
|
||||
ScanFragmentModule::class,
|
||||
SettingsFragmentModule::class
|
||||
]
|
||||
)
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package cash.z.android.wallet.extention
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
|
||||
internal inline fun Context.alert(@StringRes messageResId: Int, crossinline block: () -> Unit = {}) {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(messageResId)
|
||||
.setPositiveButton(android.R.string.ok) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
block()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.show()
|
||||
}
|
|
@ -2,6 +2,7 @@ package cash.z.android.wallet.extention
|
|||
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.IntegerRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import cash.z.android.wallet.ZcashWalletApplication
|
||||
|
@ -20,3 +21,10 @@ internal inline fun @receiver:ColorRes Int.toAppColor(): Int {
|
|||
internal inline fun @receiver:StringRes Int.toAppString(): String {
|
||||
return ZcashWalletApplication.instance.getString(this)}
|
||||
|
||||
|
||||
/**
|
||||
* Grab an integer from the application resources
|
||||
*/
|
||||
internal inline fun @receiver:IntegerRes Int.toAppInt(): Int {
|
||||
return ZcashWalletApplication.instance.resources.getInteger(this)}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ object AliceWallet {
|
|||
val spendingKeyProvider = SampleSpendingKeySharedPref(name)
|
||||
const val cacheDbName = "testalice_cache.db"
|
||||
const val dataDbName = "testalice_data8.db"
|
||||
const val defaultSendAddress = "ztestsapling1snmqdnfqnc407pvqw7sld8w5zxx6nd0523kvlj4jf39uvxvh7vn0hs3q38n07806dwwecqwke3t" // dummyseed //TODO: add bob's address here
|
||||
}
|
||||
|
||||
object BobWallet {
|
||||
|
@ -17,6 +18,7 @@ object BobWallet {
|
|||
val spendingKeyProvider = SampleSpendingKeySharedPref(name)
|
||||
const val cacheDbName = "testbob_cache.db"
|
||||
const val dataDbName = "testbob_data.db"
|
||||
const val defaultSendAddress = "ztestsapling1snmqdnfqnc407pvqw7sld8w5zxx6nd0523kvlj4jf39uvxvh7vn0hs3q38n07806dwwecqwke3t" // dummyseed //TODO: add alice's address here
|
||||
}
|
||||
|
||||
object MyWallet {
|
||||
|
@ -26,6 +28,7 @@ object MyWallet {
|
|||
val spendingKeyProvider = SampleSpendingKeySharedPref(name)
|
||||
const val cacheDbName = "wallet_cache1202.db"
|
||||
const val dataDbName = "wallet_data1202.db"
|
||||
const val defaultSendAddress = "ztestsapling1snmqdnfqnc407pvqw7sld8w5zxx6nd0523kvlj4jf39uvxvh7vn0hs3q38n07806dwwecqwke3t" // dummyseed
|
||||
}
|
||||
|
||||
enum class Servers(val host: String) {
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package cash.z.android.wallet.sample
|
||||
|
||||
import cash.z.android.qrecycler.QScanner
|
||||
|
||||
class SampleQrScanner : QScanner {
|
||||
override fun scanBarcode(callback: (Result<String>) -> Unit) {
|
||||
callback(Result.success("sampleqrcode_scan_success"))
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@ import cash.z.android.qrecycler.QRecycler
|
|||
import cash.z.android.wallet.R
|
||||
import cash.z.android.wallet.ui.activity.MainActivity
|
||||
import cash.z.android.wallet.ui.util.AddressPartNumberSpan
|
||||
import cash.z.wallet.sdk.data.Synchronizer
|
||||
import cash.z.wallet.sdk.jni.JniConverter
|
||||
import cash.z.wallet.sdk.secure.Wallet
|
||||
import dagger.Module
|
||||
|
@ -28,7 +29,7 @@ class ReceiveFragment : BaseFragment() {
|
|||
lateinit var qrecycler: QRecycler
|
||||
|
||||
@Inject
|
||||
lateinit var wallet: Wallet
|
||||
lateinit var synchronizer: Synchronizer
|
||||
|
||||
lateinit var addressParts: Array<TextView>
|
||||
|
||||
|
@ -89,7 +90,7 @@ class ReceiveFragment : BaseFragment() {
|
|||
|
||||
// TODO: replace with tiered load. First check memory reference (textview contents?) then check DB, then load from JNI and write to DB
|
||||
private fun loadAddress(): String {
|
||||
return wallet.getAddress()
|
||||
return synchronizer.getAddress()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,185 @@
|
|||
package cash.z.android.wallet.ui.fragment
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import cash.z.android.wallet.R
|
||||
import cash.z.android.wallet.databinding.FragmentScanBinding
|
||||
import cash.z.android.wallet.ui.activity.MainActivity
|
||||
import dagger.Module
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
|
||||
/**
|
||||
* Fragment for scanning addresss, hopefully.
|
||||
*/
|
||||
class ScanFragment : BaseFragment() {
|
||||
|
||||
lateinit var binding: FragmentScanBinding
|
||||
|
||||
// private var cameraSource: CameraSource? = null
|
||||
|
||||
private val requiredPermissions: Array<String?>
|
||||
get() {
|
||||
return try {
|
||||
val info = mainActivity.packageManager
|
||||
.getPackageInfo(mainActivity.packageName, PackageManager.GET_PERMISSIONS)
|
||||
val ps = info.requestedPermissions
|
||||
if (ps != null && ps.isNotEmpty()) {
|
||||
ps
|
||||
} else {
|
||||
arrayOfNulls(0)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
arrayOfNulls(0)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return DataBindingUtil.inflate<FragmentScanBinding>(
|
||||
inflater, R.layout.fragment_scan, container, false
|
||||
).let {
|
||||
binding = it
|
||||
it.root
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
(activity as MainActivity).let { mainActivity ->
|
||||
mainActivity.setSupportActionBar(view.findViewById(R.id.toolbar))
|
||||
mainActivity.supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
mainActivity.supportActionBar?.setTitle(R.string.destination_title_send)
|
||||
}
|
||||
// binding.previewCameraSource.doOnLayout {
|
||||
// if (allPermissionsGranted()) {
|
||||
// createCameraSource(it.width, it.height)
|
||||
// } else {
|
||||
// getRuntimePermissions()
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
if(!allPermissionsGranted()) getRuntimePermissions()
|
||||
|
||||
|
||||
// sendPresenter = SendPresenter(this, mainActivity.synchronizer)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if(allPermissionsGranted()) onStartCamera()
|
||||
// launch {
|
||||
// sendPresenter.start()
|
||||
// }
|
||||
// startCameraSource()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
binding.cameraView.stop()
|
||||
super.onPause()
|
||||
// sendPresenter.stop()
|
||||
// binding.previewCameraSource?.stop()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
// cameraSource?.release()
|
||||
}
|
||||
|
||||
/* Camera */
|
||||
// private fun createCameraSource(width: Int, height: Int) {
|
||||
// Toaster.short("w: $width h: $height")
|
||||
// // If there's no existing cameraSource, create one.
|
||||
// if (cameraSource == null) {
|
||||
// cameraSource = CameraSource(mainActivity, binding.graphicOverlay)
|
||||
// }
|
||||
//
|
||||
// try {
|
||||
// cameraSource?.setMachineLearningFrameProcessor(BarcodeScanningProcessor())
|
||||
// } catch (e: FirebaseMLException) {
|
||||
// Log.e("temporaryBehavior", "can not create camera source")
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Starts or restarts the camera source, if it exists. If the camera source doesn't exist yet
|
||||
* (e.g., because onResume was called before the camera source was created), this will be called
|
||||
* again when the camera source is created.
|
||||
*/
|
||||
// private fun startCameraSource() {
|
||||
// cameraSource?.let {
|
||||
// try {
|
||||
// binding.previewCameraSource?.start(cameraSource!!, binding.graphicOverlay)
|
||||
// } catch (e: IOException) {
|
||||
// Log.e("temporaryBehavior", "Unable to start camera source.", e)
|
||||
// cameraSource?.release()
|
||||
// cameraSource = null
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
/* Permissions */
|
||||
|
||||
private fun allPermissionsGranted(): Boolean {
|
||||
for (permission in requiredPermissions) {
|
||||
if (!isPermissionGranted(mainActivity, permission!!)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun getRuntimePermissions() {
|
||||
val allNeededPermissions = arrayListOf<String>()
|
||||
for (permission in requiredPermissions) {
|
||||
if (!isPermissionGranted(mainActivity, permission!!)) {
|
||||
allNeededPermissions.add(permission)
|
||||
}
|
||||
}
|
||||
|
||||
if (!allNeededPermissions.isEmpty()) {
|
||||
requestPermissions(allNeededPermissions.toTypedArray(), CAMERA_PERMISSION_REQUEST)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
if (allPermissionsGranted()) {
|
||||
onStartCamera()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onStartCamera() {
|
||||
with(binding.cameraView) {
|
||||
postDelayed({
|
||||
start()
|
||||
}, 1500L)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
// TODO: continue doing permissions here in a more specific, less general way
|
||||
private const val CAMERA_PERMISSION_REQUEST = 1001
|
||||
|
||||
private fun isPermissionGranted(context: Context, permission: String): Boolean {
|
||||
return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Module
|
||||
abstract class ScanFragmentModule {
|
||||
@ContributesAndroidInjector
|
||||
abstract fun contributeScanFragment(): ScanFragment
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package cash.z.android.wallet.ui.fragment
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.text.Spanned
|
||||
|
@ -9,16 +10,19 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.appcompat.widget.TooltipCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.core.text.toSpannable
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.navigation.fragment.FragmentNavigatorExtras
|
||||
import androidx.transition.TransitionInflater
|
||||
import cash.z.android.qrecycler.QScanner
|
||||
import cash.z.android.wallet.BuildConfig
|
||||
import cash.z.android.wallet.R
|
||||
import cash.z.android.wallet.databinding.FragmentSendBinding
|
||||
import cash.z.android.wallet.extention.afterTextChanged
|
||||
import cash.z.android.wallet.extention.toAppColor
|
||||
import cash.z.android.wallet.extention.tryIgnore
|
||||
import cash.z.android.wallet.extention.*
|
||||
import cash.z.android.wallet.sample.SampleProperties
|
||||
import cash.z.android.wallet.sample.SampleProperties.DEV_MODE
|
||||
import cash.z.android.wallet.ui.activity.MainActivity
|
||||
|
@ -27,6 +31,8 @@ import dagger.Module
|
|||
import dagger.android.ContributesAndroidInjector
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.DecimalFormat
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
|
||||
/**
|
||||
|
@ -35,12 +41,17 @@ import java.text.DecimalFormat
|
|||
*/
|
||||
class SendFragment : BaseFragment(), SendPresenter.SendView {
|
||||
|
||||
@Inject
|
||||
lateinit var qrCodeScanner: QScanner
|
||||
lateinit var sendPresenter: SendPresenter
|
||||
lateinit var binding: FragmentSendBinding
|
||||
|
||||
private val zecFormatter = DecimalFormat("#.######")
|
||||
private val usdFormatter = DecimalFormat("###,###,###.##")
|
||||
private val zecSelected get() = binding.groupZecSelected.visibility == View.VISIBLE
|
||||
private val usdFormatter = DecimalFormat("###,###,##0.00")
|
||||
private val usdSelected get() = binding.groupUsdSelected.visibility == View.VISIBLE
|
||||
|
||||
private val zec = R.string.zec_abbreviation.toAppString()
|
||||
private val usd = R.string.usd_abbreviation.toAppString()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
|
@ -71,35 +82,111 @@ class SendFragment : BaseFragment(), SendPresenter.SendView {
|
|||
mainActivity.supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
mainActivity.supportActionBar?.setTitle(R.string.destination_title_send)
|
||||
}
|
||||
initPresenter()
|
||||
init()
|
||||
initDialog()
|
||||
}
|
||||
|
||||
// temporary function until presenter is setup
|
||||
private fun initPresenter() {
|
||||
private fun init() {
|
||||
binding.imageSwapCurrency.setOnClickListener {
|
||||
onToggleCurrency()
|
||||
}
|
||||
|
||||
binding.textValueHeader.afterTextChanged {
|
||||
tryIgnore {
|
||||
val value = binding.textValueHeader.text.toString().toDouble()
|
||||
binding.textValueSubheader.text = if (zecSelected) {
|
||||
usdFormatter.format(value * SampleProperties.USD_PER_ZEC)
|
||||
} else {
|
||||
zecFormatter.format(value / SampleProperties.USD_PER_ZEC)
|
||||
binding.textValueHeader.apply {
|
||||
afterTextChanged {
|
||||
tryIgnore {
|
||||
// only update things if the user is actively editing. in other words, don't update on programmatic changes
|
||||
if (binding.textValueHeader.hasFocus()) {
|
||||
val value = binding.textValueHeader.text.toString().toDouble()
|
||||
binding.textValueSubheader.text = if (usdSelected) {
|
||||
zecFormatter.format(value / SampleProperties.USD_PER_ZEC) + " $zec"
|
||||
} else {
|
||||
if (value == 0.0) "0 $usd"
|
||||
else usdFormatter.format(value * SampleProperties.USD_PER_ZEC) + " $usd"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.textAreaMemo.afterTextChanged {
|
||||
binding.textMemoCharCount.text = "${binding.textAreaMemo.text.length} / ${resources.getInteger(R.integer.max_memo_length)}"
|
||||
binding.textMemoCharCount.text =
|
||||
"${binding.textAreaMemo.text.length} / ${resources.getInteger(R.integer.memo_max_length)}"
|
||||
}
|
||||
|
||||
binding.buttonSendZec.setOnClickListener {
|
||||
showSendDialog()
|
||||
}
|
||||
binding.buttonSendZec.isEnabled = false
|
||||
|
||||
with(binding.imageScanQr) {
|
||||
TooltipCompat.setTooltipText(this, context.getString(R.string.send_tooltip_scan_qr))
|
||||
}
|
||||
binding.imageAddressShortcut?.apply {
|
||||
if (BuildConfig.DEBUG) {
|
||||
TooltipCompat.setTooltipText(this, context.getString(R.string.send_tooltip_address_shortcut))
|
||||
setOnClickListener(::onPasteShortcutAddress)
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
binding.imageScanQr.setOnClickListener(::onScanQrCode)
|
||||
binding.textValueHeader.setText("0")
|
||||
binding.textValueSubheader.text =
|
||||
mainActivity.resources.getString(R.string.send_subheader_value, if (usdSelected) zec else usd)
|
||||
|
||||
// allow background taps to dismiss the keyboard and clear focus
|
||||
binding.contentFragmentSend.setOnClickListener {
|
||||
it?.findFocus()?.clearFocus()
|
||||
formatUserInput()
|
||||
hideKeyboard()
|
||||
}
|
||||
|
||||
setSendEnabled(true)
|
||||
onToggleCurrency()
|
||||
}
|
||||
|
||||
private fun setAddressLineColor(@ColorRes colorRes: Int = R.color.zcashBlack_12) {
|
||||
DrawableCompat.setTint(
|
||||
binding.inputZcashAddress.background,
|
||||
ContextCompat.getColor(mainActivity, colorRes)
|
||||
)
|
||||
}
|
||||
|
||||
fun formatUserInput() {
|
||||
formatAmountInput()
|
||||
formatAddressInput()
|
||||
}
|
||||
|
||||
private fun formatAmountInput() {
|
||||
val value = binding.textValueHeader.text.toString().toDouble().absoluteValue
|
||||
binding.textValueHeader.setText(
|
||||
when {
|
||||
value == 0.0 -> "0"
|
||||
usdSelected -> usdFormatter.format(value)
|
||||
else -> zecFormatter.format(value)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatAddressInput() {
|
||||
val address = binding.inputZcashAddress.text
|
||||
if(address.isNotEmpty() && address.length < R.integer.z_address_min_length.toAppInt()) setAddressError(R.string.send_error_address_too_short.toAppString())
|
||||
else setAddressError(null)
|
||||
}
|
||||
|
||||
private fun setAddressError(message: String?) {
|
||||
if (message == null) {
|
||||
setAddressLineColor()
|
||||
binding.textAddressError.text = null
|
||||
binding.textAddressError.visibility = View.GONE
|
||||
binding.buttonSendZec.isEnabled = true
|
||||
} else {
|
||||
setAddressLineColor(R.color.zcashRed)
|
||||
binding.textAddressError.text = message
|
||||
binding.textAddressError.visibility = View.VISIBLE
|
||||
binding.buttonSendZec.isEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun initDialog() {
|
||||
|
@ -114,7 +201,6 @@ class SendFragment : BaseFragment(), SendPresenter.SendView {
|
|||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
sendPresenter = SendPresenter(this, mainActivity.synchronizer)
|
||||
onToggleCurrency()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
@ -162,16 +248,51 @@ class SendFragment : BaseFragment(), SendPresenter.SendView {
|
|||
extras)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun onToggleCurrency() {
|
||||
val headerValue = binding.textValueHeader.text
|
||||
binding.textValueHeader.setText(binding.textValueSubheader.text)
|
||||
binding.textValueSubheader.text = headerValue
|
||||
if (zecSelected) {
|
||||
binding.groupZecSelected.visibility = View.GONE
|
||||
binding.groupUsdSelected.visibility = View.VISIBLE
|
||||
} else {
|
||||
view?.findFocus()?.clearFocus()
|
||||
formatUserInput()
|
||||
val isInitiallyUsd = usdSelected // hold this value because we modify visibility here and that's what the value is based on
|
||||
val subHeaderValue = binding.textValueSubheader.text.toString().substringBefore(' ')
|
||||
val currencyLabelAfterToggle = if (isInitiallyUsd) usd else zec // what is selected is about to move to the subheader where the currency is labelled
|
||||
|
||||
binding.textValueSubheader.post {
|
||||
binding.textValueSubheader.text = "${binding.textValueHeader.text} $currencyLabelAfterToggle"
|
||||
binding.textValueHeader.setText(subHeaderValue)
|
||||
}
|
||||
if (isInitiallyUsd) {
|
||||
binding.groupZecSelected.visibility = View.VISIBLE
|
||||
binding.groupUsdSelected.visibility = View.GONE
|
||||
} else {
|
||||
binding.groupZecSelected.visibility = View.GONE
|
||||
binding.groupUsdSelected.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
private fun onScanQrCode(view: View) {
|
||||
hideKeyboard()
|
||||
val fragment = ScanFragment()
|
||||
val ft = childFragmentManager.beginTransaction()
|
||||
.add(R.id.camera_placeholder, fragment, "camera_fragment")
|
||||
.commit()
|
||||
// val intent = Intent(mainActivity, CameraQrScanner::class.java)
|
||||
// mainActivity.startActivity(intent)
|
||||
// qrCodeScanner.scanBarcode { barcode: Result<String> ->
|
||||
// if (barcode.isSuccess) {
|
||||
// binding.inputZcashAddress.setText(barcode.getOrThrow())
|
||||
// formatAddressInput()
|
||||
// } else {
|
||||
// Toaster.short("failed to scan QR code")
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
// TODO: possibly move this behavior to only live in the debug build. Perhaps with a viewholder that I just delegate to. Then inject the holder here.
|
||||
private fun onPasteShortcutAddress(view: View) {
|
||||
view.context.alert(R.string.send_alert_shortcut_clicked) {
|
||||
binding.inputZcashAddress.setText(SampleProperties.wallet.defaultSendAddress)
|
||||
setAddressError(null)
|
||||
hideKeyboard()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -180,7 +301,7 @@ class SendFragment : BaseFragment(), SendPresenter.SendView {
|
|||
val usdBalance = zecBalance * SampleProperties.USD_PER_ZEC
|
||||
val availableZecFormatter = DecimalFormat("#.########")
|
||||
// TODO: use a formatted string resource here
|
||||
val availableTextSpan = "${availableZecFormatter.format(zecBalance)} ZEC Available".toSpannable()
|
||||
val availableTextSpan = "${availableZecFormatter.format(zecBalance)} $zec Available".toSpannable()
|
||||
availableTextSpan.setSpan(ForegroundColorSpan(R.color.colorPrimary.toAppColor()), availableTextSpan.length - "Available".length, availableTextSpan.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
availableTextSpan.setSpan(StyleSpan(Typeface.BOLD), 0, 6, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
binding.textZecValueAvailable.text = availableTextSpan
|
||||
|
@ -205,14 +326,27 @@ class SendFragment : BaseFragment(), SendPresenter.SendView {
|
|||
//
|
||||
|
||||
private fun showSendDialog() {
|
||||
// hide soft keyboard
|
||||
mainActivity.getSystemService<InputMethodManager>()
|
||||
?.hideSoftInputFromWindow(view?.windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
|
||||
hideKeyboard()
|
||||
|
||||
val address = binding.inputZcashAddress.text
|
||||
val headerString = binding.textValueHeader.text.toString()
|
||||
val subheaderString = binding.textValueSubheader.text.toString().substringBefore(' ')
|
||||
val zecString = if(usdSelected) subheaderString else headerString
|
||||
val usdString = if(usdSelected) headerString else subheaderString
|
||||
val memo = binding.textAreaMemo.text.toString().trim()
|
||||
|
||||
setSendEnabled(false) // partially because we need to lower the button elevation
|
||||
binding.dialogTextTitle.text = getString(R.string.send_dialog_title, zecString, zec, usdString)
|
||||
binding.dialogTextAddress.text = address
|
||||
binding.dialogTextMemoIncluded.visibility = if(memo.isNotEmpty()) View.VISIBLE else View.GONE
|
||||
binding.groupDialogSend.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
private fun hideKeyboard() {
|
||||
mainActivity.getSystemService<InputMethodManager>()
|
||||
?.hideSoftInputFromWindow(view?.windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
|
||||
}
|
||||
|
||||
private fun hideSendDialog() {
|
||||
setSendEnabled(true)
|
||||
binding.groupDialogSend.visibility = View.GONE
|
||||
|
@ -221,7 +355,7 @@ class SendFragment : BaseFragment(), SendPresenter.SendView {
|
|||
private fun setSendEnabled(isEnabled: Boolean) {
|
||||
binding.buttonSendZec.isEnabled = isEnabled
|
||||
if (isEnabled) {
|
||||
binding.buttonSendZec.text = "send zec"
|
||||
binding.buttonSendZec.text = "send $zec"
|
||||
// binding.progressSend.visibility = View.GONE
|
||||
} else {
|
||||
binding.buttonSendZec.text = "sending..."
|
||||
|
|
|
@ -26,7 +26,7 @@ class HistoryPresenter(
|
|||
|
||||
override suspend fun start() {
|
||||
Log.e("@TWIG", "historyPresenter starting!")
|
||||
launchTransactionBinder(synchronizer.repository.allTransactions())
|
||||
launchTransactionBinder(synchronizer.allTransactions())
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
|
|
|
@ -30,9 +30,9 @@ class HomePresenter(
|
|||
|
||||
override suspend fun start() {
|
||||
Log.e("@TWIG-t", "homePresenter starting!")
|
||||
launchBalanceBinder(synchronizer.repository.balance())
|
||||
launchTransactionBinder(synchronizer.repository.allTransactions())
|
||||
launchProgressMonitor(synchronizer.downloader.progress())
|
||||
launchBalanceBinder(synchronizer.balance())
|
||||
launchTransactionBinder(synchronizer.allTransactions())
|
||||
launchProgressMonitor(synchronizer.progress())
|
||||
launchActiveTransactionMonitor(synchronizer.activeTransactions())
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ class SendPresenter(
|
|||
override suspend fun start() {
|
||||
Log.e("@TWIG-v", "sendPresenter starting!")
|
||||
with(view) {
|
||||
balanceJob = launchBalanceBinder(synchronizer.repository.balance())
|
||||
balanceJob = launchBalanceBinder(synchronizer.balance())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,10 +54,6 @@ class SendPresenter(
|
|||
view.submit()
|
||||
}
|
||||
|
||||
private suspend fun findTransaction(txId: Long): Transaction? {
|
||||
return if (txId < 0) null else synchronizer.repository.findTransactionById(txId)
|
||||
}
|
||||
|
||||
fun bind(old: Long?, new: Long) {
|
||||
Log.e("@TWIG-v", "binding balance of $new")
|
||||
view.updateBalance(old ?: 0L, new)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<corners android:radius="8dp" />
|
||||
<corners android:radius="6dp" />
|
||||
<solid android:color="@color/zcashWhite_87" />
|
||||
</shape>
|
|
@ -0,0 +1,9 @@
|
|||
<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="#FF000000"
|
||||
android:pathData="M19,2h-4.18C14.4,0.84 13.3,0 12,0c-1.3,0 -2.4,0.84 -2.82,2L5,2c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,4c0,-1.1 -0.9,-2 -2,-2zM12,2c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM19,20L5,20L5,4h2v3h10L17,4h2v16z"/>
|
||||
</vector>
|
|
@ -0,0 +1,66 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/fireTopLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#000"
|
||||
android:keepScreenOn="true"
|
||||
android:orientation="vertical">
|
||||
|
||||
<cash.z.android.wallet.ui.util.vision.CameraSourcePreview
|
||||
android:id="@+id/firePreview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentTop="true">
|
||||
|
||||
<cash.z.android.wallet.ui.util.vision.GraphicOverlay
|
||||
android:id="@+id/graphic_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentBottom="true" />
|
||||
</cash.z.android.wallet.ui.util.vision.CameraSourcePreview>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="80dp"
|
||||
android:textColor="#FFF"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/control"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="60dp"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_toEndOf="@id/firePreview"
|
||||
android:layout_toRightOf="@id/firePreview"
|
||||
android:background="#000">
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
<ToggleButton
|
||||
android:id="@+id/facingSwitch"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:checked="false"
|
||||
android:textOff=""
|
||||
android:textOn="" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</RelativeLayout>
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<cash.z.android.cameraview.CameraView
|
||||
android:id="@+id/camera_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
</layout>
|
|
@ -0,0 +1,86 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/content_fragment_send"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/fragment_send_background">
|
||||
|
||||
<!-- -->
|
||||
<!-- Guidelines -->
|
||||
<!-- -->
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guideline_content_start"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_percent="0.1" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guideline_content_end"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_percent="0.9" />
|
||||
|
||||
<View
|
||||
android:id="@+id/background_header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:background="@color/zcashWhite"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/appbar"
|
||||
app:layout_constraintVertical_chainStyle="spread" />
|
||||
|
||||
<View
|
||||
android:id="@+id/transition_active_transaction_bg"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:background="@drawable/background_rounded_corners"
|
||||
android:backgroundTint="@color/zcashWhite_40"
|
||||
app:layout_constraintBottom_toTopOf="@id/input_zcash_address"
|
||||
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
|
||||
app:layout_constraintHeight_default="percent"
|
||||
app:layout_constraintHeight_percent="0.2"
|
||||
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
|
||||
app:layout_constraintTop_toBottomOf="@id/background_header"
|
||||
app:layout_constraintVertical_chainStyle="spread_inside" />
|
||||
|
||||
<cash.z.android.wallet.ui.util.vision.CameraSourcePreview
|
||||
android:id="@+id/preview_camera_source"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/transition_active_transaction_bg"
|
||||
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
|
||||
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
|
||||
app:layout_constraintTop_toTopOf="@id/transition_active_transaction_bg">
|
||||
|
||||
<cash.z.android.wallet.ui.util.vision.GraphicOverlay
|
||||
android:id="@+id/graphic_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentBottom="true" />
|
||||
</cash.z.android.wallet.ui.util.vision.CameraSourcePreview>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_amount_background"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:text="Scan"
|
||||
android:textColor="@color/text_dark"
|
||||
app:layout_constraintStart_toStartOf="@id/transition_active_transaction_bg"
|
||||
app:layout_constraintTop_toTopOf="@id/transition_active_transaction_bg" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
|
@ -8,9 +8,11 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true"
|
||||
android:isScrollContainer="true"
|
||||
tools:context=".ui.fragment.SendFragment">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/content_fragment_send"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/fragment_send_background">
|
||||
|
@ -71,6 +73,7 @@
|
|||
android:layout_height="0dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:background="@drawable/background_rounded_corners"
|
||||
android:backgroundTint="@color/zcashWhite_40"
|
||||
app:layout_constraintBottom_toTopOf="@id/input_zcash_address"
|
||||
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
|
||||
app:layout_constraintHeight_default="percent"
|
||||
|
@ -79,6 +82,15 @@
|
|||
app:layout_constraintTop_toBottomOf="@id/background_header"
|
||||
app:layout_constraintVertical_chainStyle="spread_inside" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/camera_placeholder"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toTopOf="@id/transition_active_transaction_bg"
|
||||
app:layout_constraintBottom_toBottomOf="@id/transition_active_transaction_bg"
|
||||
app:layout_constraintStart_toStartOf="@id/transition_active_transaction_bg"
|
||||
app:layout_constraintEnd_toEndOf="@id/transition_active_transaction_bg"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_amount_background"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -99,7 +111,6 @@
|
|||
android:layout_marginRight="28dp"
|
||||
android:backgroundTint="@color/zcashPrimaryMedium"
|
||||
android:foregroundTint="@color/colorAccent"
|
||||
android:tint="@color/zcashGray_light"
|
||||
app:layout_constraintBottom_toBottomOf="@id/transition_active_transaction_bg"
|
||||
app:layout_constraintEnd_toEndOf="@id/transition_active_transaction_bg"
|
||||
app:layout_constraintTop_toTopOf="@id/transition_active_transaction_bg"
|
||||
|
@ -125,11 +136,11 @@
|
|||
android:id="@+id/text_value_subheader"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="0"
|
||||
android:textColor="@color/text_dark"
|
||||
app:layout_constraintEnd_toEndOf="@id/text_value_header"
|
||||
app:layout_constraintStart_toStartOf="@id/text_value_header"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_value_header" />
|
||||
app:layout_constraintTop_toBottomOf="@id/text_value_header"
|
||||
tools:text="0 ZEC" />
|
||||
|
||||
<!-- Zec symbol : header -->
|
||||
<ImageView
|
||||
|
@ -178,17 +189,33 @@
|
|||
app:layout_constraintEnd_toStartOf="@id/image_zec_symbol_subheader"
|
||||
app:layout_constraintTop_toTopOf="@id/image_zec_symbol_subheader" />
|
||||
|
||||
<!-- Address -->
|
||||
<EditText
|
||||
android:id="@+id/input_zcash_address"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/send_hint_input_zcash_address"
|
||||
android:paddingRight="68dp"
|
||||
android:singleLine="true"
|
||||
app:backgroundTint="@color/zcashBlack_12"
|
||||
app:layout_constraintBottom_toTopOf="@id/text_area_memo"
|
||||
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="@id/guideline_content_start"
|
||||
app:layout_constraintTop_toBottomOf="@id/transition_active_transaction_bg" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_address_error"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:fontFamily="@font/inconsolata"
|
||||
android:textColor="@color/zcashRed"
|
||||
android:textSize="@dimen/text_size_caption"
|
||||
app:layout_constraintStart_toStartOf="@id/input_zcash_address"
|
||||
app:layout_constraintTop_toBottomOf="@id/input_zcash_address" />
|
||||
|
||||
<!-- Scan QR code -->
|
||||
<ImageView
|
||||
android:id="@+id/image_scan_qr"
|
||||
|
@ -201,6 +228,16 @@
|
|||
app:layout_constraintTop_toTopOf="@id/input_zcash_address"
|
||||
app:srcCompat="@drawable/ic_qrcode_24dp" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_address_shortcut"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginRight="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/image_scan_qr"
|
||||
app:layout_constraintEnd_toStartOf="@id/image_scan_qr"
|
||||
app:layout_constraintTop_toTopOf="@id/image_scan_qr"
|
||||
app:srcCompat="@drawable/ic_content_paste_black_24dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_to_label"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -209,16 +246,17 @@
|
|||
app:layout_constraintBottom_toTopOf="@id/input_zcash_address"
|
||||
app:layout_constraintStart_toStartOf="@id/input_zcash_address" />
|
||||
|
||||
<!-- memo -->
|
||||
<EditText
|
||||
android:id="@+id/text_area_memo"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@drawable/background_rounded_corners"
|
||||
android:backgroundTint="@color/zcashWhite_87"
|
||||
android:gravity="top|left"
|
||||
android:inputType="textMultiLine"
|
||||
android:maxLength="@integer/max_memo_length"
|
||||
android:maxLength="@integer/memo_max_length"
|
||||
android:padding="16dp"
|
||||
app:backgroundTint="@color/zcashWhite_60"
|
||||
app:layout_constraintBottom_toTopOf="@id/button_send_zec"
|
||||
app:layout_constraintEnd_toEndOf="@id/guideline_content_end"
|
||||
app:layout_constraintHeight_default="percent"
|
||||
|
@ -252,7 +290,7 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginBottom="40dp"
|
||||
android:background="@color/zcashGray_light"
|
||||
android:background="@color/zcashBlack_12"
|
||||
app:layout_constraintBottom_toBottomOf="@id/text_area_memo"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
@ -264,7 +302,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="0 / 512"
|
||||
android:textColor="@color/zcashGray_light"
|
||||
android:textColor="@color/zcashGray"
|
||||
app:layout_constraintBottom_toBottomOf="@id/text_area_memo"
|
||||
app:layout_constraintEnd_toEndOf="@id/divider_memo"
|
||||
app:layout_constraintTop_toBottomOf="@id/divider_memo" />
|
||||
|
@ -371,6 +409,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="@color/divider_background"
|
||||
app:layout_goneMarginTop="32dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/dialog_submit_button"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/dialog_text_memo_included" />
|
||||
|
@ -380,7 +419,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:background="@null"
|
||||
android:text="swipe right to send"
|
||||
android:text="send"
|
||||
android:textColor="@color/colorPrimary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
@ -410,7 +449,6 @@
|
|||
android:visibility="gone"
|
||||
app:constraint_referenced_ids="dialog_send_contents, dialog_send_background" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
||||
</layout>
|
|
@ -24,7 +24,11 @@
|
|||
android:id="@+id/nav_send_fragment"
|
||||
android:name="cash.z.android.wallet.ui.fragment.SendFragment"
|
||||
android:label="@string/destination_title_send"
|
||||
tools:layout="@layout/fragment_send" />
|
||||
tools:layout="@layout/fragment_send" >
|
||||
<action
|
||||
android:id="@+id/action_send_fragment_to_scan_fragment"
|
||||
app:destination="@+id/nav_scan_fragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/nav_receive_fragment"
|
||||
android:name="cash.z.android.wallet.ui.fragment.ReceiveFragment"
|
||||
|
@ -35,6 +39,11 @@
|
|||
android:name="cash.z.android.wallet.ui.fragment.RequestFragment"
|
||||
android:label="@string/destination_title_request"
|
||||
tools:layout="@layout/fragment_request" />
|
||||
<fragment
|
||||
android:id="@+id/nav_scan_fragment"
|
||||
android:name="cash.z.android.wallet.ui.fragment.ScanFragment"
|
||||
android:label="@string/destination_title_scan"
|
||||
tools:layout="@layout/fragment_scan" />
|
||||
|
||||
<!-- unimplemented -->
|
||||
<fragment
|
||||
|
|
|
@ -22,9 +22,13 @@
|
|||
<color name="zcashBlue">#2196F3</color>
|
||||
<color name="zcashBlueGray">#0D364C66</color>
|
||||
<color name="zcashWhite">#FFFFFF</color>
|
||||
<color name="zcashWhite_12">#1FFFFFFF</color>
|
||||
<color name="zcashWhite_40">#66FFFFFF</color>
|
||||
<color name="zcashWhite_60">#A3FFFFFF</color>
|
||||
<color name="zcashWhite_87">#BFFFFFFF</color>
|
||||
<color name="zcashWhite_light">#EDEDED</color>
|
||||
<color name="zcashGray_light">#CACACA</color>
|
||||
<color name="zcashGray">#9B9B9B</color>
|
||||
<color name="zcashBlack_light">#2B2B2B</color>
|
||||
<color name="zcashBlack_12">#1F000000</color>
|
||||
<color name="zcashBlack_40">#66000000</color>
|
||||
|
|
|
@ -2,5 +2,6 @@
|
|||
<resources>
|
||||
<integer name="nav_default_slow_transition_duration">400</integer>
|
||||
<integer name="nav_default_quick_transition_duration">100</integer>
|
||||
<integer name="max_memo_length">512</integer>
|
||||
<integer name="memo_max_length">512</integer>
|
||||
<integer name="z_address_min_length">78</integer>
|
||||
</resources>
|
|
@ -11,12 +11,15 @@
|
|||
<string name="cancel">cancel</string>
|
||||
<string name="cancelled">cancelled</string>
|
||||
<string name="ok_allcaps">OK</string>
|
||||
<string name="zec_abbreviation">ZECC</string>
|
||||
<string name="usd_abbreviation">USD</string>
|
||||
|
||||
<!-- Destinations -->
|
||||
<string name="destination_title_home" />
|
||||
<string name="destination_title_send">Send Zcash</string>
|
||||
<string name="destination_title_receive">Receive Zcash</string>
|
||||
<string name="destination_title_request">Request Zcash</string>
|
||||
<string name="destination_title_scan">Scan Address</string>
|
||||
<string name="destination_title_history">History</string>
|
||||
<string name="destination_title_about">About</string>
|
||||
<string name="destination_title_import">Import</string>
|
||||
|
@ -60,5 +63,11 @@
|
|||
<string name="send_label_to">To</string>
|
||||
<string name="send_label_memo">Memo (optional)</string>
|
||||
<string name="send_submit_button_text">Send Zec</string>
|
||||
<string name="send_tooltip_scan_qr">Scan QR Code</string>
|
||||
<string name="send_tooltip_address_shortcut">Paste Sample Address</string>
|
||||
<string name="send_subheader_value">0 %1$s</string>
|
||||
<string name="send_dialog_title">Send %1$s %2$s ($%3$s)?</string>
|
||||
<string name="send_alert_shortcut_clicked">Paste a valid sample address for testing?</string>
|
||||
<string name="send_error_address_too_short">Address is too short.</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
package cash.z.android.wallet.di.module
|
||||
|
||||
import cash.z.android.qrecycler.QScanner
|
||||
import cash.z.android.wallet.sample.SampleQrScanner
|
||||
import cash.z.wallet.sdk.data.MockSynchronizer
|
||||
import cash.z.wallet.sdk.data.Synchronizer
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Module that contributes all the objects necessary for the synchronizer, which is basically everything that has
|
||||
* application scope.
|
||||
*/
|
||||
@Module
|
||||
internal object SynchronizerModule {
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideQRScanner(): QScanner {
|
||||
// TODO: make an MLKit scanner
|
||||
return SampleQrScanner()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSynchronizer(): Synchronizer {
|
||||
return MockSynchronizer(
|
||||
transactionInterval = 60_000L,
|
||||
activeTransactionUpdateFrequency = 18_000L,
|
||||
isFirstRun = true
|
||||
)
|
||||
}
|
||||
}
|
|
@ -79,7 +79,7 @@ internal object SynchronizerModule {
|
|||
wallet: Wallet,
|
||||
twigger: Twig
|
||||
): Synchronizer {
|
||||
return Synchronizer(downloader, processor, repository, manager, wallet, logger = twigger)
|
||||
return SdkSynchronizer(downloader, processor, repository, manager, wallet, blockPollFrequency = 500_000L, logger = twigger)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<integer name="z_address_min_length">88</integer>
|
||||
</resources>
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="zec_abbreviation">TAZ</string>
|
||||
</resources>
|
|
@ -1,7 +1,7 @@
|
|||
buildscript {
|
||||
ext.buildConfig = [
|
||||
'compileSdkVersion': 28,
|
||||
'minSdkVersion': 16,
|
||||
'minSdkVersion': 21,
|
||||
'targetSdkVersion': 28
|
||||
]
|
||||
ext.versions = [
|
||||
|
@ -61,6 +61,7 @@ buildscript {
|
|||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}"
|
||||
classpath "com.github.ben-manes:gradle-versions-plugin:0.20.0"
|
||||
classpath 'com.deploygate:gradle:1.1.5'
|
||||
classpath 'com.google.gms:google-services:4.2.0'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -27,9 +27,8 @@ android {
|
|||
dependencies {
|
||||
implementation deps.androidx.coreKtx
|
||||
implementation deps.kotlin.stdlib
|
||||
|
||||
api 'com.google.firebase:firebase-ml-vision:19.0.2'
|
||||
implementation 'com.google.zxing:core:3.2.1'
|
||||
|
||||
implementation 'com.android.support:appcompat-v7:28.0.0'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||
|
|
|
@ -1,2 +1,5 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="cash.z.android.qrecycler" />
|
||||
package="cash.z.android.qrecycler" >
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
</manifest>
|
||||
|
|
|
@ -0,0 +1,510 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cash.z.android.cameraview
|
||||
|
||||
import cash.z.android.qrecycler.R
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import androidx.annotation.IntDef
|
||||
import androidx.annotation.NonNull
|
||||
import androidx.annotation.Nullable
|
||||
import androidx.core.os.ParcelableCompat
|
||||
import androidx.core.os.ParcelableCompatCreatorCallbacks
|
||||
import androidx.core.view.ViewCompat
|
||||
import cash.z.android.cameraview.api21.Camera2
|
||||
import cash.z.android.cameraview.base.AspectRatio
|
||||
import cash.z.android.cameraview.base.CameraViewImpl
|
||||
import cash.z.android.cameraview.base.Constants
|
||||
import cash.z.android.cameraview.base.PreviewImpl
|
||||
import com.google.android.cameraview.Camera2Api23
|
||||
import com.google.android.cameraview.TextureViewPreview
|
||||
import java.lang.IllegalStateException
|
||||
import java.lang.annotation.Retention
|
||||
import java.lang.annotation.RetentionPolicy
|
||||
import java.util.*
|
||||
|
||||
open class CameraView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
|
||||
FrameLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
@JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : this(context, attrs, 0)
|
||||
|
||||
internal lateinit var mImpl: CameraViewImpl
|
||||
|
||||
private var mCallbacks: CallbackBridge?
|
||||
|
||||
private var mAdjustViewBounds: Boolean = false
|
||||
|
||||
private var mDisplayOrientationDetector: DisplayOrientationDetector?
|
||||
|
||||
/**
|
||||
* @return `true` if the camera is opened.
|
||||
*/
|
||||
val isCameraOpened: Boolean
|
||||
get() = mImpl.isCameraOpened
|
||||
|
||||
/**
|
||||
* @return True when this CameraView is adjusting its bounds to preserve the aspect ratio of
|
||||
* camera.
|
||||
* @see .setAdjustViewBounds
|
||||
*/
|
||||
/**
|
||||
* @param adjustViewBounds `true` if you want the CameraView to adjust its bounds to
|
||||
* preserve the aspect ratio of camera.
|
||||
* @see .getAdjustViewBounds
|
||||
*/
|
||||
var adjustViewBounds: Boolean
|
||||
get() = mAdjustViewBounds
|
||||
set(adjustViewBounds) {
|
||||
if (mAdjustViewBounds != adjustViewBounds) {
|
||||
mAdjustViewBounds = adjustViewBounds
|
||||
requestLayout()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the direction that the current camera faces.
|
||||
*
|
||||
* @return The camera facing.
|
||||
*/
|
||||
/**
|
||||
* Chooses camera by the direction it faces.
|
||||
*
|
||||
* @param facing The camera facing. Must be either [.FACING_BACK] or
|
||||
* [.FACING_FRONT].
|
||||
*/
|
||||
var facing: Int
|
||||
@Facing
|
||||
get() = mImpl.facing
|
||||
set(@Facing facing) {
|
||||
mImpl.facing = facing
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the aspect ratios supported by the current camera.
|
||||
*/
|
||||
val supportedAspectRatios: Set<AspectRatio>
|
||||
get() = mImpl.supportedAspectRatios
|
||||
|
||||
/**
|
||||
* Gets the current aspect ratio of camera.
|
||||
*
|
||||
* @return The current [AspectRatio]. Can be `null` if no camera is opened yet.
|
||||
*/
|
||||
/**
|
||||
* Sets the aspect ratio of camera.
|
||||
*
|
||||
* @param ratio The [AspectRatio] to be set.
|
||||
*/
|
||||
var aspectRatio: AspectRatio?
|
||||
@Nullable
|
||||
get() = mImpl.aspectRatio
|
||||
set(@NonNull ratio) {
|
||||
if (mImpl.setAspectRatio(ratio)) {
|
||||
requestLayout()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the continuous auto-focus mode is enabled.
|
||||
*
|
||||
* @return `true` if the continuous auto-focus mode is enabled. `false` if it is
|
||||
* disabled, or if it is not supported by the current camera.
|
||||
*/
|
||||
/**
|
||||
* Enables or disables the continuous auto-focus mode. When the current camera doesn't support
|
||||
* auto-focus, calling this method will be ignored.
|
||||
*
|
||||
* @param autoFocus `true` to enable continuous auto-focus mode. `false` to
|
||||
* disable it.
|
||||
*/
|
||||
var autoFocus: Boolean
|
||||
get() = mImpl.autoFocus
|
||||
set(autoFocus) {
|
||||
mImpl.autoFocus = autoFocus
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current flash mode.
|
||||
*
|
||||
* @return The current flash mode.
|
||||
*/
|
||||
/**
|
||||
* Sets the flash mode.
|
||||
*
|
||||
* @param flash The desired flash mode.
|
||||
*/
|
||||
var flash: Int
|
||||
@Flash
|
||||
get() = mImpl.flash
|
||||
set(@Flash flash) {
|
||||
mImpl.flash = flash
|
||||
}
|
||||
|
||||
/** Direction the camera faces relative to device screen. */
|
||||
@IntDef(FACING_BACK, FACING_FRONT)
|
||||
@kotlin.annotation.Retention(AnnotationRetention.SOURCE)
|
||||
annotation class Facing
|
||||
|
||||
/** The mode for for the camera device's flash control */
|
||||
@IntDef(FLASH_OFF, FLASH_ON, FLASH_TORCH, FLASH_AUTO, FLASH_RED_EYE)
|
||||
annotation class Flash
|
||||
|
||||
init {
|
||||
if (isInEditMode) {
|
||||
mCallbacks = null
|
||||
mDisplayOrientationDetector = null
|
||||
} else {
|
||||
// Internal setup
|
||||
val preview = createPreviewImpl(context)
|
||||
mCallbacks = CallbackBridge()
|
||||
if (Build.VERSION.SDK_INT < 23) {
|
||||
mImpl = Camera2(mCallbacks!!, preview, context)
|
||||
} else {
|
||||
mImpl = Camera2Api23(mCallbacks!!, preview, context)
|
||||
}
|
||||
// Attributes
|
||||
val a = context.obtainStyledAttributes(
|
||||
attrs, R.styleable.CameraView, defStyleAttr,
|
||||
R.style.Widget_CameraView
|
||||
)
|
||||
mAdjustViewBounds = a.getBoolean(R.styleable.CameraView_android_adjustViewBounds, false)
|
||||
facing = a.getInt(R.styleable.CameraView_facing, FACING_BACK)
|
||||
var aspectRatioString = a.getString(R.styleable.CameraView_aspectRatio)
|
||||
if (aspectRatioString != null) {
|
||||
aspectRatio = AspectRatio.parse(aspectRatioString)
|
||||
} else {
|
||||
aspectRatio = Constants.DEFAULT_ASPECT_RATIO
|
||||
}
|
||||
autoFocus = a.getBoolean(R.styleable.CameraView_autoFocus, true)
|
||||
flash = a.getInt(R.styleable.CameraView_flash, Constants.FLASH_AUTO)
|
||||
a.recycle()
|
||||
// Display orientation detector
|
||||
mDisplayOrientationDetector = object : DisplayOrientationDetector(context) {
|
||||
override fun onDisplayOrientationChanged(displayOrientation: Int) {
|
||||
mImpl.setDisplayOrientation(displayOrientation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private fun createPreviewImpl(context: Context): PreviewImpl {
|
||||
return TextureViewPreview(context, this)
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
if (!isInEditMode) {
|
||||
mDisplayOrientationDetector!!.enable(ViewCompat.getDisplay(this)!!)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
if (!isInEditMode) {
|
||||
mDisplayOrientationDetector!!.disable()
|
||||
}
|
||||
super.onDetachedFromWindow()
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
if (isInEditMode) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
return
|
||||
}
|
||||
// Handle android:adjustViewBounds
|
||||
if (mAdjustViewBounds) {
|
||||
if (!isCameraOpened) {
|
||||
mCallbacks!!.reserveRequestLayoutOnOpen()
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
return
|
||||
}
|
||||
val widthMode = View.MeasureSpec.getMode(widthMeasureSpec)
|
||||
val heightMode = View.MeasureSpec.getMode(heightMeasureSpec)
|
||||
if (widthMode == View.MeasureSpec.EXACTLY && heightMode != View.MeasureSpec.EXACTLY) {
|
||||
val ratio = aspectRatio!!
|
||||
var height = (View.MeasureSpec.getSize(widthMeasureSpec) * ratio!!.toFloat()) as Int
|
||||
if (heightMode == View.MeasureSpec.AT_MOST) {
|
||||
height = Math.min(height, View.MeasureSpec.getSize(heightMeasureSpec))
|
||||
}
|
||||
super.onMeasure(
|
||||
widthMeasureSpec,
|
||||
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
|
||||
)
|
||||
} else if (widthMode != View.MeasureSpec.EXACTLY && heightMode == View.MeasureSpec.EXACTLY) {
|
||||
val ratio = aspectRatio!!
|
||||
var width = (View.MeasureSpec.getSize(heightMeasureSpec) * ratio!!.toFloat()) as Int
|
||||
if (widthMode == View.MeasureSpec.AT_MOST) {
|
||||
width = Math.min(width, View.MeasureSpec.getSize(widthMeasureSpec))
|
||||
}
|
||||
super.onMeasure(
|
||||
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
|
||||
heightMeasureSpec
|
||||
)
|
||||
} else {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
}
|
||||
} else {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
}
|
||||
// Measure the TextureView
|
||||
val width = measuredWidth
|
||||
val height = measuredHeight
|
||||
var ratio = aspectRatio
|
||||
if (mDisplayOrientationDetector!!.lastKnownDisplayOrientation % 180 == 0) {
|
||||
ratio = ratio!!.inverse()
|
||||
}
|
||||
assert(ratio != null)
|
||||
if (height < width * ratio!!.y / ratio!!.x) {
|
||||
mImpl.view.measure(
|
||||
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
|
||||
View.MeasureSpec.makeMeasureSpec(
|
||||
width * ratio!!.y / ratio!!.x,
|
||||
View.MeasureSpec.EXACTLY
|
||||
)
|
||||
)
|
||||
} else {
|
||||
mImpl.view.measure(
|
||||
View.MeasureSpec.makeMeasureSpec(
|
||||
height * ratio!!.x / ratio!!.y,
|
||||
View.MeasureSpec.EXACTLY
|
||||
),
|
||||
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(): Parcelable? {
|
||||
val state = SavedState(super.onSaveInstanceState())
|
||||
state.facing = facing
|
||||
state.ratio = aspectRatio
|
||||
state.autoFocus = autoFocus
|
||||
state.flash = flash
|
||||
return state
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(state: Parcelable?) {
|
||||
if (state !is SavedState) {
|
||||
super.onRestoreInstanceState(state)
|
||||
return
|
||||
}
|
||||
val ss = state as SavedState?
|
||||
super.onRestoreInstanceState(ss!!.getSuperState())
|
||||
facing = ss.facing
|
||||
aspectRatio = ss.ratio
|
||||
autoFocus = ss.autoFocus
|
||||
flash = ss.flash
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a camera device and start showing camera preview. This is typically called from
|
||||
* [Activity.onResume].
|
||||
*/
|
||||
fun start() {
|
||||
if (!mImpl.start()) {
|
||||
throw IllegalStateException("failed to start even though we're on API 21+")
|
||||
// //store the state ,and restore this state after fall back o Camera1
|
||||
// val state = onSaveInstanceState()
|
||||
// // Camera2 uses legacy hardware layer; fall back to Camera1
|
||||
// mImpl = Camera1(mCallbacks, createPreviewImpl(context))
|
||||
// onRestoreInstanceState(state)
|
||||
// mImpl.start()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop camera preview and close the device. This is typically called from
|
||||
* [Activity.onPause].
|
||||
*/
|
||||
fun stop() {
|
||||
mImpl.stop()
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new callback.
|
||||
*
|
||||
* @param callback The [Callback] to add.
|
||||
* @see .removeCallback
|
||||
*/
|
||||
fun addCallback(@NonNull callback: Callback) {
|
||||
mCallbacks!!.add(callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a callback.
|
||||
*
|
||||
* @param callback The [Callback] to remove.
|
||||
* @see .addCallback
|
||||
*/
|
||||
fun removeCallback(@NonNull callback: Callback) {
|
||||
mCallbacks!!.remove(callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a picture. The result will be returned to
|
||||
* [Callback.onPictureTaken].
|
||||
*/
|
||||
fun takePicture() {
|
||||
mImpl.takePicture()
|
||||
}
|
||||
|
||||
private inner class CallbackBridge internal constructor() : CameraViewImpl.Callback {
|
||||
|
||||
private val mCallbacks = ArrayList<Callback>()
|
||||
|
||||
private var mRequestLayoutOnOpen: Boolean = false
|
||||
|
||||
fun add(callback: Callback) {
|
||||
mCallbacks.add(callback)
|
||||
}
|
||||
|
||||
fun remove(callback: Callback) {
|
||||
mCallbacks.remove(callback)
|
||||
}
|
||||
|
||||
override fun onCameraOpened() {
|
||||
if (mRequestLayoutOnOpen) {
|
||||
mRequestLayoutOnOpen = false
|
||||
requestLayout()
|
||||
}
|
||||
for (callback in mCallbacks) {
|
||||
callback.onCameraOpened(this@CameraView)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCameraClosed() {
|
||||
for (callback in mCallbacks) {
|
||||
callback.onCameraClosed(this@CameraView)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPictureTaken(data: ByteArray) {
|
||||
for (callback in mCallbacks) {
|
||||
callback.onPictureTaken(this@CameraView, data)
|
||||
}
|
||||
}
|
||||
|
||||
fun reserveRequestLayoutOnOpen() {
|
||||
mRequestLayoutOnOpen = true
|
||||
}
|
||||
}
|
||||
|
||||
protected class SavedState : View.BaseSavedState {
|
||||
|
||||
@Facing
|
||||
internal var facing: Int = 0
|
||||
|
||||
internal var ratio: AspectRatio? = null
|
||||
|
||||
internal var autoFocus: Boolean = false
|
||||
|
||||
@Flash
|
||||
internal var flash: Int = 0
|
||||
|
||||
constructor(source: Parcel) : this(source, AspectRatio::class.java.classLoader!!)
|
||||
|
||||
constructor(source: Parcel, loader: ClassLoader) : super(source) {
|
||||
facing = source.readInt()
|
||||
ratio = source.readParcelable(loader)
|
||||
autoFocus = source.readByte().toInt() != 0
|
||||
flash = source.readInt()
|
||||
}
|
||||
|
||||
constructor(parcelable: Parcelable) : super(parcelable)
|
||||
|
||||
override fun writeToParcel(out: Parcel, flags: Int) {
|
||||
super.writeToParcel(out, flags)
|
||||
out.writeInt(facing)
|
||||
out.writeParcelable(ratio, 0)
|
||||
out.writeByte((if (autoFocus) 1 else 0).toByte())
|
||||
out.writeInt(flash)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<SavedState> {
|
||||
override fun createFromParcel(parcel: Parcel): SavedState {
|
||||
return SavedState(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<SavedState?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for monitoring events about [CameraView].
|
||||
*/
|
||||
abstract class Callback {
|
||||
|
||||
/**
|
||||
* Called when camera is opened.
|
||||
*
|
||||
* @param cameraView The associated [CameraView].
|
||||
*/
|
||||
fun onCameraOpened(cameraView: CameraView) {}
|
||||
|
||||
/**
|
||||
* Called when camera is closed.
|
||||
*
|
||||
* @param cameraView The associated [CameraView].
|
||||
*/
|
||||
fun onCameraClosed(cameraView: CameraView) {}
|
||||
|
||||
/**
|
||||
* Called when a picture is taken.
|
||||
*
|
||||
* @param cameraView The associated [CameraView].
|
||||
* @param data JPEG data.
|
||||
*/
|
||||
fun onPictureTaken(cameraView: CameraView, data: ByteArray) {}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
/** The camera device faces the opposite direction as the device's screen. */
|
||||
const val FACING_BACK = Constants.FACING_BACK
|
||||
|
||||
/** The camera device faces the same direction as the device's screen. */
|
||||
const val FACING_FRONT = Constants.FACING_FRONT
|
||||
|
||||
/** Flash will not be fired. */
|
||||
const val FLASH_OFF = Constants.FLASH_OFF
|
||||
|
||||
/** Flash will always be fired during snapshot. */
|
||||
const val FLASH_ON = Constants.FLASH_ON
|
||||
|
||||
/** Constant emission of light during preview, auto-focus and snapshot. */
|
||||
const val FLASH_TORCH = Constants.FLASH_TORCH
|
||||
|
||||
/** Flash will be fired automatically when required. */
|
||||
const val FLASH_AUTO = Constants.FLASH_AUTO
|
||||
|
||||
/** Flash will be fired in red-eye reduction mode. */
|
||||
const val FLASH_RED_EYE = Constants.FLASH_RED_EYE
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cash.z.android.cameraview
|
||||
|
||||
import android.content.Context
|
||||
import android.util.SparseIntArray
|
||||
import android.view.Display
|
||||
import android.view.OrientationEventListener
|
||||
import android.view.Surface
|
||||
|
||||
|
||||
/**
|
||||
* Monitors the value returned from [Display.getRotation].
|
||||
*/
|
||||
internal abstract class DisplayOrientationDetector(context: Context) {
|
||||
|
||||
private val mOrientationEventListener: OrientationEventListener
|
||||
|
||||
var mDisplay: Display? = null
|
||||
|
||||
var lastKnownDisplayOrientation = 0
|
||||
private set
|
||||
|
||||
init {
|
||||
mOrientationEventListener = object : OrientationEventListener(context) {
|
||||
|
||||
/** This is either Surface.Rotation_0, _90, _180, _270, or -1 (invalid). */
|
||||
private var mLastKnownRotation = -1
|
||||
|
||||
override fun onOrientationChanged(orientation: Int) {
|
||||
if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN || mDisplay == null) {
|
||||
return
|
||||
}
|
||||
val rotation = mDisplay!!.rotation
|
||||
if (mLastKnownRotation != rotation) {
|
||||
mLastKnownRotation = rotation
|
||||
dispatchOnDisplayOrientationChanged(DISPLAY_ORIENTATIONS.get(rotation))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun enable(display: Display) {
|
||||
mDisplay = display
|
||||
mOrientationEventListener.enable()
|
||||
// Immediately dispatch the first callback
|
||||
dispatchOnDisplayOrientationChanged(DISPLAY_ORIENTATIONS.get(display.rotation))
|
||||
}
|
||||
|
||||
fun disable() {
|
||||
mOrientationEventListener.disable()
|
||||
mDisplay = null
|
||||
}
|
||||
|
||||
fun dispatchOnDisplayOrientationChanged(displayOrientation: Int) {
|
||||
lastKnownDisplayOrientation = displayOrientation
|
||||
onDisplayOrientationChanged(displayOrientation)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when display orientation is changed.
|
||||
*
|
||||
* @param displayOrientation One of 0, 90, 180, and 270.
|
||||
*/
|
||||
abstract fun onDisplayOrientationChanged(displayOrientation: Int)
|
||||
|
||||
companion object {
|
||||
|
||||
/** Mapping from Surface.Rotation_n to degrees. */
|
||||
val DISPLAY_ORIENTATIONS = SparseIntArray()
|
||||
|
||||
init {
|
||||
DISPLAY_ORIENTATIONS.put(Surface.ROTATION_0, 0)
|
||||
DISPLAY_ORIENTATIONS.put(Surface.ROTATION_90, 90)
|
||||
DISPLAY_ORIENTATIONS.put(Surface.ROTATION_180, 180)
|
||||
DISPLAY_ORIENTATIONS.put(Surface.ROTATION_270, 270)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.cameraview
|
||||
|
||||
|
||||
import cash.z.android.qrecycler.R
|
||||
import android.annotation.TargetApi
|
||||
import android.content.Context
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.SurfaceTexture
|
||||
import android.view.Surface
|
||||
import android.view.TextureView
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import cash.z.android.cameraview.base.PreviewImpl
|
||||
|
||||
@TargetApi(14)
|
||||
internal class TextureViewPreview(context: Context, parent: ViewGroup) : PreviewImpl() {
|
||||
|
||||
private val mTextureView: TextureView
|
||||
|
||||
private var mDisplayOrientation: Int = 0
|
||||
|
||||
override val surface: Surface
|
||||
get() = Surface(mTextureView.surfaceTexture)
|
||||
|
||||
override val surfaceTexture: SurfaceTexture
|
||||
get() = mTextureView.surfaceTexture
|
||||
|
||||
override val view: View
|
||||
get() = mTextureView
|
||||
|
||||
override val outputClass: Class<*>
|
||||
get() = SurfaceTexture::class.java
|
||||
|
||||
override val isReady: Boolean
|
||||
get() = mTextureView.surfaceTexture != null
|
||||
|
||||
init {
|
||||
val view = View.inflate(context, R.layout.texture_view, parent)
|
||||
mTextureView = view.findViewById(R.id.texture_view) as TextureView
|
||||
mTextureView.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
|
||||
|
||||
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
|
||||
setSize(width, height)
|
||||
configureTransform()
|
||||
dispatchSurfaceChanged()
|
||||
}
|
||||
|
||||
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
|
||||
setSize(width, height)
|
||||
configureTransform()
|
||||
dispatchSurfaceChanged()
|
||||
}
|
||||
|
||||
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
|
||||
setSize(0, 0)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {}
|
||||
}
|
||||
}
|
||||
|
||||
// This method is called only from Camera2.
|
||||
@TargetApi(15)
|
||||
override fun setBufferSize(width: Int, height: Int) {
|
||||
mTextureView.surfaceTexture.setDefaultBufferSize(width, height)
|
||||
}
|
||||
|
||||
override fun setDisplayOrientation(displayOrientation: Int) {
|
||||
mDisplayOrientation = displayOrientation
|
||||
configureTransform()
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the transform matrix for TextureView based on [.mDisplayOrientation] and
|
||||
* the surface size.
|
||||
*/
|
||||
fun configureTransform() {
|
||||
val matrix = Matrix()
|
||||
if (mDisplayOrientation % 180 == 90) {
|
||||
val width = width.toFloat()
|
||||
val height = height.toFloat()
|
||||
// Rotate the camera preview when the screen is landscape.
|
||||
matrix.setPolyToPoly(
|
||||
floatArrayOf(
|
||||
0f, 0f, // top left
|
||||
width, 0f, // top right
|
||||
0f, height, // bottom left
|
||||
width, height
|
||||
)// bottom right
|
||||
, 0,
|
||||
if (mDisplayOrientation == 90)
|
||||
// Clockwise
|
||||
floatArrayOf(
|
||||
0f, height, // top left
|
||||
0f, 0f, // top right
|
||||
width, height, // bottom left
|
||||
width, 0f
|
||||
)// bottom right
|
||||
else
|
||||
// mDisplayOrientation == 270
|
||||
// Counter-clockwise
|
||||
floatArrayOf(
|
||||
width, 0f, // top left
|
||||
width, height, // top right
|
||||
0f, 0f, // bottom left
|
||||
0f, height
|
||||
)// bottom right
|
||||
, 0,
|
||||
4
|
||||
)
|
||||
} else if (mDisplayOrientation == 180) {
|
||||
matrix.postRotate(180f, width / 2f, height / 2f)
|
||||
}
|
||||
mTextureView.setTransform(matrix)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,788 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cash.z.android.cameraview.api21
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.TargetApi
|
||||
import android.content.Context
|
||||
import android.graphics.ImageFormat
|
||||
import android.hardware.camera2.*
|
||||
import android.hardware.camera2.params.StreamConfigurationMap
|
||||
import android.media.ImageReader
|
||||
import android.util.Log
|
||||
import android.util.SparseIntArray
|
||||
import androidx.annotation.NonNull
|
||||
import androidx.annotation.RequiresPermission
|
||||
import cash.z.android.cameraview.base.*
|
||||
import java.util.*
|
||||
|
||||
@TargetApi(21)
|
||||
internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewImpl, context: Context) : CameraViewImpl(callback, preview) {
|
||||
|
||||
private val mCameraManager: CameraManager
|
||||
|
||||
private val mCameraDeviceCallback = object : CameraDevice.StateCallback() {
|
||||
|
||||
override fun onOpened(@NonNull camera: CameraDevice) {
|
||||
mCamera = camera
|
||||
mCallback.onCameraOpened()
|
||||
startCaptureSession()
|
||||
}
|
||||
|
||||
override fun onClosed(@NonNull camera: CameraDevice) {
|
||||
mCallback.onCameraClosed()
|
||||
}
|
||||
|
||||
override fun onDisconnected(@NonNull camera: CameraDevice) {
|
||||
mCamera = null
|
||||
}
|
||||
|
||||
override fun onError(@NonNull camera: CameraDevice, error: Int) {
|
||||
Log.e(TAG, "onError: " + camera.id + " (" + error + ")")
|
||||
mCamera = null
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private val mSessionCallback = object : CameraCaptureSession.StateCallback() {
|
||||
|
||||
override fun onConfigured(@NonNull session: CameraCaptureSession) {
|
||||
if (mCamera == null) {
|
||||
return
|
||||
}
|
||||
mCaptureSession = session
|
||||
updateAutoFocus()
|
||||
updateFlash()
|
||||
try {
|
||||
mCaptureSession!!.setRepeatingRequest(
|
||||
mPreviewRequestBuilder!!.build(),
|
||||
mCaptureCallback, null
|
||||
)
|
||||
} catch (e: CameraAccessException) {
|
||||
Log.e(TAG, "Failed to start camera preview because it couldn't access camera", e)
|
||||
} catch (e: IllegalStateException) {
|
||||
Log.e(TAG, "Failed to start camera preview.", e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onConfigureFailed(@NonNull session: CameraCaptureSession) {
|
||||
Log.e(TAG, "Failed to configure capture session.")
|
||||
}
|
||||
|
||||
override fun onClosed(@NonNull session: CameraCaptureSession) {
|
||||
if (mCaptureSession != null && mCaptureSession == session) {
|
||||
mCaptureSession = null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private var mCaptureCallback: PictureCaptureCallback = object : PictureCaptureCallback() {
|
||||
|
||||
override fun onPrecaptureRequired() {
|
||||
mPreviewRequestBuilder!!.set(
|
||||
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
|
||||
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START
|
||||
)
|
||||
setState(Camera2.PictureCaptureCallback.STATE_PRECAPTURE)
|
||||
try {
|
||||
mCaptureSession!!.capture(mPreviewRequestBuilder!!.build(), this, null)
|
||||
mPreviewRequestBuilder!!.set(
|
||||
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
|
||||
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE
|
||||
)
|
||||
} catch (e: CameraAccessException) {
|
||||
Log.e(TAG, "Failed to run precapture sequence.", e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onReady() {
|
||||
captureStillPicture()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private val mOnImageAvailableListener = ImageReader.OnImageAvailableListener { reader ->
|
||||
reader.acquireNextImage().use { image ->
|
||||
val planes = image.planes
|
||||
if (planes.size > 0) {
|
||||
val buffer = planes[0].buffer
|
||||
val data = ByteArray(buffer.remaining())
|
||||
buffer.get(data)
|
||||
mCallback.onPictureTaken(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var mCameraId: String? = null
|
||||
|
||||
private var mCameraCharacteristics: CameraCharacteristics? = null
|
||||
|
||||
var mCamera: CameraDevice? = null
|
||||
|
||||
var mCaptureSession: CameraCaptureSession? = null
|
||||
|
||||
var mPreviewRequestBuilder: CaptureRequest.Builder? = null
|
||||
|
||||
private var mImageReader: ImageReader? = null
|
||||
|
||||
private val mPreviewSizes = SizeMap()
|
||||
|
||||
private val mPictureSizes = SizeMap()
|
||||
|
||||
private var mFacing: Int = 0
|
||||
|
||||
private var mAspectRatio = Constants.DEFAULT_ASPECT_RATIO
|
||||
|
||||
private var mAutoFocus: Boolean = false
|
||||
|
||||
// Revert
|
||||
override var flash: Int = 0
|
||||
set(flash) {
|
||||
if (this.flash == flash) {
|
||||
return
|
||||
}
|
||||
val saved = this.flash
|
||||
field = flash
|
||||
if (mPreviewRequestBuilder != null) {
|
||||
updateFlash()
|
||||
if (mCaptureSession != null) {
|
||||
try {
|
||||
mCaptureSession!!.setRepeatingRequest(
|
||||
mPreviewRequestBuilder!!.build(),
|
||||
mCaptureCallback, null
|
||||
)
|
||||
} catch (e: CameraAccessException) {
|
||||
field = saved
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var mDisplayOrientation: Int = 0
|
||||
|
||||
override val isCameraOpened: Boolean
|
||||
get() = mCamera != null
|
||||
|
||||
override var facing: Int
|
||||
get() = mFacing
|
||||
@RequiresPermission(Manifest.permission.CAMERA) set(facing) {
|
||||
if (mFacing == facing) {
|
||||
return
|
||||
}
|
||||
mFacing = facing
|
||||
if (isCameraOpened) {
|
||||
stop()
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
override val supportedAspectRatios: Set<AspectRatio>
|
||||
get() = mPreviewSizes.ratios()
|
||||
|
||||
// Revert
|
||||
override var autoFocus: Boolean
|
||||
get() = mAutoFocus
|
||||
set(autoFocus) {
|
||||
if (mAutoFocus == autoFocus) {
|
||||
return
|
||||
}
|
||||
mAutoFocus = autoFocus
|
||||
if (mPreviewRequestBuilder != null) {
|
||||
updateAutoFocus()
|
||||
if (mCaptureSession != null) {
|
||||
try {
|
||||
mCaptureSession!!.setRepeatingRequest(
|
||||
mPreviewRequestBuilder!!.build(),
|
||||
mCaptureCallback, null
|
||||
)
|
||||
} catch (e: CameraAccessException) {
|
||||
mAutoFocus = !mAutoFocus
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
mCameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
|
||||
mPreview.setCallback(object : PreviewImpl.Callback {
|
||||
override fun onSurfaceChanged() {
|
||||
startCaptureSession()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@RequiresPermission(Manifest.permission.CAMERA)
|
||||
override fun start(): Boolean {
|
||||
if (!chooseCameraIdByFacing()) {
|
||||
return false
|
||||
}
|
||||
collectCameraInfo()
|
||||
prepareImageReader()
|
||||
startOpeningCamera()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
if (mCaptureSession != null) {
|
||||
mCaptureSession!!.close()
|
||||
mCaptureSession = null
|
||||
}
|
||||
if (mCamera != null) {
|
||||
mCamera!!.close()
|
||||
mCamera = null
|
||||
}
|
||||
if (mImageReader != null) {
|
||||
mImageReader!!.close()
|
||||
mImageReader = null
|
||||
}
|
||||
}
|
||||
|
||||
override val aspectRatio: AspectRatio get() = mAspectRatio
|
||||
|
||||
override fun setAspectRatio(ratio: AspectRatio?): Boolean {
|
||||
if (ratio == null || ratio == mAspectRatio ||
|
||||
!mPreviewSizes.ratios().contains(ratio)
|
||||
) {
|
||||
// TODO: Better error handling
|
||||
return false
|
||||
}
|
||||
mAspectRatio = ratio
|
||||
prepareImageReader()
|
||||
if (mCaptureSession != null) {
|
||||
mCaptureSession!!.close()
|
||||
mCaptureSession = null
|
||||
startCaptureSession()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun takePicture() {
|
||||
if (mAutoFocus) {
|
||||
lockFocus()
|
||||
} else {
|
||||
captureStillPicture()
|
||||
}
|
||||
}
|
||||
|
||||
override fun setDisplayOrientation(displayOrientation: Int) {
|
||||
mDisplayOrientation = displayOrientation
|
||||
mPreview.setDisplayOrientation(mDisplayOrientation)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Chooses a camera ID by the specified camera facing ([.mFacing]).
|
||||
*
|
||||
* This rewrites [.mCameraId], [.mCameraCharacteristics], and optionally
|
||||
* [.mFacing].
|
||||
*/
|
||||
private fun chooseCameraIdByFacing(): Boolean {
|
||||
try {
|
||||
val internalFacing = INTERNAL_FACINGS.get(mFacing)
|
||||
val ids = mCameraManager.cameraIdList
|
||||
if (ids.size == 0) { // No camera
|
||||
throw RuntimeException("No camera available.")
|
||||
}
|
||||
for (id in ids) {
|
||||
val characteristics = mCameraManager.getCameraCharacteristics(id)
|
||||
val level = characteristics.get(
|
||||
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL
|
||||
)
|
||||
if (level == null || level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
|
||||
continue
|
||||
}
|
||||
val internal = characteristics.get(CameraCharacteristics.LENS_FACING)
|
||||
?: throw NullPointerException("Unexpected state: LENS_FACING null")
|
||||
if (internal == internalFacing) {
|
||||
mCameraId = id
|
||||
mCameraCharacteristics = characteristics
|
||||
return true
|
||||
}
|
||||
}
|
||||
// Not found
|
||||
mCameraId = ids[0]
|
||||
mCameraCharacteristics = mCameraManager.getCameraCharacteristics(mCameraId!!)
|
||||
val level = mCameraCharacteristics!!.get(
|
||||
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL
|
||||
)
|
||||
if (level == null || level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
|
||||
return false
|
||||
}
|
||||
val internal = mCameraCharacteristics!!.get(CameraCharacteristics.LENS_FACING)
|
||||
?: throw NullPointerException("Unexpected state: LENS_FACING null")
|
||||
var i = 0
|
||||
val count = INTERNAL_FACINGS.size()
|
||||
while (i < count) {
|
||||
if (INTERNAL_FACINGS.valueAt(i) == internal) {
|
||||
mFacing = INTERNAL_FACINGS.keyAt(i)
|
||||
return true
|
||||
}
|
||||
i++
|
||||
}
|
||||
// The operation can reach here when the only camera device is an external one.
|
||||
// We treat it as facing back.
|
||||
mFacing = Constants.FACING_BACK
|
||||
return true
|
||||
} catch (e: CameraAccessException) {
|
||||
throw RuntimeException("Failed to get a list of camera devices", e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Collects some information from [.mCameraCharacteristics].
|
||||
*
|
||||
* This rewrites [.mPreviewSizes], [.mPictureSizes], and optionally,
|
||||
* [.mAspectRatio].
|
||||
*/
|
||||
private fun collectCameraInfo() {
|
||||
val map = mCameraCharacteristics!!.get(
|
||||
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
|
||||
) ?: throw IllegalStateException("Failed to get configuration map: " + mCameraId!!)
|
||||
mPreviewSizes.clear()
|
||||
for (size in map.getOutputSizes(mPreview.outputClass)) {
|
||||
val width = size.width
|
||||
val height = size.height
|
||||
if (width <= MAX_PREVIEW_WIDTH && height <= MAX_PREVIEW_HEIGHT) {
|
||||
mPreviewSizes.add(Size(width, height))
|
||||
}
|
||||
}
|
||||
mPictureSizes.clear()
|
||||
collectPictureSizes(mPictureSizes, map)
|
||||
for (ratio in mPreviewSizes.ratios()) {
|
||||
if (!mPictureSizes.ratios().contains(ratio)) {
|
||||
mPreviewSizes.remove(ratio)
|
||||
}
|
||||
}
|
||||
|
||||
if (!mPreviewSizes.ratios().contains(mAspectRatio)) {
|
||||
mAspectRatio = mPreviewSizes.ratios().iterator().next()
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun collectPictureSizes(sizes: SizeMap, map: StreamConfigurationMap) {
|
||||
val outputSizes = map.getOutputSizes(ImageFormat.JPEG)
|
||||
for (size in outputSizes) {
|
||||
mPictureSizes.add(Size(size.width, size.height))
|
||||
}
|
||||
}
|
||||
|
||||
private fun prepareImageReader() {
|
||||
if (mImageReader != null) {
|
||||
mImageReader!!.close()
|
||||
}
|
||||
val largest = mPictureSizes.sizes(mAspectRatio).last()
|
||||
mImageReader = ImageReader.newInstance(
|
||||
largest.width, largest.height,
|
||||
ImageFormat.JPEG, /* maxImages */ 2
|
||||
)
|
||||
mImageReader!!.setOnImageAvailableListener(mOnImageAvailableListener, null)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Starts opening a camera device.
|
||||
*
|
||||
* The result will be processed in [.mCameraDeviceCallback].
|
||||
*/
|
||||
@RequiresPermission(Manifest.permission.CAMERA)
|
||||
private fun startOpeningCamera() {
|
||||
try {
|
||||
mCameraManager.openCamera(mCameraId!!, mCameraDeviceCallback, null)
|
||||
} catch (e: CameraAccessException) {
|
||||
throw RuntimeException("Failed to open camera: " + mCameraId!!, e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Starts a capture session for camera preview.
|
||||
*
|
||||
* This rewrites [.mPreviewRequestBuilder].
|
||||
*
|
||||
* The result will be continuously processed in [.mSessionCallback].
|
||||
*/
|
||||
fun startCaptureSession() {
|
||||
if (!isCameraOpened || !mPreview.isReady || mImageReader == null) {
|
||||
return
|
||||
}
|
||||
val previewSize = chooseOptimalSize()
|
||||
mPreview.setBufferSize(previewSize.width, previewSize.height)
|
||||
val surface = mPreview.surface
|
||||
try {
|
||||
mPreviewRequestBuilder = mCamera!!.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
|
||||
mPreviewRequestBuilder!!.addTarget(surface)
|
||||
mCamera!!.createCaptureSession(
|
||||
Arrays.asList(surface, mImageReader!!.surface),
|
||||
mSessionCallback, null
|
||||
)
|
||||
} catch (e: CameraAccessException) {
|
||||
throw RuntimeException("Failed to start camera session")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Chooses the optimal preview size based on [.mPreviewSizes] and the surface size.
|
||||
*
|
||||
* @return The picked size for camera preview.
|
||||
*/
|
||||
private fun chooseOptimalSize(): Size {
|
||||
val surfaceLonger: Int
|
||||
val surfaceShorter: Int
|
||||
val surfaceWidth = mPreview.width
|
||||
val surfaceHeight = mPreview.height
|
||||
if (surfaceWidth < surfaceHeight) {
|
||||
surfaceLonger = surfaceHeight
|
||||
surfaceShorter = surfaceWidth
|
||||
} else {
|
||||
surfaceLonger = surfaceWidth
|
||||
surfaceShorter = surfaceHeight
|
||||
}
|
||||
val candidates = mPreviewSizes.sizes(mAspectRatio)
|
||||
|
||||
// Pick the smallest of those big enough
|
||||
for (size in candidates) {
|
||||
if (size.width >= surfaceLonger && size.height >= surfaceShorter) {
|
||||
return size
|
||||
}
|
||||
}
|
||||
// If no size is big enough, pick the largest one.
|
||||
return candidates.last()
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the internal state of auto-focus to [.mAutoFocus].
|
||||
*/
|
||||
fun updateAutoFocus() {
|
||||
if (mAutoFocus) {
|
||||
val modes = mCameraCharacteristics!!.get(
|
||||
CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES
|
||||
)
|
||||
// Auto focus is not supported
|
||||
if (modes == null || modes.size == 0 ||
|
||||
modes.size == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF
|
||||
) {
|
||||
mAutoFocus = false
|
||||
mPreviewRequestBuilder!!.set(
|
||||
CaptureRequest.CONTROL_AF_MODE,
|
||||
CaptureRequest.CONTROL_AF_MODE_OFF
|
||||
)
|
||||
} else {
|
||||
mPreviewRequestBuilder!!.set(
|
||||
CaptureRequest.CONTROL_AF_MODE,
|
||||
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE
|
||||
)
|
||||
}
|
||||
} else {
|
||||
mPreviewRequestBuilder!!.set(
|
||||
CaptureRequest.CONTROL_AF_MODE,
|
||||
CaptureRequest.CONTROL_AF_MODE_OFF
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the internal state of flash to [.mFlash].
|
||||
*/
|
||||
fun updateFlash() {
|
||||
when (flash) {
|
||||
Constants.FLASH_OFF -> {
|
||||
mPreviewRequestBuilder!!.set(
|
||||
CaptureRequest.CONTROL_AE_MODE,
|
||||
CaptureRequest.CONTROL_AE_MODE_ON
|
||||
)
|
||||
mPreviewRequestBuilder!!.set(
|
||||
CaptureRequest.FLASH_MODE,
|
||||
CaptureRequest.FLASH_MODE_OFF
|
||||
)
|
||||
}
|
||||
Constants.FLASH_ON -> {
|
||||
mPreviewRequestBuilder!!.set(
|
||||
CaptureRequest.CONTROL_AE_MODE,
|
||||
CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH
|
||||
)
|
||||
mPreviewRequestBuilder!!.set(
|
||||
CaptureRequest.FLASH_MODE,
|
||||
CaptureRequest.FLASH_MODE_OFF
|
||||
)
|
||||
}
|
||||
Constants.FLASH_TORCH -> {
|
||||
mPreviewRequestBuilder!!.set(
|
||||
CaptureRequest.CONTROL_AE_MODE,
|
||||
CaptureRequest.CONTROL_AE_MODE_ON
|
||||
)
|
||||
mPreviewRequestBuilder!!.set(
|
||||
CaptureRequest.FLASH_MODE,
|
||||
CaptureRequest.FLASH_MODE_TORCH
|
||||
)
|
||||
}
|
||||
Constants.FLASH_AUTO -> {
|
||||
mPreviewRequestBuilder!!.set(
|
||||
CaptureRequest.CONTROL_AE_MODE,
|
||||
CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH
|
||||
)
|
||||
mPreviewRequestBuilder!!.set(
|
||||
CaptureRequest.FLASH_MODE,
|
||||
CaptureRequest.FLASH_MODE_OFF
|
||||
)
|
||||
}
|
||||
Constants.FLASH_RED_EYE -> {
|
||||
mPreviewRequestBuilder!!.set(
|
||||
CaptureRequest.CONTROL_AE_MODE,
|
||||
CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE
|
||||
)
|
||||
mPreviewRequestBuilder!!.set(
|
||||
CaptureRequest.FLASH_MODE,
|
||||
CaptureRequest.FLASH_MODE_OFF
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks the focus as the first step for a still image capture.
|
||||
*/
|
||||
private fun lockFocus() {
|
||||
mPreviewRequestBuilder!!.set(
|
||||
CaptureRequest.CONTROL_AF_TRIGGER,
|
||||
CaptureRequest.CONTROL_AF_TRIGGER_START
|
||||
)
|
||||
try {
|
||||
mCaptureCallback.setState(PictureCaptureCallback.STATE_LOCKING)
|
||||
mCaptureSession!!.capture(mPreviewRequestBuilder!!.build(), mCaptureCallback, null)
|
||||
} catch (e: CameraAccessException) {
|
||||
Log.e(TAG, "Failed to lock focus.", e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures a still picture.
|
||||
*/
|
||||
fun captureStillPicture() {
|
||||
try {
|
||||
val captureRequestBuilder = mCamera!!.createCaptureRequest(
|
||||
CameraDevice.TEMPLATE_STILL_CAPTURE
|
||||
)
|
||||
captureRequestBuilder.addTarget(mImageReader!!.surface)
|
||||
captureRequestBuilder.set(
|
||||
CaptureRequest.CONTROL_AF_MODE,
|
||||
mPreviewRequestBuilder!!.get(CaptureRequest.CONTROL_AF_MODE)
|
||||
)
|
||||
when (flash) {
|
||||
Constants.FLASH_OFF -> {
|
||||
captureRequestBuilder.set(
|
||||
CaptureRequest.CONTROL_AE_MODE,
|
||||
CaptureRequest.CONTROL_AE_MODE_ON
|
||||
)
|
||||
captureRequestBuilder.set(
|
||||
CaptureRequest.FLASH_MODE,
|
||||
CaptureRequest.FLASH_MODE_OFF
|
||||
)
|
||||
}
|
||||
Constants.FLASH_ON -> captureRequestBuilder.set(
|
||||
CaptureRequest.CONTROL_AE_MODE,
|
||||
CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH
|
||||
)
|
||||
Constants.FLASH_TORCH -> {
|
||||
captureRequestBuilder.set(
|
||||
CaptureRequest.CONTROL_AE_MODE,
|
||||
CaptureRequest.CONTROL_AE_MODE_ON
|
||||
)
|
||||
captureRequestBuilder.set(
|
||||
CaptureRequest.FLASH_MODE,
|
||||
CaptureRequest.FLASH_MODE_TORCH
|
||||
)
|
||||
}
|
||||
Constants.FLASH_AUTO -> captureRequestBuilder.set(
|
||||
CaptureRequest.CONTROL_AE_MODE,
|
||||
CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH
|
||||
)
|
||||
Constants.FLASH_RED_EYE -> captureRequestBuilder.set(
|
||||
CaptureRequest.CONTROL_AE_MODE,
|
||||
CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH
|
||||
)
|
||||
}
|
||||
// Calculate JPEG orientation.
|
||||
val sensorOrientation = mCameraCharacteristics!!.get(
|
||||
CameraCharacteristics.SENSOR_ORIENTATION
|
||||
)!!
|
||||
captureRequestBuilder.set(
|
||||
CaptureRequest.JPEG_ORIENTATION,
|
||||
(sensorOrientation +
|
||||
mDisplayOrientation * (if (mFacing == Constants.FACING_FRONT) 1 else -1) +
|
||||
360) % 360
|
||||
)
|
||||
// Stop preview and capture a still picture.
|
||||
mCaptureSession!!.stopRepeating()
|
||||
mCaptureSession!!.capture(captureRequestBuilder.build(),
|
||||
object : CameraCaptureSession.CaptureCallback() {
|
||||
override fun onCaptureCompleted(
|
||||
@NonNull session: CameraCaptureSession,
|
||||
@NonNull request: CaptureRequest,
|
||||
@NonNull result: TotalCaptureResult
|
||||
) {
|
||||
unlockFocus()
|
||||
}
|
||||
}, null
|
||||
)
|
||||
} catch (e: CameraAccessException) {
|
||||
Log.e(TAG, "Cannot capture a still picture.", e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlocks the auto-focus and restart camera preview. This is supposed to be called after
|
||||
* capturing a still picture.
|
||||
*/
|
||||
fun unlockFocus() {
|
||||
mPreviewRequestBuilder!!.set(
|
||||
CaptureRequest.CONTROL_AF_TRIGGER,
|
||||
CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
|
||||
)
|
||||
try {
|
||||
mCaptureSession!!.capture(mPreviewRequestBuilder!!.build(), mCaptureCallback, null)
|
||||
updateAutoFocus()
|
||||
updateFlash()
|
||||
mPreviewRequestBuilder!!.set(
|
||||
CaptureRequest.CONTROL_AF_TRIGGER,
|
||||
CaptureRequest.CONTROL_AF_TRIGGER_IDLE
|
||||
)
|
||||
mCaptureSession!!.setRepeatingRequest(mPreviewRequestBuilder!!.build(), mCaptureCallback, null)
|
||||
mCaptureCallback.setState(PictureCaptureCallback.STATE_PREVIEW)
|
||||
} catch (e: CameraAccessException) {
|
||||
Log.e(TAG, "Failed to restart camera preview.", e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A [CameraCaptureSession.CaptureCallback] for capturing a still picture.
|
||||
*/
|
||||
private abstract class PictureCaptureCallback internal constructor() : CameraCaptureSession.CaptureCallback() {
|
||||
|
||||
private var mState: Int = 0
|
||||
|
||||
internal fun setState(state: Int) {
|
||||
mState = state
|
||||
}
|
||||
|
||||
override fun onCaptureProgressed(
|
||||
@NonNull session: CameraCaptureSession,
|
||||
@NonNull request: CaptureRequest, @NonNull partialResult: CaptureResult
|
||||
) {
|
||||
process(partialResult)
|
||||
}
|
||||
|
||||
override fun onCaptureCompleted(
|
||||
@NonNull session: CameraCaptureSession,
|
||||
@NonNull request: CaptureRequest, @NonNull result: TotalCaptureResult
|
||||
) {
|
||||
process(result)
|
||||
}
|
||||
|
||||
private fun process(@NonNull result: CaptureResult) {
|
||||
when (mState) {
|
||||
STATE_LOCKING -> {
|
||||
val af = result.get(CaptureResult.CONTROL_AF_STATE)
|
||||
if (af != null) {
|
||||
if (af == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || af == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) {
|
||||
val ae = result.get(CaptureResult.CONTROL_AE_STATE)
|
||||
if (ae == null || ae == CaptureResult.CONTROL_AE_STATE_CONVERGED) {
|
||||
setState(STATE_CAPTURING)
|
||||
onReady()
|
||||
} else {
|
||||
setState(STATE_LOCKED)
|
||||
onPrecaptureRequired()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
STATE_PRECAPTURE -> {
|
||||
val ae = result.get(CaptureResult.CONTROL_AE_STATE)
|
||||
if (ae == null || ae == CaptureResult.CONTROL_AE_STATE_PRECAPTURE ||
|
||||
ae == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED ||
|
||||
ae == CaptureResult.CONTROL_AE_STATE_CONVERGED
|
||||
) {
|
||||
setState(STATE_WAITING)
|
||||
}
|
||||
}
|
||||
STATE_WAITING -> {
|
||||
val ae = result.get(CaptureResult.CONTROL_AE_STATE)
|
||||
if (ae == null || ae != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) {
|
||||
setState(STATE_CAPTURING)
|
||||
onReady()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when it is ready to take a still picture.
|
||||
*/
|
||||
abstract fun onReady()
|
||||
|
||||
/**
|
||||
* Called when it is necessary to run the precapture sequence.
|
||||
*/
|
||||
abstract fun onPrecaptureRequired()
|
||||
|
||||
companion object {
|
||||
|
||||
internal val STATE_PREVIEW = 0
|
||||
internal val STATE_LOCKING = 1
|
||||
internal val STATE_LOCKED = 2
|
||||
internal val STATE_PRECAPTURE = 3
|
||||
internal val STATE_WAITING = 4
|
||||
internal val STATE_CAPTURING = 5
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = "Camera2"
|
||||
|
||||
private val INTERNAL_FACINGS = SparseIntArray()
|
||||
|
||||
init {
|
||||
INTERNAL_FACINGS.put(Constants.FACING_BACK, CameraCharacteristics.LENS_FACING_BACK)
|
||||
INTERNAL_FACINGS.put(Constants.FACING_FRONT, CameraCharacteristics.LENS_FACING_FRONT)
|
||||
}
|
||||
|
||||
/**
|
||||
* Max preview width that is guaranteed by Camera2 API
|
||||
*/
|
||||
private val MAX_PREVIEW_WIDTH = 1920
|
||||
|
||||
/**
|
||||
* Max preview height that is guaranteed by Camera2 API
|
||||
*/
|
||||
private val MAX_PREVIEW_HEIGHT = 1080
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.cameraview
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.content.Context
|
||||
import android.graphics.ImageFormat
|
||||
import android.hardware.camera2.params.StreamConfigurationMap
|
||||
import cash.z.android.cameraview.CameraView
|
||||
import cash.z.android.cameraview.api21.Camera2
|
||||
import cash.z.android.cameraview.base.CameraViewImpl
|
||||
import cash.z.android.cameraview.base.PreviewImpl
|
||||
import cash.z.android.cameraview.base.Size
|
||||
import cash.z.android.cameraview.base.SizeMap
|
||||
|
||||
|
||||
@TargetApi(23)
|
||||
internal class Camera2Api23(callback: CameraViewImpl.Callback, preview: PreviewImpl, context: Context) :
|
||||
Camera2(callback, preview, context) {
|
||||
|
||||
protected override fun collectPictureSizes(sizes: SizeMap, map: StreamConfigurationMap) {
|
||||
// Try to get hi-res output sizes
|
||||
val outputSizes = map.getHighResolutionOutputSizes(ImageFormat.JPEG)
|
||||
if (outputSizes != null) {
|
||||
for (size in map.getHighResolutionOutputSizes(ImageFormat.JPEG)) {
|
||||
sizes.add(Size(size.width, size.height))
|
||||
}
|
||||
}
|
||||
if (sizes.isEmpty) {
|
||||
super.collectPictureSizes(sizes, map)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cash.z.android.cameraview.base
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.NonNull
|
||||
import androidx.collection.SparseArrayCompat
|
||||
|
||||
/**
|
||||
* Immutable class for describing proportional relationship between width and height.
|
||||
*/
|
||||
class AspectRatio private constructor(val x: Int, val y: Int) : Comparable<AspectRatio>, Parcelable {
|
||||
|
||||
constructor(parcel: Parcel) : this(
|
||||
parcel.readInt(),
|
||||
parcel.readInt()
|
||||
) {
|
||||
}
|
||||
|
||||
fun matches(size: Size): Boolean {
|
||||
val gcd = gcd(size.width, size.height)
|
||||
val x = size.width / gcd
|
||||
val y = size.height / gcd
|
||||
return this.x == x && this.y == y
|
||||
}
|
||||
|
||||
override fun equals(o: Any?): Boolean {
|
||||
if (o == null) {
|
||||
return false
|
||||
}
|
||||
if (this === o) {
|
||||
return true
|
||||
}
|
||||
if (o is AspectRatio) {
|
||||
val ratio = o as AspectRatio?
|
||||
return x == ratio!!.x && y == ratio.y
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return x.toString() + ":" + y
|
||||
}
|
||||
|
||||
fun toFloat(): Float {
|
||||
return x.toFloat() / y
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
// assuming most sizes are <2^16, doing a rotate will give us perfect hashing
|
||||
return y xor (x shl Integer.SIZE / 2 or x.ushr(Integer.SIZE / 2))
|
||||
}
|
||||
|
||||
override fun compareTo(@NonNull another: AspectRatio): Int {
|
||||
if (equals(another)) {
|
||||
return 0
|
||||
} else if (toFloat() - another.toFloat() > 0) {
|
||||
return 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The inverse of this [AspectRatio].
|
||||
*/
|
||||
fun inverse(): AspectRatio {
|
||||
|
||||
return AspectRatio.of(y, x)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||
dest.writeInt(x)
|
||||
dest.writeInt(y)
|
||||
}
|
||||
companion object CREATOR : Parcelable.Creator<AspectRatio> {
|
||||
|
||||
private val sCache = SparseArrayCompat<SparseArrayCompat<AspectRatio>>(16)
|
||||
|
||||
/**
|
||||
* Returns an instance of [AspectRatio] specified by `x` and `y` values.
|
||||
* The values `x` and `` will be reduced by their greatest common divider.
|
||||
*
|
||||
* @param x The width
|
||||
* @param y The height
|
||||
* @return An instance of [AspectRatio]
|
||||
*/
|
||||
fun of(inX: Int, inY: Int): AspectRatio {
|
||||
var x = inX
|
||||
var y = inY
|
||||
val gcd = gcd(x, y)
|
||||
x /= gcd
|
||||
y /= gcd
|
||||
var arrayX: SparseArrayCompat<AspectRatio>? = sCache.get(x)
|
||||
if (arrayX == null) {
|
||||
val ratio = AspectRatio(x, y)
|
||||
arrayX = SparseArrayCompat()
|
||||
arrayX!!.put(y, ratio)
|
||||
sCache.put(x, arrayX)
|
||||
return ratio
|
||||
} else {
|
||||
var ratio = arrayX!!.get(y)
|
||||
if (ratio == null) {
|
||||
ratio = AspectRatio(x, y)
|
||||
arrayX!!.put(y, ratio)
|
||||
}
|
||||
return ratio
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an [AspectRatio] from a [String] formatted like "4:3".
|
||||
*
|
||||
* @param s The string representation of the aspect ratio
|
||||
* @return The aspect ratio
|
||||
* @throws IllegalArgumentException when the format is incorrect.
|
||||
*/
|
||||
fun parse(s: String): AspectRatio {
|
||||
val position = s.indexOf(':')
|
||||
if (position == -1) {
|
||||
throw IllegalArgumentException("Malformed aspect ratio: $s")
|
||||
}
|
||||
try {
|
||||
val x = Integer.parseInt(s.substring(0, position))
|
||||
val y = Integer.parseInt(s.substring(position + 1))
|
||||
return AspectRatio.of(x, y)
|
||||
} catch (e: NumberFormatException) {
|
||||
throw IllegalArgumentException("Malformed aspect ratio: $s", e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun gcd(a: Int, b: Int): Int {
|
||||
var a = a
|
||||
var b = b
|
||||
while (b != 0) {
|
||||
val c = b
|
||||
b = a % b
|
||||
a = c
|
||||
}
|
||||
return a
|
||||
}
|
||||
override fun createFromParcel(parcel: Parcel): AspectRatio {
|
||||
return AspectRatio(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<AspectRatio?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cash.z.android.cameraview.base
|
||||
|
||||
import android.view.View
|
||||
|
||||
internal abstract class CameraViewImpl(protected val mCallback: Callback, protected val mPreview: PreviewImpl) {
|
||||
|
||||
val view: View
|
||||
get() = mPreview.view
|
||||
|
||||
internal abstract val isCameraOpened: Boolean
|
||||
|
||||
internal abstract var facing: Int
|
||||
|
||||
internal abstract val supportedAspectRatios: Set<AspectRatio>
|
||||
|
||||
internal abstract val aspectRatio: AspectRatio
|
||||
|
||||
internal abstract var autoFocus: Boolean
|
||||
|
||||
internal abstract var flash: Int
|
||||
|
||||
/**
|
||||
* @return `true` if the implementation was able to start the camera session.
|
||||
*/
|
||||
internal abstract fun start(): Boolean
|
||||
|
||||
internal abstract fun stop()
|
||||
|
||||
/**
|
||||
* @return `true` if the aspect ratio was changed.
|
||||
*/
|
||||
internal abstract fun setAspectRatio(ratio: AspectRatio?): Boolean
|
||||
|
||||
internal abstract fun takePicture()
|
||||
|
||||
internal abstract fun setDisplayOrientation(displayOrientation: Int)
|
||||
|
||||
internal interface Callback {
|
||||
|
||||
fun onCameraOpened()
|
||||
|
||||
fun onCameraClosed()
|
||||
|
||||
fun onPictureTaken(data: ByteArray)
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cash.z.android.cameraview.base
|
||||
|
||||
|
||||
interface Constants {
|
||||
companion object {
|
||||
|
||||
val DEFAULT_ASPECT_RATIO = AspectRatio.of(4, 3)
|
||||
|
||||
const val FACING_BACK = 0
|
||||
const val FACING_FRONT = 1
|
||||
|
||||
const val FLASH_OFF = 0
|
||||
const val FLASH_ON = 1
|
||||
const val FLASH_TORCH = 2
|
||||
const val FLASH_AUTO = 3
|
||||
const val FLASH_RED_EYE = 4
|
||||
|
||||
const val LANDSCAPE_90 = 90
|
||||
const val LANDSCAPE_270 = 270
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cash.z.android.cameraview.base
|
||||
|
||||
import android.view.Surface
|
||||
import android.view.SurfaceHolder
|
||||
import android.view.View
|
||||
|
||||
|
||||
/**
|
||||
* Encapsulates all the operations related to camera preview in a backward-compatible manner.
|
||||
*/
|
||||
internal abstract class PreviewImpl {
|
||||
|
||||
private var mCallback: Callback? = null
|
||||
|
||||
var width: Int = 0
|
||||
private set
|
||||
|
||||
var height: Int = 0
|
||||
private set
|
||||
|
||||
internal abstract val surface: Surface
|
||||
|
||||
internal abstract val view: View
|
||||
|
||||
internal abstract val outputClass: Class<*>
|
||||
|
||||
internal abstract val isReady: Boolean
|
||||
|
||||
val surfaceHolder: SurfaceHolder?
|
||||
get() = null
|
||||
|
||||
open val surfaceTexture: Any?
|
||||
get() = null
|
||||
|
||||
internal interface Callback {
|
||||
fun onSurfaceChanged()
|
||||
}
|
||||
|
||||
fun setCallback(callback: Callback) {
|
||||
mCallback = callback
|
||||
}
|
||||
|
||||
internal abstract fun setDisplayOrientation(displayOrientation: Int)
|
||||
|
||||
protected fun dispatchSurfaceChanged() {
|
||||
mCallback!!.onSurfaceChanged()
|
||||
}
|
||||
|
||||
open fun setBufferSize(width: Int, height: Int) {}
|
||||
|
||||
fun setSize(width: Int, height: Int) {
|
||||
this.width = width
|
||||
this.height = height
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cash.z.android.cameraview.base
|
||||
|
||||
import androidx.annotation.NonNull
|
||||
|
||||
|
||||
/**
|
||||
* Immutable class for describing width and height dimensions in pixels.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a new immutable Size instance.
|
||||
*
|
||||
* @param width The width of the size, in pixels
|
||||
* @param height The height of the size, in pixels
|
||||
*/
|
||||
class Size(val width: Int, val height: Int) : Comparable<Size> {
|
||||
|
||||
override fun equals(o: Any?): Boolean {
|
||||
if (o == null) {
|
||||
return false
|
||||
}
|
||||
if (this === o) {
|
||||
return true
|
||||
}
|
||||
if (o is Size) {
|
||||
val size = o as Size?
|
||||
return width == size!!.width && height == size.height
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return width.toString() + "x" + height
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
// assuming most sizes are <2^16, doing a rotate will give us perfect hashing
|
||||
return height xor (width shl Integer.SIZE / 2 or width.ushr(Integer.SIZE / 2))
|
||||
}
|
||||
|
||||
override fun compareTo(@NonNull another: Size): Int {
|
||||
return width * height - another.width * another.height
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package cash.z.android.cameraview.base
|
||||
|
||||
|
||||
import androidx.collection.ArrayMap
|
||||
import java.util.SortedSet
|
||||
import java.util.TreeSet
|
||||
|
||||
/**
|
||||
* A collection class that automatically groups [Size]s by their [AspectRatio]s.
|
||||
*/
|
||||
internal class SizeMap {
|
||||
|
||||
private val mRatios = ArrayMap<AspectRatio, SortedSet<Size>>()
|
||||
|
||||
val isEmpty: Boolean
|
||||
get() = mRatios.isEmpty()
|
||||
|
||||
/**
|
||||
* Add a new [Size] to this collection.
|
||||
*
|
||||
* @param size The size to add.
|
||||
* @return `true` if it is added, `false` if it already exists and is not added.
|
||||
*/
|
||||
fun add(size: Size): Boolean {
|
||||
for (ratio in mRatios.keys) {
|
||||
if (ratio.matches(size)) {
|
||||
val sizes = mRatios[ratio]
|
||||
if (sizes?.contains(size) == true) {
|
||||
return false
|
||||
} else {
|
||||
if(sizes == null) mRatios[ratio] = TreeSet<Size>().let { it.add(size); it }
|
||||
else sizes.add(size)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
// None of the existing ratio matches the provided size; add a new key
|
||||
val sizes = TreeSet<Size>()
|
||||
sizes.add(size)
|
||||
mRatios.put(AspectRatio.of(size.width, size.height), sizes)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the specified aspect ratio and all sizes associated with it.
|
||||
*
|
||||
* @param ratio The aspect ratio to be removed.
|
||||
*/
|
||||
fun remove(ratio: AspectRatio) {
|
||||
mRatios.remove(ratio)
|
||||
}
|
||||
|
||||
fun ratios(): Set<AspectRatio> {
|
||||
return mRatios.keys
|
||||
}
|
||||
|
||||
fun sizes(ratio: AspectRatio): SortedSet<Size> {
|
||||
return mRatios[ratio]!!
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
mRatios.clear()
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package cash.z.android.qrecycler
|
||||
|
||||
/**
|
||||
* An interface to allow for plugging in any scanner
|
||||
*/
|
||||
interface QScanner {
|
||||
fun scanBarcode(callback: (Result<String>) -> Unit)
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package cash.z.android.wallet.ui.util.vision
|
||||
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import com.google.firebase.ml.vision.barcode.FirebaseVisionBarcode
|
||||
|
||||
class BarcodeGraphic(overlay: GraphicOverlay, private val barcode: FirebaseVisionBarcode) :
|
||||
GraphicOverlay.Graphic(overlay) {
|
||||
|
||||
private var rectPaint = Paint().apply {
|
||||
color = TEXT_COLOR
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = STROKE_WIDTH
|
||||
}
|
||||
|
||||
private var barcodePaint = Paint().apply {
|
||||
color = TEXT_COLOR
|
||||
textSize = TEXT_SIZE
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the barcode block annotations for position, size, and raw value on the supplied canvas.
|
||||
*/
|
||||
override fun draw(canvas: Canvas) {
|
||||
// Draws the bounding box around the BarcodeBlock.
|
||||
val rect = RectF(barcode.boundingBox)
|
||||
rect.left = translateX(rect.left)
|
||||
rect.top = translateY(rect.top)
|
||||
rect.right = translateX(rect.right)
|
||||
rect.bottom = translateY(rect.bottom)
|
||||
canvas.drawRect(rect, rectPaint)
|
||||
|
||||
// Renders the barcode at the bottom of the box.
|
||||
barcode.rawValue?.let { value ->
|
||||
canvas.drawText(value, rect.left, rect.bottom, barcodePaint)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TEXT_COLOR = Color.WHITE
|
||||
private const val TEXT_SIZE = 54.0f
|
||||
private const val STROKE_WIDTH = 4.0f
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
package cash.z.android.wallet.ui.util.vision
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.util.Log
|
||||
import cash.z.android.wallet.ui.util.VisionProcessorBase
|
||||
import com.google.android.gms.tasks.Task
|
||||
import com.google.firebase.ml.vision.FirebaseVision
|
||||
import com.google.firebase.ml.vision.barcode.FirebaseVisionBarcode
|
||||
import com.google.firebase.ml.vision.barcode.FirebaseVisionBarcodeDetector
|
||||
import com.google.firebase.ml.vision.common.FirebaseVisionImage
|
||||
import java.io.IOException
|
||||
|
||||
/** Barcode Detector Demo. */
|
||||
class BarcodeScanningProcessor : VisionProcessorBase<List<FirebaseVisionBarcode>>() {
|
||||
|
||||
// Note that if you know which format of barcode your app is dealing with, detection will be
|
||||
// faster to specify the supported barcode formats one by one, e.g.
|
||||
// FirebaseVisionBarcodeDetectorOptions.Builder()
|
||||
// .setBarcodeFormats(FirebaseVisionBarcode.FORMAT_QR_CODE)
|
||||
// .build()
|
||||
public val detector: FirebaseVisionBarcodeDetector by lazy {
|
||||
FirebaseVision.getInstance().visionBarcodeDetector
|
||||
}
|
||||
|
||||
|
||||
override fun stop() {
|
||||
try {
|
||||
detector.close()
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Exception thrown while trying to close Barcode Detector: $e")
|
||||
}
|
||||
}
|
||||
|
||||
override fun detectInImage(image: FirebaseVisionImage): Task<List<FirebaseVisionBarcode>> {
|
||||
return detector.detectInImage(image)
|
||||
}
|
||||
|
||||
override fun onSuccess(
|
||||
originalCameraImage: Bitmap?,
|
||||
barcodes: List<FirebaseVisionBarcode>,
|
||||
frameMetadata: FrameMetadata,
|
||||
graphicOverlay: GraphicOverlay
|
||||
) {
|
||||
graphicOverlay.clear()
|
||||
|
||||
originalCameraImage?.let {
|
||||
val imageGraphic = CameraImageGraphic(graphicOverlay, it)
|
||||
graphicOverlay.add(imageGraphic)
|
||||
}
|
||||
|
||||
barcodes.forEach {
|
||||
val barcodeGraphic = BarcodeGraphic(graphicOverlay, it)
|
||||
graphicOverlay.add(barcodeGraphic)
|
||||
}
|
||||
graphicOverlay.postInvalidate()
|
||||
}
|
||||
|
||||
override fun onFailure(e: Exception) {
|
||||
Log.e(TAG, "Barcode detection failed $e")
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "BarcodeScanProc"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package cash.z.android.wallet.ui.util.vision
|
||||
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// 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.
|
||||
|
||||
|
||||
import android.graphics.*
|
||||
import android.hardware.Camera.CameraInfo
|
||||
import android.util.Log
|
||||
import androidx.annotation.Nullable
|
||||
import com.google.firebase.ml.vision.common.FirebaseVisionImageMetadata
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
/** Utils functions for bitmap conversions. */
|
||||
object BitmapUtils {
|
||||
|
||||
// Convert NV21 format byte buffer to bitmap.
|
||||
@Nullable
|
||||
fun getBitmap(data: ByteBuffer, metadata: FrameMetadata): Bitmap? {
|
||||
data.rewind()
|
||||
val imageInBuffer = ByteArray(data.limit())
|
||||
data.get(imageInBuffer, 0, imageInBuffer.size)
|
||||
try {
|
||||
val image = YuvImage(
|
||||
imageInBuffer, ImageFormat.NV21, metadata.width, metadata.height, null
|
||||
)
|
||||
if (image != null) {
|
||||
val stream = ByteArrayOutputStream()
|
||||
image.compressToJpeg(Rect(0, 0, metadata.width, metadata.height), 80, stream)
|
||||
|
||||
val bmp = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size())
|
||||
|
||||
stream.close()
|
||||
return rotateBitmap(
|
||||
bmp,
|
||||
metadata.rotation,
|
||||
metadata.cameraFacing
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("VisionProcessorBase", "Error: " + e.message)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// Rotates a bitmap if it is converted from a bytebuffer.
|
||||
private fun rotateBitmap(bitmap: Bitmap, rotation: Int, facing: Int): Bitmap {
|
||||
val matrix = Matrix()
|
||||
var rotationDegree = 0
|
||||
when (rotation) {
|
||||
FirebaseVisionImageMetadata.ROTATION_90 -> rotationDegree = 90
|
||||
FirebaseVisionImageMetadata.ROTATION_180 -> rotationDegree = 180
|
||||
FirebaseVisionImageMetadata.ROTATION_270 -> rotationDegree = 270
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
|
||||
// Rotate the image back to straight.}
|
||||
matrix.postRotate(rotationDegree.toFloat())
|
||||
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package cash.z.android.wallet.ui.util.vision
|
||||
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import cash.z.android.wallet.ui.util.vision.GraphicOverlay.Graphic
|
||||
|
||||
/** Draw camera image to background. */
|
||||
class CameraImageGraphic(overlay: GraphicOverlay, private val bitmap: Bitmap) : Graphic(overlay) {
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
canvas.drawBitmap(bitmap, null, Rect(0, 0, canvas.width, canvas.height), null)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,723 @@
|
|||
package cash.z.android.wallet.ui.util.vision
|
||||
|
||||
// Copyright 2019 Electric Coin Company
|
||||
//
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// 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
|
||||
//
|
||||
// This file has been modified by Electric Coin Company to allow for
|
||||
// better fitting previews.
|
||||
//
|
||||
// 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.
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.graphics.ImageFormat
|
||||
import android.graphics.SurfaceTexture
|
||||
import android.hardware.Camera
|
||||
import android.util.Log
|
||||
import android.view.Surface
|
||||
import android.view.SurfaceHolder
|
||||
import android.view.WindowManager
|
||||
import androidx.annotation.Nullable
|
||||
import androidx.annotation.RequiresPermission
|
||||
import cash.z.android.wallet.ui.util.VisionImageProcessor
|
||||
import com.google.android.gms.common.images.Size
|
||||
import java.io.IOException
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.*
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
class CameraSource(protected var activity: Activity, private val graphicOverlay: GraphicOverlay, private val requestedPreviewWidth: Int = 480, private val requestedPreviewHeight: Int = 360) {
|
||||
|
||||
private var camera: Camera? = null
|
||||
|
||||
/**
|
||||
* Returns the selected camera; one of [.CAMERA_FACING_BACK] or [ ][.CAMERA_FACING_FRONT].
|
||||
*/
|
||||
var cameraFacing = CAMERA_FACING_BACK
|
||||
protected set
|
||||
|
||||
/**
|
||||
* Rotation of the device, and thus the associated preview images captured from the device. See
|
||||
* Frame.Metadata#getRotation().
|
||||
*/
|
||||
private var rotation: Int = 0
|
||||
|
||||
/** Returns the preview size that is currently in use by the underlying camera. */
|
||||
var previewSize: Size? = null
|
||||
private set
|
||||
|
||||
// These values may be requested by the caller. Due to hardware limitations, we may need to
|
||||
// select close, but not exactly the same values for these.
|
||||
private val requestedFps = 20.0f
|
||||
private val requestedAutoFocus = true
|
||||
|
||||
// These instances need to be held onto to avoid GC of their underlying resources. Even though
|
||||
// these aren't used outside of the method that creates them, they still must have hard
|
||||
// references maintained to them.
|
||||
private var dummySurfaceTexture: SurfaceTexture? = null
|
||||
|
||||
// True if a SurfaceTexture is being used for the preview, false if a SurfaceHolder is being
|
||||
// used for the preview. We want to be compatible back to Gingerbread, but SurfaceTexture
|
||||
// wasn't introduced until Honeycomb. Since the interface cannot use a SurfaceTexture, if the
|
||||
// developer wants to display a preview we must use a SurfaceHolder. If the developer doesn't
|
||||
// want to display a preview we use a SurfaceTexture if we are running at least Honeycomb.
|
||||
private var usingSurfaceTexture: Boolean = false
|
||||
|
||||
/**
|
||||
* Dedicated thread and associated runnable for calling into the detector with frames, as the
|
||||
* frames become available from the camera.
|
||||
*/
|
||||
private var processingThread: Thread? = null
|
||||
|
||||
private val processingRunnable: FrameProcessingRunnable
|
||||
|
||||
private val processorLock = Any()
|
||||
// @GuardedBy("processorLock")
|
||||
private var frameProcessor: VisionImageProcessor? = null
|
||||
|
||||
/**
|
||||
* Map to convert between a byte array, received from the camera, and its associated byte buffer.
|
||||
* We use byte buffers internally because this is a more efficient way to call into native code
|
||||
* later (avoids a potential copy).
|
||||
*
|
||||
*
|
||||
* **Note:** uses IdentityHashMap here instead of HashMap because the behavior of an array's
|
||||
* equals, hashCode and toString methods is both useless and unexpected. IdentityHashMap enforces
|
||||
* identity ('==') check on the keys.
|
||||
*/
|
||||
private val bytesToByteBuffer = IdentityHashMap<ByteArray, ByteBuffer>()
|
||||
|
||||
init {
|
||||
graphicOverlay.clear()
|
||||
processingRunnable = FrameProcessingRunnable()
|
||||
|
||||
if (Camera.getNumberOfCameras() == 1) {
|
||||
val cameraInfo = Camera.CameraInfo()
|
||||
Camera.getCameraInfo(0, cameraInfo)
|
||||
cameraFacing = cameraInfo.facing
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================================================================
|
||||
// Public
|
||||
// ==============================================================================================
|
||||
|
||||
/** Stops the camera and releases the resources of the camera and underlying detector. */
|
||||
fun release() {
|
||||
synchronized(processorLock) {
|
||||
stop()
|
||||
processingRunnable.release()
|
||||
cleanScreen()
|
||||
|
||||
if (frameProcessor != null) {
|
||||
frameProcessor!!.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the camera and starts sending preview frames to the underlying detector. The preview
|
||||
* frames are not displayed.
|
||||
*
|
||||
* @throws IOException if the camera's preview texture or display could not be initialized
|
||||
*/
|
||||
@SuppressLint("MissingPermission")
|
||||
@RequiresPermission(Manifest.permission.CAMERA)
|
||||
@Synchronized
|
||||
@Throws(IOException::class)
|
||||
fun start(): CameraSource {
|
||||
if (camera != null) {
|
||||
return this
|
||||
}
|
||||
|
||||
camera = createCamera()
|
||||
dummySurfaceTexture = SurfaceTexture(DUMMY_TEXTURE_NAME)
|
||||
camera!!.setPreviewTexture(dummySurfaceTexture)
|
||||
usingSurfaceTexture = true
|
||||
camera!!.startPreview()
|
||||
|
||||
processingThread = Thread(processingRunnable)
|
||||
processingRunnable.setActive(true)
|
||||
processingThread!!.start()
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the camera and starts sending preview frames to the underlying detector. The supplied
|
||||
* surface holder is used for the preview so frames can be displayed to the user.
|
||||
*
|
||||
* @param surfaceHolder the surface holder to use for the preview frames
|
||||
* @throws IOException if the supplied surface holder could not be used as the preview display
|
||||
*/
|
||||
@RequiresPermission(Manifest.permission.CAMERA)
|
||||
@Synchronized
|
||||
@Throws(IOException::class)
|
||||
fun start(surfaceHolder: SurfaceHolder): CameraSource {
|
||||
if (camera != null) {
|
||||
return this
|
||||
}
|
||||
|
||||
camera = createCamera()
|
||||
camera!!.setPreviewDisplay(surfaceHolder)
|
||||
camera!!.startPreview()
|
||||
|
||||
processingThread = Thread(processingRunnable)
|
||||
processingRunnable.setActive(true)
|
||||
processingThread!!.start()
|
||||
|
||||
usingSurfaceTexture = false
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the camera and stops sending frames to the underlying frame detector.
|
||||
*
|
||||
*
|
||||
* This camera source may be restarted again by calling [.start] or [ ][.start].
|
||||
*
|
||||
*
|
||||
* Call [.release] instead to completely shut down this camera source and release the
|
||||
* resources of the underlying detector.
|
||||
*/
|
||||
@Synchronized
|
||||
fun stop() {
|
||||
processingRunnable.setActive(false)
|
||||
if (processingThread != null) {
|
||||
try {
|
||||
// Wait for the thread to complete to ensure that we can't have multiple threads
|
||||
// executing at the same time (i.e., which would happen if we called start too
|
||||
// quickly after stop).
|
||||
processingThread!!.join()
|
||||
} catch (e: InterruptedException) {
|
||||
Log.d(TAG, "Frame processing thread interrupted on release.")
|
||||
}
|
||||
|
||||
processingThread = null
|
||||
}
|
||||
|
||||
if (camera != null) {
|
||||
camera!!.stopPreview()
|
||||
camera!!.setPreviewCallbackWithBuffer(null)
|
||||
try {
|
||||
if (usingSurfaceTexture) {
|
||||
camera!!.setPreviewTexture(null)
|
||||
} else {
|
||||
camera!!.setPreviewDisplay(null)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to clear camera preview: $e")
|
||||
}
|
||||
|
||||
camera!!.release()
|
||||
camera = null
|
||||
}
|
||||
|
||||
// Release the reference to any image buffers, since these will no longer be in use.
|
||||
bytesToByteBuffer.clear()
|
||||
}
|
||||
|
||||
/** Changes the facing of the camera. */
|
||||
@Synchronized
|
||||
fun setFacing(facing: Int) {
|
||||
if (facing != CAMERA_FACING_BACK && facing != CAMERA_FACING_FRONT) {
|
||||
throw IllegalArgumentException("Invalid camera: $facing")
|
||||
}
|
||||
this.cameraFacing = facing
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the camera and applies the user settings.
|
||||
*
|
||||
* @throws IOException if camera cannot be found or preview cannot be processed
|
||||
*/
|
||||
@SuppressLint("InlinedApi")
|
||||
@Throws(IOException::class)
|
||||
private fun createCamera(): Camera {
|
||||
val requestedCameraId = getIdForRequestedCamera(cameraFacing)
|
||||
if (requestedCameraId == -1) {
|
||||
throw IOException("Could not find requested camera.")
|
||||
}
|
||||
val camera = Camera.open(requestedCameraId)
|
||||
|
||||
val sizePair = selectSizePair(camera, requestedPreviewWidth, requestedPreviewHeight)
|
||||
?: throw IOException("Could not find suitable preview size.")
|
||||
val pictureSize = sizePair.pictureSize()
|
||||
previewSize = sizePair.previewSize()
|
||||
|
||||
val previewFpsRange = selectPreviewFpsRange(camera, requestedFps)
|
||||
?: throw IOException("Could not find suitable preview frames per second range.")
|
||||
|
||||
val parameters = camera.parameters
|
||||
|
||||
if (pictureSize != null) {
|
||||
parameters.setPictureSize(pictureSize.width, pictureSize.height)
|
||||
}
|
||||
parameters.setPreviewSize(previewSize!!.width, previewSize!!.height)
|
||||
parameters.setPreviewFpsRange(
|
||||
previewFpsRange[Camera.Parameters.PREVIEW_FPS_MIN_INDEX],
|
||||
previewFpsRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]
|
||||
)
|
||||
parameters.previewFormat = ImageFormat.NV21
|
||||
|
||||
setRotation(camera, parameters, requestedCameraId)
|
||||
|
||||
if (requestedAutoFocus) {
|
||||
if (parameters
|
||||
.supportedFocusModes
|
||||
.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)
|
||||
) {
|
||||
parameters.focusMode = Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO
|
||||
} else {
|
||||
Log.i(TAG, "Camera auto focus is not supported on this device.")
|
||||
}
|
||||
}
|
||||
|
||||
camera.parameters = parameters
|
||||
|
||||
// Four frame buffers are needed for working with the camera:
|
||||
//
|
||||
// one for the frame that is currently being executed upon in doing detection
|
||||
// one for the next pending frame to process immediately upon completing detection
|
||||
// two for the frames that the camera uses to populate future preview images
|
||||
//
|
||||
// Through trial and error it appears that two free buffers, in addition to the two buffers
|
||||
// used in this code, are needed for the camera to work properly. Perhaps the camera has
|
||||
// one thread for acquiring images, and another thread for calling into user code. If only
|
||||
// three buffers are used, then the camera will spew thousands of warning messages when
|
||||
// detection takes a non-trivial amount of time.
|
||||
camera.setPreviewCallbackWithBuffer(CameraPreviewCallback())
|
||||
camera.addCallbackBuffer(createPreviewBuffer(previewSize!!))
|
||||
camera.addCallbackBuffer(createPreviewBuffer(previewSize!!))
|
||||
camera.addCallbackBuffer(createPreviewBuffer(previewSize!!))
|
||||
camera.addCallbackBuffer(createPreviewBuffer(previewSize!!))
|
||||
|
||||
return camera
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a preview size and a corresponding same-aspect-ratio picture size. To avoid distorted
|
||||
* preview images on some devices, the picture size must be set to a size that is the same aspect
|
||||
* ratio as the preview size or the preview may end up being distorted. If the picture size is
|
||||
* null, then there is no picture size with the same aspect ratio as the preview size.
|
||||
*/
|
||||
private class SizePair internal constructor(
|
||||
previewSize: android.hardware.Camera.Size,
|
||||
@Nullable pictureSize: android.hardware.Camera.Size?
|
||||
) {
|
||||
private val preview: Size
|
||||
private var picture: Size? = null
|
||||
|
||||
init {
|
||||
preview = Size(previewSize.width, previewSize.height)
|
||||
if (pictureSize != null) {
|
||||
picture = Size(pictureSize.width, pictureSize.height)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun previewSize(): Size {
|
||||
return preview
|
||||
}
|
||||
|
||||
@Nullable
|
||||
internal fun pictureSize(): Size? {
|
||||
return picture
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the correct rotation for the given camera id and sets the rotation in the
|
||||
* parameters. It also sets the camera's display orientation and rotation.
|
||||
*
|
||||
* @param parameters the camera parameters for which to set the rotation
|
||||
* @param cameraId the camera id to set rotation based on
|
||||
*/
|
||||
private fun setRotation(camera: Camera, parameters: Camera.Parameters, cameraId: Int) {
|
||||
val windowManager = activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||
var degrees = 0
|
||||
val rotation = windowManager.defaultDisplay.rotation
|
||||
when (rotation) {
|
||||
Surface.ROTATION_0 -> degrees = 0
|
||||
Surface.ROTATION_90 -> degrees = 90
|
||||
Surface.ROTATION_180 -> degrees = 180
|
||||
Surface.ROTATION_270 -> degrees = 270
|
||||
else -> Log.e(TAG, "Bad rotation value: $rotation")
|
||||
}
|
||||
|
||||
val cameraInfo = Camera.CameraInfo()
|
||||
Camera.getCameraInfo(cameraId, cameraInfo)
|
||||
|
||||
val angle: Int
|
||||
val displayAngle: Int
|
||||
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
|
||||
angle = (cameraInfo.orientation + degrees) % 360
|
||||
displayAngle = (360 - angle) % 360 // compensate for it being mirrored
|
||||
} else { // back-facing
|
||||
angle = (cameraInfo.orientation - degrees + 360) % 360
|
||||
displayAngle = angle
|
||||
}
|
||||
|
||||
// This corresponds to the rotation constants.
|
||||
this.rotation = angle / 90
|
||||
|
||||
camera.setDisplayOrientation(displayAngle)
|
||||
parameters.setRotation(angle)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates one buffer for the camera preview callback. The size of the buffer is based off of the
|
||||
* camera preview size and the format of the camera image.
|
||||
*
|
||||
* @return a new preview buffer of the appropriate size for the current camera settings
|
||||
*/
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun createPreviewBuffer(previewSize: Size): ByteArray {
|
||||
val bitsPerPixel = ImageFormat.getBitsPerPixel(ImageFormat.NV21)
|
||||
val sizeInBits = previewSize.height.toLong() * previewSize.width.toLong() * bitsPerPixel.toLong()
|
||||
val bufferSize = Math.ceil(sizeInBits / 8.0).toInt() + 1
|
||||
|
||||
// Creating the byte array this way and wrapping it, as opposed to using .allocate(),
|
||||
// should guarantee that there will be an array to work with.
|
||||
val byteArray = ByteArray(bufferSize)
|
||||
val buffer = ByteBuffer.wrap(byteArray)
|
||||
if (!buffer.hasArray() || buffer.array() != byteArray) {
|
||||
// I don't think that this will ever happen. But if it does, then we wouldn't be
|
||||
// passing the preview content to the underlying detector later.
|
||||
throw IllegalStateException("Failed to create valid buffer for camera source.")
|
||||
}
|
||||
|
||||
bytesToByteBuffer[byteArray] = buffer
|
||||
return byteArray
|
||||
}
|
||||
|
||||
// ==============================================================================================
|
||||
// Frame processing
|
||||
// ==============================================================================================
|
||||
|
||||
/** Called when the camera has a new preview frame. */
|
||||
private inner class CameraPreviewCallback : Camera.PreviewCallback {
|
||||
override fun onPreviewFrame(data: ByteArray, camera: Camera) {
|
||||
processingRunnable.setNextFrame(data, camera)
|
||||
}
|
||||
}
|
||||
|
||||
fun setMachineLearningFrameProcessor(processor: VisionImageProcessor) {
|
||||
synchronized(processorLock) {
|
||||
cleanScreen()
|
||||
if (frameProcessor != null) {
|
||||
frameProcessor!!.stop()
|
||||
}
|
||||
frameProcessor = processor
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This runnable controls access to the underlying receiver, calling it to process frames when
|
||||
* available from the camera. This is designed to run detection on frames as fast as possible
|
||||
* (i.e., without unnecessary context switching or waiting on the next frame).
|
||||
*
|
||||
*
|
||||
* While detection is running on a frame, new frames may be received from the camera. As these
|
||||
* frames come in, the most recent frame is held onto as pending. As soon as detection and its
|
||||
* associated processing is done for the previous frame, detection on the mostly recently received
|
||||
* frame will immediately start on the same thread.
|
||||
*/
|
||||
private inner class FrameProcessingRunnable internal constructor() : Runnable {
|
||||
|
||||
// This lock guards all of the member variables below.
|
||||
private val lock = ReentrantLock()
|
||||
private val condition = lock.newCondition()
|
||||
private var active = true
|
||||
|
||||
// These pending variables hold the state associated with the new frame awaiting processing.
|
||||
private var pendingFrameData: ByteBuffer? = null
|
||||
|
||||
/**
|
||||
* Releases the underlying receiver. This is only safe to do after the associated thread has
|
||||
* completed, which is managed in camera source's release method above.
|
||||
*/
|
||||
@SuppressLint("Assert")
|
||||
internal fun release() {
|
||||
//assert(processingThread!!.state == Thread.State.TERMINATED)
|
||||
}
|
||||
|
||||
/** Marks the runnable as active/not active. Signals any blocked threads to continue. */
|
||||
internal fun setActive(active: Boolean) {
|
||||
lock.withLock {
|
||||
this.active = active
|
||||
condition.signalAll()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the frame data received from the camera. This adds the previous unused frame buffer (if
|
||||
* present) back to the camera, and keeps a pending reference to the frame data for future use.
|
||||
*/
|
||||
internal fun setNextFrame(data: ByteArray, camera: Camera) {
|
||||
lock.withLock {
|
||||
if (pendingFrameData != null) {
|
||||
camera.addCallbackBuffer(pendingFrameData!!.array())
|
||||
pendingFrameData = null
|
||||
}
|
||||
|
||||
if (!bytesToByteBuffer.containsKey(data)) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"Skipping frame. Could not find ByteBuffer associated with the image " + "data from the camera."
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
pendingFrameData = bytesToByteBuffer[data]
|
||||
|
||||
// Notify the processor thread if it is waiting on the next frame (see below).
|
||||
condition.signalAll()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* As long as the processing thread is active, this executes detection on frames continuously.
|
||||
* The next pending frame is either immediately available or hasn't been received yet. Once it
|
||||
* is available, we transfer the frame info to local variables and run detection on that frame.
|
||||
* It immediately loops back for the next frame without pausing.
|
||||
*
|
||||
*
|
||||
* If detection takes longer than the time in between new frames from the camera, this will
|
||||
* mean that this loop will run without ever waiting on a frame, avoiding any context switching
|
||||
* or frame acquisition time latency.
|
||||
*
|
||||
*
|
||||
* If you find that this is using more CPU than you'd like, you should probably decrease the
|
||||
* FPS setting above to allow for some idle time in between frames.
|
||||
*/
|
||||
@SuppressLint("InlinedApi")
|
||||
override fun run() {
|
||||
var data: ByteBuffer? = null
|
||||
|
||||
while (true) {
|
||||
lock.withLock {
|
||||
while (active && pendingFrameData == null) {
|
||||
try {
|
||||
// Wait for the next frame to be received from the camera, since we
|
||||
// don't have it yet.
|
||||
condition.await()
|
||||
} catch (e: InterruptedException) {
|
||||
Log.d(TAG, "Frame processing loop terminated.", e)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!active) {
|
||||
// Exit the loop once this camera source is stopped or released. We check
|
||||
// this here, immediately after the wait() above, to handle the case where
|
||||
// setActive(false) had been called, triggering the termination of this
|
||||
// loop.
|
||||
return
|
||||
}
|
||||
|
||||
// Hold onto the frame data locally, so that we can use this for detection
|
||||
// below. We need to clear pendingFrameData to ensure that this buffer isn't
|
||||
// recycled back to the camera before we are done using that data.
|
||||
data = pendingFrameData!!
|
||||
pendingFrameData = null
|
||||
}
|
||||
|
||||
// The code below needs to run outside of synchronization, because this will allow
|
||||
// the camera to add pending frame(s) while we are running detection on the current
|
||||
// frame.
|
||||
|
||||
try {
|
||||
synchronized(processorLock) {
|
||||
Log.d(TAG, "Process an image")
|
||||
frameProcessor!!.process(
|
||||
data!!,
|
||||
FrameMetadata.Builder()
|
||||
.setWidth(previewSize!!.width)
|
||||
.setHeight(previewSize!!.height)
|
||||
.setRotation(rotation)
|
||||
.setCameraFacing(cameraFacing)
|
||||
.build(),
|
||||
graphicOverlay
|
||||
)
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Log.e(TAG, "Exception thrown from receiver.", t)
|
||||
} finally {
|
||||
camera!!.addCallbackBuffer(data!!.array())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Cleans up graphicOverlay and child classes can do their cleanups as well . */
|
||||
private fun cleanScreen() {
|
||||
graphicOverlay.clear()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@SuppressLint("InlinedApi")
|
||||
val CAMERA_FACING_BACK = Camera.CameraInfo.CAMERA_FACING_BACK
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
val CAMERA_FACING_FRONT = Camera.CameraInfo.CAMERA_FACING_FRONT
|
||||
|
||||
private val TAG = "MIDemoApp:CameraSource"
|
||||
|
||||
/**
|
||||
* The dummy surface texture must be assigned a chosen name. Since we never use an OpenGL context,
|
||||
* we can choose any ID we want here. The dummy surface texture is not a crazy hack - it is
|
||||
* actually how the camera team recommends using the camera without a preview.
|
||||
*/
|
||||
private val DUMMY_TEXTURE_NAME = 100
|
||||
|
||||
/**
|
||||
* If the absolute difference between a preview size aspect ratio and a picture size aspect ratio
|
||||
* is less than this tolerance, they are considered to be the same aspect ratio.
|
||||
*/
|
||||
private val ASPECT_RATIO_TOLERANCE = 0.01f
|
||||
|
||||
/**
|
||||
* Gets the id for the camera specified by the direction it is facing. Returns -1 if no such
|
||||
* camera was found.
|
||||
*
|
||||
* @param facing the desired camera (front-facing or rear-facing)
|
||||
*/
|
||||
private fun getIdForRequestedCamera(facing: Int): Int {
|
||||
val cameraInfo = Camera.CameraInfo()
|
||||
for (i in 0 until Camera.getNumberOfCameras()) {
|
||||
Camera.getCameraInfo(i, cameraInfo)
|
||||
if (cameraInfo.facing == facing) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the most suitable preview and picture size, given the desired width and height.
|
||||
*
|
||||
*
|
||||
* Even though we only need to find the preview size, it's necessary to find both the preview
|
||||
* size and the picture size of the camera together, because these need to have the same aspect
|
||||
* ratio. On some hardware, if you would only set the preview size, you will get a distorted
|
||||
* image.
|
||||
*
|
||||
* @param camera the camera to select a preview size from
|
||||
* @param desiredWidth the desired width of the camera preview frames
|
||||
* @param desiredHeight the desired height of the camera preview frames
|
||||
* @return the selected preview and picture size pair
|
||||
*/
|
||||
private fun selectSizePair(camera: Camera, desiredWidth: Int, desiredHeight: Int): SizePair? {
|
||||
val validPreviewSizes = generateValidPreviewSizeList(camera)
|
||||
|
||||
// The method for selecting the best size is to minimize the sum of the differences between
|
||||
// the desired values and the actual values for width and height. This is certainly not the
|
||||
// only way to select the best size, but it provides a decent tradeoff between using the
|
||||
// closest aspect ratio vs. using the closest pixel area.
|
||||
var selectedPair: SizePair? = null
|
||||
var minDiff = Integer.MAX_VALUE
|
||||
for (sizePair in validPreviewSizes) {
|
||||
val size = sizePair.previewSize()
|
||||
val diff = Math.abs(size.width - desiredWidth) + Math.abs(size.height - desiredHeight)
|
||||
if (diff < minDiff) {
|
||||
selectedPair = sizePair
|
||||
minDiff = diff
|
||||
}
|
||||
}
|
||||
|
||||
return selectedPair
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a list of acceptable preview sizes. Preview sizes are not acceptable if there is not
|
||||
* a corresponding picture size of the same aspect ratio. If there is a corresponding picture size
|
||||
* of the same aspect ratio, the picture size is paired up with the preview size.
|
||||
*
|
||||
*
|
||||
* This is necessary because even if we don't use still pictures, the still picture size must
|
||||
* be set to a size that is the same aspect ratio as the preview size we choose. Otherwise, the
|
||||
* preview images may be distorted on some devices.
|
||||
*/
|
||||
private fun generateValidPreviewSizeList(camera: Camera): List<SizePair> {
|
||||
val parameters = camera.parameters
|
||||
val supportedPreviewSizes = parameters.supportedPreviewSizes
|
||||
val supportedPictureSizes = parameters.supportedPictureSizes
|
||||
val validPreviewSizes = ArrayList<SizePair>()
|
||||
for (previewSize in supportedPreviewSizes) {
|
||||
val previewAspectRatio = previewSize.width.toFloat() / previewSize.height.toFloat()
|
||||
|
||||
// By looping through the picture sizes in order, we favor the higher resolutions.
|
||||
// We choose the highest resolution in order to support taking the full resolution
|
||||
// picture later.
|
||||
for (pictureSize in supportedPictureSizes) {
|
||||
val pictureAspectRatio = pictureSize.width.toFloat() / pictureSize.height.toFloat()
|
||||
if (Math.abs(previewAspectRatio - pictureAspectRatio) < ASPECT_RATIO_TOLERANCE) {
|
||||
validPreviewSizes.add(SizePair(previewSize, pictureSize))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there are no picture sizes with the same aspect ratio as any preview sizes, allow all
|
||||
// of the preview sizes and hope that the camera can handle it. Probably unlikely, but we
|
||||
// still account for it.
|
||||
if (validPreviewSizes.size == 0) {
|
||||
Log.w(TAG, "No preview sizes have a corresponding same-aspect-ratio picture size")
|
||||
for (previewSize in supportedPreviewSizes) {
|
||||
// The null picture size will let us know that we shouldn't set a picture size.
|
||||
validPreviewSizes.add(SizePair(previewSize, null))
|
||||
}
|
||||
}
|
||||
|
||||
return validPreviewSizes
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the most suitable preview frames per second range, given the desired frames per second.
|
||||
*
|
||||
* @param camera the camera to select a frames per second range from
|
||||
* @param desiredPreviewFps the desired frames per second for the camera preview frames
|
||||
* @return the selected preview frames per second range
|
||||
*/
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun selectPreviewFpsRange(camera: Camera, desiredPreviewFps: Float): IntArray? {
|
||||
// The camera API uses integers scaled by a factor of 1000 instead of floating-point frame
|
||||
// rates.
|
||||
val desiredPreviewFpsScaled = (desiredPreviewFps * 1000.0f).toInt()
|
||||
|
||||
// The method for selecting the best range is to minimize the sum of the differences between
|
||||
// the desired value and the upper and lower bounds of the range. This may select a range
|
||||
// that the desired value is outside of, but this is often preferred. For example, if the
|
||||
// desired frame rate is 29.97, the range (30, 30) is probably more desirable than the
|
||||
// range (15, 30).
|
||||
var selectedFpsRange: IntArray? = null
|
||||
var minDiff = Integer.MAX_VALUE
|
||||
val previewFpsRangeList = camera.parameters.supportedPreviewFpsRange
|
||||
for (range in previewFpsRangeList) {
|
||||
val deltaMin = desiredPreviewFpsScaled - range[Camera.Parameters.PREVIEW_FPS_MIN_INDEX]
|
||||
val deltaMax = desiredPreviewFpsScaled - range[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]
|
||||
val diff = Math.abs(deltaMin) + Math.abs(deltaMax)
|
||||
if (diff < minDiff) {
|
||||
selectedFpsRange = range
|
||||
minDiff = diff
|
||||
}
|
||||
}
|
||||
return selectedFpsRange
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,185 @@
|
|||
package cash.z.android.wallet.ui.util.vision
|
||||
|
||||
// Copyright 2019 Electric Coin Company
|
||||
//
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// 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
|
||||
//
|
||||
// This file has been modified by Electric Coin Company to all for
|
||||
// better fitting previews.
|
||||
//
|
||||
// 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.
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.view.SurfaceHolder
|
||||
import android.view.SurfaceView
|
||||
import android.view.ViewGroup
|
||||
import java.io.IOException
|
||||
|
||||
|
||||
/** Preview the camera image in the screen. */
|
||||
class CameraSourcePreview : ViewGroup {
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
startRequested = false
|
||||
surfaceAvailable = false
|
||||
surfaceView = SurfaceView(context)
|
||||
surfaceView.holder.addCallback(SurfaceCallback())
|
||||
addView(surfaceView)
|
||||
}
|
||||
|
||||
private val surfaceView: SurfaceView
|
||||
private var startRequested: Boolean = false
|
||||
private var surfaceAvailable: Boolean = false
|
||||
private var cameraSource: CameraSource? = null
|
||||
|
||||
private var overlay: GraphicOverlay? = null
|
||||
|
||||
private val isPortraitMode: Boolean
|
||||
get() {
|
||||
val orientation = context.resources.configuration.orientation
|
||||
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
return false
|
||||
}
|
||||
if (orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||
return true
|
||||
}
|
||||
|
||||
Log.d(TAG, "isPortraitMode returning false by default")
|
||||
return false
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun start(cameraSource: CameraSource?) {
|
||||
if (cameraSource == null) {
|
||||
stop()
|
||||
}
|
||||
|
||||
this.cameraSource = cameraSource
|
||||
|
||||
if (this.cameraSource != null) {
|
||||
startRequested = true
|
||||
startIfReady()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun start(cameraSource: CameraSource, overlay: GraphicOverlay) {
|
||||
this.overlay = overlay
|
||||
start(cameraSource)
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
if (cameraSource != null) {
|
||||
cameraSource!!.stop()
|
||||
}
|
||||
}
|
||||
|
||||
fun release() {
|
||||
if (cameraSource != null) {
|
||||
cameraSource!!.release()
|
||||
cameraSource = null
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
@Throws(IOException::class)
|
||||
private fun startIfReady() {
|
||||
if (startRequested && surfaceAvailable) {
|
||||
cameraSource!!.start()
|
||||
if (overlay != null) {
|
||||
val size = cameraSource!!.previewSize!!
|
||||
val min = Math.min(size.width, size.height)
|
||||
val max = Math.max(size.width, size.height)
|
||||
if (isPortraitMode) {
|
||||
// Swap width and height sizes when in portrait, since it will be rotated by
|
||||
// 90 degrees
|
||||
overlay!!.setCameraInfo(min, max, cameraSource!!.cameraFacing)
|
||||
} else {
|
||||
overlay!!.setCameraInfo(max, min, cameraSource!!.cameraFacing)
|
||||
}
|
||||
overlay!!.clear()
|
||||
}
|
||||
startRequested = false
|
||||
}
|
||||
}
|
||||
|
||||
private inner class SurfaceCallback : SurfaceHolder.Callback {
|
||||
override fun surfaceCreated(surface: SurfaceHolder) {
|
||||
surfaceAvailable = true
|
||||
try {
|
||||
startIfReady()
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Could not start camera source.", e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun surfaceDestroyed(surface: SurfaceHolder) {
|
||||
surfaceAvailable = false
|
||||
}
|
||||
|
||||
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {}
|
||||
}
|
||||
|
||||
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
||||
var width = 320
|
||||
var height = 240
|
||||
if (cameraSource != null) {
|
||||
val size = cameraSource!!.previewSize
|
||||
if (size != null) {
|
||||
width = size.width
|
||||
height = size.height
|
||||
}
|
||||
}
|
||||
|
||||
// Swap width and height sizes when in portrait, since it will be rotated 90 degrees
|
||||
if (isPortraitMode) {
|
||||
val tmp = width
|
||||
width = height
|
||||
height = tmp
|
||||
}
|
||||
|
||||
val layoutWidth = right - left
|
||||
val layoutHeight = bottom - top
|
||||
|
||||
// Computes height and width for potentially doing fit width.
|
||||
var childWidth = layoutWidth
|
||||
var childHeight = (layoutWidth.toFloat() / width.toFloat() * height).toInt()
|
||||
|
||||
// If height is too tall using fit width, does fit height instead.
|
||||
if (childHeight > layoutHeight) {
|
||||
childHeight = layoutHeight
|
||||
childWidth = (layoutHeight.toFloat() / height.toFloat() * width).toInt()
|
||||
}
|
||||
|
||||
for (i in 0 until childCount) {
|
||||
getChildAt(i).layout(0, 0, childWidth, childHeight)
|
||||
Log.d(TAG, "Assigned view: $i")
|
||||
}
|
||||
|
||||
try {
|
||||
startIfReady()
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Could not start camera source.", e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = "MIDemoApp:Preview"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package cash.z.android.wallet.ui.util.vision
|
||||
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// 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.
|
||||
|
||||
/** Describing a frame info. */
|
||||
class FrameMetadata private constructor(val width: Int, val height: Int, val rotation: Int, val cameraFacing: Int) {
|
||||
|
||||
/** Builder of [FrameMetadata]. */
|
||||
class Builder {
|
||||
|
||||
private var width: Int = 0
|
||||
private var height: Int = 0
|
||||
private var rotation: Int = 0
|
||||
private var cameraFacing: Int = 0
|
||||
|
||||
fun setWidth(width: Int): Builder {
|
||||
this.width = width
|
||||
return this
|
||||
}
|
||||
|
||||
fun setHeight(height: Int): Builder {
|
||||
this.height = height
|
||||
return this
|
||||
}
|
||||
|
||||
fun setRotation(rotation: Int): Builder {
|
||||
this.rotation = rotation
|
||||
return this
|
||||
}
|
||||
|
||||
fun setCameraFacing(facing: Int): Builder {
|
||||
cameraFacing = facing
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): FrameMetadata {
|
||||
return FrameMetadata(width, height, rotation, cameraFacing)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
package cash.z.android.wallet.ui.util.vision
|
||||
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// 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.
|
||||
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import cash.z.android.wallet.ui.util.vision.GraphicOverlay.Graphic
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* A view which renders a series of custom graphics to be overlayed on top of an associated preview
|
||||
* (i.e., the camera preview). The creator can add graphics objects, update the objects, and remove
|
||||
* them, triggering the appropriate drawing and invalidation within the view.
|
||||
*
|
||||
*
|
||||
* Supports scaling and mirroring of the graphics relative the camera's preview properties. The
|
||||
* idea is that detection items are expressed in terms of a preview size, but need to be scaled up
|
||||
* to the full view size, and also mirrored in the case of the front-facing camera.
|
||||
*
|
||||
*
|
||||
* Associated [Graphic] items should use the following methods to convert to view
|
||||
* coordinates for the graphics that are drawn:
|
||||
*
|
||||
*
|
||||
* 1. [Graphic.scaleX] and [Graphic.scaleY] adjust the size of the
|
||||
* supplied value from the preview scale to the view scale.
|
||||
* 1. [Graphic.translateX] and [Graphic.translateY] adjust the
|
||||
* coordinate from the preview's coordinate system to the view coordinate system.
|
||||
*
|
||||
*/
|
||||
class GraphicOverlay(context: Context, attrs: AttributeSet) : View(context, attrs) {
|
||||
private val lock = Any()
|
||||
private var previewWidth: Int = 0
|
||||
private var widthScaleFactor = 1.0f
|
||||
private var previewHeight: Int = 0
|
||||
private var heightScaleFactor = 1.0f
|
||||
private val graphics = ArrayList<Graphic>()
|
||||
|
||||
/**
|
||||
* Base class for a custom graphics object to be rendered within the graphic overlay. Subclass
|
||||
* this and implement the [Graphic.draw] method to define the graphics element. Add
|
||||
* instances to the overlay using [GraphicOverlay.add].
|
||||
*/
|
||||
abstract class Graphic(private val overlay: GraphicOverlay) {
|
||||
|
||||
/** Returns the application context of the app. */
|
||||
val applicationContext: Context
|
||||
get() = overlay.context.applicationContext
|
||||
|
||||
/**
|
||||
* Draw the graphic on the supplied canvas. Drawing should use the following methods to convert
|
||||
* to view coordinates for the graphics that are drawn:
|
||||
*
|
||||
*
|
||||
* 1. [Graphic.scaleX] and [Graphic.scaleY] adjust the size of the
|
||||
* supplied value from the preview scale to the view scale.
|
||||
* 1. [Graphic.translateX] and [Graphic.translateY] adjust the
|
||||
* coordinate from the preview's coordinate system to the view coordinate system.
|
||||
*
|
||||
*
|
||||
* @param canvas drawing canvas
|
||||
*/
|
||||
abstract fun draw(canvas: Canvas)
|
||||
|
||||
/**
|
||||
* Adjusts a horizontal value of the supplied value from the preview scale to the view scale.
|
||||
*/
|
||||
fun scaleX(horizontal: Float): Float {
|
||||
return horizontal * overlay.widthScaleFactor
|
||||
}
|
||||
|
||||
/** Adjusts a vertical value of the supplied value from the preview scale to the view scale. */
|
||||
fun scaleY(vertical: Float): Float {
|
||||
return vertical * overlay.heightScaleFactor
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts the x coordinate from the preview's coordinate system to the view coordinate system.
|
||||
*/
|
||||
fun translateX(x: Float): Float {
|
||||
return scaleX(x)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts the y coordinate from the preview's coordinate system to the view coordinate system.
|
||||
*/
|
||||
fun translateY(y: Float): Float {
|
||||
return scaleY(y)
|
||||
}
|
||||
|
||||
fun postInvalidate() {
|
||||
overlay.postInvalidate()
|
||||
}
|
||||
}
|
||||
|
||||
/** Removes all graphics from the overlay. */
|
||||
fun clear() {
|
||||
synchronized(lock) {
|
||||
graphics.clear()
|
||||
}
|
||||
postInvalidate()
|
||||
}
|
||||
|
||||
/** Adds a graphic to the overlay. */
|
||||
fun add(graphic: Graphic) {
|
||||
synchronized(lock) {
|
||||
graphics.add(graphic)
|
||||
}
|
||||
}
|
||||
|
||||
/** Removes a graphic from the overlay. */
|
||||
fun remove(graphic: Graphic) {
|
||||
synchronized(lock) {
|
||||
graphics.remove(graphic)
|
||||
}
|
||||
postInvalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the camera attributes for size and facing direction, which informs how to transform image
|
||||
* coordinates later.
|
||||
*/
|
||||
fun setCameraInfo(previewWidth: Int, previewHeight: Int, facing: Int) {
|
||||
synchronized(lock) {
|
||||
this.previewWidth = previewWidth
|
||||
this.previewHeight = previewHeight
|
||||
}
|
||||
postInvalidate()
|
||||
}
|
||||
|
||||
/** Draws the overlay with its associated graphic objects. */
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
synchronized(lock) {
|
||||
if (previewWidth != 0 && previewHeight != 0) {
|
||||
widthScaleFactor = canvas.width.toFloat() / previewWidth.toFloat()
|
||||
heightScaleFactor = canvas.height.toFloat() / previewHeight.toFloat()
|
||||
}
|
||||
|
||||
for (graphic in graphics) {
|
||||
graphic.draw(canvas)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
package cash.z.android.wallet.ui.util
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.annotation.GuardedBy
|
||||
import cash.z.android.wallet.ui.util.vision.BitmapUtils
|
||||
import cash.z.android.wallet.ui.util.vision.FrameMetadata
|
||||
import cash.z.android.wallet.ui.util.vision.GraphicOverlay
|
||||
import com.google.android.gms.tasks.Task
|
||||
import com.google.firebase.ml.vision.common.FirebaseVisionImage
|
||||
import com.google.firebase.ml.vision.common.FirebaseVisionImageMetadata
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
/**
|
||||
* Abstract base class for ML Kit frame processors. Subclasses need to implement {@link
|
||||
* #onSuccess(T, FrameMetadata, GraphicOverlay)} to define what they want to with the detection
|
||||
* results and {@link #detectInImage(FirebaseVisionImage)} to specify the detector object.
|
||||
*
|
||||
* @param <T> The type of the detected feature.
|
||||
*/
|
||||
abstract class VisionProcessorBase<T> : VisionImageProcessor {
|
||||
|
||||
// To keep the latest images and its metadata.
|
||||
@GuardedBy("this")
|
||||
private var latestImage: ByteBuffer? = null
|
||||
|
||||
@GuardedBy("this")
|
||||
private var latestImageMetaData: FrameMetadata? = null
|
||||
|
||||
// To keep the images and metadata in process.
|
||||
@GuardedBy("this")
|
||||
private var processingImage: ByteBuffer? = null
|
||||
|
||||
@GuardedBy("this")
|
||||
private var processingMetaData: FrameMetadata? = null
|
||||
|
||||
@Synchronized
|
||||
override fun process(
|
||||
data: ByteBuffer,
|
||||
frameMetadata: FrameMetadata,
|
||||
graphicOverlay: GraphicOverlay
|
||||
) {
|
||||
latestImage = data
|
||||
latestImageMetaData = frameMetadata
|
||||
if (processingImage == null && processingMetaData == null) {
|
||||
processLatestImage(graphicOverlay)
|
||||
}
|
||||
}
|
||||
|
||||
// Bitmap version
|
||||
override fun process(bitmap: Bitmap, graphicOverlay: GraphicOverlay) {
|
||||
detectInVisionImage(
|
||||
null, /* bitmap */
|
||||
FirebaseVisionImage.fromBitmap(bitmap),
|
||||
null,
|
||||
graphicOverlay
|
||||
)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun processLatestImage(graphicOverlay: GraphicOverlay) {
|
||||
processingImage = latestImage
|
||||
processingMetaData = latestImageMetaData
|
||||
latestImage = null
|
||||
latestImageMetaData = null
|
||||
if (processingImage != null && processingMetaData != null) {
|
||||
processImage(processingImage!!, processingMetaData!!, graphicOverlay)
|
||||
}
|
||||
}
|
||||
|
||||
private fun processImage(
|
||||
data: ByteBuffer,
|
||||
frameMetadata: FrameMetadata,
|
||||
graphicOverlay: GraphicOverlay
|
||||
) {
|
||||
val metadata = FirebaseVisionImageMetadata.Builder()
|
||||
.setFormat(FirebaseVisionImageMetadata.IMAGE_FORMAT_NV21)
|
||||
.setWidth(frameMetadata.width)
|
||||
.setHeight(frameMetadata.height)
|
||||
.setRotation(frameMetadata.rotation)
|
||||
.build()
|
||||
|
||||
val bitmap = BitmapUtils.getBitmap(data, frameMetadata)
|
||||
detectInVisionImage(
|
||||
bitmap, FirebaseVisionImage.fromByteBuffer(data, metadata), frameMetadata,
|
||||
graphicOverlay
|
||||
)
|
||||
}
|
||||
|
||||
private fun detectInVisionImage(
|
||||
originalCameraImage: Bitmap?,
|
||||
image: FirebaseVisionImage,
|
||||
metadata: FrameMetadata?,
|
||||
graphicOverlay: GraphicOverlay
|
||||
) {
|
||||
detectInImage(image)
|
||||
.addOnSuccessListener { results ->
|
||||
onSuccess(
|
||||
originalCameraImage, results,
|
||||
metadata!!,
|
||||
graphicOverlay
|
||||
)
|
||||
processLatestImage(graphicOverlay)
|
||||
}
|
||||
.addOnFailureListener { e -> onFailure(e) }
|
||||
}
|
||||
|
||||
override fun stop() {}
|
||||
|
||||
protected abstract fun detectInImage(image: FirebaseVisionImage): Task<T>
|
||||
|
||||
/**
|
||||
* Callback that executes with a successful detection result.
|
||||
*
|
||||
* @param originalCameraImage hold the original image from camera, used to draw the background
|
||||
* image.
|
||||
*/
|
||||
protected abstract fun onSuccess(
|
||||
originalCameraImage: Bitmap?,
|
||||
results: T,
|
||||
frameMetadata: FrameMetadata,
|
||||
graphicOverlay: GraphicOverlay
|
||||
)
|
||||
|
||||
protected abstract fun onFailure(e: Exception)
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package cash.z.android.wallet.ui.util
|
||||
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// 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.
|
||||
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import cash.z.android.wallet.ui.util.vision.FrameMetadata
|
||||
import cash.z.android.wallet.ui.util.vision.GraphicOverlay
|
||||
import com.google.firebase.ml.common.FirebaseMLException
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
/** An inferface to process the images with different ML Kit detectors and custom image models. */
|
||||
interface VisionImageProcessor {
|
||||
|
||||
/** Processes the images with the underlying machine learning models. */
|
||||
@Throws(FirebaseMLException::class)
|
||||
fun process(data: ByteBuffer, frameMetadata: FrameMetadata, graphicOverlay: GraphicOverlay)
|
||||
|
||||
/** Processes the bitmap images. */
|
||||
fun process(bitmap: Bitmap, graphicOverlay: GraphicOverlay)
|
||||
|
||||
/** Stops the underlying machine learning model and release resources. */
|
||||
fun stop()
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2016 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.
|
||||
-->
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<TextureView
|
||||
android:id="@+id/texture_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
/>
|
||||
|
||||
</merge>
|
|
@ -0,0 +1,58 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2016 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>
|
||||
<declare-styleable name="CameraView">
|
||||
<!--
|
||||
Set this to true if you want the CameraView to adjust its bounds to preserve the aspect
|
||||
ratio of its camera preview.
|
||||
-->
|
||||
<attr name="android:adjustViewBounds"/>
|
||||
<!-- Direction the camera faces relative to device screen. -->
|
||||
<attr name="facing" format="enum">
|
||||
<!-- The camera device faces the opposite direction as the device's screen. -->
|
||||
<enum name="back" value="0"/>
|
||||
<!-- The camera device faces the same direction as the device's screen. -->
|
||||
<enum name="front" value="1"/>
|
||||
</attr>
|
||||
<!-- Aspect ratio of camera preview and pictures. -->
|
||||
<attr name="aspectRatio" format="string"/>
|
||||
<!-- Continuous auto focus mode. -->
|
||||
<attr name="autoFocus" format="boolean"/>
|
||||
<!-- The flash mode. -->
|
||||
<attr name="flash" format="enum">
|
||||
<!-- Flash will not be fired. -->
|
||||
<enum name="off" value="0"/>
|
||||
<!--
|
||||
Flash will always be fired during snapshot.
|
||||
The flash may also be fired during preview or auto-focus depending on the driver.
|
||||
-->
|
||||
<enum name="on" value="1"/>
|
||||
<!--
|
||||
Constant emission of light during preview, auto-focus and snapshot.
|
||||
This can also be used for video recording.
|
||||
-->
|
||||
<enum name="torch" value="2"/>
|
||||
<!--
|
||||
Flash will be fired automatically when required.
|
||||
The flash may be fired during preview, auto-focus, or snapshot depending on the
|
||||
driver.
|
||||
-->
|
||||
<enum name="auto" value="3"/>
|
||||
<!--
|
||||
Flash will be fired in red-eye reduction mode.
|
||||
-->
|
||||
<enum name="redEye" value="4"/>
|
||||
</attr>
|
||||
</declare-styleable>
|
||||
</resources>
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2016 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>
|
||||
<public name="facing" type="attr"/>
|
||||
<public name="aspectRatio" type="attr"/>
|
||||
<public name="autoFocus" type="attr"/>
|
||||
<public name="flash" type="attr"/>
|
||||
|
||||
<public name="Widget.CameraView" type="style"/>
|
||||
</resources>
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2016 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>
|
||||
|
||||
<style name="Widget.CameraView" parent="android:Widget">
|
||||
<item name="android:adjustViewBounds">false</item>
|
||||
<item name="facing">back</item>
|
||||
<item name="aspectRatio">4:3</item>
|
||||
<item name="autoFocus">true</item>
|
||||
<item name="flash">auto</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
Loading…
Reference in New Issue