Implement profile picture editing

pull/58/head
Niels Andriesse 5 years ago
parent fd14d66d4f
commit 15b4c6aacc

@ -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

@ -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" />

@ -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"
@ -256,3 +261,23 @@
</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>

@ -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;

@ -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
} }

@ -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
} }

@ -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)

@ -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);

Loading…
Cancel
Save