2020-05-11 08:19:26 +02:00
|
|
|
package org.thoughtcrime.securesms.loki.activities
|
2020-01-06 04:26:52 +01:00
|
|
|
|
2020-01-07 04:51:11 +01:00
|
|
|
import android.app.Activity
|
2020-01-06 04:26:52 +01:00
|
|
|
import android.content.ClipData
|
|
|
|
import android.content.ClipboardManager
|
|
|
|
import android.content.Context
|
|
|
|
import android.content.Intent
|
2020-08-19 03:48:16 +02:00
|
|
|
import android.content.res.Configuration
|
2020-01-07 04:51:11 +01:00
|
|
|
import android.net.Uri
|
2020-08-26 15:12:01 +02:00
|
|
|
import android.os.*
|
2020-08-20 09:48:41 +02:00
|
|
|
import android.view.ActionMode
|
|
|
|
import android.view.Menu
|
|
|
|
import android.view.MenuItem
|
2020-01-07 02:00:30 +01:00
|
|
|
import android.view.View
|
|
|
|
import android.view.inputmethod.InputMethodManager
|
2020-01-06 04:26:52 +01:00
|
|
|
import android.widget.Toast
|
2020-08-19 05:38:49 +02:00
|
|
|
import androidx.appcompat.app.AppCompatDelegate
|
2020-01-06 04:26:52 +01:00
|
|
|
import kotlinx.android.synthetic.main.activity_settings.*
|
2020-02-25 04:04:15 +01:00
|
|
|
import network.loki.messenger.BuildConfig
|
2020-01-06 04:26:52 +01:00
|
|
|
import network.loki.messenger.R
|
2020-01-07 04:51:11 +01:00
|
|
|
import nl.komponents.kovenant.Promise
|
|
|
|
import nl.komponents.kovenant.all
|
|
|
|
import nl.komponents.kovenant.deferred
|
|
|
|
import nl.komponents.kovenant.ui.alwaysUi
|
2020-01-07 02:00:30 +01:00
|
|
|
import org.thoughtcrime.securesms.ApplicationContext
|
2020-01-06 04:26:52 +01:00
|
|
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
2020-01-07 04:51:11 +01:00
|
|
|
import org.thoughtcrime.securesms.avatar.AvatarSelection
|
|
|
|
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
|
|
|
import org.thoughtcrime.securesms.database.Address
|
2020-01-06 04:26:52 +01:00
|
|
|
import org.thoughtcrime.securesms.database.DatabaseFactory
|
2020-05-11 08:19:26 +02:00
|
|
|
import org.thoughtcrime.securesms.loki.dialogs.ClearAllDataDialog
|
|
|
|
import org.thoughtcrime.securesms.loki.dialogs.SeedDialog
|
2020-05-29 03:16:52 +02:00
|
|
|
import org.thoughtcrime.securesms.loki.utilities.fadeIn
|
|
|
|
import org.thoughtcrime.securesms.loki.utilities.fadeOut
|
2020-05-11 08:19:26 +02:00
|
|
|
import org.thoughtcrime.securesms.loki.utilities.push
|
2020-01-06 04:26:52 +01:00
|
|
|
import org.thoughtcrime.securesms.mms.GlideApp
|
|
|
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
2020-01-07 04:51:11 +01:00
|
|
|
import org.thoughtcrime.securesms.profiles.AvatarHelper
|
|
|
|
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints
|
|
|
|
import org.thoughtcrime.securesms.util.BitmapDecodingException
|
|
|
|
import org.thoughtcrime.securesms.util.BitmapUtil
|
2020-01-06 04:26:52 +01:00
|
|
|
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
2020-01-07 04:51:11 +01:00
|
|
|
import org.whispersystems.signalservice.api.crypto.ProfileCipher
|
|
|
|
import org.whispersystems.signalservice.api.util.StreamDetails
|
2020-07-15 04:24:43 +02:00
|
|
|
import org.whispersystems.signalservice.loki.api.fileserver.FileServerAPI
|
2020-01-07 04:51:11 +01:00
|
|
|
import java.io.ByteArrayInputStream
|
|
|
|
import java.io.File
|
|
|
|
import java.security.SecureRandom
|
2020-02-24 04:57:51 +01:00
|
|
|
import java.util.*
|
2020-01-06 04:26:52 +01:00
|
|
|
|
|
|
|
class SettingsActivity : PassphraseRequiredActionBarActivity() {
|
2020-08-20 09:48:41 +02:00
|
|
|
|
|
|
|
private var displayNameEditActionMode: ActionMode? = null
|
|
|
|
set(value) { field = value; handleDisplayNameEditActionModeChanged() }
|
|
|
|
|
2020-01-06 04:26:52 +01:00
|
|
|
private lateinit var glide: GlideRequests
|
2020-01-07 02:00:30 +01:00
|
|
|
private var displayNameToBeUploaded: String? = null
|
2020-01-07 04:51:11 +01:00
|
|
|
private var profilePictureToBeUploaded: ByteArray? = null
|
|
|
|
private var tempFile: File? = null
|
2020-01-06 04:26:52 +01:00
|
|
|
|
|
|
|
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)
|
2020-08-19 03:48:16 +02:00
|
|
|
|
2020-01-06 04:26:52 +01:00
|
|
|
setContentView(R.layout.activity_settings)
|
2020-08-20 09:48:41 +02:00
|
|
|
|
2020-01-06 04:26:52 +01:00
|
|
|
glide = GlideApp.with(this)
|
|
|
|
profilePictureView.glide = glide
|
2020-07-15 06:26:20 +02:00
|
|
|
profilePictureView.publicKey = hexEncodedPublicKey
|
2020-01-06 04:26:52 +01:00
|
|
|
profilePictureView.isLarge = true
|
|
|
|
profilePictureView.update()
|
2020-01-07 04:51:11 +01:00
|
|
|
profilePictureView.setOnClickListener { showEditProfilePictureUI() }
|
2020-08-20 09:48:41 +02:00
|
|
|
ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) }
|
2020-08-14 11:26:56 +02:00
|
|
|
btnGroupNameDisplay.text = DatabaseFactory.getLokiUserDatabase(this).getDisplayName(hexEncodedPublicKey)
|
2020-01-06 04:26:52 +01:00
|
|
|
publicKeyTextView.text = hexEncodedPublicKey
|
|
|
|
copyButton.setOnClickListener { copyPublicKey() }
|
|
|
|
shareButton.setOnClickListener { sharePublicKey() }
|
2020-01-08 05:16:34 +01:00
|
|
|
val isMasterDevice = (TextSecurePreferences.getMasterHexEncodedPublicKey(this) == null)
|
2020-07-30 08:53:34 +02:00
|
|
|
linkedDevicesButtonTopSeparator.visibility = View.GONE
|
|
|
|
linkedDevicesButton.visibility = View.GONE
|
2020-01-08 05:16:34 +01:00
|
|
|
if (!isMasterDevice) {
|
|
|
|
seedButtonTopSeparator.visibility = View.GONE
|
|
|
|
seedButton.visibility = View.GONE
|
|
|
|
}
|
2020-01-09 01:35:43 +01:00
|
|
|
privacyButton.setOnClickListener { showPrivacySettings() }
|
2020-01-09 04:15:43 +01:00
|
|
|
notificationsButton.setOnClickListener { showNotificationSettings() }
|
|
|
|
chatsButton.setOnClickListener { showChatSettings() }
|
2020-07-30 08:53:34 +02:00
|
|
|
// linkedDevicesButton.setOnClickListener { showLinkedDevices() }
|
2020-01-07 05:59:18 +01:00
|
|
|
seedButton.setOnClickListener { showSeed() }
|
2020-01-07 06:11:02 +01:00
|
|
|
clearAllDataButton.setOnClickListener { clearAllData() }
|
2020-03-05 04:24:10 +01:00
|
|
|
versionTextView.text = String.format(getString(R.string.version_s), BuildConfig.VERSION_NAME)
|
2020-08-19 03:48:16 +02:00
|
|
|
}
|
|
|
|
|
2020-08-20 09:48:41 +02:00
|
|
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
|
|
|
menuInflater.inflate(R.menu.settings_general, menu)
|
2020-08-26 15:12:01 +02:00
|
|
|
|
2020-08-27 04:30:27 +02:00
|
|
|
// // Day/night themes are only available since Android 10
|
|
|
|
// menu.findItem(R.id.action_change_theme)
|
|
|
|
// .setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
|
2020-08-26 15:12:01 +02:00
|
|
|
|
2020-08-20 09:48:41 +02:00
|
|
|
return true
|
2020-08-19 03:48:16 +02:00
|
|
|
}
|
|
|
|
|
2020-08-20 09:48:41 +02:00
|
|
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
|
|
return when (item.itemId) {
|
|
|
|
R.id.action_qr_code -> {
|
|
|
|
showQRCode()
|
2020-08-26 15:12:01 +02:00
|
|
|
true
|
|
|
|
}
|
|
|
|
R.id.action_change_theme -> {
|
|
|
|
// A temporary demo code that manually switches between day/night themes.
|
|
|
|
// The effect is reset after the app restart.
|
|
|
|
val currentUiMode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
|
|
|
val nightMode: Int = when(currentUiMode) {
|
|
|
|
Configuration.UI_MODE_NIGHT_YES -> AppCompatDelegate.MODE_NIGHT_NO
|
|
|
|
else -> AppCompatDelegate.MODE_NIGHT_YES
|
|
|
|
}
|
|
|
|
AppCompatDelegate.setDefaultNightMode(nightMode)
|
|
|
|
recreate()
|
|
|
|
true
|
2020-08-20 09:48:41 +02:00
|
|
|
}
|
|
|
|
else -> super.onOptionsItemSelected(item)
|
|
|
|
}
|
2020-01-06 04:26:52 +01:00
|
|
|
}
|
2020-01-07 04:51:11 +01:00
|
|
|
|
2020-08-20 09:48:41 +02:00
|
|
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
2020-01-07 04:51:11 +01:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-01-07 02:00:30 +01:00
|
|
|
// endregion
|
2020-01-06 04:26:52 +01:00
|
|
|
|
2020-01-07 02:00:30 +01:00
|
|
|
// region Updating
|
2020-08-20 09:48:41 +02:00
|
|
|
private fun handleDisplayNameEditActionModeChanged() {
|
|
|
|
val isEditingDisplayName = this.displayNameEditActionMode !== null
|
|
|
|
|
2020-08-14 11:26:56 +02:00
|
|
|
btnGroupNameDisplay.visibility = if (isEditingDisplayName) View.INVISIBLE else View.VISIBLE
|
2020-01-07 02:00:30 +01:00
|
|
|
displayNameEditText.visibility = if (isEditingDisplayName) View.VISIBLE else View.INVISIBLE
|
2020-08-20 09:48:41 +02:00
|
|
|
|
2020-01-07 02:00:30 +01:00
|
|
|
val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
|
|
|
if (isEditingDisplayName) {
|
2020-08-20 09:48:41 +02:00
|
|
|
displayNameEditText.setText(btnGroupNameDisplay.text)
|
|
|
|
displayNameEditText.selectAll()
|
2020-01-07 02:00:30 +01:00
|
|
|
displayNameEditText.requestFocus()
|
|
|
|
inputMethodManager.showSoftInput(displayNameEditText, 0)
|
|
|
|
} else {
|
|
|
|
inputMethodManager.hideSoftInputFromWindow(displayNameEditText.windowToken, 0)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-07 04:51:11 +01:00
|
|
|
private fun updateProfile(isUpdatingProfilePicture: Boolean) {
|
2020-05-29 03:16:52 +02:00
|
|
|
loader.fadeIn()
|
2020-01-07 04:51:11 +01:00
|
|
|
val promises = mutableListOf<Promise<*, Exception>>()
|
|
|
|
val displayName = displayNameToBeUploaded
|
|
|
|
if (displayName != null) {
|
2020-07-15 04:24:43 +02:00
|
|
|
val publicChatAPI = ApplicationContext.getInstance(this).publicChatAPI
|
2020-01-07 04:51:11 +01:00
|
|
|
if (publicChatAPI != null) {
|
|
|
|
val servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers()
|
|
|
|
promises.addAll(servers.map { publicChatAPI.setDisplayName(displayName, it) })
|
2020-01-07 02:00:30 +01:00
|
|
|
}
|
2020-01-07 04:51:11 +01:00
|
|
|
TextSecurePreferences.setProfileName(this, displayName)
|
|
|
|
}
|
|
|
|
val profilePicture = profilePictureToBeUploaded
|
|
|
|
val encodedProfileKey = ProfileKeyUtil.generateEncodedProfileKey(this)
|
|
|
|
val profileKey = ProfileKeyUtil.getProfileKeyFromEncodedString(encodedProfileKey)
|
|
|
|
if (isUpdatingProfilePicture && profilePicture != null) {
|
2020-07-15 04:24:43 +02:00
|
|
|
val storageAPI = FileServerAPI.shared
|
2020-01-07 04:51:11 +01:00
|
|
|
val deferred = deferred<Unit, Exception>()
|
|
|
|
AsyncTask.execute {
|
|
|
|
val stream = StreamDetails(ByteArrayInputStream(profilePicture), "image/jpeg", profilePicture.size.toLong())
|
2020-02-24 04:57:51 +01:00
|
|
|
val (_, url) = storageAPI.uploadProfilePicture(storageAPI.server, profileKey, stream) {
|
|
|
|
TextSecurePreferences.setLastProfilePictureUpload(this@SettingsActivity, Date().time)
|
|
|
|
}
|
2020-05-11 08:19:26 +02:00
|
|
|
TextSecurePreferences.setProfilePictureURL(this, url)
|
2020-01-07 04:51:11 +01:00
|
|
|
deferred.resolve(Unit)
|
|
|
|
}
|
|
|
|
promises.add(deferred.promise)
|
|
|
|
}
|
|
|
|
all(promises).alwaysUi {
|
|
|
|
if (displayName != null) {
|
2020-08-14 11:26:56 +02:00
|
|
|
btnGroupNameDisplay.text = displayName
|
2020-01-07 04:51:11 +01:00
|
|
|
}
|
|
|
|
displayNameToBeUploaded = null
|
|
|
|
if (isUpdatingProfilePicture && profilePicture != null) {
|
|
|
|
AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)), profilePicture)
|
|
|
|
TextSecurePreferences.setProfileAvatarId(this, SecureRandom().nextInt())
|
|
|
|
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
|
2020-05-14 01:35:34 +02:00
|
|
|
ApplicationContext.getInstance(this).updateOpenGroupProfilePicturesIfNeeded()
|
2020-01-07 04:51:11 +01:00
|
|
|
profilePictureView.update()
|
|
|
|
}
|
|
|
|
profilePictureToBeUploaded = null
|
2020-05-29 03:16:52 +02:00
|
|
|
loader.fadeOut()
|
2020-01-07 02:00:30 +01:00
|
|
|
}
|
2020-01-07 04:51:11 +01:00
|
|
|
}
|
2020-01-06 04:26:52 +01:00
|
|
|
// endregion
|
|
|
|
|
|
|
|
// region Interaction
|
2020-01-07 02:00:30 +01:00
|
|
|
|
2020-08-20 09:48:41 +02:00
|
|
|
/**
|
|
|
|
* @return true if the update was successful.
|
|
|
|
*/
|
|
|
|
private fun saveDisplayName(): Boolean {
|
2020-01-07 04:51:11 +01:00
|
|
|
val displayName = displayNameEditText.text.toString().trim()
|
|
|
|
if (displayName.isEmpty()) {
|
2020-08-20 09:48:41 +02:00
|
|
|
Toast.makeText(this, R.string.activity_settings_display_name_missing_error, Toast.LENGTH_SHORT).show()
|
|
|
|
return false
|
2020-01-07 04:51:11 +01:00
|
|
|
}
|
|
|
|
if (displayName.toByteArray().size > ProfileCipher.NAME_PADDED_LENGTH) {
|
2020-08-20 09:48:41 +02:00
|
|
|
Toast.makeText(this, R.string.activity_settings_display_name_too_long_error, Toast.LENGTH_SHORT).show()
|
|
|
|
return false
|
2020-01-07 04:51:11 +01:00
|
|
|
}
|
2020-08-20 09:48:41 +02:00
|
|
|
// isEditingDisplayName = false
|
2020-01-07 02:00:30 +01:00
|
|
|
displayNameToBeUploaded = displayName
|
2020-01-07 04:51:11 +01:00
|
|
|
updateProfile(false)
|
2020-08-20 09:48:41 +02:00
|
|
|
return true
|
2020-01-06 04:26:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun showQRCode() {
|
2020-01-06 06:05:57 +01:00
|
|
|
val intent = Intent(this, QRCodeActivity::class.java)
|
|
|
|
push(intent)
|
2020-01-06 04:26:52 +01:00
|
|
|
}
|
|
|
|
|
2020-01-07 04:51:11 +01:00
|
|
|
private fun showEditProfilePictureUI() {
|
|
|
|
tempFile = AvatarSelection.startAvatarSelection(this, false, true)
|
|
|
|
}
|
|
|
|
|
2020-01-06 04:26:52 +01:00
|
|
|
private fun copyPublicKey() {
|
|
|
|
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
|
|
val clip = ClipData.newPlainText("Session ID", hexEncodedPublicKey)
|
2020-08-19 02:06:26 +02:00
|
|
|
clipboard.setPrimaryClip(clip)
|
2020-05-25 08:38:36 +02:00
|
|
|
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
2020-01-06 04:26:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun sharePublicKey() {
|
|
|
|
val intent = Intent()
|
|
|
|
intent.action = Intent.ACTION_SEND
|
|
|
|
intent.putExtra(Intent.EXTRA_TEXT, hexEncodedPublicKey)
|
|
|
|
intent.type = "text/plain"
|
|
|
|
startActivity(intent)
|
|
|
|
}
|
2020-01-07 05:59:18 +01:00
|
|
|
|
2020-01-09 01:35:43 +01:00
|
|
|
private fun showPrivacySettings() {
|
2020-01-09 04:15:43 +01:00
|
|
|
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)
|
2020-01-09 01:35:43 +01:00
|
|
|
push(intent)
|
|
|
|
}
|
|
|
|
|
2020-01-09 05:52:40 +01:00
|
|
|
private fun showLinkedDevices() {
|
|
|
|
val intent = Intent(this, LinkedDevicesActivity::class.java)
|
|
|
|
push(intent)
|
|
|
|
}
|
|
|
|
|
2020-01-07 05:59:18 +01:00
|
|
|
private fun showSeed() {
|
2020-01-08 05:16:34 +01:00
|
|
|
SeedDialog().show(supportFragmentManager, "Recovery Phrase Dialog")
|
2020-01-07 05:59:18 +01:00
|
|
|
}
|
2020-01-07 06:11:02 +01:00
|
|
|
|
|
|
|
private fun clearAllData() {
|
|
|
|
ClearAllDataDialog().show(supportFragmentManager, "Clear All Data Dialog")
|
|
|
|
}
|
2020-01-06 04:26:52 +01:00
|
|
|
// endregion
|
2020-08-20 09:48:41 +02:00
|
|
|
|
|
|
|
private inner class DisplayNameEditActionModeCallback: ActionMode.Callback {
|
|
|
|
|
|
|
|
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
|
|
|
mode.title = getString(R.string.activity_settings_display_name_edit_text_hint)
|
|
|
|
mode.menuInflater.inflate(R.menu.menu_apply, menu)
|
|
|
|
this@SettingsActivity.displayNameEditActionMode = mode
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onDestroyActionMode(mode: ActionMode) {
|
|
|
|
this@SettingsActivity.displayNameEditActionMode = null
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
|
|
|
when (item.itemId) {
|
|
|
|
R.id.applyButton -> {
|
|
|
|
if (this@SettingsActivity.saveDisplayName()) {
|
|
|
|
mode.finish()
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
2020-01-06 04:26:52 +01:00
|
|
|
}
|