Implement profile picture editing

This commit is contained in:
Niels Andriesse 2020-01-07 14:51:11 +11:00
parent fd14d66d4f
commit 15b4c6aacc
8 changed files with 398 additions and 261 deletions

View File

@ -197,6 +197,7 @@ dependencies {
} }
implementation "com.jakewharton.rxbinding3:rxbinding:3.1.0" implementation "com.jakewharton.rxbinding3:rxbinding:3.1.0"
implementation "com.github.tbruyelle:rxpermissions:0.10.2" implementation "com.github.tbruyelle:rxpermissions:0.10.2"
implementation "com.github.ybq:Android-SpinKit:1.4.0"
} }
def canonicalVersionCode = 23 def canonicalVersionCode = 23

View File

@ -24,7 +24,7 @@
android:id="@+id/profileButton" android:id="@+id/profileButton"
android:layout_width="@dimen/small_profile_picture_size" android:layout_width="@dimen/small_profile_picture_size"
android:layout_height="@dimen/small_profile_picture_size" android:layout_height="@dimen/small_profile_picture_size"
android:layout_marginLeft="8dp" android:layout_marginLeft="10dp"
android:layout_alignParentLeft="true" android:layout_alignParentLeft="true"
android:layout_centerVertical="true" /> android:layout_centerVertical="true" />

View File

@ -1,15 +1,19 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<ScrollView <RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/default_session_background">
<ScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:scrollbars="none"> android:scrollbars="none">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/default_session_background"
android:orientation="vertical" android:orientation="vertical"
android:gravity="center_horizontal"> android:gravity="center_horizontal">
@ -86,6 +90,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true" android:layout_centerInParent="true"
android:textAlignment="center"
android:paddingTop="12dp" android:paddingTop="12dp"
android:paddingBottom="12dp" android:paddingBottom="12dp"
android:visibility="invisible" android:visibility="invisible"
@ -255,4 +260,24 @@
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>
<RelativeLayout
android:id="@+id/loader"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#A4000000"
android:visibility="gone"
android:alpha="0">
<com.github.ybq.android.spinkit.SpinKitView
style="@style/SpinKitView.Large.ThreeBounce"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_centerInParent="true"
app:SpinKit_Color="@color/text" />
</RelativeLayout>
</RelativeLayout>

View File

