Skip to content

Commit

Permalink
Parity signer/retrieve signature (#377)
Browse files Browse the repository at this point in the history
* Extract scanning into base class

* Scan screen draft

* Prevent expiration dialog from showing twice

* Share expiration dialog logic between show and scan screens

* Fix expiration presenting

* Return mortality period

* Code style

* Fix qr code width on long devices

* Fix - scan scree breaks dapp brower webview

* Center scan labels horizontally
  • Loading branch information
valentunn authored Aug 5, 2022
1 parent a1475e3 commit 3327c05
Show file tree
Hide file tree
Showing 34 changed files with 875 additions and 231 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import io.novafoundation.nova.feature_account_impl.presentation.node.details.Nod
import io.novafoundation.nova.feature_account_impl.presentation.paritySigner.connect.ParitySignerAccountPayload
import io.novafoundation.nova.feature_account_impl.presentation.paritySigner.connect.finish.FinishImportParitySignerFragment
import io.novafoundation.nova.feature_account_impl.presentation.paritySigner.connect.preview.PreviewImportParitySignerFragment
import io.novafoundation.nova.feature_account_impl.presentation.paritySigner.sign.scan.ScanSignParitySignerFragment
import io.novafoundation.nova.feature_account_impl.presentation.paritySigner.sign.scan.model.ScanSignParitySignerPayload
import io.novafoundation.nova.feature_account_impl.presentation.pincode.PinCodeAction
import io.novafoundation.nova.feature_account_impl.presentation.pincode.PincodeFragment
import io.novafoundation.nova.feature_account_impl.presentation.pincode.ToolbarConfiguration
Expand Down Expand Up @@ -499,6 +501,16 @@ class Navigator(
navController?.navigate(R.id.action_previewImportParitySignerFragment_to_finishImportParitySignerFragment, bundle)
}

override fun openScanParitySignerSignature(payload: ScanSignParitySignerPayload) {
val bundle = ScanSignParitySignerFragment.getBundle(payload)

navController?.navigate(R.id.action_showSignParitySignerFragment_to_scanSignParitySignerFragment, bundle)
}

override fun finishParitySignerFlow() {
navController?.navigate(R.id.action_finish_parity_signer_flow)
}

override fun openCreateWatchWallet() {
navController?.navigate(R.id.action_welcomeFragment_to_createWatchWalletFragment)
}
Expand Down
32 changes: 28 additions & 4 deletions app/src/main/res/navigation/sign_parity_signer_graph.xml
Original file line number Diff line number Diff line change
@@ -1,15 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/sign_parity_signer_graph"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/sign_parity_signer_graph"
app:startDestination="@id/showSignParitySignerFragment">

<action
android:id="@+id/action_finish_parity_signer_flow"
app:enterAnim="@anim/fragment_close_enter"
app:exitAnim="@anim/fragment_close_exit"
app:popEnterAnim="@anim/fragment_open_enter"
app:popExitAnim="@anim/fragment_open_exit"
app:popUpTo="@id/sign_parity_signer_graph"
app:popUpToInclusive="true" />

<fragment
app:useAdd="true"
android:id="@+id/showSignParitySignerFragment"
tools:layout="@layout/fragment_sign_parity_signer_show"
android:name="io.novafoundation.nova.feature_account_impl.presentation.paritySigner.sign.show.ShowSignParitySignerFragment"
android:label="FinishImportParitySignerFragment" />
android:label="FinishImportParitySignerFragment"
app:useAdd="true"
tools:layout="@layout/fragment_sign_parity_signer_show">

<action
android:id="@+id/action_showSignParitySignerFragment_to_scanSignParitySignerFragment"
app:destination="@id/scanSignParitySignerFragment"
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit" />
</fragment>

<fragment
app:useAdd="true"
android:id="@+id/scanSignParitySignerFragment"
android:name="io.novafoundation.nova.feature_account_impl.presentation.paritySigner.sign.scan.ScanSignParitySignerFragment"
android:label="ScanSignParitySignerFragment"
tools:layout="@layout/fragment_sign_parity_signer_scan" />
</navigation>
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,5 @@ fun <P> ActionAwaitableMixin.Factory.confirmingAction(): ConfirmationAwaitable<P
fun <P> ActionAwaitableMixin.Factory.confirmingOrDenyingAction(): ConfirmOrDenyAwaitable<P> = create()

suspend fun <R> ActionAwaitableMixin.Presentation<Unit, R>.awaitAction() = awaitAction(Unit)

fun ActionAwaitableMixin<*, *>.hasAlreadyTriggered(): Boolean = awaitableActionLiveData.value != null
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package io.novafoundation.nova.common.presentation.scan

import android.view.WindowManager
import androidx.annotation.CallSuper
import io.novafoundation.nova.common.base.BaseFragment
import io.novafoundation.nova.common.utils.permissions.setupPermissionAsker

abstract class ScanQrFragment<V : ScanQrViewModel> : BaseFragment<V>() {

abstract val scanView: ScanView

@CallSuper
override fun initViews() {
startScanning()
}

@CallSuper
override fun subscribe(viewModel: V) {
viewModel.scanningAvailable.observe { scanningAvailable ->
if (scanningAvailable) {
scanView.resume()
} else {
scanView.pause()
}
}

viewModel.resetScanningEvent.observeEvent {
startScanning()
}

setupPermissionAsker(viewModel)
}

override fun onStart() {
super.onStart()

viewModel.onStart()
}

override fun onResume() {
super.onResume()

if (viewModel.scanningAvailable.value) {
scanView.resume()
}

requireActivity().window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}

override fun onPause() {
super.onPause()

scanView.pause()

requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}

private fun startScanning() {
scanView.startDecoding { viewModel.onScanned(it) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.novafoundation.nova.common.presentation.scan

import android.Manifest
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import io.novafoundation.nova.common.base.BaseViewModel
import io.novafoundation.nova.common.utils.Event
import io.novafoundation.nova.common.utils.permissions.PermissionsAsker
import io.novafoundation.nova.common.utils.sendEvent
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch

abstract class ScanQrViewModel(
private val permissionsAsker: PermissionsAsker.Presentation,
) : BaseViewModel(), PermissionsAsker by permissionsAsker {

val scanningAvailable = MutableStateFlow(false)

private val _resetScanningEvent = MutableLiveData<Event<Unit>>()
val resetScanningEvent: LiveData<Event<Unit>> = _resetScanningEvent

protected abstract suspend fun scanned(result: String)

fun onScanned(result: String) {
launch {
scanned(result)
}
}

fun onStart() {
requirePermissions()
}

protected fun resetScanning() {
_resetScanningEvent.sendEvent()
}

private fun requirePermissions() = launch {
val granted = permissionsAsker.requirePermissionsOrExit(Manifest.permission.CAMERA)

scanningAvailable.value = granted
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package io.novafoundation.nova.common.presentation.scan

import android.content.Context
import android.graphics.Rect
import android.util.AttributeSet
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.TextView
import com.google.zxing.BarcodeFormat
import com.google.zxing.ResultPoint
import com.journeyapps.barcodescanner.BarcodeCallback
import com.journeyapps.barcodescanner.BarcodeResult
import com.journeyapps.barcodescanner.DefaultDecoderFactory
import io.novafoundation.nova.common.R
import io.novafoundation.nova.common.utils.WithContextExtensions
import io.novafoundation.nova.common.utils.makeVisible
import io.novafoundation.nova.common.utils.useAttributes
import kotlinx.android.synthetic.main.view_scan.view.viewScanScanner
import kotlinx.android.synthetic.main.view_scan.view.viewScanSubtitle
import kotlinx.android.synthetic.main.view_scan.view.viewScanTitle
import kotlinx.android.synthetic.main.view_scan.view.viewScanViewFinder

class ScanView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr), WithContextExtensions by WithContextExtensions(context) {

init {
View.inflate(context, R.layout.view_scan, this)

setupDecoder()

viewScanViewFinder.setCameraPreview(viewScanScanner)

viewScanViewFinder.onFinderRectChanges {
positionLabels(it)
}

attrs?.let(::applyAttributes)
}

val subtitle: TextView
get() = viewScanSubtitle

fun resume() {
viewScanScanner.resume()
}

fun pause() {
viewScanScanner.pause()
}

inline fun startDecoding(crossinline onScanned: (String) -> Unit) {
viewScanScanner.decodeSingle(object : BarcodeCallback {
override fun barcodeResult(result: BarcodeResult) {
onScanned(result.toString())
}

override fun possibleResultPoints(resultPoints: MutableList<ResultPoint>?) {}
})
}

override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)

if (!changed) return

viewScanViewFinder.framingRect?.let {
positionLabels(it)
}
}

fun setTitle(title: String) {
viewScanTitle.text = title
}

fun setSubtitle(subtitle: String) {
viewScanSubtitle.text = subtitle
}

private fun setupDecoder() {
val charSet = null
val formats = listOf(BarcodeFormat.QR_CODE)
val hints = null
val inverted = false

viewScanScanner.decoderFactory = DefaultDecoderFactory(
formats,
hints,
charSet,
inverted
)
}

private fun positionLabels(finderRect: Rect) {
viewScanTitle.doIfHasText { positionTitle(finderRect) }
viewScanSubtitle.doIfHasText { positionSubTitle(finderRect) }
}

private inline fun TextView.doIfHasText(action: () -> Unit) {
if (text.isNotEmpty()) action()
}

private fun positionTitle(finderRect: Rect) {
val rectTop = finderRect.top

// how much finderRect offsets from center of the screen + half of textView height since it is originally centered itself
val requiredBottomMargin = height / 2 - rectTop + viewScanTitle.height / 2

viewScanTitle.layoutParams = LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply {
gravity = Gravity.CENTER
setMargins(16.dp, 0, 16.dp, requiredBottomMargin + 24.dp)
}
viewScanTitle.makeVisible()
}

private fun positionSubTitle(finderRect: Rect) {
val rectBottom = finderRect.bottom

// how much finderRect offsets from center of the screen + half of textView height since it is originally centered itself
val requiredTopMargin = rectBottom - height / 2 + viewScanSubtitle.height / 2

viewScanSubtitle.layoutParams = LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply {
gravity = Gravity.CENTER
setMargins(16.dp, requiredTopMargin + 24.dp, 16.dp, 0)
}
viewScanSubtitle.makeVisible()
}

private fun applyAttributes(attrs: AttributeSet) = context.useAttributes(attrs, R.styleable.ScanView) { typedArray ->
val title = typedArray.getString(R.styleable.ScanView_title)
title?.let(::setTitle)

val subTitle = typedArray.getString(R.styleable.ScanView_subTitle)
subTitle?.let(::setSubtitle)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,5 @@ fun ByteArray?.optionalContentEquals(other: ByteArray?): Boolean {
fun Uri.Builder.appendNullableQueryParameter(name: String, value: String?) = apply {
value?.let { appendQueryParameter(name, value) }
}

fun ByteArray.dropBytes(count: Int) = copyOfRange(count, size)
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package io.novafoundation.nova.common.utils

interface SharedState<T> {

fun get(): T?
fun getOrNull(): T?
}

fun <T> SharedState<T>.getOrThrow(): T = getOrNull() ?: throw IllegalStateException("State is null")

interface MutableSharedState<T> : SharedState<T> {

fun set(value: T)
Expand All @@ -18,7 +20,7 @@ class DefaultMutableSharedState<T> : MutableSharedState<T> {
private var value: T? = null

@Synchronized
override fun get(): T? {
override fun getOrNull(): T? {
return value
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import android.os.CountDownTimer
import android.widget.CompoundButton
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleCoroutineScope
import io.novafoundation.nova.common.R
import io.novafoundation.nova.common.utils.bindTo
import io.novafoundation.nova.common.utils.format
import io.novafoundation.nova.common.utils.formatting.TimerValue
import io.novafoundation.nova.common.utils.makeGone
import io.novafoundation.nova.common.utils.makeVisible
import io.novafoundation.nova.common.utils.onDestroy
import kotlinx.coroutines.flow.MutableStateFlow
import kotlin.time.Duration
import kotlin.time.ExperimentalTime
Expand All @@ -22,14 +24,16 @@ private val TIMER_TAG = R.string.common_time_left
fun TextView.startTimer(
value: TimerValue,
@StringRes customMessageFormat: Int? = null,
lifecycle: Lifecycle? = null,
onTick: ((view: TextView, millisUntilFinished: Long) -> Unit)? = null,
onFinish: ((view: TextView) -> Unit)? = null
) = startTimer(value.millis, value.millisCalculatedAt, customMessageFormat, onTick, onFinish)
) = startTimer(value.millis, value.millisCalculatedAt, lifecycle, customMessageFormat, onTick, onFinish)

@OptIn(ExperimentalTime::class)
fun TextView.startTimer(
millis: Long,
millisCalculatedAt: Long? = null,
lifecycle: Lifecycle? = null,
@StringRes customMessageFormat: Int? = null,
onTick: ((view: TextView, millisUntilFinished: Long) -> Unit)? = null,
onFinish: ((view: TextView) -> Unit)? = null
Expand Down Expand Up @@ -68,6 +72,10 @@ fun TextView.startTimer(
}
}

lifecycle?.onDestroy {
newTimer.cancel()
}

newTimer.start()

setTag(TIMER_TAG, newTimer)
Expand Down
Loading

0 comments on commit 3327c05

Please sign in to comment.