mirror of
https://github.com/oxen-io/session-android.git
synced 2025-12-03 12:32:17 +00:00
Merge branch 'dev' into private-group-chat
This commit is contained in:
@@ -1,78 +0,0 @@
|
||||
package org.thoughtcrime.securesms.loki
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Patterns
|
||||
import android.view.MenuItem
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import kotlinx.android.synthetic.main.activity_add_public_chat.*
|
||||
import network.loki.messenger.R
|
||||
import nl.komponents.kovenant.ui.failUi
|
||||
import nl.komponents.kovenant.ui.successUi
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
|
||||
class AddPublicChatActivity : PassphraseRequiredActionBarActivity() {
|
||||
private val dynamicTheme = DynamicTheme()
|
||||
|
||||
override fun onPreCreate() {
|
||||
dynamicTheme.onCreate(this)
|
||||
}
|
||||
|
||||
override fun onCreate(bundle: Bundle?, isReady: Boolean) {
|
||||
supportActionBar!!.setTitle(R.string.fragment_add_public_chat_title)
|
||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||
setContentView(R.layout.activity_add_public_chat)
|
||||
updateUI(false)
|
||||
addButton.setOnClickListener { addPublicChatIfPossible() }
|
||||
}
|
||||
|
||||
public override fun onResume() {
|
||||
super.onResume()
|
||||
dynamicTheme.onResume(this)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == android.R.id.home) {
|
||||
onBackPressed()
|
||||
return true
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun addPublicChatIfPossible() {
|
||||
val inputMethodManager = getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputMethodManager.hideSoftInputFromWindow(urlEditText.windowToken, 0)
|
||||
val url = urlEditText.text.toString().toLowerCase().replace("http://", "https://")
|
||||
if (!Patterns.WEB_URL.matcher(url).matches() || !url.startsWith("https://")) {
|
||||
return Toast.makeText(this, R.string.fragment_add_public_chat_invalid_url_message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
updateUI(true)
|
||||
val application = ApplicationContext.getInstance(this)
|
||||
val channel: Long = 1
|
||||
val displayName = TextSecurePreferences.getProfileName(this)
|
||||
val lokiPublicChatAPI = application.lokiPublicChatAPI!!
|
||||
application.lokiPublicChatManager.addChat(url, channel).successUi {
|
||||
lokiPublicChatAPI.getMessages(channel, url)
|
||||
lokiPublicChatAPI.setDisplayName(displayName, url)
|
||||
val profileKey: ByteArray = ProfileKeyUtil.getProfileKey(this)
|
||||
val profileUrl: String? = TextSecurePreferences.getProfileAvatarUrl(this)
|
||||
lokiPublicChatAPI.setProfilePicture(url, profileKey, profileUrl)
|
||||
finish()
|
||||
}.failUi {
|
||||
updateUI(false)
|
||||
Toast.makeText(this, R.string.fragment_add_public_chat_connection_failed_message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateUI(isConnecting: Boolean) {
|
||||
addButton.isEnabled = !isConnecting
|
||||
val text = if (isConnecting) R.string.fragment_add_public_chat_add_button_title_2 else R.string.fragment_add_public_chat_add_button_title_1
|
||||
addButton.setText(text)
|
||||
urlEditText.isEnabled = !isConnecting
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package org.thoughtcrime.securesms.loki
|
||||
|
||||
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
|
||||
|
||||
// Loki - TODO: Remove this yucky delegate pattern for device linking dialog once we have the redesign
|
||||
interface DeviceLinkingDelegate {
|
||||
companion object {
|
||||
fun combine(vararg delegates: DeviceLinkingDelegate?): DeviceLinkingDelegate {
|
||||
val validDelegates = delegates.filterNotNull()
|
||||
return object : DeviceLinkingDelegate {
|
||||
override fun handleDeviceLinkAuthorized(pairingAuthorisation: PairingAuthorisation) {
|
||||
for (delegate in validDelegates) { delegate.handleDeviceLinkAuthorized(pairingAuthorisation) }
|
||||
}
|
||||
|
||||
override fun handleDeviceLinkingDialogDismissed() {
|
||||
for (delegate in validDelegates) { delegate.handleDeviceLinkingDialogDismissed() }
|
||||
}
|
||||
|
||||
override fun sendPairingAuthorizedMessage(pairingAuthorisation: PairingAuthorisation) {
|
||||
for (delegate in validDelegates) { delegate.sendPairingAuthorizedMessage(pairingAuthorisation) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleDeviceLinkAuthorized(pairingAuthorisation: PairingAuthorisation) {}
|
||||
fun handleDeviceLinkingDialogDismissed() {}
|
||||
fun sendPairingAuthorizedMessage(pairingAuthorisation: PairingAuthorisation) {}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package org.thoughtcrime.securesms.loki
|
||||
|
||||
import android.content.Context
|
||||
import android.support.v7.app.AlertDialog
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.loki.api.DeviceLinkingSession
|
||||
import org.whispersystems.signalservice.loki.api.DeviceLinkingSessionListener
|
||||
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
|
||||
|
||||
class DeviceLinkingDialog private constructor(private val context: Context, private val mode: DeviceLinkingView.Mode, private val delegate: DeviceLinkingDelegate?) : DeviceLinkingDelegate, DeviceLinkingSessionListener {
|
||||
private lateinit var view: DeviceLinkingView
|
||||
private lateinit var dialog: AlertDialog
|
||||
|
||||
companion object {
|
||||
fun show(context: Context, mode: DeviceLinkingView.Mode, delegate: DeviceLinkingDelegate?): DeviceLinkingDialog {
|
||||
val dialog = DeviceLinkingDialog(context, mode, delegate)
|
||||
dialog.show()
|
||||
return dialog
|
||||
}
|
||||
}
|
||||
|
||||
private fun show() {
|
||||
val delegate = DeviceLinkingDelegate.combine(this, this.delegate)
|
||||
view = DeviceLinkingView(context, mode, delegate)
|
||||
dialog = AlertDialog.Builder(context).setView(view).show()
|
||||
dialog.setCanceledOnTouchOutside(false)
|
||||
view.dismiss = { dismiss() }
|
||||
DeviceLinkingSession.shared.startListeningForLinkingRequests()
|
||||
DeviceLinkingSession.shared.addListener(this)
|
||||
}
|
||||
|
||||
private fun dismiss() {
|
||||
DeviceLinkingSession.shared.stopListeningForLinkingRequests()
|
||||
DeviceLinkingSession.shared.removeListener(this)
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
override fun handleDeviceLinkingDialogDismissed() {
|
||||
if (mode == DeviceLinkingView.Mode.Master && view.pairingAuthorisation != null) {
|
||||
val authorisation = view.pairingAuthorisation!!
|
||||
DatabaseFactory.getLokiPreKeyBundleDatabase(context).removePreKeyBundle(authorisation.secondaryDevicePublicKey)
|
||||
}
|
||||
}
|
||||
|
||||
override fun requestUserAuthorization(authorisation: PairingAuthorisation) {
|
||||
Util.runOnMain {
|
||||
view.requestUserAuthorization(authorisation)
|
||||
}
|
||||
DeviceLinkingSession.shared.stopListeningForLinkingRequests()
|
||||
}
|
||||
|
||||
override fun onDeviceLinkRequestAuthorized(authorisation: PairingAuthorisation) {
|
||||
Util.runOnMain {
|
||||
view.onDeviceLinkAuthorized(authorisation)
|
||||
}
|
||||
DeviceLinkingSession.shared.stopListeningForLinkingRequests()
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
package org.thoughtcrime.securesms.loki
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.PorterDuff
|
||||
import android.os.Handler
|
||||
import android.util.AttributeSet
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import kotlinx.android.synthetic.main.view_device_linking.view.*
|
||||
import kotlinx.android.synthetic.main.view_device_linking.view.cancelButton
|
||||
import kotlinx.android.synthetic.main.view_device_linking.view.explanationTextView
|
||||
import kotlinx.android.synthetic.main.view_device_linking.view.titleTextView
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.qr.QrCode
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
|
||||
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec
|
||||
import java.io.File
|
||||
|
||||
class DeviceLinkingView private constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, private val mode: Mode, private var delegate: DeviceLinkingDelegate) : LinearLayout(context, attrs, defStyleAttr) {
|
||||
private val languageFileDirectory: File = MnemonicUtilities.getLanguageFileDirectory(context)
|
||||
var dismiss: (() -> Unit)? = null
|
||||
var pairingAuthorisation: PairingAuthorisation? = null
|
||||
private set
|
||||
|
||||
// region Types
|
||||
enum class Mode { Master, Slave }
|
||||
// endregion
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context, mode: Mode, delegate: DeviceLinkingDelegate) : this(context, null, 0, mode, delegate)
|
||||
private constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0, Mode.Master, object : DeviceLinkingDelegate { }) // Just pass in a dummy mode
|
||||
private constructor(context: Context) : this(context, null)
|
||||
|
||||
init {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
private fun setUpViewHierarchy() {
|
||||
inflate(context, R.layout.view_device_linking, this)
|
||||
spinner.indeterminateDrawable.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN)
|
||||
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)
|
||||
mnemonicTextView.text = MnemonicUtilities.getFirst3Words(MnemonicCodec(languageFileDirectory), hexEncodedPublicKey)
|
||||
}
|
||||
authorizeButton.visibility = View.GONE
|
||||
authorizeButton.setOnClickListener { authorizePairing() }
|
||||
|
||||
// QR Code
|
||||
spinner.visibility = if (mode == Mode.Master) View.GONE else View.VISIBLE
|
||||
qrCodeImageView.visibility = if (mode == Mode.Master) View.VISIBLE else View.GONE
|
||||
if (mode == Mode.Master) {
|
||||
val hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val displayMetrics = DisplayMetrics()
|
||||
ServiceUtil.getWindowManager(context).defaultDisplay.getMetrics(displayMetrics)
|
||||
val size = displayMetrics.widthPixels - 2 * toPx(96, resources)
|
||||
val qrCode = QrCode.create(hexEncodedPublicKey, size)
|
||||
qrCodeImageView.setImageBitmap(qrCode)
|
||||
}
|
||||
|
||||
cancelButton.setOnClickListener { cancel() }
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Device Linking
|
||||
fun requestUserAuthorization(pairingAuthorisation: PairingAuthorisation) {
|
||||
if (mode != Mode.Master || pairingAuthorisation.type != PairingAuthorisation.Type.REQUEST || this.pairingAuthorisation != null) { return }
|
||||
this.pairingAuthorisation = pairingAuthorisation
|
||||
spinner.visibility = View.GONE
|
||||
qrCodeImageView.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
|
||||
mnemonicTextView.text = MnemonicUtilities.getFirst3Words(MnemonicCodec(languageFileDirectory), pairingAuthorisation.secondaryDevicePublicKey)
|
||||
authorizeButton.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
fun onDeviceLinkAuthorized(pairingAuthorisation: PairingAuthorisation) {
|
||||
if (mode != Mode.Slave || pairingAuthorisation.type != PairingAuthorisation.Type.GRANT || this.pairingAuthorisation != null) { return }
|
||||
this.pairingAuthorisation = pairingAuthorisation
|
||||
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
|
||||
cancelButton.visibility = View.GONE
|
||||
Handler().postDelayed({
|
||||
delegate.handleDeviceLinkAuthorized(pairingAuthorisation)
|
||||
dismiss?.invoke()
|
||||
}, 4000)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Interaction
|
||||
private fun authorizePairing() {
|
||||
val pairingAuthorisation = this.pairingAuthorisation
|
||||
if (mode != Mode.Master || pairingAuthorisation == null) { return; }
|
||||
delegate.sendPairingAuthorizedMessage(pairingAuthorisation)
|
||||
delegate.handleDeviceLinkAuthorized(pairingAuthorisation)
|
||||
dismiss?.invoke()
|
||||
}
|
||||
|
||||
private fun cancel() {
|
||||
delegate.handleDeviceLinkingDialogDismissed()
|
||||
dismiss?.invoke()
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
@@ -9,16 +9,16 @@ import kotlinx.android.synthetic.main.fragment_device_list_bottom_sheet.*
|
||||
import network.loki.messenger.R
|
||||
|
||||
public class DeviceListBottomSheetFragment : BottomSheetDialogFragment() {
|
||||
var onEditTapped: (() -> Unit)? = null
|
||||
var onUnlinkTapped: (() -> Unit)? = null
|
||||
var onEditTapped: (() -> Unit)? = null
|
||||
var onUnlinkTapped: (() -> Unit)? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_device_list_bottom_sheet, container, false)
|
||||
}
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_device_list_bottom_sheet, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
editDisplayNameText.setOnClickListener { onEditTapped?.invoke() }
|
||||
unlinkDeviceText.setOnClickListener { onUnlinkTapped?.invoke() }
|
||||
}
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
editDisplayNameText.setOnClickListener { onEditTapped?.invoke() }
|
||||
unlinkDeviceText.setOnClickListener { onUnlinkTapped?.invoke() }
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package org.thoughtcrime.securesms.loki
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import kotlinx.android.synthetic.main.activity_display_name.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.ConversationListActivity
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.api.crypto.ProfileCipher
|
||||
import org.whispersystems.signalservice.loki.utilities.Analytics
|
||||
|
||||
class DisplayNameActivity : BaseActionBarActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_display_name)
|
||||
nextButton.setOnClickListener { continueIfPossible() }
|
||||
Analytics.shared.track("Display Name Screen Viewed")
|
||||
}
|
||||
|
||||
private fun continueIfPossible() {
|
||||
val name = nameEditText.text.toString()
|
||||
if (name.isEmpty()) {
|
||||
return nameEditText.input.setError("Invalid")
|
||||
}
|
||||
if (!name.matches(Regex("[a-zA-Z0-9_]+"))) {
|
||||
return nameEditText.input.setError("Invalid (a-z, A-Z, 0-9 and _ only)")
|
||||
}
|
||||
if (name.toByteArray().size > ProfileCipher.NAME_PADDED_LENGTH) {
|
||||
return nameEditText.input.setError("Too Long")
|
||||
} else {
|
||||
Analytics.shared.track("Display Name Updated")
|
||||
TextSecurePreferences.setProfileName(this, name)
|
||||
}
|
||||
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputMethodManager.hideSoftInputFromWindow(nameEditText.windowToken, 0)
|
||||
TextSecurePreferences.setHasSeenWelcomeScreen(this, true)
|
||||
TextSecurePreferences.setPromptedPushRegistration(this, true)
|
||||
val application = ApplicationContext.getInstance(this)
|
||||
application.setUpP2PAPI()
|
||||
application.startLongPollingIfNeeded()
|
||||
application.setUpStorageAPIIfNeeded()
|
||||
startActivity(Intent(this, ConversationListActivity::class.java))
|
||||
finish()
|
||||
val publicChatAPI = ApplicationContext.getInstance(this).lokiPublicChatAPI
|
||||
if (publicChatAPI != null) {
|
||||
application.createDefaultPublicChatsIfNeeded()
|
||||
application.createRSSFeedsIfNeeded()
|
||||
application.lokiPublicChatManager.startPollersIfNeeded()
|
||||
application.startRSSFeedPollersIfNeeded()
|
||||
val servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers()
|
||||
servers.forEach { publicChatAPI.setDisplayName(name, it) }
|
||||
application.updatePublicChatProfileAvatarIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package org.thoughtcrime.securesms.loki
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
|
||||
interface FriendRequestViewDelegate {
|
||||
/**
|
||||
* Implementations of this method should update the thread's friend request status
|
||||
* and send a friend request accepted message.
|
||||
*/
|
||||
fun acceptFriendRequest(friendRequest: MessageRecord)
|
||||
/**
|
||||
* Implementations of this method should update the thread's friend request status
|
||||
* and remove the pre keys associated with the contact.
|
||||
*/
|
||||
fun rejectFriendRequest(friendRequest: MessageRecord)
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package org.thoughtcrime.securesms.loki
|
||||
|
||||
import android.os.AsyncTask
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import org.thoughtcrime.securesms.*
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage
|
||||
import network.loki.messenger.R
|
||||
import nl.komponents.kovenant.then
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.sms.MessageSender
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.loki.api.LokiStorageAPI
|
||||
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
|
||||
|
||||
class LinkedDevicesActivity : PassphraseRequiredActionBarActivity(), DeviceLinkingDelegate {
|
||||
|
||||
companion object {
|
||||
private val TAG = DeviceActivity::class.java.simpleName
|
||||
}
|
||||
|
||||
private val dynamicTheme = DynamicTheme()
|
||||
private val dynamicLanguage = DynamicLanguage()
|
||||
private lateinit var deviceListFragment: DeviceListFragment
|
||||
|
||||
public override fun onPreCreate() {
|
||||
dynamicTheme.onCreate(this)
|
||||
dynamicLanguage.onCreate(this)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
super.onCreate(savedInstanceState, ready)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setTitle(R.string.AndroidManifest__linked_devices)
|
||||
this.deviceListFragment = DeviceListFragment()
|
||||
this.deviceListFragment.setAddDeviceButtonListener {
|
||||
DeviceLinkingDialog.show(this, DeviceLinkingView.Mode.Master, this)
|
||||
}
|
||||
this.deviceListFragment.setHandleDisconnectDevice { devicePublicKey ->
|
||||
// Purge the device pairing from our database
|
||||
val ourPublicKey = TextSecurePreferences.getLocalNumber(this)
|
||||
val database = DatabaseFactory.getLokiAPIDatabase(this)
|
||||
database.removePairingAuthorisation(ourPublicKey, devicePublicKey)
|
||||
// Update mapping on the file server
|
||||
LokiStorageAPI.shared.updateUserDeviceMappings().success {
|
||||
// Send an unpair request to let the device know that it has been revoked
|
||||
MessageSender.sendUnpairRequest(this, devicePublicKey)
|
||||
}
|
||||
// Refresh the list
|
||||
this.deviceListFragment.refresh()
|
||||
Toast.makeText(this, R.string.DeviceListActivity_unlinked_device, Toast.LENGTH_LONG).show()
|
||||
return@setHandleDisconnectDevice null
|
||||
}
|
||||
this.deviceListFragment.setHandleDeviceNameChange { pair ->
|
||||
DatabaseFactory.getLokiUserDatabase(this).setDisplayName(pair.first, pair.second)
|
||||
this.deviceListFragment.refresh()
|
||||
return@setHandleDeviceNameChange null
|
||||
}
|
||||
initFragment(android.R.id.content, deviceListFragment, dynamicLanguage.currentLocale)
|
||||
}
|
||||
|
||||
public override fun onResume() {
|
||||
super.onResume()
|
||||
dynamicTheme.onResume(this)
|
||||
dynamicLanguage.onResume(this)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == android.R.id.home) {
|
||||
finish()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun sendPairingAuthorizedMessage(pairingAuthorisation: PairingAuthorisation) {
|
||||
AsyncTask.execute {
|
||||
signAndSendPairingAuthorisationMessage(this, pairingAuthorisation)
|
||||
Util.runOnMain { this.deviceListFragment.refresh() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import org.thoughtcrime.securesms.database.Database
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.*
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.loki.api.LokiAPIDatabaseProtocol
|
||||
@@ -55,15 +56,24 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
||||
private val grantSignature = "grant_signature"
|
||||
@JvmStatic val createPairingAuthorisationTableCommand = "CREATE TABLE $pairingAuthorisationCache ($primaryDevicePublicKey TEXT, $secondaryDevicePublicKey TEXT, " +
|
||||
"$requestSignature TEXT NULLABLE DEFAULT NULL, $grantSignature TEXT NULLABLE DEFAULT NULL, PRIMARY KEY ($primaryDevicePublicKey, $secondaryDevicePublicKey));"
|
||||
// User count cache
|
||||
private val userCountCache = "loki_user_count_cache"
|
||||
private val publicChatID = "public_chat_id"
|
||||
private val userCount = "user_count"
|
||||
@JvmStatic val createUserCountTableCommand = "CREATE TABLE $userCountCache ($publicChatID STRING PRIMARY KEY, $userCount INTEGER DEFAULT 0);"
|
||||
}
|
||||
|
||||
override fun getSwarmCache(hexEncodedPublicKey: String): Set<LokiAPITarget>? {
|
||||
val database = databaseHelper.readableDatabase
|
||||
return database.get(swarmCache, "${Companion.hexEncodedPublicKey} = ?", wrap(hexEncodedPublicKey)) { cursor ->
|
||||
val swarmAsString = cursor.getString(cursor.getColumnIndexOrThrow(swarm))
|
||||
swarmAsString.split(", ").map { targetAsString ->
|
||||
val components = targetAsString.split("?port=")
|
||||
LokiAPITarget(components[0], components[1].toInt())
|
||||
swarmAsString.split(", ").mapNotNull { targetAsString ->
|
||||
val components = targetAsString.split("-")
|
||||
val address = components[0]
|
||||
val port = components.getOrNull(1)?.toIntOrNull() ?: return@mapNotNull null
|
||||
val idKey = components.getOrNull(2) ?: return@mapNotNull null
|
||||
val encryptionKey = components.getOrNull(3)?: return@mapNotNull null
|
||||
LokiAPITarget(address, port, LokiAPITarget.KeySet(idKey, encryptionKey))
|
||||
}
|
||||
}?.toSet()
|
||||
}
|
||||
@@ -71,7 +81,12 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
||||
override fun setSwarmCache(hexEncodedPublicKey: String, newValue: Set<LokiAPITarget>) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val swarmAsString = newValue.joinToString(", ") { target ->
|
||||
"${target.address}?port=${target.port}"
|
||||
var string = "${target.address}-${target.port}"
|
||||
val keySet = target.publicKeySet
|
||||
if (keySet != null) {
|
||||
string += "-${keySet.idKey}-${keySet.encryptionKey}"
|
||||
}
|
||||
string
|
||||
}
|
||||
val row = wrap(mapOf( Companion.hexEncodedPublicKey to hexEncodedPublicKey, swarm to swarmAsString ))
|
||||
database.insertOrUpdate(swarmCache, row, "${Companion.hexEncodedPublicKey} = ?", wrap(hexEncodedPublicKey))
|
||||
@@ -186,14 +201,29 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
|
||||
}
|
||||
|
||||
override fun removePairingAuthorisations(hexEncodedPublicKey: String) {
|
||||
val database = databaseHelper.readableDatabase
|
||||
val database = databaseHelper.writableDatabase
|
||||
database.delete(pairingAuthorisationCache, "$primaryDevicePublicKey = ? OR $secondaryDevicePublicKey = ?", arrayOf( hexEncodedPublicKey, hexEncodedPublicKey ))
|
||||
}
|
||||
|
||||
fun removePairingAuthorisation(primaryDevicePublicKey: String, secondaryDevicePublicKey: String) {
|
||||
val database = databaseHelper.readableDatabase
|
||||
val database = databaseHelper.writableDatabase
|
||||
database.delete(pairingAuthorisationCache, "${Companion.primaryDevicePublicKey} = ? OR ${Companion.secondaryDevicePublicKey} = ?", arrayOf( primaryDevicePublicKey, secondaryDevicePublicKey ))
|
||||
}
|
||||
|
||||
fun getUserCount(group: Long, server: String): Int? {
|
||||
val database = databaseHelper.readableDatabase
|
||||
val index = "$server.$group"
|
||||
return database.get(userCountCache, "$publicChatID = ?", wrap(index)) { cursor ->
|
||||
cursor.getInt(userCount)
|
||||
}?.toInt()
|
||||
}
|
||||
|
||||
override fun setUserCount(userCount: Int, group: Long, server: String) {
|
||||
val database = databaseHelper.writableDatabase
|
||||
val index = "$server.$group"
|
||||
val row = wrap(mapOf( publicChatID to index, LokiAPIDatabase.userCount to userCount.toString() ))
|
||||
database.insertOrUpdate(userCountCache, row, "$publicChatID = ?", wrap(index))
|
||||
}
|
||||
}
|
||||
|
||||
// region Convenience
|
||||
|
||||
@@ -6,6 +6,9 @@ import org.thoughtcrime.securesms.database.Address
|
||||
import org.thoughtcrime.securesms.database.Database
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.get
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.getInt
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.insertOrUpdate
|
||||
import org.whispersystems.signalservice.loki.messaging.LokiMessageDatabaseProtocol
|
||||
import org.whispersystems.signalservice.loki.messaging.LokiMessageFriendRequestStatus
|
||||
|
||||
|
||||
@@ -9,6 +9,10 @@ import org.thoughtcrime.securesms.database.Database
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
import org.thoughtcrime.securesms.logging.Log
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.get
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.getBase64EncodedData
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.getInt
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.insertOrUpdate
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.libsignal.IdentityKey
|
||||
|
||||
@@ -5,6 +5,9 @@ import android.content.Context
|
||||
import org.thoughtcrime.securesms.crypto.PreKeyUtil
|
||||
import org.thoughtcrime.securesms.database.Database
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.get
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.getInt
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.insertOrUpdate
|
||||
import org.whispersystems.libsignal.state.PreKeyRecord
|
||||
import org.whispersystems.signalservice.loki.messaging.LokiPreKeyRecordDatabaseProtocol
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.groups.GroupManager
|
||||
import org.thoughtcrime.securesms.loki.redesign.messaging.LokiPublicChatPoller
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.loki.api.LokiPublicChat
|
||||
|
||||
@@ -4,8 +4,9 @@ import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.text.Html
|
||||
import android.util.Log
|
||||
import com.prof.rssparser.Parser
|
||||
import kotlinx.coroutines.*
|
||||
import com.prof.rssparser.engine.XMLParser
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.Runnable
|
||||
import org.thoughtcrime.securesms.jobs.PushDecryptJob
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceContent
|
||||
@@ -13,6 +14,8 @@ import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroup
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.loki.api.LokiRSSFeed
|
||||
import org.whispersystems.signalservice.loki.api.LokiRSSProxy
|
||||
import org.whispersystems.signalservice.loki.utilities.successBackground
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.regex.Pattern
|
||||
|
||||
@@ -22,7 +25,6 @@ class LokiRSSFeedPoller(private val context: Context, private val feed: LokiRSSF
|
||||
private var hasStarted = false
|
||||
|
||||
private val task = object : Runnable {
|
||||
|
||||
override fun run() {
|
||||
poll()
|
||||
handler.postDelayed(this, interval)
|
||||
@@ -46,32 +48,28 @@ class LokiRSSFeedPoller(private val context: Context, private val feed: LokiRSSF
|
||||
}
|
||||
|
||||
private fun poll() {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
try {
|
||||
val url = feed.url
|
||||
val parser = Parser()
|
||||
val items = parser.getArticles(url)
|
||||
items.reversed().forEach { item ->
|
||||
val title = item.title ?: return@forEach
|
||||
val description = item.description ?: return@forEach
|
||||
val dateAsString = item.pubDate ?: return@forEach
|
||||
val formatter = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z") // e.g. Tue, 27 Aug 2019 03:52:05 +0000
|
||||
val date = formatter.parse(dateAsString)
|
||||
val timestamp = date.time
|
||||
var bodyAsHTML = "$title<br>$description"
|
||||
val urlRegex = Pattern.compile("<a\\s+(?:[^>]*?\\s+)?href=\"([^\"]*)\".*?>(.*?)<.*?\\/a>")
|
||||
val matcher = urlRegex.matcher(bodyAsHTML)
|
||||
bodyAsHTML = matcher.replaceAll("$2 ($1)")
|
||||
val body = Html.fromHtml(bodyAsHTML).toString().trim()
|
||||
val id = feed.id.toByteArray()
|
||||
val x1 = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, SignalServiceGroup.GroupType.RSS_FEED, null, null, null, null)
|
||||
val x2 = SignalServiceDataMessage(timestamp, x1, null, body)
|
||||
val x3 = SignalServiceContent(x2, "Loki", SignalServiceAddress.DEFAULT_DEVICE_ID, timestamp, false)
|
||||
PushDecryptJob(context).handleTextMessage(x3, x2, Optional.absent(), Optional.absent())
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
Log.d("Loki", "Couldn't update RSS feed with ID: $feed.id.")
|
||||
LokiRSSProxy.fetch(feed.url).successBackground { xml ->
|
||||
val items = XMLParser(xml).call()
|
||||
items.reversed().forEach { item ->
|
||||
val title = item.title ?: return@forEach
|
||||
val description = item.description ?: return@forEach
|
||||
val dateAsString = item.pubDate ?: return@forEach
|
||||
val formatter = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z") // e.g. Tue, 27 Aug 2019 03:52:05 +0000
|
||||
val date = formatter.parse(dateAsString)
|
||||
val timestamp = date.time
|
||||
var bodyAsHTML = "$title<br>$description"
|
||||
val urlRegex = Pattern.compile("<a\\s+(?:[^>]*?\\s+)?href=\"([^\"]*)\".*?>(.*?)<.*?\\/a>")
|
||||
val matcher = urlRegex.matcher(bodyAsHTML)
|
||||
bodyAsHTML = matcher.replaceAll("$2 ($1)")
|
||||
val body = Html.fromHtml(bodyAsHTML).toString().trim()
|
||||
val id = feed.id.toByteArray()
|
||||
val x1 = SignalServiceGroup(SignalServiceGroup.Type.UPDATE, id, SignalServiceGroup.GroupType.RSS_FEED, null, null, null, null)
|
||||
val x2 = SignalServiceDataMessage(timestamp, x1, null, body)
|
||||
val x3 = SignalServiceContent(x2, "Loki", SignalServiceAddress.DEFAULT_DEVICE_ID, timestamp, false)
|
||||
PushDecryptJob(context).handleTextMessage(x3, x2, Optional.absent(), Optional.absent())
|
||||
}
|
||||
}.fail { exception ->
|
||||
Log.d("Loki", "Couldn't update RSS feed with ID: $feed.id. $exception")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,11 @@ import org.thoughtcrime.securesms.database.Address
|
||||
import org.thoughtcrime.securesms.database.Database
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.get
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.getInt
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.getLong
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.getString
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.insertOrUpdate
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.internal.util.JsonUtil
|
||||
@@ -158,9 +163,9 @@ class LokiThreadDatabase(context: Context, helper: SQLCipherOpenHelper) : Databa
|
||||
|
||||
fun getSessionRestoreDevices(threadID: Long): Set<String> {
|
||||
return TextSecurePreferences.getStringPreference(context, "session_restore_devices_$threadID", "")
|
||||
.split(",")
|
||||
.filter { PublicKeyValidation.isValid(it) }
|
||||
.toSet()
|
||||
.split(",")
|
||||
.filter { PublicKeyValidation.isValid(it) }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
fun removeAllSessionRestoreDevices(threadID: Long) {
|
||||
|
||||
@@ -7,6 +7,8 @@ import android.util.Log
|
||||
import org.thoughtcrime.securesms.database.Address
|
||||
import org.thoughtcrime.securesms.database.Database
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.get
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.insertOrUpdate
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.loki.messaging.LokiUserDatabaseProtocol
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
package org.thoughtcrime.securesms.loki
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity
|
||||
import org.thoughtcrime.securesms.database.Address
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.qr.ScanListener
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.loki.utilities.Analytics
|
||||
import org.whispersystems.signalservice.loki.utilities.PublicKeyValidation
|
||||
|
||||
class NewConversationActivity : PassphraseRequiredActionBarActivity(), ScanListener {
|
||||
private val dynamicTheme = DynamicTheme()
|
||||
|
||||
override fun onPreCreate() {
|
||||
dynamicTheme.onCreate(this)
|
||||
}
|
||||
|
||||
override fun onCreate(bundle: Bundle?, isReady: Boolean) {
|
||||
supportActionBar!!.setTitle(R.string.fragment_new_conversation_title)
|
||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||
val fragment = NewConversationFragment()
|
||||
initFragment(android.R.id.content, fragment, null)
|
||||
}
|
||||
|
||||
public override fun onResume() {
|
||||
super.onResume()
|
||||
dynamicTheme.onResume(this)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == android.R.id.home) {
|
||||
onBackPressed()
|
||||
return true
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
fun scanQRCode() {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(getString(R.string.fragment_scan_qr_code_camera_permission_dialog_message))
|
||||
.onAllGranted {
|
||||
val fragment = ScanQRCodeFragment()
|
||||
fragment.scanListener = this
|
||||
supportFragmentManager.beginTransaction().replace(android.R.id.content, fragment).addToBackStack(null).commitAllowingStateLoss()
|
||||
}
|
||||
.onAnyDenied { Toast.makeText(this, R.string.fragment_scan_qr_code_camera_permission_dialog_message, Toast.LENGTH_SHORT).show() }
|
||||
.execute()
|
||||
}
|
||||
|
||||
override fun onQrDataFound(hexEncodedPublicKey: String) {
|
||||
Analytics.shared.track("QR Code Scanned")
|
||||
startNewConversationIfPossible(hexEncodedPublicKey)
|
||||
}
|
||||
|
||||
fun startNewConversationIfPossible(hexEncodedPublicKey: String) {
|
||||
if (!PublicKeyValidation.isValid(hexEncodedPublicKey)) { return Toast.makeText(this, R.string.fragment_new_conversation_invalid_public_key_message, Toast.LENGTH_SHORT).show() }
|
||||
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this)
|
||||
// If we try to contact our master device then redirect to note to self
|
||||
val contactPublicKey = if (TextSecurePreferences.getMasterHexEncodedPublicKey(this) == hexEncodedPublicKey) userHexEncodedPublicKey else hexEncodedPublicKey
|
||||
val contact = Recipient.from(this, Address.fromSerialized(contactPublicKey), true)
|
||||
val intent = Intent(this, ConversationActivity::class.java)
|
||||
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, contact.address)
|
||||
intent.putExtra(ConversationActivity.TEXT_EXTRA, getIntent().getStringExtra(ConversationActivity.TEXT_EXTRA))
|
||||
intent.setDataAndType(getIntent().data, getIntent().type)
|
||||
val existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(contact)
|
||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, existingThread)
|
||||
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT)
|
||||
Analytics.shared.track("New Conversation Started")
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package org.thoughtcrime.securesms.loki
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import kotlinx.android.synthetic.main.fragment_new_conversation.*
|
||||
import network.loki.messenger.R
|
||||
|
||||
class NewConversationFragment : Fragment() {
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_new_conversation, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
qrCodeButton.setOnClickListener {
|
||||
val activity = activity as NewConversationActivity
|
||||
activity.scanQRCode()
|
||||
}
|
||||
nextButton.setOnClickListener {
|
||||
val activity = activity as NewConversationActivity
|
||||
val hexEncodedPublicKey = publicKeyEditText.text.toString().trim()
|
||||
activity.startNewConversationIfPossible(hexEncodedPublicKey)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val activity = activity as NewConversationActivity
|
||||
activity.supportActionBar!!.setTitle(R.string.fragment_new_conversation_title)
|
||||
}
|
||||
}
|
||||
@@ -16,32 +16,23 @@ import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
data class BackgroundMessage private constructor(val data: Map<String, Any>) {
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
fun create(recipient: String) = BackgroundMessage(mapOf("recipient" to recipient))
|
||||
|
||||
@JvmStatic
|
||||
fun createFriendRequest(recipient: String, messageBody: String) = BackgroundMessage(mapOf(
|
||||
"recipient" to recipient,
|
||||
"body" to messageBody,
|
||||
"friendRequest" to true
|
||||
))
|
||||
fun createFriendRequest(recipient: String, messageBody: String) = BackgroundMessage(mapOf( "recipient" to recipient, "body" to messageBody, "friendRequest" to true ))
|
||||
|
||||
@JvmStatic
|
||||
fun createUnpairingRequest(recipient: String) = BackgroundMessage(mapOf(
|
||||
"recipient" to recipient,
|
||||
"unpairingRequest" to true
|
||||
))
|
||||
fun createUnpairingRequest(recipient: String) = BackgroundMessage(mapOf( "recipient" to recipient, "unpairingRequest" to true ))
|
||||
|
||||
@JvmStatic
|
||||
fun createSessionRestore(recipient: String) = BackgroundMessage(mapOf(
|
||||
"recipient" to recipient,
|
||||
"friendRequest" to true,
|
||||
"sessionRestore" to true
|
||||
))
|
||||
fun createSessionRestore(recipient: String) = BackgroundMessage(mapOf( "recipient" to recipient, "friendRequest" to true, "sessionRestore" to true ))
|
||||
|
||||
@JvmStatic
|
||||
fun createSessionRequest(recipient: String) = BackgroundMessage(mapOf(
|
||||
"recipient" to recipient,
|
||||
"friendRequest" to true,
|
||||
"sessionRequest" to true
|
||||
))
|
||||
fun createSessionRequest(recipient: String) = BackgroundMessage(mapOf("recipient" to recipient, "friendRequest" to true, "sessionRequest" to true))
|
||||
|
||||
internal fun parse(serialized: String): BackgroundMessage {
|
||||
val data = JsonUtil.fromJson(serialized, Map::class.java) as? Map<String, Any> ?: throw AssertionError("JSON parsing failed")
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
package org.thoughtcrime.securesms.loki
|
||||
|
||||
import android.content.Context
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.util.AttributeSet
|
||||
import android.util.DisplayMetrics
|
||||
import android.widget.LinearLayout
|
||||
import kotlinx.android.synthetic.main.view_qr_code.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.qr.QrCode
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
|
||||
object QRCodeDialog {
|
||||
|
||||
fun show(context: Context) {
|
||||
val view = QRCodeView(context)
|
||||
val dialog = AlertDialog.Builder(context).setView(view).show()
|
||||
view.onCancel = { dialog.dismiss() }
|
||||
}
|
||||
}
|
||||
|
||||
class QRCodeView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : LinearLayout(context, attrs, defStyleAttr) {
|
||||
var onCancel: (() -> Unit)? = null
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
||||
constructor(context: Context) : this(context, null)
|
||||
|
||||
init {
|
||||
inflate(context, R.layout.view_qr_code, this)
|
||||
val hexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(getContext()) ?: TextSecurePreferences.getLocalNumber(context)
|
||||
val displayMetrics = DisplayMetrics()
|
||||
ServiceUtil.getWindowManager(context).defaultDisplay.getMetrics(displayMetrics)
|
||||
val size = displayMetrics.widthPixels - 2 * toPx(96, resources)
|
||||
val qrCode = QrCode.create(hexEncodedPublicKey, size)
|
||||
qrCodeImageView.setImageBitmap(qrCode)
|
||||
cancelButton.setOnClickListener { onCancel?.invoke() }
|
||||
}
|
||||
}
|
||||
@@ -1,273 +0,0 @@
|
||||
package org.thoughtcrime.securesms.loki
|
||||
|
||||
import android.Manifest
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.AsyncTask
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.FragmentManager
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import kotlinx.android.synthetic.main.activity_seed.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.ConversationListActivity
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.database.Address
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase
|
||||
import org.thoughtcrime.securesms.logging.Log
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.qr.ScanListener
|
||||
import org.thoughtcrime.securesms.util.Hex
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.curve25519.Curve25519
|
||||
import org.whispersystems.libsignal.util.KeyHelper
|
||||
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
|
||||
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec
|
||||
import org.whispersystems.signalservice.loki.utilities.Analytics
|
||||
import org.whispersystems.signalservice.loki.utilities.PublicKeyValidation
|
||||
import org.whispersystems.signalservice.loki.utilities.hexEncodedPublicKey
|
||||
import org.whispersystems.signalservice.loki.utilities.retryIfNeeded
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
class SeedActivity : BaseActionBarActivity(), DeviceLinkingDelegate, ScanListener {
|
||||
private lateinit var languageFileDirectory: File
|
||||
private var mode = Mode.Register
|
||||
set(newValue) { field = newValue; updateUI() }
|
||||
private var seed: ByteArray? = null
|
||||
set(newValue) { field = newValue; updateMnemonic() }
|
||||
private var mnemonic: String? = null
|
||||
set(newValue) { field = newValue; updateMnemonicTextView() }
|
||||
|
||||
// region Types
|
||||
enum class Mode { Register, Restore, Link }
|
||||
// endregion
|
||||
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_seed)
|
||||
setUpLanguageFileDirectory()
|
||||
mnemonicEditText.input.imeOptions = mnemonicEditText.input.imeOptions or 16777216 // Always use incognito keyboard for this
|
||||
updateSeed()
|
||||
copyButton.setOnClickListener { copy() }
|
||||
toggleRegisterModeButton.setOnClickListener { mode = Mode.Register }
|
||||
toggleRestoreModeButton.setOnClickListener { mode = Mode.Restore }
|
||||
toggleLinkModeButton.setOnClickListener { mode = Mode.Link }
|
||||
mainButton.setOnClickListener { handleMainButtonTapped() }
|
||||
scanQRButton.setOnClickListener {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(getString(R.string.fragment_scan_qr_code_camera_permission_dialog_message))
|
||||
.onAllGranted {
|
||||
val fragment = ScanQRCodeFragment()
|
||||
fragment.mode = ScanQRCodeFragment.Mode.LinkDevice
|
||||
fragment.scanListener = this
|
||||
supportFragmentManager.beginTransaction().replace(android.R.id.content, fragment).addToBackStack("QR").commitAllowingStateLoss()
|
||||
publicKeyEditText.clearFocus()
|
||||
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputMethodManager.hideSoftInputFromWindow(publicKeyEditText.windowToken, 0)
|
||||
}
|
||||
.onAnyDenied { Toast.makeText(this, R.string.fragment_scan_qr_code_camera_permission_dialog_message, Toast.LENGTH_SHORT).show() }
|
||||
.execute()
|
||||
}
|
||||
Analytics.shared.track("Seed Screen Viewed")
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region General
|
||||
private fun setUpLanguageFileDirectory() {
|
||||
val languages = listOf( "english", "japanese", "portuguese", "spanish" )
|
||||
val directory = File(applicationInfo.dataDir)
|
||||
for (language in languages) {
|
||||
val fileName = "$language.txt"
|
||||
if (directory.list().contains(fileName)) { continue }
|
||||
val inputStream = 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
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
private fun updateSeed() {
|
||||
val seed = Curve25519.getInstance(Curve25519.BEST).generateSeed(16)
|
||||
try {
|
||||
IdentityKeyUtil.generateIdentityKeyPair(this, seed + seed)
|
||||
} catch (exception: Exception) {
|
||||
return updateSeed()
|
||||
}
|
||||
this.seed = seed
|
||||
}
|
||||
|
||||
private fun updateUI() {
|
||||
val registerModeVisibility = if (mode == Mode.Register) View.VISIBLE else View.GONE
|
||||
val restoreModeVisibility = if (mode == Mode.Restore) View.VISIBLE else View.GONE
|
||||
val linkModeVisibility = if (mode == Mode.Link) View.VISIBLE else View.GONE
|
||||
seedExplanationTextView1.visibility = registerModeVisibility
|
||||
mnemonicTextView.visibility = registerModeVisibility
|
||||
copyButton.visibility = registerModeVisibility
|
||||
seedExplanationTextView2.visibility = restoreModeVisibility
|
||||
mnemonicEditText.visibility = restoreModeVisibility
|
||||
linkExplanationTextView.visibility = linkModeVisibility
|
||||
publicKeyEditText.visibility = linkModeVisibility
|
||||
scanQRButton.visibility = linkModeVisibility
|
||||
toggleRegisterModeButton.visibility = if (mode != Mode.Register) View.VISIBLE else View.GONE
|
||||
toggleRestoreModeButton.visibility = if (mode != Mode.Restore) View.VISIBLE else View.GONE
|
||||
toggleLinkModeButton.visibility = if (mode != Mode.Link) View.VISIBLE else View.GONE
|
||||
val mainButtonTitleID = when (mode) {
|
||||
Mode.Register -> R.string.activity_key_pair_main_button_title_1
|
||||
Mode.Restore -> R.string.activity_key_pair_main_button_title_2
|
||||
Mode.Link -> R.string.activity_key_pair_main_button_title_3
|
||||
}
|
||||
mainButton.setText(mainButtonTitleID)
|
||||
if (mode == Mode.Restore) {
|
||||
mnemonicEditText.requestFocus()
|
||||
} else {
|
||||
mnemonicEditText.clearFocus()
|
||||
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputMethodManager.hideSoftInputFromWindow(mnemonicEditText.windowToken, 0)
|
||||
}
|
||||
if (mode == Mode.Link) {
|
||||
publicKeyEditText.requestFocus()
|
||||
} else {
|
||||
publicKeyEditText.clearFocus()
|
||||
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputMethodManager.hideSoftInputFromWindow(publicKeyEditText.windowToken, 0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateMnemonic() {
|
||||
val hexEncodedSeed = Hex.toStringCondensed(seed)
|
||||
mnemonic = MnemonicCodec(languageFileDirectory).encode(hexEncodedSeed)
|
||||
}
|
||||
|
||||
private fun updateMnemonicTextView() {
|
||||
mnemonicTextView.text = mnemonic!!
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Interaction
|
||||
private fun copy() {
|
||||
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("Mnemonic", mnemonic)
|
||||
clipboard.primaryClip = clip
|
||||
Toast.makeText(this, R.string.activity_key_pair_mnemonic_copied_message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun handleMainButtonTapped() {
|
||||
var seed: ByteArray
|
||||
when (mode) {
|
||||
Mode.Register -> seed = this.seed!!
|
||||
Mode.Restore -> {
|
||||
val mnemonic = mnemonicEditText.text.toString()
|
||||
try {
|
||||
val hexEncodedSeed = MnemonicCodec(languageFileDirectory).decode(mnemonic)
|
||||
seed = Hex.fromStringCondensed(hexEncodedSeed)
|
||||
} catch (e: Exception) {
|
||||
val message = if (e is MnemonicCodec.DecodingError) e.description else MnemonicCodec.DecodingError.Generic.description
|
||||
return Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
Mode.Link -> {
|
||||
val hexEncodedPublicKey = publicKeyEditText.text.trim().toString()
|
||||
if (!PublicKeyValidation.isValid(hexEncodedPublicKey)) {
|
||||
return Toast.makeText(this, "Invalid public key", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
seed = this.seed!!
|
||||
}
|
||||
}
|
||||
val hexEncodedSeed = Hex.toStringCondensed(seed)
|
||||
IdentityKeyUtil.save(this, IdentityKeyUtil.lokiSeedKey, hexEncodedSeed)
|
||||
if (seed.count() == 16) seed += seed
|
||||
if (mode == Mode.Restore) {
|
||||
IdentityKeyUtil.generateIdentityKeyPair(this, seed)
|
||||
}
|
||||
val keyPair = IdentityKeyUtil.getIdentityKeyPair(this)
|
||||
val userHexEncodedPublicKey = keyPair.hexEncodedPublicKey
|
||||
val registrationID = KeyHelper.generateRegistrationId(false)
|
||||
TextSecurePreferences.setLocalRegistrationId(this, registrationID)
|
||||
DatabaseFactory.getIdentityDatabase(this).saveIdentity(Address.fromSerialized(userHexEncodedPublicKey), keyPair.publicKey,
|
||||
IdentityDatabase.VerifiedStatus.VERIFIED, true, System.currentTimeMillis(), true)
|
||||
TextSecurePreferences.setLocalNumber(this, userHexEncodedPublicKey)
|
||||
if (mode == Mode.Restore) {
|
||||
TextSecurePreferences.setRestorationTime(this, System.currentTimeMillis())
|
||||
}
|
||||
when (mode) {
|
||||
Mode.Register -> Analytics.shared.track("Seed Created")
|
||||
Mode.Restore -> Analytics.shared.track("Seed Restored")
|
||||
Mode.Link -> Analytics.shared.track("Device Linking Attempted")
|
||||
}
|
||||
if (mode == Mode.Link) {
|
||||
TextSecurePreferences.setHasSeenWelcomeScreen(this, true)
|
||||
TextSecurePreferences.setPromptedPushRegistration(this, true)
|
||||
val masterHexEncodedPublicKey = publicKeyEditText.text.trim().toString()
|
||||
val authorisation = PairingAuthorisation(masterHexEncodedPublicKey, userHexEncodedPublicKey).sign(PairingAuthorisation.Type.REQUEST, keyPair.privateKey.serialize())
|
||||
if (authorisation == null) {
|
||||
Log.d("Loki", "Failed to sign pairing request.")
|
||||
resetForRegistration()
|
||||
return Toast.makeText(application, "Couldn't start device linking process.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
val application = ApplicationContext.getInstance(this)
|
||||
application.startLongPollingIfNeeded()
|
||||
application.setUpP2PAPI()
|
||||
application.setUpStorageAPIIfNeeded()
|
||||
DeviceLinkingDialog.show(this, DeviceLinkingView.Mode.Slave, this)
|
||||
AsyncTask.execute {
|
||||
retryIfNeeded(8) {
|
||||
sendPairingAuthorisationMessage(this@SeedActivity, authorisation.primaryDevicePublicKey, authorisation)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
startActivity(Intent(this, DisplayNameActivity::class.java))
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleDeviceLinkAuthorized(pairingAuthorisation: PairingAuthorisation) {
|
||||
Analytics.shared.track("Device Linked Successfully")
|
||||
if (pairingAuthorisation.secondaryDevicePublicKey == TextSecurePreferences.getLocalNumber(this)) {
|
||||
TextSecurePreferences.setMasterHexEncodedPublicKey(this, pairingAuthorisation.primaryDevicePublicKey)
|
||||
}
|
||||
startActivity(Intent(this, ConversationListActivity::class.java))
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun handleDeviceLinkingDialogDismissed() {
|
||||
resetForRegistration()
|
||||
}
|
||||
|
||||
private fun resetForRegistration() {
|
||||
IdentityKeyUtil.delete(this, IdentityKeyUtil.lokiSeedKey)
|
||||
TextSecurePreferences.removeLocalNumber(this)
|
||||
TextSecurePreferences.setHasSeenWelcomeScreen(this, false)
|
||||
TextSecurePreferences.setPromptedPushRegistration(this, false)
|
||||
}
|
||||
// endregion
|
||||
override fun onQrDataFound(data: String?) {
|
||||
runOnUiThread {
|
||||
if (data != null && PublicKeyValidation.isValid(data.trim())) {
|
||||
publicKeyEditText.setText(data.trim())
|
||||
supportFragmentManager.popBackStackImmediate("QR", FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||
handleMainButtonTapped()
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
@@ -10,18 +10,14 @@ import kotlinx.android.synthetic.main.session_restore_banner.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
/**
|
||||
* View to display actionable reminders to the user
|
||||
*/
|
||||
class SessionRestoreBannerView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : LinearLayout(context, attrs, defStyleAttr) {
|
||||
class SessionRestoreBannerView : LinearLayout {
|
||||
lateinit var recipient: Recipient
|
||||
var onDismiss: (() -> Unit)? = null
|
||||
var onRestore: (() -> Unit)? = null
|
||||
|
||||
// region Initialization
|
||||
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
|
||||
constructor(context: Context) : this(context, null)
|
||||
// endregion
|
||||
constructor(context: Context) : super(context, null)
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs, 0)
|
||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
|
||||
|
||||
init {
|
||||
LayoutInflater.from(context).inflate(R.layout.session_restore_banner, this, true)
|
||||
@@ -31,7 +27,7 @@ class SessionRestoreBannerView(context: Context, attrs: AttributeSet?, defStyleA
|
||||
|
||||
fun update(recipient: Recipient) {
|
||||
this.recipient = recipient
|
||||
restoreText.text = context.getString(R.string.session_restore_banner_message, recipient.toShortString())
|
||||
messageTextView.text = context.getString(R.string.session_restore_banner_message, recipient.toShortString())
|
||||
}
|
||||
|
||||
fun show() {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.activities
|
||||
|
||||
import android.os.Bundle
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.preferences.ChatsPreferenceFragment
|
||||
|
||||
class ChatSettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
setContentView(R.layout.activity_fragment_wrapper)
|
||||
supportActionBar!!.title = "Chats"
|
||||
val fragment = ChatsPreferenceFragment()
|
||||
val transaction = supportFragmentManager.beginTransaction()
|
||||
transaction.replace(R.id.fragmentContainer, fragment)
|
||||
transaction.commit()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.activities
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.app.FragmentPagerAdapter
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import kotlinx.android.synthetic.main.activity_create_private_chat.*
|
||||
import kotlinx.android.synthetic.main.fragment_enter_public_key.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity
|
||||
import org.thoughtcrime.securesms.database.Address
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.loki.redesign.fragments.ScanQRCodeWrapperFragment
|
||||
import org.thoughtcrime.securesms.loki.redesign.fragments.ScanQRCodeWrapperFragmentDelegate
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.loki.utilities.PublicKeyValidation
|
||||
|
||||
class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
||||
private val adapter = CreatePrivateChatActivityAdapter(this)
|
||||
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
// Set content view
|
||||
setContentView(R.layout.activity_create_private_chat)
|
||||
// Set title
|
||||
supportActionBar!!.title = "New Session"
|
||||
// Set up view pager
|
||||
viewPager.adapter = adapter
|
||||
tabLayout.setupWithViewPager(viewPager)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Interaction
|
||||
override fun handleQRCodeScanned(hexEncodedPublicKey: String) {
|
||||
createPrivateChatIfPossible(hexEncodedPublicKey)
|
||||
}
|
||||
|
||||
fun createPrivateChatIfPossible(hexEncodedPublicKey: String) {
|
||||
if (!PublicKeyValidation.isValid(hexEncodedPublicKey)) { return Toast.makeText(this, "Invalid Session ID", Toast.LENGTH_SHORT).show() }
|
||||
val masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(this)
|
||||
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this)
|
||||
val targetHexEncodedPublicKey = if (hexEncodedPublicKey == masterHexEncodedPublicKey) userHexEncodedPublicKey else hexEncodedPublicKey
|
||||
val recipient = Recipient.from(this, Address.fromSerialized(targetHexEncodedPublicKey), true)
|
||||
val intent = Intent(this, ConversationActivity::class.java)
|
||||
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.address)
|
||||
intent.putExtra(ConversationActivity.TEXT_EXTRA, getIntent().getStringExtra(ConversationActivity.TEXT_EXTRA))
|
||||
intent.setDataAndType(getIntent().data, getIntent().type)
|
||||
val existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient)
|
||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, existingThread)
|
||||
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
// region Adapter
|
||||
private class CreatePrivateChatActivityAdapter(val activity: CreatePrivateChatActivity) : FragmentPagerAdapter(activity.supportFragmentManager) {
|
||||
|
||||
override fun getCount(): Int {
|
||||
return 2
|
||||
}
|
||||
|
||||
override fun getItem(index: Int): Fragment {
|
||||
return when (index) {
|
||||
0 -> EnterPublicKeyFragment()
|
||||
1 -> {
|
||||
val result = ScanQRCodeWrapperFragment()
|
||||
result.delegate = activity
|
||||
result.message = "Scan a user’s QR code to start a session. QR codes can be found by tapping the QR code icon in account settings."
|
||||
result
|
||||
}
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPageTitle(index: Int): CharSequence? {
|
||||
return when (index) {
|
||||
0 -> "Enter Session ID"
|
||||
1 -> "Scan QR Code"
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Enter Public Key Fragment
|
||||
class EnterPublicKeyFragment : Fragment() {
|
||||
|
||||
private val hexEncodedPublicKey: String
|
||||
get() {
|
||||
val masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context!!)
|
||||
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context!!)
|
||||
return masterHexEncodedPublicKey ?: userHexEncodedPublicKey
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
return inflater.inflate(R.layout.fragment_enter_public_key, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
publicKeyTextView.imeOptions = publicKeyTextView.imeOptions or 16777216 // Always use incognito keyboard
|
||||
publicKeyTextView.text = hexEncodedPublicKey
|
||||
copyButton.setOnClickListener { copyPublicKey() }
|
||||
shareButton.setOnClickListener { sharePublicKey() }
|
||||
createPrivateChatButton.setOnClickListener { createPrivateChatIfPossible() }
|
||||
}
|
||||
|
||||
private fun copyPublicKey() {
|
||||
val clipboard = activity!!.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("Session ID", hexEncodedPublicKey)
|
||||
clipboard.primaryClip = clip
|
||||
Toast.makeText(context!!, R.string.activity_register_public_key_copied_message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun sharePublicKey() {
|
||||
val intent = Intent()
|
||||
intent.action = Intent.ACTION_SEND
|
||||
intent.putExtra(Intent.EXTRA_TEXT, hexEncodedPublicKey)
|
||||
intent.type = "text/plain"
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun createPrivateChatIfPossible() {
|
||||
val hexEncodedPublicKey = publicKeyEditText.text.trim().toString()
|
||||
(activity!! as CreatePrivateChatActivity).createPrivateChatIfPossible(hexEncodedPublicKey)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import kotlinx.android.synthetic.main.activity_display_name_v2.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.setUpActionBarSessionLogo
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.show
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.api.crypto.ProfileCipher
|
||||
|
||||
class DisplayNameActivity : BaseActionBarActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setUpActionBarSessionLogo()
|
||||
setContentView(R.layout.activity_display_name_v2)
|
||||
displayNameEditText.imeOptions = displayNameEditText.imeOptions or 16777216 // Always use incognito keyboard
|
||||
registerButton.setOnClickListener { register() }
|
||||
}
|
||||
|
||||
private fun register() {
|
||||
val displayName = displayNameEditText.text.toString().trim()
|
||||
if (displayName.isEmpty()) {
|
||||
return Toast.makeText(this, "Please pick a display name", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
if (!displayName.matches(Regex("[a-zA-Z0-9_]+"))) {
|
||||
return Toast.makeText(this, "Please pick a display name that consists of only a-z, A-Z, 0-9 and _ characters", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
if (displayName.toByteArray().size > ProfileCipher.NAME_PADDED_LENGTH) {
|
||||
return Toast.makeText(this, "Please pick a shorter display name", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputMethodManager.hideSoftInputFromWindow(displayNameEditText.windowToken, 0)
|
||||
TextSecurePreferences.setProfileName(this, displayName)
|
||||
TextSecurePreferences.setHasSeenWelcomeScreen(this, true)
|
||||
TextSecurePreferences.setPromptedPushRegistration(this, true)
|
||||
val application = ApplicationContext.getInstance(this)
|
||||
application.setUpStorageAPIIfNeeded()
|
||||
application.setUpP2PAPI()
|
||||
val publicChatAPI = ApplicationContext.getInstance(this).lokiPublicChatAPI
|
||||
if (publicChatAPI != null) {
|
||||
// TODO: This won't be necessary anymore when we don't auto-join the Loki Public Chat anymore
|
||||
application.createDefaultPublicChatsIfNeeded()
|
||||
val servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers()
|
||||
servers.forEach { publicChatAPI.setDisplayName(displayName, it) }
|
||||
}
|
||||
val intent = Intent(this, HomeActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
show(intent)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.activities
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.arch.lifecycle.Observer
|
||||
import android.content.Intent
|
||||
import android.database.Cursor
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.os.AsyncTask
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.support.design.widget.Snackbar
|
||||
import android.support.v4.app.LoaderManager
|
||||
import android.support.v4.content.Loader
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.support.v7.widget.helper.ItemTouchHelper
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.view.View
|
||||
import kotlinx.android.synthetic.main.activity_home.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import org.thoughtcrime.securesms.loki.getColorWithID
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.push
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.show
|
||||
import org.thoughtcrime.securesms.loki.redesign.views.ConversationView
|
||||
import org.thoughtcrime.securesms.loki.redesign.views.SeedReminderViewDelegate
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import kotlin.math.abs
|
||||
|
||||
class HomeActivity : PassphraseRequiredActionBarActivity, ConversationClickListener, SeedReminderViewDelegate {
|
||||
private lateinit var glide: GlideRequests
|
||||
|
||||
private val hexEncodedPublicKey: String
|
||||
get() {
|
||||
val masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(this)
|
||||
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this)
|
||||
return masterHexEncodedPublicKey ?: userHexEncodedPublicKey
|
||||
}
|
||||
|
||||
// region Lifecycle
|
||||
constructor() : super()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
// Process any outstanding deletes
|
||||
val threadDatabase = DatabaseFactory.getThreadDatabase(this)
|
||||
val archivedConversationCount = threadDatabase.archivedConversationListCount
|
||||
if (archivedConversationCount > 0) {
|
||||
val archivedConversations = threadDatabase.archivedConversationList
|
||||
archivedConversations.moveToFirst()
|
||||
fun deleteThreadAtCurrentPosition() {
|
||||
val threadID = archivedConversations.getLong(archivedConversations.getColumnIndex(ThreadDatabase.ID))
|
||||
AsyncTask.execute {
|
||||
threadDatabase.deleteConversation(threadID)
|
||||
MessageNotifier.updateNotification(this)
|
||||
}
|
||||
}
|
||||
deleteThreadAtCurrentPosition()
|
||||
while (archivedConversations.moveToNext()) {
|
||||
deleteThreadAtCurrentPosition()
|
||||
}
|
||||
}
|
||||
// Set content view
|
||||
setContentView(R.layout.activity_home)
|
||||
// Set custom toolbar
|
||||
setSupportActionBar(toolbar)
|
||||
// Set up Glide
|
||||
glide = GlideApp.with(this)
|
||||
// Set up toolbar buttons
|
||||
profileButton.glide = glide
|
||||
profileButton.hexEncodedPublicKey = hexEncodedPublicKey
|
||||
profileButton.update()
|
||||
profileButton.setOnClickListener { openSettings() }
|
||||
joinPublicChatButton.setOnClickListener { joinPublicChat() }
|
||||
// Set up seed reminder view
|
||||
val isMasterDevice = (TextSecurePreferences.getMasterHexEncodedPublicKey(this) == null)
|
||||
val hasViewedSeed = TextSecurePreferences.getHasViewedSeed(this)
|
||||
if (!hasViewedSeed && isMasterDevice) {
|
||||
val seedReminderViewTitle = SpannableString("You're almost finished! 80%")
|
||||
seedReminderViewTitle.setSpan(ForegroundColorSpan(resources.getColorWithID(R.color.accent, theme)), 24, 27, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
seedReminderView.title = seedReminderViewTitle
|
||||
seedReminderView.subtitle = "Secure your account by saving your recovery phrase"
|
||||
seedReminderView.setProgress(80, false)
|
||||
seedReminderView.delegate = this
|
||||
} else {
|
||||
seedReminderView.visibility = View.GONE
|
||||
}
|
||||
// Set up recycler view
|
||||
val cursor = DatabaseFactory.getThreadDatabase(this).conversationList
|
||||
val homeAdapter = HomeAdapter(this, cursor)
|
||||
homeAdapter.glide = glide
|
||||
homeAdapter.conversationClickListener = this
|
||||
recyclerView.adapter = homeAdapter
|
||||
recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
ItemTouchHelper(SwipeCallback(this)).attachToRecyclerView(recyclerView)
|
||||
// This is a workaround for the fact that CursorRecyclerViewAdapter doesn't actually auto-update (even though it says it will)
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, object : LoaderManager.LoaderCallbacks<Cursor> {
|
||||
|
||||
override fun onCreateLoader(id: Int, bundle: Bundle?): Loader<Cursor> {
|
||||
return HomeLoader(this@HomeActivity)
|
||||
}
|
||||
|
||||
override fun onLoadFinished(loader: Loader<Cursor>, cursor: Cursor?) {
|
||||
homeAdapter.changeCursor(cursor)
|
||||
}
|
||||
|
||||
override fun onLoaderReset(cursor: Loader<Cursor>) {
|
||||
homeAdapter.changeCursor(null)
|
||||
}
|
||||
})
|
||||
// Set up new conversation button
|
||||
newConversationButton.setOnClickListener { createPrivateChat() }
|
||||
// Set up typing observer
|
||||
ApplicationContext.getInstance(this).typingStatusRepository.typingThreads.observe(this, Observer<Set<Long>> { threadIDs ->
|
||||
val adapter = recyclerView.adapter as HomeAdapter
|
||||
adapter.typingThreadIDs = threadIDs ?: setOf()
|
||||
})
|
||||
// Set up public chats and RSS feeds if needed
|
||||
if (TextSecurePreferences.getLocalNumber(this) != null) {
|
||||
val application = ApplicationContext.getInstance(this)
|
||||
application.createDefaultPublicChatsIfNeeded()
|
||||
application.createRSSFeedsIfNeeded()
|
||||
application.lokiPublicChatManager.startPollersIfNeeded()
|
||||
application.startRSSFeedPollersIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val isMasterDevice = (TextSecurePreferences.getMasterHexEncodedPublicKey(this) == null)
|
||||
val hasViewedSeed = TextSecurePreferences.getHasViewedSeed(this)
|
||||
if (hasViewedSeed || !isMasterDevice) {
|
||||
seedReminderView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
override fun handleSeedReminderViewContinueButtonTapped() {
|
||||
val intent = Intent(this, SeedActivity::class.java)
|
||||
show(intent)
|
||||
}
|
||||
|
||||
override fun onConversationClick(view: ConversationView) {
|
||||
val thread = view.thread ?: return
|
||||
openConversation(thread)
|
||||
}
|
||||
|
||||
override fun onLongConversationClick(view: ConversationView) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
private fun openConversation(thread: ThreadRecord) {
|
||||
val intent = Intent(this, ConversationActivity::class.java)
|
||||
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, thread.recipient.getAddress())
|
||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, thread.threadId)
|
||||
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, thread.distributionType)
|
||||
intent.putExtra(ConversationActivity.TIMING_EXTRA, System.currentTimeMillis())
|
||||
intent.putExtra(ConversationActivity.LAST_SEEN_EXTRA, thread.lastSeen)
|
||||
intent.putExtra(ConversationActivity.STARTING_POSITION_EXTRA, -1)
|
||||
push(intent)
|
||||
}
|
||||
|
||||
private fun openSettings() {
|
||||
val intent = Intent(this, SettingsActivity::class.java)
|
||||
show(intent)
|
||||
}
|
||||
|
||||
private fun createPrivateChat() {
|
||||
val intent = Intent(this, CreatePrivateChatActivity::class.java)
|
||||
show(intent)
|
||||
}
|
||||
|
||||
private fun joinPublicChat() {
|
||||
val intent = Intent(this, JoinPublicChatActivity::class.java)
|
||||
show(intent)
|
||||
}
|
||||
|
||||
private class SwipeCallback(val activity: HomeActivity) : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {
|
||||
|
||||
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
val threadID = (viewHolder as HomeAdapter.ViewHolder).view.thread!!.threadId
|
||||
val threadDatabase = DatabaseFactory.getThreadDatabase(activity)
|
||||
threadDatabase.archiveConversation(threadID)
|
||||
val deleteThread = object : Runnable {
|
||||
|
||||
override fun run() {
|
||||
AsyncTask.execute {
|
||||
val publicChat = DatabaseFactory.getLokiThreadDatabase(activity).getPublicChat(threadID)
|
||||
if (publicChat != null) {
|
||||
val apiDatabase = DatabaseFactory.getLokiAPIDatabase(activity)
|
||||
apiDatabase.removeLastMessageServerID(publicChat.channel, publicChat.server)
|
||||
apiDatabase.removeLastDeletionServerID(publicChat.channel, publicChat.server)
|
||||
ApplicationContext.getInstance(activity).lokiPublicChatAPI!!.leave(publicChat.channel, publicChat.server)
|
||||
}
|
||||
threadDatabase.deleteConversation(threadID)
|
||||
MessageNotifier.updateNotification(activity)
|
||||
}
|
||||
}
|
||||
}
|
||||
val handler = Handler()
|
||||
handler.postDelayed(deleteThread, 5000)
|
||||
val snackbar = Snackbar.make(activity.contentView, "Conversation Deleted", Snackbar.LENGTH_LONG)
|
||||
snackbar.setAction("Undo") {
|
||||
threadDatabase.unarchiveConversation(threadID)
|
||||
handler.removeCallbacks(deleteThread)
|
||||
animate(viewHolder, 0.0f)
|
||||
}
|
||||
snackbar.setActionTextColor(activity.resources.getColorWithID(R.color.accent, activity.theme))
|
||||
snackbar.show()
|
||||
}
|
||||
|
||||
override fun onChildDraw(c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dx: Float, dy: Float, actionState: Int, isCurrentlyActive: Boolean) {
|
||||
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE && dx < 0) {
|
||||
val itemView = viewHolder.itemView
|
||||
animate(viewHolder, dx)
|
||||
val backgroundPaint = Paint()
|
||||
backgroundPaint.color = activity.resources.getColorWithID(R.color.destructive, activity.theme)
|
||||
c.drawRect(itemView.right.toFloat() - abs(dx), itemView.top.toFloat(), itemView.right.toFloat(), itemView.bottom.toFloat(), backgroundPaint)
|
||||
val icon = BitmapFactory.decodeResource(activity.resources, R.drawable.ic_trash_filled_32)
|
||||
val iconPaint = Paint()
|
||||
val left = itemView.right.toFloat() - abs(dx) + activity.resources.getDimension(R.dimen.medium_spacing)
|
||||
val top = itemView.top.toFloat() + (itemView.bottom.toFloat() - itemView.top.toFloat() - icon.height) / 2
|
||||
c.drawBitmap(icon, left, top, iconPaint)
|
||||
} else {
|
||||
super.onChildDraw(c, recyclerView, viewHolder, dx, dy, actionState, isCurrentlyActive)
|
||||
}
|
||||
}
|
||||
|
||||
private fun animate(viewHolder: RecyclerView.ViewHolder, dx: Float) {
|
||||
val alpha = 1.0f - abs(dx) / viewHolder.itemView.width.toFloat()
|
||||
viewHolder.itemView.alpha = alpha
|
||||
viewHolder.itemView.translationX = dx
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.activities
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.ViewGroup
|
||||
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import org.thoughtcrime.securesms.loki.redesign.views.ConversationView
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
|
||||
class HomeAdapter(context: Context, cursor: Cursor) : CursorRecyclerViewAdapter<HomeAdapter.ViewHolder>(context, cursor) {
|
||||
private val threadDatabase = DatabaseFactory.getThreadDatabase(context)
|
||||
lateinit var glide: GlideRequests
|
||||
var typingThreadIDs = setOf<Long>()
|
||||
set(value) { field = value; notifyDataSetChanged() }
|
||||
var conversationClickListener: ConversationClickListener? = null
|
||||
|
||||
class ViewHolder(val view: ConversationView) : RecyclerView.ViewHolder(view)
|
||||
|
||||
override fun onCreateItemViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = ConversationView(context)
|
||||
view.setOnClickListener { conversationClickListener?.onConversationClick(view) }
|
||||
view.setOnLongClickListener {
|
||||
conversationClickListener?.onLongConversationClick(view)
|
||||
true
|
||||
}
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindItemViewHolder(viewHolder: ViewHolder, cursor: Cursor) {
|
||||
val thread = getThread(cursor)!!
|
||||
val isTyping = typingThreadIDs.contains(thread.threadId)
|
||||
viewHolder.view.bind(thread, isTyping, glide)
|
||||
}
|
||||
|
||||
private fun getThread(cursor: Cursor): ThreadRecord? {
|
||||
return threadDatabase.readerFor(cursor).current
|
||||
}
|
||||
}
|
||||
|
||||
interface ConversationClickListener {
|
||||
fun onConversationClick(view: ConversationView)
|
||||
fun onLongConversationClick(view: ConversationView)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.activities
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.util.AbstractCursorLoader
|
||||
|
||||
class HomeLoader(context: Context) : AbstractCursorLoader(context) {
|
||||
|
||||
override fun getCursor(): Cursor {
|
||||
return DatabaseFactory.getThreadDatabase(context).conversationList
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.activities
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.app.FragmentPagerAdapter
|
||||
import android.util.Patterns
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import kotlinx.android.synthetic.main.activity_join_public_chat.*
|
||||
import kotlinx.android.synthetic.main.fragment_enter_chat_url.*
|
||||
import network.loki.messenger.R
|
||||
import nl.komponents.kovenant.ui.failUi
|
||||
import nl.komponents.kovenant.ui.successUi
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
||||
import org.thoughtcrime.securesms.loki.redesign.fragments.ScanQRCodeWrapperFragment
|
||||
import org.thoughtcrime.securesms.loki.redesign.fragments.ScanQRCodeWrapperFragmentDelegate
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
|
||||
class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
||||
private val adapter = JoinPublicChatActivityAdapter(this)
|
||||
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
// Set content view
|
||||
setContentView(R.layout.activity_join_public_chat)
|
||||
// Set title
|
||||
supportActionBar!!.title = "Join Open Group"
|
||||
// Set up view pager
|
||||
viewPager.adapter = adapter
|
||||
tabLayout.setupWithViewPager(viewPager)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
private fun showLoader() {
|
||||
loader.visibility = View.VISIBLE
|
||||
loader.animate().setDuration(150).alpha(1.0f).start()
|
||||
}
|
||||
|
||||
private fun hideLoader() {
|
||||
loader.animate().setDuration(150).alpha(0.0f).setListener(object : AnimatorListenerAdapter() {
|
||||
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
super.onAnimationEnd(animation)
|
||||
loader.visibility = View.GONE
|
||||
}
|
||||
})
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Interaction
|
||||
override fun handleQRCodeScanned(url: String) {
|
||||
joinPublicChatIfPossible(url)
|
||||
}
|
||||
|
||||
fun joinPublicChatIfPossible(url: String) {
|
||||
if (!Patterns.WEB_URL.matcher(url).matches() || !url.startsWith("https://")) {
|
||||
return Toast.makeText(this, "Invalid URL", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
showLoader()
|
||||
val application = ApplicationContext.getInstance(this)
|
||||
val channel: Long = 1
|
||||
val displayName = TextSecurePreferences.getProfileName(this)
|
||||
val lokiPublicChatAPI = application.lokiPublicChatAPI!!
|
||||
application.lokiPublicChatManager.addChat(url, channel).successUi {
|
||||
lokiPublicChatAPI.getMessages(channel, url)
|
||||
lokiPublicChatAPI.setDisplayName(displayName, url)
|
||||
lokiPublicChatAPI.join(channel, url)
|
||||
val profileKey: ByteArray = ProfileKeyUtil.getProfileKey(this)
|
||||
val profileUrl: String? = TextSecurePreferences.getProfileAvatarUrl(this)
|
||||
lokiPublicChatAPI.setProfilePicture(url, profileKey, profileUrl)
|
||||
finish()
|
||||
}.failUi {
|
||||
hideLoader()
|
||||
Toast.makeText(this, "Couldn't join channel", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
// region Adapter
|
||||
private class JoinPublicChatActivityAdapter(val activity: JoinPublicChatActivity) : FragmentPagerAdapter(activity.supportFragmentManager) {
|
||||
|
||||
override fun getCount(): Int {
|
||||
return 2
|
||||
}
|
||||
|
||||
override fun getItem(index: Int): Fragment {
|
||||
return when (index) {
|
||||
0 -> EnterChatURLFragment()
|
||||
1 -> {
|
||||
val result = ScanQRCodeWrapperFragment()
|
||||
result.delegate = activity
|
||||
result.message = "Scan the QR code of the open group you'd like to join"
|
||||
result
|
||||
}
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPageTitle(index: Int): CharSequence? {
|
||||
return when (index) {
|
||||
0 -> "Open Group URL"
|
||||
1 -> "Scan QR Code"
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Enter Chat URL Fragment
|
||||
class EnterChatURLFragment : Fragment() {
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
return inflater.inflate(R.layout.fragment_enter_chat_url, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
chatURLEditText.imeOptions = chatURLEditText.imeOptions or 16777216 // Always use incognito keyboard
|
||||
joinPublicChatButton.setOnClickListener { joinPublicChatIfPossible() }
|
||||
}
|
||||
|
||||
private fun joinPublicChatIfPossible() {
|
||||
val inputMethodManager = context!!.getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputMethodManager.hideSoftInputFromWindow(chatURLEditText.windowToken, 0)
|
||||
val chatURL = chatURLEditText.text.trim().toString().toLowerCase().replace("http://", "https://")
|
||||
(activity!! as JoinPublicChatActivity).joinPublicChatIfPossible(chatURL)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
@@ -0,0 +1,132 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.AsyncTask
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import kotlinx.android.synthetic.main.activity_landing.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.database.Address
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase
|
||||
import org.thoughtcrime.securesms.logging.Log
|
||||
import org.thoughtcrime.securesms.loki.redesign.dialogs.LinkDeviceSlaveModeDialog
|
||||
import org.thoughtcrime.securesms.loki.redesign.dialogs.LinkDeviceSlaveModeDialogDelegate
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.push
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.setUpActionBarSessionLogo
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.show
|
||||
import org.thoughtcrime.securesms.loki.sendPairingAuthorisationMessage
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
import org.thoughtcrime.securesms.util.Hex
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.curve25519.Curve25519
|
||||
import org.whispersystems.libsignal.ecc.Curve
|
||||
import org.whispersystems.libsignal.ecc.ECKeyPair
|
||||
import org.whispersystems.libsignal.util.KeyHelper
|
||||
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
|
||||
import org.whispersystems.signalservice.loki.utilities.hexEncodedPublicKey
|
||||
import org.whispersystems.signalservice.loki.utilities.retryIfNeeded
|
||||
|
||||
class LandingActivity : BaseActionBarActivity(), LinkDeviceSlaveModeDialogDelegate {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_landing)
|
||||
setUpActionBarSessionLogo()
|
||||
fakeChatView.startAnimating()
|
||||
registerButton.setOnClickListener { register() }
|
||||
restoreButton.setOnClickListener { restore() }
|
||||
linkButton.setOnClickListener { linkDevice() }
|
||||
if (TextSecurePreferences.databaseResetFromUnpair(this)) {
|
||||
Toast.makeText(this, "Your device was unlinked successfully", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, intent)
|
||||
if (resultCode != RESULT_OK) { return }
|
||||
val hexEncodedPublicKey = data!!.getStringExtra("hexEncodedPublicKey")
|
||||
requestDeviceLink(hexEncodedPublicKey)
|
||||
}
|
||||
|
||||
private fun register() {
|
||||
val intent = Intent(this, RegisterActivity::class.java)
|
||||
push(intent)
|
||||
}
|
||||
|
||||
private fun restore() {
|
||||
val intent = Intent(this, RestoreActivity::class.java)
|
||||
push(intent)
|
||||
}
|
||||
|
||||
private fun linkDevice() {
|
||||
val intent = Intent(this, LinkDeviceActivity::class.java)
|
||||
show(intent, true)
|
||||
}
|
||||
|
||||
private fun requestDeviceLink(hexEncodedPublicKey: String) {
|
||||
var seed: ByteArray? = null
|
||||
var keyPair: ECKeyPair? = null
|
||||
fun generateKeyPair() {
|
||||
val seedCandidate = Curve25519.getInstance(Curve25519.BEST).generateSeed(16)
|
||||
try {
|
||||
keyPair = Curve.generateKeyPair(seedCandidate + seedCandidate) // Validate the seed
|
||||
} catch (exception: Exception) {
|
||||
return generateKeyPair()
|
||||
}
|
||||
seed = seedCandidate
|
||||
}
|
||||
generateKeyPair()
|
||||
IdentityKeyUtil.save(this, IdentityKeyUtil.lokiSeedKey, Hex.toStringCondensed(seed))
|
||||
IdentityKeyUtil.save(this, IdentityKeyUtil.IDENTITY_PUBLIC_KEY_PREF, Base64.encodeBytes(keyPair!!.publicKey.serialize()))
|
||||
IdentityKeyUtil.save(this, IdentityKeyUtil.IDENTITY_PRIVATE_KEY_PREF, Base64.encodeBytes(keyPair!!.privateKey.serialize()))
|
||||
val userHexEncodedPublicKey = keyPair!!.hexEncodedPublicKey
|
||||
val registrationID = KeyHelper.generateRegistrationId(false)
|
||||
TextSecurePreferences.setLocalRegistrationId(this, registrationID)
|
||||
DatabaseFactory.getIdentityDatabase(this).saveIdentity(Address.fromSerialized(userHexEncodedPublicKey),
|
||||
IdentityKeyUtil.getIdentityKeyPair(this).publicKey, IdentityDatabase.VerifiedStatus.VERIFIED,
|
||||
true, System.currentTimeMillis(), true)
|
||||
TextSecurePreferences.setLocalNumber(this, userHexEncodedPublicKey)
|
||||
TextSecurePreferences.setHasSeenWelcomeScreen(this, true)
|
||||
TextSecurePreferences.setPromptedPushRegistration(this, true)
|
||||
val authorisation = PairingAuthorisation(hexEncodedPublicKey, userHexEncodedPublicKey).sign(PairingAuthorisation.Type.REQUEST, keyPair!!.privateKey.serialize())
|
||||
if (authorisation == null) {
|
||||
Log.d("Loki", "Failed to sign device link request.")
|
||||
reset()
|
||||
return Toast.makeText(application, "Couldn't link device.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
val application = ApplicationContext.getInstance(this)
|
||||
application.startLongPollingIfNeeded()
|
||||
application.setUpP2PAPI()
|
||||
application.setUpStorageAPIIfNeeded()
|
||||
val linkDeviceDialog = LinkDeviceSlaveModeDialog()
|
||||
linkDeviceDialog.delegate = this
|
||||
linkDeviceDialog.show(supportFragmentManager, "Link Device Dialog")
|
||||
AsyncTask.execute {
|
||||
retryIfNeeded(8) {
|
||||
sendPairingAuthorisationMessage(this@LandingActivity, authorisation.primaryDevicePublicKey, authorisation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDeviceLinkRequestAuthorized(authorization: PairingAuthorisation) {
|
||||
TextSecurePreferences.setMasterHexEncodedPublicKey(this, authorization.primaryDevicePublicKey)
|
||||
val intent = Intent(this, HomeActivity::class.java)
|
||||
show(intent)
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onDeviceLinkCanceled() {
|
||||
reset()
|
||||
}
|
||||
|
||||
private fun reset() {
|
||||
IdentityKeyUtil.delete(this, IdentityKeyUtil.lokiSeedKey)
|
||||
TextSecurePreferences.removeLocalNumber(this)
|
||||
TextSecurePreferences.setHasSeenWelcomeScreen(this, false)
|
||||
TextSecurePreferences.setPromptedPushRegistration(this, false)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.app.FragmentPagerAdapter
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import kotlinx.android.synthetic.main.activity_link_device.*
|
||||
import kotlinx.android.synthetic.main.fragment_enter_session_id.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.loki.redesign.fragments.ScanQRCodeWrapperFragment
|
||||
import org.thoughtcrime.securesms.loki.redesign.fragments.ScanQRCodeWrapperFragmentDelegate
|
||||
import org.whispersystems.signalservice.loki.utilities.PublicKeyValidation
|
||||
|
||||
class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
||||
private val adapter = LinkDeviceActivityAdapter(this)
|
||||
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
// Set content view
|
||||
setContentView(R.layout.activity_link_device)
|
||||
// Set title
|
||||
supportActionBar!!.title = "Link Device"
|
||||
// Set up view pager
|
||||
viewPager.adapter = adapter
|
||||
tabLayout.setupWithViewPager(viewPager)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Interaction
|
||||
override fun handleQRCodeScanned(hexEncodedPublicKey: String) {
|
||||
requestDeviceLinkIfPossible(hexEncodedPublicKey)
|
||||
}
|
||||
|
||||
fun requestDeviceLinkIfPossible(hexEncodedPublicKey: String) {
|
||||
if (!PublicKeyValidation.isValid(hexEncodedPublicKey)) {
|
||||
Toast.makeText(this, "Invalid Session ID", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
val intent = Intent()
|
||||
intent.putExtra("hexEncodedPublicKey", hexEncodedPublicKey)
|
||||
setResult(RESULT_OK, intent)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
// region Adapter
|
||||
private class LinkDeviceActivityAdapter(val activity: LinkDeviceActivity) : FragmentPagerAdapter(activity.supportFragmentManager) {
|
||||
|
||||
override fun getCount(): Int {
|
||||
return 2
|
||||
}
|
||||
|
||||
override fun getItem(index: Int): Fragment {
|
||||
return when (index) {
|
||||
0 -> EnterSessionIDFragment()
|
||||
1 -> {
|
||||
val result = ScanQRCodeWrapperFragment()
|
||||
result.delegate = activity
|
||||
result.message = "Link to your existing account by going into your in-app settings and clicking \"Linked Devices\""
|
||||
result
|
||||
}
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPageTitle(index: Int): CharSequence? {
|
||||
return when (index) {
|
||||
0 -> "Enter Session ID"
|
||||
1 -> "Scan QR Code"
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Enter Session ID Fragment
|
||||
class EnterSessionIDFragment : Fragment() {
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
return inflater.inflate(R.layout.fragment_enter_session_id, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
sessionIDEditText.imeOptions = sessionIDEditText.imeOptions or 16777216 // Always use incognito keyboard
|
||||
requestDeviceLinkButton.setOnClickListener { requestDeviceLinkIfPossible() }
|
||||
}
|
||||
|
||||
private fun requestDeviceLinkIfPossible() {
|
||||
val inputMethodManager = context!!.getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputMethodManager.hideSoftInputFromWindow(sessionIDEditText.windowToken, 0)
|
||||
val hexEncodedPublicKey = sessionIDEditText.text.trim().toString().toLowerCase()
|
||||
(activity!! as LinkDeviceActivity).requestDeviceLinkIfPossible(hexEncodedPublicKey)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
@@ -0,0 +1,146 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.activities
|
||||
|
||||
import android.os.AsyncTask
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.LoaderManager
|
||||
import android.support.v4.content.Loader
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import kotlinx.android.synthetic.main.activity_linked_devices.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.devicelist.Device
|
||||
import org.thoughtcrime.securesms.loki.DeviceListBottomSheetFragment
|
||||
import org.thoughtcrime.securesms.loki.redesign.dialogs.EditDeviceNameDialog
|
||||
import org.thoughtcrime.securesms.loki.redesign.dialogs.EditDeviceNameDialogDelegate
|
||||
import org.thoughtcrime.securesms.loki.redesign.dialogs.LinkDeviceMasterModeDialog
|
||||
import org.thoughtcrime.securesms.loki.redesign.dialogs.LinkDeviceMasterModeDialogDelegate
|
||||
import org.thoughtcrime.securesms.loki.signAndSendPairingAuthorisationMessage
|
||||
import org.thoughtcrime.securesms.sms.MessageSender
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.loki.api.LokiStorageAPI
|
||||
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
|
||||
|
||||
class LinkedDevicesActivity : PassphraseRequiredActionBarActivity, LoaderManager.LoaderCallbacks<List<Device>>, DeviceClickListener, EditDeviceNameDialogDelegate, LinkDeviceMasterModeDialogDelegate {
|
||||
private var devices = listOf<Device>()
|
||||
set(value) { field = value; linkedDevicesAdapter.devices = value }
|
||||
|
||||
private val linkedDevicesAdapter by lazy {
|
||||
val result = LinkedDevicesAdapter(this)
|
||||
result.deviceClickListener = this
|
||||
result
|
||||
}
|
||||
|
||||
// region Lifecycle
|
||||
constructor() : super()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
setContentView(R.layout.activity_linked_devices)
|
||||
supportActionBar!!.title = "Devices"
|
||||
recyclerView.adapter = linkedDevicesAdapter
|
||||
recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
linkDeviceButton.setOnClickListener { linkDevice() }
|
||||
LoaderManager.getInstance(this).initLoader(0, null, this)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_linked_devices, menu)
|
||||
return true
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
override fun onCreateLoader(id: Int, bundle: Bundle?): Loader<List<Device>> {
|
||||
return LinkedDevicesLoader(this)
|
||||
}
|
||||
|
||||
override fun onLoadFinished(loader: Loader<List<Device>>, devices: List<Device>?) {
|
||||
update(devices ?: listOf())
|
||||
}
|
||||
|
||||
override fun onLoaderReset(loader: Loader<List<Device>>) {
|
||||
update(listOf())
|
||||
}
|
||||
|
||||
private fun update(devices: List<Device>) {
|
||||
this.devices = devices
|
||||
emptyStateContainer.visibility = if (devices.isEmpty()) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
override fun handleDeviceNameChanged(device: Device) {
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, this)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Interaction
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
val id = item.itemId
|
||||
when(id) {
|
||||
R.id.linkDeviceButton -> linkDevice()
|
||||
else -> { /* Do nothing */ }
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun linkDevice() {
|
||||
if (devices.isEmpty()) {
|
||||
val linkDeviceDialog = LinkDeviceMasterModeDialog()
|
||||
linkDeviceDialog.delegate = this
|
||||
linkDeviceDialog.show(supportFragmentManager, "Link Device Dialog")
|
||||
} else {
|
||||
val builder = AlertDialog.Builder(this)
|
||||
builder.setTitle("Multi Device Limit Reached")
|
||||
builder.setMessage("It's currently not allowed to link more than one device.")
|
||||
builder.setPositiveButton("OK", { dialog, _ -> dialog.dismiss() })
|
||||
builder.create().show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDeviceClick(device: Device) {
|
||||
val bottomSheet = DeviceListBottomSheetFragment()
|
||||
bottomSheet.onEditTapped = {
|
||||
bottomSheet.dismiss()
|
||||
val editDeviceNameDialog = EditDeviceNameDialog()
|
||||
editDeviceNameDialog.device = device
|
||||
editDeviceNameDialog.delegate = this
|
||||
editDeviceNameDialog.show(supportFragmentManager, "Edit Device Name Dialog")
|
||||
}
|
||||
bottomSheet.onUnlinkTapped = {
|
||||
bottomSheet.dismiss()
|
||||
unlinkDevice(device.id)
|
||||
}
|
||||
bottomSheet.show(supportFragmentManager, bottomSheet.tag)
|
||||
}
|
||||
|
||||
private fun unlinkDevice(slaveDeviceHexEncodedPublicKey: String) {
|
||||
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this)
|
||||
val database = DatabaseFactory.getLokiAPIDatabase(this)
|
||||
database.removePairingAuthorisation(userHexEncodedPublicKey, slaveDeviceHexEncodedPublicKey)
|
||||
LokiStorageAPI.shared.updateUserDeviceMappings().success {
|
||||
MessageSender.sendUnpairRequest(this, slaveDeviceHexEncodedPublicKey)
|
||||
}
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, this)
|
||||
Toast.makeText(this, "Your device was unlinked successfully", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
override fun onDeviceLinkRequestAuthorized(authorization: PairingAuthorisation) {
|
||||
AsyncTask.execute {
|
||||
signAndSendPairingAuthorisationMessage(this, authorization)
|
||||
Util.runOnMain {
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDeviceLinkCanceled() {
|
||||
// Do nothing
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.activities
|
||||
|
||||
import android.content.Context
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.ViewGroup
|
||||
import org.thoughtcrime.securesms.devicelist.Device
|
||||
import org.thoughtcrime.securesms.loki.redesign.views.DeviceView
|
||||
|
||||
class LinkedDevicesAdapter(private val context: Context) : RecyclerView.Adapter<LinkedDevicesAdapter.ViewHolder>() {
|
||||
var devices = listOf<Device>()
|
||||
set(value) { field = value; notifyDataSetChanged() }
|
||||
var deviceClickListener: DeviceClickListener? = null
|
||||
|
||||
class ViewHolder(val view: DeviceView) : RecyclerView.ViewHolder(view)
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return devices.size
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = DeviceView(context)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
|
||||
val device = devices[position]
|
||||
viewHolder.view.setOnClickListener { deviceClickListener?.onDeviceClick(device) }
|
||||
viewHolder.view.bind(device)
|
||||
}
|
||||
}
|
||||
|
||||
interface DeviceClickListener {
|
||||
|
||||
fun onDeviceClick(device: Device)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.activities
|
||||
|
||||
import android.content.Context
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.devicelist.Device
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.MnemonicUtilities
|
||||
import org.thoughtcrime.securesms.util.AsyncLoader
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.loki.api.LokiStorageAPI
|
||||
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec
|
||||
import java.io.File
|
||||
|
||||
class LinkedDevicesLoader(context: Context) : AsyncLoader<List<Device>>(context) {
|
||||
|
||||
private val mnemonicCodec by lazy {
|
||||
val languageFileDirectory = File(context.applicationInfo.dataDir)
|
||||
MnemonicCodec(languageFileDirectory)
|
||||
}
|
||||
|
||||
override fun loadInBackground(): List<Device>? {
|
||||
try {
|
||||
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val slaveDeviceHexEncodedPublicKeys = LokiStorageAPI.shared.getSecondaryDevicePublicKeys(userHexEncodedPublicKey).get()
|
||||
return slaveDeviceHexEncodedPublicKeys.map { hexEncodedPublicKey ->
|
||||
val shortID = MnemonicUtilities.getFirst3Words(mnemonicCodec, hexEncodedPublicKey)
|
||||
val name = DatabaseFactory.getLokiUserDatabase(context).getDisplayName(hexEncodedPublicKey)
|
||||
Device(hexEncodedPublicKey, shortID, name)
|
||||
}.sortedBy { it.name }
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.activities
|
||||
|
||||
import android.os.Bundle
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.preferences.NotificationsPreferenceFragment
|
||||
|
||||
class NotificationSettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
setContentView(R.layout.activity_fragment_wrapper)
|
||||
supportActionBar!!.title = "Notifications"
|
||||
val fragment = NotificationsPreferenceFragment()
|
||||
val transaction = supportFragmentManager.beginTransaction()
|
||||
transaction.replace(R.id.fragmentContainer, fragment)
|
||||
transaction.commit()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.activities
|
||||
|
||||
import android.os.Bundle
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment
|
||||
|
||||
class PrivacySettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
setContentView(R.layout.activity_fragment_wrapper)
|
||||
supportActionBar!!.title = "Privacy"
|
||||
val fragment = AppProtectionPreferenceFragment()
|
||||
val transaction = supportFragmentManager.beginTransaction()
|
||||
transaction.replace(R.id.fragmentContainer, fragment)
|
||||
transaction.commit()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.activities
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.app.FragmentPagerAdapter
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import com.tbruyelle.rxpermissions2.RxPermissions
|
||||
import kotlinx.android.synthetic.main.activity_qr_code.*
|
||||
import kotlinx.android.synthetic.main.fragment_view_my_qr_code.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity
|
||||
import org.thoughtcrime.securesms.database.Address
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.loki.redesign.fragments.ScanQRCodeWrapperFragment
|
||||
import org.thoughtcrime.securesms.loki.redesign.fragments.ScanQRCodeWrapperFragmentDelegate
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.QRCodeUtilities
|
||||
import org.thoughtcrime.securesms.loki.toPx
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.FileProviderUtil
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.loki.utilities.PublicKeyValidation
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
class QRCodeActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
||||
private val adapter = QRCodeActivityAdapter(this)
|
||||
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
// Set content view
|
||||
setContentView(R.layout.activity_qr_code)
|
||||
// Set title
|
||||
supportActionBar!!.title = "QR Code"
|
||||
// Set up view pager
|
||||
viewPager.adapter = adapter
|
||||
tabLayout.setupWithViewPager(viewPager)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Interaction
|
||||
override fun handleQRCodeScanned(hexEncodedPublicKey: String) {
|
||||
createPrivateChatIfPossible(hexEncodedPublicKey)
|
||||
}
|
||||
|
||||
fun createPrivateChatIfPossible(hexEncodedPublicKey: String) {
|
||||
if (!PublicKeyValidation.isValid(hexEncodedPublicKey)) { return Toast.makeText(this, "Invalid Session ID", Toast.LENGTH_SHORT).show() }
|
||||
val masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(this)
|
||||
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this)
|
||||
val targetHexEncodedPublicKey = if (hexEncodedPublicKey == masterHexEncodedPublicKey) userHexEncodedPublicKey else hexEncodedPublicKey
|
||||
val recipient = Recipient.from(this, Address.fromSerialized(targetHexEncodedPublicKey), true)
|
||||
val intent = Intent(this, ConversationActivity::class.java)
|
||||
intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.address)
|
||||
intent.putExtra(ConversationActivity.TEXT_EXTRA, getIntent().getStringExtra(ConversationActivity.TEXT_EXTRA))
|
||||
intent.setDataAndType(getIntent().data, getIntent().type)
|
||||
val existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient)
|
||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, existingThread)
|
||||
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
// region Adapter
|
||||
private class QRCodeActivityAdapter(val activity: QRCodeActivity) : FragmentPagerAdapter(activity.supportFragmentManager) {
|
||||
|
||||
override fun getCount(): Int {
|
||||
return 2
|
||||
}
|
||||
|
||||
override fun getItem(index: Int): Fragment {
|
||||
return when (index) {
|
||||
0 -> ViewMyQRCodeFragment()
|
||||
1 -> {
|
||||
val result = ScanQRCodeWrapperFragment()
|
||||
result.delegate = activity
|
||||
result.message = "Scan someone\'s QR code to start a conversation with them"
|
||||
result
|
||||
}
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPageTitle(index: Int): CharSequence? {
|
||||
return when (index) {
|
||||
0 -> "View My QR Code"
|
||||
1 -> "Scan QR Code"
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region View My QR Code Fragment
|
||||
class ViewMyQRCodeFragment : Fragment() {
|
||||
|
||||
private val hexEncodedPublicKey: String
|
||||
get() {
|
||||
val masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(context!!)
|
||||
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context!!)
|
||||
return masterHexEncodedPublicKey ?: userHexEncodedPublicKey
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
return inflater.inflate(R.layout.fragment_view_my_qr_code, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val size = toPx(280, resources)
|
||||
val qrCode = QRCodeUtilities.encode(hexEncodedPublicKey, size, false, false)
|
||||
qrCodeImageView.setImageBitmap(qrCode)
|
||||
// val explanation = SpannableStringBuilder("This is your unique public QR code. Other users can scan this to start a conversation with you.")
|
||||
// explanation.setSpan(StyleSpan(Typeface.BOLD), 8, 34, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
explanationTextView.text = "This is your QR code. Other users can scan it to start a session with you."
|
||||
shareButton.setOnClickListener { shareQRCode() }
|
||||
}
|
||||
|
||||
private fun shareQRCode() {
|
||||
fun proceed() {
|
||||
val directory = File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_PICTURES)
|
||||
val fileName = "$hexEncodedPublicKey.png"
|
||||
val file = File(directory, fileName)
|
||||
file.createNewFile()
|
||||
val fos = FileOutputStream(file)
|
||||
val size = toPx(280, resources)
|
||||
val qrCode = QRCodeUtilities.encode(hexEncodedPublicKey, size, false, false)
|
||||
qrCode.compress(Bitmap.CompressFormat.PNG, 100, fos)
|
||||
fos.flush()
|
||||
fos.close()
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.putExtra(Intent.EXTRA_STREAM, FileProviderUtil.getUriFor(activity!!, file))
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
intent.type = "image/png"
|
||||
startActivity(Intent.createChooser(intent, "Share QR Code"))
|
||||
}
|
||||
if (RxPermissions(this).isGranted(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||
proceed()
|
||||
} else {
|
||||
@SuppressWarnings("unused")
|
||||
val unused = RxPermissions(this).request(Manifest.permission.WRITE_EXTERNAL_STORAGE).subscribe { isGranted ->
|
||||
if (isGranted) {
|
||||
proceed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
@@ -0,0 +1,154 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.activities
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Typeface
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.StyleSpan
|
||||
import android.widget.Toast
|
||||
import kotlinx.android.synthetic.main.activity_register.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.database.Address
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.push
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.setUpActionBarSessionLogo
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
import org.thoughtcrime.securesms.util.Hex
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.curve25519.Curve25519
|
||||
import org.whispersystems.libsignal.ecc.Curve
|
||||
import org.whispersystems.libsignal.ecc.ECKeyPair
|
||||
import org.whispersystems.libsignal.util.KeyHelper
|
||||
import org.whispersystems.signalservice.loki.utilities.hexEncodedPublicKey
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
class RegisterActivity : BaseActionBarActivity() {
|
||||
private var seed: ByteArray? = null
|
||||
private var keyPair: ECKeyPair? = null
|
||||
set(value) { field = value; updatePublicKeyTextView() }
|
||||
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_register)
|
||||
setUpLanguageFileDirectory()
|
||||
setUpActionBarSessionLogo()
|
||||
registerButton.setOnClickListener { register() }
|
||||
copyButton.setOnClickListener { copyPublicKey() }
|
||||
val termsExplanation = SpannableStringBuilder("By using this service, you agree to our Terms and Conditions and Privacy Statement")
|
||||
termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 40, 60, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 65, 82, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
termsButton.text = termsExplanation
|
||||
termsButton.setOnClickListener { showTerms() }
|
||||
updateKeyPair()
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region General
|
||||
private fun setUpLanguageFileDirectory() {
|
||||
val languages = listOf( "english", "japanese", "portuguese", "spanish" )
|
||||
val directory = File(applicationInfo.dataDir)
|
||||
for (language in languages) {
|
||||
val fileName = "$language.txt"
|
||||
if (directory.list().contains(fileName)) { continue }
|
||||
val inputStream = 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()
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
private fun updateKeyPair() {
|
||||
val seedCandidate = Curve25519.getInstance(Curve25519.BEST).generateSeed(16)
|
||||
try {
|
||||
this.keyPair = Curve.generateKeyPair(seedCandidate + seedCandidate) // Validate the seed
|
||||
} catch (exception: Exception) {
|
||||
return updateKeyPair()
|
||||
}
|
||||
seed = seedCandidate
|
||||
}
|
||||
|
||||
private fun updatePublicKeyTextView() {
|
||||
val hexEncodedPublicKey = keyPair!!.hexEncodedPublicKey
|
||||
val characterCount = hexEncodedPublicKey.count()
|
||||
var count = 0
|
||||
val limit = 32
|
||||
fun animate() {
|
||||
val numberOfIndexesToShuffle = 32 - count
|
||||
val indexesToShuffle = (0 until characterCount).shuffled().subList(0, numberOfIndexesToShuffle)
|
||||
var mangledHexEncodedPublicKey = hexEncodedPublicKey
|
||||
for (index in indexesToShuffle) {
|
||||
try {
|
||||
mangledHexEncodedPublicKey = mangledHexEncodedPublicKey.substring(0, index) + "0123456789abcdef__".random() + mangledHexEncodedPublicKey.substring(index + 1, mangledHexEncodedPublicKey.count())
|
||||
} catch (exception: Exception) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
count += 1
|
||||
if (count < limit) {
|
||||
publicKeyTextView.text = mangledHexEncodedPublicKey
|
||||
Handler().postDelayed({
|
||||
animate()
|
||||
}, 32)
|
||||
} else {
|
||||
publicKeyTextView.text = hexEncodedPublicKey
|
||||
}
|
||||
}
|
||||
animate()
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Interaction
|
||||
private fun register() {
|
||||
IdentityKeyUtil.save(this, IdentityKeyUtil.lokiSeedKey, Hex.toStringCondensed(seed))
|
||||
IdentityKeyUtil.save(this, IdentityKeyUtil.IDENTITY_PUBLIC_KEY_PREF, Base64.encodeBytes(keyPair!!.publicKey.serialize()))
|
||||
IdentityKeyUtil.save(this, IdentityKeyUtil.IDENTITY_PRIVATE_KEY_PREF, Base64.encodeBytes(keyPair!!.privateKey.serialize()))
|
||||
val userHexEncodedPublicKey = keyPair!!.hexEncodedPublicKey
|
||||
val registrationID = KeyHelper.generateRegistrationId(false)
|
||||
TextSecurePreferences.setLocalRegistrationId(this, registrationID)
|
||||
DatabaseFactory.getIdentityDatabase(this).saveIdentity(Address.fromSerialized(userHexEncodedPublicKey),
|
||||
IdentityKeyUtil.getIdentityKeyPair(this).publicKey, IdentityDatabase.VerifiedStatus.VERIFIED,
|
||||
true, System.currentTimeMillis(), true)
|
||||
TextSecurePreferences.setLocalNumber(this, userHexEncodedPublicKey)
|
||||
TextSecurePreferences.setRestorationTime(this, 0)
|
||||
TextSecurePreferences.setHasViewedSeed(this, false)
|
||||
val intent = Intent(this, DisplayNameActivity::class.java)
|
||||
push(intent)
|
||||
}
|
||||
|
||||
private fun copyPublicKey() {
|
||||
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("Session ID", keyPair!!.hexEncodedPublicKey)
|
||||
clipboard.primaryClip = clip
|
||||
Toast.makeText(this, R.string.activity_register_public_key_copied_message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun showTerms() {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/loki-project/loki-messenger-android/blob/master/privacy-policy.md"))
|
||||
startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(this, "Couldn't open link", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Typeface
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.StyleSpan
|
||||
import android.widget.Toast
|
||||
import kotlinx.android.synthetic.main.activity_restore.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.database.Address
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.push
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.setUpActionBarSessionLogo
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
import org.thoughtcrime.securesms.util.Hex
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.libsignal.ecc.Curve
|
||||
import org.whispersystems.libsignal.util.KeyHelper
|
||||
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec
|
||||
import org.whispersystems.signalservice.loki.utilities.hexEncodedPublicKey
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
class RestoreActivity : BaseActionBarActivity() {
|
||||
private lateinit var languageFileDirectory: File
|
||||
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setUpLanguageFileDirectory()
|
||||
setUpActionBarSessionLogo()
|
||||
setContentView(R.layout.activity_restore)
|
||||
mnemonicEditText.imeOptions = mnemonicEditText.imeOptions or 16777216 // Always use incognito keyboard
|
||||
restoreButton.setOnClickListener { restore() }
|
||||
val termsExplanation = SpannableStringBuilder("By using this service, you agree to our Terms and Conditions and Privacy Statement")
|
||||
termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 40, 60, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 65, 82, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
termsButton.text = termsExplanation
|
||||
termsButton.setOnClickListener { showTerms() }
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region General
|
||||
private fun setUpLanguageFileDirectory() {
|
||||
val languages = listOf( "english", "japanese", "portuguese", "spanish" )
|
||||
val directory = File(applicationInfo.dataDir)
|
||||
for (language in languages) {
|
||||
val fileName = "$language.txt"
|
||||
if (directory.list().contains(fileName)) { continue }
|
||||
val inputStream = 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
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Interaction
|
||||
private fun restore() {
|
||||
val mnemonic = mnemonicEditText.text.toString()
|
||||
try {
|
||||
val hexEncodedSeed = MnemonicCodec(languageFileDirectory).decode(mnemonic)
|
||||
var seed = Hex.fromStringCondensed(hexEncodedSeed)
|
||||
IdentityKeyUtil.save(this, IdentityKeyUtil.lokiSeedKey, Hex.toStringCondensed(seed))
|
||||
if (seed.size == 16) { seed = seed + seed }
|
||||
val keyPair = Curve.generateKeyPair(seed)
|
||||
IdentityKeyUtil.save(this, IdentityKeyUtil.IDENTITY_PUBLIC_KEY_PREF, Base64.encodeBytes(keyPair.publicKey.serialize()))
|
||||
IdentityKeyUtil.save(this, IdentityKeyUtil.IDENTITY_PRIVATE_KEY_PREF, Base64.encodeBytes(keyPair.privateKey.serialize()))
|
||||
val userHexEncodedPublicKey = keyPair.hexEncodedPublicKey
|
||||
val registrationID = KeyHelper.generateRegistrationId(false)
|
||||
TextSecurePreferences.setLocalRegistrationId(this, registrationID)
|
||||
DatabaseFactory.getIdentityDatabase(this).saveIdentity(Address.fromSerialized(userHexEncodedPublicKey),
|
||||
IdentityKeyUtil.getIdentityKeyPair(this).publicKey, IdentityDatabase.VerifiedStatus.VERIFIED,
|
||||
true, System.currentTimeMillis(), true)
|
||||
TextSecurePreferences.setLocalNumber(this, userHexEncodedPublicKey)
|
||||
TextSecurePreferences.setRestorationTime(this, System.currentTimeMillis())
|
||||
TextSecurePreferences.setHasViewedSeed(this, true)
|
||||
val intent = Intent(this, DisplayNameActivity::class.java)
|
||||
push(intent)
|
||||
} catch (e: Exception) {
|
||||
val message = if (e is MnemonicCodec.DecodingError) e.description else MnemonicCodec.DecodingError.Generic.description
|
||||
return Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showTerms() {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/loki-project/loki-messenger-android/blob/master/privacy-policy.md"))
|
||||
startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(this, "Couldn't open link", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.activities
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Toast
|
||||
import kotlinx.android.synthetic.main.activity_seed_v2.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.loki.getColorWithID
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec
|
||||
import org.whispersystems.signalservice.loki.utilities.hexEncodedPrivateKey
|
||||
import java.io.File
|
||||
|
||||
class SeedActivity : BaseActionBarActivity() {
|
||||
|
||||
private val seed by lazy {
|
||||
val languageFileDirectory = File(applicationInfo.dataDir)
|
||||
var hexEncodedSeed = IdentityKeyUtil.retrieve(this, IdentityKeyUtil.lokiSeedKey)
|
||||
if (hexEncodedSeed == null) {
|
||||
hexEncodedSeed = IdentityKeyUtil.getIdentityKeyPair(this).hexEncodedPrivateKey // Legacy account
|
||||
}
|
||||
MnemonicCodec(languageFileDirectory).encode(hexEncodedSeed!!, MnemonicCodec.Language.Configuration.english)
|
||||
}
|
||||
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_seed_v2)
|
||||
supportActionBar!!.title = "Your Recovery Phrase"
|
||||
val seedReminderViewTitle = SpannableString("You're almost finished! 90%")
|
||||
seedReminderViewTitle.setSpan(ForegroundColorSpan(resources.getColorWithID(R.color.accent, theme)), 24, 27, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
seedReminderView.title = seedReminderViewTitle
|
||||
seedReminderView.subtitle = "Tap and hold the redacted words to reveal your recovery phrase, then store it safely to secure your Session ID."
|
||||
seedReminderView.setProgress(90, false)
|
||||
seedReminderView.hideContinueButton()
|
||||
var redactedSeed = seed
|
||||
var index = 0
|
||||
for (character in seed) {
|
||||
if (character.isLetter()) {
|
||||
redactedSeed = redactedSeed.replaceRange(index, index + 1, "▆")
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
seedTextView.setTextColor(resources.getColorWithID(R.color.accent, theme))
|
||||
seedTextView.text = redactedSeed
|
||||
seedTextView.setOnLongClickListener { revealSeed(); true }
|
||||
revealButton.setOnLongClickListener { revealSeed(); true }
|
||||
copyButton.setOnClickListener { copySeed() }
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
private fun revealSeed() {
|
||||
val seedReminderViewTitle = SpannableString("Account secured! 100%")
|
||||
seedReminderViewTitle.setSpan(ForegroundColorSpan(resources.getColorWithID(R.color.accent, theme)), 17, 21, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
seedReminderView.title = seedReminderViewTitle
|
||||
seedReminderView.subtitle = "Make sure to store your recovery phrase in a safe place"
|
||||
seedReminderView.setProgress(100, true)
|
||||
val seedTextViewLayoutParams = seedTextView.layoutParams as LinearLayout.LayoutParams
|
||||
seedTextViewLayoutParams.height = seedTextView.height
|
||||
seedTextView.layoutParams = seedTextViewLayoutParams
|
||||
seedTextView.setTextColor(resources.getColorWithID(R.color.text, theme))
|
||||
seedTextView.text = seed
|
||||
TextSecurePreferences.setHasViewedSeed(this, true)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Interaction
|
||||
private fun copySeed() {
|
||||
revealSeed()
|
||||
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("Seed", seed)
|
||||
clipboard.primaryClip = clip
|
||||
Toast.makeText(this, R.string.activity_register_public_key_copied_message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.activities
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.app.Activity
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.AsyncTask
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Toast
|
||||
import kotlinx.android.synthetic.main.activity_settings.*
|
||||
import network.loki.messenger.R
|
||||
import nl.komponents.kovenant.Promise
|
||||
import nl.komponents.kovenant.all
|
||||
import nl.komponents.kovenant.deferred
|
||||
import nl.komponents.kovenant.ui.alwaysUi
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.avatar.AvatarSelection
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
||||
import org.thoughtcrime.securesms.database.Address
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.loki.redesign.dialogs.ClearAllDataDialog
|
||||
import org.thoughtcrime.securesms.loki.redesign.dialogs.SeedDialog
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.push
|
||||
import org.thoughtcrime.securesms.loki.toPx
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper
|
||||
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints
|
||||
import org.thoughtcrime.securesms.util.BitmapDecodingException
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.api.crypto.ProfileCipher
|
||||
import org.whispersystems.signalservice.api.util.StreamDetails
|
||||
import org.whispersystems.signalservice.loki.api.LokiStorageAPI
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.File
|
||||
import java.security.SecureRandom
|
||||
|
||||
class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
||||
private lateinit var glide: GlideRequests
|
||||
private var isEditingDisplayName = false
|
||||
set(value) { field = value; handleIsEditingDisplayNameChanged() }
|
||||
private var displayNameToBeUploaded: String? = null
|
||||
private var profilePictureToBeUploaded: ByteArray? = null
|
||||
private var tempFile: File? = null
|
||||
|
||||
private val hexEncodedPublicKey: String
|
||||
get() {
|
||||
val masterHexEncodedPublicKey = TextSecurePreferences.getMasterHexEncodedPublicKey(this)
|
||||
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this)
|
||||
return masterHexEncodedPublicKey ?: userHexEncodedPublicKey
|
||||
}
|
||||
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
setContentView(R.layout.activity_settings)
|
||||
setSupportActionBar(toolbar)
|
||||
cancelButton.setOnClickListener { cancelEditingDisplayName() }
|
||||
saveButton.setOnClickListener { saveDisplayName() }
|
||||
showQRCodeButton.setOnClickListener { showQRCode() }
|
||||
glide = GlideApp.with(this)
|
||||
profilePictureView.glide = glide
|
||||
profilePictureView.hexEncodedPublicKey = hexEncodedPublicKey
|
||||
profilePictureView.isLarge = true
|
||||
profilePictureView.update()
|
||||
profilePictureView.setOnClickListener { showEditProfilePictureUI() }
|
||||
displayNameContainer.setOnClickListener { showEditDisplayNameUI() }
|
||||
displayNameTextView.text = DatabaseFactory.getLokiUserDatabase(this).getDisplayName(hexEncodedPublicKey)
|
||||
publicKeyTextView.text = hexEncodedPublicKey
|
||||
copyButton.setOnClickListener { copyPublicKey() }
|
||||
shareButton.setOnClickListener { sharePublicKey() }
|
||||
val isMasterDevice = (TextSecurePreferences.getMasterHexEncodedPublicKey(this) == null)
|
||||
if (!isMasterDevice) {
|
||||
linkedDevicesButtonTopSeparator.visibility = View.GONE
|
||||
linkedDevicesButton.visibility = View.GONE
|
||||
seedButtonTopSeparator.visibility = View.GONE
|
||||
seedButton.visibility = View.GONE
|
||||
}
|
||||
privacyButton.setOnClickListener { showPrivacySettings() }
|
||||
notificationsButton.setOnClickListener { showNotificationSettings() }
|
||||
chatsButton.setOnClickListener { showChatSettings() }
|
||||
linkedDevicesButton.setOnClickListener { showLinkedDevices() }
|
||||
seedButton.setOnClickListener { showSeed() }
|
||||
clearAllDataButton.setOnClickListener { clearAllData() }
|
||||
}
|
||||
|
||||
public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
when (requestCode) {
|
||||
AvatarSelection.REQUEST_CODE_AVATAR -> {
|
||||
if (resultCode != Activity.RESULT_OK) { return }
|
||||
val outputFile = Uri.fromFile(File(cacheDir, "cropped"))
|
||||
var inputFile: Uri? = data?.data
|
||||
if (inputFile == null && tempFile != null) {
|
||||
inputFile = Uri.fromFile(tempFile)
|
||||
}
|
||||
AvatarSelection.circularCropImage(this, inputFile, outputFile, R.string.CropImageActivity_profile_avatar)
|
||||
}
|
||||
AvatarSelection.REQUEST_CODE_CROP_IMAGE -> {
|
||||
if (resultCode != Activity.RESULT_OK) { return }
|
||||
AsyncTask.execute {
|
||||
try {
|
||||
profilePictureToBeUploaded = BitmapUtil.createScaledBytes(this@SettingsActivity, AvatarSelection.getResultUri(data), ProfileMediaConstraints()).bitmap
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
updateProfile(true)
|
||||
}
|
||||
} catch (e: BitmapDecodingException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
private fun handleIsEditingDisplayNameChanged() {
|
||||
cancelButton.visibility = if (isEditingDisplayName) View.VISIBLE else View.GONE
|
||||
showQRCodeButton.visibility = if (isEditingDisplayName) View.GONE else View.VISIBLE
|
||||
saveButton.visibility = if (isEditingDisplayName) View.VISIBLE else View.GONE
|
||||
displayNameTextView.visibility = if (isEditingDisplayName) View.INVISIBLE else View.VISIBLE
|
||||
displayNameEditText.visibility = if (isEditingDisplayName) View.VISIBLE else View.INVISIBLE
|
||||
val titleTextViewLayoutParams = titleTextView.layoutParams as LinearLayout.LayoutParams
|
||||
titleTextViewLayoutParams.leftMargin = if (isEditingDisplayName) toPx(16, resources) else 0
|
||||
titleTextView.layoutParams = titleTextViewLayoutParams
|
||||
val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
if (isEditingDisplayName) {
|
||||
displayNameEditText.requestFocus()
|
||||
inputMethodManager.showSoftInput(displayNameEditText, 0)
|
||||
} else {
|
||||
inputMethodManager.hideSoftInputFromWindow(displayNameEditText.windowToken, 0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateProfile(isUpdatingProfilePicture: Boolean) {
|
||||
showLoader()
|
||||
val promises = mutableListOf<Promise<*, Exception>>()
|
||||
val displayName = displayNameToBeUploaded
|
||||
if (displayName != null) {
|
||||
val publicChatAPI = ApplicationContext.getInstance(this).lokiPublicChatAPI
|
||||
if (publicChatAPI != null) {
|
||||
val servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers()
|
||||
promises.addAll(servers.map { publicChatAPI.setDisplayName(displayName, it) })
|
||||
}
|
||||
TextSecurePreferences.setProfileName(this, displayName)
|
||||
}
|
||||
val profilePicture = profilePictureToBeUploaded
|
||||
val encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this)
|
||||
val profileKey = ProfileKeyUtil.getProfileKeyFromEncodedString(encodedProfileKey)
|
||||
if (isUpdatingProfilePicture && profilePicture != null) {
|
||||
val storageAPI = LokiStorageAPI.shared
|
||||
val deferred = deferred<Unit, Exception>()
|
||||
AsyncTask.execute {
|
||||
val stream = StreamDetails(ByteArrayInputStream(profilePicture), "image/jpeg", profilePicture.size.toLong())
|
||||
val (_, url) = storageAPI.uploadProfilePicture(storageAPI.server, profileKey, stream)
|
||||
TextSecurePreferences.setProfileAvatarUrl(this, url)
|
||||
deferred.resolve(Unit)
|
||||
}
|
||||
promises.add(deferred.promise)
|
||||
}
|
||||
all(promises).alwaysUi {
|
||||
if (displayName != null) {
|
||||
displayNameTextView.text = displayName
|
||||
}
|
||||
displayNameToBeUploaded = null
|
||||
if (isUpdatingProfilePicture && profilePicture != null) {
|
||||
AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)), profilePicture)
|
||||
TextSecurePreferences.setProfileAvatarId(this, SecureRandom().nextInt())
|
||||
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
|
||||
ApplicationContext.getInstance(this).updatePublicChatProfileAvatarIfNeeded()
|
||||
profilePictureView.update()
|
||||
}
|
||||
profilePictureToBeUploaded = null
|
||||
hideLoader()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showLoader() {
|
||||
loader.visibility = View.VISIBLE
|
||||
loader.animate().setDuration(150).alpha(1.0f).start()
|
||||
}
|
||||
|
||||
private fun hideLoader() {
|
||||
loader.animate().setDuration(150).alpha(0.0f).setListener(object : AnimatorListenerAdapter() {
|
||||
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
super.onAnimationEnd(animation)
|
||||
loader.visibility = View.GONE
|
||||
}
|
||||
})
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Interaction
|
||||
private fun cancelEditingDisplayName() {
|
||||
isEditingDisplayName = false
|
||||
}
|
||||
|
||||
private fun saveDisplayName() {
|
||||
val displayName = displayNameEditText.text.toString().trim()
|
||||
if (displayName.isEmpty()) {
|
||||
return Toast.makeText(this, "Please pick a display name", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
if (!displayName.matches(Regex("[a-zA-Z0-9_]+"))) {
|
||||
return Toast.makeText(this, "Please pick a display name that consists of only a-z, A-Z, 0-9 and _ characters", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
if (displayName.toByteArray().size > ProfileCipher.NAME_PADDED_LENGTH) {
|
||||
return Toast.makeText(this, "Please pick a shorter display name", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
isEditingDisplayName = false
|
||||
displayNameToBeUploaded = displayName
|
||||
updateProfile(false)
|
||||
}
|
||||
|
||||
private fun showQRCode() {
|
||||
val intent = Intent(this, QRCodeActivity::class.java)
|
||||
push(intent)
|
||||
}
|
||||
|
||||
private fun showEditProfilePictureUI() {
|
||||
tempFile = AvatarSelection.startAvatarSelection(this, false, true)
|
||||
}
|
||||
|
||||
private fun showEditDisplayNameUI() {
|
||||
isEditingDisplayName = true
|
||||
}
|
||||
|
||||
private fun copyPublicKey() {
|
||||
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("Session ID", hexEncodedPublicKey)
|
||||
clipboard.primaryClip = clip
|
||||
Toast.makeText(this, R.string.activity_register_public_key_copied_message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun sharePublicKey() {
|
||||
val intent = Intent()
|
||||
intent.action = Intent.ACTION_SEND
|
||||
intent.putExtra(Intent.EXTRA_TEXT, hexEncodedPublicKey)
|
||||
intent.type = "text/plain"
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun showPrivacySettings() {
|
||||
val intent = Intent(this, PrivacySettingsActivity::class.java)
|
||||
push(intent)
|
||||
}
|
||||
|
||||
private fun showNotificationSettings() {
|
||||
val intent = Intent(this, NotificationSettingsActivity::class.java)
|
||||
push(intent)
|
||||
}
|
||||
|
||||
private fun showChatSettings() {
|
||||
val intent = Intent(this, ChatSettingsActivity::class.java)
|
||||
push(intent)
|
||||
}
|
||||
|
||||
private fun showLinkedDevices() {
|
||||
val intent = Intent(this, LinkedDevicesActivity::class.java)
|
||||
push(intent)
|
||||
}
|
||||
|
||||
private fun showSeed() {
|
||||
SeedDialog().show(supportFragmentManager, "Recovery Phrase Dialog")
|
||||
}
|
||||
|
||||
private fun clearAllData() {
|
||||
ClearAllDataDialog().show(supportFragmentManager, "Clear All Data Dialog")
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.DialogFragment
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.view.LayoutInflater
|
||||
import kotlinx.android.synthetic.main.dialog_clear_all_data.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
|
||||
class ClearAllDataDialog : DialogFragment() {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val builder = AlertDialog.Builder(context!!)
|
||||
val contentView = LayoutInflater.from(context!!).inflate(R.layout.dialog_clear_all_data, null)
|
||||
contentView.cancelButton.setOnClickListener { dismiss() }
|
||||
contentView.clearAllDataButton.setOnClickListener { clearAllData() }
|
||||
builder.setView(contentView)
|
||||
val result = builder.create()
|
||||
result.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
return result
|
||||
}
|
||||
|
||||
private fun clearAllData() {
|
||||
ApplicationContext.getInstance(context).clearData()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.DialogFragment
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import kotlinx.android.synthetic.main.dialog_edit_device_name.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.devicelist.Device
|
||||
|
||||
class EditDeviceNameDialog : DialogFragment() {
|
||||
private lateinit var contentView: View
|
||||
var device: Device? = null
|
||||
var delegate: EditDeviceNameDialogDelegate? = null
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val builder = AlertDialog.Builder(context!!)
|
||||
contentView = LayoutInflater.from(context!!).inflate(R.layout.dialog_edit_device_name, null)
|
||||
contentView.cancelButton.setOnClickListener { dismiss() }
|
||||
contentView.okButton.setOnClickListener { updateDeviceName() }
|
||||
builder.setView(contentView)
|
||||
val result = builder.create()
|
||||
result.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
return result
|
||||
}
|
||||
|
||||
private fun updateDeviceName() {
|
||||
DatabaseFactory.getLokiUserDatabase(context).setDisplayName(device!!.id, contentView.deviceNameEditText.text.toString())
|
||||
delegate?.handleDeviceNameChanged(device!!)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
interface EditDeviceNameDialogDelegate {
|
||||
|
||||
fun handleDeviceNameChanged(device: Device)
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.DialogFragment
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import kotlinx.android.synthetic.main.dialog_link_device_master_mode.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.MnemonicUtilities
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.QRCodeUtilities
|
||||
import org.thoughtcrime.securesms.loki.toPx
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.loki.api.DeviceLinkingSession
|
||||
import org.whispersystems.signalservice.loki.api.DeviceLinkingSessionListener
|
||||
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
|
||||
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec
|
||||
|
||||
class LinkDeviceMasterModeDialog : DialogFragment(), DeviceLinkingSessionListener {
|
||||
private val languageFileDirectory by lazy { MnemonicUtilities.getLanguageFileDirectory(context!!) }
|
||||
private lateinit var contentView: View
|
||||
private var authorization: PairingAuthorisation? = null
|
||||
var delegate: LinkDeviceMasterModeDialogDelegate? = null
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val builder = AlertDialog.Builder(context!!)
|
||||
contentView = LayoutInflater.from(context!!).inflate(R.layout.dialog_link_device_master_mode, null)
|
||||
val size = toPx(128, resources)
|
||||
val hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context!!)
|
||||
val qrCode = QRCodeUtilities.encode(hexEncodedPublicKey, size, false, false)
|
||||
contentView.qrCodeImageView.setImageBitmap(qrCode)
|
||||
contentView.cancelButton.setOnClickListener { onDeviceLinkCanceled() }
|
||||
contentView.authorizeButton.setOnClickListener { authorizeDeviceLink() }
|
||||
builder.setView(contentView)
|
||||
DeviceLinkingSession.shared.startListeningForLinkingRequests() // FIXME: This flag is named poorly as it's actually also used for authorizations
|
||||
DeviceLinkingSession.shared.addListener(this)
|
||||
val result = builder.create()
|
||||
result.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
return result
|
||||
}
|
||||
|
||||
override fun requestUserAuthorization(authorization: PairingAuthorisation) {
|
||||
if (authorization.type != PairingAuthorisation.Type.REQUEST || authorization.primaryDevicePublicKey != TextSecurePreferences.getLocalNumber(context!!) || this.authorization != null) { return }
|
||||
Util.runOnMain {
|
||||
this.authorization = authorization
|
||||
contentView.qrCodeImageView.visibility = View.GONE
|
||||
val titleTextViewLayoutParams = contentView.titleTextView.layoutParams as LinearLayout.LayoutParams
|
||||
titleTextViewLayoutParams.topMargin = toPx(8, resources)
|
||||
contentView.titleTextView.layoutParams = titleTextViewLayoutParams
|
||||
contentView.titleTextView.text = "Linking Request Received"
|
||||
contentView.explanationTextView.text = "Please check that the words below match those shown on your other device"
|
||||
contentView.mnemonicTextView.visibility = View.VISIBLE
|
||||
contentView.mnemonicTextView.text = MnemonicUtilities.getFirst3Words(MnemonicCodec(languageFileDirectory), authorization.secondaryDevicePublicKey)
|
||||
contentView.authorizeButton.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
private fun authorizeDeviceLink() {
|
||||
val authorization = this.authorization ?: return
|
||||
delegate?.onDeviceLinkRequestAuthorized(authorization)
|
||||
DeviceLinkingSession.shared.stopListeningForLinkingRequests()
|
||||
DeviceLinkingSession.shared.removeListener(this)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
private fun onDeviceLinkCanceled() {
|
||||
DeviceLinkingSession.shared.stopListeningForLinkingRequests()
|
||||
DeviceLinkingSession.shared.removeListener(this)
|
||||
if (authorization != null) {
|
||||
DatabaseFactory.getLokiPreKeyBundleDatabase(context).removePreKeyBundle(authorization!!.secondaryDevicePublicKey)
|
||||
}
|
||||
dismiss()
|
||||
delegate?.onDeviceLinkCanceled()
|
||||
}
|
||||
}
|
||||
|
||||
interface LinkDeviceMasterModeDialogDelegate {
|
||||
|
||||
fun onDeviceLinkRequestAuthorized(authorization: PairingAuthorisation)
|
||||
fun onDeviceLinkCanceled()
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.support.v4.app.DialogFragment
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import kotlinx.android.synthetic.main.dialog_link_device_slave_mode.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.MnemonicUtilities
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.loki.api.DeviceLinkingSession
|
||||
import org.whispersystems.signalservice.loki.api.DeviceLinkingSessionListener
|
||||
import org.whispersystems.signalservice.loki.api.PairingAuthorisation
|
||||
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec
|
||||
|
||||
class LinkDeviceSlaveModeDialog : DialogFragment(), DeviceLinkingSessionListener {
|
||||
private val languageFileDirectory by lazy { MnemonicUtilities.getLanguageFileDirectory(context!!) }
|
||||
private lateinit var contentView: View
|
||||
private var authorization: PairingAuthorisation? = null
|
||||
var delegate: LinkDeviceSlaveModeDialogDelegate? = null
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val builder = AlertDialog.Builder(context!!)
|
||||
contentView = LayoutInflater.from(context!!).inflate(R.layout.dialog_link_device_slave_mode, null)
|
||||
val hexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
contentView.mnemonicTextView.text = MnemonicUtilities.getFirst3Words(MnemonicCodec(languageFileDirectory), hexEncodedPublicKey)
|
||||
contentView.cancelButton.setOnClickListener { onDeviceLinkCanceled() }
|
||||
builder.setView(contentView)
|
||||
DeviceLinkingSession.shared.startListeningForLinkingRequests()
|
||||
DeviceLinkingSession.shared.addListener(this)
|
||||
val result = builder.create()
|
||||
result.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
return result
|
||||
}
|
||||
|
||||
override fun onDeviceLinkRequestAuthorized(authorization: PairingAuthorisation) {
|
||||
if (authorization.type != PairingAuthorisation.Type.GRANT || authorization.secondaryDevicePublicKey != TextSecurePreferences.getLocalNumber(context!!) || this.authorization != null) { return }
|
||||
Util.runOnMain {
|
||||
this.authorization = authorization
|
||||
DeviceLinkingSession.shared.stopListeningForLinkingRequests()
|
||||
DeviceLinkingSession.shared.removeListener(this)
|
||||
contentView.spinner.visibility = View.GONE
|
||||
val titleTextViewLayoutParams = contentView.titleTextView.layoutParams as LinearLayout.LayoutParams
|
||||
titleTextViewLayoutParams.topMargin = 0
|
||||
contentView.titleTextView.layoutParams = titleTextViewLayoutParams
|
||||
contentView.titleTextView.text = "Device Link Authorized"
|
||||
contentView.explanationTextView.text = "Your device has been linked successfully"
|
||||
contentView.mnemonicTextView.visibility = View.GONE
|
||||
contentView.cancelButton.visibility = View.GONE
|
||||
Handler().postDelayed({
|
||||
dismiss()
|
||||
delegate?.onDeviceLinkRequestAuthorized(authorization)
|
||||
}, 4000)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onDeviceLinkCanceled() {
|
||||
DeviceLinkingSession.shared.stopListeningForLinkingRequests()
|
||||
DeviceLinkingSession.shared.removeListener(this)
|
||||
dismiss()
|
||||
delegate?.onDeviceLinkCanceled()
|
||||
}
|
||||
}
|
||||
|
||||
interface LinkDeviceSlaveModeDialogDelegate {
|
||||
|
||||
fun onDeviceLinkRequestAuthorized(authorization: PairingAuthorisation)
|
||||
fun onDeviceLinkCanceled()
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.dialogs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.DialogFragment
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.Toast
|
||||
import kotlinx.android.synthetic.main.dialog_seed.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec
|
||||
import org.whispersystems.signalservice.loki.utilities.hexEncodedPrivateKey
|
||||
import java.io.File
|
||||
|
||||
class SeedDialog : DialogFragment() {
|
||||
|
||||
private val seed by lazy {
|
||||
val languageFileDirectory = File(context!!.applicationInfo.dataDir)
|
||||
var hexEncodedSeed = IdentityKeyUtil.retrieve(context!!, IdentityKeyUtil.lokiSeedKey)
|
||||
if (hexEncodedSeed == null) {
|
||||
hexEncodedSeed = IdentityKeyUtil.getIdentityKeyPair(context!!).hexEncodedPrivateKey // Legacy account
|
||||
}
|
||||
MnemonicCodec(languageFileDirectory).encode(hexEncodedSeed!!, MnemonicCodec.Language.Configuration.english)
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val builder = AlertDialog.Builder(context!!)
|
||||
val contentView = LayoutInflater.from(context!!).inflate(R.layout.dialog_seed, null)
|
||||
contentView.seedTextView.text = seed
|
||||
contentView.cancelButton.setOnClickListener { dismiss() }
|
||||
contentView.copyButton.setOnClickListener { copySeed() }
|
||||
builder.setView(contentView)
|
||||
val result = builder.create()
|
||||
result.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
return result
|
||||
}
|
||||
|
||||
private fun copySeed() {
|
||||
val clipboard = activity!!.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("Seed", seed)
|
||||
clipboard.primaryClip = clip
|
||||
Toast.makeText(context!!, R.string.activity_register_public_key_copied_message, Toast.LENGTH_SHORT).show()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
@@ -1,60 +1,46 @@
|
||||
package org.thoughtcrime.securesms.loki
|
||||
package org.thoughtcrime.securesms.loki.redesign.fragments
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import kotlinx.android.synthetic.main.fragment_scan_qr_code.*
|
||||
import kotlinx.android.synthetic.main.fragment_scan_qr_code_v2.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.qr.ScanListener
|
||||
import org.thoughtcrime.securesms.qr.ScanningThread
|
||||
|
||||
class ScanQRCodeFragment : Fragment() {
|
||||
class ScanQRCodeFragmentV2 : Fragment() {
|
||||
private val scanningThread = ScanningThread()
|
||||
private var viewCreated = false
|
||||
var scanListener: ScanListener? = null
|
||||
set(value) { field = value; scanningThread.setScanListener(scanListener) }
|
||||
var mode: Mode = Mode.NewConversation
|
||||
set(value) { field = value; updateDescription(); }
|
||||
|
||||
// region Types
|
||||
enum class Mode { NewConversation, LinkDevice }
|
||||
// endregion
|
||||
var message: CharSequence = ""
|
||||
|
||||
override fun onCreateView(layoutInflater: LayoutInflater, viewGroup: ViewGroup?, bundle: Bundle?): View? {
|
||||
return layoutInflater.inflate(R.layout.fragment_scan_qr_code, viewGroup, false)
|
||||
return layoutInflater.inflate(R.layout.fragment_scan_qr_code_v2, viewGroup, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, bundle: Bundle?) {
|
||||
super.onViewCreated(view, bundle)
|
||||
viewCreated = true
|
||||
when (resources.configuration.orientation) {
|
||||
Configuration.ORIENTATION_LANDSCAPE -> overlayView.orientation = LinearLayout.HORIZONTAL
|
||||
else -> overlayView.orientation = LinearLayout.VERTICAL
|
||||
}
|
||||
updateDescription()
|
||||
messageTextView.text = message
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
this.scanningThread.setScanListener(scanListener)
|
||||
this.cameraView.onResume()
|
||||
this.cameraView.setPreviewCallback(scanningThread)
|
||||
this.scanningThread.start()
|
||||
if (activity is AppCompatActivity) {
|
||||
val activity = activity as AppCompatActivity
|
||||
activity.supportActionBar?.setTitle(R.string.fragment_scan_qr_code_title)
|
||||
cameraView.onResume()
|
||||
cameraView.setPreviewCallback(scanningThread)
|
||||
try {
|
||||
scanningThread.start()
|
||||
} catch (exception: Exception) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
this.cameraView.onPause()
|
||||
this.scanningThread.stopScanning()
|
||||
scanningThread.setScanListener(scanListener)
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfiguration: Configuration) {
|
||||
@@ -68,12 +54,9 @@ class ScanQRCodeFragment : Fragment() {
|
||||
cameraView.setPreviewCallback(scanningThread)
|
||||
}
|
||||
|
||||
fun updateDescription() {
|
||||
if (!viewCreated) { return }
|
||||
val text = when (mode) {
|
||||
Mode.NewConversation -> R.string.fragment_scan_qr_code_explanation_new_conversation
|
||||
Mode.LinkDevice -> R.string.fragment_scan_qr_code_explanation_link_device
|
||||
}
|
||||
descriptionTextView.setText(text)
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
this.cameraView.onPause()
|
||||
this.scanningThread.stopScanning()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import kotlinx.android.synthetic.main.fragment_scan_qr_code_placeholder.*
|
||||
import network.loki.messenger.R
|
||||
|
||||
class ScanQRCodePlaceholderFragment: Fragment() {
|
||||
var delegate: ScanQRCodePlaceholderFragmentDelegate? = null
|
||||
|
||||
override fun onCreateView(layoutInflater: LayoutInflater, viewGroup: ViewGroup?, bundle: Bundle?): View? {
|
||||
return layoutInflater.inflate(R.layout.fragment_scan_qr_code_placeholder, viewGroup, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
grantCameraAccessButton.setOnClickListener { delegate?.requestCameraAccess() }
|
||||
}
|
||||
}
|
||||
|
||||
interface ScanQRCodePlaceholderFragmentDelegate {
|
||||
|
||||
fun requestCameraAccess()
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.fragments
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.tbruyelle.rxpermissions2.RxPermissions
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.qr.ScanListener
|
||||
|
||||
class ScanQRCodeWrapperFragment : Fragment(), ScanQRCodePlaceholderFragmentDelegate, ScanListener {
|
||||
var delegate: ScanQRCodeWrapperFragmentDelegate? = null
|
||||
var message: CharSequence = ""
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_scan_qr_code_wrapper, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
update()
|
||||
}
|
||||
|
||||
private fun update() {
|
||||
val fragment: Fragment
|
||||
if (ContextCompat.checkSelfPermission(activity!!, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
||||
val scanQRCodeFragment = ScanQRCodeFragmentV2()
|
||||
scanQRCodeFragment.scanListener = this
|
||||
scanQRCodeFragment.message = message
|
||||
fragment = scanQRCodeFragment
|
||||
} else {
|
||||
val scanQRCodePlaceholderFragment = ScanQRCodePlaceholderFragment()
|
||||
scanQRCodePlaceholderFragment.delegate = this
|
||||
fragment = scanQRCodePlaceholderFragment
|
||||
}
|
||||
val transaction = childFragmentManager.beginTransaction()
|
||||
transaction.replace(R.id.fragmentContainer, fragment)
|
||||
transaction.commit()
|
||||
}
|
||||
|
||||
override fun requestCameraAccess() {
|
||||
@SuppressWarnings("unused")
|
||||
val unused = RxPermissions(this).request(Manifest.permission.CAMERA).subscribe { isGranted ->
|
||||
if (isGranted) {
|
||||
update()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onQrDataFound(string: String) {
|
||||
delegate?.handleQRCodeScanned(string)
|
||||
}
|
||||
}
|
||||
|
||||
interface ScanQRCodeWrapperFragmentDelegate {
|
||||
|
||||
fun handleQRCodeScanned(string: String)
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
package org.thoughtcrime.securesms.loki
|
||||
package org.thoughtcrime.securesms.loki.redesign.messaging
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import nl.komponents.kovenant.functional.map
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.jobs.PushContentReceiveJob
|
||||
import org.thoughtcrime.securesms.service.PersistentAlarmManagerListener
|
||||
@@ -33,7 +34,7 @@ class BackgroundPollWorker : PersistentAlarmManagerListener() {
|
||||
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
val lokiAPIDatabase = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
try {
|
||||
LokiAPI(userHexEncodedPublicKey, lokiAPIDatabase).getMessages().map { messages ->
|
||||
LokiAPI(userHexEncodedPublicKey, lokiAPIDatabase, (context.applicationContext as ApplicationContext).broadcaster).getMessages().map { messages ->
|
||||
messages.forEach {
|
||||
PushContentReceiveJob(context).processEnvelope(SignalServiceEnvelope(it))
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.messaging
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.service.PersistentAlarmManagerListener
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class BackgroundPublicChatPollWorker : PersistentAlarmManagerListener() {
|
||||
|
||||
companion object {
|
||||
private val pollInterval = TimeUnit.MINUTES.toMillis(4)
|
||||
|
||||
@JvmStatic
|
||||
fun schedule(context: Context) {
|
||||
BackgroundPublicChatPollWorker().onReceive(context, Intent())
|
||||
}
|
||||
}
|
||||
|
||||
override fun getNextScheduledExecutionTime(context: Context): Long {
|
||||
return TextSecurePreferences.getPublicChatBackgroundPollTime(context)
|
||||
}
|
||||
|
||||
override fun onAlarm(context: Context, scheduledTime: Long): Long {
|
||||
if (scheduledTime != 0L) {
|
||||
val publicChats = DatabaseFactory.getLokiThreadDatabase(context).getAllPublicChats().map { it.value }
|
||||
for (publicChat in publicChats) {
|
||||
val poller = LokiPublicChatPoller(context, publicChat)
|
||||
poller.stop()
|
||||
poller.pollForNewMessages()
|
||||
}
|
||||
}
|
||||
val nextTime = System.currentTimeMillis() + pollInterval
|
||||
TextSecurePreferences.setPublicChatBackgroundPollTime(context, nextTime)
|
||||
return nextTime
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.loki
|
||||
package org.thoughtcrime.securesms.loki.redesign.messaging
|
||||
|
||||
import android.content.Context
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.loki
|
||||
package org.thoughtcrime.securesms.loki.redesign.messaging
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
@@ -153,7 +153,7 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
|
||||
return SignalServiceDataMessage(message.timestamp, serviceGroup, attachments, body, false, 0, false, null, false, quote, null, signalLinkPreviews, null)
|
||||
}
|
||||
|
||||
private fun pollForNewMessages() {
|
||||
fun pollForNewMessages() {
|
||||
fun processIncomingMessage(message: LokiPublicChatMessage) {
|
||||
// If the sender of the current message is not a secondary device, we need to set the display name in the database
|
||||
val primaryDevice = LokiStorageAPI.shared.getPrimaryDevicePublicKey(message.hexEncodedPublicKey).get()
|
||||
@@ -220,6 +220,9 @@ class LokiPublicChatPoller(private val context: Context, private val group: Loki
|
||||
}
|
||||
var userDevices = setOf<String>()
|
||||
var uniqueDevices = setOf<String>()
|
||||
val userPrivateKey = IdentityKeyUtil.getIdentityKeyPair(context).privateKey.serialize()
|
||||
val database = DatabaseFactory.getLokiAPIDatabase(context)
|
||||
LokiStorageAPI.configure(false, userHexEncodedPublicKey, userPrivateKey, database)
|
||||
LokiStorageAPI.shared.getAllDevicePublicKeys(userHexEncodedPublicKey).bind { devices ->
|
||||
userDevices = devices
|
||||
api.getMessages(group.channel, group.server)
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.utilities
|
||||
|
||||
import android.content.Intent
|
||||
import android.support.v7.app.ActionBar
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.view.Gravity
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
import network.loki.messenger.R
|
||||
|
||||
fun AppCompatActivity.setUpActionBarSessionLogo() {
|
||||
supportActionBar!!.setDisplayShowHomeEnabled(false)
|
||||
supportActionBar!!.setDisplayShowTitleEnabled(false)
|
||||
val logoImageView = ImageView(this)
|
||||
logoImageView.setImageResource(R.drawable.session_logo)
|
||||
val logoImageViewContainer = RelativeLayout(this)
|
||||
logoImageViewContainer.addView(logoImageView)
|
||||
logoImageViewContainer.gravity = Gravity.CENTER
|
||||
val logoImageViewContainerLayoutParams = ActionBar.LayoutParams(ActionBar.LayoutParams.MATCH_PARENT, ActionBar.LayoutParams.WRAP_CONTENT)
|
||||
supportActionBar!!.setCustomView(logoImageViewContainer, logoImageViewContainerLayoutParams)
|
||||
supportActionBar!!.setDisplayShowCustomEnabled(true)
|
||||
}
|
||||
|
||||
fun AppCompatActivity.push(intent: Intent, isForResult: Boolean = false) {
|
||||
if (isForResult) {
|
||||
startActivityForResult(intent, 42)
|
||||
} else {
|
||||
startActivity(intent)
|
||||
}
|
||||
overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out)
|
||||
}
|
||||
|
||||
fun AppCompatActivity.show(intent: Intent, isForResult: Boolean = false) {
|
||||
if (isForResult) {
|
||||
startActivityForResult(intent, 42)
|
||||
} else {
|
||||
startActivity(intent)
|
||||
}
|
||||
overridePendingTransition(R.anim.slide_from_bottom, R.anim.fade_scale_out)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.utilities
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.support.v4.content.LocalBroadcastManager
|
||||
|
||||
class Broadcaster(private val context: Context) : org.whispersystems.signalservice.loki.utilities.Broadcaster {
|
||||
|
||||
override fun broadcast(event: String, long: Long) {
|
||||
val intent = Intent(event)
|
||||
intent.putExtra("long", long)
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.loki
|
||||
package org.thoughtcrime.securesms.loki.redesign.utilities
|
||||
|
||||
import android.content.ContentValues
|
||||
import net.sqlcipher.Cursor
|
||||
@@ -1,12 +1,16 @@
|
||||
package org.thoughtcrime.securesms.loki
|
||||
package org.thoughtcrime.securesms.loki.redesign.utilities
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Typeface
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.style.BackgroundColorSpan
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.util.Range
|
||||
import network.loki.messenger.R
|
||||
import nl.komponents.kovenant.combine.Tuple2
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.loki.getColorWithID
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import java.util.regex.Pattern
|
||||
|
||||
@@ -14,7 +18,7 @@ object MentionUtilities {
|
||||
|
||||
@JvmStatic
|
||||
fun highlightMentions(text: CharSequence, threadID: Long, context: Context): String {
|
||||
return MentionUtilities.highlightMentions(text, false, threadID, context).toString() // isOutgoingMessage is irrelevant
|
||||
return highlightMentions(text, false, threadID, context).toString() // isOutgoingMessage is irrelevant
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@@ -22,13 +26,14 @@ object MentionUtilities {
|
||||
var text = text
|
||||
val pattern = Pattern.compile("@[0-9a-fA-F]*")
|
||||
var matcher = pattern.matcher(text)
|
||||
val mentions = mutableListOf<Range<Int>>()
|
||||
val mentions = mutableListOf<Tuple2<Range<Int>, String>>()
|
||||
var startIndex = 0
|
||||
val publicChat = DatabaseFactory.getLokiThreadDatabase(context).getPublicChat(threadID)
|
||||
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(context)
|
||||
if (matcher.find(startIndex)) {
|
||||
while (true) {
|
||||
val hexEncodedPublicKey = text.subSequence(matcher.start() + 1, matcher.end()).toString() // +1 to get rid of the @
|
||||
val userDisplayName: String? = if (hexEncodedPublicKey.toLowerCase() == TextSecurePreferences.getLocalNumber(context).toLowerCase()) {
|
||||
val userDisplayName: String? = if (hexEncodedPublicKey.toLowerCase() == userHexEncodedPublicKey.toLowerCase()) {
|
||||
TextSecurePreferences.getProfileName(context)
|
||||
} else if (publicChat != null) {
|
||||
DatabaseFactory.getLokiUserDatabase(context).getServerDisplayName(publicChat.id, hexEncodedPublicKey)
|
||||
@@ -39,7 +44,7 @@ object MentionUtilities {
|
||||
text = text.subSequence(0, matcher.start()).toString() + "@" + userDisplayName + text.subSequence(matcher.end(), text.length)
|
||||
val endIndex = matcher.start() + 1 + userDisplayName.length
|
||||
startIndex = endIndex
|
||||
mentions.add(Range.create(matcher.start(), endIndex))
|
||||
mentions.add(Tuple2(Range.create(matcher.start(), endIndex), hexEncodedPublicKey))
|
||||
} else {
|
||||
startIndex = matcher.end()
|
||||
}
|
||||
@@ -48,9 +53,12 @@ object MentionUtilities {
|
||||
}
|
||||
}
|
||||
val result = SpannableString(text)
|
||||
for (range in mentions) {
|
||||
val highlightColor = if (isOutgoingMessage) context.resources.getColor(R.color.loki_dark_green) else context.resources.getColor(R.color.loki_green)
|
||||
result.setSpan(BackgroundColorSpan(highlightColor), range.lower, range.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
val userLinkedDeviceHexEncodedPublicKeys = DatabaseFactory.getLokiAPIDatabase(context).getPairingAuthorisations(userHexEncodedPublicKey).flatMap { listOf( it.primaryDevicePublicKey, it.secondaryDevicePublicKey ) }.toMutableSet()
|
||||
userLinkedDeviceHexEncodedPublicKeys.add(userHexEncodedPublicKey)
|
||||
for (mention in mentions) {
|
||||
if (!userLinkedDeviceHexEncodedPublicKeys.contains(mention.second)) { continue }
|
||||
result.setSpan(ForegroundColorSpan(context.resources.getColorWithID(R.color.accent, context.theme)), mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
result.setSpan(StyleSpan(Typeface.BOLD), mention.first.lower, mention.first.upper, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.loki
|
||||
package org.thoughtcrime.securesms.loki.redesign.utilities
|
||||
|
||||
import android.content.Context
|
||||
import org.whispersystems.signalservice.loki.crypto.MnemonicCodec
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.utilities
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.EncodeHintType
|
||||
import com.google.zxing.WriterException
|
||||
import com.google.zxing.qrcode.QRCodeWriter
|
||||
|
||||
object QRCodeUtilities {
|
||||
|
||||
fun encode(data: String, size: Int, isInverted: Boolean = false, hasTransparentBackground: Boolean = true): Bitmap {
|
||||
try {
|
||||
val hints = hashMapOf( EncodeHintType.MARGIN to 1 )
|
||||
val result = QRCodeWriter().encode(data, BarcodeFormat.QR_CODE, size, size, hints)
|
||||
val bitmap = Bitmap.createBitmap(result.width, result.height, Bitmap.Config.ARGB_8888)
|
||||
for (y in 0 until result.height) {
|
||||
for (x in 0 until result.width) {
|
||||
if (result.get(x, y)) {
|
||||
bitmap.setPixel(x, y, if (isInverted) Color.WHITE else Color.BLACK)
|
||||
} else if (!hasTransparentBackground) {
|
||||
bitmap.setPixel(x, y, if (isInverted) Color.BLACK else Color.WHITE)
|
||||
}
|
||||
}
|
||||
}
|
||||
return bitmap
|
||||
} catch (e: WriterException) {
|
||||
return Bitmap.createBitmap(512, 512, Bitmap.Config.ARGB_8888)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.views
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Typeface
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import kotlinx.android.synthetic.main.view_conversation.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import org.thoughtcrime.securesms.loki.redesign.messaging.LokiAPIUtilities.populateUserHexEncodedPublicKeyCacheIfNeeded
|
||||
import org.thoughtcrime.securesms.loki.redesign.utilities.MentionUtilities.highlightMentions
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.whispersystems.signalservice.loki.api.LokiAPI
|
||||
import java.util.*
|
||||
|
||||
class ConversationView : LinearLayout {
|
||||
var thread: ThreadRecord? = null
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
private fun setUpViewHierarchy() {
|
||||
val inflater = context.applicationContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
val contentView = inflater.inflate(R.layout.view_conversation, null)
|
||||
addView(contentView)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
fun bind(thread: ThreadRecord, isTyping: Boolean, glide: GlideRequests) {
|
||||
this.thread = thread
|
||||
populateUserHexEncodedPublicKeyCacheIfNeeded(thread.threadId, context) // FIXME: This is a terrible place to do this
|
||||
unreadMessagesIndicatorView.visibility = if (thread.unreadCount > 0) View.VISIBLE else View.INVISIBLE
|
||||
if (thread.recipient.isGroupRecipient) {
|
||||
val users = LokiAPI.userHexEncodedPublicKeyCache[thread.threadId]?.toList() ?: listOf()
|
||||
val randomUsers = users.sorted() // Sort to provide a level of stability
|
||||
profilePictureView.hexEncodedPublicKey = randomUsers.getOrNull(0) ?: ""
|
||||
profilePictureView.additionalHexEncodedPublicKey = randomUsers.getOrNull(1) ?: ""
|
||||
profilePictureView.isRSSFeed = thread.recipient.name == "Loki News" || thread.recipient.name == "Loki Messenger Updates"
|
||||
} else {
|
||||
profilePictureView.hexEncodedPublicKey = thread.recipient.address.toString()
|
||||
profilePictureView.additionalHexEncodedPublicKey = null
|
||||
profilePictureView.isRSSFeed = false
|
||||
}
|
||||
profilePictureView.glide = glide
|
||||
profilePictureView.update()
|
||||
val senderDisplayName = if (thread.recipient.isLocalNumber) context.getString(R.string.note_to_self) else if (!thread.recipient.name.isNullOrEmpty()) thread.recipient.name else thread.recipient.address.toString()
|
||||
displayNameTextView.text = senderDisplayName
|
||||
timestampTextView.text = DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), thread.date)
|
||||
muteIndicatorImageView.visibility = if (thread.recipient.isMuted) VISIBLE else GONE
|
||||
val rawSnippet = thread.getDisplayBody(context)
|
||||
val snippet = highlightMentions(rawSnippet, thread.threadId, context)
|
||||
snippetTextView.text = snippet
|
||||
snippetTextView.typeface = if (thread.unreadCount > 0) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
|
||||
snippetTextView.visibility = if (isTyping) View.GONE else View.VISIBLE
|
||||
if (isTyping) {
|
||||
typingIndicatorView.startAnimation()
|
||||
} else {
|
||||
typingIndicatorView.stopAnimation()
|
||||
}
|
||||
typingIndicatorView.visibility = if (isTyping) View.VISIBLE else View.GONE
|
||||
statusIndicatorImageView.visibility = View.VISIBLE
|
||||
when {
|
||||
!thread.isOutgoing || thread.isVerificationStatusChange -> statusIndicatorImageView.visibility = View.GONE
|
||||
thread.isFailed -> statusIndicatorImageView.setImageResource(R.drawable.ic_error)
|
||||
thread.isPending -> statusIndicatorImageView.setImageResource(R.drawable.ic_circle_dot_dot_dot)
|
||||
thread.isRemoteRead -> statusIndicatorImageView.setImageResource(R.drawable.ic_filled_circle_check)
|
||||
else -> statusIndicatorImageView.setImageResource(R.drawable.ic_circle_check)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.views
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import kotlinx.android.synthetic.main.view_device.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.devicelist.Device
|
||||
import org.thoughtcrime.securesms.loki.toPx
|
||||
|
||||
class DeviceView : LinearLayout {
|
||||
var device: Device? = null
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
private fun setUpViewHierarchy() {
|
||||
val inflater = context.applicationContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
val contentView = inflater.inflate(R.layout.view_device, null)
|
||||
addView(contentView)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
fun bind(device: Device) {
|
||||
titleTextView.text = if (!device.name.isNullOrBlank()) device.name else "Unnamed Device"
|
||||
// FIXME: Hacky way of getting the view to be screen width
|
||||
val titleTextViewLayoutParams = titleTextView.layoutParams
|
||||
titleTextViewLayoutParams.width = resources.displayMetrics.widthPixels - toPx(32, resources)
|
||||
titleTextView.layoutParams = titleTextViewLayoutParams
|
||||
subtitleTextView.text = device.shortId
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.views
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Context.LAYOUT_INFLATER_SERVICE
|
||||
import android.os.Handler
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.ScrollView
|
||||
import kotlinx.android.synthetic.main.view_fake_chat.view.*
|
||||
import network.loki.messenger.R
|
||||
|
||||
class FakeChatView : ScrollView {
|
||||
|
||||
// region Settings
|
||||
private val spacing = context.resources.getDimension(R.dimen.medium_spacing)
|
||||
private val startDelay: Long = 2000
|
||||
private val delayBetweenMessages: Long = 3000
|
||||
private val animationDuration: Long = 400
|
||||
// endregion
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
private fun setUpViewHierarchy() {
|
||||
val inflater = context.applicationContext.getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
val contentView = inflater.inflate(R.layout.view_fake_chat, null)
|
||||
addView(contentView)
|
||||
isVerticalScrollBarEnabled = false
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Animation
|
||||
fun startAnimating() {
|
||||
listOf( bubble1, bubble2, bubble3, bubble4, bubble5 ).forEach { it.alpha = 0.0f }
|
||||
fun show(view: View) {
|
||||
view.animate().alpha(1.0f).setDuration(animationDuration).start()
|
||||
}
|
||||
Handler().postDelayed({
|
||||
show(bubble1)
|
||||
Handler().postDelayed({
|
||||
show(bubble2)
|
||||
Handler().postDelayed({
|
||||
show(bubble3)
|
||||
smoothScrollTo(0, (bubble1.height + spacing).toInt())
|
||||
Handler().postDelayed({
|
||||
show(bubble4)
|
||||
smoothScrollTo(0, (bubble1.height + spacing).toInt() + (bubble2.height + spacing).toInt())
|
||||
Handler().postDelayed({
|
||||
show(bubble5)
|
||||
smoothScrollTo(0, (bubble1.height + spacing).toInt() + (bubble2.height + spacing).toInt() + (bubble3.height + spacing).toInt())
|
||||
}, delayBetweenMessages)
|
||||
}, delayBetweenMessages)
|
||||
}, delayBetweenMessages)
|
||||
}, delayBetweenMessages)
|
||||
}, startDelay)
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
@@ -1,17 +1,21 @@
|
||||
package org.thoughtcrime.securesms.loki
|
||||
package org.thoughtcrime.securesms.loki.redesign.views
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import com.github.ybq.android.spinkit.style.DoubleBounce
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.loki.getColorWithID
|
||||
import org.thoughtcrime.securesms.loki.toPx
|
||||
import org.whispersystems.signalservice.loki.messaging.LokiMessageFriendRequestStatus
|
||||
|
||||
class FriendRequestView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : LinearLayout(context, attrs, defStyleAttr) {
|
||||
@@ -28,14 +32,16 @@ class FriendRequestView(context: Context, attrs: AttributeSet?, defStyleAttr: In
|
||||
|
||||
private val label by lazy {
|
||||
val result = TextView(context)
|
||||
result.setTextColor(resources.getColorWithID(R.color.white, context.theme))
|
||||
result.setTextColor(resources.getColorWithID(R.color.text, context.theme))
|
||||
result.textAlignment = TextView.TEXT_ALIGNMENT_CENTER
|
||||
result.setTextSize(TypedValue.COMPLEX_UNIT_PX, resources.getDimension(R.dimen.small_font_size))
|
||||
result
|
||||
}
|
||||
|
||||
private val buttonLinearLayout by lazy {
|
||||
val result = LinearLayout(context)
|
||||
result.orientation = HORIZONTAL
|
||||
result.setPadding(0, resources.getDimension(R.dimen.medium_spacing).toInt(), 0, 0)
|
||||
result
|
||||
}
|
||||
|
||||
@@ -64,39 +70,45 @@ class FriendRequestView(context: Context, attrs: AttributeSet?, defStyleAttr: In
|
||||
if (isUISetUp) { return }
|
||||
isUISetUp = true
|
||||
orientation = VERTICAL
|
||||
setPadding(toPx(48, resources), 0, toPx(48, resources), 0)
|
||||
addView(topSpacer)
|
||||
addView(label)
|
||||
if (!message!!.isOutgoing) {
|
||||
val loader = ProgressBar(context)
|
||||
loader.isIndeterminate = true
|
||||
val color = resources.getColorWithID(R.color.white, context.theme)
|
||||
loader.indeterminateDrawable.setColorFilter(color, android.graphics.PorterDuff.Mode.SRC_IN)
|
||||
loader.indeterminateDrawable = DoubleBounce()
|
||||
val loaderLayoutParams = LayoutParams(LayoutParams.MATCH_PARENT, toPx(24, resources))
|
||||
loader.layoutParams = loaderLayoutParams
|
||||
loaderContainer.addView(loader)
|
||||
addView(loaderContainer)
|
||||
fun button(): Button {
|
||||
val result = Button(context)
|
||||
result.setBackgroundColor(resources.getColorWithID(R.color.transparent, context.theme))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
result.elevation = 0f
|
||||
result.stateListAnimator = null
|
||||
}
|
||||
val buttonLayoutParams = LayoutParams(0, toPx(50, resources))
|
||||
result.setTextColor(resources.getColorWithID(R.color.text, context.theme))
|
||||
result.setTextSize(TypedValue.COMPLEX_UNIT_PX, resources.getDimension(R.dimen.small_font_size))
|
||||
result.isAllCaps = false
|
||||
result.setPadding(0, 0, 0, 0)
|
||||
val buttonLayoutParams = LayoutParams(0, resources.getDimension(R.dimen.small_button_height).toInt())
|
||||
buttonLayoutParams.weight = 1f
|
||||
result.layoutParams = buttonLayoutParams
|
||||
return result
|
||||
}
|
||||
val acceptButton = button()
|
||||
acceptButton.text = resources.getString(R.string.view_friend_request_accept_button_title)
|
||||
acceptButton.setTextColor(resources.getColorWithID(R.color.signal_primary, context.theme))
|
||||
acceptButton.setOnClickListener { accept() }
|
||||
buttonLinearLayout.addView(acceptButton)
|
||||
val rejectButton = button()
|
||||
rejectButton.text = resources.getString(R.string.view_friend_request_reject_button_title)
|
||||
rejectButton.setTextColor(resources.getColorWithID(R.color.red, context.theme))
|
||||
rejectButton.setBackgroundResource(R.drawable.unimportant_dialog_button_background)
|
||||
rejectButton.setOnClickListener { reject() }
|
||||
buttonLinearLayout.addView(rejectButton)
|
||||
val acceptButton = button()
|
||||
acceptButton.text = resources.getString(R.string.view_friend_request_accept_button_title)
|
||||
acceptButton.setBackgroundResource(R.drawable.prominent_dialog_button_background)
|
||||
val acceptButtonLayoutParams = acceptButton.layoutParams as LayoutParams
|
||||
acceptButtonLayoutParams.setMargins(resources.getDimension(R.dimen.medium_spacing).toInt(), 0, 0, 0)
|
||||
acceptButton.layoutParams = acceptButtonLayoutParams
|
||||
acceptButton.setOnClickListener { accept() }
|
||||
buttonLinearLayout.addView(acceptButton)
|
||||
buttonLinearLayout.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, toPx(50, resources))
|
||||
addView(buttonLinearLayout)
|
||||
}
|
||||
@@ -155,4 +167,19 @@ class FriendRequestView(context: Context, attrs: AttributeSet?, defStyleAttr: In
|
||||
delegate?.rejectFriendRequest(message!!)
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
}
|
||||
|
||||
// region Delegate
|
||||
interface FriendRequestViewDelegate {
|
||||
/**
|
||||
* Implementations of this method should update the thread's friend request status
|
||||
* and send a friend request accepted message.
|
||||
*/
|
||||
fun acceptFriendRequest(friendRequest: MessageRecord)
|
||||
/**
|
||||
* Implementations of this method should update the thread's friend request status
|
||||
* and remove the pre keys associated with the contact.
|
||||
*/
|
||||
fun rejectFriendRequest(friendRequest: MessageRecord)
|
||||
}
|
||||
// endregion
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.loki
|
||||
package org.thoughtcrime.securesms.loki.redesign.views
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
@@ -8,11 +8,15 @@ import android.view.ViewGroup
|
||||
import android.widget.BaseAdapter
|
||||
import android.widget.ListView
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.loki.toPx
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.whispersystems.signalservice.loki.messaging.Mention
|
||||
|
||||
class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : ListView(context, attrs, defStyleAttr) {
|
||||
private var mentionCandidates = listOf<Mention>()
|
||||
set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.mentionCandidates = newValue }
|
||||
var glide: GlideRequests? = null
|
||||
set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.glide = newValue }
|
||||
var publicChatServer: String? = null
|
||||
set(newValue) { field = newValue; mentionCandidateSelectionViewAdapter.publicChatServer = publicChatServer }
|
||||
var publicChatChannel: Long? = null
|
||||
@@ -24,6 +28,7 @@ class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defS
|
||||
private class Adapter(private val context: Context) : BaseAdapter() {
|
||||
var mentionCandidates = listOf<Mention>()
|
||||
set(newValue) { field = newValue; notifyDataSetChanged() }
|
||||
var glide: GlideRequests? = null
|
||||
var publicChatServer: String? = null
|
||||
var publicChatChannel: Long? = null
|
||||
|
||||
@@ -40,8 +45,9 @@ class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defS
|
||||
}
|
||||
|
||||
override fun getView(position: Int, cellToBeReused: View?, parent: ViewGroup): View {
|
||||
val cell = cellToBeReused as MentionCandidateSelectionViewCell? ?: MentionCandidateSelectionViewCell.inflate(LayoutInflater.from(context), parent)
|
||||
val cell = cellToBeReused as MentionCandidateView? ?: MentionCandidateView.inflate(LayoutInflater.from(context), parent)
|
||||
val mentionCandidate = getItem(position)
|
||||
cell.glide = glide
|
||||
cell.mentionCandidate = mentionCandidate
|
||||
cell.publicChatServer = publicChatServer
|
||||
cell.publicChatChannel = publicChatChannel
|
||||
@@ -53,6 +59,7 @@ class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defS
|
||||
constructor(context: Context) : this(context, null)
|
||||
|
||||
init {
|
||||
clipToOutline = true
|
||||
adapter = mentionCandidateSelectionViewAdapter
|
||||
mentionCandidateSelectionViewAdapter.mentionCandidates = mentionCandidates
|
||||
setOnItemClickListener { _, _, position, _ ->
|
||||
@@ -68,7 +75,7 @@ class MentionCandidateSelectionView(context: Context, attrs: AttributeSet?, defS
|
||||
}
|
||||
this.mentionCandidates = mentionCandidates
|
||||
val layoutParams = this.layoutParams as ViewGroup.LayoutParams
|
||||
layoutParams.height = toPx(6 + Math.min(mentionCandidates.count(), 4) * 52, resources)
|
||||
layoutParams.height = toPx(Math.min(mentionCandidates.count(), 4) * 44, resources)
|
||||
this.layoutParams = layoutParams
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
package org.thoughtcrime.securesms.loki
|
||||
package org.thoughtcrime.securesms.loki.redesign.views
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Outline
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewOutlineProvider
|
||||
import android.widget.LinearLayout
|
||||
import kotlinx.android.synthetic.main.cell_mention_candidate_selection_view.view.*
|
||||
import kotlinx.android.synthetic.main.view_mention_candidate.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.whispersystems.signalservice.loki.api.LokiPublicChatAPI
|
||||
import org.whispersystems.signalservice.loki.messaging.Mention
|
||||
|
||||
class MentionCandidateSelectionViewCell(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : LinearLayout(context, attrs, defStyleAttr) {
|
||||
class MentionCandidateView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : LinearLayout(context, attrs, defStyleAttr) {
|
||||
var mentionCandidate = Mention("", "")
|
||||
set(newValue) { field = newValue; update() }
|
||||
var glide: GlideRequests? = null
|
||||
var publicChatServer: String? = null
|
||||
var publicChatChannel: Long? = null
|
||||
|
||||
@@ -24,25 +24,18 @@ class MentionCandidateSelectionViewCell(context: Context, attrs: AttributeSet?,
|
||||
|
||||
companion object {
|
||||
|
||||
fun inflate(layoutInflater: LayoutInflater, parent: ViewGroup): MentionCandidateSelectionViewCell {
|
||||
return layoutInflater.inflate(R.layout.cell_mention_candidate_selection_view, parent, false) as MentionCandidateSelectionViewCell
|
||||
fun inflate(layoutInflater: LayoutInflater, parent: ViewGroup): MentionCandidateView {
|
||||
return layoutInflater.inflate(R.layout.view_mention_candidate, parent, false) as MentionCandidateView
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFinishInflate() {
|
||||
super.onFinishInflate()
|
||||
profilePictureImageViewContainer.outlineProvider = object : ViewOutlineProvider() {
|
||||
|
||||
override fun getOutline(view: View, outline: Outline) {
|
||||
outline.setOval(0, 0, view.width, view.height)
|
||||
}
|
||||
}
|
||||
profilePictureImageViewContainer.clipToOutline = true
|
||||
}
|
||||
|
||||
private fun update() {
|
||||
displayNameTextView.text = mentionCandidate.displayName
|
||||
profilePictureImageView.update(mentionCandidate.hexEncodedPublicKey)
|
||||
profilePictureView.hexEncodedPublicKey = mentionCandidate.hexEncodedPublicKey
|
||||
profilePictureView.additionalHexEncodedPublicKey = null
|
||||
profilePictureView.isRSSFeed = false
|
||||
profilePictureView.glide = glide!!
|
||||
profilePictureView.update()
|
||||
if (publicChatServer != null && publicChatChannel != null) {
|
||||
val isUserModerator = LokiPublicChatAPI.isUserModerator(mentionCandidate.hexEncodedPublicKey, publicChatChannel!!, publicChatServer!!)
|
||||
moderatorIconImageView.visibility = if (isUserModerator) View.VISIBLE else View.GONE
|
||||
@@ -0,0 +1,81 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.views
|
||||
|
||||
import android.content.Context
|
||||
import android.support.annotation.DimenRes
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import kotlinx.android.synthetic.main.view_profile_picture.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto
|
||||
import org.thoughtcrime.securesms.database.Address
|
||||
import org.thoughtcrime.securesms.loki.JazzIdenticonDrawable
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
// TODO: Look into a better way of handling different sizes. Maybe an enum (with associated values) encapsulating the different modes?
|
||||
|
||||
class ProfilePictureView : RelativeLayout {
|
||||
lateinit var glide: GlideRequests
|
||||
var hexEncodedPublicKey: String? = null
|
||||
var additionalHexEncodedPublicKey: String? = null
|
||||
var isRSSFeed = false
|
||||
var isLarge = false
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
private fun setUpViewHierarchy() {
|
||||
val inflater = context.applicationContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
val contentView = inflater.inflate(R.layout.view_profile_picture, null)
|
||||
addView(contentView)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
fun update() {
|
||||
val hexEncodedPublicKey = hexEncodedPublicKey ?: return
|
||||
val additionalHexEncodedPublicKey = additionalHexEncodedPublicKey
|
||||
doubleModeImageViewContainer.visibility = if (additionalHexEncodedPublicKey != null && !isRSSFeed) View.VISIBLE else View.INVISIBLE
|
||||
singleModeImageViewContainer.visibility = if (additionalHexEncodedPublicKey == null && !isRSSFeed && !isLarge) View.VISIBLE else View.INVISIBLE
|
||||
largeSingleModeImageViewContainer.visibility = if (additionalHexEncodedPublicKey == null && !isRSSFeed && isLarge) View.VISIBLE else View.INVISIBLE
|
||||
rssImageView.visibility = if (isRSSFeed) View.VISIBLE else View.INVISIBLE
|
||||
fun setProfilePictureIfNeeded(imageView: ImageView, hexEncodedPublicKey: String, @DimenRes sizeID: Int) {
|
||||
glide.clear(imageView)
|
||||
if (hexEncodedPublicKey.isNotEmpty()) {
|
||||
val signalProfilePicture = Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false).contactPhoto
|
||||
if (signalProfilePicture != null && (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "0") {
|
||||
glide.load(signalProfilePicture).diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView)
|
||||
} else {
|
||||
val size = resources.getDimensionPixelSize(sizeID)
|
||||
val jazzIcon = JazzIdenticonDrawable(size, size, hexEncodedPublicKey)
|
||||
glide.load(jazzIcon).diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView)
|
||||
}
|
||||
} else {
|
||||
imageView.setImageDrawable(null)
|
||||
}
|
||||
}
|
||||
setProfilePictureIfNeeded(doubleModeImageView1, hexEncodedPublicKey, R.dimen.small_profile_picture_size)
|
||||
setProfilePictureIfNeeded(doubleModeImageView2, additionalHexEncodedPublicKey ?: "", R.dimen.small_profile_picture_size)
|
||||
setProfilePictureIfNeeded(singleModeImageView, hexEncodedPublicKey, R.dimen.medium_profile_picture_size)
|
||||
setProfilePictureIfNeeded(largeSingleModeImageView, hexEncodedPublicKey, R.dimen.large_profile_picture_size)
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.views
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import kotlinx.android.synthetic.main.view_seed_reminder.view.*
|
||||
import network.loki.messenger.R
|
||||
|
||||
class SeedReminderView : FrameLayout {
|
||||
var title: CharSequence
|
||||
get() = titleTextView.text
|
||||
set(value) { titleTextView.text = value }
|
||||
var subtitle: CharSequence
|
||||
get() = subtitleTextView.text
|
||||
set(value) { subtitleTextView.text = value }
|
||||
var delegate: SeedReminderViewDelegate? = null
|
||||
|
||||
constructor(context: Context) : super(context) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
private fun setUpViewHierarchy() {
|
||||
val inflater = context.applicationContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
val contentView = inflater.inflate(R.layout.view_seed_reminder, null)
|
||||
addView(contentView)
|
||||
button.setOnClickListener { delegate?.handleSeedReminderViewContinueButtonTapped() }
|
||||
}
|
||||
|
||||
fun setProgress(progress: Int, isAnimated: Boolean) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
progressBar.setProgress(progress, isAnimated)
|
||||
} else {
|
||||
progressBar.progress = progress
|
||||
}
|
||||
}
|
||||
|
||||
fun hideContinueButton() {
|
||||
button.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
interface SeedReminderViewDelegate {
|
||||
|
||||
fun handleSeedReminderViewContinueButtonTapped()
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package org.thoughtcrime.securesms.loki.redesign.views
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.RelativeLayout
|
||||
import kotlinx.android.synthetic.main.view_separator.view.*
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.loki.getColorWithID
|
||||
import org.thoughtcrime.securesms.loki.toPx
|
||||
|
||||
class SeparatorView : RelativeLayout {
|
||||
|
||||
private val path = Path()
|
||||
|
||||
private val paint: Paint = {
|
||||
val result = Paint()
|
||||
result.style = Paint.Style.STROKE
|
||||
result.color = resources.getColorWithID(R.color.separator, context.theme)
|
||||
result.strokeWidth = toPx(1, resources).toFloat()
|
||||
result.isAntiAlias = true
|
||||
result
|
||||
}()
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
private fun setUpViewHierarchy() {
|
||||
val inflater = context.applicationContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
val contentView = inflater.inflate(R.layout.view_separator, null)
|
||||
val layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
||||
addView(contentView, layoutParams)
|
||||
setWillNotDraw(false)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
override fun onDraw(c: Canvas) {
|
||||
super.onDraw(c)
|
||||
val w = width.toFloat()
|
||||
val h = height.toFloat()
|
||||
val hMargin = toPx(10, resources).toFloat()
|
||||
path.reset()
|
||||
path.moveTo(0.0f, h / 2)
|
||||
path.lineTo(titleTextView.left - hMargin, h / 2)
|
||||
path.addRoundRect(titleTextView.left - hMargin, toPx(1, resources).toFloat(), titleTextView.right + hMargin, h - toPx(1, resources).toFloat(), h / 2, h / 2, Path.Direction.CCW)
|
||||
path.moveTo(titleTextView.right + hMargin, h / 2)
|
||||
path.lineTo(w, h / 2)
|
||||
path.close()
|
||||
c.drawPath(path, paint)
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
Reference in New Issue
Block a user