|
|
|
@ -3,154 +3,170 @@ package org.thoughtcrime.securesms.onboarding
|
|
|
|
|
import android.content.Context
|
|
|
|
|
import android.content.Intent
|
|
|
|
|
import android.os.Bundle
|
|
|
|
|
import android.text.InputType
|
|
|
|
|
import android.view.LayoutInflater
|
|
|
|
|
import android.view.View
|
|
|
|
|
import android.view.ViewGroup
|
|
|
|
|
import android.view.inputmethod.EditorInfo
|
|
|
|
|
import android.view.inputmethod.InputMethodManager
|
|
|
|
|
import android.widget.Toast
|
|
|
|
|
import androidx.fragment.app.Fragment
|
|
|
|
|
import androidx.fragment.app.FragmentPagerAdapter
|
|
|
|
|
import androidx.activity.viewModels
|
|
|
|
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
|
|
|
import androidx.compose.foundation.layout.Arrangement
|
|
|
|
|
import androidx.compose.foundation.layout.Column
|
|
|
|
|
import androidx.compose.foundation.layout.Row
|
|
|
|
|
import androidx.compose.foundation.layout.Spacer
|
|
|
|
|
import androidx.compose.foundation.layout.height
|
|
|
|
|
import androidx.compose.foundation.layout.padding
|
|
|
|
|
import androidx.compose.foundation.layout.size
|
|
|
|
|
import androidx.compose.foundation.layout.width
|
|
|
|
|
import androidx.compose.foundation.pager.HorizontalPager
|
|
|
|
|
import androidx.compose.foundation.pager.rememberPagerState
|
|
|
|
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
|
|
|
import androidx.compose.foundation.text.KeyboardActions
|
|
|
|
|
import androidx.compose.material.ContentAlpha
|
|
|
|
|
import androidx.compose.material.Icon
|
|
|
|
|
import androidx.compose.material.LocalContentAlpha
|
|
|
|
|
import androidx.compose.material.LocalContentColor
|
|
|
|
|
import androidx.compose.material.MaterialTheme
|
|
|
|
|
import androidx.compose.material.OutlinedTextField
|
|
|
|
|
import androidx.compose.material.Tab
|
|
|
|
|
import androidx.compose.material.TabRow
|
|
|
|
|
import androidx.compose.material.Text
|
|
|
|
|
import androidx.compose.material.TextFieldDefaults
|
|
|
|
|
import androidx.compose.runtime.Composable
|
|
|
|
|
import androidx.compose.runtime.collectAsState
|
|
|
|
|
import androidx.compose.runtime.getValue
|
|
|
|
|
import androidx.compose.ui.Alignment
|
|
|
|
|
import androidx.compose.ui.Modifier
|
|
|
|
|
import androidx.compose.ui.graphics.Color
|
|
|
|
|
import androidx.compose.ui.platform.ComposeView
|
|
|
|
|
import androidx.compose.ui.res.painterResource
|
|
|
|
|
import androidx.compose.ui.res.stringResource
|
|
|
|
|
import androidx.compose.ui.unit.dp
|
|
|
|
|
import androidx.lifecycle.lifecycleScope
|
|
|
|
|
import dagger.hilt.android.AndroidEntryPoint
|
|
|
|
|
import kotlinx.coroutines.launch
|
|
|
|
|
import network.loki.messenger.R
|
|
|
|
|
import network.loki.messenger.databinding.ActivityLinkDeviceBinding
|
|
|
|
|
import network.loki.messenger.databinding.FragmentRecoveryPhraseBinding
|
|
|
|
|
import org.session.libsession.utilities.TextSecurePreferences
|
|
|
|
|
import org.session.libsignal.crypto.MnemonicCodec
|
|
|
|
|
import org.session.libsignal.utilities.Hex
|
|
|
|
|
import org.session.libsignal.utilities.Log
|
|
|
|
|
import org.thoughtcrime.securesms.BaseActionBarActivity
|
|
|
|
|
import org.thoughtcrime.securesms.crypto.MnemonicUtilities
|
|
|
|
|
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment
|
|
|
|
|
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate
|
|
|
|
|
import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo
|
|
|
|
|
import org.thoughtcrime.securesms.ui.AppTheme
|
|
|
|
|
import org.thoughtcrime.securesms.ui.OutlineButton
|
|
|
|
|
import org.thoughtcrime.securesms.ui.baseBold
|
|
|
|
|
import org.thoughtcrime.securesms.ui.colorDestructive
|
|
|
|
|
import javax.inject.Inject
|
|
|
|
|
|
|
|
|
|
@AndroidEntryPoint
|
|
|
|
|
class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
|
|
|
|
class LinkDeviceActivity : BaseActionBarActivity() {
|
|
|
|
|
|
|
|
|
|
private lateinit var binding: ActivityLinkDeviceBinding
|
|
|
|
|
@Inject
|
|
|
|
|
lateinit var prefs: TextSecurePreferences
|
|
|
|
|
|
|
|
|
|
private val adapter = LinkDeviceActivityAdapter(this)
|
|
|
|
|
val viewModel: LinkDeviceViewModel by viewModels()
|
|
|
|
|
|
|
|
|
|
// region Lifecycle
|
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
|
|
|
super.onCreate(savedInstanceState)
|
|
|
|
|
setUpActionBarSessionLogo()
|
|
|
|
|
TextSecurePreferences.apply {
|
|
|
|
|
setHasViewedSeed(this@LinkDeviceActivity, true)
|
|
|
|
|
setConfigurationMessageSynced(this@LinkDeviceActivity, false)
|
|
|
|
|
setRestorationTime(this@LinkDeviceActivity, System.currentTimeMillis())
|
|
|
|
|
setLastProfileUpdateTime(this@LinkDeviceActivity, 0)
|
|
|
|
|
}
|
|
|
|
|
binding = ActivityLinkDeviceBinding.inflate(layoutInflater)
|
|
|
|
|
setContentView(binding.root)
|
|
|
|
|
binding.viewPager.adapter = adapter
|
|
|
|
|
binding.tabLayout.setupWithViewPager(binding.viewPager)
|
|
|
|
|
}
|
|
|
|
|
// endregion
|
|
|
|
|
|
|
|
|
|
// region Interaction
|
|
|
|
|
override fun handleQRCodeScanned(mnemonic: String) {
|
|
|
|
|
try {
|
|
|
|
|
val seed = Hex.fromStringCondensed(mnemonic)
|
|
|
|
|
continueWithSeed(seed)
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
Log.e("Loki","Error getting seed from QR code", e)
|
|
|
|
|
Toast.makeText(this, "An error occurred.", Toast.LENGTH_LONG).show()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun continueWithMnemonic(mnemonic: String) {
|
|
|
|
|
val loadFileContents: (String) -> String = { fileName ->
|
|
|
|
|
MnemonicUtilities.loadFileContents(this, fileName)
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
val hexEncodedSeed = MnemonicCodec(loadFileContents).decode(mnemonic)
|
|
|
|
|
val seed = Hex.fromStringCondensed(hexEncodedSeed)
|
|
|
|
|
continueWithSeed(seed)
|
|
|
|
|
} catch (error: Exception) {
|
|
|
|
|
val message = if (error is MnemonicCodec.DecodingError) {
|
|
|
|
|
error.description
|
|
|
|
|
} else {
|
|
|
|
|
"An error occurred."
|
|
|
|
|
supportActionBar?.title = "Load Account"
|
|
|
|
|
prefs.setHasViewedSeed(true)
|
|
|
|
|
prefs.setConfigurationMessageSynced(false)
|
|
|
|
|
prefs.setRestorationTime(System.currentTimeMillis())
|
|
|
|
|
prefs.setLastProfileUpdateTime(0)
|
|
|
|
|
|
|
|
|
|
lifecycleScope.launch {
|
|
|
|
|
viewModel.eventFlow.collect {
|
|
|
|
|
startLoadingActivity(it.mnemonic)
|
|
|
|
|
}
|
|
|
|
|
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun continueWithSeed(seed: ByteArray) {
|
|
|
|
|
startLoadingActivity(seed)
|
|
|
|
|
}
|
|
|
|
|
// endregion
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// region Adapter
|
|
|
|
|
private class LinkDeviceActivityAdapter(private val activity: LinkDeviceActivity) : FragmentPagerAdapter(activity.supportFragmentManager) {
|
|
|
|
|
val recoveryPhraseFragment = RecoveryPhraseFragment()
|
|
|
|
|
|
|
|
|
|
override fun getCount(): Int {
|
|
|
|
|
return 2
|
|
|
|
|
ComposeView(this).apply {
|
|
|
|
|
setContent {
|
|
|
|
|
val state by viewModel.stateFlow.collectAsState()
|
|
|
|
|
AppTheme {
|
|
|
|
|
LoadAccountScreen(state, viewModel::onChange, viewModel::onRecoveryPhrase)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}.let(::setContentView)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun getItem(index: Int): Fragment {
|
|
|
|
|
return when (index) {
|
|
|
|
|
0 -> recoveryPhraseFragment
|
|
|
|
|
1 -> {
|
|
|
|
|
val result = ScanQRCodeWrapperFragment()
|
|
|
|
|
result.delegate = activity
|
|
|
|
|
result.message = activity.getString(R.string.activity_link_device_qr_message)
|
|
|
|
|
result
|
|
|
|
|
@OptIn(ExperimentalFoundationApi::class)
|
|
|
|
|
@Composable
|
|
|
|
|
fun LoadAccountScreen(state: LinkDeviceState, onChange: (String) -> Unit = {}, onContinue: () -> Unit = {}) {
|
|
|
|
|
val tabs = listOf(R.string.activity_recovery_password, R.string.activity_link_device_scan_qr_code)
|
|
|
|
|
val pagerState = rememberPagerState { tabs.size }
|
|
|
|
|
|
|
|
|
|
Column {
|
|
|
|
|
TabRow(
|
|
|
|
|
selectedTabIndex = pagerState.currentPage,
|
|
|
|
|
modifier = Modifier.height(48.dp)
|
|
|
|
|
) {
|
|
|
|
|
tabs.forEachIndexed { i, it ->
|
|
|
|
|
Tab(i == pagerState.currentPage, onClick = { pagerState.targetPage }) {
|
|
|
|
|
Text(stringResource(id = it))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
HorizontalPager(
|
|
|
|
|
state = pagerState,
|
|
|
|
|
modifier = Modifier.weight(1f)
|
|
|
|
|
) { i ->
|
|
|
|
|
when(tabs[i]) {
|
|
|
|
|
R.string.activity_recovery_password -> RecoveryPassword(state, onChange, onContinue)
|
|
|
|
|
R.string.activity_link_device_scan_qr_code -> ScanQrCode()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else -> throw IllegalStateException()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun getPageTitle(index: Int): CharSequence {
|
|
|
|
|
return when (index) {
|
|
|
|
|
0 -> activity.getString(R.string.activity_link_device_recovery_phrase)
|
|
|
|
|
1 -> activity.getString(R.string.activity_link_device_scan_qr_code)
|
|
|
|
|
else -> throw IllegalStateException()
|
|
|
|
|
@Composable
|
|
|
|
|
fun RecoveryPassword(state: LinkDeviceState, onChange: (String) -> Unit = {}, onContinue: () -> Unit = {}) {
|
|
|
|
|
Column(
|
|
|
|
|
modifier = Modifier.padding(horizontal = 60.dp)
|
|
|
|
|
) {
|
|
|
|
|
Spacer(Modifier.weight(1f))
|
|
|
|
|
Row {
|
|
|
|
|
Text("Recovery Password", style = MaterialTheme.typography.h4)
|
|
|
|
|
Spacer(Modifier.width(6.dp))
|
|
|
|
|
Icon(
|
|
|
|
|
painter = painterResource(id = R.drawable.ic_recovery_phrase),
|
|
|
|
|
contentDescription = "",
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
Spacer(Modifier.size(28.dp))
|
|
|
|
|
Text("Enter your recovery password to load your account. If you haven't saved it, you can find it in your app settings.")
|
|
|
|
|
Spacer(Modifier.size(24.dp))
|
|
|
|
|
OutlinedTextField(
|
|
|
|
|
value = state.recoveryPhrase,
|
|
|
|
|
onValueChange = { onChange(it) },
|
|
|
|
|
placeholder = { Text("Enter your recovery password") },
|
|
|
|
|
colors = TextFieldDefaults.outlinedTextFieldColors(
|
|
|
|
|
textColor = state.error?.let { colorDestructive } ?: LocalContentColor.current.copy(LocalContentAlpha.current),
|
|
|
|
|
focusedBorderColor = Color(0xff414141),
|
|
|
|
|
unfocusedBorderColor = Color(0xff414141),
|
|
|
|
|
cursorColor = LocalContentColor.current,
|
|
|
|
|
placeholderColor = state.error?.let { colorDestructive } ?: MaterialTheme.colors.onSurface.copy(ContentAlpha.medium)
|
|
|
|
|
),
|
|
|
|
|
singleLine = true,
|
|
|
|
|
keyboardActions = KeyboardActions(
|
|
|
|
|
onDone = { onContinue() },
|
|
|
|
|
onGo = { onContinue() },
|
|
|
|
|
onSearch = { onContinue() },
|
|
|
|
|
onSend = { onContinue() },
|
|
|
|
|
),
|
|
|
|
|
isError = state.error != null,
|
|
|
|
|
shape = RoundedCornerShape(12.dp)
|
|
|
|
|
)
|
|
|
|
|
Spacer(Modifier.size(12.dp))
|
|
|
|
|
state.error?.let {
|
|
|
|
|
Text(it, style = MaterialTheme.typography.baseBold, color = MaterialTheme.colors.error)
|
|
|
|
|
}
|
|
|
|
|
Spacer(Modifier.weight(2f))
|
|
|
|
|
OutlineButton(
|
|
|
|
|
text = stringResource(id = R.string.continue_2),
|
|
|
|
|
modifier = Modifier.align(Alignment.CenterHorizontally).padding(horizontal = 64.dp, vertical = 20.dp).width(200.dp)
|
|
|
|
|
) { onContinue() }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// endregion
|
|
|
|
|
|
|
|
|
|
// region Recovery Phrase Fragment
|
|
|
|
|
class RecoveryPhraseFragment : Fragment() {
|
|
|
|
|
private lateinit var binding: FragmentRecoveryPhraseBinding
|
|
|
|
|
@Composable
|
|
|
|
|
fun ScanQrCode() {
|
|
|
|
|
|
|
|
|
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
|
|
|
|
binding = FragmentRecoveryPhraseBinding.inflate(inflater, container, false)
|
|
|
|
|
return binding.root
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
|
|
|
super.onViewCreated(view, savedInstanceState)
|
|
|
|
|
with(binding) {
|
|
|
|
|
mnemonicEditText.imeOptions = EditorInfo.IME_ACTION_DONE or 16777216 // Always use incognito keyboard
|
|
|
|
|
mnemonicEditText.setRawInputType(InputType.TYPE_CLASS_TEXT)
|
|
|
|
|
mnemonicEditText.setOnEditorActionListener { v, actionID, _ ->
|
|
|
|
|
if (actionID == EditorInfo.IME_ACTION_DONE) {
|
|
|
|
|
val imm = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
|
|
|
|
imm.hideSoftInputFromWindow(v.windowToken, 0)
|
|
|
|
|
handleContinueButtonTapped()
|
|
|
|
|
true
|
|
|
|
|
} else {
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
continueButton.setOnClickListener { handleContinueButtonTapped() }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun handleContinueButtonTapped() {
|
|
|
|
|
val mnemonic = binding.mnemonicEditText.text?.trim().toString()
|
|
|
|
|
(requireActivity() as LinkDeviceActivity).continueWithMnemonic(mnemonic)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// endregion
|
|
|
|
|
|
|
|
|
|
fun Context.startLinkDeviceActivity() {
|
|
|
|
|
Intent(this, LinkDeviceActivity::class.java).let(::startActivity)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|