Implement device linking dialog

This commit is contained in:
Niels Andriesse 2019-10-01 12:21:38 +10:00
parent d8e86a5e42
commit 9f7437aa9e
4 changed files with 163 additions and 29 deletions

View File

@ -24,7 +24,7 @@
android:textSize="20sp" android:textSize="20sp"
android:fontFamily="sans-serif-medium" android:fontFamily="sans-serif-medium"
android:textAlignment="center" android:textAlignment="center"
android:text="@string/view_device_linking_title" /> android:text="@string/view_device_linking_title_1" />
<TextView <TextView
android:id="@+id/explanationTextView" android:id="@+id/explanationTextView"
@ -33,12 +33,40 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:textAlignment="center" android:textAlignment="center"
android:text="@string/view_device_linking_explanation" /> android:text="@string/view_device_linking_explanation_1" />
<TextView
android:id="@+id/mnemonicTextView"
style="@style/Signal.Text.Body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textAlignment="center"
android:text="word word word" />
<LinearLayout
android:id="@+id/buttonContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/authorizeButton"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_weight="1"
android:layout_marginTop="16dp"
android:background="@color/transparent"
android:textColor="@color/signal_primary"
android:text="@string/view_device_linking_authorize_button_title"
android:elevation="0dp"
android:stateListAnimator="@null" />
<Button <Button
android:id="@+id/cancelButton" android:id="@+id/cancelButton"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="50dp" android:layout_height="50dp"
android:layout_weight="1"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:background="@color/transparent" android:background="@color/transparent"
android:textColor="@color/signal_primary" android:textColor="@color/signal_primary"
@ -46,4 +74,6 @@
android:elevation="0dp" android:elevation="0dp"
android:stateListAnimator="@null" /> android:stateListAnimator="@null" />
</LinearLayout>
</LinearLayout> </LinearLayout>

View File

@ -1612,8 +1612,14 @@
<string name="view_qr_code_explanation">This is your personal QR code. Other people can scan it to start a secure conversation with you.</string> <string name="view_qr_code_explanation">This is your personal QR code. Other people can scan it to start a secure conversation with you.</string>
<string name="view_qr_code_cancel_button_title">Cancel</string> <string name="view_qr_code_cancel_button_title">Cancel</string>
<!-- Device linking view --> <!-- Device linking view -->
<string name="view_device_linking_title">Waiting for Device</string> <string name="view_device_linking_title_1">Waiting for Device</string>
<string name="view_device_linking_explanation">Create a new account on your other device and click \"Link Device\" when you\'re at the \"Create Your Loki Messenger Account\" step to start the linking process</string> <string name="view_device_linking_title_2">Waiting for Authorization</string>
<string name="view_device_linking_title_3">Linking Request Received</string>
<string name="view_device_linking_title_4">Device Link Authorized</string>
<string name="view_device_linking_explanation_1">Create a new account on your other device and click \"Link Device\" when you\'re at the \"Create Your Loki Messenger Account\" step to start the linking process</string>
<string name="view_device_linking_explanation_2">Please check that the words below match the ones shown on your other device</string>
<string name="view_device_linking_explanation_3">Your device has been linked successfully</string>
<string name="view_device_linking_authorize_button_title">Authorize</string>
<string name="view_device_linking_cancel_button_title">Cancel</string> <string name="view_device_linking_cancel_button_title">Cancel</string>
<!-- Scan QR code fragment --> <!-- Scan QR code fragment -->
<string name="fragment_scan_qr_code_title">Scan QR Code</string> <string name="fragment_scan_qr_code_title">Scan QR Code</string>

View File

@ -41,6 +41,7 @@ import android.widget.Toast;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.loki.DeviceLinkingDialog; import org.thoughtcrime.securesms.loki.DeviceLinkingDialog;
import org.thoughtcrime.securesms.loki.DeviceLinkingView;
import org.thoughtcrime.securesms.loki.QRCodeDialog; import org.thoughtcrime.securesms.loki.QRCodeDialog;
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment; import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
import org.thoughtcrime.securesms.preferences.ChatsPreferenceFragment; import org.thoughtcrime.securesms.preferences.ChatsPreferenceFragment;
@ -327,7 +328,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
QRCodeDialog.INSTANCE.show(getContext()); QRCodeDialog.INSTANCE.show(getContext());
break; break;
case PREFERENCE_CATEGORY_LINK_DEVICE: case PREFERENCE_CATEGORY_LINK_DEVICE:
DeviceLinkingDialog.INSTANCE.show(getContext()); DeviceLinkingDialog.INSTANCE.show(getContext(), DeviceLinkingView.Mode.Master);
break; break;
case PREFERENCE_CATEGORY_SEED: case PREFERENCE_CATEGORY_SEED:
File languageFileDirectory = new File(getContext().getApplicationInfo().dataDir); File languageFileDirectory = new File(getContext().getApplicationInfo().dataDir);

