1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

App | Settings | Blur mnemonic (#2391)

This commit is contained in:
Mikhail 2022-07-01 16:32:57 +03:00 committed by GitHub
parent 28a0397c8b
commit d219f74bf3
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 274 additions and 242 deletions

View file

@ -0,0 +1,125 @@
package com.anytypeio.anytype.ui.dashboard
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.view.animation.DecelerateInterpolator
import android.widget.TextView
import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans
import androidx.fragment.app.viewModels
import androidx.transition.ChangeBounds
import androidx.transition.TransitionManager
import androidx.transition.TransitionSet
import androidx.viewbinding.ViewBinding
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_utils.ext.dimen
import com.anytypeio.anytype.core_utils.ext.toast
import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment
import com.anytypeio.anytype.presentation.keychain.KeychainPhraseViewModel
import com.anytypeio.anytype.presentation.keychain.KeychainPhraseViewModelFactory
import com.anytypeio.anytype.presentation.keychain.KeychainViewState
import com.anytypeio.anytype.ui.profile.RoundedBackgroundColorSpan
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import javax.inject.Inject
abstract class BaseMnemonicFragment<T : ViewBinding> : BaseBottomSheetFragment<T>() {
protected val vm: KeychainPhraseViewModel by viewModels { factory }
private val fakePhrase by lazy {
val bg = RoundedBackgroundColorSpan(
requireContext().getColor(R.color.palette_light_ice),
dimen(R.dimen.shimmer_radius).toFloat(),
dimen(R.dimen.shimmer_radius).toFloat(),
dimen(R.dimen.shimmer_line_height).toFloat()
)
buildSpannedString { inSpans(bg) { append(SHIMMER_PHRASE) } }
}
protected abstract val keychain: TextView
@Inject
lateinit var factory: KeychainPhraseViewModelFactory
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
keychain.setOnClickListener { vm.onKeychainClicked() }
binding.root.setOnClickListener { vm.onRootClicked() }
vm.state.observe(viewLifecycleOwner) { render(it) }
setupFullHeight()
}
private fun render(state: KeychainViewState) {
when (state) {
is KeychainViewState.Displayed -> {
setKeychainPhrase(state.mnemonic)
copyMnemonicToClipboard(state.mnemonic)
}
is KeychainViewState.Blurred -> {
setBlurredPhrase()
}
}
}
private fun setupFullHeight() {
binding.apply { root.layoutParams.height = resources.displayMetrics.heightPixels }
val bottomSheetDialog = dialog as BottomSheetDialog
val behavior = bottomSheetDialog.behavior
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
private fun animateBlurChange() {
val transitionSet = TransitionSet().apply {
addTransition(ChangeBounds())
duration = ANIMATION_LENGTH
interpolator = DecelerateInterpolator(ANIMATION_FACTOR)
ordering = TransitionSet.ORDERING_TOGETHER
}
TransitionManager.beginDelayedTransition(binding.root as ViewGroup, transitionSet)
}
private fun setBlurredPhrase() {
keychain
animateBlurChange()
keychain.setShadowLayer(0f, 0f, 0f, 0)
keychain.setTextColor(requireContext().getColor(R.color.palette_light_ice))
keychain.setShadowLayer(dimen(R.dimen.shimmer_radius).toFloat(), 0f, 0f, 0)
keychain.text = fakePhrase
}
private fun setKeychainPhrase(mnemonic: String) {
animateBlurChange()
keychain.setTextColor(requireContext().getColor(R.color.keychain_text_color))
keychain.setShadowLayer(0f, 0f, 0f, 0)
keychain.text = mnemonic
}
private fun copyMnemonicToClipboard(keychainPhrase: String) {
try {
val clipboard =
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText(
DashboardMnemonicReminderDialog.MNEMONIC_LABEL,
keychainPhrase
)
clipboard.setPrimaryClip(clip)
toast("Recovery phrase copied to clipboard.")
} catch (e: Exception) {
toast("Could not copy your recovery phrase. Please try again later, or copy it manually.")
}
}
}
private const val SHIMMER_LENGTH = 50
private val SHIMMER_PHRASE = "- ".repeat(SHIMMER_LENGTH)
private const val ANIMATION_LENGTH = 700L
private const val ANIMATION_FACTOR = 2.5f

View file

@ -1,129 +1,41 @@
package com.anytypeio.anytype.ui.dashboard
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.graphics.BlurMaskFilter
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import android.widget.TextView
import com.anytypeio.anytype.analytics.base.EventsDictionary
import com.anytypeio.anytype.core_utils.ext.toast
import com.anytypeio.anytype.core_utils.ui.ViewState
import com.anytypeio.anytype.databinding.DialogDashboardKeychainPhraseBinding
import com.anytypeio.anytype.di.common.componentManager
import com.anytypeio.anytype.presentation.keychain.KeychainPhraseViewModel
import com.anytypeio.anytype.presentation.keychain.KeychainPhraseViewModelFactory
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import javax.inject.Inject
class DashboardMnemonicReminderDialog : BottomSheetDialogFragment(), Observer<ViewState<String>> {
private var _binding: DialogDashboardKeychainPhraseBinding? = null
private val binding: DialogDashboardKeychainPhraseBinding get() = _binding!!
private val vm : KeychainPhraseViewModel by viewModels { factory }
@Inject
lateinit var factory: KeychainPhraseViewModelFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
injectDependencies()
}
override fun onDestroy() {
super.onDestroy()
releaseDependencies()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = DialogDashboardKeychainPhraseBinding.inflate(inflater, container, false)
return _binding?.root
}
class DashboardMnemonicReminderDialog :
BaseMnemonicFragment<DialogDashboardKeychainPhraseBinding>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
vm.sendShowEvent(EventsDictionary.Type.firstSession)
setBlur()
binding.keychain.setOnClickListener {
if (binding.keychain.layerType == View.LAYER_TYPE_SOFTWARE) {
removeBlur()
}
}
binding.btnCopy.setOnClickListener {
vm.onCopyClickedFromScreenSettings()
copyMnemonicToClipboard()
}
binding.root.setOnClickListener {
setBlur()
}
vm.state.observe(viewLifecycleOwner, this)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private fun copyMnemonicToClipboard() {
try {
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText(MNEMONIC_LABEL, binding.keychain.text.toString())
clipboard.setPrimaryClip(clip)
toast("Recovery phrase copied to clipboard.")
} catch (e: Exception) {
toast("Could not copy your recovery phrase. Please try again later, or copy it manually.")
}
}
override fun onChanged(state: ViewState<String>) {
when (state) {
is ViewState.Success -> {
binding.keychain.text = state.data
}
is ViewState.Error -> {
// TODO
}
is ViewState.Loading -> {
// TODO
}
is ViewState.Init -> {
}
}
}
private fun setBlur() = with(binding.keychain) {
setLayerType(View.LAYER_TYPE_SOFTWARE, null)
val radius = textSize / 3
val filter = BlurMaskFilter(radius, BlurMaskFilter.Blur.NORMAL)
paint.maskFilter = filter
}
private fun removeBlur() = with(binding.keychain) {
setLayerType(View.LAYER_TYPE_NONE, null)
paint.maskFilter = null
isFocusable = true
setTextIsSelectable(true)
}
private fun injectDependencies() {
override fun injectDependencies() {
componentManager().keychainPhraseComponent.get().inject(this)
}
private fun releaseDependencies() {
override fun releaseDependencies() {
componentManager().keychainPhraseComponent.release()
}
override fun inflateBinding(
inflater: LayoutInflater,
container: ViewGroup?
): DialogDashboardKeychainPhraseBinding {
return DialogDashboardKeychainPhraseBinding.inflate(
inflater, container, false
)
}
override val keychain: TextView by lazy { binding.keychain }
companion object {
const val MNEMONIC_LABEL = "Your Anytype mnemonic phrase"
}

View file

@ -0,0 +1,68 @@
package com.anytypeio.anytype.ui.profile
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
import android.text.style.LineBackgroundSpan
import kotlin.math.abs
class RoundedBackgroundColorSpan(
backgroundColor: Int,
private val padding: Float,
private val radius: Float,
private val lineHeight: Float
) : LineBackgroundSpan {
companion object {
private const val NO_INIT = -1f
}
private val rect = RectF()
private val paint = Paint().apply {
color = backgroundColor
isAntiAlias = true
}
private var prevWidth = NO_INIT
private var prevRight = NO_INIT
override fun drawBackground(
c: Canvas,
p: Paint,
left: Int,
right: Int,
top: Int,
baseline: Int,
bottom: Int,
text: CharSequence,
start: Int,
end: Int,
lineNumber: Int
) {
val actualWidth = p.measureText(text, start, end) + 2f * padding
val widthDiff = abs(prevWidth - actualWidth)
val diffIsShort = widthDiff < 2f * radius
val width = if (lineNumber == 0) {
actualWidth
} else if ((actualWidth < prevWidth) && diffIsShort) {
prevWidth
} else if ((actualWidth > prevWidth) && diffIsShort) {
actualWidth + (2f * radius - widthDiff)
} else {
actualWidth
}
val shiftLeft = 0f - padding
val shiftRight = width + shiftLeft
val shiftGap = abs((bottom.toFloat() - top.toFloat() - lineHeight) /2f)
rect.set(shiftLeft, top.toFloat() + shiftGap, shiftRight, bottom.toFloat() - shiftGap)
c.drawRoundRect(rect, radius, radius, paint)
prevWidth = width
prevRight = rect.right
}
}

View file

@ -1,97 +1,22 @@
package com.anytypeio.anytype.ui.profile
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.graphics.BlurMaskFilter
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import android.widget.TextView
import com.anytypeio.anytype.core_utils.ext.arg
import com.anytypeio.anytype.core_utils.ext.toast
import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment
import com.anytypeio.anytype.core_utils.ui.ViewState
import com.anytypeio.anytype.databinding.DialogKeychainPhraseBinding
import com.anytypeio.anytype.di.common.componentManager
import com.anytypeio.anytype.presentation.keychain.KeychainPhraseViewModel
import com.anytypeio.anytype.presentation.keychain.KeychainPhraseViewModelFactory
import javax.inject.Inject
import com.anytypeio.anytype.ui.dashboard.BaseMnemonicFragment
class KeychainPhraseDialog : BaseBottomSheetFragment<DialogKeychainPhraseBinding>(), Observer<ViewState<String>> {
private val vm : KeychainPhraseViewModel by viewModels { factory }
@Inject
lateinit var factory: KeychainPhraseViewModelFactory
class KeychainPhraseDialog : BaseMnemonicFragment<DialogKeychainPhraseBinding>() {
private val screenType get() = arg<String>(ARG_SCREEN_TYPE)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
vm.sendShowEvent(screenType)
setBlur()
binding.keychain.setOnClickListener {
if (binding.keychain.layerType == View.LAYER_TYPE_SOFTWARE) {
removeBlur()
}
}
binding.btnCopy.setOnClickListener {
vm.onCopyClickedFromLogout()
copyMnemonicToClipboard()
}
binding.root.setOnClickListener {
setBlur()
}
}
private fun copyMnemonicToClipboard() {
try {
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText(MNEMONIC_LABEL, binding.keychain.text.toString())
clipboard.setPrimaryClip(clip)
toast("Recovery phrase copied to clipboard.")
} catch (e: Exception) {
toast("Could not copy your mnemonic. Please try again later, or copy it manually.")
}
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
vm.state.observe(viewLifecycleOwner, this)
}
override fun onChanged(state: ViewState<String>) {
when (state) {
is ViewState.Success -> {
binding.keychain.text = state.data
}
is ViewState.Error -> {
// TODO
}
is ViewState.Loading -> {
// TODO
}
ViewState.Init -> {
// Do nothing
}
}
}
private fun setBlur() = with(binding.keychain) {
setLayerType(View.LAYER_TYPE_SOFTWARE, null)
val radius = textSize / 3
val filter = BlurMaskFilter(radius, BlurMaskFilter.Blur.NORMAL)
paint.maskFilter = filter
}
private fun removeBlur() = with(binding.keychain) {
setLayerType(View.LAYER_TYPE_NONE, null)
paint.maskFilter = null
isFocusable = true
setTextIsSelectable(true)
}
override fun injectDependencies() {
@ -109,8 +34,9 @@ class KeychainPhraseDialog : BaseBottomSheetFragment<DialogKeychainPhraseBinding
inflater, container, false
)
override val keychain: TextView by lazy { binding.keychain }
companion object {
const val MNEMONIC_LABEL = "Your Anytype recovery phrase"
const val ARG_SCREEN_TYPE = "arg.keychain.screen.type"
}
}

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple
xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?colorControlHighlight">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/shape_transparent" />
<corners android:radius="8dp" />
</shape>
</item>
</ripple>

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="8dp" />
<solid android:color="@color/shape_transparent" />
</shape>

View file

@ -4,20 +4,31 @@
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="match_parent"
tools:context="com.anytypeio.anytype.ui.dashboard.DashboardMnemonicReminderDialog">
<ImageView
android:id="@+id/drag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="6dp"
android:src="@drawable/ic_drag"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/title"
style="@style/KeychainDialogTitleStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="23dp"
android:layout_marginStart="20dp"
android:layout_marginTop="23dp"
android:layout_marginEnd="20dp"
android:text="@string/do_not_forget_mnemonic_phrase"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toBottomOf="@+id/drag" />
<TextView
android:id="@+id/subtitle"
@ -40,8 +51,8 @@
android:layout_marginStart="20dp"
android:layout_marginTop="18dp"
android:layout_marginEnd="20dp"
android:focusable="false"
android:focusableInTouchMode="false"
android:clickable="true"
android:foreground="?attr/selectableItemBackground"
android:paddingStart="20dp"
android:paddingTop="12dp"
android:paddingEnd="20dp"
@ -50,21 +61,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/subtitle"
tools:text="@string/keychain_mock" />
<TextView
android:id="@+id/btnCopy"
style="@style/DefaultStrokeButtonStyle"
android:layout_width="0dp"
android:layout_height="@dimen/auth_default_button_height"
android:layout_marginStart="20dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="20dp"
android:layout_marginBottom="10dp"
android:text="Copy to clipboard"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/keychain" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="match_parent">
<ImageView
android:id="@+id/drag"
@ -21,10 +21,10 @@
android:id="@+id/title"
style="@style/KeychainDialogTitleStyle"
android:layout_width="match_parent"
android:textAlignment="center"
android:layout_height="wrap_content"
android:layout_marginTop="54dp"
android:text="@string/back_up_your_recovery_phrase"
android:textAlignment="center"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/drag" />
@ -49,6 +49,7 @@
android:layout_marginStart="20dp"
android:layout_marginTop="34dp"
android:layout_marginEnd="20dp"
android:clickable="true"
android:focusable="false"
android:focusableInTouchMode="false"
android:paddingStart="20dp"
@ -60,20 +61,4 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/subtitle"
tools:text="@string/keychain_mock" />
<TextView
android:id="@+id/btnCopy"
style="@style/DefaultStrokeButtonStyle"
android:layout_width="0dp"
android:layout_height="@dimen/auth_default_button_height"
android:layout_marginStart="20dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="20dp"
android:layout_marginBottom="10dp"
android:text="Copy to clipboard"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/keychain" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -31,6 +31,8 @@
<dimen name="action_toolbar_bar_margin_bottom">16dp</dimen>
<dimen name="action_toolbar_bar_margin_top">8dp</dimen>
<dimen name="shimmer_radius">5dp</dimen>
<dimen name="shimmer_line_height">8dp</dimen>
<dimen name="scroll_and_move_start_end_padding">8dp</dimen>
<dimen name="height_avatar_on_dashboard">80dp</dimen>
<dimen name="width_avatar_on_dashboard">80dp</dimen>

View file

@ -86,7 +86,7 @@ Do the computation of an expensive paragraph of text on a background thread:
<string name="your_page">Your public page</string>
<string name="updates">Updates</string>
<string name="log_out">Log out</string>
<string name="recovery_phrase_text">Your recovery phrase protects your account. You need it to sign in if you don\'t have access to your devices. Keep it in a safe place.</string>
<string name="recovery_phrase_text">Youll need it to sign in. Keep it in a safe place. If you lose it, you can no longer access your account.</string>
<string name="back_up_your_recovery_phrase">Back up your recovery \nphrase</string>
<string name="i_ve_written_it_down">i\'ve written it down</string>
<string name="keychain_mock">witch collapse practice feed shame open despair creek road again ice least lake tree young address brain envelope</string>

View file

@ -153,10 +153,10 @@
</style>
<style name="KeychainDialogKeychainStyle">
<item name="android:background">@drawable/rectangle_keychain_background</item>
<item name="android:background">@drawable/keychain_ripple</item>
<item name="android:fontFamily">monospace</item>
<item name="android:lineSpacingExtra">2sp</item>
<item name="android:textColor">#1BA0EB</item>
<item name="android:textColor">@color/keychain_text_color</item>
<item name="android:textIsSelectable">true</item>
<item name="android:textSize">15sp</item>
</style>

View file

@ -110,6 +110,7 @@
<color name="RelationPlaceholderTextColor">#929082</color>
<color name="default_status_text_color">#929082</color>
<color name="default_grey">#929082</color>
<color name="keychain_text_color">#1BA0EB</color>
<color name="orange_button_background_color">#FFB522</color>
<color name="editor_default_hint_text_color">#CBC9BD</color>

View file

@ -10,7 +10,6 @@ import com.anytypeio.anytype.analytics.base.EventsDictionary.keychainPhraseScree
import com.anytypeio.anytype.analytics.base.EventsPropertiesKey
import com.anytypeio.anytype.analytics.base.sendEvent
import com.anytypeio.anytype.analytics.props.Props
import com.anytypeio.anytype.core_utils.ui.ViewState
import com.anytypeio.anytype.core_utils.ui.ViewStateViewModel
import com.anytypeio.anytype.domain.auth.interactor.GetMnemonic
import timber.log.Timber
@ -18,10 +17,13 @@ import timber.log.Timber
class KeychainPhraseViewModel(
private val getMnemonic: GetMnemonic,
private val analytics: Analytics
) : ViewStateViewModel<ViewState<String>>() {
) : ViewStateViewModel<KeychainViewState>() {
private var mnemonic: String = ""
init {
proceedWithGettingMnemonic()
stateData.postValue(KeychainViewState.Blurred)
}
fun sendShowEvent(type: String) {
@ -34,7 +36,7 @@ class KeychainPhraseViewModel(
)
}
fun onCopyClickedFromScreenSettings() {
private fun copyClickedFromScreenSettings() {
viewModelScope.sendEvent(
analytics = analytics,
eventName = keychainCopy,
@ -44,21 +46,23 @@ class KeychainPhraseViewModel(
)
}
fun onCopyClickedFromLogout() {
viewModelScope.sendEvent(
analytics = analytics,
eventName = keychainCopy,
props = Props(
mapOf(EventsPropertiesKey.type to EventsDictionary.Type.beforeLogout)
)
)
fun onKeychainClicked() {
stateData.postValue(KeychainViewState.Displayed(mnemonic))
copyClickedFromScreenSettings()
}
fun onRootClicked() {
stateData.postValue(KeychainViewState.Blurred)
}
private fun proceedWithGettingMnemonic() {
getMnemonic.invoke(viewModelScope, Unit) { result ->
result.either(
fnL = { e -> Timber.e(e, "Error while getting mnemonic") },
fnR = { stateData.postValue(ViewState.Success(it)) }
fnR = { mnemonic ->
this.mnemonic = mnemonic
Any()
}
)
}
}

View file

@ -0,0 +1,6 @@
package com.anytypeio.anytype.presentation.keychain
sealed class KeychainViewState {
data class Displayed(val mnemonic: String) : KeychainViewState()
object Blurred : KeychainViewState()
}