diff --git a/jami-android/app/src/main/java/cx/ring/account/JamiAccountSummaryFragment.kt b/jami-android/app/src/main/java/cx/ring/account/JamiAccountSummaryFragment.kt index 84c3bf33f98088c1a35ba5933c3d25e049f4ffa5..c41c33d0f155195b29ed098706c6f61d2d6d99b5 100644 --- a/jami-android/app/src/main/java/cx/ring/account/JamiAccountSummaryFragment.kt +++ b/jami-android/app/src/main/java/cx/ring/account/JamiAccountSummaryFragment.kt @@ -25,7 +25,7 @@ import android.content.DialogInterface import android.content.Intent import android.content.pm.PackageManager import android.graphics.Bitmap -import android.net.Uri +import net.jami.model.Uri import android.os.Bundle import android.provider.MediaStore import android.util.Log @@ -47,6 +47,7 @@ import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentTransaction import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.snackbar.Snackbar import cx.ring.R import cx.ring.account.AccountPasswordDialog.UnlockAccountListener @@ -76,6 +77,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers import net.jami.account.JamiAccountSummaryPresenter import net.jami.account.JamiAccountSummaryView import net.jami.model.Account +import net.jami.model.Contact import net.jami.model.Profile import net.jami.services.AccountService import java.io.File @@ -106,8 +108,9 @@ class JamiAccountSummaryFragment : private var mAccount: Account? = null private var mCacheArchive: File? = null private var mProfilePhoto: ImageView? = null + private var mDialogRemovePhoto: FloatingActionButton? = null private var mSourcePhoto: Bitmap? = null - private var tmpProfilePhotoUri: Uri? = null + private var tmpProfilePhotoUri: android.net.Uri? = null private var mDeviceAdapter: DeviceAdapter? = null private val mDisposableBag = CompositeDisposable() private var mBinding: FragAccSummaryBinding? = null @@ -135,7 +138,7 @@ class JamiAccountSummaryFragment : private val exportBackupLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(StartActivityForResult()) { result -> if (result.resultCode != Activity.RESULT_OK) return@registerForActivityResult - result.data?.data?.let { uri: Uri -> + result.data?.data?.let { uri: android.net.Uri -> mCacheArchive?.let { cacheArchive -> AndroidFileUtils.moveToUri(requireContext().contentResolver, cacheArchive, uri) .observeOn(DeviceUtils.uiScheduler) @@ -309,8 +312,23 @@ class JamiAccountSummaryFragment : val view = DialogProfileBinding.inflate(inflater).apply { camera.setOnClickListener { presenter.cameraClicked() } gallery.setOnClickListener { presenter.galleryClicked() } + removePhoto.setOnClickListener { + removePhoto(account.loadedProfile!!.blockingGet().displayName) + } } mProfilePhoto = view.profilePhoto + + // Show `delete` option if the account has a profile photo. + mDialogRemovePhoto = view.removePhoto + mDisposableBag.add( + mAccountService.getObservableAccountProfile(account.accountId) + .observeOn(DeviceUtils.uiScheduler) + .subscribe { + view.removePhoto.visibility = + if (it.second.avatar != null) View.VISIBLE else View.GONE + } + ) + mDisposableBag.add(AvatarDrawable.load(inflater.context, account) .observeOn(DeviceUtils.uiScheduler) .subscribe { a -> view.profilePhoto.setImageDrawable(a) }) @@ -322,10 +340,12 @@ class JamiAccountSummaryFragment : mSourcePhoto?.let { source -> presenter.saveVCard(mBinding!!.username.text.toString(), Single.just(source).map { obj -> BitmapUtils.bitmapToPhoto(obj) }) - } + } ?: presenter.saveVCard(mBinding!!.username.text.toString(), null) } .setOnDismissListener { + // Todo: Should release dialog disposable here. mProfilePhoto = null + mDialogRemovePhoto = null mSourcePhoto = null } .show() @@ -512,10 +532,28 @@ class JamiAccountSummaryFragment : pickProfilePicture.launch(PickVisualMediaRequest(PickVisualMedia.ImageOnly)) } - private fun updatePhoto(uriImage: Uri) { + private fun updatePhoto(uriImage: android.net.Uri) { updatePhoto(AndroidFileUtils.loadBitmap(requireContext(), uriImage)) } + /** + * Remove the photo from the profile picture dialog. + * Replace it with the default avatar. + */ + private fun removePhoto(profileName: String?) { + val account = presenter.account ?: return + mDialogRemovePhoto?.visibility = View.GONE + mProfilePhoto?.setImageDrawable( + AvatarDrawable.Builder() + .withNameData(profileName, account.registeredName) + .withId(Uri(Uri.JAMI_URI_SCHEME, account.username!!).rawUriString) + .withCircleCrop(true) + .withOnlineState(Contact.PresenceStatus.OFFLINE) + .build(requireContext()) + ) + mSourcePhoto = null + } + private fun updatePhoto(image: Single<Bitmap>) { val account = presenter.account ?: return mDisposableBag.add(image.subscribeOn(Schedulers.io()) @@ -524,12 +562,14 @@ class JamiAccountSummaryFragment : AvatarDrawable.Builder() .withPhoto(img) .withNameData(null, account.registeredName) - .withId(account.uri) + .withId(Uri(Uri.JAMI_URI_SCHEME, account.username!!).rawUriString) .withCircleCrop(true) .build(requireContext()) } .observeOn(DeviceUtils.uiScheduler) - .subscribe({ avatar: AvatarDrawable -> mProfilePhoto?.setImageDrawable(avatar) }) { e: Throwable -> + .subscribe({ avatar: AvatarDrawable -> + mDialogRemovePhoto?.visibility = View.VISIBLE + mProfilePhoto?.setImageDrawable(avatar) }) { e: Throwable -> Log.e(TAG, "Error loading image", e) }) } diff --git a/jami-android/app/src/main/java/cx/ring/account/ProfileCreationFragment.kt b/jami-android/app/src/main/java/cx/ring/account/ProfileCreationFragment.kt index 9fb5e53b40778b3d602664282121d6acfbcd2e8f..f74addb5c1bda30adc8057a9defb316020989ef8 100644 --- a/jami-android/app/src/main/java/cx/ring/account/ProfileCreationFragment.kt +++ b/jami-android/app/src/main/java/cx/ring/account/ProfileCreationFragment.kt @@ -22,7 +22,6 @@ import android.content.ActivityNotFoundException import android.content.Intent import android.content.pm.PackageManager import android.graphics.Bitmap -import android.net.Uri import android.os.Bundle import android.provider.MediaStore import android.text.Editable @@ -45,12 +44,13 @@ import dagger.hilt.android.AndroidEntryPoint import io.reactivex.rxjava3.core.Single import net.jami.account.ProfileCreationPresenter import net.jami.account.ProfileCreationView +import net.jami.model.Uri import java.io.IOException @AndroidEntryPoint class ProfileCreationFragment : BaseSupportFragment<ProfileCreationPresenter, ProfileCreationView>(), ProfileCreationView { private val model: AccountCreationViewModel by activityViewModels() - private var tmpProfilePhotoUri: Uri? = null + private var tmpProfilePhotoUri: android.net.Uri? = null private var binding: FragAccProfileCreateBinding? = null private val pickProfilePicture = @@ -63,6 +63,7 @@ class ProfileCreationFragment : BaseSupportFragment<ProfileCreationPresenter, Pr FragAccProfileCreateBinding.inflate(inflater, container, false).apply { gallery.setOnClickListener { presenter.galleryClick() } camera.setOnClickListener { presenter.cameraClick() } + removePhoto.setOnClickListener { presenter.photoRemoved() } nextCreateAccount.setOnClickListener { presenter.nextClick() } skipCreateAccount.setOnClickListener { presenter.skipClick() } username.addTextChangedListener(object : TextWatcher { @@ -81,9 +82,11 @@ class ProfileCreationFragment : BaseSupportFragment<ProfileCreationPresenter, Pr } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val binding = binding ?: return super.onViewCreated(view, savedInstanceState) - if (binding!!.profilePhoto.drawable == null) { - binding!!.profilePhoto.setImageDrawable(AvatarDrawable.Builder() + if (binding.profilePhoto.drawable == null) { + binding.removePhoto.visibility = View.GONE // Hide `delete` option if no photo. + binding.profilePhoto.setImageDrawable(AvatarDrawable.Builder() .withNameData(model.model.fullName, model.model.username) .withCircleCrop(true) .build(view.context)) @@ -156,15 +159,18 @@ class ProfileCreationFragment : BaseSupportFragment<ProfileCreationPresenter, Pr } override fun setProfile() { + val binding = binding ?: return val m = model.model - binding!!.profilePhoto.setImageDrawable( + val username = m.newAccount?.username ?: return + binding.profilePhoto.setImageDrawable( AvatarDrawable.Builder() .withPhoto(m.photo as Bitmap?) .withNameData(m.fullName, m.username) - .withId(m.newAccount?.username) + .withId(Uri(Uri.JAMI_URI_SCHEME, username).rawRingId) .withCircleCrop(true) .build(requireContext()) ) + binding.removePhoto.visibility = if (m.photo != null) View.VISIBLE else View.GONE } companion object { diff --git a/jami-android/app/src/main/res/layout/dialog_profile.xml b/jami-android/app/src/main/res/layout/dialog_profile.xml index e1c13fbd24d9d699d347106985c316e9d3aa358c..8c31c2f86922b14dff49c4bc17360bae7d032de5 100644 --- a/jami-android/app/src/main/res/layout/dialog_profile.xml +++ b/jami-android/app/src/main/res/layout/dialog_profile.xml @@ -1,6 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" android:id="@+id/profile_scrollview" android:layout_width="match_parent" android:layout_height="wrap_content"> @@ -22,7 +23,7 @@ android:text="@string/profile_message_warning" android:textAlignment="center" /> - <RelativeLayout + <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/profile_container" android:layout_width="match_parent" android:layout_height="180dp" @@ -32,9 +33,11 @@ android:id="@+id/profile_photo" android:layout_width="120dp" android:layout_height="120dp" - android:layout_centerHorizontal="true" android:layout_margin="15dp" - android:scaleType="fitCenter" + android:contentDescription="@null" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" app:srcCompat="@drawable/ic_contact_picture_fallback" /> <com.google.android.material.floatingactionbutton.FloatingActionButton @@ -42,37 +45,55 @@ style="@style/Widget.Material3.FloatingActionButton.Primary" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignTop="@+id/anchor" - android:layout_toStartOf="@+id/anchor" + android:layout_marginTop="-20dp" android:contentDescription="@string/open_the_gallery" android:text="@string/open_the_gallery" app:backgroundTint="@color/surface" + app:layout_constraintEnd_toStartOf="@+id/camera" + app:layout_constraintHorizontal_chainStyle="packed" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/profile_photo" app:rippleColor="@android:color/white" app:srcCompat="@drawable/baseline_insert_photo_24" app:tint="?attr/colorControlNormal" /> - <View - android:id="@+id/anchor" - android:layout_width="20dp" - android:layout_height="20dp" - android:layout_alignBottom="@+id/profile_photo" - android:layout_centerHorizontal="true" /> - <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/camera" + style="@style/Widget.Material3.FloatingActionButton.Primary" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignTop="@+id/anchor" - android:layout_toEndOf="@+id/anchor" + android:layout_marginStart="10dp" + android:layout_marginTop="-20dp" android:contentDescription="@string/take_a_photo" android:text="@string/take_a_photo" - app:tint="?attr/colorControlNormal" + app:backgroundTint="@color/surface" + app:layout_constraintEnd_toStartOf="@id/remove_photo" + app:layout_constraintStart_toEndOf="@+id/gallery" + app:layout_constraintTop_toBottomOf="@id/profile_photo" + app:rippleColor="@android:color/white" + app:srcCompat="@drawable/baseline_photo_camera_24" + app:tint="?attr/colorControlNormal" /> + + <com.google.android.material.floatingactionbutton.FloatingActionButton + android:id="@+id/remove_photo" style="@style/Widget.Material3.FloatingActionButton.Primary" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="10dp" + android:layout_marginTop="-20dp" + android:contentDescription="@string/remove_photo" + android:text="@string/remove_photo" + android:visibility="gone" app:backgroundTint="@color/surface" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/camera" + app:layout_constraintTop_toBottomOf="@id/profile_photo" app:rippleColor="@android:color/white" - app:srcCompat="@drawable/baseline_photo_camera_24" /> + app:srcCompat="@drawable/baseline_cancel_24" + app:tint="?attr/colorControlNormal" + tools:visibility="visible" /> - </RelativeLayout> + </androidx.constraintlayout.widget.ConstraintLayout> </LinearLayout> </ScrollView> \ No newline at end of file diff --git a/jami-android/app/src/main/res/layout/frag_acc_profile_create.xml b/jami-android/app/src/main/res/layout/frag_acc_profile_create.xml index 61136ad564549fe5bfba52fa5b8dfbcaacad3d0d..b7618b3d5bbd80a5a169dbd2d69dbf4c513d89c6 100644 --- a/jami-android/app/src/main/res/layout/frag_acc_profile_create.xml +++ b/jami-android/app/src/main/res/layout/frag_acc_profile_create.xml @@ -54,56 +54,71 @@ android:padding="6dp"/> </androidx.constraintlayout.widget.ConstraintLayout> - <RelativeLayout + <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/profile_container" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" - android:layout_gravity="center" - android:clipToPadding="false" - android:clipChildren="false"> + android:clipChildren="false" + android:clipToPadding="false"> <ImageView android:id="@+id/profile_photo" android:layout_width="90dp" android:layout_height="90dp" - android:layout_alignParentTop="true" - android:layout_centerHorizontal="true" - android:scaleType="fitCenter" + android:contentDescription="@null" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" tools:src="@drawable/ic_contact_picture_fallback" /> <com.google.android.material.floatingactionbutton.FloatingActionButton - android:id="@+id/camera" + android:id="@+id/gallery" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignTop="@+id/anchor" - android:layout_toEndOf="@+id/anchor" + android:layout_marginTop="-20dp" + android:contentDescription="@string/open_the_gallery" + android:text="@string/open_the_gallery" app:backgroundTint="@color/light" app:fabCustomSize="40dp" - app:srcCompat="@drawable/baseline_photo_camera_24" - app:rippleColor="@android:color/white" /> + app:layout_constraintEnd_toStartOf="@+id/camera" + app:layout_constraintHorizontal_chainStyle="packed" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/profile_photo" + app:rippleColor="@android:color/white" + app:srcCompat="@drawable/baseline_insert_photo_24" /> - <Space - android:id="@+id/anchor" - android:layout_width="15dp" - android:layout_height="25dp" - android:layout_alignBottom="@+id/profile_photo" - android:layout_centerHorizontal="true" /> + <com.google.android.material.floatingactionbutton.FloatingActionButton + android:id="@+id/camera" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="-20dp" + android:contentDescription="@string/take_a_photo" + app:backgroundTint="@color/light" + app:fabCustomSize="40dp" + app:layout_constraintEnd_toStartOf="@+id/remove_photo" + app:layout_constraintStart_toEndOf="@+id/gallery" + app:layout_constraintTop_toBottomOf="@id/profile_photo" + app:rippleColor="@android:color/white" + app:srcCompat="@drawable/baseline_photo_camera_24" /> <com.google.android.material.floatingactionbutton.FloatingActionButton - android:id="@+id/gallery" + android:id="@+id/remove_photo" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignTop="@+id/anchor" - android:layout_toStartOf="@+id/anchor" - android:contentDescription="@string/open_the_gallery" - android:text="@string/open_the_gallery" + android:layout_marginTop="-20dp" + android:contentDescription="@string/remove_photo" + android:visibility="gone" app:backgroundTint="@color/light" app:fabCustomSize="40dp" - app:srcCompat="@drawable/baseline_insert_photo_24" - app:rippleColor="@android:color/white" /> + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/camera" + app:layout_constraintTop_toBottomOf="@id/profile_photo" + app:rippleColor="@android:color/white" + app:srcCompat="@drawable/baseline_cancel_24" + tools:visibility="visible" /> - </RelativeLayout> + </androidx.constraintlayout.widget.ConstraintLayout> <com.google.android.material.textfield.TextInputLayout android:id="@+id/username_box" diff --git a/jami-android/app/src/main/res/values/strings.xml b/jami-android/app/src/main/res/values/strings.xml index 3f5d161008e4f69498c2b07a28f277d9a9cc5228..1cd7fbdc4b61cc8142aa339104f73aa2f3d43ffd 100644 --- a/jami-android/app/src/main/res/values/strings.xml +++ b/jami-android/app/src/main/res/values/strings.xml @@ -412,6 +412,7 @@ along with this program; if not, write to the Free Software <string name="profile_message_warning">Your profile is only shared with your contacts</string> <string name="open_the_gallery">Open the gallery</string> <string name="take_a_photo">Take photo</string> + <string name="remove_photo">Remove photo</string> <string name="profile_name_hint">Enter your name</string> <string name="registered_name">Registered Name</string> <string name="no_registered_name">No name registered</string> diff --git a/jami-android/libjamiclient/src/main/kotlin/net/jami/account/JamiAccountSummaryPresenter.kt b/jami-android/libjamiclient/src/main/kotlin/net/jami/account/JamiAccountSummaryPresenter.kt index 84f091861df68a641a8985337210b4aaf97ef3fc..d4053e6df0433edf72b297ac079853e9637b6afe 100644 --- a/jami-android/libjamiclient/src/main/kotlin/net/jami/account/JamiAccountSummaryPresenter.kt +++ b/jami-android/libjamiclient/src/main/kotlin/net/jami/account/JamiAccountSummaryPresenter.kt @@ -125,28 +125,57 @@ class JamiAccountSummaryPresenter @Inject constructor( .subscribe({}) { e: Throwable -> Log.e(TAG, "Error saving vCard " + e.message) }) } - fun saveVCard(username: String?, photo: Single<Photo>) { + /** + * Save the vCard to the disk. + * @param username: the username to save, if null, the username will be removed from the vCard + * @param photo: the photo to save, if null, the photo will be removed from the vCard + */ + fun saveVCard(username: String?, photo: Single<Photo>?) { val accountId = mAccountID ?: return val account = mAccountService.getAccount(accountId)!! val ringId = account.username val filesDir = mDeviceRuntimeService.provideFilesDir() - mCompositeDisposable.add(Single.zip( - VCardUtils.loadLocalProfileFromDiskWithDefault(filesDir, accountId).subscribeOn(Schedulers.io()), - photo - ) { vcard: VCard, pic: Photo -> - vcard.uid = Uid(ringId) - if (!username.isNullOrEmpty()) { - vcard.setFormattedName(username) - } - vcard.removeProperties(Photo::class.java) - vcard.addPhoto(pic) - vcard.removeProperties(RawProperty::class.java) - vcard + + if (photo == null) { + mCompositeDisposable.add( + VCardUtils.loadLocalProfileFromDiskWithDefault(filesDir, accountId) + .subscribeOn(Schedulers.io()) + .map { vcard: VCard -> + vcard.uid = Uid(ringId) + if (!username.isNullOrEmpty()) vcard.setFormattedName(username) + vcard.removeProperties(Photo::class.java) + vcard.removeProperties(RawProperty::class.java) + vcard + } + .flatMap { vcard: VCard -> + VCardUtils.saveLocalProfileToDisk(vcard, accountId, filesDir) + } + .subscribe({ vcard: VCard -> + account.loadedProfile = mVcardService.loadVCardProfile(vcard).cache() + }) { e: Throwable -> Log.e(TAG, "Error saving vCard !", e) } + ) + } else { + mCompositeDisposable.add( + Single.zip( + VCardUtils.loadLocalProfileFromDiskWithDefault(filesDir, accountId) + .subscribeOn(Schedulers.io()), + photo + ) { vcard: VCard, pic: Photo -> + vcard.uid = Uid(ringId) + if (!username.isNullOrEmpty()) vcard.setFormattedName(username) + vcard.removeProperties(Photo::class.java) + vcard.addPhoto(pic) + vcard.removeProperties(RawProperty::class.java) + vcard + } + .flatMap { vcard: VCard -> + VCardUtils.saveLocalProfileToDisk(vcard, accountId, filesDir) + } + .subscribeOn(Schedulers.io()) + .subscribe({ vcard: VCard -> + account.loadedProfile = mVcardService.loadVCardProfile(vcard).cache() + }) { e: Throwable -> Log.e(TAG, "Error saving vCard !", e) }) } - .flatMap { vcard: VCard -> VCardUtils.saveLocalProfileToDisk(vcard, accountId, filesDir) } - .subscribeOn(Schedulers.io()) - .subscribe({ vcard: VCard -> account.loadedProfile = mVcardService.loadVCardProfile(vcard).cache() }) - { e: Throwable -> Log.e(TAG, "Error saving vCard !", e) }) } fun cameraClicked() { diff --git a/jami-android/libjamiclient/src/main/kotlin/net/jami/account/ProfileCreationPresenter.kt b/jami-android/libjamiclient/src/main/kotlin/net/jami/account/ProfileCreationPresenter.kt index 6d269d3332fe0bad6a3c3648d77c0b0aabe93e94..325131d995d29e878f93281ef5d33cf01f609daf 100644 --- a/jami-android/libjamiclient/src/main/kotlin/net/jami/account/ProfileCreationPresenter.kt +++ b/jami-android/libjamiclient/src/main/kotlin/net/jami/account/ProfileCreationPresenter.kt @@ -59,6 +59,10 @@ class ProfileCreationPresenter @Inject constructor( { e: Throwable -> Log.e(TAG, "Can't load image", e) }) } + fun photoRemoved() { + mAccountCreationModel?.photo = null + } + fun galleryClick() { view?.goToGallery() }