View File

@ -3,66 +3,163 @@ package org.thoughtcrime.securesms.loki
import android.content.Context import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.graphics.PorterDuff import android.graphics.PorterDuff
import android.os.Handler
import android.support.v7.app.AlertDialog import android.support.v7.app.AlertDialog
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Log
import android.view.View
import android.widget.LinearLayout import android.widget.LinearLayout
import kotlinx.android.synthetic.main.view_device_linking.view.* import kotlinx.android.synthetic.main.view_device_linking.view.*
import network.loki.messenger.R import network.loki.messenger.R
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec
import org.whispersystems.signalservice.loki.utilities.removing05PrefixIfNeeded
import java.io.File
import java.io.FileOutputStream
object DeviceLinkingDialog { object DeviceLinkingDialog {
fun show(context: Context, mode: DeviceLinkingView.Mode) { fun show(context: Context, mode: DeviceLinkingView.Mode) {
val view = DeviceLinkingView(context, mode) val view = DeviceLinkingView(context, mode)
val dialog = AlertDialog.Builder(context).setView(view).show() val dialog = AlertDialog.Builder(context).setView(view).show()
view.onCancel = { dialog.dismiss() } view.dismiss = { dialog.dismiss() }
} }
} }
class DeviceLinkingView private constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : LinearLayout(context, attrs, defStyleAttr) { class DeviceLinkingView private constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, private val mode: Mode) : LinearLayout(context, attrs, defStyleAttr) {
private lateinit var mode: Mode
private var delegate: DeviceLinkingDialogDelegate? = null private var delegate: DeviceLinkingDialogDelegate? = null
var onCancel: (() -> Unit)? = null private lateinit var languageFileDirectory: File
var dismiss: (() -> Unit)? = null
// region Types // region Types
enum class Mode { Master, Slave } enum class Mode { Master, Slave }
// endregion // endregion
// region Lifecycle // region Lifecycle
constructor(context: Context, mode: Mode) : this(context, null, 0) { constructor(context: Context, mode: Mode) : this(context, null, 0, mode)
this.mode = mode private constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0, Mode.Master) // Just pass in a dummy mode
}
private constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
private constructor(context: Context) : this(context, null) private constructor(context: Context) : this(context, null)
init { init {
if (mode == Mode.Slave) { if (mode == Mode.Slave) {
if (delegate == null) { throw IllegalStateException("Missing delegate for device linking dialog in slave mode.") } if (delegate == null) { throw IllegalStateException("Missing delegate for device linking dialog in slave mode.") }
} }
setUpLanguageFileDirectory()
setUpViewHierarchy() setUpViewHierarchy()
when (mode) { when (mode) {
Mode.Master -> throw IllegalStateException() // DeviceLinkingSession.startListeningForLinkingRequests(this) Mode.Master -> Log.d("Loki", "TODO: DeviceLinkingSession.startListeningForLinkingRequests(this)")
Mode.Slave -> throw IllegalStateException() // DeviceLinkingSession.startListeningForAuthorization(this) Mode.Slave -> Log.d("Loki", "TODO: DeviceLinkingSession.startListeningForAuthorization(this)")
} }
} }
private fun setUpLanguageFileDirectory() {
val languages = listOf( "english", "japanese", "portuguese", "spanish" )
val directory = File(context.applicationInfo.dataDir)
for (language in languages) {
val fileName = "$language.txt"
if (directory.list().contains(fileName)) { continue }
val inputStream = context.assets.open("mnemonic/$fileName")
val file = File(directory, fileName)
val outputStream = FileOutputStream(file)
val buffer = ByteArray(1024)
while (true) {
val count = inputStream.read(buffer)
if (count < 0) { break }
outputStream.write(buffer, 0, count)
}
inputStream.close()
outputStream.close()
}
languageFileDirectory = directory
}
private fun setUpViewHierarchy() { private fun setUpViewHierarchy() {
inflate(context, R.layout.view_device_linking, this) inflate(context, R.layout.view_device_linking, this)
spinner.indeterminateDrawable.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN) spinner.indeterminateDrawable.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN)
cancelButton.setOnClickListener { onCancel?.invoke() } val titleID = when (mode) {
Mode.Master -> R.string.view_device_linking_title_1
Mode.Slave -> R.string.view_device_linking_title_2
}
titleTextView.text = resources.getString(titleID)
val explanationID = when (mode) {
Mode.Master -> R.string.view_device_linking_explanation_1
Mode.Slave -> R.string.view_device_linking_explanation_2
}
explanationTextView.text = resources.getString(explanationID)
mnemonicTextView.visibility = if (mode == Mode.Master) View.GONE else View.VISIBLE
if (mode == Mode.Slave) {
val hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context).removing05PrefixIfNeeded()
mnemonicTextView.text = MnemonicCodec(languageFileDirectory).encode(hexEncodedPublicKey).split(" ").slice(0 until 3).joinToString(" ")
}
authorizeButton.visibility = View.GONE
cancelButton.setOnClickListener { cancel() }
} }
// endregion // endregion
// region Device Linking // region Device Linking
private fun requestUserAuthorization() { // TODO: Device link private fun requestUserAuthorization() { // TODO: deviceLink parameter
// Called by DeviceLinkingSession when a linking request has been received // To be called by DeviceLinkingSession when a linking request has been received
// TODO: this.deviceLink = deviceLink
spinner.visibility = View.GONE
val titleTextViewLayoutParams = titleTextView.layoutParams as LayoutParams
titleTextViewLayoutParams.topMargin = toPx(16, resources)
titleTextView.layoutParams = titleTextViewLayoutParams
titleTextView.text = resources.getString(R.string.view_device_linking_title_3)
explanationTextView.text = resources.getString(R.string.view_device_linking_explanation_2)
mnemonicTextView.visibility = View.VISIBLE
val hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context).removing05PrefixIfNeeded() // TODO: deviceLink.slave.hexEncodedPublicKey.removing05PrefixIfNeeded()
mnemonicTextView.text = MnemonicCodec(languageFileDirectory).encode(hexEncodedPublicKey).split(" ").slice(0 until 3).joinToString(" ")
authorizeButton.visibility = View.VISIBLE
} }
private fun authorizeDeviceLink() { private fun authorizeDeviceLink() {
// TODO: val deviceLink = this.deviceLink!!
// TODO: val linkingAuthorizationMessage = DeviceLinkingUtilities.getLinkingAuthorizationMessage(deviceLink)
// TODO: Send the linking authorization message
// TODO: val session = DeviceLinkingSession.current!!
// TODO: session.stopListeningForLinkingRequests()
// TODO: session.markLinkingRequestAsProcessed()
dismiss?.invoke()
// TODO: val master = DeviceLink.Device(deviceLink.master.hexEncodedPublicKey, linkingAuthorizationMessage.masterSignature)
// TODO: val signedDeviceLink = DeviceLink(master, deviceLink.slave)
// TODO: LokiStorageAPI.addDeviceLink(signedDeviceLink).fail { error ->
// TODO: Log.d("Loki", "Failed to add device link due to error: $error.")
// TODO: }
} }
private fun handleDeviceLinkAuthorized() { // TODO: Device link private fun handleDeviceLinkAuthorized() { // TODO: deviceLink parameter
// Called by DeviceLinkingSession when a device link has been authorized // To be called by DeviceLinkingSession when a device link has been authorized
// TODO: val session = DeviceLinkingSession.current!!
// TODO: session.stopListeningForLinkingAuthorization()
spinner.visibility = View.GONE
val titleTextViewLayoutParams = titleTextView.layoutParams as LayoutParams
titleTextViewLayoutParams.topMargin = toPx(8, resources)
titleTextView.layoutParams = titleTextViewLayoutParams
titleTextView.text = resources.getString(R.string.view_device_linking_title_4)
val explanationTextViewLayoutParams = explanationTextView.layoutParams as LayoutParams
explanationTextViewLayoutParams.bottomMargin = toPx(12, resources)
explanationTextView.layoutParams = explanationTextViewLayoutParams
explanationTextView.text = resources.getString(R.string.view_device_linking_explanation_3)
titleTextView.text = resources.getString(R.string.view_device_linking_title_4)
mnemonicTextView.visibility = View.GONE
buttonContainer.visibility = View.GONE
// TODO: LokiStorageAPI.addDeviceLink(signedDeviceLink).fail { error ->
// TODO: Log.d("Loki", "Failed to add device link due to error: $error.")
// TODO: }
Handler().postDelayed({
delegate?.handleDeviceLinkAuthorized()
dismiss?.invoke()
}, 4000)
}
// endregion
// region Interaction
private fun cancel() {
// TODO: val session = DeviceLinkingSession.current!!
// TODO: session.stopListeningForLinkingRequests()
// TODO: session.markLinkingRequestAsProcessed() // Only relevant in master mode
delegate?.handleDeviceLinkingDialogDismissed() // Only relevant in slave mode
dismiss?.invoke()
} }
// endregion // endregion
} }