From ed23c3fe7c3ed9f7b4c892c6943c2a329efe3438 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Tue, 20 Jul 2021 13:08:52 -0300 Subject: [PATCH] Add avatar picker and defaults. --- app/build.gradle | 2 +- app/src/main/assets/fonts/Inter-Medium.otf | Bin 0 -> 269692 bytes .../securesms/ApplicationContext.java | 7 + .../securesms/DeviceActivity.java | 1 + .../securesms/MediaPreviewActivity.java | 1 + .../securesms/VerifyIdentityActivity.java | 1 + .../securesms/WebRtcCallActivity.java | 2 + .../thoughtcrime/securesms/avatar/Avatar.kt | 79 ++++++ .../securesms/avatar/AvatarBundler.kt | 69 ++++++ .../securesms/avatar/AvatarColorItem.kt | 42 ++++ .../securesms/avatar/AvatarPickerStorage.kt | 55 ++++ .../securesms/avatar/AvatarRenderer.kt | 156 ++++++++++++ .../thoughtcrime/securesms/avatar/Avatars.kt | 153 ++++++++++++ .../avatar/photo/PhotoEditorFragment.kt | 65 +++++ .../avatar/picker/AvatarPickerFragment.kt | 223 +++++++++++++++++ .../avatar/picker/AvatarPickerItem.kt | 150 +++++++++++ .../avatar/picker/AvatarPickerRepository.kt | 175 +++++++++++++ .../avatar/picker/AvatarPickerState.kt | 10 + .../avatar/picker/AvatarPickerViewModel.kt | 193 +++++++++++++++ .../avatar/text/TextAvatarCreationFragment.kt | 148 +++++++++++ .../avatar/text/TextAvatarCreationState.kt | 11 + .../text/TextAvatarCreationViewModel.kt | 33 +++ .../vector/VectorAvatarCreationFragment.kt | 64 +++++ .../vector/VectorAvatarCreationState.kt | 11 + .../vector/VectorAvatarCreationViewModel.kt | 27 ++ .../securesms/components/AvatarImageView.java | 6 +- .../components/BoldSelectionTabItem.kt | 88 +++++++ .../components/ButtonStripItemView.kt | 41 +++ .../recyclerview/GridDividerDecoration.kt | 50 ++++ .../avatars/FallbackContactPhoto.java | 12 +- .../contacts/avatars/FallbackPhoto20dp.java | 20 +- .../contacts/avatars/FallbackPhoto80dp.java | 40 ++- .../avatars/GeneratedContactPhoto.java | 73 ++---- .../avatars/ResourceContactPhoto.java | 30 +-- .../avatars/TransparentContactPhoto.java | 11 +- .../conversation/ConversationActivity.java | 3 +- .../conversation/colors/AvatarColor.java | 191 +++++++------- .../securesms/database/DatabaseFactory.java | 11 +- .../database/helpers/SQLCipherOpenHelper.java | 23 +- .../database/model/AvatarPickerDatabase.kt | 185 ++++++++++++++ .../details/AddGroupDetailsActivity.java | 7 +- .../details/AddGroupDetailsFragment.java | 92 +++---- .../details/AddGroupDetailsViewModel.java | 11 + .../imageeditor/model/EditorModel.java | 19 +- .../insights/InsightsUserAvatar.java | 2 +- .../MediaGridDividerDecoration.kt | 37 +-- .../mediaoverview/MediaOverviewActivity.java | 34 +-- .../mediaoverview/MediaOverviewTabItem.kt | 39 --- .../mediasend/AvatarSelectionActivity.java | 2 +- ...tarSelectionBottomSheetDialogFragment.java | 234 ------------------ .../mediasend/MediaSendActivity.java | 2 + .../securesms/mms/PartAuthority.java | 59 +++-- .../v2/NotificationConversation.kt | 2 +- .../v2/NotificationExtensions.kt | 6 +- .../securesms/permissions/Permissions.java | 1 + .../edit/EditGroupProfileRepository.java | 6 + .../profiles/edit/EditProfileActivity.java | 9 +- .../profiles/edit/EditProfileFragment.java | 115 +++++---- .../profiles/edit/EditProfileRepository.java | 3 + .../profiles/edit/EditProfileViewModel.java | 22 ++ .../edit/EditSelfProfileRepository.java | 6 + .../manage/ManageProfileFragment.java | 28 +-- .../profiles/spoofing/ReviewBannerView.java | 3 +- .../securesms/recipients/Recipient.java | 4 +- .../RecipientBottomSheetDialogFragment.java | 2 +- .../scribbles/ImageEditorFragment.java | 105 ++++++-- .../securesms/util/AvatarUtil.java | 2 +- .../util/ConversationShortcutPhoto.java | 20 +- .../securesms/util/MappingAdapter.java | 37 ++- .../securesms/util/MappingModel.java | 5 + .../securesms/util/MappingViewHolder.java | 12 +- .../thoughtcrime/securesms/util/NameUtil.kt | 34 +++ .../securesms/util/storage/FileStorage.java | 92 +++++++ .../wallpaper/ChatWallpaperViewModel.java | 2 +- .../securesms/wallpaper/WallpaperStorage.java | 50 +--- app/src/main/proto/Database.proto | 24 ++ .../main/res/anim/fragment_close_enter.xml | 40 +++ app/src/main/res/anim/fragment_close_exit.xml | 41 +++ app/src/main/res/anim/fragment_open_enter.xml | 42 ++++ app/src/main/res/anim/fragment_open_exit.xml | 41 +++ .../res/drawable/avatar_color_item_ring.xml | 8 + .../res/drawable/avatar_picker_item_ring.xml | 7 + .../drawable/circle_transparent_black_20.xml | 5 + .../res/drawable/ic_avatar_abstract_01.xml | 27 ++ .../res/drawable/ic_avatar_abstract_02.xml | 23 ++ .../res/drawable/ic_avatar_abstract_03.xml | 25 ++ .../main/res/drawable/ic_avatar_balloon.xml | 13 + app/src/main/res/drawable/ic_avatar_book.xml | 55 ++++ .../main/res/drawable/ic_avatar_briefcase.xml | 108 ++++++++ app/src/main/res/drawable/ic_avatar_cat.xml | 16 ++ .../res/drawable/ic_avatar_celebration.xml | 112 +++++++++ .../main/res/drawable/ic_avatar_dinosour.xml | 15 ++ app/src/main/res/drawable/ic_avatar_dog.xml | 31 +++ app/src/main/res/drawable/ic_avatar_drink.xml | 102 ++++++++ .../main/res/drawable/ic_avatar_football.xml | 53 ++++ app/src/main/res/drawable/ic_avatar_fox.xml | 29 +++ app/src/main/res/drawable/ic_avatar_ghost.xml | 27 ++ app/src/main/res/drawable/ic_avatar_heart.xml | 15 ++ app/src/main/res/drawable/ic_avatar_house.xml | 65 +++++ .../main/res/drawable/ic_avatar_incognito.xml | 57 +++++ app/src/main/res/drawable/ic_avatar_melon.xml | 21 ++ app/src/main/res/drawable/ic_avatar_pig.xml | 16 ++ app/src/main/res/drawable/ic_avatar_sloth.xml | 33 +++ .../res/drawable/ic_avatar_soccerball.xml | 129 ++++++++++ .../main/res/drawable/ic_avatar_sunset.xml | 19 ++ .../main/res/drawable/ic_avatar_surfboard.xml | 22 ++ app/src/main/res/drawable/ic_avatar_tucan.xml | 28 +++ app/src/main/res/drawable/ic_text_24.xml | 9 + .../res/layout/add_group_details_activity.xml | 8 +- app/src/main/res/layout/avatar_color_item.xml | 20 ++ .../layout/avatar_photo_editor_fragment.xml | 5 + .../res/layout/avatar_picker_fragment.xml | 152 ++++++++++++ .../main/res/layout/avatar_picker_item.xml | 44 ++++ ...b_item.xml => bold_selection_tab_item.xml} | 4 +- .../res/layout/button_strip_item_view.xml | 33 +++ .../res/layout/profile_create_activity.xml | 20 +- .../res/layout/profile_create_fragment.xml | 2 - .../layout/text_avatar_creation_fragment.xml | 126 ++++++++++ ...atar_creation_fragment_hidden_recycler.xml | 126 ++++++++++ .../vector_avatar_creation_fragment.xml | 101 ++++++++ .../main/res/menu/avatar_picker_context.xml | 4 + app/src/main/res/navigation/avatar_picker.xml | 98 ++++++++ app/src/main/res/navigation/create_group.xml | 26 ++ app/src/main/res/navigation/edit_profile.xml | 20 ++ .../main/res/navigation/manage_profile.xml | 22 ++ app/src/main/res/values-sw360dp/dimens.xml | 2 + app/src/main/res/values/attrs.xml | 6 + app/src/main/res/values/dimens.xml | 2 + app/src/main/res/values/strings.xml | 15 ++ app/src/main/res/values/themes.xml | 5 + app/witness-verifications.gradle | 78 +++--- .../org/signal/core/util/EditTextUtil.java | 45 ++++ .../service/witness-verifications.gradle | 6 + 133 files changed, 4935 insertions(+), 859 deletions(-) create mode 100644 app/src/main/assets/fonts/Inter-Medium.otf create mode 100644 app/src/main/java/org/thoughtcrime/securesms/avatar/Avatar.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarBundler.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarColorItem.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarPickerStorage.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarRenderer.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/avatar/Avatars.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/avatar/photo/PhotoEditorFragment.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerFragment.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerItem.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerRepository.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerState.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerViewModel.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/avatar/text/TextAvatarCreationFragment.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/avatar/text/TextAvatarCreationState.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/avatar/text/TextAvatarCreationViewModel.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/avatar/vector/VectorAvatarCreationFragment.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/avatar/vector/VectorAvatarCreationState.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/avatar/vector/VectorAvatarCreationViewModel.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/BoldSelectionTabItem.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/ButtonStripItemView.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/recyclerview/GridDividerDecoration.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/model/AvatarPickerDatabase.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaOverviewTabItem.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionBottomSheetDialogFragment.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/util/NameUtil.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/util/storage/FileStorage.java create mode 100644 app/src/main/res/anim/fragment_close_enter.xml create mode 100644 app/src/main/res/anim/fragment_close_exit.xml create mode 100644 app/src/main/res/anim/fragment_open_enter.xml create mode 100644 app/src/main/res/anim/fragment_open_exit.xml create mode 100644 app/src/main/res/drawable/avatar_color_item_ring.xml create mode 100644 app/src/main/res/drawable/avatar_picker_item_ring.xml create mode 100644 app/src/main/res/drawable/circle_transparent_black_20.xml create mode 100644 app/src/main/res/drawable/ic_avatar_abstract_01.xml create mode 100644 app/src/main/res/drawable/ic_avatar_abstract_02.xml create mode 100644 app/src/main/res/drawable/ic_avatar_abstract_03.xml create mode 100644 app/src/main/res/drawable/ic_avatar_balloon.xml create mode 100644 app/src/main/res/drawable/ic_avatar_book.xml create mode 100644 app/src/main/res/drawable/ic_avatar_briefcase.xml create mode 100644 app/src/main/res/drawable/ic_avatar_cat.xml create mode 100644 app/src/main/res/drawable/ic_avatar_celebration.xml create mode 100644 app/src/main/res/drawable/ic_avatar_dinosour.xml create mode 100644 app/src/main/res/drawable/ic_avatar_dog.xml create mode 100644 app/src/main/res/drawable/ic_avatar_drink.xml create mode 100644 app/src/main/res/drawable/ic_avatar_football.xml create mode 100644 app/src/main/res/drawable/ic_avatar_fox.xml create mode 100644 app/src/main/res/drawable/ic_avatar_ghost.xml create mode 100644 app/src/main/res/drawable/ic_avatar_heart.xml create mode 100644 app/src/main/res/drawable/ic_avatar_house.xml create mode 100644 app/src/main/res/drawable/ic_avatar_incognito.xml create mode 100644 app/src/main/res/drawable/ic_avatar_melon.xml create mode 100644 app/src/main/res/drawable/ic_avatar_pig.xml create mode 100644 app/src/main/res/drawable/ic_avatar_sloth.xml create mode 100644 app/src/main/res/drawable/ic_avatar_soccerball.xml create mode 100644 app/src/main/res/drawable/ic_avatar_sunset.xml create mode 100644 app/src/main/res/drawable/ic_avatar_surfboard.xml create mode 100644 app/src/main/res/drawable/ic_avatar_tucan.xml create mode 100644 app/src/main/res/drawable/ic_text_24.xml create mode 100644 app/src/main/res/layout/avatar_color_item.xml create mode 100644 app/src/main/res/layout/avatar_photo_editor_fragment.xml create mode 100644 app/src/main/res/layout/avatar_picker_fragment.xml create mode 100644 app/src/main/res/layout/avatar_picker_item.xml rename app/src/main/res/layout/{media_overview_tab_item.xml => bold_selection_tab_item.xml} (84%) create mode 100644 app/src/main/res/layout/button_strip_item_view.xml create mode 100644 app/src/main/res/layout/text_avatar_creation_fragment.xml create mode 100644 app/src/main/res/layout/text_avatar_creation_fragment_hidden_recycler.xml create mode 100644 app/src/main/res/layout/vector_avatar_creation_fragment.xml create mode 100644 app/src/main/res/menu/avatar_picker_context.xml create mode 100644 app/src/main/res/navigation/avatar_picker.xml diff --git a/app/build.gradle b/app/build.gradle index bd83252544..38a8034227 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -368,7 +368,7 @@ android { dependencies { implementation 'androidx.core:core-ktx:1.5.0' - implementation 'androidx.fragment:fragment-ktx:1.2.5' + implementation 'androidx.fragment:fragment-ktx:1.3.5' lintChecks project(':lintchecks') coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' diff --git a/app/src/main/assets/fonts/Inter-Medium.otf b/app/src/main/assets/fonts/Inter-Medium.otf new file mode 100644 index 0000000000000000000000000000000000000000..ca7bfcd4340ea6125717ff6c6e8dad0a65c5795e GIT binary patch literal 269692 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 Unit + +/** + * Selectable color item for choosing colors when editing a Text or Vector avatar. + */ +data class AvatarColorItem( + val colors: Avatars.ColorPair, + val selected: Boolean +) { + + companion object { + fun registerViewHolder(adapter: MappingAdapter, onAvatarColorClickListener: OnAvatarColorClickListener) { + adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it, onAvatarColorClickListener) }, R.layout.avatar_color_item)) + } + } + + class Model(val colorItem: AvatarColorItem) : MappingModel { + override fun areItemsTheSame(newItem: Model): Boolean = newItem.colorItem.colors == colorItem.colors + override fun areContentsTheSame(newItem: Model): Boolean = newItem.colorItem == colorItem + } + + private class ViewHolder(itemView: View, private val onAvatarColorClickListener: OnAvatarColorClickListener) : MappingViewHolder(itemView) { + + private val imageView: ImageView = findViewById(R.id.avatar_color_item) + + override fun bind(model: Model) { + itemView.setOnClickListener { onAvatarColorClickListener(model.colorItem.colors) } + imageView.background.colorFilter = SimpleColorFilter(model.colorItem.colors.backgroundColor) + imageView.isSelected = model.colorItem.selected + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarPickerStorage.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarPickerStorage.kt new file mode 100644 index 0000000000..d3e498fec1 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarPickerStorage.kt @@ -0,0 +1,55 @@ +package org.thoughtcrime.securesms.avatar + +import android.content.Context +import android.net.Uri +import android.webkit.MimeTypeMap +import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.mediasend.Media +import org.thoughtcrime.securesms.mms.PartAuthority +import org.thoughtcrime.securesms.util.MediaUtil +import org.thoughtcrime.securesms.util.storage.FileStorage +import java.io.InputStream + +object AvatarPickerStorage { + + private const val DIRECTORY = "avatar_picker" + private const val FILENAME_BASE = "avatar" + + @JvmStatic + fun read(context: Context, fileName: String) = FileStorage.read(context, DIRECTORY, fileName) + + fun save(context: Context, media: Media): Uri { + val fileName = FileStorage.save(context, PartAuthority.getAttachmentStream(context, media.uri), DIRECTORY, FILENAME_BASE, MediaUtil.getExtension(context, media.uri) ?: "") + + return PartAuthority.getAvatarPickerUri(fileName) + } + + fun save(context: Context, inputStream: InputStream): Uri { + val fileName = FileStorage.save(context, inputStream, DIRECTORY, FILENAME_BASE, MimeTypeMap.getSingleton().getExtensionFromMimeType(MediaUtil.IMAGE_JPEG) ?: "") + + return PartAuthority.getAvatarPickerUri(fileName) + } + + @JvmStatic + fun cleanOrphans(context: Context) { + val avatarFiles = FileStorage.getAllFiles(context, DIRECTORY, FILENAME_BASE) + val database = DatabaseFactory.getAvatarPickerDatabase(context) + val photoAvatars = database + .getAllAvatars() + .filterIsInstance() + + val inDatabaseFileNames = photoAvatars.map { PartAuthority.getAvatarPickerFilename(it.uri) } + val onDiskFileNames = avatarFiles.map { it.name } + + val inDatabaseButNotOnDisk = inDatabaseFileNames - onDiskFileNames + val onDiskButNotInDatabase = onDiskFileNames - inDatabaseFileNames + + avatarFiles + .filter { onDiskButNotInDatabase.contains(it.name) } + .forEach { it.delete() } + + photoAvatars + .filter { inDatabaseButNotOnDisk.contains(PartAuthority.getAvatarPickerFilename(it.uri)) } + .forEach { database.deleteAvatar(it) } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarRenderer.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarRenderer.kt new file mode 100644 index 0000000000..937db845ce --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/AvatarRenderer.kt @@ -0,0 +1,156 @@ +package org.thoughtcrime.securesms.avatar + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Typeface +import android.graphics.drawable.Drawable +import android.net.Uri +import androidx.appcompat.content.res.AppCompatResources +import com.airbnb.lottie.SimpleColorFilter +import com.amulyakhare.textdrawable.TextDrawable +import org.signal.core.util.concurrent.SignalExecutors +import org.thoughtcrime.securesms.mediasend.Media +import org.thoughtcrime.securesms.mms.PartAuthority +import org.thoughtcrime.securesms.profiles.AvatarHelper +import org.thoughtcrime.securesms.providers.BlobProvider +import org.thoughtcrime.securesms.util.MediaUtil +import org.whispersystems.libsignal.util.guava.Optional +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.IOException +import javax.annotation.meta.Exhaustive + +/** + * Renders Avatar objects into Media objects. This can involve creating a Bitmap, depending on the + * type of Avatar passed to `renderAvatar` + */ +object AvatarRenderer { + + private val DIMENSIONS = AvatarHelper.AVATAR_DIMENSIONS + + fun getTypeface(context: Context): Typeface { + return Typeface.createFromAsset(context.assets, "fonts/Inter-Medium.otf") + } + + fun renderAvatar(context: Context, avatar: Avatar, onAvatarRendered: (Media) -> Unit, onRenderFailed: (Throwable?) -> Unit) { + @Exhaustive + when (avatar) { + is Avatar.Resource -> renderResource(context, avatar, onAvatarRendered, onRenderFailed) + is Avatar.Vector -> renderVector(context, avatar, onAvatarRendered, onRenderFailed) + is Avatar.Photo -> renderPhoto(context, avatar, onAvatarRendered) + is Avatar.Text -> renderText(context, avatar, onAvatarRendered, onRenderFailed) + } + } + + @JvmStatic + fun createTextDrawable( + context: Context, + avatar: Avatar.Text, + inverted: Boolean = false, + size: Int = DIMENSIONS, + isRect: Boolean = true + ): Drawable { + val typeface = getTypeface(context) + val color: Int = if (inverted) { + avatar.color.backgroundColor + } else { + avatar.color.foregroundColor + } + + val builder = TextDrawable + .builder() + .beginConfig() + .fontSize(Avatars.getTextSizeForLength(context, avatar.text, size * 0.8f, size * 0.45f).toInt()) + .textColor(color) + .useFont(typeface) + .width(size) + .height(size) + .endConfig() + + return if (isRect) { + builder.buildRect(avatar.text, Color.TRANSPARENT) + } else { + builder.buildRound(avatar.text, Color.TRANSPARENT) + } + } + + private fun renderVector(context: Context, avatar: Avatar.Vector, onAvatarRendered: (Media) -> Unit, onRenderFailed: (Throwable?) -> Unit) { + renderInBackground(context, onAvatarRendered, onRenderFailed) { canvas -> + val drawableResourceId = Avatars.getDrawableResource(avatar.key) ?: return@renderInBackground Result.failure(Exception("Drawable resource for key ${avatar.key} does not exist.")) + val vector: Drawable = requireNotNull(AppCompatResources.getDrawable(context, drawableResourceId)) + vector.setBounds(0, 0, DIMENSIONS, DIMENSIONS) + + canvas.drawColor(avatar.color.backgroundColor) + vector.draw(canvas) + Result.success(Unit) + } + } + + private fun renderText(context: Context, avatar: Avatar.Text, onAvatarRendered: (Media) -> Unit, onRenderFailed: (Throwable?) -> Unit) { + renderInBackground(context, onAvatarRendered, onRenderFailed) { canvas -> + val textDrawable = createTextDrawable(context, avatar) + + canvas.drawColor(avatar.color.backgroundColor) + textDrawable.draw(canvas) + Result.success(Unit) + } + } + + private fun renderPhoto(context: Context, avatar: Avatar.Photo, onAvatarRendered: (Media) -> Unit) { + SignalExecutors.BOUNDED.execute { + val blob = BlobProvider.getInstance() + .forData(AvatarPickerStorage.read(context, PartAuthority.getAvatarPickerFilename(avatar.uri)), avatar.size) + .createForSingleSessionOnDisk(context) + + onAvatarRendered(createMedia(blob, avatar.size)) + } + } + + private fun renderResource(context: Context, avatar: Avatar.Resource, onAvatarRendered: (Media) -> Unit, onRenderFailed: (Throwable?) -> Unit) { + renderInBackground(context, onAvatarRendered, onRenderFailed) { canvas -> + val resource: Drawable = requireNotNull(AppCompatResources.getDrawable(context, avatar.resourceId)) + resource.colorFilter = SimpleColorFilter(avatar.color.foregroundColor) + + val padding = (DIMENSIONS * 0.2).toInt() + resource.setBounds(0 + padding, 0 + padding, DIMENSIONS - padding, DIMENSIONS - padding) + + canvas.drawColor(avatar.color.backgroundColor) + resource.draw(canvas) + Result.success(Unit) + } + } + + private fun renderInBackground(context: Context, onAvatarRendered: (Media) -> Unit, onRenderFailed: (Throwable?) -> Unit, drawAvatar: (Canvas) -> Result) { + SignalExecutors.BOUNDED.execute { + val canvasBitmap = Bitmap.createBitmap(DIMENSIONS, DIMENSIONS, Bitmap.Config.ARGB_8888) + val canvas = Canvas(canvasBitmap) + + val drawResult = drawAvatar(canvas) + if (drawResult.isFailure) { + canvasBitmap.recycle() + onRenderFailed(drawResult.exceptionOrNull()) + } + + val outStream = ByteArrayOutputStream() + val compressed = canvasBitmap.compress(Bitmap.CompressFormat.JPEG, 80, outStream) + canvasBitmap.recycle() + + if (!compressed) { + onRenderFailed(IOException("Failed to compress bitmap")) + return@execute + } + + val bytes = outStream.toByteArray() + val inStream = ByteArrayInputStream(bytes) + val uri = BlobProvider.getInstance().forData(inStream, bytes.size.toLong()).createForSingleSessionOnDisk(context) + + onAvatarRendered(createMedia(uri, bytes.size.toLong())) + } + } + + private fun createMedia(uri: Uri, size: Long): Media { + return Media(uri, MediaUtil.IMAGE_JPEG, System.currentTimeMillis(), DIMENSIONS, DIMENSIONS, size, 0, false, false, Optional.absent(), Optional.absent(), Optional.absent()) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/Avatars.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/Avatars.kt new file mode 100644 index 0000000000..4ffb9d5dad --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/Avatars.kt @@ -0,0 +1,153 @@ +package org.thoughtcrime.securesms.avatar + +import android.content.Context +import android.graphics.Paint +import androidx.annotation.ColorInt +import androidx.annotation.DrawableRes +import androidx.annotation.Px +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.conversation.colors.AvatarColor +import kotlin.math.abs +import kotlin.math.min + +object Avatars { + + /** + * Enum class mirroring AvatarColors codes but utilizing foreground colors for text or icon tinting. + */ + enum class ForegroundColor(private val code: String, @ColorInt val colorInt: Int) { + A100("A100", 0xFF3838F5.toInt()), + A110("A110", 0xFF1251D3.toInt()), + A120("A120", 0xFF086DA0.toInt()), + A130("A130", 0xFF067906.toInt()), + A140("A140", 0xFF661AFF.toInt()), + A150("A150", 0xFF9F00F0.toInt()), + A160("A160", 0xFFB8057C.toInt()), + A170("A170", 0xFFBE0404.toInt()), + A180("A180", 0xFF836B01.toInt()), + A190("A190", 0xFF7D6F40.toInt()), + A200("A200", 0xFF4F4F6D.toInt()), + A210("A210", 0xFF5C5C5C.toInt()); + + fun deserialize(code: String): ForegroundColor { + return values().find { it.code == code } ?: throw IllegalArgumentException() + } + + fun serialize(): String = code + } + + /** + * Mapping which associates color codes to ColorPair objects containing background and foreground colors. + */ + val colorMap: Map = ForegroundColor.values().map { + ColorPair(AvatarColor.deserialize(it.serialize()), it) + }.associateBy { + it.code + } + + val colors: List = colorMap.values.toList() + + val defaultAvatarsForSelf = linkedMapOf( + "avatar_abstract_01" to DefaultAvatar(R.drawable.ic_avatar_abstract_01, "A130"), + "avatar_abstract_02" to DefaultAvatar(R.drawable.ic_avatar_abstract_02, "A120"), + "avatar_abstract_03" to DefaultAvatar(R.drawable.ic_avatar_abstract_03, "A170"), + "avatar_cat" to DefaultAvatar(R.drawable.ic_avatar_cat, "A190"), + "avatar_dog" to DefaultAvatar(R.drawable.ic_avatar_dog, "A140"), + "avatar_fox" to DefaultAvatar(R.drawable.ic_avatar_fox, "A190"), + "avatar_tucan" to DefaultAvatar(R.drawable.ic_avatar_tucan, "A120"), + "avatar_sloth" to DefaultAvatar(R.drawable.ic_avatar_sloth, "A160"), + "avatar_dinosaur" to DefaultAvatar(R.drawable.ic_avatar_dinosour, "A130"), + "avatar_pig" to DefaultAvatar(R.drawable.ic_avatar_pig, "A180"), + "avatar_incognito" to DefaultAvatar(R.drawable.ic_avatar_incognito, "A220"), + "avatar_ghost" to DefaultAvatar(R.drawable.ic_avatar_ghost, "A100") + ) + + val defaultAvatarsForGroup = linkedMapOf( + "avatar_heart" to DefaultAvatar(R.drawable.ic_avatar_heart, "A180"), + "avatar_house" to DefaultAvatar(R.drawable.ic_avatar_house, "A120"), + "avatar_melon" to DefaultAvatar(R.drawable.ic_avatar_melon, "A110"), + "avatar_drink" to DefaultAvatar(R.drawable.ic_avatar_drink, "A170"), + "avatar_celebration" to DefaultAvatar(R.drawable.ic_avatar_celebration, "A100"), + "avatar_balloon" to DefaultAvatar(R.drawable.ic_avatar_balloon, "A220"), + "avatar_book" to DefaultAvatar(R.drawable.ic_avatar_book, "A100"), + "avatar_briefcase" to DefaultAvatar(R.drawable.ic_avatar_briefcase, "A180"), + "avatar_sunset" to DefaultAvatar(R.drawable.ic_avatar_sunset, "A120"), + "avatar_surfboard" to DefaultAvatar(R.drawable.ic_avatar_surfboard, "A110"), + "avatar_soccerball" to DefaultAvatar(R.drawable.ic_avatar_soccerball, "A130"), + "avatar_football" to DefaultAvatar(R.drawable.ic_avatar_football, "A220"), + ) + + @DrawableRes + fun getDrawableResource(key: String): Int? { + val defaultAvatar = defaultAvatarsForSelf.getOrDefault(key, defaultAvatarsForGroup[key]) + + return defaultAvatar?.vectorDrawableId + } + + private fun textPaint(context: Context) = Paint().apply { + isAntiAlias = true + typeface = AvatarRenderer.getTypeface(context) + textSize = 1f + } + + /** + * Calculate the text size for a give string using a maximum desired width and a maximum desired font size. + */ + @JvmStatic + fun getTextSizeForLength(context: Context, text: String, @Px maxWidth: Float, @Px maxSize: Float): Float { + val paint = textPaint(context) + return branchSizes(0f, maxWidth / 2, maxWidth, maxSize, paint, text) + } + + /** + * Uses binary search to determine optimal font size to within 1% given the input parameters. + */ + private fun branchSizes(@Px lastFontSize: Float, @Px fontSize: Float, @Px target: Float, @Px maxFontSize: Float, paint: Paint, text: String): Float { + paint.textSize = fontSize + val textWidth = paint.measureText(text) + val delta = abs(lastFontSize - fontSize) / 2f + val isWithinThreshold = abs(1f - (textWidth / target)) <= 0.01f + + if (textWidth == 0f) { + return maxFontSize + } + + if (delta == 0f) { + return min(maxFontSize, fontSize) + } + + return when { + fontSize >= maxFontSize -> { + maxFontSize + } + isWithinThreshold -> { + fontSize + } + textWidth > target -> { + branchSizes(fontSize, fontSize - delta, target, maxFontSize, paint, text) + } + else -> { + branchSizes(fontSize, fontSize + delta, target, maxFontSize, paint, text) + } + } + } + + @JvmStatic + fun getForegroundColor(avatarColor: AvatarColor): ForegroundColor { + return ForegroundColor.values().firstOrNull { it.serialize() == avatarColor.serialize() } ?: ForegroundColor.A210 + } + + data class DefaultAvatar( + @DrawableRes val vectorDrawableId: Int, + val colorCode: String + ) + + data class ColorPair( + val backgroundAvatarColor: AvatarColor, + val foregroundAvatarColor: ForegroundColor + ) { + @ColorInt val backgroundColor: Int = backgroundAvatarColor.colorInt() + @ColorInt val foregroundColor: Int = foregroundAvatarColor.colorInt + val code: String = backgroundAvatarColor.serialize() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/photo/PhotoEditorFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/photo/PhotoEditorFragment.kt new file mode 100644 index 0000000000..8ba3630620 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/photo/PhotoEditorFragment.kt @@ -0,0 +1,65 @@ +package org.thoughtcrime.securesms.avatar.photo + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.fragment.app.commit +import androidx.fragment.app.setFragmentResult +import androidx.navigation.Navigation +import org.signal.core.util.ThreadUtil +import org.signal.core.util.concurrent.SignalExecutors +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.avatar.AvatarBundler +import org.thoughtcrime.securesms.avatar.AvatarPickerStorage +import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.providers.BlobProvider +import org.thoughtcrime.securesms.scribbles.ImageEditorFragment + +class PhotoEditorFragment : Fragment(R.layout.avatar_photo_editor_fragment), ImageEditorFragment.Controller { + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val args = PhotoEditorFragmentArgs.fromBundle(requireArguments()) + val photo = AvatarBundler.extractPhoto(args.photoAvatar) + val imageEditorFragment = ImageEditorFragment.newInstanceForAvatarEdit(photo.uri) + + childFragmentManager.commit { + add(R.id.fragment_container, imageEditorFragment, IMAGE_EDITOR) + } + } + + override fun onTouchEventsNeeded(needed: Boolean) { + } + + override fun onRequestFullScreen(fullScreen: Boolean, hideKeyboard: Boolean) { + } + + override fun onDoneEditing() { + val args = PhotoEditorFragmentArgs.fromBundle(requireArguments()) + val applicationContext = requireContext().applicationContext + val imageEditorFragment: ImageEditorFragment = childFragmentManager.findFragmentByTag(IMAGE_EDITOR) as ImageEditorFragment + + SignalExecutors.BOUNDED.execute { + val editedImageUri = imageEditorFragment.renderToSingleUseBlob() + val size = BlobProvider.getFileSize(editedImageUri) ?: 0 + val inputStream = BlobProvider.getInstance().getStream(applicationContext, editedImageUri) + val onDiskUri = AvatarPickerStorage.save(applicationContext, inputStream) + val photo = AvatarBundler.extractPhoto(args.photoAvatar) + val database = DatabaseFactory.getAvatarPickerDatabase(applicationContext) + val newPhoto = photo.copy(uri = onDiskUri, size = size) + + database.update(newPhoto) + BlobProvider.getInstance().delete(requireContext(), photo.uri) + + ThreadUtil.runOnMain { + setFragmentResult(REQUEST_KEY_EDIT, AvatarBundler.bundlePhoto(newPhoto)) + Navigation.findNavController(requireView()).popBackStack() + } + } + } + + companion object { + const val REQUEST_KEY_EDIT = "org.thoughtcrime.securesms.avatar.photo.EDIT" + + private const val IMAGE_EDITOR = "image_editor" + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerFragment.kt new file mode 100644 index 0000000000..519c92cd93 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerFragment.kt @@ -0,0 +1,223 @@ +package org.thoughtcrime.securesms.avatar.picker + +import android.Manifest +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.view.Gravity +import android.view.View +import android.widget.PopupMenu +import android.widget.Toast +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.Fragment +import androidx.fragment.app.setFragmentResult +import androidx.fragment.app.setFragmentResultListener +import androidx.fragment.app.viewModels +import androidx.navigation.Navigation +import androidx.recyclerview.widget.RecyclerView +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.avatar.Avatar +import org.thoughtcrime.securesms.avatar.AvatarBundler +import org.thoughtcrime.securesms.avatar.photo.PhotoEditorFragment +import org.thoughtcrime.securesms.avatar.text.TextAvatarCreationFragment +import org.thoughtcrime.securesms.avatar.vector.VectorAvatarCreationFragment +import org.thoughtcrime.securesms.components.ButtonStripItemView +import org.thoughtcrime.securesms.components.recyclerview.GridDividerDecoration +import org.thoughtcrime.securesms.groups.ParcelableGroupId +import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity +import org.thoughtcrime.securesms.mediasend.Media +import org.thoughtcrime.securesms.permissions.Permissions +import org.thoughtcrime.securesms.util.MappingAdapter +import org.thoughtcrime.securesms.util.ViewUtil +import org.thoughtcrime.securesms.util.visible +import java.util.Objects + +/** + * Primary Avatar picker fragment, displays current user avatar and a list of recently used avatars and defaults. + */ +class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) { + + companion object { + const val REQUEST_KEY_SELECT_AVATAR = "org.thoughtcrime.securesms.avatar.picker.SELECT_AVATAR" + const val SELECT_AVATAR_MEDIA = "org.thoughtcrime.securesms.avatar.picker.SELECT_AVATAR_MEDIA" + + private const val REQUEST_CODE_SELECT_IMAGE = 1 + } + + private val viewModel: AvatarPickerViewModel by viewModels(factoryProducer = this::createFactory) + + private fun createFactory(): AvatarPickerViewModel.Factory { + val args = AvatarPickerFragmentArgs.fromBundle(requireArguments()) + val groupId = ParcelableGroupId.get(args.groupId) + + return AvatarPickerViewModel.Factory(AvatarPickerRepository(requireContext()), groupId, args.isNewGroup, args.groupAvatarMedia) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val toolbar: Toolbar = view.findViewById(R.id.avatar_picker_toolbar) + val recycler: RecyclerView = view.findViewById(R.id.avatar_picker_recycler) + val cameraButton: ButtonStripItemView = view.findViewById(R.id.avatar_picker_camera) + val photoButton: ButtonStripItemView = view.findViewById(R.id.avatar_picker_photo) + val textButton: ButtonStripItemView = view.findViewById(R.id.avatar_picker_text) + val saveButton: View = view.findViewById(R.id.avatar_picker_save) + val clearButton: View = view.findViewById(R.id.avatar_picker_clear) + + recycler.addItemDecoration(GridDividerDecoration(4, ViewUtil.dpToPx(16))) + + val adapter = MappingAdapter() + AvatarPickerItem.register(adapter, this::onAvatarClick, this::onAvatarLongClick) + + recycler.adapter = adapter + + val avatarViewHolder = AvatarPickerItem.ViewHolder(view) + + viewModel.state.observe(viewLifecycleOwner) { state -> + if (state.currentAvatar != null) { + avatarViewHolder.bind(AvatarPickerItem.Model(state.currentAvatar, false)) + } + + clearButton.visible = state.canClear + + val wasEnabled = saveButton.isEnabled + saveButton.isEnabled = state.canSave + if (wasEnabled != state.canSave) { + val alpha = if (state.canSave) 1f else 0.5f + saveButton.animate().cancel() + saveButton.animate().alpha(alpha) + } + + adapter.submitList(state.selectableAvatars.map { AvatarPickerItem.Model(it, it == state.currentAvatar) }) + } + + toolbar.setNavigationOnClickListener { Navigation.findNavController(it).popBackStack() } + cameraButton.setOnIconClickedListener { openCameraCapture() } + photoButton.setOnIconClickedListener { openGallery() } + textButton.setOnIconClickedListener { openTextEditor(null) } + saveButton.setOnClickListener { v -> + viewModel.save { + setFragmentResult( + REQUEST_KEY_SELECT_AVATAR, + Bundle().apply { + putParcelable(SELECT_AVATAR_MEDIA, it) + } + ) + Navigation.findNavController(v).popBackStack() + } + } + clearButton.setOnClickListener { viewModel.clear() } + + setFragmentResultListener(TextAvatarCreationFragment.REQUEST_KEY_TEXT) { _, bundle -> + val text = AvatarBundler.extractText(bundle) + viewModel.onAvatarEditCompleted(text) + } + + setFragmentResultListener(VectorAvatarCreationFragment.REQUEST_KEY_VECTOR) { _, bundle -> + val vector = AvatarBundler.extractVector(bundle) + viewModel.onAvatarEditCompleted(vector) + } + + setFragmentResultListener(PhotoEditorFragment.REQUEST_KEY_EDIT) { _, bundle -> + val photo = AvatarBundler.extractPhoto(bundle) + viewModel.onAvatarEditCompleted(photo) + } + } + + override fun onResume() { + super.onResume() + ViewUtil.hideKeyboard(requireContext(), requireView()) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == REQUEST_CODE_SELECT_IMAGE && resultCode == Activity.RESULT_OK && data != null) { + val media: Media = Objects.requireNonNull(data.getParcelableExtra(AvatarSelectionActivity.EXTRA_MEDIA)) + viewModel.onAvatarPhotoSelectionCompleted(media) + } else { + super.onActivityResult(requestCode, resultCode, data) + } + } + + private fun onAvatarClick(avatar: Avatar, isSelected: Boolean) { + if (isSelected) { + openEditor(avatar) + } else { + viewModel.onAvatarSelectedFromGrid(avatar) + } + } + + private fun onAvatarLongClick(anchorView: View, avatar: Avatar): Boolean { + val menuRes = when (avatar) { + is Avatar.Photo -> R.menu.avatar_picker_context + is Avatar.Text -> R.menu.avatar_picker_context + is Avatar.Vector -> return false + is Avatar.Resource -> return false + } + + val popup = PopupMenu(context, anchorView, Gravity.TOP) + popup.menuInflater.inflate(menuRes, popup.menu) + popup.setOnMenuItemClickListener { menuItem -> + when (menuItem.itemId) { + R.id.action_delete -> viewModel.delete(avatar) + } + + true + } + popup.show() + + return true + } + + fun openEditor(avatar: Avatar) { + when (avatar) { + is Avatar.Photo -> openPhotoEditor(avatar) + is Avatar.Resource -> throw UnsupportedOperationException() + is Avatar.Text -> openTextEditor(avatar) + is Avatar.Vector -> openVectorEditor(avatar) + } + } + + fun openPhotoEditor(photo: Avatar.Photo) { + Navigation.findNavController(requireView()) + .navigate(AvatarPickerFragmentDirections.actionAvatarPickerFragmentToAvatarPhotoEditorFragment(AvatarBundler.bundlePhoto(photo))) + } + + fun openVectorEditor(vector: Avatar.Vector) { + Navigation.findNavController(requireView()) + .navigate(AvatarPickerFragmentDirections.actionAvatarPickerFragmentToVectorAvatarCreationFragment(AvatarBundler.bundleVector(vector))) + } + + fun openTextEditor(text: Avatar.Text?) { + val bundle = if (text != null) AvatarBundler.bundleText(text) else null + Navigation.findNavController(requireView()) + .navigate(AvatarPickerFragmentDirections.actionAvatarPickerFragmentToTextAvatarCreationFragment(bundle)) + } + + fun openCameraCapture() { + Permissions.with(this) + .request(Manifest.permission.CAMERA) + .ifNecessary() + .onAllGranted { + val intent = AvatarSelectionActivity.getIntentForCameraCapture(requireContext()) + startActivityForResult(intent, REQUEST_CODE_SELECT_IMAGE) + } + .onAnyDenied { + Toast.makeText(requireContext(), R.string.AvatarSelectionBottomSheetDialogFragment__taking_a_photo_requires_the_camera_permission, Toast.LENGTH_SHORT) + .show() + } + .execute() + } + + fun openGallery() { + Permissions.with(this) + .request(Manifest.permission.READ_EXTERNAL_STORAGE) + .ifNecessary() + .onAllGranted { + val intent = AvatarSelectionActivity.getIntentForGallery(requireContext()) + startActivityForResult(intent, REQUEST_CODE_SELECT_IMAGE) + } + .onAnyDenied { + Toast.makeText(requireContext(), R.string.AvatarSelectionBottomSheetDialogFragment__viewing_your_gallery_requires_the_storage_permission, Toast.LENGTH_SHORT) + .show() + } + .execute() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerItem.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerItem.kt new file mode 100644 index 0000000000..65e27cbfac --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerItem.kt @@ -0,0 +1,150 @@ +package org.thoughtcrime.securesms.avatar.picker + +import android.util.TypedValue +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.view.setPadding +import androidx.core.widget.addTextChangedListener +import com.airbnb.lottie.SimpleColorFilter +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.avatar.Avatar +import org.thoughtcrime.securesms.avatar.AvatarRenderer +import org.thoughtcrime.securesms.avatar.Avatars +import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader +import org.thoughtcrime.securesms.mms.GlideApp +import org.thoughtcrime.securesms.util.MappingAdapter +import org.thoughtcrime.securesms.util.MappingModel +import org.thoughtcrime.securesms.util.MappingViewHolder +import org.thoughtcrime.securesms.util.visible + +typealias OnAvatarClickListener = (Avatar, Boolean) -> Unit +typealias OnAvatarLongClickListener = (View, Avatar) -> Boolean + +object AvatarPickerItem { + + private val SELECTION_CHANGED = Any() + + fun register(adapter: MappingAdapter, onAvatarClickListener: OnAvatarClickListener, onAvatarLongClickListener: OnAvatarLongClickListener) { + adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it, onAvatarClickListener, onAvatarLongClickListener) }, R.layout.avatar_picker_item)) + } + + class Model(val avatar: Avatar, val isSelected: Boolean) : MappingModel { + override fun areItemsTheSame(newItem: Model): Boolean = avatar.isSameAs(newItem.avatar) + + override fun areContentsTheSame(newItem: Model): Boolean = avatar == newItem.avatar && isSelected == newItem.isSelected + + override fun getChangePayload(newItem: Model): Any? { + return if (newItem.avatar == avatar && isSelected != newItem.isSelected) { + SELECTION_CHANGED + } else { + null + } + } + } + + class ViewHolder( + itemView: View, + private val onAvatarClickListener: OnAvatarClickListener? = null, + private val onAvatarLongClickListener: OnAvatarLongClickListener? = null + ) : MappingViewHolder(itemView) { + + private val imageView: ImageView = itemView.findViewById(R.id.avatar_picker_item_image) + private val textView: TextView = itemView.findViewById(R.id.avatar_picker_item_text) + private val selectedFader: View? = itemView.findViewById(R.id.avatar_picker_item_fader) + private val selectedOverlay: View? = itemView.findViewById(R.id.avatar_picker_item_selection_overlay) + + init { + textView.typeface = AvatarRenderer.getTypeface(context) + textView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> + updateTextSize() + } + textView.addTextChangedListener( + afterTextChanged = { + updateTextSize() + } + ) + } + + private fun updateTextSize() { + textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, Avatars.getTextSizeForLength(context, textView.text.toString(), textView.measuredWidth * 0.8f, textView.measuredHeight * 0.45f)) + } + + override fun bind(model: Model) { + val alpha = if (model.isSelected) 1f else 0f + val scale = if (model.isSelected) 0.9f else 1f + + imageView.animate().cancel() + textView.animate().cancel() + selectedOverlay?.animate()?.cancel() + selectedFader?.animate()?.cancel() + + if (model.isSelected) { + itemView.setOnLongClickListener { + onAvatarLongClickListener?.invoke(itemView, model.avatar) ?: false + } + } else { + itemView.setOnLongClickListener(null) + } + + itemView.setOnClickListener { onAvatarClickListener?.invoke(model.avatar, model.isSelected) } + + if (payload.isNotEmpty() && payload.contains(SELECTION_CHANGED)) { + imageView.animate().scaleX(scale).scaleY(scale) + textView.animate().scaleX(scale).scaleY(scale) + selectedOverlay?.animate()?.alpha(alpha) + selectedFader?.animate()?.alpha(alpha) + return + } + + imageView.scaleX = scale + imageView.scaleY = scale + textView.scaleX = scale + textView.scaleY = scale + selectedFader?.alpha = alpha + selectedOverlay?.alpha = alpha + + imageView.clearColorFilter() + imageView.setPadding(0) + + when (model.avatar) { + is Avatar.Text -> { + textView.visible = true + + if (textView.text.toString() != model.avatar.text) { + textView.text = model.avatar.text + } + + imageView.setImageDrawable(null) + imageView.background.colorFilter = SimpleColorFilter(model.avatar.color.backgroundColor) + textView.setTextColor(model.avatar.color.foregroundColor) + } + is Avatar.Vector -> { + textView.visible = false + + val drawableId = Avatars.getDrawableResource(model.avatar.key) + if (drawableId == null) { + imageView.setImageDrawable(null) + } else { + imageView.setImageDrawable(AppCompatResources.getDrawable(context, drawableId)) + } + + imageView.background.colorFilter = SimpleColorFilter(model.avatar.color.backgroundColor) + } + is Avatar.Photo -> { + textView.visible = false + GlideApp.with(imageView).load(DecryptableStreamUriLoader.DecryptableUri(model.avatar.uri)).into(imageView) + } + is Avatar.Resource -> { + imageView.setPadding((imageView.width * 0.2).toInt()) + textView.visible = false + GlideApp.with(imageView).clear(imageView) + imageView.setImageResource(model.avatar.resourceId) + imageView.colorFilter = SimpleColorFilter(model.avatar.color.foregroundColor) + imageView.background.colorFilter = SimpleColorFilter(model.avatar.color.backgroundColor) + } + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerRepository.kt new file mode 100644 index 0000000000..36443a1f52 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerRepository.kt @@ -0,0 +1,175 @@ +package org.thoughtcrime.securesms.avatar.picker + +import android.content.Context +import android.net.Uri +import android.widget.Toast +import io.reactivex.rxjava3.core.Single +import org.signal.core.util.StreamUtil +import org.signal.core.util.ThreadUtil +import org.signal.core.util.concurrent.SignalExecutors +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.avatar.Avatar +import org.thoughtcrime.securesms.avatar.AvatarPickerStorage +import org.thoughtcrime.securesms.avatar.AvatarRenderer +import org.thoughtcrime.securesms.avatar.Avatars +import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.groups.GroupId +import org.thoughtcrime.securesms.mediasend.Media +import org.thoughtcrime.securesms.profiles.AvatarHelper +import org.thoughtcrime.securesms.providers.BlobProvider +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.util.NameUtil +import org.whispersystems.signalservice.api.util.StreamDetails +import java.io.IOException + +private val TAG = Log.tag(AvatarPickerRepository::class.java) + +class AvatarPickerRepository(context: Context) { + + private val applicationContext = context.applicationContext + + fun getAvatarForSelf(): Single = Single.fromCallable { + val details: StreamDetails? = AvatarHelper.getSelfProfileAvatarStream(applicationContext) + if (details != null) { + try { + val bytes = StreamUtil.readFully(details.stream) + Avatar.Photo( + BlobProvider.getInstance().forData(bytes).createForSingleSessionInMemory(), + details.length, + Avatar.DatabaseId.DoNotPersist + ) + } catch (e: IOException) { + Log.w(TAG, "Failed to read avatar!") + getDefaultAvatarForSelf() + } + } else { + getDefaultAvatarForSelf() + } + } + + fun getAvatarForGroup(groupId: GroupId): Single = Single.fromCallable { + val recipient = Recipient.externalGroupExact(applicationContext, groupId) + + if (AvatarHelper.hasAvatar(applicationContext, recipient.id)) { + try { + val bytes = AvatarHelper.getAvatarBytes(applicationContext, recipient.id) + Avatar.Photo( + BlobProvider.getInstance().forData(bytes).createForSingleSessionInMemory(), + AvatarHelper.getAvatarLength(applicationContext, recipient.id), + Avatar.DatabaseId.DoNotPersist + ) + } catch (e: IOException) { + Log.w(TAG, "Failed to read group avatar!") + getDefaultAvatarForGroup() + } + } else { + getDefaultAvatarForGroup() + } + } + + fun getPersistedAvatarsForSelf(): Single> = Single.fromCallable { + DatabaseFactory.getAvatarPickerDatabase(applicationContext).getAvatarsForSelf() + } + + fun getPersistedAvatarsForGroup(groupId: GroupId): Single> = Single.fromCallable { + DatabaseFactory.getAvatarPickerDatabase(applicationContext).getAvatarsForGroup(groupId) + } + + fun getDefaultAvatarsForSelf(): Single> = Single.fromCallable { + Avatars.defaultAvatarsForSelf.entries.mapIndexed { index, entry -> + Avatar.Vector(entry.key, color = Avatars.colors[index % Avatars.colors.size], Avatar.DatabaseId.NotSet) + } + } + + fun getDefaultAvatarsForGroup(): Single> = Single.fromCallable { + Avatars.defaultAvatarsForGroup.entries.mapIndexed { index, entry -> + Avatar.Vector(entry.key, color = Avatars.colors[index % Avatars.colors.size], Avatar.DatabaseId.NotSet) + } + } + + fun writeMediaToMultiSessionStorage(media: Media, onMediaWrittenToMultiSessionStorage: (Uri) -> Unit) { + SignalExecutors.BOUNDED.execute { + onMediaWrittenToMultiSessionStorage(AvatarPickerStorage.save(applicationContext, media)) + } + } + + fun persistAvatarForSelf(avatar: Avatar, onPersisted: (Avatar) -> Unit) { + SignalExecutors.BOUNDED.execute { + val avatarDatabase = DatabaseFactory.getAvatarPickerDatabase(applicationContext) + val savedAvatar = avatarDatabase.saveAvatarForSelf(avatar) + avatarDatabase.markUsage(savedAvatar) + onPersisted(savedAvatar) + } + } + + fun persistAvatarForGroup(avatar: Avatar, groupId: GroupId, onPersisted: (Avatar) -> Unit) { + SignalExecutors.BOUNDED.execute { + val avatarDatabase = DatabaseFactory.getAvatarPickerDatabase(applicationContext) + val savedAvatar = avatarDatabase.saveAvatarForGroup(avatar, groupId) + avatarDatabase.markUsage(savedAvatar) + onPersisted(savedAvatar) + } + } + + fun persistAndCreateMediaForSelf(avatar: Avatar, onSaved: (Media) -> Unit) { + SignalExecutors.BOUNDED.execute { + if (avatar.databaseId !is Avatar.DatabaseId.DoNotPersist) { + persistAvatarForSelf(avatar) { + AvatarRenderer.renderAvatar(applicationContext, avatar, onSaved, this::handleRenderFailure) + } + } else { + AvatarRenderer.renderAvatar(applicationContext, avatar, onSaved, this::handleRenderFailure) + } + } + } + + fun persistAndCreateMediaForGroup(avatar: Avatar, groupId: GroupId, onSaved: (Media) -> Unit) { + SignalExecutors.BOUNDED.execute { + if (avatar.databaseId !is Avatar.DatabaseId.DoNotPersist) { + persistAvatarForGroup(avatar, groupId) { + AvatarRenderer.renderAvatar(applicationContext, avatar, onSaved, this::handleRenderFailure) + } + } else { + AvatarRenderer.renderAvatar(applicationContext, avatar, onSaved, this::handleRenderFailure) + } + } + } + + fun createMediaForNewGroup(avatar: Avatar, onSaved: (Media) -> Unit) { + SignalExecutors.BOUNDED.execute { + AvatarRenderer.renderAvatar(applicationContext, avatar, onSaved, this::handleRenderFailure) + } + } + + fun handleRenderFailure(throwable: Throwable?) { + Log.w(TAG, "Failed to render avatar.", throwable) + ThreadUtil.postToMain { + Toast.makeText(applicationContext, R.string.AvatarPickerRepository__failed_to_save_avatar, Toast.LENGTH_SHORT).show() + } + } + + fun getDefaultAvatarForSelf(): Avatar { + val initials = NameUtil.getAbbreviation(Recipient.self().getDisplayName(applicationContext)) + + return if (initials.isNullOrBlank()) { + Avatar.getDefaultForSelf() + } else { + Avatar.Text(initials, Avatars.colors.random(), Avatar.DatabaseId.NotSet) + } + } + + fun getDefaultAvatarForGroup(): Avatar { + return Avatar.getDefaultForGroup() + } + + fun delete(avatar: Avatar, onDelete: () -> Unit) { + SignalExecutors.BOUNDED.execute { + if (avatar.databaseId is Avatar.DatabaseId.Saved) { + val avatarDatabase = DatabaseFactory.getAvatarPickerDatabase(applicationContext) + avatarDatabase.deleteAvatar(avatar) + } + onDelete() + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerState.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerState.kt new file mode 100644 index 0000000000..e58d166e8d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerState.kt @@ -0,0 +1,10 @@ +package org.thoughtcrime.securesms.avatar.picker + +import org.thoughtcrime.securesms.avatar.Avatar + +data class AvatarPickerState( + val currentAvatar: Avatar? = null, + val selectableAvatars: List = listOf(), + val canSave: Boolean = false, + val canClear: Boolean = false +) diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerViewModel.kt new file mode 100644 index 0000000000..436a0f3d14 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerViewModel.kt @@ -0,0 +1,193 @@ +package org.thoughtcrime.securesms.avatar.picker + +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.schedulers.Schedulers +import org.thoughtcrime.securesms.avatar.Avatar +import org.thoughtcrime.securesms.groups.GroupId +import org.thoughtcrime.securesms.mediasend.Media +import org.thoughtcrime.securesms.util.livedata.Store + +sealed class AvatarPickerViewModel(private val repository: AvatarPickerRepository) : ViewModel() { + + private val disposables = CompositeDisposable() + private val store = Store(AvatarPickerState()) + + val state: LiveData = store.stateLiveData + + protected abstract fun getAvatar(): Single + protected abstract fun getDefaultAvatarFromRepository(): Avatar + protected abstract fun getPersistedAvatars(): Single> + protected abstract fun getDefaultAvatars(): Single> + protected abstract fun persistAvatar(avatar: Avatar, onPersisted: (Avatar) -> Unit) + protected abstract fun persistAndCreateMedia(avatar: Avatar, onSaved: (Media) -> Unit) + + fun delete(avatar: Avatar) { + repository.delete(avatar) { + refreshSelectableAvatars() + } + } + + fun clear() { + store.update { + val avatar = getDefaultAvatarFromRepository() + it.copy(currentAvatar = avatar, canSave = isSaveable(avatar), canClear = false) + } + } + + fun save(onSaved: (Media) -> Unit) { + val avatar = store.state.currentAvatar ?: throw AssertionError() + persistAndCreateMedia(avatar, onSaved) + } + + fun onAvatarSelectedFromGrid(avatar: Avatar) { + store.update { it.copy(currentAvatar = avatar, canSave = isSaveable(avatar), canClear = true) } + } + + fun onAvatarEditCompleted(avatar: Avatar) { + persistAvatar(avatar) { saved -> + store.update { it.copy(currentAvatar = saved, canSave = isSaveable(saved), canClear = true) } + refreshSelectableAvatars() + } + } + + fun onAvatarPhotoSelectionCompleted(media: Media) { + repository.writeMediaToMultiSessionStorage(media) { multiSessionUri -> + persistAvatar(Avatar.Photo(multiSessionUri, media.size, Avatar.DatabaseId.NotSet)) { avatar -> + store.update { it.copy(currentAvatar = avatar, canSave = isSaveable(avatar), canClear = true) } + refreshSelectableAvatars() + } + } + } + + protected fun refreshAvatar() { + disposables.add( + getAvatar().subscribeOn(Schedulers.io()).subscribe { avatar -> + store.update { it.copy(currentAvatar = avatar, canSave = isSaveable(avatar), canClear = !isSaveable(avatar)) } + } + ) + } + + protected fun refreshSelectableAvatars() { + disposables.add( + Single.zip(getPersistedAvatars(), getDefaultAvatars()) { custom, def -> + val customKeys = custom.filterIsInstance(Avatar.Vector::class.java).map { it.key } + custom + def.filterNot { + it is Avatar.Vector && customKeys.contains(it.key) + } + }.subscribeOn(Schedulers.io()).subscribe { avatars -> + store.update { it.copy(selectableAvatars = avatars) } + } + ) + } + + private fun isSaveable(avatar: Avatar) = !(avatar is Avatar.Photo && avatar.databaseId == Avatar.DatabaseId.DoNotPersist) + + override fun onCleared() { + disposables.dispose() + } + + private class SelfAvatarPickerViewModel(private val repository: AvatarPickerRepository) : AvatarPickerViewModel(repository) { + + init { + refreshAvatar() + refreshSelectableAvatars() + } + + override fun getAvatar(): Single = repository.getAvatarForSelf() + override fun getDefaultAvatarFromRepository(): Avatar = repository.getDefaultAvatarForSelf() + override fun getPersistedAvatars(): Single> = repository.getPersistedAvatarsForSelf() + override fun getDefaultAvatars(): Single> = repository.getDefaultAvatarsForSelf() + + override fun persistAvatar(avatar: Avatar, onPersisted: (Avatar) -> Unit) { + repository.persistAvatarForSelf(avatar, onPersisted) + } + + override fun persistAndCreateMedia(avatar: Avatar, onSaved: (Media) -> Unit) { + repository.persistAndCreateMediaForSelf(avatar, onSaved) + } + } + + private class GroupAvatarPickerViewModel( + private val groupId: GroupId, + private val repository: AvatarPickerRepository, + groupAvatarMedia: Media? + ) : AvatarPickerViewModel(repository) { + + private val initialAvatar: Avatar? = groupAvatarMedia?.let { Avatar.Photo(it.uri, it.size, Avatar.DatabaseId.DoNotPersist) } + + init { + refreshAvatar() + refreshSelectableAvatars() + } + + override fun getAvatar(): Single { + return if (initialAvatar != null) { + Single.just(initialAvatar) + } else { + repository.getAvatarForGroup(groupId) + } + } + + override fun getDefaultAvatarFromRepository(): Avatar = repository.getDefaultAvatarForGroup() + override fun getPersistedAvatars(): Single> = repository.getPersistedAvatarsForGroup(groupId) + override fun getDefaultAvatars(): Single> = repository.getDefaultAvatarsForGroup() + + override fun persistAvatar(avatar: Avatar, onPersisted: (Avatar) -> Unit) { + repository.persistAvatarForGroup(avatar, groupId, onPersisted) + } + + override fun persistAndCreateMedia(avatar: Avatar, onSaved: (Media) -> Unit) { + repository.persistAndCreateMediaForGroup(avatar, groupId, onSaved) + } + } + + private class NewGroupAvatarPickerViewModel( + private val repository: AvatarPickerRepository, + initialMedia: Media? + ) : AvatarPickerViewModel(repository) { + + private val initialAvatar: Avatar? = initialMedia?.let { Avatar.Photo(it.uri, it.size, Avatar.DatabaseId.DoNotPersist) } + + init { + refreshAvatar() + refreshSelectableAvatars() + } + + override fun getAvatar(): Single { + return if (initialAvatar != null) { + Single.just(initialAvatar) + } else { + Single.just(getDefaultAvatarFromRepository()) + } + } + + override fun getDefaultAvatarFromRepository(): Avatar = repository.getDefaultAvatarForGroup() + override fun getPersistedAvatars(): Single> = Single.just(listOf()) + override fun getDefaultAvatars(): Single> = repository.getDefaultAvatarsForGroup() + override fun persistAvatar(avatar: Avatar, onPersisted: (Avatar) -> Unit) = onPersisted(avatar) + override fun persistAndCreateMedia(avatar: Avatar, onSaved: (Media) -> Unit) = repository.createMediaForNewGroup(avatar, onSaved) + } + + class Factory( + private val repository: AvatarPickerRepository, + private val groupId: GroupId?, + private val isNewGroup: Boolean, + private val groupAvatarMedia: Media? + ) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + val viewModel = if (groupId == null && !isNewGroup) { + SelfAvatarPickerViewModel(repository) + } else if (groupId == null) { + NewGroupAvatarPickerViewModel(repository, groupAvatarMedia) + } else { + GroupAvatarPickerViewModel(groupId, repository, groupAvatarMedia) + } + + return requireNotNull(modelClass.cast(viewModel)) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/text/TextAvatarCreationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/text/TextAvatarCreationFragment.kt new file mode 100644 index 0000000000..aae39d6a47 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/text/TextAvatarCreationFragment.kt @@ -0,0 +1,148 @@ +package org.thoughtcrime.securesms.avatar.text + +import android.os.Bundle +import android.view.View +import android.view.inputmethod.EditorInfo +import android.widget.EditText +import androidx.appcompat.widget.Toolbar +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet +import androidx.core.widget.doAfterTextChanged +import androidx.fragment.app.Fragment +import androidx.fragment.app.setFragmentResult +import androidx.fragment.app.viewModels +import androidx.navigation.Navigation +import androidx.recyclerview.widget.RecyclerView +import androidx.transition.TransitionManager +import com.google.android.material.tabs.TabLayout +import org.signal.core.util.EditTextUtil +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.avatar.Avatar +import org.thoughtcrime.securesms.avatar.AvatarBundler +import org.thoughtcrime.securesms.avatar.AvatarColorItem +import org.thoughtcrime.securesms.avatar.Avatars +import org.thoughtcrime.securesms.avatar.picker.AvatarPickerItem +import org.thoughtcrime.securesms.components.BoldSelectionTabItem +import org.thoughtcrime.securesms.components.ControllableTabLayout +import org.thoughtcrime.securesms.components.recyclerview.GridDividerDecoration +import org.thoughtcrime.securesms.util.MappingAdapter +import org.thoughtcrime.securesms.util.ViewUtil + +/** + * Fragment to create an avatar based off of a Vector or Text (via a pager) + */ +class TextAvatarCreationFragment : Fragment(R.layout.text_avatar_creation_fragment_hidden_recycler) { + + private val viewModel: TextAvatarCreationViewModel by viewModels(factoryProducer = this::createFactory) + + private lateinit var textInput: EditText + private lateinit var recycler: RecyclerView + + private val withRecyclerSet = ConstraintSet() + private val withoutRecyclerSet = ConstraintSet() + + private fun createFactory(): TextAvatarCreationViewModel.Factory { + val args = TextAvatarCreationFragmentArgs.fromBundle(requireArguments()) + val textBundle = args.textAvatar + val text = if (textBundle != null) { + AvatarBundler.extractText(textBundle) + } else { + Avatar.Text("", Avatars.colors.random(), Avatar.DatabaseId.NotSet) + } + + return TextAvatarCreationViewModel.Factory(text) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val toolbar: Toolbar = view.findViewById(R.id.text_avatar_creation_toolbar) + val tabLayout: ControllableTabLayout = view.findViewById(R.id.text_avatar_creation_tabs) + val doneButton: View = view.findViewById(R.id.text_avatar_creation_done) + + withRecyclerSet.load(requireContext(), R.layout.text_avatar_creation_fragment) + withoutRecyclerSet.load(requireContext(), R.layout.text_avatar_creation_fragment_hidden_recycler) + + recycler = view.findViewById(R.id.text_avatar_creation_recycler) + textInput = view.findViewById(R.id.avatar_picker_item_text) + + toolbar.setNavigationOnClickListener { Navigation.findNavController(it).popBackStack() } + BoldSelectionTabItem.registerListeners(tabLayout) + + val onTabSelectedListener = OnTabSelectedListener() + tabLayout.addOnTabSelectedListener(onTabSelectedListener) + onTabSelectedListener.onTabSelected(requireNotNull(tabLayout.getTabAt(tabLayout.selectedTabPosition))) + + val adapter = MappingAdapter() + recycler.addItemDecoration(GridDividerDecoration(4, ViewUtil.dpToPx(16))) + AvatarColorItem.registerViewHolder(adapter) { + viewModel.setColor(it) + } + recycler.adapter = adapter + + val viewHolder = AvatarPickerItem.ViewHolder(view) + viewModel.state.observe(viewLifecycleOwner) { state -> + EditTextUtil.setCursorColor(textInput, state.currentAvatar.color.foregroundColor) + + val hadText = textInput.length() > 0 + viewHolder.bind(AvatarPickerItem.Model(state.currentAvatar, false)) + if (!hadText) { + textInput.setSelection(textInput.length()) + } + + adapter.submitList(state.colors().map { AvatarColorItem.Model(it) }) + } + + EditTextUtil.addGraphemeClusterLimitFilter(textInput, 3) + textInput.doAfterTextChanged { + if (it != null) { + viewModel.setText(it.toString()) + } + } + + doneButton.setOnClickListener { v -> + setFragmentResult(REQUEST_KEY_TEXT, AvatarBundler.bundleText(viewModel.getCurrentAvatar())) + Navigation.findNavController(v).popBackStack() + } + + textInput.setOnEditorActionListener { v, actionId, event -> + if (actionId == EditorInfo.IME_ACTION_DONE) { + doneButton.performClick() + true + } else { + false + } + } + } + + private inner class OnTabSelectedListener : TabLayout.OnTabSelectedListener { + override fun onTabSelected(tab: TabLayout.Tab) { + when (tab.position) { + 0 -> { + textInput.isEnabled = true + ViewUtil.focusAndShowKeyboard(textInput) + + val constraintLayout = requireView() as ConstraintLayout + TransitionManager.endTransitions(constraintLayout) + withoutRecyclerSet.applyTo(constraintLayout) + TransitionManager.beginDelayedTransition(constraintLayout) + textInput.setSelection(textInput.length()) + } + 1 -> { + textInput.isEnabled = false + ViewUtil.hideKeyboard(requireContext(), textInput) + + val constraintLayout = requireView() as ConstraintLayout + TransitionManager.endTransitions(constraintLayout) + withRecyclerSet.applyTo(constraintLayout) + TransitionManager.beginDelayedTransition(constraintLayout) + } + } + } + + override fun onTabUnselected(tab: TabLayout.Tab?) = Unit + override fun onTabReselected(tab: TabLayout.Tab?) = Unit + } + + companion object { + const val REQUEST_KEY_TEXT = "org.thoughtcrime.securesms.avatar.text.TEXT" + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/text/TextAvatarCreationState.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/text/TextAvatarCreationState.kt new file mode 100644 index 0000000000..52493ad3a7 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/text/TextAvatarCreationState.kt @@ -0,0 +1,11 @@ +package org.thoughtcrime.securesms.avatar.text + +import org.thoughtcrime.securesms.avatar.Avatar +import org.thoughtcrime.securesms.avatar.AvatarColorItem +import org.thoughtcrime.securesms.avatar.Avatars + +data class TextAvatarCreationState( + val currentAvatar: Avatar.Text, +) { + fun colors(): List = Avatars.colors.map { AvatarColorItem(it, currentAvatar.color == it) } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/text/TextAvatarCreationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/text/TextAvatarCreationViewModel.kt new file mode 100644 index 0000000000..891afd51c0 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/text/TextAvatarCreationViewModel.kt @@ -0,0 +1,33 @@ +package org.thoughtcrime.securesms.avatar.text + +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import org.thoughtcrime.securesms.avatar.Avatar +import org.thoughtcrime.securesms.avatar.Avatars +import org.thoughtcrime.securesms.util.livedata.Store + +class TextAvatarCreationViewModel(initialText: Avatar.Text) : ViewModel() { + + private val store = Store(TextAvatarCreationState(initialText)) + + val state: LiveData = store.stateLiveData + + fun setColor(colorPair: Avatars.ColorPair) { + store.update { it.copy(currentAvatar = it.currentAvatar.copy(color = colorPair)) } + } + + fun setText(text: String) { + store.update { it.copy(currentAvatar = it.currentAvatar.copy(text = text)) } + } + + fun getCurrentAvatar(): Avatar.Text { + return store.state.currentAvatar + } + + class Factory(private val initialText: Avatar.Text) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return requireNotNull(modelClass.cast(TextAvatarCreationViewModel(initialText))) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/vector/VectorAvatarCreationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/vector/VectorAvatarCreationFragment.kt new file mode 100644 index 0000000000..802ecd7d5d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/vector/VectorAvatarCreationFragment.kt @@ -0,0 +1,64 @@ +package org.thoughtcrime.securesms.avatar.vector + +import android.os.Bundle +import android.view.View +import android.widget.ImageView +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.Fragment +import androidx.fragment.app.setFragmentResult +import androidx.fragment.app.viewModels +import androidx.navigation.Navigation +import androidx.recyclerview.widget.RecyclerView +import com.airbnb.lottie.SimpleColorFilter +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.avatar.AvatarBundler +import org.thoughtcrime.securesms.avatar.AvatarColorItem +import org.thoughtcrime.securesms.avatar.Avatars +import org.thoughtcrime.securesms.components.recyclerview.GridDividerDecoration +import org.thoughtcrime.securesms.util.MappingAdapter +import org.thoughtcrime.securesms.util.ViewUtil + +/** + * Fragment to create an avatar based off a default vector. + */ +class VectorAvatarCreationFragment : Fragment(R.layout.vector_avatar_creation_fragment) { + + private val viewModel: VectorAvatarCreationViewModel by viewModels(factoryProducer = this::createFactory) + + private fun createFactory(): VectorAvatarCreationViewModel.Factory { + val args = VectorAvatarCreationFragmentArgs.fromBundle(requireArguments()) + val vectorBundle = args.vectorAvatar + + return VectorAvatarCreationViewModel.Factory(AvatarBundler.extractVector(vectorBundle)) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val toolbar: Toolbar = view.findViewById(R.id.vector_avatar_creation_toolbar) + val recycler: RecyclerView = view.findViewById(R.id.vector_avatar_creation_recycler) + val doneButton: View = view.findViewById(R.id.vector_avatar_creation_done) + val preview: ImageView = view.findViewById(R.id.vector_avatar_creation_image) + + val adapter = MappingAdapter() + recycler.adapter = adapter + recycler.addItemDecoration(GridDividerDecoration(4, ViewUtil.dpToPx(16))) + AvatarColorItem.registerViewHolder(adapter) { + viewModel.setColor(it) + } + + viewModel.state.observe(viewLifecycleOwner) { state -> + preview.background.colorFilter = SimpleColorFilter(state.currentAvatar.color.backgroundColor) + preview.setImageResource(requireNotNull(Avatars.getDrawableResource(state.currentAvatar.key))) + adapter.submitList(state.colors().map { AvatarColorItem.Model(it) }) + } + + toolbar.setNavigationOnClickListener { Navigation.findNavController(view).popBackStack() } + doneButton.setOnClickListener { + setFragmentResult(REQUEST_KEY_VECTOR, AvatarBundler.bundleVector(viewModel.getCurrentAvatar())) + Navigation.findNavController(it).popBackStack() + } + } + + companion object { + const val REQUEST_KEY_VECTOR = "org.thoughtcrime.securesms.avatar.text.VECTOR" + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/vector/VectorAvatarCreationState.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/vector/VectorAvatarCreationState.kt new file mode 100644 index 0000000000..20da33108f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/vector/VectorAvatarCreationState.kt @@ -0,0 +1,11 @@ +package org.thoughtcrime.securesms.avatar.vector + +import org.thoughtcrime.securesms.avatar.Avatar +import org.thoughtcrime.securesms.avatar.AvatarColorItem +import org.thoughtcrime.securesms.avatar.Avatars + +data class VectorAvatarCreationState( + val currentAvatar: Avatar.Vector, +) { + fun colors(): List = Avatars.colors.map { AvatarColorItem(it, currentAvatar.color == it) } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/vector/VectorAvatarCreationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/vector/VectorAvatarCreationViewModel.kt new file mode 100644 index 0000000000..de5cee8dbb --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/vector/VectorAvatarCreationViewModel.kt @@ -0,0 +1,27 @@ +package org.thoughtcrime.securesms.avatar.vector + +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import org.thoughtcrime.securesms.avatar.Avatar +import org.thoughtcrime.securesms.avatar.Avatars +import org.thoughtcrime.securesms.util.livedata.Store + +class VectorAvatarCreationViewModel(initialAvatar: Avatar.Vector) : ViewModel() { + + private val store = Store(VectorAvatarCreationState(initialAvatar)) + + val state: LiveData = store.stateLiveData + + fun setColor(colorPair: Avatars.ColorPair) { + store.update { it.copy(currentAvatar = it.currentAvatar.copy(color = colorPair)) } + } + + fun getCurrentAvatar() = store.state.currentAvatar + + class Factory(private val initialAvatar: Avatar.Vector) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return requireNotNull(modelClass.cast(VectorAvatarCreationViewModel(initialAvatar))) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java b/app/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java index 8025dc3935..9ab63d0a43 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java @@ -106,7 +106,7 @@ public final class AvatarImageView extends AppCompatImageView { outlinePaint = ThemeUtil.isDarkTheme(context) ? DARK_THEME_OUTLINE_PAINT : LIGHT_THEME_OUTLINE_PAINT; - unknownRecipientDrawable = new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20).asDrawable(context, AvatarColor.UNKNOWN.colorInt(), inverted); + unknownRecipientDrawable = new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20).asDrawable(context, AvatarColor.UNKNOWN, inverted); blurred = false; chatColors = null; } @@ -248,7 +248,7 @@ public final class AvatarImageView extends AppCompatImageView { requestManager.clear(this); if (fallbackPhotoProvider != null) { setImageDrawable(fallbackPhotoProvider.getPhotoForRecipientWithoutName() - .asDrawable(getContext(), AvatarColor.UNKNOWN.colorInt(), inverted)); + .asDrawable(getContext(), AvatarColor.UNKNOWN, inverted)); } else { setImageDrawable(unknownRecipientDrawable); } @@ -285,7 +285,7 @@ public final class AvatarImageView extends AppCompatImageView { { Drawable fallback = Util.firstNonNull(fallbackPhotoProvider, Recipient.DEFAULT_FALLBACK_PHOTO_PROVIDER) .getPhotoForGroup() - .asDrawable(getContext(), color.colorInt()); + .asDrawable(getContext(), color); GlideApp.with(this) .load(avatarBytes) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/BoldSelectionTabItem.kt b/app/src/main/java/org/thoughtcrime/securesms/components/BoldSelectionTabItem.kt new file mode 100644 index 0000000000..6912d415fb --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/BoldSelectionTabItem.kt @@ -0,0 +1,88 @@ +package org.thoughtcrime.securesms.components + +import android.content.Context +import android.util.AttributeSet +import android.widget.FrameLayout +import android.widget.TextView +import androidx.core.widget.doAfterTextChanged +import com.google.android.material.tabs.TabLayout +import org.thoughtcrime.securesms.R +import java.util.Objects + +/** + * Custom View for Tabs which will render bold text when the view is selected + */ +class BoldSelectionTabItem @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr) { + + private lateinit var unselectedTextView: TextView + private lateinit var selectedTextView: TextView + + override fun onFinishInflate() { + super.onFinishInflate() + + unselectedTextView = findViewById(android.R.id.text1) + selectedTextView = findViewById(R.id.text1_bold) + + unselectedTextView.doAfterTextChanged { + selectedTextView.text = it + } + } + + fun select() { + unselectedTextView.alpha = 0f + selectedTextView.alpha = 1f + } + + fun unselect() { + unselectedTextView.alpha = 1f + selectedTextView.alpha = 0f + } + + companion object { + @JvmStatic + fun registerListeners(tabLayout: ControllableTabLayout) { + val newTabListener = NewTabListener() + val onTabSelectedListener = OnTabSelectedListener() + + (0 until tabLayout.tabCount).mapNotNull { tabLayout.getTabAt(it) }.forEach { + newTabListener.onNewTab(it) + + if (it.isSelected) { + onTabSelectedListener.onTabSelected(it) + } else { + onTabSelectedListener.onTabUnselected(it) + } + } + + tabLayout.setNewTabListener(newTabListener) + tabLayout.addOnTabSelectedListener(onTabSelectedListener) + } + } + + private class NewTabListener : ControllableTabLayout.NewTabListener { + override fun onNewTab(tab: TabLayout.Tab) { + val customView = tab.customView + if (customView == null) { + tab.setCustomView(R.layout.bold_selection_tab_item) + } + } + } + + private class OnTabSelectedListener : TabLayout.OnTabSelectedListener { + override fun onTabSelected(tab: TabLayout.Tab) { + val view = Objects.requireNonNull(tab.customView) as BoldSelectionTabItem + view.select() + } + + override fun onTabUnselected(tab: TabLayout.Tab) { + val view = Objects.requireNonNull(tab.customView) as BoldSelectionTabItem + view.unselect() + } + + override fun onTabReselected(tab: TabLayout.Tab) = Unit + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ButtonStripItemView.kt b/app/src/main/java/org/thoughtcrime/securesms/components/ButtonStripItemView.kt new file mode 100644 index 0000000000..9f94331cb9 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ButtonStripItemView.kt @@ -0,0 +1,41 @@ +package org.thoughtcrime.securesms.components + +import android.content.Context +import android.util.AttributeSet +import android.widget.ImageView +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import org.thoughtcrime.securesms.R + +class ButtonStripItemView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { + + private val iconView: ImageView + private val labelView: TextView + + init { + inflate(context, R.layout.button_strip_item_view, this) + + iconView = findViewById(R.id.icon) + labelView = findViewById(R.id.label) + + val array = context.obtainStyledAttributes(attrs, R.styleable.ButtonStripItemView) + + val icon = array.getDrawable(R.styleable.ButtonStripItemView_bsiv_icon) + val contentDescription = array.getString(R.styleable.ButtonStripItemView_bsiv_icon_contentDescription) + val label = array.getString(R.styleable.ButtonStripItemView_bsiv_label) + + iconView.setImageDrawable(icon) + iconView.contentDescription = contentDescription + labelView.text = label + + array.recycle() + } + + fun setOnIconClickedListener(onIconClickedListener: (() -> Unit)?) { + iconView.setOnClickListener { onIconClickedListener?.invoke() } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/recyclerview/GridDividerDecoration.kt b/app/src/main/java/org/thoughtcrime/securesms/components/recyclerview/GridDividerDecoration.kt new file mode 100644 index 0000000000..1c70de4094 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/recyclerview/GridDividerDecoration.kt @@ -0,0 +1,50 @@ +package org.thoughtcrime.securesms.components.recyclerview + +import android.graphics.Rect +import android.view.View +import androidx.annotation.Px +import androidx.recyclerview.widget.RecyclerView +import org.thoughtcrime.securesms.util.ViewUtil + +/** + * Decoration which will add an equal amount of space between each item in a grid. + */ +open class GridDividerDecoration( + private val spanCount: Int, + @Px private val space: Int +) : RecyclerView.ItemDecoration() { + + override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { + return setItemOffsets(parent.getChildAdapterPosition(view), view, outRect) + } + + protected fun setItemOffsets(position: Int, view: View, outRect: Rect) { + val column = position % spanCount + val isRtl = ViewUtil.isRtl(view) + + val distanceFromEnd = spanCount - 1 - column + + val spaceStart = (column / spanCount.toFloat()) * space + val spaceEnd = (distanceFromEnd / spanCount.toFloat()) * space + + outRect.setStart(spaceStart.toInt(), isRtl) + outRect.setEnd(spaceEnd.toInt(), isRtl) + outRect.bottom = space + } + + private fun Rect.setEnd(end: Int, isRtl: Boolean) { + if (isRtl) { + left = end + } else { + right = end + } + } + + private fun Rect.setStart(start: Int, isRtl: Boolean) { + if (isRtl) { + right = start + } else { + left = start + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/FallbackContactPhoto.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/FallbackContactPhoto.java index 9a4e265c6f..6ee00d7fd0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/FallbackContactPhoto.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/FallbackContactPhoto.java @@ -3,11 +3,15 @@ package org.thoughtcrime.securesms.contacts.avatars; import android.content.Context; import android.graphics.drawable.Drawable; +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.conversation.colors.AvatarColor; + public interface FallbackContactPhoto { - public Drawable asDrawable(Context context, int color); - public Drawable asDrawable(Context context, int color, boolean inverted); - public Drawable asSmallDrawable(Context context, int color, boolean inverted); - public Drawable asCallCard(Context context); + Drawable asDrawable(@NonNull Context context, @NonNull AvatarColor color); + Drawable asDrawable(@NonNull Context context, @NonNull AvatarColor color, boolean inverted); + Drawable asSmallDrawable(@NonNull Context context, @NonNull AvatarColor color, boolean inverted); + Drawable asCallCard(@NonNull Context context); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/FallbackPhoto20dp.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/FallbackPhoto20dp.java index 96c535e030..fe2630b73e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/FallbackPhoto20dp.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/FallbackPhoto20dp.java @@ -10,6 +10,8 @@ import androidx.appcompat.content.res.AppCompatResources; import androidx.core.graphics.drawable.DrawableCompat; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.avatar.Avatars; +import org.thoughtcrime.securesms.conversation.colors.AvatarColor; import org.thoughtcrime.securesms.util.ViewUtil; import java.util.Objects; @@ -26,33 +28,33 @@ public final class FallbackPhoto20dp implements FallbackContactPhoto { } @Override - public Drawable asDrawable(Context context, int color) { + public Drawable asDrawable(@NonNull Context context, @NonNull AvatarColor color) { return buildDrawable(context, color); } @Override - public Drawable asDrawable(Context context, int color, boolean inverted) { + public Drawable asDrawable(@NonNull Context context, @NonNull AvatarColor color, boolean inverted) { return buildDrawable(context, color); } @Override - public Drawable asSmallDrawable(Context context, int color, boolean inverted) { + public Drawable asSmallDrawable(@NonNull Context context, @NonNull AvatarColor color, boolean inverted) { return buildDrawable(context, color); } @Override - public Drawable asCallCard(Context context) { + public Drawable asCallCard(@NonNull Context context) { throw new UnsupportedOperationException(); } - private @NonNull Drawable buildDrawable(@NonNull Context context, int color) { + private @NonNull Drawable buildDrawable(@NonNull Context context, @NonNull AvatarColor color) { Drawable background = DrawableCompat.wrap(Objects.requireNonNull(AppCompatResources.getDrawable(context, R.drawable.circle_tintable))).mutate(); - Drawable foreground = AppCompatResources.getDrawable(context, drawable20dp); - Drawable gradient = AppCompatResources.getDrawable(context, R.drawable.avatar_gradient); - LayerDrawable drawable = new LayerDrawable(new Drawable[]{background, foreground, gradient}); + Drawable foreground = Objects.requireNonNull(AppCompatResources.getDrawable(context, drawable20dp)); + LayerDrawable drawable = new LayerDrawable(new Drawable[]{background, foreground}); int foregroundInset = ViewUtil.dpToPx(2); - DrawableCompat.setTint(background, color); + DrawableCompat.setTint(background, color.colorInt()); + DrawableCompat.setTint(foreground, Avatars.getForegroundColor(color).getColorInt()); drawable.setLayerInset(1, foregroundInset, foregroundInset, foregroundInset, foregroundInset); diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/FallbackPhoto80dp.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/FallbackPhoto80dp.java index f4c2b64cda..4d19fe5fcf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/FallbackPhoto80dp.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/FallbackPhoto80dp.java @@ -1,57 +1,55 @@ package org.thoughtcrime.securesms.contacts.avatars; import android.content.Context; -import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; -import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.LayerDrawable; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.appcompat.content.res.AppCompatResources; -import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.avatar.Avatars; +import org.thoughtcrime.securesms.conversation.colors.AvatarColor; import org.thoughtcrime.securesms.util.ViewUtil; import java.util.Objects; public final class FallbackPhoto80dp implements FallbackContactPhoto { - @DrawableRes private final int drawable80dp; - private final int backgroundColor; + @DrawableRes private final int drawable80dp; + private final AvatarColor color; - public FallbackPhoto80dp(@DrawableRes int drawable80dp, int backgroundColor) { - this.drawable80dp = drawable80dp; - this.backgroundColor = backgroundColor; + public FallbackPhoto80dp(@DrawableRes int drawable80dp, @NonNull AvatarColor color) { + this.drawable80dp = drawable80dp; + this.color = color; } @Override - public Drawable asDrawable(Context context, int color) { + public Drawable asDrawable(@NonNull Context context, @NonNull AvatarColor color) { return buildDrawable(context); } @Override - public Drawable asDrawable(Context context, int color, boolean inverted) { + public Drawable asDrawable(@NonNull Context context, @NonNull AvatarColor color, boolean inverted) { return buildDrawable(context); } @Override - public Drawable asSmallDrawable(Context context, int color, boolean inverted) { + public Drawable asSmallDrawable(@NonNull Context context, @NonNull AvatarColor color, boolean inverted) { throw new UnsupportedOperationException(); } @Override - public Drawable asCallCard(Context context) { - Drawable background = new ColorDrawable(backgroundColor); - Drawable foreground = AppCompatResources.getDrawable(context, drawable80dp); - int transparent20 = ContextCompat.getColor(context, R.color.signal_transparent_20); - Drawable gradient = new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, new int[]{ Color.TRANSPARENT, transparent20 }); - LayerDrawable drawable = new LayerDrawable(new Drawable[]{background, foreground, gradient}); + public Drawable asCallCard(@NonNull Context context) { + Drawable background = new ColorDrawable(color.colorInt()); + Drawable foreground = Objects.requireNonNull(AppCompatResources.getDrawable(context, drawable80dp)); + LayerDrawable drawable = new LayerDrawable(new Drawable[]{background, foreground}); int foregroundInset = ViewUtil.dpToPx(24); + DrawableCompat.setTint(foreground, Avatars.getForegroundColor(color).getColorInt()); drawable.setLayerInset(1, foregroundInset, foregroundInset, foregroundInset, foregroundInset); return drawable; @@ -59,12 +57,12 @@ public final class FallbackPhoto80dp implements FallbackContactPhoto { private @NonNull Drawable buildDrawable(@NonNull Context context) { Drawable background = DrawableCompat.wrap(Objects.requireNonNull(AppCompatResources.getDrawable(context, R.drawable.circle_tintable))).mutate(); - Drawable foreground = AppCompatResources.getDrawable(context, drawable80dp); - Drawable gradient = AppCompatResources.getDrawable(context, R.drawable.avatar_gradient); - LayerDrawable drawable = new LayerDrawable(new Drawable[]{background, foreground, gradient}); + Drawable foreground = Objects.requireNonNull(AppCompatResources.getDrawable(context, drawable80dp)); + LayerDrawable drawable = new LayerDrawable(new Drawable[]{background, foreground}); int foregroundInset = ViewUtil.dpToPx(24); - DrawableCompat.setTint(background, backgroundColor); + DrawableCompat.setTint(background, color.colorInt()); + DrawableCompat.setTint(foreground, Avatars.getForegroundColor(color).getColorInt()); drawable.setLayerInset(1, foregroundInset, foregroundInset, foregroundInset, foregroundInset); diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/GeneratedContactPhoto.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/GeneratedContactPhoto.java index eae791436f..35f4d44649 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/GeneratedContactPhoto.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/GeneratedContactPhoto.java @@ -1,79 +1,72 @@ package org.thoughtcrime.securesms.contacts.avatars; import android.content.Context; -import android.graphics.Color; -import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.text.TextUtils; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.appcompat.content.res.AppCompatResources; +import androidx.core.content.ContextCompat; -import com.amulyakhare.textdrawable.TextDrawable; +import com.airbnb.lottie.SimpleColorFilter; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.avatar.Avatar; +import org.thoughtcrime.securesms.avatar.AvatarRenderer; +import org.thoughtcrime.securesms.avatar.Avatars; +import org.thoughtcrime.securesms.conversation.colors.AvatarColor; import org.thoughtcrime.securesms.util.ContextUtil; -import org.thoughtcrime.securesms.util.ViewUtil; +import org.thoughtcrime.securesms.util.NameUtil; -import java.util.regex.Pattern; +import java.util.Objects; public class GeneratedContactPhoto implements FallbackContactPhoto { - private static final Pattern PATTERN = Pattern.compile("[^\\p{L}\\p{Nd}\\p{S}]+"); - private static final Typeface TYPEFACE = Typeface.create("sans-serif-medium", Typeface.NORMAL); - private final String name; private final int fallbackResId; private final int targetSize; - private final int fontSize; public GeneratedContactPhoto(@NonNull String name, @DrawableRes int fallbackResId) { - this(name, fallbackResId, -1, ViewUtil.dpToPx(24)); + this(name, fallbackResId, -1); } - public GeneratedContactPhoto(@NonNull String name, @DrawableRes int fallbackResId, int targetSize, int fontSize) { + public GeneratedContactPhoto(@NonNull String name, @DrawableRes int fallbackResId, int targetSize) { this.name = name; this.fallbackResId = fallbackResId; this.targetSize = targetSize; - this.fontSize = fontSize; } @Override - public Drawable asDrawable(Context context, int color) { + public Drawable asDrawable(@NonNull Context context, @NonNull AvatarColor color) { return asDrawable(context, color,false); } @Override - public Drawable asDrawable(Context context, int color, boolean inverted) { + public Drawable asDrawable(@NonNull Context context, @NonNull AvatarColor color, boolean inverted) { int targetSize = this.targetSize != -1 ? this.targetSize : context.getResources().getDimensionPixelSize(R.dimen.contact_photo_target_size); - String character = getAbbreviation(name); + String character = NameUtil.getAbbreviation(name); if (!TextUtils.isEmpty(character)) { - Drawable base = TextDrawable.builder() - .beginConfig() - .width(targetSize) - .height(targetSize) - .useFont(TYPEFACE) - .fontSize(fontSize) - .textColor(inverted ? color : Color.WHITE) - .endConfig() - .buildRound(character, inverted ? Color.WHITE : color); + Avatars.ForegroundColor foregroundColor = Avatars.getForegroundColor(color); + Avatar.Text avatar = new Avatar.Text(character, new Avatars.ColorPair(color, foregroundColor), Avatar.DatabaseId.DoNotPersist.INSTANCE); + Drawable foreground = AvatarRenderer.createTextDrawable(context, avatar, inverted, targetSize, false); + Drawable background = Objects.requireNonNull(ContextCompat.getDrawable(context, R.drawable.circle_tintable)); - Drawable gradient = ContextUtil.requireDrawable(context, R.drawable.avatar_gradient); - return new LayerDrawable(new Drawable[] { base, gradient }); + background.setColorFilter(new SimpleColorFilter(inverted ? foregroundColor.getColorInt() : color.colorInt())); + + return new LayerDrawable(new Drawable[] { background, foreground }); } return newFallbackDrawable(context, color, inverted); } @Override - public Drawable asSmallDrawable(Context context, int color, boolean inverted) { + public Drawable asSmallDrawable(@NonNull Context context, @NonNull AvatarColor color, boolean inverted) { return asDrawable(context, color, inverted); } @@ -81,32 +74,12 @@ public class GeneratedContactPhoto implements FallbackContactPhoto { return fallbackResId; } - protected Drawable newFallbackDrawable(@NonNull Context context, int color, boolean inverted) { + protected Drawable newFallbackDrawable(@NonNull Context context, @NonNull AvatarColor color, boolean inverted) { return new ResourceContactPhoto(fallbackResId).asDrawable(context, color, inverted); } - private @Nullable String getAbbreviation(String name) { - String[] parts = name.split(" "); - StringBuilder builder = new StringBuilder(); - int count = 0; - - for (int i = 0; i < parts.length && count < 2; i++) { - String cleaned = PATTERN.matcher(parts[i]).replaceFirst(""); - if (!TextUtils.isEmpty(cleaned)) { - builder.appendCodePoint(cleaned.codePointAt(0)); - count++; - } - } - - if (builder.length() == 0) { - return null; - } else { - return builder.toString(); - } - } - @Override - public Drawable asCallCard(Context context) { + public Drawable asCallCard(@NonNull Context context) { return AppCompatResources.getDrawable(context, R.drawable.ic_person_large); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/ResourceContactPhoto.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/ResourceContactPhoto.java index 272310d5ee..63e3cb763e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/ResourceContactPhoto.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/ResourceContactPhoto.java @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.contacts.avatars; import android.content.Context; -import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; @@ -15,8 +14,9 @@ import androidx.appcompat.content.res.AppCompatResources; import com.amulyakhare.textdrawable.TextDrawable; import com.makeramen.roundedimageview.RoundedDrawable; -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.util.ContextUtil; +import org.jetbrains.annotations.NotNull; +import org.thoughtcrime.securesms.avatar.Avatars; +import org.thoughtcrime.securesms.conversation.colors.AvatarColor; public class ResourceContactPhoto implements FallbackContactPhoto { @@ -45,38 +45,34 @@ public class ResourceContactPhoto implements FallbackContactPhoto { } @Override - public @NonNull Drawable asDrawable(@NonNull Context context, int color) { + public @NonNull Drawable asDrawable(@NonNull Context context, @NonNull AvatarColor color) { return asDrawable(context, color, false); } @Override - public @NonNull Drawable asDrawable(@NonNull Context context, int color, boolean inverted) { + public @NonNull Drawable asDrawable(@NonNull Context context, @NonNull AvatarColor color, boolean inverted) { return buildDrawable(context, resourceId, color, inverted); } @Override - public @NonNull Drawable asSmallDrawable(@NonNull Context context, int color, boolean inverted) { + public @NonNull Drawable asSmallDrawable(@NonNull Context context, @NonNull AvatarColor color, boolean inverted) { return buildDrawable(context, smallResourceId, color, inverted); } - private @NonNull Drawable buildDrawable(@NonNull Context context, int resourceId, int color, boolean inverted) { - Drawable background = TextDrawable.builder().buildRound(" ", inverted ? Color.WHITE : color); - RoundedDrawable foreground = (RoundedDrawable) RoundedDrawable.fromDrawable(AppCompatResources.getDrawable(context, resourceId)); + private @NonNull Drawable buildDrawable(@NonNull Context context, int resourceId, @NonNull AvatarColor color, boolean inverted) { + Avatars.ForegroundColor foregroundColor = Avatars.getForegroundColor(color); + Drawable background = TextDrawable.builder().buildRound(" ", inverted ? foregroundColor.getColorInt() : color.colorInt()); + RoundedDrawable foreground = (RoundedDrawable) RoundedDrawable.fromDrawable(AppCompatResources.getDrawable(context, resourceId)); //noinspection ConstantConditions foreground.setScaleType(scaleType); + foreground.setColorFilter(inverted ? color.colorInt() : foregroundColor.getColorInt(), PorterDuff.Mode.SRC_ATOP); - if (inverted) { - foreground.setColorFilter(color, PorterDuff.Mode.SRC_ATOP); - } - - Drawable gradient = ContextUtil.requireDrawable(context, R.drawable.avatar_gradient); - - return new ExpandingLayerDrawable(new Drawable[] {background, foreground, gradient}); + return new ExpandingLayerDrawable(new Drawable[] {background, foreground}); } @Override - public @Nullable Drawable asCallCard(@NonNull Context context) { + public @Nullable Drawable asCallCard(@NotNull @NonNull Context context) { return AppCompatResources.getDrawable(context, callCardResourceId); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/TransparentContactPhoto.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/TransparentContactPhoto.java index f38e6f0f1f..6046d025af 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/TransparentContactPhoto.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/avatars/TransparentContactPhoto.java @@ -3,33 +3,36 @@ package org.thoughtcrime.securesms.contacts.avatars; import android.content.Context; import android.graphics.drawable.Drawable; +import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import com.makeramen.roundedimageview.RoundedDrawable; +import org.jetbrains.annotations.NotNull; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.conversation.colors.AvatarColor; public class TransparentContactPhoto implements FallbackContactPhoto { public TransparentContactPhoto() {} @Override - public Drawable asDrawable(Context context, int color) { + public Drawable asDrawable(@NonNull Context context, @NonNull AvatarColor color) { return asDrawable(context, color, false); } @Override - public Drawable asDrawable(Context context, int color, boolean inverted) { + public Drawable asDrawable(@NonNull Context context, @NonNull AvatarColor color, boolean inverted) { return RoundedDrawable.fromDrawable(context.getResources().getDrawable(android.R.color.transparent)); } @Override - public Drawable asSmallDrawable(Context context, int color, boolean inverted) { + public Drawable asSmallDrawable(@NonNull Context context, @NonNull AvatarColor color, boolean inverted) { return asDrawable(context, color, inverted); } @Override - public Drawable asCallCard(Context context) { + public Drawable asCallCard(@NonNull Context context) { return ContextCompat.getDrawable(context, R.drawable.ic_contact_picture_large); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 529518fb79..38cc54fb5e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -1129,6 +1129,7 @@ public class ConversationActivity extends PassphraseRequiredActivity updateReminders(); } + @SuppressLint("MissingSuperCall") @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); @@ -1302,7 +1303,7 @@ public class ConversationActivity extends PassphraseRequiredActivity GlideApp.with(this) .asBitmap() .load(recipient.getContactPhoto()) - .error(recipient.getFallbackContactPhoto().asDrawable(this, recipient.getAvatarColor().colorInt(), false)) + .error(recipient.getFallbackContactPhoto().asDrawable(this, recipient.getAvatarColor(), false)) .into(new CustomTarget() { @Override public void onLoadFailed(@Nullable Drawable errorDrawable) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/AvatarColor.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/AvatarColor.java index ca1231eab8..5ae92eabf5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/AvatarColor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/AvatarColor.java @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation.colors; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import java.util.HashMap; import java.util.Map; @@ -11,56 +12,20 @@ import java.util.Objects; * A serializable set of color constants that can be used for avatars. */ public enum AvatarColor { - C000("C000", 0xFFD00B0B), - C010("C010", 0xFFC72A0A), - C020("C020", 0xFFB34209), - C030("C030", 0xFF9C5711), - C040("C040", 0xFF866118), - C050("C050", 0xFF76681E), - C060("C060", 0xFF6C6C13), - C070("C070", 0xFF5E6E0C), - C080("C080", 0xFF507406), - C090("C090", 0xFF3D7406), - C100("C100", 0xFF2D7906), - C110("C110", 0xFF1A7906), - C120("C120", 0xFF067906), - C130("C130", 0xFF067919), - C140("C140", 0xFF06792D), - C150("C150", 0xFF067940), - C160("C160", 0xFF067953), - C170("C170", 0xFF067462), - C180("C180", 0xFF067474), - C190("C190", 0xFF077288), - C200("C200", 0xFF086DA0), - C210("C210", 0xFF0A69C7), - C220("C220", 0xFF0D59F2), - C230("C230", 0xFF3454F4), - C240("C240", 0xFF5151F6), - C250("C250", 0xFF6447F5), - C260("C260", 0xFF7A3DF5), - C270("C270", 0xFF8F2AF4), - C280("C280", 0xFFA20CED), - C290("C290", 0xFFAF0BD0), - C300("C300", 0xFFB80AB8), - C310("C310", 0xFFC20AA3), - C320("C320", 0xFFC70A88), - C330("C330", 0xFFCB0B6B), - C340("C340", 0xFFD00B4D), - C350("C350", 0xFFD00B2C), - CRIMSON("crimson", ChatColorsPalette.Bubbles.CRIMSON.asSingleColor()), - VERMILLION("vermillion", ChatColorsPalette.Bubbles.VERMILION.asSingleColor()), - BURLAP("burlap", ChatColorsPalette.Bubbles.BURLAP.asSingleColor()), - FOREST("forest", ChatColorsPalette.Bubbles.FOREST.asSingleColor()), - WINTERGREEN("wintergreen", ChatColorsPalette.Bubbles.WINTERGREEN.asSingleColor()), - TEAL("teal", ChatColorsPalette.Bubbles.TEAL.asSingleColor()), - BLUE("blue", ChatColorsPalette.Bubbles.BLUE.asSingleColor()), - INDIGO("indigo", ChatColorsPalette.Bubbles.INDIGO.asSingleColor()), - VIOLET("violet", ChatColorsPalette.Bubbles.VIOLET.asSingleColor()), - PLUM("plum", ChatColorsPalette.Bubbles.PLUM.asSingleColor()), - TAUPE("taupe", ChatColorsPalette.Bubbles.TAUPE.asSingleColor()), - STEEL("steel", ChatColorsPalette.Bubbles.STEEL.asSingleColor()), - ULTRAMARINE("ultramarine", ChatColorsPalette.Bubbles.ULTRAMARINE.asSingleColor()), - UNKNOWN("unknown", ChatColorsPalette.Bubbles.STEEL.asSingleColor()); + A100("A100", 0xFFE3E3FE), + A110("A110", 0xFFDDE7FC), + A120("A120", 0xFFD8E8F0), + A130("A130", 0xFFCDE4CD), + A140("A140", 0xFFEAE0F8), + A150("A150", 0xFFF5E3FE), + A160("A160", 0xFFF6D8EC), + A170("A170", 0xFFF5D7D7), + A180("A180", 0xFFFEF5D0), + A190("A190", 0xFFEAE6D5), + A200("A200", 0xFFD2D2DC), + A210("A210", 0xFFD7D7D9); + + public static final AvatarColor UNKNOWN = A210; /** Fast map of name to enum, while also giving us a location to map old colors to new ones. */ private static final Map NAME_MAP = new HashMap<>(); @@ -69,61 +34,83 @@ public enum AvatarColor { NAME_MAP.put(color.serialize(), color); } - NAME_MAP.put("red", CRIMSON); - NAME_MAP.put("orange", VERMILLION); - NAME_MAP.put("deep_orange", VERMILLION); - NAME_MAP.put("brown", BURLAP); - NAME_MAP.put("green", FOREST); - NAME_MAP.put("light_green", WINTERGREEN); - NAME_MAP.put("teal", TEAL); - NAME_MAP.put("blue", BLUE); - NAME_MAP.put("indigo", INDIGO); - NAME_MAP.put("purple", VIOLET); - NAME_MAP.put("deep_purple", VIOLET); - NAME_MAP.put("pink", PLUM); - NAME_MAP.put("blue_grey", TAUPE); - NAME_MAP.put("grey", STEEL); - NAME_MAP.put("ultramarine", ULTRAMARINE); + NAME_MAP.put("C020", A170); + NAME_MAP.put("C030", A170); + NAME_MAP.put("C040", A180); + NAME_MAP.put("C050", A180); + NAME_MAP.put("C000", A190); + NAME_MAP.put("C060", A190); + NAME_MAP.put("C070", A190); + NAME_MAP.put("C080", A130); + NAME_MAP.put("C090", A130); + NAME_MAP.put("C100", A130); + NAME_MAP.put("C110", A130); + NAME_MAP.put("C120", A130); + NAME_MAP.put("C130", A130); + NAME_MAP.put("C140", A130); + NAME_MAP.put("C150", A130); + NAME_MAP.put("C160", A130); + NAME_MAP.put("C170", A120); + NAME_MAP.put("C180", A120); + NAME_MAP.put("C190", A120); + NAME_MAP.put("C200", A110); + NAME_MAP.put("C210", A110); + NAME_MAP.put("C220", A110); + NAME_MAP.put("C230", A100); + NAME_MAP.put("C240", A100); + NAME_MAP.put("C250", A100); + NAME_MAP.put("C260", A100); + NAME_MAP.put("C270", A140); + NAME_MAP.put("C280", A140); + NAME_MAP.put("C290", A140); + NAME_MAP.put("C300", A150); + NAME_MAP.put("C010", A170); + NAME_MAP.put("C310", A150); + NAME_MAP.put("C320", A150); + NAME_MAP.put("C330", A160); + NAME_MAP.put("C340", A160); + NAME_MAP.put("C350", A160); + NAME_MAP.put("crimson", A170); + NAME_MAP.put("vermillion", A170); + NAME_MAP.put("burlap", A190); + NAME_MAP.put("forest", A130); + NAME_MAP.put("wintergreen", A130); + NAME_MAP.put("teal", A120); + NAME_MAP.put("blue", A110); + NAME_MAP.put("indigo", A100); + NAME_MAP.put("violet", A140); + NAME_MAP.put("plum", A150); + NAME_MAP.put("taupe", A190); + NAME_MAP.put("steel", A210); + NAME_MAP.put("ultramarine", A100); + NAME_MAP.put("unknown", A210); + NAME_MAP.put("red", A170); + NAME_MAP.put("orange", A170); + NAME_MAP.put("deep_orange", A170); + NAME_MAP.put("brown", A190); + NAME_MAP.put("green", A130); + NAME_MAP.put("light_green", A130); + NAME_MAP.put("purple", A140); + NAME_MAP.put("deep_purple", A140); + NAME_MAP.put("pink", A150); + NAME_MAP.put("blue_grey", A190); + NAME_MAP.put("grey", A210); } /** Colors that can be assigned via {@link #random()}. */ private static final AvatarColor[] RANDOM_OPTIONS = new AvatarColor[] { - C000, - C010, - C020, - C030, - C040, - C050, - C060, - C070, - C080, - C090, - C100, - C110, - C120, - C130, - C140, - C150, - C160, - C170, - C180, - C190, - C200, - C210, - C220, - C230, - C240, - C250, - C260, - C270, - C280, - C290, - C300, - C310, - C320, - C330, - C340, - C350, + A100, + A110, + A120, + A130, + A140, + A150, + A160, + A170, + A180, + A190, + A200, + A210 }; private final String name; @@ -148,6 +135,6 @@ public enum AvatarColor { } public static @NonNull AvatarColor deserialize(@NonNull String name) { - return Objects.requireNonNull(NAME_MAP.getOrDefault(name, C000)); + return Objects.requireNonNull(NAME_MAP.getOrDefault(name, A210)); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseFactory.java b/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseFactory.java index 39b1c257c2..54ab1ac8b3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseFactory.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseFactory.java @@ -31,6 +31,7 @@ import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.helpers.ClassicOpenHelper; import org.thoughtcrime.securesms.database.helpers.SQLCipherMigrationHelper; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; +import org.thoughtcrime.securesms.database.model.AvatarPickerDatabase; import org.thoughtcrime.securesms.migrations.LegacyMigrationJob; import org.thoughtcrime.securesms.util.SqlUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -70,6 +71,7 @@ public class DatabaseFactory { private final ChatColorsDatabase chatColorsDatabase; private final EmojiSearchDatabase emojiSearchDatabase; private final MessageSendLogDatabase messageSendLogDatabase; + private final AvatarPickerDatabase avatarPickerDatabase; public static DatabaseFactory getInstance(Context context) { if (instance == null) { @@ -200,6 +202,10 @@ public class DatabaseFactory { return getInstance(context).messageSendLogDatabase; } + public static AvatarPickerDatabase getAvatarPickerDatabase(Context context) { + return getInstance(context).avatarPickerDatabase; + } + public static SQLiteDatabase getBackupDatabase(Context context) { return getInstance(context).databaseHelper.getReadableDatabase().getSqlCipherDatabase(); } @@ -259,8 +265,9 @@ public class DatabaseFactory { this.mentionDatabase = new MentionDatabase(context, databaseHelper); this.paymentDatabase = new PaymentDatabase(context, databaseHelper); this.chatColorsDatabase = new ChatColorsDatabase(context, databaseHelper); - this.emojiSearchDatabase = new EmojiSearchDatabase(context, databaseHelper); - this.messageSendLogDatabase = new MessageSendLogDatabase(context, databaseHelper); + this.emojiSearchDatabase = new EmojiSearchDatabase(context, databaseHelper); + this.messageSendLogDatabase = new MessageSendLogDatabase(context, databaseHelper); + this.avatarPickerDatabase = new AvatarPickerDatabase(context, databaseHelper); } public void onApplicationLevelUpgrade(@NonNull Context context, @NonNull MasterSecret masterSecret, diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index decf51ced6..4653c9730e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -57,6 +57,7 @@ import org.thoughtcrime.securesms.database.SqlCipherErrorHandler; import org.thoughtcrime.securesms.database.StickerDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.UnknownStorageIdDatabase; +import org.thoughtcrime.securesms.database.model.AvatarPickerDatabase; import org.thoughtcrime.securesms.database.model.databaseprotos.ReactionList; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.GroupId; @@ -207,8 +208,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab private static final int THREAD_AUTOINCREMENT = 108; private static final int MMS_AUTOINCREMENT = 109; private static final int ABANDONED_ATTACHMENT_CLEANUP = 110; + private static final int AVATAR_PICKER = 111; - private static final int DATABASE_VERSION = 110; + private static final int DATABASE_VERSION = 111; private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -245,6 +247,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab db.execSQL(PaymentDatabase.CREATE_TABLE); db.execSQL(ChatColorsDatabase.CREATE_TABLE); db.execSQL(EmojiSearchDatabase.CREATE_TABLE); + db.execSQL(AvatarPickerDatabase.CREATE_TABLE); executeStatements(db, SearchDatabase.CREATE_TABLE); executeStatements(db, RemappedRecordsDatabase.CREATE_TABLE); executeStatements(db, MessageSendLogDatabase.CREATE_TABLE); @@ -1934,6 +1937,24 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab db.delete("part", "mid != -8675309 AND mid NOT IN (SELECT _id FROM mms)", null); } + if (oldVersion < AVATAR_PICKER) { + db.execSQL("CREATE TABLE avatar_picker (_id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "last_used INTEGER DEFAULT 0, " + + "group_id TEXT DEFAULT NULL, " + + "avatar BLOB NOT NULL)"); + + try (Cursor cursor = db.query("recipient", new String[] { "_id" }, "color IS NULL", null, null, null, null)) { + while (cursor.moveToNext()) { + long id = cursor.getInt(cursor.getColumnIndexOrThrow("_id")); + + ContentValues values = new ContentValues(1); + values.put("color", AvatarColor.random().serialize()); + + db.update("recipient", values, "_id = ?", new String[] { String.valueOf(id) }); + } + } + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/AvatarPickerDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/AvatarPickerDatabase.kt new file mode 100644 index 0000000000..d284de23ef --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/AvatarPickerDatabase.kt @@ -0,0 +1,185 @@ +package org.thoughtcrime.securesms.database.model + +import android.content.ContentValues +import android.content.Context +import android.net.Uri +import org.thoughtcrime.securesms.avatar.Avatar +import org.thoughtcrime.securesms.avatar.Avatars +import org.thoughtcrime.securesms.database.Database +import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper +import org.thoughtcrime.securesms.database.model.databaseprotos.CustomAvatar +import org.thoughtcrime.securesms.groups.GroupId +import org.thoughtcrime.securesms.util.CursorUtil +import org.thoughtcrime.securesms.util.SqlUtil + +/** + * Database which manages the record keeping for custom created avatars. + */ +class AvatarPickerDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Database(context, databaseHelper) { + + companion object { + private const val TABLE_NAME = "avatar_picker" + private const val ID = "_id" + private const val LAST_USED = "last_used" + private const val GROUP_ID = "group_id" + private const val AVATAR = "avatar" + + //language=sql + @JvmField + val CREATE_TABLE = """ + CREATE TABLE $TABLE_NAME ( + $ID INTEGER PRIMARY KEY AUTOINCREMENT, + $LAST_USED INTEGER DEFAULT 0, + $GROUP_ID TEXT DEFAULT NULL, + $AVATAR BLOB NOT NULL + ) + """.trimIndent() + } + + fun saveAvatarForSelf(avatar: Avatar): Avatar { + return saveAvatar(avatar, null) + } + + fun saveAvatarForGroup(avatar: Avatar, groupId: GroupId): Avatar { + return saveAvatar(avatar, groupId) + } + + fun markUsage(avatar: Avatar) { + val databaseId = avatar.databaseId + if (databaseId !is Avatar.DatabaseId.Saved) { + throw IllegalArgumentException("Must save this avatar before trying to mark usage.") + } + + val db = databaseHelper.writableDatabase + val where = ID_WHERE + val args = SqlUtil.buildArgs(databaseId.id) + val values = ContentValues(1) + + values.put(LAST_USED, System.currentTimeMillis()) + db.update(TABLE_NAME, values, where, args) + } + + fun update(avatar: Avatar) { + val databaseId = avatar.databaseId + if (databaseId !is Avatar.DatabaseId.Saved) { + throw IllegalArgumentException("Cannot update an unsaved avatar") + } + + val db = databaseHelper.writableDatabase + val where = ID_WHERE + val values = ContentValues(1) + + values.put(AVATAR, avatar.toProto().toByteArray()) + db.update(TABLE_NAME, values, where, SqlUtil.buildArgs(databaseId.id)) + } + + fun deleteAvatar(avatar: Avatar) { + val databaseId = avatar.databaseId + if (databaseId !is Avatar.DatabaseId.Saved) { + throw IllegalArgumentException("Cannot delete an unsaved avatar.") + } + + val db = databaseHelper.writableDatabase + val where = ID_WHERE + val args = SqlUtil.buildArgs(databaseId.id) + + db.delete(TABLE_NAME, where, args) + } + + private fun saveAvatar(avatar: Avatar, groupId: GroupId?): Avatar { + val db = databaseHelper.writableDatabase + val databaseId = avatar.databaseId + + if (databaseId is Avatar.DatabaseId.DoNotPersist) { + throw IllegalArgumentException("Cannot persist this avatar") + } + + if (databaseId is Avatar.DatabaseId.Saved) { + val values = ContentValues(2) + values.put(AVATAR, avatar.toProto().toByteArray()) + + db.update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(databaseId.id)) + + return avatar + } else { + val values = ContentValues(4) + values.put(AVATAR, avatar.toProto().toByteArray()) + + if (groupId != null) { + values.put(GROUP_ID, groupId.toString()) + } + + val id = db.insert(TABLE_NAME, null, values) + if (id == -1L) { + throw AssertionError("Failed to save avatar") + } + + return avatar.withDatabaseId(Avatar.DatabaseId.Saved(id)) + } + } + + fun getAllAvatars(): List { + val db = databaseHelper.readableDatabase + val results = mutableListOf() + + db.query(TABLE_NAME, SqlUtil.buildArgs(ID, AVATAR), null, null, null, null, null)?.use { + while (it.moveToNext()) { + val id = CursorUtil.requireLong(it, ID) + val blob = CursorUtil.requireBlob(it, AVATAR) + val proto = CustomAvatar.parseFrom(blob) + results.add(proto.toAvatar(id)) + } + } + + return results + } + + fun getAvatarsForSelf(): List { + return getAvatars(null) + } + + fun getAvatarsForGroup(groupId: GroupId): List { + return getAvatars(groupId) + } + + private fun getAvatars(groupId: GroupId?): List { + val db = databaseHelper.readableDatabase + val orderBy = "$LAST_USED DESC" + val results = mutableListOf() + + val (where, args) = if (groupId == null) { + Pair("$GROUP_ID is NULL", null) + } else { + Pair("$GROUP_ID = ?", SqlUtil.buildArgs(groupId)) + } + + db.query(TABLE_NAME, SqlUtil.buildArgs(ID, AVATAR), where, args, null, null, orderBy)?.use { + while (it.moveToNext()) { + val id = CursorUtil.requireLong(it, ID) + val blob = CursorUtil.requireBlob(it, AVATAR) + val proto = CustomAvatar.parseFrom(blob) + results.add(proto.toAvatar(id)) + } + } + + return results + } + + private fun Avatar.toProto(): CustomAvatar { + return when (this) { + is Avatar.Photo -> CustomAvatar.newBuilder().setPhoto(CustomAvatar.Photo.newBuilder().setUri(this.uri.toString())).build() + is Avatar.Text -> CustomAvatar.newBuilder().setText(CustomAvatar.Text.newBuilder().setText(this.text).setColors(this.color.code)).build() + is Avatar.Vector -> CustomAvatar.newBuilder().setVector(CustomAvatar.Vector.newBuilder().setKey(this.key).setColors(this.color.code)).build() + else -> throw AssertionError() + } + } + + private fun CustomAvatar.toAvatar(id: Long): Avatar { + return when { + hasPhoto() -> Avatar.Photo(Uri.parse(photo.uri), photo.size, Avatar.DatabaseId.Saved(id)) + hasText() -> Avatar.Text(text.text, Avatars.colorMap[text.colors] ?: Avatars.colors[0], Avatar.DatabaseId.Saved(id)) + hasVector() -> Avatar.Vector(vector.key, Avatars.colorMap[vector.colors] ?: Avatars.colors[0], Avatar.DatabaseId.Saved(id)) + else -> throw AssertionError() + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsActivity.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsActivity.java index 638323ce0f..85ac7b4ef6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsActivity.java @@ -9,6 +9,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.navigation.NavGraph; import androidx.navigation.Navigation; +import androidx.navigation.fragment.NavHostFragment; import org.thoughtcrime.securesms.PassphraseRequiredActivity; import org.thoughtcrime.securesms.R; @@ -46,9 +47,11 @@ public class AddGroupDetailsActivity extends PassphraseRequiredActivity implemen if (bundle == null) { ArrayList recipientIds = getIntent().getParcelableArrayListExtra(EXTRA_RECIPIENTS); AddGroupDetailsFragmentArgs arguments = new AddGroupDetailsFragmentArgs.Builder(recipientIds.toArray(new RecipientId[0])).build(); - NavGraph graph = Navigation.findNavController(this, R.id.nav_host_fragment).getGraph(); + NavHostFragment fragment = NavHostFragment.create(R.navigation.create_group, arguments.toBundle()); - Navigation.findNavController(this, R.id.nav_host_fragment).setGraph(graph, arguments.toBundle()); + getSupportFragmentManager().beginTransaction() + .replace(R.id.nav_host_fragment, fragment) + .commit(); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsFragment.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsFragment.java index adc8ea6bda..5f45876206 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsFragment.java @@ -18,25 +18,26 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; -import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.Toolbar; import androidx.lifecycle.ViewModelProviders; +import androidx.navigation.Navigation; import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.transition.Transition; import com.dd.CircularProgressButton; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import org.signal.core.util.EditTextUtil; import org.thoughtcrime.securesms.LoggingFragment; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.avatar.picker.AvatarPickerFragment; import org.thoughtcrime.securesms.components.settings.app.privacy.expire.ExpireTimerSettingsFragment; import org.thoughtcrime.securesms.groups.ui.GroupMemberListView; import org.thoughtcrime.securesms.groups.ui.creategroup.dialogs.NonGv2MemberDialog; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity; -import org.thoughtcrime.securesms.mediasend.AvatarSelectionBottomSheetDialogFragment; import org.thoughtcrime.securesms.mediasend.Media; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader; import org.thoughtcrime.securesms.mms.GlideApp; @@ -58,7 +59,6 @@ import java.util.Objects; public class AddGroupDetailsFragment extends LoggingFragment { private static final int AVATAR_PLACEHOLDER_INSET_DP = 18; - private static final short REQUEST_CODE_AVATAR = 27621; private static final short REQUEST_DISAPPEARING_TIMER = 28621; private CircularProgressButton create; @@ -112,7 +112,7 @@ public class AddGroupDetailsFragment extends LoggingFragment { initializeViewModel(); - avatar.setOnClickListener(v -> showAvatarSelectionBottomSheet()); + avatar.setOnClickListener(v -> showAvatarPicker()); members.setRecipientClickListener(this::handleRecipientClick); EditTextUtil.addGraphemeClusterLimitFilter(name, FeatureFlags.getMaxGroupNameGraphemeLength()); name.addTextChangedListener(new AfterTextChanged(editable -> viewModel.setName(editable.toString()))); @@ -154,44 +154,46 @@ public class AddGroupDetailsFragment extends LoggingFragment { }); name.requestFocus(); + + getParentFragmentManager().setFragmentResultListener(AvatarPickerFragment.REQUEST_KEY_SELECT_AVATAR, + getViewLifecycleOwner(), + (key, bundle) -> handleMediaResult(bundle)); } @Override public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - if (requestCode == REQUEST_CODE_AVATAR && resultCode == Activity.RESULT_OK && data != null) { - - if (data.getBooleanExtra("delete", false)) { - viewModel.setAvatar(null); - return; - } - - final Media result = data.getParcelableExtra(AvatarSelectionActivity.EXTRA_MEDIA); - final DecryptableStreamUriLoader.DecryptableUri decryptableUri = new DecryptableStreamUriLoader.DecryptableUri(result.getUri()); - - GlideApp.with(this) - .asBitmap() - .load(decryptableUri) - .skipMemoryCache(true) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .centerCrop() - .override(AvatarHelper.AVATAR_DIMENSIONS, AvatarHelper.AVATAR_DIMENSIONS) - .into(new CustomTarget() { - @Override - public void onResourceReady(@NonNull Bitmap resource, Transition transition) { - viewModel.setAvatar(Objects.requireNonNull(BitmapUtil.toByteArray(resource))); - } - - @Override - public void onLoadCleared(@Nullable Drawable placeholder) { - } - }); - } else if (requestCode == REQUEST_DISAPPEARING_TIMER && resultCode == Activity.RESULT_OK && data != null) { + if (requestCode == REQUEST_DISAPPEARING_TIMER && resultCode == Activity.RESULT_OK && data != null) { viewModel.setDisappearingMessageTimer(data.getIntExtra(ExpireTimerSettingsFragment.FOR_RESULT_VALUE, SignalStore.settings().getUniversalExpireTimer())); } else { super.onActivityResult(requestCode, resultCode, data); } } + private void handleMediaResult(Bundle data) { + final Media result = data.getParcelable(AvatarPickerFragment.SELECT_AVATAR_MEDIA); + final DecryptableStreamUriLoader.DecryptableUri decryptableUri = new DecryptableStreamUriLoader.DecryptableUri(result.getUri()); + + viewModel.setAvatarMedia(result); + + GlideApp.with(this) + .asBitmap() + .load(decryptableUri) + .skipMemoryCache(true) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .centerCrop() + .override(AvatarHelper.AVATAR_DIMENSIONS, AvatarHelper.AVATAR_DIMENSIONS) + .into(new CustomTarget() { + @Override + public void onResourceReady(@NonNull Bitmap resource, Transition transition) { + viewModel.setAvatar(Objects.requireNonNull(BitmapUtil.toByteArray(resource))); + } + + @Override + public void onLoadCleared(@Nullable Drawable placeholder) { + } + }); + } + private void initializeViewModel() { AddGroupDetailsFragmentArgs args = AddGroupDetailsFragmentArgs.fromBundle(requireArguments()); AddGroupDetailsRepository repository = new AddGroupDetailsRepository(requireContext()); @@ -211,15 +213,15 @@ public class AddGroupDetailsFragment extends LoggingFragment { } private void handleRecipientClick(@NonNull Recipient recipient) { - new AlertDialog.Builder(requireContext()) - .setMessage(getString(R.string.AddGroupDetailsFragment__remove_s_from_this_group, recipient.getDisplayName(requireContext()))) - .setCancelable(true) - .setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel()) - .setPositiveButton(R.string.AddGroupDetailsFragment__remove, (dialog, which) -> { - viewModel.delete(recipient.getId()); - dialog.dismiss(); - }) - .show(); + new MaterialAlertDialogBuilder(requireContext()) + .setMessage(getString(R.string.AddGroupDetailsFragment__remove_s_from_this_group, recipient.getDisplayName(requireContext()))) + .setCancelable(true) + .setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel()) + .setPositiveButton(R.string.AddGroupDetailsFragment__remove, (dialog, which) -> { + viewModel.delete(recipient.getId()); + dialog.dismiss(); + }) + .show(); } private void handleGroupCreateResult(@NonNull GroupCreateResult groupCreateResult) { @@ -263,13 +265,15 @@ public class AddGroupDetailsFragment extends LoggingFragment { .alpha(isEnabled ? 1f : 0.5f); } - private void showAvatarSelectionBottomSheet() { - AvatarSelectionBottomSheetDialogFragment.create(viewModel.hasAvatar(), true, REQUEST_CODE_AVATAR, true) - .show(getChildFragmentManager(), "BOTTOM"); + private void showAvatarPicker() { + Media media = viewModel.getAvatarMedia(); + + Navigation.findNavController(requireView()).navigate(AddGroupDetailsFragmentDirections.actionAddGroupDetailsFragmentToAvatarPicker(null, media).setIsNewGroup(true)); } public interface Callback { void onGroupCreated(@NonNull RecipientId recipientId, long threadId, @NonNull List invitedMembers); + void onNavigationButtonPressed(); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsViewModel.java index b88359f6ff..b2e2fbb28a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsViewModel.java @@ -15,6 +15,7 @@ import com.annimon.stream.Stream; import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry; import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.mediasend.Media; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.DefaultValueLiveData; @@ -42,6 +43,8 @@ public final class AddGroupDetailsViewModel extends ViewModel { private final AddGroupDetailsRepository repository; private final LiveData> nonGv2CapableMembers; + private Media avatarMedia; + private AddGroupDetailsViewModel(@NonNull Collection recipientIds, @NonNull AddGroupDetailsRepository repository) { @@ -152,6 +155,14 @@ public final class AddGroupDetailsViewModel extends ViewModel { disappearingMessagesTimer.setValue(timer); } + public void setAvatarMedia(@Nullable Media media) { + this.avatarMedia = media; + } + + public @Nullable Media getAvatarMedia() { + return avatarMedia; + } + static final class Factory implements ViewModelProvider.Factory { private final Collection recipientIds; diff --git a/app/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorModel.java b/app/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorModel.java index 0ed3e94802..c005000a8e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/imageeditor/model/EditorModel.java @@ -67,7 +67,8 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { private enum EditingPurpose { IMAGE, - AVATAR_CIRCLE, + AVATAR_CAPTURE, + AVATAR_EDIT, WALLPAPER } @@ -95,8 +96,14 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { return new EditorModel(EditingPurpose.IMAGE, 0, EditorElementHierarchy.create()); } - public static EditorModel createForCircleEditing() { - EditorModel editorModel = new EditorModel(EditingPurpose.AVATAR_CIRCLE, 1, EditorElementHierarchy.createForCircleEditing()); + public static EditorModel createForAvatarCapture() { + EditorModel editorModel = new EditorModel(EditingPurpose.AVATAR_CAPTURE, 1, EditorElementHierarchy.createForCircleEditing()); + editorModel.setCropAspectLock(true); + return editorModel; + } + + public static EditorModel createForAvatarEdit() { + EditorModel editorModel = new EditorModel(EditingPurpose.AVATAR_EDIT, 1, EditorElementHierarchy.createForCircleEditing()); editorModel.setCropAspectLock(true); return editorModel; } @@ -642,7 +649,7 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { if (imageCropMatrix.isIdentity()) { imageCropMatrix.set(cropMatrix); - if (editingPurpose == EditingPurpose.AVATAR_CIRCLE || editingPurpose == EditingPurpose.WALLPAPER) { + if (editingPurpose == EditingPurpose.AVATAR_CAPTURE || editingPurpose == EditingPurpose.WALLPAPER || editingPurpose == EditingPurpose.AVATAR_EDIT) { Matrix userCropMatrix = editorElementHierarchy.getCropEditorElement().getLocalMatrix(); if (size.x > size.y) { userCropMatrix.setScale(fixedRatio * size.y / (float) size.x, 1f); @@ -658,7 +665,7 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { } switch (editingPurpose) { - case AVATAR_CIRCLE: { + case AVATAR_CAPTURE: { startCrop(); break; } @@ -667,6 +674,8 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { startCrop(); break; } + default: + break; } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/insights/InsightsUserAvatar.java b/app/src/main/java/org/thoughtcrime/securesms/insights/InsightsUserAvatar.java index 5296bd13be..763e457612 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/insights/InsightsUserAvatar.java +++ b/app/src/main/java/org/thoughtcrime/securesms/insights/InsightsUserAvatar.java @@ -25,7 +25,7 @@ class InsightsUserAvatar { } private Drawable fallbackDrawable(@NonNull Context context) { - return fallbackContactPhoto.asDrawable(context, fallbackColor.colorInt()); + return fallbackContactPhoto.asDrawable(context, fallbackColor); } void load(ImageView into) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaGridDividerDecoration.kt b/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaGridDividerDecoration.kt index 949ac47b22..8efe6f0073 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaGridDividerDecoration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaGridDividerDecoration.kt @@ -2,15 +2,14 @@ package org.thoughtcrime.securesms.mediaoverview import android.graphics.Rect import android.view.View -import androidx.annotation.Px import androidx.recyclerview.widget.RecyclerView -import org.thoughtcrime.securesms.util.ViewUtil +import org.thoughtcrime.securesms.components.recyclerview.GridDividerDecoration internal class MediaGridDividerDecoration( - private val spanCount: Int, - @Px private val space: Int, + spanCount: Int, + space: Int, private val adapter: MediaGalleryAllAdapter -) : RecyclerView.ItemDecoration() { +) : GridDividerDecoration(spanCount, space) { override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { val holder = parent.getChildViewHolder(view) @@ -28,32 +27,6 @@ internal class MediaGridDividerDecoration( return } - val column = itemSectionOffset % spanCount - val isRtl = ViewUtil.isRtl(view) - - val distanceFromEnd = spanCount - 1 - column - - val spaceStart = (column / spanCount.toFloat()) * space - val spaceEnd = (distanceFromEnd / spanCount.toFloat()) * space - - outRect.setStart(spaceStart.toInt(), isRtl) - outRect.setEnd(spaceEnd.toInt(), isRtl) - outRect.bottom = space - } - - private fun Rect.setEnd(end: Int, isRtl: Boolean) { - if (isRtl) { - left = end - } else { - right = end - } - } - - private fun Rect.setStart(start: Int, isRtl: Boolean) { - if (isRtl) { - right = start - } else { - left = start - } + setItemOffsets(itemSectionOffset, view, outRect) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaOverviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaOverviewActivity.java index 872916a9ff..c70fbc6a2d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaOverviewActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaOverviewActivity.java @@ -38,6 +38,7 @@ import com.google.android.material.tabs.TabLayout; import org.thoughtcrime.securesms.PassphraseRequiredActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.AnimatingToggle; +import org.thoughtcrime.securesms.components.BoldSelectionTabItem; import org.thoughtcrime.securesms.components.ControllableTabLayout; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MediaDatabase; @@ -98,8 +99,7 @@ public final class MediaOverviewActivity extends PassphraseRequiredActivity { boolean allThreads = threadId == MediaDatabase.ALL_THREADS; - tabLayout.setNewTabListener(new NewTabListener()); - tabLayout.addOnTabSelectedListener(new OnTabSelectedListener()); + BoldSelectionTabItem.registerListeners(tabLayout); fillTabLayoutIfFits(tabLayout); tabLayout.setupWithViewPager(viewPager); viewPager.setAdapter(new MediaOverviewPagerAdapter(getSupportFragmentManager())); @@ -286,34 +286,4 @@ public final class MediaOverviewActivity extends PassphraseRequiredActivity { return pages.get(position).second(); } } - - private static final class NewTabListener implements ControllableTabLayout.NewTabListener { - @Override - public void onNewTab(@NonNull TabLayout.Tab tab) { - View customView = tab.getCustomView(); - if (customView == null) { - tab.setCustomView(R.layout.media_overview_tab_item); - } - } - } - - private static final class OnTabSelectedListener implements TabLayout.OnTabSelectedListener { - - @Override - public void onTabSelected(@NonNull TabLayout.Tab tab) { - MediaOverviewTabItem view = (MediaOverviewTabItem) Objects.requireNonNull(tab.getCustomView()); - view.select(); - } - - @Override - public void onTabUnselected(@NonNull TabLayout.Tab tab) { - MediaOverviewTabItem view = (MediaOverviewTabItem) Objects.requireNonNull(tab.getCustomView()); - view.unselect(); - } - - @Override - public void onTabReselected(@NonNull TabLayout.Tab tab) { - // Intentionally Blank. - } - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaOverviewTabItem.kt b/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaOverviewTabItem.kt deleted file mode 100644 index b885a89a5f..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaOverviewTabItem.kt +++ /dev/null @@ -1,39 +0,0 @@ -package org.thoughtcrime.securesms.mediaoverview - -import android.content.Context -import android.util.AttributeSet -import android.widget.FrameLayout -import android.widget.TextView -import androidx.core.widget.doAfterTextChanged -import org.thoughtcrime.securesms.R - -class MediaOverviewTabItem @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : FrameLayout(context, attrs, defStyleAttr) { - - private lateinit var unselectedTextView: TextView - private lateinit var selectedTextView: TextView - - override fun onFinishInflate() { - super.onFinishInflate() - - unselectedTextView = findViewById(android.R.id.text1) - selectedTextView = findViewById(R.id.text1_bold) - - unselectedTextView.doAfterTextChanged { - selectedTextView.text = it - } - } - - fun select() { - unselectedTextView.alpha = 0f - selectedTextView.alpha = 1f - } - - fun unselect() { - unselectedTextView.alpha = 1f - selectedTextView.alpha = 0f - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionActivity.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionActivity.java index e8b892afe1..ffffd3a6f3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionActivity.java @@ -157,7 +157,7 @@ public class AvatarSelectionActivity extends AppCompatActivity implements Camera currentMedia = media; getSupportFragmentManager().beginTransaction() - .replace(R.id.fragment_container, ImageEditorFragment.newInstanceForAvatar(media.getUri()), IMAGE_EDITOR) + .replace(R.id.fragment_container, ImageEditorFragment.newInstanceForAvatarCapture(media.getUri()), IMAGE_EDITOR) .addToBackStack(IMAGE_EDITOR) .commit(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionBottomSheetDialogFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionBottomSheetDialogFragment.java deleted file mode 100644 index 24f40bb36b..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/AvatarSelectionBottomSheetDialogFragment.java +++ /dev/null @@ -1,234 +0,0 @@ -package org.thoughtcrime.securesms.mediasend; - -import android.Manifest; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Toast; - -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.appcompat.widget.AppCompatTextView; -import androidx.core.content.ContextCompat; -import androidx.core.util.Consumer; -import androidx.fragment.app.DialogFragment; -import androidx.recyclerview.widget.RecyclerView; - -import com.annimon.stream.Stream; -import com.google.android.material.bottomsheet.BottomSheetDialogFragment; - -import org.thoughtcrime.securesms.ClearAvatarPromptActivity; -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.permissions.Permissions; -import org.thoughtcrime.securesms.util.ThemeUtil; - -import java.util.ArrayList; -import java.util.List; - -public class AvatarSelectionBottomSheetDialogFragment extends BottomSheetDialogFragment { - - private static final String ARG_OPTIONS = "options"; - private static final String ARG_REQUEST_CODE = "request_code"; - private static final String ARG_IS_GROUP = "is_group"; - - public static DialogFragment create(boolean includeClear, boolean includeCamera, short requestCode, boolean isGroup) { - DialogFragment fragment = new AvatarSelectionBottomSheetDialogFragment(); - List selectionOptions = new ArrayList<>(3); - Bundle args = new Bundle(); - - if (includeCamera) { - selectionOptions.add(SelectionOption.CAPTURE); - } - - selectionOptions.add(SelectionOption.GALLERY); - - if (includeClear) { - selectionOptions.add(SelectionOption.DELETE); - } - - String[] options = Stream.of(selectionOptions) - .map(SelectionOption::getCode) - .toArray(String[]::new); - - args.putStringArray(ARG_OPTIONS, options); - args.putShort(ARG_REQUEST_CODE, requestCode); - args.putBoolean(ARG_IS_GROUP, isGroup); - fragment.setArguments(args); - - return fragment; - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - setStyle(DialogFragment.STYLE_NORMAL, - ThemeUtil.isDarkTheme(requireContext()) ? R.style.Theme_Signal_BottomSheetDialog_Fixed - : R.style.Theme_Signal_Light_BottomSheetDialog_Fixed); - - super.onCreate(savedInstanceState); - - if (getOptionsCount() == 1) { - askForPermissionIfNeededAndLaunch(getOptionsFromArguments().get(0)); - } - } - - @Override - public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - return inflater.inflate(R.layout.avatar_selection_bottom_sheet_dialog_fragment, container, false); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - RecyclerView recyclerView = view.findViewById(R.id.avatar_selection_bottom_sheet_dialog_fragment_recycler); - recyclerView.setAdapter(new SelectionOptionAdapter(getOptionsFromArguments(), this::askForPermissionIfNeededAndLaunch)); - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); - } - - @SuppressWarnings("ConstantConditions") - private int getOptionsCount() { - return requireArguments().getStringArray(ARG_OPTIONS).length; - } - - @SuppressWarnings("ConstantConditions") - private List getOptionsFromArguments() { - String[] optionCodes = requireArguments().getStringArray(ARG_OPTIONS); - - return Stream.of(optionCodes).map(SelectionOption::fromCode).toList(); - } - - private void askForPermissionIfNeededAndLaunch(@NonNull SelectionOption option) { - if (option == SelectionOption.CAPTURE) { - Permissions.with(this) - .request(Manifest.permission.CAMERA) - .ifNecessary() - .onAllGranted(() -> launchOptionAndDismiss(option)) - .onAnyDenied(() -> Toast.makeText(requireContext(), R.string.AvatarSelectionBottomSheetDialogFragment__taking_a_photo_requires_the_camera_permission, Toast.LENGTH_SHORT) - .show()) - .execute(); - } else if (option == SelectionOption.GALLERY) { - Permissions.with(this) - .request(Manifest.permission.READ_EXTERNAL_STORAGE) - .ifNecessary() - .onAllGranted(() -> launchOptionAndDismiss(option)) - .onAnyDenied(() -> Toast.makeText(requireContext(), R.string.AvatarSelectionBottomSheetDialogFragment__viewing_your_gallery_requires_the_storage_permission, Toast.LENGTH_SHORT) - .show()) - .execute(); - } else { - launchOptionAndDismiss(option); - } - } - - private void launchOptionAndDismiss(@NonNull SelectionOption option) { - Intent intent = createIntent(requireContext(), option, requireArguments().getBoolean(ARG_IS_GROUP)); - - int requestCode = requireArguments().getShort(ARG_REQUEST_CODE); - if (getParentFragment() != null) { - requireParentFragment().startActivityForResult(intent, requestCode); - } else { - requireActivity().startActivityForResult(intent, requestCode); - } - - dismiss(); - } - - private static Intent createIntent(@NonNull Context context, @NonNull SelectionOption selectionOption, boolean isGroup) { - switch (selectionOption) { - case CAPTURE: - return AvatarSelectionActivity.getIntentForCameraCapture(context); - case GALLERY: - return AvatarSelectionActivity.getIntentForGallery(context); - case DELETE: - return isGroup ? ClearAvatarPromptActivity.createForGroupProfilePhoto() - : ClearAvatarPromptActivity.createForUserProfilePhoto(); - default: - throw new IllegalStateException("Unknown option: " + selectionOption); - } - } - - private enum SelectionOption { - CAPTURE("capture", R.string.AvatarSelectionBottomSheetDialogFragment__take_photo, R.drawable.ic_camera_24), - GALLERY("gallery", R.string.AvatarSelectionBottomSheetDialogFragment__choose_from_gallery, R.drawable.ic_photo_24), - DELETE("delete", R.string.AvatarSelectionBottomSheetDialogFragment__remove_photo, R.drawable.ic_trash_24); - - private final String code; - private final @StringRes int label; - private final @DrawableRes int icon; - - SelectionOption(@NonNull String code, @StringRes int label, @DrawableRes int icon) { - this.code = code; - this.label = label; - this.icon = icon; - } - - public @NonNull String getCode() { - return code; - } - - static SelectionOption fromCode(@NonNull String code) { - for (SelectionOption option : values()) { - if (option.code.equals(code)) { - return option; - } - } - - throw new IllegalStateException("Unknown option: " + code); - } - } - - private static class SelectionOptionViewHolder extends RecyclerView.ViewHolder { - - private final AppCompatTextView optionView; - - SelectionOptionViewHolder(@NonNull View itemView, @NonNull Consumer onClick) { - super(itemView); - itemView.setOnClickListener(v -> { - if (getAdapterPosition() != RecyclerView.NO_POSITION) { - onClick.accept(getAdapterPosition()); - } - }); - - optionView = (AppCompatTextView) itemView; - } - - void bind(@NonNull SelectionOption selectionOption) { - optionView.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(optionView.getContext(), selectionOption.icon), null, null, null); - optionView.setText(selectionOption.label); - } - } - - private static class SelectionOptionAdapter extends RecyclerView.Adapter { - - private final List options; - private final Consumer onOptionClicked; - - private SelectionOptionAdapter(@NonNull List options, @NonNull Consumer onOptionClicked) { - this.options = options; - this.onOptionClicked = onOptionClicked; - } - - @NonNull - @Override - public SelectionOptionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.avatar_selection_bottom_sheet_dialog_fragment_option, parent, false); - return new SelectionOptionViewHolder(view, (position) -> onOptionClicked.accept(options.get(position))); - } - - @Override - public void onBindViewHolder(@NonNull SelectionOptionViewHolder holder, int position) { - holder.bind(options.get(position)); - } - - @Override - public int getItemCount() { - return options.size(); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java index e4a9c8e581..09ecf77528 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.mediasend; import android.Manifest; +import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.graphics.PorterDuff; @@ -395,6 +396,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med } } + @SuppressLint("MissingSuperCall") @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/PartAuthority.java b/app/src/main/java/org/thoughtcrime/securesms/mms/PartAuthority.java index dc89d64318..9166c0e58f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/PartAuthority.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/PartAuthority.java @@ -8,10 +8,10 @@ import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import org.apache.http.auth.AUTH; import org.thoughtcrime.securesms.BuildConfig; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.AttachmentId; +import org.thoughtcrime.securesms.avatar.AvatarPickerStorage; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.emoji.EmojiFiles; import org.thoughtcrime.securesms.providers.BlobProvider; @@ -24,22 +24,25 @@ import java.io.InputStream; public class PartAuthority { - private static final String AUTHORITY = BuildConfig.APPLICATION_ID; - private static final String PART_URI_STRING = "content://" + AUTHORITY + "/part"; - private static final String STICKER_URI_STRING = "content://" + AUTHORITY + "/sticker"; - private static final String WALLPAPER_URI_STRING = "content://" + AUTHORITY + "/wallpaper"; - private static final String EMOJI_URI_STRING = "content://" + AUTHORITY + "/emoji"; - private static final Uri PART_CONTENT_URI = Uri.parse(PART_URI_STRING); - private static final Uri STICKER_CONTENT_URI = Uri.parse(STICKER_URI_STRING); - private static final Uri WALLPAPER_CONTENT_URI = Uri.parse(WALLPAPER_URI_STRING); - private static final Uri EMOJI_CONTENT_URI = Uri.parse(EMOJI_URI_STRING); + private static final String AUTHORITY = BuildConfig.APPLICATION_ID; + private static final String PART_URI_STRING = "content://" + AUTHORITY + "/part"; + private static final String STICKER_URI_STRING = "content://" + AUTHORITY + "/sticker"; + private static final String WALLPAPER_URI_STRING = "content://" + AUTHORITY + "/wallpaper"; + private static final String EMOJI_URI_STRING = "content://" + AUTHORITY + "/emoji"; + private static final String AVATAR_PICKER_URI_STRING = "content://" + AUTHORITY + "/avatar_picker"; + private static final Uri PART_CONTENT_URI = Uri.parse(PART_URI_STRING); + private static final Uri STICKER_CONTENT_URI = Uri.parse(STICKER_URI_STRING); + private static final Uri WALLPAPER_CONTENT_URI = Uri.parse(WALLPAPER_URI_STRING); + private static final Uri EMOJI_CONTENT_URI = Uri.parse(EMOJI_URI_STRING); + private static final Uri AVATAR_PICKER_CONTENT_URI = Uri.parse(AVATAR_PICKER_URI_STRING); - private static final int PART_ROW = 1; - private static final int PERSISTENT_ROW = 2; - private static final int BLOB_ROW = 3; - private static final int STICKER_ROW = 4; - private static final int WALLPAPER_ROW = 5; - private static final int EMOJI_ROW = 6; + private static final int PART_ROW = 1; + private static final int PERSISTENT_ROW = 2; + private static final int BLOB_ROW = 3; + private static final int STICKER_ROW = 4; + private static final int WALLPAPER_ROW = 5; + private static final int EMOJI_ROW = 6; + private static final int AVATAR_PICKER_ROW = 7; private static final UriMatcher uriMatcher; @@ -49,6 +52,7 @@ public class PartAuthority { uriMatcher.addURI(AUTHORITY, "sticker/#", STICKER_ROW); uriMatcher.addURI(AUTHORITY, "wallpaper/*", WALLPAPER_ROW); uriMatcher.addURI(AUTHORITY, "emoji/*", EMOJI_ROW); + uriMatcher.addURI(AUTHORITY, "avatar_picker/*", AVATAR_PICKER_ROW); uriMatcher.addURI(DeprecatedPersistentBlobProvider.AUTHORITY, DeprecatedPersistentBlobProvider.EXPECTED_PATH_OLD, PERSISTENT_ROW); uriMatcher.addURI(DeprecatedPersistentBlobProvider.AUTHORITY, DeprecatedPersistentBlobProvider.EXPECTED_PATH_NEW, PERSISTENT_ROW); uriMatcher.addURI(BlobProvider.AUTHORITY, BlobProvider.PATH, BLOB_ROW); @@ -66,13 +70,14 @@ public class PartAuthority { int match = uriMatcher.match(uri); try { switch (match) { - case PART_ROW: return DatabaseFactory.getAttachmentDatabase(context).getAttachmentStream(new PartUriParser(uri).getPartId(), 0); - case STICKER_ROW: return DatabaseFactory.getStickerDatabase(context).getStickerStream(ContentUris.parseId(uri)); - case PERSISTENT_ROW: return DeprecatedPersistentBlobProvider.getInstance(context).getStream(context, ContentUris.parseId(uri)); - case BLOB_ROW: return BlobProvider.getInstance().getStream(context, uri); - case WALLPAPER_ROW: return WallpaperStorage.read(context, getWallpaperFilename(uri)); - case EMOJI_ROW: return EmojiFiles.openForReading(context, getEmojiFilename(uri)); - default: return context.getContentResolver().openInputStream(uri); + case PART_ROW: return DatabaseFactory.getAttachmentDatabase(context).getAttachmentStream(new PartUriParser(uri).getPartId(), 0); + case STICKER_ROW: return DatabaseFactory.getStickerDatabase(context).getStickerStream(ContentUris.parseId(uri)); + case PERSISTENT_ROW: return DeprecatedPersistentBlobProvider.getInstance(context).getStream(context, ContentUris.parseId(uri)); + case BLOB_ROW: return BlobProvider.getInstance().getStream(context, uri); + case WALLPAPER_ROW: return WallpaperStorage.read(context, getWallpaperFilename(uri)); + case EMOJI_ROW: return EmojiFiles.openForReading(context, getEmojiFilename(uri)); + case AVATAR_PICKER_ROW: return AvatarPickerStorage.read(context, getAvatarPickerFilename(uri)); + default: return context.getContentResolver().openInputStream(uri); } } catch (SecurityException se) { throw new IOException(se); @@ -169,6 +174,10 @@ public class PartAuthority { return Uri.withAppendedPath(WALLPAPER_CONTENT_URI, filename); } + public static Uri getAvatarPickerUri(String filename) { + return Uri.withAppendedPath(AVATAR_PICKER_CONTENT_URI, filename); + } + public static Uri getEmojiUri(String sprite) { return Uri.withAppendedPath(EMOJI_CONTENT_URI, sprite); } @@ -181,6 +190,10 @@ public class PartAuthority { return uri.getPathSegments().get(1); } + public static String getAvatarPickerFilename(Uri uri) { + return uri.getPathSegments().get(1); + } + public static boolean isLocalUri(final @NonNull Uri uri) { int match = uriMatcher.match(uri); switch (match) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationConversation.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationConversation.kt index 8f08b96571..271844e565 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationConversation.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationConversation.kt @@ -52,7 +52,7 @@ data class NotificationConversation( return if (SignalStore.settings().messageNotificationsPrivacy.isDisplayContact) { recipient.getContactDrawable(context) } else { - GeneratedContactPhoto("Unknown", R.drawable.ic_profile_outline_40).asDrawable(context, AvatarColor.UNKNOWN.colorInt()) + GeneratedContactPhoto("Unknown", R.drawable.ic_profile_outline_40).asDrawable(context, AvatarColor.UNKNOWN) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationExtensions.kt index 2a6ecd132e..99d652fcb9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationExtensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationExtensions.kt @@ -56,12 +56,12 @@ fun Recipient.getContactDrawable(context: Context): Drawable? { ) .get() } catch (e: InterruptedException) { - fallbackContactPhoto.asDrawable(context, avatarColor.colorInt()) + fallbackContactPhoto.asDrawable(context, avatarColor) } catch (e: ExecutionException) { - fallbackContactPhoto.asDrawable(context, avatarColor.colorInt()) + fallbackContactPhoto.asDrawable(context, avatarColor) } } else { - fallbackContactPhoto.asDrawable(context, avatarColor.colorInt()) + fallbackContactPhoto.asDrawable(context, avatarColor) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java b/app/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java index acaee81dff..79f390411f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java +++ b/app/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java @@ -14,6 +14,7 @@ import android.view.Display; import android.view.ViewGroup; import android.view.WindowManager; +import androidx.activity.result.ActivityResultCallback; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.core.app.ActivityCompat; diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditGroupProfileRepository.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditGroupProfileRepository.java index 2da2d50368..9b0cdf2232 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditGroupProfileRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditGroupProfileRepository.java @@ -9,6 +9,7 @@ import androidx.core.util.Consumer; import org.signal.core.util.StreamUtil; import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.conversation.colors.AvatarColor; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.groups.GroupChangeException; import org.thoughtcrime.securesms.groups.GroupId; @@ -34,6 +35,11 @@ class EditGroupProfileRepository implements EditProfileRepository { this.groupId = groupId; } + @Override + public void getCurrentAvatarColor(@NonNull Consumer avatarColorConsumer) { + SimpleTask.run(() -> Recipient.resolved(getRecipientId()).getAvatarColor(), avatarColorConsumer::accept); + } + @Override public void getCurrentProfileName(@NonNull Consumer profileNameConsumer) { profileNameConsumer.accept(ProfileName.EMPTY); diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileActivity.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileActivity.java index 2768df2e0e..9fd688e020 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileActivity.java @@ -10,6 +10,7 @@ import androidx.annotation.NonNull; import androidx.navigation.NavDirections; import androidx.navigation.NavGraph; import androidx.navigation.Navigation; +import androidx.navigation.fragment.NavHostFragment; import org.thoughtcrime.securesms.BaseActivity; import org.thoughtcrime.securesms.R; @@ -61,10 +62,10 @@ public class EditProfileActivity extends BaseActivity implements EditProfileFrag setContentView(R.layout.profile_create_activity); if (bundle == null) { - Bundle extras = getIntent().getExtras(); - NavGraph graph = Navigation.findNavController(this, R.id.nav_host_fragment).getGraph(); - - Navigation.findNavController(this, R.id.nav_host_fragment).setGraph(graph, extras != null ? extras : new Bundle()); + NavHostFragment fragment = NavHostFragment.create(R.navigation.edit_profile, getIntent().getExtras()); + getSupportFragmentManager().beginTransaction() + .add(R.id.fragment_container, fragment) + .commit(); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileFragment.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileFragment.java index 8e43b9fd74..be3e1b7c14 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileFragment.java @@ -5,6 +5,7 @@ import android.content.Context; import android.content.Intent; import android.os.Build; import android.os.Bundle; +import android.os.Parcelable; import android.text.InputType; import android.view.LayoutInflater; import android.view.View; @@ -20,7 +21,9 @@ import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.appcompat.widget.Toolbar; import androidx.lifecycle.ViewModelProviders; +import androidx.navigation.Navigation; +import com.airbnb.lottie.SimpleColorFilter; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.dd.CircularProgressButton; @@ -29,11 +32,10 @@ import org.signal.core.util.StreamUtil; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.LoggingFragment; import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto; -import org.thoughtcrime.securesms.conversation.colors.AvatarColor; +import org.thoughtcrime.securesms.avatar.Avatars; +import org.thoughtcrime.securesms.avatar.picker.AvatarPickerFragment; import org.thoughtcrime.securesms.groups.GroupId; -import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity; -import org.thoughtcrime.securesms.mediasend.AvatarSelectionBottomSheetDialogFragment; +import org.thoughtcrime.securesms.groups.ParcelableGroupId; import org.thoughtcrime.securesms.mediasend.Media; import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.profiles.manage.EditProfileNameFragment; @@ -47,7 +49,6 @@ import org.thoughtcrime.securesms.util.views.LearnMoreTextView; import java.io.IOException; import java.io.InputStream; -import static android.app.Activity.RESULT_OK; import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.EXCLUDE_SYSTEM; import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.GROUP_ID; import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.NEXT_BUTTON_TEXT; @@ -57,7 +58,6 @@ import static org.thoughtcrime.securesms.profiles.edit.EditProfileActivity.SHOW_ public class EditProfileFragment extends LoggingFragment { private static final String TAG = Log.tag(EditProfileFragment.class); - private static final short REQUEST_CODE_SELECT_AVATAR = 31726; private static final int MAX_DESCRIPTION_GLYPHS = 480; private static final int MAX_DESCRIPTION_BYTES = 8192; @@ -69,6 +69,8 @@ public class EditProfileFragment extends LoggingFragment { private EditText familyName; private View reveal; private TextView preview; + private ImageView avatarPreviewBackground; + private ImageView avatarPreview; private Intent nextIntent; @@ -100,45 +102,38 @@ public class EditProfileFragment extends LoggingFragment { initializeResources(view, groupId); initializeProfileAvatar(); initializeProfileName(); + + getParentFragmentManager().setFragmentResultListener(AvatarPickerFragment.REQUEST_KEY_SELECT_AVATAR, getViewLifecycleOwner(), (key, bundle) -> { + Media media = bundle.getParcelable(AvatarPickerFragment.SELECT_AVATAR_MEDIA); + handleMediaFromResult(media); + }); } - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); + private void handleMediaFromResult(@NonNull Media media) { + SimpleTask.run(() -> { + try { + InputStream stream = BlobProvider.getInstance().getStream(requireContext(), media.getUri()); - if (requestCode == REQUEST_CODE_SELECT_AVATAR && resultCode == RESULT_OK) { - - if (data != null && data.getBooleanExtra("delete", false)) { - viewModel.setAvatar(null); - avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_camera_solid_white_24).asDrawable(requireActivity(), AvatarColor.UNKNOWN.colorInt())); - return; + return StreamUtil.readFully(stream); + } catch (IOException ioException) { + Log.w(TAG, ioException); + return null; } - - SimpleTask.run(() -> { - try { - Media result = data.getParcelableExtra(AvatarSelectionActivity.EXTRA_MEDIA); - InputStream stream = BlobProvider.getInstance().getStream(requireContext(), result.getUri()); - - return StreamUtil.readFully(stream); - } catch (IOException ioException) { - Log.w(TAG, ioException); - return null; - } - }, - (avatarBytes) -> { - if (avatarBytes != null) { - viewModel.setAvatar(avatarBytes); - GlideApp.with(EditProfileFragment.this) - .load(avatarBytes) - .skipMemoryCache(true) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .circleCrop() - .into(avatar); - } else { - Toast.makeText(requireActivity(), R.string.CreateProfileActivity_error_setting_profile_photo, Toast.LENGTH_LONG).show(); - } - }); - } + }, + (avatarBytes) -> { + if (avatarBytes != null) { + viewModel.setAvatarMedia(media); + viewModel.setAvatar(avatarBytes); + GlideApp.with(EditProfileFragment.this) + .load(avatarBytes) + .skipMemoryCache(true) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .circleCrop() + .into(avatar); + } else { + Toast.makeText(requireActivity(), R.string.CreateProfileActivity_error_setting_profile_photo, Toast.LENGTH_LONG).show(); + } + }); } private void initializeViewModel(boolean excludeSystem, @Nullable GroupId groupId, boolean hasSavedInstanceState) { @@ -160,15 +155,17 @@ public class EditProfileFragment extends LoggingFragment { Bundle arguments = requireArguments(); boolean isEditingGroup = groupId != null; - this.toolbar = view.findViewById(R.id.toolbar); - this.title = view.findViewById(R.id.title); - this.avatar = view.findViewById(R.id.avatar); - this.givenName = view.findViewById(R.id.given_name); - this.familyName = view.findViewById(R.id.family_name); - this.finishButton = view.findViewById(R.id.finish_button); - this.reveal = view.findViewById(R.id.reveal); - this.preview = view.findViewById(R.id.name_preview); - this.nextIntent = arguments.getParcelable(NEXT_INTENT); + this.toolbar = view.findViewById(R.id.toolbar); + this.title = view.findViewById(R.id.title); + this.avatar = view.findViewById(R.id.avatar); + this.givenName = view.findViewById(R.id.given_name); + this.familyName = view.findViewById(R.id.family_name); + this.finishButton = view.findViewById(R.id.finish_button); + this.reveal = view.findViewById(R.id.reveal); + this.preview = view.findViewById(R.id.name_preview); + this.avatarPreviewBackground = view.findViewById(R.id.avatar_background); + this.avatarPreview = view.findViewById(R.id.avatar_placeholder); + this.nextIntent = arguments.getParcelable(NEXT_INTENT); this.avatar.setOnClickListener(v -> startAvatarSelection()); @@ -255,6 +252,13 @@ public class EditProfileFragment extends LoggingFragment { .circleCrop() .into(avatar); }); + + viewModel.avatarColor().observe(getViewLifecycleOwner(), avatarColor -> { + Avatars.ForegroundColor foregroundColor = Avatars.getForegroundColor(avatarColor); + + avatarPreview.getDrawable().setColorFilter(new SimpleColorFilter(foregroundColor.getColorInt())); + avatarPreviewBackground.getDrawable().setColorFilter(new SimpleColorFilter(avatarColor.colorInt())); + }); } private static void updateFieldIfNeeded(@NonNull EditText field, @NonNull String value) { @@ -273,11 +277,12 @@ public class EditProfileFragment extends LoggingFragment { } private void startAvatarSelection() { - AvatarSelectionBottomSheetDialogFragment.create(viewModel.canRemoveProfilePhoto(), - true, - REQUEST_CODE_SELECT_AVATAR, - viewModel.isGroup()) - .show(getChildFragmentManager(), null); + if (viewModel.isGroup()) { + Parcelable groupId = ParcelableGroupId.from(viewModel.getGroupId()); + Navigation.findNavController(requireView()).navigate(EditProfileFragmentDirections.actionCreateProfileFragmentToAvatarPicker((ParcelableGroupId) groupId, viewModel.getAvatarMedia())); + } else { + Navigation.findNavController(requireView()).navigate(EditProfileFragmentDirections.actionCreateProfileFragmentToAvatarPicker(null, null)); + } } private void handleUpload() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileRepository.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileRepository.java index 230ea5ae15..2a4a374cbb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileRepository.java @@ -4,11 +4,14 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.util.Consumer; +import org.thoughtcrime.securesms.conversation.colors.AvatarColor; import org.thoughtcrime.securesms.profiles.ProfileName; import org.whispersystems.libsignal.util.guava.Optional; interface EditProfileRepository { + void getCurrentAvatarColor(@NonNull Consumer avatarColorConsumer); + void getCurrentProfileName(@NonNull Consumer profileNameConsumer); void getCurrentAvatar(@NonNull Consumer avatarConsumer); diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileViewModel.java index e121272244..5d3c71a6ba 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileViewModel.java @@ -8,7 +8,9 @@ import androidx.lifecycle.Transformations; import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; +import org.thoughtcrime.securesms.conversation.colors.AvatarColor; import org.thoughtcrime.securesms.groups.GroupId; +import org.thoughtcrime.securesms.mediasend.Media; import org.thoughtcrime.securesms.profiles.ProfileName; import org.thoughtcrime.securesms.profiles.edit.EditProfileRepository.UploadResult; import org.thoughtcrime.securesms.util.SingleLiveEvent; @@ -29,10 +31,12 @@ class EditProfileViewModel extends ViewModel { private final MutableLiveData originalAvatar = new MutableLiveData<>(); private final MutableLiveData originalDisplayName = new MutableLiveData<>(); private final SingleLiveEvent uploadResult = new SingleLiveEvent<>(); + private final MutableLiveData avatarColor = new MutableLiveData<>(); private final LiveData isFormValid; private final EditProfileRepository repository; private final GroupId groupId; private String originalDescription; + private Media avatarMedia; private EditProfileViewModel(@NonNull EditProfileRepository repository, boolean hasInstanceState, @Nullable GroupId groupId) { this.repository = repository; @@ -59,9 +63,15 @@ class EditProfileViewModel extends ViewModel { internalAvatar.setValue(value); originalAvatar.setValue(value); }); + + repository.getCurrentAvatarColor(avatarColor::setValue); } } + public LiveData avatarColor() { + return Transformations.distinctUntilChanged(avatarColor); + } + public LiveData givenName() { return Transformations.distinctUntilChanged(givenName); } @@ -90,6 +100,18 @@ class EditProfileViewModel extends ViewModel { return groupId != null; } + public @Nullable Media getAvatarMedia() { + return avatarMedia; + } + + public void setAvatarMedia(@Nullable Media avatarMedia) { + this.avatarMedia = avatarMedia; + } + + public @Nullable GroupId getGroupId() { + return groupId; + } + public boolean canRemoveProfilePhoto() { return hasAvatar(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditSelfProfileRepository.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditSelfProfileRepository.java index f4eccdf5c4..0e02f9640f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditSelfProfileRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditSelfProfileRepository.java @@ -9,6 +9,7 @@ import androidx.core.util.Consumer; import org.signal.core.util.StreamUtil; import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.conversation.colors.AvatarColor; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobs.MultiDeviceProfileContentUpdateJob; @@ -42,6 +43,11 @@ public class EditSelfProfileRepository implements EditProfileRepository { this.excludeSystem = excludeSystem; } + @Override + public void getCurrentAvatarColor(@NonNull Consumer avatarColorConsumer) { + SimpleTask.run(() -> Recipient.self().getAvatarColor(), avatarColorConsumer::accept); + } + @Override public void getCurrentProfileName(@NonNull Consumer profileNameConsumer) { ProfileName storedProfileName = Recipient.self().getProfileName(); 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 c3f1253f4b..cb0fe6c873 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 @@ -23,9 +23,9 @@ import com.bumptech.glide.Glide; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.LoggingFragment; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.avatar.picker.AvatarPickerFragment; import org.thoughtcrime.securesms.components.emoji.EmojiUtil; import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity; -import org.thoughtcrime.securesms.mediasend.AvatarSelectionBottomSheetDialogFragment; import org.thoughtcrime.securesms.mediasend.Media; import org.thoughtcrime.securesms.profiles.ProfileName; import org.thoughtcrime.securesms.profiles.manage.ManageProfileViewModel.AvatarState; @@ -35,8 +35,7 @@ import static android.app.Activity.RESULT_OK; public class ManageProfileFragment extends LoggingFragment { - private static final String TAG = Log.tag(ManageProfileFragment.class); - private static final short REQUEST_CODE_SELECT_AVATAR = 31726; + private static final String TAG = Log.tag(ManageProfileFragment.class); private Toolbar toolbar; private ImageView avatarView; @@ -86,22 +85,11 @@ public class ManageProfileFragment extends LoggingFragment { this.aboutContainer.setOnClickListener(v -> { Navigation.findNavController(v).navigate(ManageProfileFragmentDirections.actionManageAbout()); }); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - if (requestCode == REQUEST_CODE_SELECT_AVATAR && resultCode == RESULT_OK) { - if (data != null && data.getBooleanExtra("delete", false)) { - viewModel.onAvatarSelected(requireContext(), null); - return; - } - - Media result = data.getParcelableExtra(AvatarSelectionActivity.EXTRA_MEDIA); + getParentFragmentManager().setFragmentResultListener(AvatarPickerFragment.REQUEST_KEY_SELECT_AVATAR, getViewLifecycleOwner(), (key, bundle) -> { + Media result = bundle.getParcelable(AvatarPickerFragment.SELECT_AVATAR_MEDIA); viewModel.onAvatarSelected(requireContext(), result); - } + }); } private void initializeViewModel() { @@ -193,10 +181,6 @@ public class ManageProfileFragment extends LoggingFragment { } private void onAvatarClicked() { - AvatarSelectionBottomSheetDialogFragment.create(viewModel.canRemoveAvatar(), - true, - REQUEST_CODE_SELECT_AVATAR, - false) - .show(getChildFragmentManager(), null); + Navigation.findNavController(requireView()).navigate(ManageProfileFragmentDirections.actionManageProfileFragmentToAvatarPicker(null, null)); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/spoofing/ReviewBannerView.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/spoofing/ReviewBannerView.java index a4c45946d4..7d346bcbc3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/spoofing/ReviewBannerView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/spoofing/ReviewBannerView.java @@ -21,6 +21,7 @@ import org.thoughtcrime.securesms.components.AvatarImageView; import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto; import org.thoughtcrime.securesms.contacts.avatars.FallbackPhoto20dp; import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto; +import org.thoughtcrime.securesms.conversation.colors.AvatarColor; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.ViewUtil; @@ -122,7 +123,7 @@ public class ReviewBannerView extends LinearLayout { } @Override - protected Drawable newFallbackDrawable(@NonNull Context context, int color, boolean inverted) { + protected Drawable newFallbackDrawable(@NonNull Context context, @NonNull AvatarColor color, boolean inverted) { return new FallbackPhoto20dp(getFallbackResId()).asDrawable(context, color, inverted); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java index 7d2a54e5fb..72b85ffc88 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java @@ -793,11 +793,11 @@ public class Recipient { } public @NonNull Drawable getFallbackContactPhotoDrawable(Context context, boolean inverted, @Nullable FallbackPhotoProvider fallbackPhotoProvider) { - return getFallbackContactPhoto(Util.firstNonNull(fallbackPhotoProvider, DEFAULT_FALLBACK_PHOTO_PROVIDER)).asDrawable(context, avatarColor.colorInt(), inverted); + return getFallbackContactPhoto(Util.firstNonNull(fallbackPhotoProvider, DEFAULT_FALLBACK_PHOTO_PROVIDER)).asDrawable(context, avatarColor, inverted); } public @NonNull Drawable getSmallFallbackContactPhotoDrawable(Context context, boolean inverted, @Nullable FallbackPhotoProvider fallbackPhotoProvider) { - return getFallbackContactPhoto(Util.firstNonNull(fallbackPhotoProvider, DEFAULT_FALLBACK_PHOTO_PROVIDER)).asSmallDrawable(context, avatarColor.colorInt(), inverted); + return getFallbackContactPhoto(Util.firstNonNull(fallbackPhotoProvider, DEFAULT_FALLBACK_PHOTO_PROVIDER)).asSmallDrawable(context, avatarColor, inverted); } public @NonNull FallbackContactPhoto getFallbackContactPhoto() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java index d40bb343a5..f3a54c215f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientBottomSheetDialogFragment.java @@ -144,7 +144,7 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF avatar.setFallbackPhotoProvider(new Recipient.FallbackPhotoProvider() { @Override public @NonNull FallbackContactPhoto getPhotoForLocalNumber() { - return new FallbackPhoto80dp(R.drawable.ic_note_80, recipient.getAvatarColor().colorInt()); + return new FallbackPhoto80dp(R.drawable.ic_note_80, recipient.getAvatarColor()); } }); avatar.setAvatar(recipient); diff --git a/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java b/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java index 1959096267..96385943f2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java @@ -16,6 +16,7 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; @@ -51,6 +52,7 @@ import org.whispersystems.libsignal.util.Pair; import java.io.ByteArrayOutputStream; import java.util.Collections; import java.util.List; +import java.util.Objects; import static android.app.Activity.RESULT_OK; @@ -60,8 +62,8 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu private static final String TAG = Log.tag(ImageEditorFragment.class); - private static final String KEY_IMAGE_URI = "image_uri"; - private static final String KEY_IS_AVATAR_MODE = "avatar_mode"; + private static final String KEY_IMAGE_URI = "image_uri"; + private static final String KEY_MODE = "mode"; private static final int SELECT_STICKER_REQUEST_CODE = 124; @@ -104,15 +106,22 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu private ImageEditorHud imageEditorHud; private ImageEditorView imageEditorView; - public static ImageEditorFragment newInstanceForAvatar(@NonNull Uri imageUri) { + public static ImageEditorFragment newInstanceForAvatarCapture(@NonNull Uri imageUri) { ImageEditorFragment fragment = newInstance(imageUri); - fragment.requireArguments().putBoolean(KEY_IS_AVATAR_MODE, true); + fragment.requireArguments().putString(KEY_MODE, Mode.AVATAR_CAPTURE.code); + return fragment; + } + + public static ImageEditorFragment newInstanceForAvatarEdit(@NonNull Uri imageUri) { + ImageEditorFragment fragment = newInstance(imageUri); + fragment.requireArguments().putString(KEY_MODE, Mode.AVATAR_EDIT.code); return fragment; } public static ImageEditorFragment newInstance(@NonNull Uri imageUri) { Bundle args = new Bundle(); args.putParcelable(KEY_IMAGE_URI, imageUri); + args.putString(KEY_MODE, Mode.NORMAL.code); ImageEditorFragment fragment = new ImageEditorFragment(); fragment.setArguments(args); @@ -123,10 +132,16 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (!(getActivity() instanceof Controller)) { + + Fragment parent = getParentFragment(); + if (parent instanceof Controller) { + controller = (Controller) parent; + } else if (getActivity() instanceof Controller) { + controller = (Controller) getActivity(); + } else { throw new IllegalStateException("Parent activity must implement Controller interface."); } - controller = (Controller) getActivity(); + Bundle arguments = getArguments(); if (arguments != null) { imageUri = arguments.getParcelable(KEY_IMAGE_URI); @@ -152,7 +167,7 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - boolean isAvatarMode = requireArguments().getBoolean(KEY_IS_AVATAR_MODE, false); + Mode mode = Mode.getByCode(requireArguments().getString(KEY_MODE)); imageEditorHud = view.findViewById(R.id.scribble_hud); imageEditorView = view.findViewById(R.id.image_editor_view); @@ -171,14 +186,28 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu } if (editorModel == null) { - editorModel = isAvatarMode ? EditorModel.createForCircleEditing() : EditorModel.create(); + switch (mode) { + case AVATAR_EDIT: + editorModel = EditorModel.createForAvatarEdit(); + break; + case AVATAR_CAPTURE: + editorModel = EditorModel.createForAvatarCapture(); + break; + default: + editorModel = EditorModel.create(); + break; + } + EditorElement image = new EditorElement(new UriGlideRenderer(imageUri, true, imageMaxWidth, imageMaxHeight)); image.getFlags().setSelectable(false).persist(); editorModel.addElement(image); } - if (isAvatarMode) { + if (mode == Mode.AVATAR_CAPTURE || mode == Mode.AVATAR_EDIT) { imageEditorHud.setUpForAvatarEditing(); + } + + if (mode == Mode.AVATAR_CAPTURE) { imageEditorHud.enterMode(ImageEditorHud.Mode.CROP); } @@ -460,24 +489,27 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu } private void performSaveToDisk() { - SimpleTask.run(() -> { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - Bitmap image = imageEditorView.getModel().render(requireContext()); - - image.compress(Bitmap.CompressFormat.JPEG, 80, outputStream); - - return BlobProvider.getInstance() - .forData(outputStream.toByteArray()) - .withMimeType(MediaUtil.IMAGE_JPEG) - .createForSingleUseInMemory(); - - }, uri -> { + SimpleTask.run(this::renderToSingleUseBlob, uri -> { SaveAttachmentTask saveTask = new SaveAttachmentTask(requireContext()); SaveAttachmentTask.Attachment attachment = new SaveAttachmentTask.Attachment(uri, MediaUtil.IMAGE_JPEG, System.currentTimeMillis(), null); saveTask.executeOnExecutor(SignalExecutors.BOUNDED, attachment); }); } + @WorkerThread + public @NonNull Uri renderToSingleUseBlob() { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + Bitmap image = imageEditorView.getModel().render(requireContext()); + + image.compress(Bitmap.CompressFormat.JPEG, 80, outputStream); + image.recycle(); + + return BlobProvider.getInstance() + .forData(outputStream.toByteArray()) + .withMimeType(MediaUtil.IMAGE_JPEG) + .createForSingleUseInMemory(); + } + private void refreshUniqueColors() { imageEditorHud.setColorPalette(imageEditorView.getModel().getUniqueColorsIgnoringAlpha()); } @@ -587,4 +619,35 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu this.position.preConcat(imageProjectionMatrix); } } + + private enum Mode { + + NORMAL("normal"), + AVATAR_CAPTURE("avatar_capture"), + AVATAR_EDIT("avatar_edit"); + + private final String code; + + Mode(@NonNull String code) { + this.code = code; + } + + String getCode() { + return code; + } + + static Mode getByCode(@Nullable String code) { + if (code == null) { + return NORMAL; + } + + for (Mode mode : values()) { + if (Objects.equals(code, mode.code)) { + return mode; + } + } + + return NORMAL; + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/AvatarUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/AvatarUtil.java index 105071ff87..ee96b3c727 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/AvatarUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/AvatarUtil.java @@ -154,6 +154,6 @@ public final class AvatarUtil { private static Drawable getFallback(@NonNull Context context, @NonNull Recipient recipient) { String name = Optional.fromNullable(recipient.getDisplayName(context)).or(""); - return new GeneratedContactPhoto(name, R.drawable.ic_profile_outline_40).asDrawable(context, recipient.getAvatarColor().colorInt()); + return new GeneratedContactPhoto(name, R.drawable.ic_profile_outline_40).asDrawable(context, recipient.getAvatarColor()); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ConversationShortcutPhoto.java b/app/src/main/java/org/thoughtcrime/securesms/util/ConversationShortcutPhoto.java index 4d2ac100cd..e9cb1e198d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ConversationShortcutPhoto.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ConversationShortcutPhoto.java @@ -13,7 +13,6 @@ import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.data.DataFetcher; -import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.load.model.ModelLoaderFactory; import com.bumptech.glide.load.model.MultiModelLoaderFactory; @@ -23,9 +22,8 @@ import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto; import org.thoughtcrime.securesms.contacts.avatars.FallbackPhoto80dp; import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto; import org.thoughtcrime.securesms.contacts.avatars.SystemContactPhoto; +import org.thoughtcrime.securesms.conversation.colors.AvatarColor; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.mms.GlideApp; -import org.thoughtcrime.securesms.mms.GlideRequest; import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.recipients.Recipient; import org.whispersystems.libsignal.util.ByteUtil; @@ -186,8 +184,8 @@ public final class ConversationShortcutPhoto implements Key { photoSource = R.drawable.ic_profile_80; } - FallbackContactPhoto photo = recipient.isSelf() || recipient.isGroup() ? new FallbackPhoto80dp(photoSource, recipient.getAvatarColor().colorInt()) - : new ShortcutGeneratedContactPhoto(recipient.getDisplayName(context), photoSource, ViewUtil.dpToPx(80), ViewUtil.dpToPx(28), recipient.getAvatarColor().colorInt()); + FallbackContactPhoto photo = recipient.isSelf() || recipient.isGroup() ? new FallbackPhoto80dp(photoSource, recipient.getAvatarColor()) + : new ShortcutGeneratedContactPhoto(recipient.getDisplayName(context), photoSource, ViewUtil.dpToPx(80), recipient.getAvatarColor()); Bitmap toWrap = DrawableUtil.toBitmap(photo.asCallCard(context), ViewUtil.dpToPx(80), ViewUtil.dpToPx(80)); Bitmap wrapped = DrawableUtil.wrapBitmapForShortcutInfo(toWrap); @@ -199,20 +197,20 @@ public final class ConversationShortcutPhoto implements Key { private static final class ShortcutGeneratedContactPhoto extends GeneratedContactPhoto { - private final int color; + private final AvatarColor color; - public ShortcutGeneratedContactPhoto(@NonNull String name, int fallbackResId, int targetSize, int fontSize, int color) { - super(name, fallbackResId, targetSize, fontSize); + public ShortcutGeneratedContactPhoto(@NonNull String name, int fallbackResId, int targetSize, @NonNull AvatarColor color) { + super(name, fallbackResId, targetSize); this.color = color; } @Override - protected Drawable newFallbackDrawable(@NonNull Context context, int color, boolean inverted) { - return new FallbackPhoto80dp(getFallbackResId(), color).asDrawable(context, -1); + protected Drawable newFallbackDrawable(@NonNull Context context, @NonNull AvatarColor color, boolean inverted) { + return new FallbackPhoto80dp(getFallbackResId(), color).asDrawable(context, AvatarColor.UNKNOWN); } - @Override public Drawable asCallCard(Context context) { + @Override public Drawable asCallCard(@NonNull Context context) { return new FallbackPhoto80dp(getFallbackResId(), color).asCallCard(context); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/MappingAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/util/MappingAdapter.java index 2eccc687e8..8d0bc2b9b2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/MappingAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/MappingAdapter.java @@ -7,6 +7,7 @@ import android.view.ViewGroup; import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.recyclerview.widget.DefaultItemAnimator; import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.ListAdapter; @@ -33,16 +34,16 @@ import kotlin.jvm.functions.Function1; * override compiler typing recommendations when binding and diffing. *

* General pattern for implementation: - *
    - *
  1. Create {@link MappingModel}s for the items in the list. These encapsulate data massaging methods for views to use and the diff logic.
  2. - *
  3. Create {@link MappingViewHolder}s for each item type in the list and their corresponding {@link Factory}.
  4. - *
  5. Create an instance or subclass of {@link MappingAdapter} and register the mapping of model type to view holder factory for that model type.
  6. - *
- * Event listeners, click or otherwise, are handled at the view holder level and should be passed into the appropriate view holder factories. This - * pattern mimics how we pass data into view models via factories. - *

- * NOTE: There can only be on factory registered per model type. Registering two for the same type will result in the last one being used. However, the - * same factory can be registered multiple times for multiple model types (if the model type class hierarchy supports it). + *
    + *
  1. Create {@link MappingModel}s for the items in the list. These encapsulate data massaging methods for views to use and the diff logic.
  2. + *
  3. Create {@link MappingViewHolder}s for each item type in the list and their corresponding {@link Factory}.
  4. + *
  5. Create an instance or subclass of {@link MappingAdapter} and register the mapping of model type to view holder factory for that model type.
  6. + *
+ * Event listeners, click or otherwise, are handled at the view holder level and should be passed into the appropriate view holder factories. This + * pattern mimics how we pass data into view models via factories. + *

+ * NOTE: There can only be on factory registered per model type. Registering two for the same type will result in the last one being used. However, the + * same factory can be registered multiple times for multiple model types (if the model type class hierarchy supports it). */ public class MappingAdapter extends ListAdapter, MappingViewHolder> { @@ -102,6 +103,12 @@ public class MappingAdapter extends ListAdapter, MappingViewHold return Objects.requireNonNull(factories.get(viewType)).createViewHolder(parent); } + @Override + public void onBindViewHolder(@NonNull MappingViewHolder holder, int position, @NonNull List payloads) { + holder.setPayload(payloads); + onBindViewHolder(holder, position); + } + @Override public void onBindViewHolder(@NonNull MappingViewHolder holder, int position) { //noinspection unchecked @@ -142,6 +149,16 @@ public class MappingAdapter extends ListAdapter, MappingViewHold } return false; } + + @Override + public @Nullable Object getChangePayload(@NonNull MappingModel oldItem, @NonNull MappingModel newItem) { + if (oldItem.getClass() == newItem.getClass()) { + //noinspection unchecked + return oldItem.getChangePayload(newItem); + } + + return null; + } } public interface Factory> { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/MappingModel.java b/app/src/main/java/org/thoughtcrime/securesms/util/MappingModel.java index 0e5233d178..21fc151246 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/MappingModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/MappingModel.java @@ -1,8 +1,13 @@ package org.thoughtcrime.securesms.util; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; public interface MappingModel { boolean areItemsTheSame(@NonNull T newItem); boolean areContentsTheSame(@NonNull T newItem); + + default @Nullable Object getChangePayload(@NonNull T newItem) { + return null; + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/MappingViewHolder.java b/app/src/main/java/org/thoughtcrime/securesms/util/MappingViewHolder.java index 4fa8c76eb3..09386a2427 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/MappingViewHolder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/MappingViewHolder.java @@ -7,13 +7,18 @@ import androidx.annotation.IdRes; import androidx.annotation.NonNull; import androidx.lifecycle.LifecycleOwner; +import java.util.LinkedList; +import java.util.List; + public abstract class MappingViewHolder> extends LifecycleViewHolder implements LifecycleOwner { - protected final Context context; + protected final Context context; + protected final List payload; public MappingViewHolder(@NonNull View itemView) { super(itemView); context = itemView.getContext(); + payload = new LinkedList<>(); } public T findViewById(@IdRes int id) { @@ -26,6 +31,11 @@ public abstract class MappingViewHolder> exten public abstract void bind(@NonNull Model model); + public void setPayload(@NonNull List payload) { + this.payload.clear(); + this.payload.addAll(payload); + } + public static final class SimpleViewHolder> extends MappingViewHolder { public SimpleViewHolder(@NonNull View itemView) { super(itemView); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/NameUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/NameUtil.kt new file mode 100644 index 0000000000..18d8cf67ac --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/NameUtil.kt @@ -0,0 +1,34 @@ +package org.thoughtcrime.securesms.util + +import android.text.TextUtils +import java.util.regex.Pattern + +object NameUtil { + private val PATTERN = Pattern.compile("[^\\p{L}\\p{Nd}\\p{S}]+") + + /** + * Returns an abbreviation of the input, up to two characters long. + */ + @JvmStatic + fun getAbbreviation(name: String): String? { + val parts = name.split(" ").toTypedArray() + val builder = StringBuilder() + var count = 0 + var i = 0 + + while (i < parts.size && count < 2) { + val cleaned = PATTERN.matcher(parts[i]).replaceFirst("") + if (!TextUtils.isEmpty(cleaned)) { + builder.appendCodePoint(cleaned.codePointAt(0)) + count++ + } + i++ + } + + return if (builder.isEmpty()) { + null + } else { + builder.toString() + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/storage/FileStorage.java b/app/src/main/java/org/thoughtcrime/securesms/util/storage/FileStorage.java new file mode 100644 index 0000000000..476ee61c15 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/storage/FileStorage.java @@ -0,0 +1,92 @@ +package org.thoughtcrime.securesms.util.storage; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.WorkerThread; + +import org.signal.core.util.StreamUtil; +import org.thoughtcrime.securesms.crypto.AttachmentSecret; +import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider; +import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream; +import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Manages the storage of custom files. + */ +public final class FileStorage { + + /** + * Saves the provided input stream as a new file. + */ + @WorkerThread + public static @NonNull String save(@NonNull Context context, + @NonNull InputStream inputStream, + @NonNull String directoryName, + @NonNull String fileNameBase, + @NonNull String extension + ) throws IOException + { + File directory = context.getDir(directoryName, Context.MODE_PRIVATE); + File file = File.createTempFile(fileNameBase, "." + extension, directory); + + StreamUtil.copy(inputStream, getOutputStream(context, file)); + + return file.getName(); + } + + @WorkerThread + public static @NonNull InputStream read(@NonNull Context context, + @NonNull String directoryName, + @NonNull String filename) throws IOException + { + File directory = context.getDir(directoryName, Context.MODE_PRIVATE); + File file = new File(directory, filename); + + return getInputStream(context, file); + } + + @WorkerThread + public static @NonNull List getAll(@NonNull Context context, + @NonNull String directoryName, + @NonNull String fileNameBase) + { + return getAllFiles(context, directoryName, fileNameBase).stream() + .map(File::getName) + .collect(Collectors.toList()); + } + + @WorkerThread + public static @NonNull List getAllFiles(@NonNull Context context, + @NonNull String directoryName, + @NonNull String fileNameBase) + { + File directory = context.getDir(directoryName, Context.MODE_PRIVATE); + File[] allFiles = directory.listFiles(pathname -> pathname.getName().contains(fileNameBase)); + + if (allFiles != null) { + return Arrays.asList(allFiles); + } else { + return Collections.emptyList(); + } + } + + private static @NonNull OutputStream getOutputStream(@NonNull Context context, File outputFile) throws IOException { + AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(); + return ModernEncryptingPartOutputStream.createFor(attachmentSecret, outputFile, true).second; + } + + private static @NonNull InputStream getInputStream(@NonNull Context context, File inputFile) throws IOException { + AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(); + return ModernDecryptingPartInputStream.createFor(attachmentSecret, inputFile, 0); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperViewModel.java index e6d1fb7f76..318fbbc260 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperViewModel.java @@ -58,7 +58,7 @@ public class ChatWallpaperViewModel extends ViewModel { }); } else { liveRecipient = null; - wallpaperPreviewPortrait = new DefaultValueLiveData<>(new WallpaperPreviewPortrait.SolidColor(AvatarColor.ULTRAMARINE)); + wallpaperPreviewPortrait = new DefaultValueLiveData<>(new WallpaperPreviewPortrait.SolidColor(AvatarColor.A100)); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/WallpaperStorage.java b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/WallpaperStorage.java index 70ab2ce4be..8c2804f7ee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/WallpaperStorage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/WallpaperStorage.java @@ -6,25 +6,18 @@ import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; -import com.annimon.stream.Stream; - -import org.signal.core.util.StreamUtil; import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.crypto.AttachmentSecret; -import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider; -import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream; -import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.mms.PartAuthority; +import org.thoughtcrime.securesms.util.storage.FileStorage; import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; -import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; /** * Manages the storage of custom wallpaper files. @@ -41,36 +34,23 @@ public final class WallpaperStorage { */ @WorkerThread public static @NonNull ChatWallpaper save(@NonNull Context context, @NonNull InputStream wallpaperStream, @NonNull String extension) throws IOException { - File directory = context.getDir(DIRECTORY, Context.MODE_PRIVATE); - File file = File.createTempFile(FILENAME_BASE, "." + extension, directory); + String name = FileStorage.save(context, wallpaperStream, DIRECTORY, FILENAME_BASE, extension); - StreamUtil.copy(wallpaperStream, getOutputStream(context, file)); - - return ChatWallpaperFactory.create(PartAuthority.getWallpaperUri(file.getName())); + return ChatWallpaperFactory.create(PartAuthority.getWallpaperUri(name)); } @WorkerThread public static @NonNull InputStream read(@NonNull Context context, String filename) throws IOException { - File directory = context.getDir(DIRECTORY, Context.MODE_PRIVATE); - File wallpaperFile = new File(directory, filename); - - return getInputStream(context, wallpaperFile); + return FileStorage.read(context, DIRECTORY, filename); } @WorkerThread public static @NonNull List getAll(@NonNull Context context) { - File directory = context.getDir(DIRECTORY, Context.MODE_PRIVATE); - File[] allFiles = directory.listFiles(pathname -> pathname.getName().contains(FILENAME_BASE)); - - if (allFiles != null) { - return Stream.of(allFiles) - .map(File::getName) - .map(PartAuthority::getWallpaperUri) - .map(ChatWallpaperFactory::create) - .toList(); - } else { - return Collections.emptyList(); - } + return FileStorage.getAll(context, DIRECTORY, FILENAME_BASE) + .stream() + .map(PartAuthority::getWallpaperUri) + .map(ChatWallpaperFactory::create) + .collect(Collectors.toList()); } /** @@ -97,14 +77,4 @@ public final class WallpaperStorage { Log.w(TAG, "Failed to delete " + filename + "!"); } } - - private static @NonNull OutputStream getOutputStream(@NonNull Context context, File outputFile) throws IOException { - AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(); - return ModernEncryptingPartOutputStream.createFor(attachmentSecret, outputFile, true).second; - } - - private static @NonNull InputStream getInputStream(@NonNull Context context, File inputFile) throws IOException { - AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(); - return ModernDecryptingPartInputStream.createFor(attachmentSecret, inputFile, 0); - } } diff --git a/app/src/main/proto/Database.proto b/app/src/main/proto/Database.proto index 872205234a..9935e5feaf 100644 --- a/app/src/main/proto/Database.proto +++ b/app/src/main/proto/Database.proto @@ -149,4 +149,28 @@ message ChatColor { message RecipientExtras { bool manuallyShownAvatar = 1; +} + +message CustomAvatar { + + message Text { + string text = 1; + string colors = 2; + } + + message Vector { + string key = 1; + string colors = 2; + } + + message Photo { + string uri = 1; + int64 size = 2; + } + + oneof avatar { + Text text = 1; + Vector vector = 2; + Photo photo = 3; + } } \ No newline at end of file diff --git a/app/src/main/res/anim/fragment_close_enter.xml b/app/src/main/res/anim/fragment_close_enter.xml new file mode 100644 index 0000000000..506fd30bbf --- /dev/null +++ b/app/src/main/res/anim/fragment_close_enter.xml @@ -0,0 +1,40 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/fragment_close_exit.xml b/app/src/main/res/anim/fragment_close_exit.xml new file mode 100644 index 0000000000..be1dd7e69e --- /dev/null +++ b/app/src/main/res/anim/fragment_close_exit.xml @@ -0,0 +1,41 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/fragment_open_enter.xml b/app/src/main/res/anim/fragment_open_enter.xml new file mode 100644 index 0000000000..41dfbead9a --- /dev/null +++ b/app/src/main/res/anim/fragment_open_enter.xml @@ -0,0 +1,42 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/fragment_open_exit.xml b/app/src/main/res/anim/fragment_open_exit.xml new file mode 100644 index 0000000000..9b97832876 --- /dev/null +++ b/app/src/main/res/anim/fragment_open_exit.xml @@ -0,0 +1,41 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/avatar_color_item_ring.xml b/app/src/main/res/drawable/avatar_color_item_ring.xml new file mode 100644 index 0000000000..247fdee1b9 --- /dev/null +++ b/app/src/main/res/drawable/avatar_color_item_ring.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/avatar_picker_item_ring.xml b/app/src/main/res/drawable/avatar_picker_item_ring.xml new file mode 100644 index 0000000000..5a96888a8f --- /dev/null +++ b/app/src/main/res/drawable/avatar_picker_item_ring.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/circle_transparent_black_20.xml b/app/src/main/res/drawable/circle_transparent_black_20.xml new file mode 100644 index 0000000000..349c89fa77 --- /dev/null +++ b/app/src/main/res/drawable/circle_transparent_black_20.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/ic_avatar_abstract_01.xml b/app/src/main/res/drawable/ic_avatar_abstract_01.xml new file mode 100644 index 0000000000..fd93b0eb01 --- /dev/null +++ b/app/src/main/res/drawable/ic_avatar_abstract_01.xml @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_avatar_abstract_02.xml b/app/src/main/res/drawable/ic_avatar_abstract_02.xml new file mode 100644 index 0000000000..76f694dc9e --- /dev/null +++ b/app/src/main/res/drawable/ic_avatar_abstract_02.xml @@ -0,0 +1,23 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_avatar_abstract_03.xml b/app/src/main/res/drawable/ic_avatar_abstract_03.xml new file mode 100644 index 0000000000..242d068270 --- /dev/null +++ b/app/src/main/res/drawable/ic_avatar_abstract_03.xml @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_avatar_balloon.xml b/app/src/main/res/drawable/ic_avatar_balloon.xml new file mode 100644 index 0000000000..411e06f968 --- /dev/null +++ b/app/src/main/res/drawable/ic_avatar_balloon.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_avatar_book.xml b/app/src/main/res/drawable/ic_avatar_book.xml new file mode 100644 index 0000000000..b7ed2f631e --- /dev/null +++ b/app/src/main/res/drawable/ic_avatar_book.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_avatar_briefcase.xml b/app/src/main/res/drawable/ic_avatar_briefcase.xml new file mode 100644 index 0000000000..f200ab7e46 --- /dev/null +++ b/app/src/main/res/drawable/ic_avatar_briefcase.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_avatar_cat.xml b/app/src/main/res/drawable/ic_avatar_cat.xml new file mode 100644 index 0000000000..0ea1cdc4a1 --- /dev/null +++ b/app/src/main/res/drawable/ic_avatar_cat.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_avatar_celebration.xml b/app/src/main/res/drawable/ic_avatar_celebration.xml new file mode 100644 index 0000000000..83a5d25b3a --- /dev/null +++ b/app/src/main/res/drawable/ic_avatar_celebration.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_avatar_dinosour.xml b/app/src/main/res/drawable/ic_avatar_dinosour.xml new file mode 100644 index 0000000000..d4fd5f61a0 --- /dev/null +++ b/app/src/main/res/drawable/ic_avatar_dinosour.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_avatar_dog.xml b/app/src/main/res/drawable/ic_avatar_dog.xml new file mode 100644 index 0000000000..b195458272 --- /dev/null +++ b/app/src/main/res/drawable/ic_avatar_dog.xml @@ -0,0 +1,31 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_avatar_drink.xml b/app/src/main/res/drawable/ic_avatar_drink.xml new file mode 100644 index 0000000000..8c14af115b --- /dev/null +++ b/app/src/main/res/drawable/ic_avatar_drink.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_avatar_football.xml b/app/src/main/res/drawable/ic_avatar_football.xml new file mode 100644 index 0000000000..dc9b5c1d3a --- /dev/null +++ b/app/src/main/res/drawable/ic_avatar_football.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_avatar_fox.xml b/app/src/main/res/drawable/ic_avatar_fox.xml new file mode 100644 index 0000000000..f7ebbdd0d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_avatar_fox.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_avatar_ghost.xml b/app/src/main/res/drawable/ic_avatar_ghost.xml new file mode 100644 index 0000000000..9b7eb4cdf2 --- /dev/null +++ b/app/src/main/res/drawable/ic_avatar_ghost.xml @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_avatar_heart.xml b/app/src/main/res/drawable/ic_avatar_heart.xml new file mode 100644 index 0000000000..25d53f9bf7 --- /dev/null +++ b/app/src/main/res/drawable/ic_avatar_heart.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_avatar_house.xml b/app/src/main/res/drawable/ic_avatar_house.xml new file mode 100644 index 0000000000..2258df6e1b --- /dev/null +++ b/app/src/main/res/drawable/ic_avatar_house.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_avatar_incognito.xml b/app/src/main/res/drawable/ic_avatar_incognito.xml new file mode 100644 index 0000000000..89b38f3762 --- /dev/null +++ b/app/src/main/res/drawable/ic_avatar_incognito.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_avatar_melon.xml b/app/src/main/res/drawable/ic_avatar_melon.xml new file mode 100644 index 0000000000..1a6bd82773 --- /dev/null +++ b/app/src/main/res/drawable/ic_avatar_melon.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_avatar_pig.xml b/app/src/main/res/drawable/ic_avatar_pig.xml new file mode 100644 index 0000000000..1c92f101b8 --- /dev/null +++ b/app/src/main/res/drawable/ic_avatar_pig.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_avatar_sloth.xml b/app/src/main/res/drawable/ic_avatar_sloth.xml new file mode 100644 index 0000000000..10758e0865 --- /dev/null +++ b/app/src/main/res/drawable/ic_avatar_sloth.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_avatar_soccerball.xml b/app/src/main/res/drawable/ic_avatar_soccerball.xml new file mode 100644 index 0000000000..d949519d93 --- /dev/null +++ b/app/src/main/res/drawable/ic_avatar_soccerball.xml @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_avatar_sunset.xml b/app/src/main/res/drawable/ic_avatar_sunset.xml new file mode 100644 index 0000000000..5d4632a580 --- /dev/null +++ b/app/src/main/res/drawable/ic_avatar_sunset.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_avatar_surfboard.xml b/app/src/main/res/drawable/ic_avatar_surfboard.xml new file mode 100644 index 0000000000..e1519dc5c2 --- /dev/null +++ b/app/src/main/res/drawable/ic_avatar_surfboard.xml @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_avatar_tucan.xml b/app/src/main/res/drawable/ic_avatar_tucan.xml new file mode 100644 index 0000000000..fcd05bcf4b --- /dev/null +++ b/app/src/main/res/drawable/ic_avatar_tucan.xml @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_text_24.xml b/app/src/main/res/drawable/ic_text_24.xml new file mode 100644 index 0000000000..04c2fe2653 --- /dev/null +++ b/app/src/main/res/drawable/ic_text_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/add_group_details_activity.xml b/app/src/main/res/layout/add_group_details_activity.xml index b193a4772e..c2fe2cfdde 100644 --- a/app/src/main/res/layout/add_group_details_activity.xml +++ b/app/src/main/res/layout/add_group_details_activity.xml @@ -1,9 +1,5 @@ - \ No newline at end of file + android:layout_height="match_parent" /> \ No newline at end of file diff --git a/app/src/main/res/layout/avatar_color_item.xml b/app/src/main/res/layout/avatar_color_item.xml new file mode 100644 index 0000000000..50498ae2f4 --- /dev/null +++ b/app/src/main/res/layout/avatar_color_item.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/avatar_photo_editor_fragment.xml b/app/src/main/res/layout/avatar_photo_editor_fragment.xml new file mode 100644 index 0000000000..90c9e68422 --- /dev/null +++ b/app/src/main/res/layout/avatar_photo_editor_fragment.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/avatar_picker_fragment.xml b/app/src/main/res/layout/avatar_picker_fragment.xml new file mode 100644 index 0000000000..eed83a10ca --- /dev/null +++ b/app/src/main/res/layout/avatar_picker_fragment.xml @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/avatar_picker_item.xml b/app/src/main/res/layout/avatar_picker_item.xml new file mode 100644 index 0000000000..2c220a635a --- /dev/null +++ b/app/src/main/res/layout/avatar_picker_item.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/media_overview_tab_item.xml b/app/src/main/res/layout/bold_selection_tab_item.xml similarity index 84% rename from app/src/main/res/layout/media_overview_tab_item.xml rename to app/src/main/res/layout/bold_selection_tab_item.xml index 82725dedb3..79ae9fe5f7 100644 --- a/app/src/main/res/layout/media_overview_tab_item.xml +++ b/app/src/main/res/layout/bold_selection_tab_item.xml @@ -1,5 +1,5 @@ - @@ -27,4 +27,4 @@ android:textColor="@color/signal_text_primary" tools:text="Media" /> - + diff --git a/app/src/main/res/layout/button_strip_item_view.xml b/app/src/main/res/layout/button_strip_item_view.xml new file mode 100644 index 0000000000..cf866bb21e --- /dev/null +++ b/app/src/main/res/layout/button_strip_item_view.xml @@ -0,0 +1,33 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/profile_create_activity.xml b/app/src/main/res/layout/profile_create_activity.xml index ac5b7deded..d56c74e977 100644 --- a/app/src/main/res/layout/profile_create_activity.xml +++ b/app/src/main/res/layout/profile_create_activity.xml @@ -1,21 +1,7 @@ - - - - - + tools:context=".profiles.edit.EditProfileActivity" /> \ No newline at end of file diff --git a/app/src/main/res/layout/profile_create_fragment.xml b/app/src/main/res/layout/profile_create_fragment.xml index 3777d5c858..e10967cbc7 100644 --- a/app/src/main/res/layout/profile_create_fragment.xml +++ b/app/src/main/res/layout/profile_create_fragment.xml @@ -57,7 +57,6 @@ android:layout_height="96dp" android:layout_marginTop="16dp" android:src="@drawable/circle_tintable" - android:tint="@color/core_grey_05" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/title" @@ -69,7 +68,6 @@ android:layout_height="0dp" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" - android:tint="@color/core_grey_75" android:transitionName="avatar" app:layout_constraintBottom_toBottomOf="@+id/avatar_background" app:layout_constraintEnd_toEndOf="@+id/avatar_background" diff --git a/app/src/main/res/layout/text_avatar_creation_fragment.xml b/app/src/main/res/layout/text_avatar_creation_fragment.xml new file mode 100644 index 0000000000..abe62f9033 --- /dev/null +++ b/app/src/main/res/layout/text_avatar_creation_fragment.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/text_avatar_creation_fragment_hidden_recycler.xml b/app/src/main/res/layout/text_avatar_creation_fragment_hidden_recycler.xml new file mode 100644 index 0000000000..1ce67a29d5 --- /dev/null +++ b/app/src/main/res/layout/text_avatar_creation_fragment_hidden_recycler.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/vector_avatar_creation_fragment.xml b/app/src/main/res/layout/vector_avatar_creation_fragment.xml new file mode 100644 index 0000000000..cdf5d9374f --- /dev/null +++ b/app/src/main/res/layout/vector_avatar_creation_fragment.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/avatar_picker_context.xml b/app/src/main/res/menu/avatar_picker_context.xml new file mode 100644 index 0000000000..ab89f8d99f --- /dev/null +++ b/app/src/main/res/menu/avatar_picker_context.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/avatar_picker.xml b/app/src/main/res/navigation/avatar_picker.xml new file mode 100644 index 0000000000..e73095ec9f --- /dev/null +++ b/app/src/main/res/navigation/avatar_picker.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/create_group.xml b/app/src/main/res/navigation/create_group.xml index 38ad375bea..f964866eee 100644 --- a/app/src/main/res/navigation/create_group.xml +++ b/app/src/main/res/navigation/create_group.xml @@ -16,6 +16,32 @@ app:argType="org.thoughtcrime.securesms.recipients.RecipientId[]" app:nullable="false" /> + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/edit_profile.xml b/app/src/main/res/navigation/edit_profile.xml index a37ec5fb93..29ae9d4fea 100644 --- a/app/src/main/res/navigation/edit_profile.xml +++ b/app/src/main/res/navigation/edit_profile.xml @@ -19,6 +19,24 @@ app:popEnterAnim="@anim/nav_default_pop_enter_anim" app:popExitAnim="@anim/nav_default_pop_exit_anim" /> + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/manage_profile.xml b/app/src/main/res/navigation/manage_profile.xml index 98178db33c..f3e0088bff 100644 --- a/app/src/main/res/navigation/manage_profile.xml +++ b/app/src/main/res/navigation/manage_profile.xml @@ -35,6 +35,26 @@ app:popEnterAnim="@anim/slide_from_start" app:popExitAnim="@anim/slide_to_end" /> + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-sw360dp/dimens.xml b/app/src/main/res/values-sw360dp/dimens.xml index 739ce85332..c3dc4c6a24 100644 --- a/app/src/main/res/values-sw360dp/dimens.xml +++ b/app/src/main/res/values-sw360dp/dimens.xml @@ -27,4 +27,6 @@ 32dp 56dp 16dp + + 160dp \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 2f8fa6f825..94a8eee3d9 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -310,4 +310,10 @@ + + + + + + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 64d668661d..081ef0dafd 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -191,4 +191,6 @@ 16dp 48dp 12dp + + 100dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4c3b0e0c02..c38ed5118f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3620,10 +3620,25 @@ %1$s · %2$s + Avatar preview + Camera + Take a picture + Choose a photo + Photo + Text + Save + Select an avatar + Preview + Done + Text + Color SMS · %1$s + Clear avatar + Edit + Failed to save avatar diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 7c39f7b5f5..4dbd43a764 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -399,6 +399,11 @@ 12dp + +