@ -8,7 +8,6 @@ import android.support.annotation.Nullable;
import org.thoughtcrime.securesms.database.Address; import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.util.Conversions;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -17,7 +16,7 @@ import java.security.MessageDigest;
public class ProfileContactPhoto implements ContactPhoto { public class ProfileContactPhoto implements ContactPhoto {
private final @NonNull Address address; private final @NonNull Address address;
private final @NonNull String avatarObject; public final @NonNull String avatarObject;
public ProfileContactPhoto(@NonNull Address address, @NonNull String avatarObject) { public ProfileContactPhoto(@NonNull Address address, @NonNull String avatarObject) {
this.address = address; this.address = address;

View File

@ -91,14 +91,14 @@ class RegisterActivity : BaseActionBarActivity() {
val hexEncodedPublicKey = keyPair!!.hexEncodedPublicKey val hexEncodedPublicKey = keyPair!!.hexEncodedPublicKey
val characterCount = hexEncodedPublicKey.count() val characterCount = hexEncodedPublicKey.count()
var count = 0 var count = 0
val limit = 40 val limit = 32
fun animate() { fun animate() {
val numberOfIndexesToShuffle = (0 until (40 - count)).random() val numberOfIndexesToShuffle = 32 - count
val indexesToShuffle = (0 until characterCount).shuffled().subList(0, numberOfIndexesToShuffle) val indexesToShuffle = (0 until characterCount).shuffled().subList(0, numberOfIndexesToShuffle)
var mangledHexEncodedPublicKey = hexEncodedPublicKey var mangledHexEncodedPublicKey = hexEncodedPublicKey
for (index in indexesToShuffle) { for (index in indexesToShuffle) {
try { try {
mangledHexEncodedPublicKey = mangledHexEncodedPublicKey.substring(0, index) + "0123456789abcdef________________".random() + mangledHexEncodedPublicKey.substring(index + 1, mangledHexEncodedPublicKey.count()) mangledHexEncodedPublicKey = mangledHexEncodedPublicKey.substring(0, index) + "0123456789abcdef__".random() + mangledHexEncodedPublicKey.substring(index + 1, mangledHexEncodedPublicKey.count())
} catch (exception: Exception) { } catch (exception: Exception) {
// Do nothing // Do nothing
} }
@ -108,7 +108,7 @@ class RegisterActivity : BaseActionBarActivity() {
publicKeyTextView.text = mangledHexEncodedPublicKey publicKeyTextView.text = mangledHexEncodedPublicKey
Handler().postDelayed({ Handler().postDelayed({
animate() animate()
}, 40) }, 32)
} else { } else {
publicKeyTextView.text = hexEncodedPublicKey publicKeyTextView.text = hexEncodedPublicKey
} }

View File

@ -1,30 +1,56 @@
package org.thoughtcrime.securesms.loki.redesign.activities 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.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.AsyncTask
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View import android.view.View
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.Toast import android.widget.Toast
import kotlinx.android.synthetic.main.activity_settings.* import kotlinx.android.synthetic.main.activity_settings.*
import network.loki.messenger.R 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.ApplicationContext
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity 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.database.DatabaseFactory
import org.thoughtcrime.securesms.loki.redesign.utilities.push import org.thoughtcrime.securesms.loki.redesign.utilities.push
import org.thoughtcrime.securesms.loki.toPx import org.thoughtcrime.securesms.loki.toPx
import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests 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.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() { class SettingsActivity : PassphraseRequiredActionBarActivity() {
private lateinit var glide: GlideRequests private lateinit var glide: GlideRequests
private var isEditingDisplayName = false private var isEditingDisplayName = false
set(value) { field = value; handleIsEditingDisplayNameChanged() } set(value) { field = value; handleIsEditingDisplayNameChanged() }
private var displayNameToBeUploaded: String? = null private var displayNameToBeUploaded: String? = null
private var profilePictureToBeUploaded: ByteArray? = null
private var tempFile: File? = null
private val hexEncodedPublicKey: String private val hexEncodedPublicKey: String
get() { get() {
@ -50,6 +76,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
profilePictureView.hexEncodedPublicKey = hexEncodedPublicKey profilePictureView.hexEncodedPublicKey = hexEncodedPublicKey
profilePictureView.isLarge = true profilePictureView.isLarge = true
profilePictureView.update() profilePictureView.update()
profilePictureView.setOnClickListener { showEditProfilePictureUI() }
// Set up display name container // Set up display name container
displayNameContainer.setOnClickListener { showEditDisplayNameUI() } displayNameContainer.setOnClickListener { showEditDisplayNameUI() }
// Set up display name text view // Set up display name text view
@ -61,6 +88,34 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
// Set up share button // Set up share button
shareButton.setOnClickListener { sharePublicKey() } shareButton.setOnClickListener { sharePublicKey() }
} }
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 // endregion
// region Updating // region Updating
@ -82,18 +137,62 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
} }
} }
private fun updateProfile(isUpdatingDisplayName: Boolean, isUpdatingProfilePicture: Boolean) { private fun updateProfile(isUpdatingProfilePicture: Boolean) {
val displayName = displayNameToBeUploaded ?: TextSecurePreferences.getProfileName(this) showLoader()
TextSecurePreferences.setProfileName(this, displayName) val promises = mutableListOf<Promise<*, Exception>>()
val displayName = displayNameToBeUploaded
if (displayName != null) {
val publicChatAPI = ApplicationContext.getInstance(this).lokiPublicChatAPI val publicChatAPI = ApplicationContext.getInstance(this).lokiPublicChatAPI
if (publicChatAPI != null) { if (publicChatAPI != null) {
val servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers() val servers = DatabaseFactory.getLokiThreadDatabase(this).getAllPublicChatServers()
for (server in servers) { promises.addAll(servers.map { publicChatAPI.setDisplayName(displayName, it) })
publicChatAPI.setDisplayName(displayName, server)
} }
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 displayNameTextView.text = displayName
}
displayNameToBeUploaded = null 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 // endregion
@ -103,11 +202,19 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
} }
private fun saveDisplayName() { private fun saveDisplayName() {
val displayName = displayNameEditText.text.trim().toString() val displayName = displayNameEditText.text.toString().trim()
// TODO: Validation 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 isEditingDisplayName = false
displayNameToBeUploaded = displayName displayNameToBeUploaded = displayName
updateProfile(true, false) updateProfile(false)
} }
private fun showQRCode() { private fun showQRCode() {
@ -115,6 +222,10 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
push(intent) push(intent)
} }
private fun showEditProfilePictureUI() {
tempFile = AvatarSelection.startAvatarSelection(this, false, true)
}
private fun showEditDisplayNameUI() { private fun showEditDisplayNameUI() {
isEditingDisplayName = true isEditingDisplayName = true
} }

View File

@ -10,6 +10,7 @@ import android.widget.RelativeLayout
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import kotlinx.android.synthetic.main.view_profile_picture.view.* import kotlinx.android.synthetic.main.view_profile_picture.view.*
import network.loki.messenger.R import network.loki.messenger.R
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto
import org.thoughtcrime.securesms.database.Address import org.thoughtcrime.securesms.database.Address
import org.thoughtcrime.securesms.loki.JazzIdenticonDrawable import org.thoughtcrime.securesms.loki.JazzIdenticonDrawable
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
@ -60,7 +61,7 @@ class ProfilePictureView : RelativeLayout {
glide.clear(imageView) glide.clear(imageView)
if (hexEncodedPublicKey.isNotEmpty()) { if (hexEncodedPublicKey.isNotEmpty()) {
val signalProfilePicture = Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false).contactPhoto val signalProfilePicture = Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false).contactPhoto
if (signalProfilePicture != null) { if (signalProfilePicture != null && (signalProfilePicture as? ProfileContactPhoto)?.avatarObject != "0") {
glide.load(signalProfilePicture).diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView) glide.load(signalProfilePicture).diskCacheStrategy(DiskCacheStrategy.ALL).circleCrop().into(imageView)
} else { } else {
val size = resources.getDimensionPixelSize(sizeID) val size = resources.getDimensionPixelSize(sizeID)

View File

@ -475,7 +475,7 @@ public class Recipient implements RecipientModifiedListener {
} }
public synchronized @Nullable ContactPhoto getContactPhoto() { public synchronized @Nullable ContactPhoto getContactPhoto() {
if (isLocalNumber && profileAvatar != null) return new ProfileContactPhoto(address, String.valueOf(TextSecurePreferences.getProfileAvatarId(context))); if (isLocalNumber) return new ProfileContactPhoto(address, String.valueOf(TextSecurePreferences.getProfileAvatarId(context)));
else if (isGroupRecipient() && groupAvatarId != null) return new GroupRecordContactPhoto(address, groupAvatarId); else if (isGroupRecipient() && groupAvatarId != null) return new GroupRecordContactPhoto(address, groupAvatarId);
else if (systemContactPhoto != null) return new SystemContactPhoto(address, systemContactPhoto, 0); else if (systemContactPhoto != null) return new SystemContactPhoto(address, systemContactPhoto, 0);
else if (profileAvatar != null) return new ProfileContactPhoto(address, profileAvatar); else if (profileAvatar != null) return new ProfileContactPhoto(address, profileAvatar);