session-android/app/src/main/java/org/thoughtcrime/securesms/loki/activities/SettingsActivity.kt

338 lines
14 KiB
Kotlin
Raw Normal View History

2020-05-11 08:19:26 +02:00
package org.thoughtcrime.securesms.loki.activities
2020-01-06 04:26:52 +01:00
import android.Manifest
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-01-07 04:51:11 +01:00
import android.net.Uri
2020-08-27 06:38:41 +02:00
import android.os.AsyncTask
import android.os.Bundle
import android.os.Handler
import android.os.Looper
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
import kotlinx.android.synthetic.main.activity_settings.*
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.session.libsession.utilities.preferences.ProfileKeyUtil
2021-01-13 07:11:30 +01:00
import org.session.libsession.messaging.threads.Address
2020-01-06 04:26:52 +01:00
import org.thoughtcrime.securesms.database.DatabaseFactory
2020-08-27 06:38:25 +02:00
import org.thoughtcrime.securesms.loki.dialogs.ChangeUiModeDialog
2020-05-11 08:19:26 +02:00
import org.thoughtcrime.securesms.loki.dialogs.ClearAllDataDialog
import org.thoughtcrime.securesms.loki.dialogs.SeedDialog
2020-09-02 13:09:38 +02:00
import org.thoughtcrime.securesms.loki.utilities.UiModeUtilities
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
import org.thoughtcrime.securesms.permissions.Permissions
2021-02-02 05:40:43 +01:00
import org.session.libsession.messaging.avatars.AvatarHelper
2020-01-07 04:51:11 +01:00
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints
import org.thoughtcrime.securesms.util.BitmapDecodingException
import org.thoughtcrime.securesms.util.BitmapUtil
2021-01-15 06:51:53 +01:00
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.service.api.crypto.ProfileCipher
import org.session.libsignal.service.api.util.StreamDetails
import org.session.libsignal.service.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() {
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)
2021-01-21 05:42:43 +01:00
val userHexEncodedPublicKey = TextSecurePreferences.getLocalNumber(this)!!
2020-01-06 04:26:52 +01:00
return masterHexEncodedPublicKey ?: userHexEncodedPublicKey
}
// region Lifecycle
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
super.onCreate(savedInstanceState, isReady)
setContentView(R.layout.activity_settings)
2020-09-09 05:57:22 +02:00
val displayName = DatabaseFactory.getLokiUserDatabase(this).getDisplayName(hexEncodedPublicKey)
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-09-09 05:57:22 +02:00
profilePictureView.displayName = displayName
2020-01-06 04:26:52 +01:00
profilePictureView.isLarge = true
profilePictureView.update()
2020-01-07 04:51:11 +01:00
profilePictureView.setOnClickListener { showEditProfilePictureUI() }
ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) }
2020-09-09 05:57:22 +02:00
btnGroupNameDisplay.text = displayName
2020-01-06 04:26:52 +01:00
publicKeyTextView.text = hexEncodedPublicKey
copyButton.setOnClickListener { copyPublicKey() }
shareButton.setOnClickListener { sharePublicKey() }
val isMasterDevice = (TextSecurePreferences.getMasterHexEncodedPublicKey(this) == null)
2020-07-30 08:53:34 +02:00
linkedDevicesButtonTopSeparator.visibility = View.GONE
linkedDevicesButton.visibility = View.GONE
if (!isMasterDevice) {
seedButtonTopSeparator.visibility = View.GONE
seedButton.visibility = View.GONE
}
privacyButton.setOnClickListener { showPrivacySettings() }
notificationsButton.setOnClickListener { showNotificationSettings() }
chatsButton.setOnClickListener { showChatSettings() }
2020-07-30 08:53:34 +02:00
// linkedDevicesButton.setOnClickListener { showLinkedDevices() }
2020-11-02 04:54:10 +01:00
sendInvitationButton.setOnClickListener { sendInvitation() }
2020-01-07 05:59:18 +01:00
seedButton.setOnClickListener { showSeed() }
clearAllDataButton.setOnClickListener { clearAllData() }
2020-09-01 08:27:10 +02:00
versionTextView.text = String.format(getString(R.string.version_s), "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
2020-08-19 03:48:16 +02:00
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.settings_general, menu)
2020-09-09 05:57:22 +02:00
// Update UI mode menu icon
2020-09-02 13:09:38 +02:00
val uiMode = UiModeUtilities.getUserSelectedUiMode(this)
menu.findItem(R.id.action_change_theme).icon!!.level = uiMode.ordinal
return true
2020-08-19 03:48:16 +02:00
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_qr_code -> {
showQRCode()
true
}
R.id.action_change_theme -> {
2020-08-27 06:38:25 +02:00
ChangeUiModeDialog().show(supportFragmentManager, ChangeUiModeDialog.TAG)
true
}
else -> super.onOptionsItemSelected(item)
}
2020-01-06 04:26:52 +01:00
}
2020-01-07 04:51:11 +01: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()
}
}
}
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
}
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
private fun handleDisplayNameEditActionModeChanged() {
val isEditingDisplayName = this.displayNameEditActionMode !== null
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-01-07 02:00:30 +01:00
val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
if (isEditingDisplayName) {
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) {
btnGroupNameDisplay.text = displayName
2020-01-07 04:51:11 +01:00
}
displayNameToBeUploaded = null
if (isUpdatingProfilePicture && profilePicture != null) {
2021-01-21 05:42:43 +01:00
AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture)
2020-01-07 04:51:11 +01:00
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
/**
* @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()) {
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) {
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
}
// isEditingDisplayName = false
2020-01-07 02:00:30 +01:00
displayNameToBeUploaded = displayName
2020-01-07 04:51:11 +01:00
updateProfile(false)
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() {
// Ask for an optional camera permission.
Permissions.with(this)
.request(Manifest.permission.CAMERA)
.onAnyResult {
tempFile = AvatarSelection.startAvatarSelection(this, false, true)
}
.execute()
2020-01-07 04:51:11 +01:00
}
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)
clipboard.setPrimaryClip(clip)
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
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)
}
2020-11-02 04:54:10 +01:00
private fun sendInvitation() {
val intent = Intent()
intent.action = Intent.ACTION_SEND
val invitation = "Hey, I've been using Session to chat with complete privacy and security. Come join me! Download it at https://getsession.org/. My Session ID is $hexEncodedPublicKey!"
intent.putExtra(Intent.EXTRA_TEXT, invitation)
intent.type = "text/plain"
startActivity(intent)
}
2020-01-07 05:59:18 +01:00
private fun showSeed() {
SeedDialog().show(supportFragmentManager, "Recovery Phrase Dialog")
2020-01-07 05:59:18 +01:00
}
private fun clearAllData() {
ClearAllDataDialog().show(supportFragmentManager, "Clear All Data Dialog")
}
2020-01-06 04:26:52 +01:00
// endregion
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
}