Skip to content

Commit

Permalink
Merge pull request #426 from getbouncer/awush-on-scan-ready
Browse files Browse the repository at this point in the history
Simplify onScanReady
  • Loading branch information
awush-stripe authored Jul 19, 2021
2 parents ba90864 + 5818777 commit c2dfd62
Show file tree
Hide file tree
Showing 6 changed files with 59 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
startActivity(new Intent(this, SingleActivityDemo.class))
);

CardScanActivity.warmUp(this, API_KEY, true);
CardScanActivity.warmUp(this, API_KEY, true, false);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@ import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Parcelable
import android.util.Log
import android.view.Gravity
import android.view.View
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.fragment.app.Fragment
Expand All @@ -31,6 +29,7 @@ import com.getbouncer.scan.ui.util.setTextSizeByRes
import com.getbouncer.scan.ui.util.setVisible
import com.getbouncer.scan.ui.util.show
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
Expand Down Expand Up @@ -127,12 +126,27 @@ open class CardScanActivity :
* speed at which the scan occurs.
*
* @param context: A context to use for warming up the analyzers.
* @param apiKey: the API key used to warm up the ML models
* @param initializeNameAndExpiryExtraction: if true, include name and expiry extraction
* @param forImmediateUse: if true, attempt to use cached models instead of downloading by default
*/
@JvmStatic
fun warmUp(context: Context, apiKey: String, initializeNameAndExpiryExtraction: Boolean) {
CardScanFlow.warmUp(context, apiKey, initializeNameAndExpiryExtraction)
fun warmUp(context: Context, apiKey: String, initializeNameAndExpiryExtraction: Boolean, forImmediateUse: Boolean = false) {
GlobalScope.launch { prepareScan(context, apiKey, initializeNameAndExpiryExtraction, forImmediateUse) }
}

/**
* Warm up the analyzers and suspend the thread until it has completed.
*
* @param context: A context to use for warming up the analyzers.
* @param apiKey: the API key used to warm up the ML models
* @param initializeNameAndExpiryExtraction: if true, include name and expiry extraction
* @param forImmediateUse: if true, attempt to use cached models instead of downloading by default
*/
@JvmStatic
suspend fun prepareScan(context: Context, apiKey: String, initializeNameAndExpiryExtraction: Boolean, forImmediateUse: Boolean) =
CardScanFlow.prepareScan(context, apiKey, initializeNameAndExpiryExtraction, forImmediateUse)

/**
* Start the card scanner activity.
*
Expand All @@ -157,7 +171,7 @@ open class CardScanActivity :
enableEnterCardManually = enableEnterCardManually,
enableExpiryExtraction = enableExpiryExtraction,
enableNameExtraction = enableNameExtraction,
) ?: return
)

activity.startActivityForResult(intent, REQUEST_CODE)
}
Expand Down Expand Up @@ -187,7 +201,7 @@ open class CardScanActivity :
enableEnterCardManually = enableEnterCardManually,
enableExpiryExtraction = enableExpiryExtraction,
enableNameExtraction = enableNameExtraction,
) ?: return
)

fragment.startActivityForResult(intent, REQUEST_CODE)
}
Expand All @@ -209,19 +223,9 @@ open class CardScanActivity :
enableEnterCardManually: Boolean = false,
enableExpiryExtraction: Boolean = false,
enableNameExtraction: Boolean = false,
): Intent? {
): Intent {
Config.apiKey = apiKey

if (!CardScanFlow.attemptedNameAndExpiryInitialization && (enableExpiryExtraction || enableNameExtraction)) {
Log.e(
Config.logTag,
"Attempting to run name and expiry without initializing text detector. " +
"Please invoke the warmup() function with initializeNameAndExpiryExtraction to true."
)
showNameAndExpiryInitializationError(context)
return null
}

return Intent(context, CardScanActivity::class.java)
.putExtra(PARAM_ENABLE_ENTER_MANUALLY, enableEnterCardManually)
.putExtra(PARAM_ENABLE_EXPIRY_EXTRACTION, enableExpiryExtraction)
Expand Down Expand Up @@ -268,21 +272,6 @@ open class CardScanActivity :
*/
@JvmStatic
fun isScanReady() = CardScanFlow.isScanReady()

/**
* Determine if the optional scan models are available (have been warmed up)
*/
@JvmStatic
fun isNameAndExpiryScanReady() = CardScanFlow.isNameAndExpiryScanReady()

private fun showNameAndExpiryInitializationError(context: Context) {
AlertDialog.Builder(context)
.setTitle(R.string.bouncer_name_and_expiry_initialization_error)
.setMessage(R.string.bouncer_name_and_expiry_initialization_error_message)
.setPositiveButton(R.string.bouncer_name_and_expiry_initialization_error_ok) { dialog, _ -> dialog.dismiss() }
.setCancelable(false)
.show()
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.getbouncer.scan.framework.AggregateResultListener
import com.getbouncer.scan.framework.AnalyzerLoopErrorListener
import com.getbouncer.scan.framework.AnalyzerPool
import com.getbouncer.scan.framework.Config
import com.getbouncer.scan.framework.FetchedData
import com.getbouncer.scan.framework.FiniteAnalyzerLoop
import com.getbouncer.scan.framework.ProcessBoundAnalyzerLoop
import com.getbouncer.scan.framework.time.Duration
Expand All @@ -36,12 +37,15 @@ import com.getbouncer.scan.payment.ml.SSDOcrModelManager
import com.getbouncer.scan.payment.ml.TextDetect
import com.getbouncer.scan.ui.ScanFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext

data class SavedFrame(
val pan: String?,
Expand All @@ -67,32 +71,32 @@ open class CardScanFlow(
private const val MAX_COMPLETION_LOOP_FRAMES_FAST_DEVICE = 8
private const val MAX_COMPLETION_LOOP_FRAMES_SLOW_DEVICE = 5

/**
* This field represents whether the flow was initialized with name and expiry enabled.
*/
var attemptedNameAndExpiryInitialization = false
private set

/**
* Warm up the analyzers for card scanner. This method is optional, but will increase the speed at which the
* scan occurs.
*
* @param context: A context to use for warming up the analyzers.
*/
@JvmStatic
fun warmUp(context: Context, apiKey: String, initializeNameAndExpiryExtraction: Boolean) {
suspend fun prepareScan(
context: Context,
apiKey: String,
initializeNameAndExpiryExtraction: Boolean,
forImmediateUse: Boolean,
) = withContext(Dispatchers.IO) {
Config.apiKey = apiKey
val deferredFetchers = mutableListOf<Deferred<FetchedData>>()

// pre-fetch all of the models used by this flow.
GlobalScope.launch(Dispatchers.IO) { SSDOcrModelManager.fetchModel(context, forImmediateUse = false) }
GlobalScope.launch(Dispatchers.IO) { CardDetectModelManager.fetchModel(context, forImmediateUse = false) }
deferredFetchers.add(async { SSDOcrModelManager.fetchModel(context, forImmediateUse) })
deferredFetchers.add(async { CardDetectModelManager.fetchModel(context, forImmediateUse) })

if (initializeNameAndExpiryExtraction) {
attemptedNameAndExpiryInitialization = true
GlobalScope.launch(Dispatchers.IO) { TextDetectModelManager.fetchModel(context, forImmediateUse = false) }
GlobalScope.launch(Dispatchers.IO) { AlphabetDetectModelManager.fetchModel(context, forImmediateUse = false) }
GlobalScope.launch(Dispatchers.IO) { ExpiryDetectModelManager.fetchModel(context, forImmediateUse = false) }
deferredFetchers.add(async { TextDetectModelManager.fetchModel(context, forImmediateUse) })
deferredFetchers.add(async { AlphabetDetectModelManager.fetchModel(context, forImmediateUse) })
deferredFetchers.add(async { ExpiryDetectModelManager.fetchModel(context, forImmediateUse) })
}

deferredFetchers.fold(true) { acc, deferred -> acc && deferred.await().successfullyFetched }
}

/**
Expand All @@ -105,13 +109,7 @@ open class CardScanFlow(
* Determine if the scan models are available (have been warmed up)
*/
@JvmStatic
fun isScanReady() = SSDOcrModelManager.isReady() && CardDetectModelManager.isReady()

/**
* Determine if the optional scan models are available (have been warmed up)
*/
@JvmStatic
fun isNameAndExpiryScanReady() = TextDetectModelManager.isReady() && AlphabetDetectModelManager.isReady() && ExpiryDetectModelManager.isReady()
fun isScanReady() = runBlocking { SSDOcrModelManager.isReady() && CardDetectModelManager.isReady() }
}

/**
Expand Down
4 changes: 0 additions & 4 deletions cardscan-ui/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,4 @@
<string name="bouncer_processing_card">Processing, please wait</string>

<string name="bouncer_enter_card_manually" description="Text shown at the bottom of the screen offering for the user to type in card details manually">Enter card manually</string>

<string name="bouncer_name_and_expiry_initialization_error" description="Title of the dialog shown to the user when attempting to extract name and expiry from a card when models have not been warmed up">Config Problem</string>
<string name="bouncer_name_and_expiry_initialization_error_message" description="Message of the dialog shown to the user when attempting to extract name and expiry from a card when models have not been warmed up">Please initialize name/expiry models first in the warmup() function.</string>
<string name="bouncer_name_and_expiry_initialization_error_ok" description="Affirmative button shown as part of the name or expiry extraction error dialog">OK</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ interface Fetcher {
* @param forImmediateUse: if there is a cached version of the model, return that immediately instead of downloading a new model
*/
suspend fun fetchData(forImmediateUse: Boolean, isOptional: Boolean): FetchedData

suspend fun isCached(): Boolean
}

/**
Expand All @@ -135,6 +137,8 @@ abstract class ResourceFetcher : Fetcher {
modelHashAlgorithm = hashAlgorithm,
assetFileName = assetFileName,
)

override suspend fun isCached(): Boolean = true
}

/**
Expand Down Expand Up @@ -226,6 +230,11 @@ sealed class WebFetcher(protected val context: Context) : Fetcher {
}
}

override suspend fun isCached(): Boolean = when (val meta = tryFetchLatestCachedData()) {
is FetchedModelFileMeta -> meta.modelFile != null
is FetchedModelResourceMeta -> true
}

/**
* Get information about what version of the model to download.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,25 @@ import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

abstract class ModelManager {

private lateinit var fetcher: Fetcher
private val fetcherMutex = Mutex()
private var successfullyFetched = false

private var onFetch: ((success: Boolean) -> Unit)? = null

suspend fun fetchModel(context: Context, forImmediateUse: Boolean, isOptional: Boolean = false): FetchedData {
fetcherMutex.withLock {
if (!this::fetcher.isInitialized) {
fetcher = getModelFetcher(context.applicationContext)
}
}
return fetcher.fetchData(forImmediateUse, isOptional).also { successfullyFetched = it.successfullyFetched }
return fetcher.fetchData(forImmediateUse, isOptional).also {
onFetch?.invoke(it.successfullyFetched)
}
}

fun isReady() = successfullyFetched
suspend fun isReady() = fetcherMutex.withLock {
if (this::fetcher.isInitialized) fetcher.isCached() else false
}

@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
abstract fun getModelFetcher(context: Context): Fetcher
Expand Down

0 comments on commit c2dfd62

Please sign in to comment.