checkpoint: camera works for qr scan

This commit is contained in:
Kevin Gorham 2019-02-16 03:47:39 -05:00 committed by Kevin Gorham
parent e27e10315a
commit e26d66cc3b
58 changed files with 4484 additions and 58 deletions

View File

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

View File

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

View File

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

View File

@ -30,6 +30,7 @@ import javax.inject.Singleton
ReceiveFragmentModule::class,
RequestFragmentModule::class,
SendFragmentModule::class,
ScanFragmentModule::class,
SettingsFragmentModule::class
]
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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..."

View File

@ -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() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="z_address_min_length">88</integer>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="zec_abbreviation">TAZ</string>
</resources>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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