diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileFragment.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileFragment.java index 4d78880515..c3f1253f4b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileFragment.java @@ -182,8 +182,13 @@ public class ManageProfileFragment extends LoggingFragment { } private void presentEvent(@NonNull ManageProfileViewModel.Event event) { - if (event == ManageProfileViewModel.Event.AVATAR_FAILURE) { - Toast.makeText(requireContext(), R.string.ManageProfileFragment_failed_to_set_avatar, Toast.LENGTH_LONG).show(); + switch (event) { + case AVATAR_DISK_FAILURE: + Toast.makeText(requireContext(), R.string.ManageProfileFragment_failed_to_set_avatar, Toast.LENGTH_LONG).show(); + break; + case AVATAR_NETWORK_FAILURE: + Toast.makeText(requireContext(), R.string.EditProfileNameFragment_failed_to_save_due_to_network_issues_try_again_later, Toast.LENGTH_LONG).show(); + break; } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileRepository.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileRepository.java index 87108a1b40..7eef53aa79 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileRepository.java @@ -3,15 +3,19 @@ package org.thoughtcrime.securesms.profiles.manage; import android.content.Context; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.util.Consumer; import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.profiles.ProfileName; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.ProfileUtil; +import org.whispersystems.signalservice.api.util.StreamDetails; +import java.io.ByteArrayInputStream; import java.io.IOException; final class ManageProfileRepository { @@ -36,6 +40,33 @@ final class ManageProfileRepository { try { ProfileUtil.uploadProfileWithAbout(context, about, emoji); DatabaseFactory.getRecipientDatabase(context).setAbout(Recipient.self().getId(), about, emoji); + callback.accept(Result.SUCCESS); + } catch (IOException e) { + Log.w(TAG, "Failed to upload profile during about change.", e); + callback.accept(Result.FAILURE_NETWORK); + } + }); + } + + public void setAvatar(@NonNull Context context, @NonNull byte[] data, @NonNull String contentType, @NonNull Consumer callback) { + SignalExecutors.UNBOUNDED.execute(() -> { + try { + ProfileUtil.uploadProfileWithAvatar(context, new StreamDetails(new ByteArrayInputStream(data), contentType, data.length)); + AvatarHelper.setAvatar(context, Recipient.self().getId(), new ByteArrayInputStream(data)); + callback.accept(Result.SUCCESS); + } catch (IOException e) { + Log.w(TAG, "Failed to upload profile during avatar change.", e); + callback.accept(Result.FAILURE_NETWORK); + } + }); + } + + public void clearAvatar(@NonNull Context context, @NonNull Consumer callback) { + SignalExecutors.UNBOUNDED.execute(() -> { + try { + ProfileUtil.uploadProfileWithAvatar(context, null); + AvatarHelper.delete(context, Recipient.self().getId()); + callback.accept(Result.SUCCESS); } catch (IOException e) { Log.w(TAG, "Failed to upload profile during name change.", e); diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileViewModel.java index 3cb79aaddd..ce01fac0dc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileViewModel.java @@ -13,7 +13,6 @@ import org.signal.core.util.StreamUtil; import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.jobs.ProfileUploadJob; import org.thoughtcrime.securesms.jobs.RetrieveProfileJob; import org.thoughtcrime.securesms.mediasend.Media; import org.thoughtcrime.securesms.profiles.AvatarHelper; @@ -25,7 +24,6 @@ import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.SingleLiveEvent; import org.whispersystems.signalservice.api.util.StreamDetails; -import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Objects; @@ -41,6 +39,9 @@ class ManageProfileViewModel extends ViewModel { private final MutableLiveData aboutEmoji; private final SingleLiveEvent events; private final RecipientForeverObserver observer; + private final ManageProfileRepository repository; + + private byte[] previousAvatar; public ManageProfileViewModel() { this.avatar = new MutableLiveData<>(); @@ -49,6 +50,7 @@ class ManageProfileViewModel extends ViewModel { this.about = new MutableLiveData<>(); this.aboutEmoji = new MutableLiveData<>(); this.events = new SingleLiveEvent<>(); + this.repository = new ManageProfileRepository(); this.observer = this::onRecipientChanged; SignalExecutors.BOUNDED.execute(() -> { @@ -101,11 +103,21 @@ class ManageProfileViewModel extends ViewModel { } public void onAvatarSelected(@NonNull Context context, @Nullable Media media) { + previousAvatar = avatar.getValue() != null ? avatar.getValue().getAvatar() : null; + if (media == null) { - SignalExecutors.BOUNDED.execute(() -> { - AvatarHelper.delete(context, Recipient.self().getId()); - avatar.postValue(AvatarState.none()); - ApplicationDependencies.getJobManager().add(new ProfileUploadJob()); + avatar.postValue(AvatarState.loading(null)); + repository.clearAvatar(context, result -> { + switch (result) { + case SUCCESS: + avatar.postValue(AvatarState.loaded(null)); + previousAvatar = null; + break; + case FAILURE_NETWORK: + avatar.postValue(AvatarState.loaded(previousAvatar)); + events.postValue(Event.AVATAR_NETWORK_FAILURE); + break; + } }); } else { SignalExecutors.BOUNDED.execute(() -> { @@ -113,13 +125,23 @@ class ManageProfileViewModel extends ViewModel { InputStream stream = BlobProvider.getInstance().getStream(context, media.getUri()); byte[] data = StreamUtil.readFully(stream); - AvatarHelper.setAvatar(context, Recipient.self().getId(), new ByteArrayInputStream(data)); - avatar.postValue(AvatarState.loaded(data)); + avatar.postValue(AvatarState.loading(data)); - ApplicationDependencies.getJobManager().add(new ProfileUploadJob()); + repository.setAvatar(context, data, media.getMimeType(), result -> { + switch (result) { + case SUCCESS: + avatar.postValue(AvatarState.loaded(data)); + previousAvatar = data; + break; + case FAILURE_NETWORK: + avatar.postValue(AvatarState.loaded(previousAvatar)); + events.postValue(Event.AVATAR_NETWORK_FAILURE); + break; + } + }); } catch (IOException e) { Log.w(TAG, "Failed to save avatar!", e); - events.postValue(Event.AVATAR_FAILURE); + events.postValue(Event.AVATAR_DISK_FAILURE); } }); } @@ -176,7 +198,7 @@ class ManageProfileViewModel extends ViewModel { } enum Event { - AVATAR_FAILURE + AVATAR_NETWORK_FAILURE, AVATAR_DISK_FAILURE } static class Factory extends ViewModelProvider.NewInstanceFactory { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java index 05ad24eec1..949e42c637 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ProfileUtil.java @@ -107,10 +107,13 @@ public final class ProfileUtil { * successfully before persisting the change to disk. */ public static void uploadProfileWithName(@NonNull Context context, @NonNull ProfileName profileName) throws IOException { - uploadProfile(context, - profileName, - Optional.fromNullable(Recipient.self().getAbout()).or(""), - Optional.fromNullable(Recipient.self().getAboutEmoji()).or("")); + try (StreamDetails avatar = AvatarHelper.getSelfProfileAvatarStream(context)) { + uploadProfile(context, + profileName, + Optional.fromNullable(Recipient.self().getAbout()).or(""), + Optional.fromNullable(Recipient.self().getAboutEmoji()).or(""), + avatar); + } } /** @@ -119,35 +122,51 @@ public final class ProfileUtil { * successfully before persisting the change to disk. */ public static void uploadProfileWithAbout(@NonNull Context context, @NonNull String about, @NonNull String emoji) throws IOException { - uploadProfile(context, - Recipient.self().getProfileName(), - about, - emoji); + try (StreamDetails avatar = AvatarHelper.getSelfProfileAvatarStream(context)) { + uploadProfile(context, + Recipient.self().getProfileName(), + about, + emoji, + avatar); + } + } + + /** + * Uploads the profile based on all state that's written to disk, except we'll use the provided + * avatar instead. This is useful when you want to ensure that the profile has been uploaded + * successfully before persisting the change to disk. + */ + public static void uploadProfileWithAvatar(@NonNull Context context, @Nullable StreamDetails avatar) throws IOException { + uploadProfile(context, + Recipient.self().getProfileName(), + Optional.fromNullable(Recipient.self().getAbout()).or(""), + Optional.fromNullable(Recipient.self().getAboutEmoji()).or(""), + avatar); } /** * Uploads the profile based on all state that's already written to disk. */ public static void uploadProfile(@NonNull Context context) throws IOException { - uploadProfile(context, - Recipient.self().getProfileName(), - Optional.fromNullable(Recipient.self().getAbout()).or(""), - Optional.fromNullable(Recipient.self().getAboutEmoji()).or("")); + try (StreamDetails avatar = AvatarHelper.getSelfProfileAvatarStream(context)) { + uploadProfile(context, + Recipient.self().getProfileName(), + Optional.fromNullable(Recipient.self().getAbout()).or(""), + Optional.fromNullable(Recipient.self().getAboutEmoji()).or(""), + avatar); + } } - public static void uploadProfile(@NonNull Context context, - @NonNull ProfileName profileName, - @Nullable String about, - @Nullable String aboutEmoji) + private static void uploadProfile(@NonNull Context context, + @NonNull ProfileName profileName, + @Nullable String about, + @Nullable String aboutEmoji, + @Nullable StreamDetails avatar) throws IOException { - ProfileKey profileKey = ProfileKeyUtil.getSelfProfileKey(); - String avatarPath; - - try (StreamDetails avatar = AvatarHelper.getSelfProfileAvatarStream(context)) { - SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager(); - avatarPath = accountManager.setVersionedProfile(Recipient.self().getUuid().get(), profileKey, profileName.serialize(), about, aboutEmoji, avatar).orNull(); - } + ProfileKey profileKey = ProfileKeyUtil.getSelfProfileKey(); + SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager(); + String avatarPath = accountManager.setVersionedProfile(Recipient.self().getUuid().get(), profileKey, profileName.serialize(), about, aboutEmoji, avatar).orNull(); DatabaseFactory.getRecipientDatabase(context).setProfileAvatar(Recipient.self().getId(), avatarPath); }