Initial commit
This initial commit includes HUSH specific changes starting at this commit:
d14637012c
This commit is contained in:
53
app/src/main/java/cash/z/ecc/android/ext/Const.kt
Normal file
53
app/src/main/java/cash/z/ecc/android/ext/Const.kt
Normal file
@@ -0,0 +1,53 @@
|
||||
package cash.z.ecc.android.ext
|
||||
|
||||
import cash.z.ecc.android.BuildConfig
|
||||
|
||||
object Const {
|
||||
/**
|
||||
* Named objects for Dependency Injection.
|
||||
*/
|
||||
object Name {
|
||||
/** application data other than cryptographic keys */
|
||||
const val APP_PREFS = "const.name.app_prefs"
|
||||
const val BEFORE_SYNCHRONIZER = "const.name.before_synchronizer"
|
||||
const val SYNCHRONIZER = "const.name.synchronizer"
|
||||
}
|
||||
|
||||
/**
|
||||
* App preference key names.
|
||||
*/
|
||||
object Pref {
|
||||
const val FIRST_USE_VIEW_TX = "const.pref.first_use_view_tx"
|
||||
const val EASTER_EGG_TRIGGERED_SHIELDING = "const.pref.easter_egg_shielding"
|
||||
const val FEEDBACK_ENABLED = "const.pref.feedback_enabled"
|
||||
const val SERVER_HOST = "const.pref.server_host"
|
||||
const val SERVER_PORT = "const.pref.server_port"
|
||||
}
|
||||
|
||||
/**
|
||||
* Constants used for wallet backup.
|
||||
*/
|
||||
object Backup {
|
||||
const val SEED = "cash.z.ecc.android.SEED"
|
||||
const val SEED_PHRASE = "cash.z.ecc.android.SEED_PHRASE"
|
||||
const val HAS_SEED = "cash.z.ecc.android.HAS_SEED"
|
||||
const val HAS_SEED_PHRASE = "cash.z.ecc.android.HAS_SEED_PHRASE"
|
||||
const val HAS_BACKUP = "cash.z.ecc.android.HAS_BACKUP"
|
||||
|
||||
// Config
|
||||
const val VIEWING_KEY = "cash.z.ecc.android.VIEWING_KEY"
|
||||
const val PUBLIC_KEY = "cash.z.ecc.android.PUBLIC_KEY"
|
||||
const val BIRTHDAY_HEIGHT = "cash.z.ecc.android.BIRTHDAY_HEIGHT"
|
||||
}
|
||||
|
||||
/**
|
||||
* Default values to use application-wide. Ideally, this set of values should remain very short.
|
||||
*/
|
||||
object Default {
|
||||
object Server {
|
||||
// If you've forked the ECC repo, change this to your hosted lightwalletd instance
|
||||
const val HOST = BuildConfig.DEFAULT_SERVER_URL
|
||||
const val PORT = 9067
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package cash.z.ecc.android.ext
|
||||
|
||||
import cash.z.ecc.android.ext.ConversionsUniform.FULL_FORMATTER
|
||||
import cash.z.ecc.android.ext.ConversionsUniform.LONG_SCALE
|
||||
import cash.z.ecc.android.ext.ConversionsUniform.SHORT_FORMATTER
|
||||
import cash.z.ecc.android.sdk.ext.Conversions
|
||||
import cash.z.ecc.android.sdk.ext.ZcashSdk
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import java.math.BigDecimal
|
||||
import java.math.MathContext
|
||||
import java.math.RoundingMode
|
||||
import java.text.DecimalFormat
|
||||
import java.text.NumberFormat
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Do the necessary conversions in one place
|
||||
*
|
||||
* "1.234" -> to zatoshi
|
||||
* (zecStringToZatoshi)
|
||||
* String.toZatoshi()
|
||||
*
|
||||
* 123123 -> to "1.2132"
|
||||
* (zatoshiToZecString)
|
||||
* Long.toZecString()
|
||||
*
|
||||
*/
|
||||
object ConversionsUniform {
|
||||
val ONE_ZEC_IN_ZATOSHI = BigDecimal(Zatoshi.ZATOSHI_PER_ZEC, MathContext.DECIMAL128)
|
||||
val LONG_SCALE = 8
|
||||
val SHORT_SCALE = 4
|
||||
val SHORT_FORMATTER = from(SHORT_SCALE, SHORT_SCALE)
|
||||
val FULL_FORMATTER = from(LONG_SCALE)
|
||||
|
||||
val roundingMode = RoundingMode.HALF_EVEN
|
||||
|
||||
private fun from(maxDecimals: Int = 8, minDecimals: Int = 0) = (NumberFormat.getNumberInstance(Locale("en", "USA")) as DecimalFormat).apply {
|
||||
// applyPattern("###.##")
|
||||
isParseBigDecimal = true
|
||||
roundingMode = roundingMode
|
||||
maximumFractionDigits = maxDecimals
|
||||
minimumFractionDigits = minDecimals
|
||||
minimumIntegerDigits = 1
|
||||
}
|
||||
}
|
||||
|
||||
object WalletZecFormmatter {
|
||||
fun toZatoshi(zecString: String): Long? {
|
||||
return toBigDecimal(zecString)?.multiply(Conversions.ONE_ZEC_IN_ZATOSHI, MathContext.DECIMAL128)?.toLong()
|
||||
}
|
||||
fun toZecStringShort(amount: Zatoshi?): String {
|
||||
return SHORT_FORMATTER.format((amount ?: Zatoshi(0)).toZec())
|
||||
}
|
||||
fun toZecStringFull(amount: Zatoshi?): String {
|
||||
return formatFull((amount ?: Zatoshi(0)).toZec())
|
||||
}
|
||||
fun formatFull(zec: BigDecimal): String {
|
||||
return FULL_FORMATTER.format(zec)
|
||||
}
|
||||
fun toBigDecimal(zecString: String?): BigDecimal? {
|
||||
if (zecString.isNullOrEmpty()) return BigDecimal.ZERO
|
||||
return try {
|
||||
// ignore commas and whitespace
|
||||
var sanitizedInput = zecString.filter { it.isDigit() or (it == '.') }
|
||||
BigDecimal.ZERO.max(FULL_FORMATTER.parse(sanitizedInput) as BigDecimal)
|
||||
} catch (t: Throwable) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// convert a zatoshi value to ZEC as a BigDecimal
|
||||
private fun Zatoshi?.toZec(): BigDecimal =
|
||||
BigDecimal(this?.value ?: 0L, MathContext.DECIMAL128)
|
||||
.divide(ConversionsUniform.ONE_ZEC_IN_ZATOSHI)
|
||||
.setScale(LONG_SCALE, ConversionsUniform.roundingMode)
|
||||
}
|
||||
192
app/src/main/java/cash/z/ecc/android/ext/Dialogs.kt
Normal file
192
app/src/main/java/cash/z/ecc/android/ext/Dialogs.kt
Normal file
@@ -0,0 +1,192 @@
|
||||
package cash.z.ecc.android.ext
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.text.Html
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.getSystemService
|
||||
import cash.z.ecc.android.R
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
|
||||
fun Context.showClearDataConfirmation(onDismiss: () -> Unit = {}, onCancel: () -> Unit = {}): Dialog {
|
||||
return MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.dialog_nuke_wallet_title)
|
||||
.setMessage(R.string.dialog_nuke_wallet_message)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.dialog_nuke_wallet_button_positive) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
onDismiss()
|
||||
onCancel()
|
||||
}
|
||||
.setNegativeButton(R.string.dialog_nuke_wallet_button_negative) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
onDismiss()
|
||||
getSystemService<ActivityManager>()?.clearApplicationUserData()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
fun Context.showUninitializedError(error: Throwable? = null, onDismiss: () -> Unit = {}): Dialog {
|
||||
return MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.dialog_error_uninitialized_title)
|
||||
.setMessage(R.string.dialog_error_uninitialized_message)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(getString(R.string.dialog_error_uninitialized_button_positive)) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
onDismiss()
|
||||
if (error != null) throw error
|
||||
}
|
||||
.setNegativeButton(getString(R.string.dialog_error_uninitialized_button_negative)) { dialog, _ ->
|
||||
showClearDataConfirmation(
|
||||
onDismiss,
|
||||
onCancel = {
|
||||
// do not let the user back into the app because we cannot recover from this case
|
||||
showUninitializedError(error, onDismiss)
|
||||
}
|
||||
)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
fun Context.showInvalidSeedPhraseError(error: Throwable? = null, onDismiss: () -> Unit = {}): Dialog {
|
||||
return MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.dialog_error_invalid_seed_phrase_title)
|
||||
.setMessage(getString(R.string.dialog_error_invalid_seed_phrase_message, error?.message ?: ""))
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(getString(R.string.dialog_error_invalid_seed_phrase_button_positive)) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
onDismiss()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
fun Context.showScanFailure(error: Throwable?, onCancel: () -> Unit = {}, onDismiss: () -> Unit = {}): Dialog {
|
||||
val message = if (error == null) {
|
||||
"Unknown error"
|
||||
} else {
|
||||
"${error.message}${if (error.cause != null) "\n\nCaused by: ${error.cause}" else ""}"
|
||||
}
|
||||
return MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.dialog_error_scan_failure_title)
|
||||
.setMessage(message)
|
||||
.setCancelable(true)
|
||||
.setPositiveButton(R.string.dialog_error_scan_failure_button_positive) { d, _ ->
|
||||
d.dismiss()
|
||||
onDismiss()
|
||||
}
|
||||
.setNegativeButton(R.string.dialog_error_scan_failure_button_negative) { d, _ ->
|
||||
d.dismiss()
|
||||
onCancel()
|
||||
onDismiss()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
fun Context.showCriticalMessage(@StringRes titleResId: Int, @StringRes messageResId: Int, onDismiss: () -> Unit = {}): Dialog {
|
||||
return showCriticalMessage(titleResId.toAppString(), messageResId.toAppString(), onDismiss)
|
||||
}
|
||||
|
||||
fun Context.showCriticalMessage(title: String, message: String, onDismiss: () -> Unit = {}): Dialog {
|
||||
return MaterialAlertDialogBuilder(this)
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(android.R.string.ok) { d, _ ->
|
||||
d.dismiss()
|
||||
onDismiss()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
fun Context.showCriticalProcessorError(error: Throwable?, onRetry: () -> Unit = {}): Dialog {
|
||||
return MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.dialog_error_processor_critical_title)
|
||||
.setMessage(error?.message ?: getString(R.string.dialog_error_processor_critical_message))
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.dialog_error_processor_critical_button_positive) { d, _ ->
|
||||
d.dismiss()
|
||||
onRetry()
|
||||
}
|
||||
.setNegativeButton(R.string.dialog_error_processor_critical_button_negative) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
throw error ?: RuntimeException("Critical error while processing blocks and the user chose to exit.")
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
fun Context.showUpdateServerCriticalError(userFacingMessage: String, onConfirm: () -> Unit = {}): Dialog {
|
||||
return MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.dialog_error_change_server_title)
|
||||
.setMessage(userFacingMessage)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.dialog_error_change_server_button_positive) { d, _ ->
|
||||
d.dismiss()
|
||||
onConfirm()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
fun Context.showUpdateServerDialog(positiveResId: Int = R.string.dialog_modify_server_button_positive, onCancel: () -> Unit = {}, onUpdate: () -> Unit = {}): Dialog {
|
||||
return MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.dialog_modify_server_title)
|
||||
.setMessage(R.string.dialog_modify_server_message)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(positiveResId) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
onUpdate()
|
||||
}
|
||||
.setNegativeButton(R.string.dialog_modify_server_button_negative) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
onCancel()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
fun Context.showRescanWalletDialog(quickDistance: String, quickEstimate: String, fullDistance: String, fullEstimate: String, onWipe: () -> Unit = {}, onFullRescan: () -> Unit = {}, onQuickRescan: () -> Unit = {}): Dialog {
|
||||
return MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.dialog_rescan_wallet_title)
|
||||
.setMessage(Html.fromHtml(getString(R.string.dialog_rescan_wallet_message, quickDistance, quickEstimate, fullDistance, fullEstimate)))
|
||||
.setCancelable(true)
|
||||
.setPositiveButton(R.string.dialog_rescan_wallet_button_positive) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
onQuickRescan()
|
||||
}
|
||||
.setNeutralButton(R.string.dialog_rescan_wallet_button_neutral) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
onWipe()
|
||||
}
|
||||
.setNegativeButton(R.string.dialog_rescan_wallet_button_negative) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
onFullRescan()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
fun Context.showConfirmation(title: String, message: String, positiveButton: String, negativeButton: String = "Cancel", onPositive: () -> Unit = {}): Dialog {
|
||||
return MaterialAlertDialogBuilder(this)
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(positiveButton) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
onPositive()
|
||||
}
|
||||
.setNegativeButton("Cancel") { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
/**
|
||||
* Error to show when the Rust libraries did not properly link. This problem can happen pretty often
|
||||
* during development when a build of the SDK failed to compile and resulted in an AAR file with no
|
||||
* shared libraries (*.so files) inside. In theory, this should never be seen by an end user but if
|
||||
* it does occur it is better to show a clean message explaining the situation. Nothing can be done
|
||||
* other than rebuilding the SDK or switching to a functional version.
|
||||
* As a developer, this error probably means that you need to comment out mavenLocal() as a repo.
|
||||
*/
|
||||
fun Context.showSharedLibraryCriticalError(e: Throwable): Dialog = showCriticalMessage(
|
||||
titleResId = R.string.dialog_error_critical_link_title,
|
||||
messageResId = R.string.dialog_error_critical_link_message,
|
||||
onDismiss = { throw e }
|
||||
)
|
||||
83
app/src/main/java/cash/z/ecc/android/ext/EditText.kt
Normal file
83
app/src/main/java/cash/z/ecc/android/ext/EditText.kt
Normal file
@@ -0,0 +1,83 @@
|
||||
package cash.z.ecc.android.ext
|
||||
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.inputmethod.EditorInfo.IME_ACTION_DONE
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import cash.z.ecc.android.sdk.ext.convertZecToZatoshi
|
||||
import cash.z.ecc.android.sdk.ext.safelyConvertToBigDecimal
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.util.twig
|
||||
|
||||
fun EditText.onEditorActionDone(block: (EditText) -> Unit) {
|
||||
this.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == IME_ACTION_DONE) {
|
||||
block(this)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fun EditText.limitDecimalPlaces(max: Int) {
|
||||
val editText = this
|
||||
addTextChangedListener(object : TextWatcher {
|
||||
var previousValue = ""
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
||||
// Cache the previous value
|
||||
previousValue = text.toString()
|
||||
}
|
||||
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
var textStr = text.toString()
|
||||
|
||||
if (textStr.isNotEmpty()) {
|
||||
val oldText = text.toString()
|
||||
val number = textStr.safelyConvertToBigDecimal()
|
||||
|
||||
if (number != null && number.scale() > 8) {
|
||||
// Prevent the user from adding a new decimal place somewhere in the middle if we're already at the limit
|
||||
if (editText.selectionStart == editText.selectionEnd && editText.selectionStart != textStr.length) {
|
||||
textStr = previousValue
|
||||
} else {
|
||||
textStr = WalletZecFormmatter.formatFull(number)
|
||||
}
|
||||
}
|
||||
|
||||
// Trim leading zeroes
|
||||
textStr = textStr.trimStart('0')
|
||||
// Append a zero if this results in an empty string or if the first symbol is not a digit
|
||||
if (textStr.isEmpty() || !textStr.first().isDigit()) {
|
||||
textStr = "0$textStr"
|
||||
}
|
||||
|
||||
// Restore the cursor position
|
||||
if (oldText != textStr) {
|
||||
val cursorPosition = editText.selectionEnd
|
||||
editText.setText(textStr)
|
||||
editText.setSelection(
|
||||
(cursorPosition - (oldText.length - textStr.length)).coerceIn(
|
||||
0,
|
||||
editText.text.toString().length
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun TextView.convertZecToZatoshi(): Zatoshi? {
|
||||
return try {
|
||||
text.toString().safelyConvertToBigDecimal()?.convertZecToZatoshi()
|
||||
} catch (t: Throwable) {
|
||||
twig("Failed to convert text to Zatoshi: $text")
|
||||
null
|
||||
}
|
||||
}
|
||||
69
app/src/main/java/cash/z/ecc/android/ext/Extensions.kt
Normal file
69
app/src/main/java/cash/z/ecc/android/ext/Extensions.kt
Normal file
@@ -0,0 +1,69 @@
|
||||
package cash.z.ecc.android.ext
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.fragment.app.Fragment
|
||||
import cash.z.ecc.android.sdk.model.WalletBalance
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.util.Bush
|
||||
import cash.z.ecc.android.util.Twig
|
||||
import cash.z.ecc.android.util.twig
|
||||
import java.util.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Distribute a string into evenly-sized chunks and then execute a function with each chunk.
|
||||
*
|
||||
* @param chunks the number of chunks to create
|
||||
* @param block a function to be applied to each zero-indexed chunk.
|
||||
*/
|
||||
fun <T> String.distribute(chunks: Int, block: (Int, String) -> T) {
|
||||
val charsPerChunk = length / chunks.toFloat()
|
||||
val wholeCharsPerChunk = charsPerChunk.toInt()
|
||||
val chunksWithExtra = ((charsPerChunk - wholeCharsPerChunk) * chunks).roundToInt()
|
||||
repeat(chunks) { i ->
|
||||
val part = if (i < chunksWithExtra) {
|
||||
substring(i * (wholeCharsPerChunk + 1), (i + 1) * (wholeCharsPerChunk + 1))
|
||||
} else {
|
||||
substring(i * wholeCharsPerChunk + chunksWithExtra, (i + 1) * wholeCharsPerChunk + chunksWithExtra)
|
||||
}
|
||||
block(i, part)
|
||||
}
|
||||
}
|
||||
|
||||
inline val WalletBalance.pending: Zatoshi
|
||||
get() = (this.total - this.available)
|
||||
|
||||
inline fun <R> tryWithWarning(message: String = "", block: () -> R): R? {
|
||||
return try {
|
||||
block()
|
||||
} catch (error: Throwable) {
|
||||
twig("WARNING: $message")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <E : Throwable, R> failWith(specificErrorType: E, block: () -> R): R {
|
||||
return try {
|
||||
block()
|
||||
} catch (error: Throwable) {
|
||||
throw specificErrorType
|
||||
}
|
||||
}
|
||||
|
||||
inline fun Fragment.locale(): Locale = context?.locale() ?: Locale.getDefault()
|
||||
|
||||
inline fun Context.locale(): Locale {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
resources.configuration.locales.get(0)
|
||||
} else {
|
||||
//noinspection deprecation
|
||||
resources.configuration.locale
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: add this to the SDK and if the trunk is a CompositeTwig, search through there before returning null
|
||||
inline fun <reified T> Twig.find(): T? {
|
||||
return if (Bush.trunk::class.java.isAssignableFrom(T::class.java)) Bush.trunk as T
|
||||
else null
|
||||
}
|
||||
9
app/src/main/java/cash/z/ecc/android/ext/Fragment.kt
Normal file
9
app/src/main/java/cash/z/ecc/android/ext/Fragment.kt
Normal file
@@ -0,0 +1,9 @@
|
||||
package cash.z.ecc.android.ext
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
/**
|
||||
* A safer alternative to [Fragment.requireContext], as it avoids leaking Fragment or Activity context
|
||||
* when Application context is often sufficient.
|
||||
*/
|
||||
fun Fragment.requireApplicationContext() = requireContext().applicationContext
|
||||
46
app/src/main/java/cash/z/ecc/android/ext/Int.kt
Normal file
46
app/src/main/java/cash/z/ecc/android/ext/Int.kt
Normal file
@@ -0,0 +1,46 @@
|
||||
package cash.z.ecc.android.ext
|
||||
|
||||
import android.content.res.Resources
|
||||
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.ecc.android.ZcashWalletApp
|
||||
|
||||
/**
|
||||
* Grab a color out of the application resources, using the default theme
|
||||
*/
|
||||
@ColorInt
|
||||
internal inline fun @receiver:ColorRes Int.toAppColor(): Int {
|
||||
return ResourcesCompat.getColor(ZcashWalletApp.instance.resources, this, ZcashWalletApp.instance.theme)
|
||||
}
|
||||
|
||||
/**
|
||||
* Grab a string from the application resources
|
||||
*/
|
||||
internal inline fun @receiver:StringRes Int.toAppString(lowercase: Boolean = false): String {
|
||||
return ZcashWalletApp.instance.getString(this).let {
|
||||
if (lowercase) it.toLowerCase() else it
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Grab a formatted string from the application resources
|
||||
*/
|
||||
internal inline fun @receiver:StringRes Int.toAppStringFormatted(vararg formatArgs: Any): String {
|
||||
return ZcashWalletApp.instance.getString(this, *formatArgs)
|
||||
}
|
||||
|
||||
/**
|
||||
* Grab an integer from the application resources
|
||||
*/
|
||||
internal inline fun @receiver:IntegerRes Int.toAppInt(): Int {
|
||||
return ZcashWalletApp.instance.resources.getInteger(this)
|
||||
}
|
||||
|
||||
fun Float.toPx() = this * Resources.getSystem().displayMetrics.density
|
||||
|
||||
fun Int.toPx() = (this * Resources.getSystem().displayMetrics.density + 0.5f).toInt()
|
||||
|
||||
fun Int.toDp() = (this / Resources.getSystem().displayMetrics.density + 0.5f).toInt()
|
||||
14
app/src/main/java/cash/z/ecc/android/ext/LifeCycleOwner.kt
Normal file
14
app/src/main/java/cash/z/ecc/android/ext/LifeCycleOwner.kt
Normal file
@@ -0,0 +1,14 @@
|
||||
package cash.z.ecc.android.ext
|
||||
|
||||
import android.view.View
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
fun <T : View> LifecycleOwner.onClick(view: T, throttle: Long = 250L, block: (T) -> Unit) {
|
||||
view.clicks().debounce(throttle).onEach {
|
||||
block(view)
|
||||
}.launchIn(this.lifecycleScope)
|
||||
}
|
||||
20
app/src/main/java/cash/z/ecc/android/ext/Spannable.kt
Normal file
20
app/src/main/java/cash/z/ecc/android/ext/Spannable.kt
Normal file
@@ -0,0 +1,20 @@
|
||||
package cash.z.ecc.android.ext
|
||||
|
||||
import android.text.Spanned
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.core.text.toSpannable
|
||||
|
||||
fun CharSequence.toColoredSpan(@ColorRes colorResId: Int, coloredPortion: String): CharSequence {
|
||||
return toSpannable().apply {
|
||||
val start = this@toColoredSpan.indexOf(coloredPortion)
|
||||
setSpan(ForegroundColorSpan(colorResId.toAppColor()), start, start + coloredPortion.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
}
|
||||
|
||||
fun CharSequence.toSplitColorSpan(@ColorRes startColorResId: Int, @ColorRes endColorResId: Int, startColorLength: Int): CharSequence {
|
||||
return toSpannable().apply {
|
||||
setSpan(ForegroundColorSpan(startColorResId.toAppColor()), 0, startColorLength - 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
setSpan(ForegroundColorSpan(endColorResId.toAppColor()), startColorLength, this@toSplitColorSpan.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
}
|
||||
81
app/src/main/java/cash/z/ecc/android/ext/View.kt
Normal file
81
app/src/main/java/cash/z/ecc/android/ext/View.kt
Normal file
@@ -0,0 +1,81 @@
|
||||
package cash.z.ecc.android.ext
|
||||
|
||||
import android.view.View
|
||||
import android.view.View.GONE
|
||||
import android.view.View.INVISIBLE
|
||||
import android.view.View.VISIBLE
|
||||
import cash.z.ecc.android.ui.MainActivity
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
|
||||
fun View.gone() {
|
||||
visibility = GONE
|
||||
}
|
||||
|
||||
fun View.invisible() {
|
||||
visibility = INVISIBLE
|
||||
}
|
||||
|
||||
fun View.visible() {
|
||||
visibility = VISIBLE
|
||||
}
|
||||
|
||||
// NOTE: avoid `visibleIf` function because the false case is ambiguous: would it be gone or invisible?
|
||||
|
||||
fun View.goneIf(isGone: Boolean) {
|
||||
visibility = if (isGone) GONE else VISIBLE
|
||||
}
|
||||
|
||||
fun View.invisibleIf(isInvisible: Boolean) {
|
||||
visibility = if (isInvisible) INVISIBLE else VISIBLE
|
||||
}
|
||||
|
||||
fun View.disabledIf(isDisabled: Boolean) {
|
||||
isEnabled = !isDisabled
|
||||
}
|
||||
|
||||
fun View.transparentIf(isTransparent: Boolean) {
|
||||
alpha = if (isTransparent) 0.0f else 1.0f
|
||||
}
|
||||
|
||||
fun View.onClickNavTo(navResId: Int, block: (() -> Any) = {}) {
|
||||
setOnClickListener {
|
||||
block()
|
||||
(context as? MainActivity)?.safeNavigate(navResId)
|
||||
?: throw IllegalStateException(
|
||||
"Cannot navigate from this activity. " +
|
||||
"Expected MainActivity but found ${context.javaClass.simpleName}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun View.onClickNavUp(block: (() -> Any) = {}) {
|
||||
setOnClickListener {
|
||||
block()
|
||||
(context as? MainActivity)?.navController?.navigateUp()
|
||||
?: throw IllegalStateException(
|
||||
"Cannot navigate from this activity. " +
|
||||
"Expected MainActivity but found ${context.javaClass.simpleName}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun View.onClickNavBack(block: (() -> Any) = {}) {
|
||||
setOnClickListener {
|
||||
block()
|
||||
(context as? MainActivity)?.navController?.popBackStack()
|
||||
?: throw IllegalStateException(
|
||||
"Cannot navigate from this activity. " +
|
||||
"Expected MainActivity but found ${context.javaClass.simpleName}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun View.clicks() = channelFlow<View> {
|
||||
setOnClickListener {
|
||||
trySend(this@clicks)
|
||||
}
|
||||
awaitClose {
|
||||
setOnClickListener(null)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user