From 8d0acb277c1774fc24a3f09d4a675e3de5b89eda Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Tue, 28 Sep 2021 16:55:07 -0300 Subject: [PATCH] Add support for updated server badge image url formats. --- app/build.gradle | 2 + .../securesms/badges/BadgeImageView.kt | 41 ++---- .../thoughtcrime/securesms/badges/Badges.kt | 36 +---- .../badges/glide/BadgeSpriteTransformation.kt | 106 ++++++++++++++ .../securesms/badges/models/Badge.kt | 74 +++++----- .../badges/models/FeaturedBadgePreview.kt | 41 +----- .../securesms/badges/models/LargeBadge.kt | 9 +- .../featured/SelectFeaturedBadgeFragment.kt | 2 +- .../self/overview/BadgesOverviewFragment.kt | 2 +- .../ConversationSettingsFragment.kt | 2 +- .../securesms/database/RecipientDatabase.java | 6 +- .../securesms/glide/BadgeLoader.java | 8 +- .../securesms/jobs/RefreshOwnProfileJob.java | 7 +- .../securesms/jobs/RetrieveProfileJob.java | 33 ++++- .../securesms/util/ScreenDensity.java | 12 ++ app/src/main/proto/Database.proto | 15 +- .../main/res/layout/bio_preference_item.xml | 11 +- .../layout/contact_selection_list_item.xml | 11 +- .../res/layout/conversation_banner_view.xml | 11 +- .../layout/conversation_list_item_view.xml | 11 +- ...sation_settings_avatar_preference_item.xml | 11 +- .../res/layout/conversation_title_view.xml | 11 +- .../featured_badge_preview_preference.xml | 7 +- .../res/layout/recipient_bottom_sheet.xml | 11 +- ...adge_bottom_sheet_dialog_fragment_page.xml | 4 +- app/src/main/res/values-sw360dp/dimens.xml | 2 + app/src/main/res/values-sw480dp/dimens.xml | 2 + app/src/main/res/values/attrs.xml | 8 +- app/src/main/res/values/dimens.xml | 3 +- .../BadgeSpriteTransformationTest__mdpi.kt | 134 ++++++++++++++++++ .../BadgeSpriteTransformationTest__xxxhdpi.kt | 134 ++++++++++++++++++ .../api/profiles/SignalServiceProfile.java | 49 ++++++- 32 files changed, 602 insertions(+), 214 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/badges/glide/BadgeSpriteTransformation.kt create mode 100644 app/src/test/java/org/thoughtcrime/securesms/badges/glide/BadgeSpriteTransformationTest__mdpi.kt create mode 100644 app/src/test/java/org/thoughtcrime/securesms/badges/glide/BadgeSpriteTransformationTest__xxxhdpi.kt diff --git a/app/build.gradle b/app/build.gradle index 941632920..2b03596fe 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,6 +9,7 @@ apply from: 'translations.gradle' apply from: 'witness-verifications.gradle' apply plugin: 'org.jetbrains.kotlin.android' apply plugin: 'app.cash.exhaustive' +apply plugin: 'kotlin-parcelize' repositories { maven { @@ -177,6 +178,7 @@ android { buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"unset\"" buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"unset\"" buildConfigField "String", "BUILD_VARIANT_TYPE", "\"unset\"" + buildConfigField "String", "BADGE_STATIC_ROOT", "\"https://updates2.signal.org/static/badges/\"" ndk { abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/BadgeImageView.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/BadgeImageView.kt index 137efc711..d0bfd9038 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/BadgeImageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/BadgeImageView.kt @@ -1,20 +1,18 @@ package org.thoughtcrime.securesms.badges import android.content.Context -import android.graphics.Color -import android.graphics.drawable.Drawable import android.util.AttributeSet -import androidx.annotation.ColorInt -import androidx.annotation.Px import androidx.appcompat.widget.AppCompatImageView import androidx.core.content.res.use import androidx.lifecycle.Lifecycle +import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.badges.Badges.insetWithOutline +import org.thoughtcrime.securesms.badges.glide.BadgeSpriteTransformation import org.thoughtcrime.securesms.badges.models.Badge import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.util.ThemeUtil import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.visible @@ -25,16 +23,11 @@ class BadgeImageView @JvmOverloads constructor( attrs: AttributeSet? = null ) : AppCompatImageView(context, attrs) { - @Px - private var outlineWidth: Float = 0f - - @ColorInt - private var outlineColor: Int = Color.BLACK + private var badgeSize: Int = 0 init { context.obtainStyledAttributes(attrs, R.styleable.BadgeImageView).use { - outlineWidth = it.getDimension(R.styleable.BadgeImageView_badge_outline_width, 0f) - outlineColor = it.getColor(R.styleable.BadgeImageView_badge_outline_color, Color.BLACK) + badgeSize = it.getInt(R.styleable.BadgeImageView_badge_size, 0) } } @@ -55,21 +48,17 @@ class BadgeImageView @JvmOverloads constructor( return } - GlideApp - .with(this) - .load(badge) - .into(this) - } - - override fun setImageDrawable(drawable: Drawable?) { - if (drawable == null || outlineWidth == 0f) { - super.setImageDrawable(drawable) + if (badge != null) { + GlideApp + .with(this) + .load(badge) + .downsample(DownsampleStrategy.NONE) + .transform(BadgeSpriteTransformation(BadgeSpriteTransformation.Size.fromInteger(badgeSize), badge.imageDensity, ThemeUtil.isDarkTheme(context))) + .into(this) } else { - super.setImageDrawable( - drawable.insetWithOutline( - outlineWidth, outlineColor - ) - ) + GlideApp + .with(this) + .clear(this) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/Badges.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/Badges.kt index 02c9fac12..da3b5c4d7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/Badges.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/Badges.kt @@ -11,46 +11,23 @@ import com.google.android.flexbox.AlignItems import com.google.android.flexbox.FlexDirection import com.google.android.flexbox.FlexboxLayoutManager import com.google.android.flexbox.JustifyContent +import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.badges.models.Badge import org.thoughtcrime.securesms.badges.models.BadgeAnimator import org.thoughtcrime.securesms.components.settings.DSLConfiguration import org.thoughtcrime.securesms.util.customizeOnDraw object Badges { - fun Drawable.insetWithOutline( - @Px outlineWidth: Float, - @ColorInt outlineColor: Int - ): Drawable { - val clone = mutate().constantState?.newDrawable()?.mutate() - clone?.colorFilter = SimpleColorFilter(outlineColor) - - return customizeOnDraw { wrapped, canvas -> - clone?.bounds = wrapped.bounds - clone?.draw(canvas) - - val scale = 1 - ((outlineWidth * 2) / canvas.width) - - canvas.withScale(x = scale, y = scale, canvas.width / 2f, canvas.height / 2f) { - wrapped.draw(canvas) - } - } - } - fun Drawable.selectable( @Px outlineWidth: Float, @ColorInt outlineColor: Int, - @ColorInt gapColor: Int, animator: BadgeAnimator ): Drawable { val outline = mutate().constantState?.newDrawable()?.mutate() outline?.colorFilter = SimpleColorFilter(outlineColor) - val gap = mutate().constantState?.newDrawable()?.mutate() - gap?.colorFilter = SimpleColorFilter(gapColor) - return customizeOnDraw { wrapped, canvas -> outline?.bounds = wrapped.bounds - gap?.bounds = wrapped.bounds outline?.draw(canvas) @@ -58,11 +35,7 @@ object Badges { val interpolatedScale = scale + (1f - scale) * animator.getFraction() canvas.withScale(x = interpolatedScale, y = interpolatedScale, wrapped.bounds.width() / 2f, wrapped.bounds.height() / 2f) { - gap?.draw(canvas) - - canvas.withScale(x = interpolatedScale, y = interpolatedScale, wrapped.bounds.width() / 2f, wrapped.bounds.height() / 2f) { - wrapped.draw(canvas) - } + wrapped.draw(canvas) } if (animator.shouldInvalidate()) { @@ -71,12 +44,13 @@ object Badges { } } - fun DSLConfiguration.displayBadges(badges: List, selectedBadge: Badge? = null) { + fun DSLConfiguration.displayBadges(context: Context, badges: List, selectedBadge: Badge? = null) { badges .map { Badge.Model(it, it == selectedBadge) } .forEach { customPref(it) } - val empties = (4 - (badges.size % 4)) % 4 + val perRow = context.resources.getInteger(R.integer.badge_columns) + val empties = (perRow - (badges.size % perRow)) % perRow repeat(empties) { customPref(Badge.EmptyModel()) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/glide/BadgeSpriteTransformation.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/glide/BadgeSpriteTransformation.kt new file mode 100644 index 000000000..dda7aa503 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/glide/BadgeSpriteTransformation.kt @@ -0,0 +1,106 @@ +package org.thoughtcrime.securesms.badges.glide + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Rect +import androidx.annotation.VisibleForTesting +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool +import com.bumptech.glide.load.resource.bitmap.BitmapTransformation +import java.lang.IllegalArgumentException +import java.security.MessageDigest + +/** + * Cuts out the badge of the requested size from the sprite sheet. + */ +class BadgeSpriteTransformation( + private val size: Size, + private val density: String, + private val isDarkTheme: Boolean +) : BitmapTransformation() { + + override fun updateDiskCacheKey(messageDigest: MessageDigest) { + messageDigest.update("BadgeSpriteTransformation(${size.code},$density,$isDarkTheme)".toByteArray(CHARSET)) + } + + override fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int): Bitmap { + val outBitmap = pool.get(outWidth, outHeight, Bitmap.Config.ARGB_8888) + val canvas = Canvas(outBitmap) + val inBounds = getInBounds(density, size, isDarkTheme) + val outBounds = Rect(0, 0, outWidth, outHeight) + + canvas.drawBitmap(toTransform, inBounds, outBounds, null) + + return outBitmap + } + + enum class Size(val code: String) { + SMALL("small"), + MEDIUM("medium"), + LARGE("large"), + XLARGE("xlarge"); + + companion object { + fun fromInteger(integer: Int): Size { + return when (integer) { + 0 -> SMALL + 1 -> MEDIUM + 2 -> LARGE + 3 -> XLARGE + else -> LARGE + } + } + } + } + + companion object { + private const val PADDING = 1 + + @VisibleForTesting + fun getInBounds(density: String, size: Size, isDarkTheme: Boolean): Rect { + val scaleFactor: Int = when (density) { + "ldpi" -> 75 + "mdpi" -> 100 + "hdpi" -> 150 + "xhdpi" -> 200 + "xxhdpi" -> 300 + "xxxhdpi" -> 400 + else -> throw IllegalArgumentException("Unexpected density $density") + } + + val smallLength = 8 * scaleFactor / 100 + val mediumLength = 12 * scaleFactor / 100 + val largeLength = 18 * scaleFactor / 100 + val xlargeLength = 80 * scaleFactor / 100 + + val sideLength: Int = when (size) { + Size.SMALL -> smallLength + Size.MEDIUM -> mediumLength + Size.LARGE -> largeLength + Size.XLARGE -> xlargeLength + } + + val lightOffset: Int = when (size) { + Size.LARGE -> PADDING + Size.MEDIUM -> (largeLength + PADDING * 2) * 2 + PADDING + Size.SMALL -> (largeLength + PADDING * 2) * 2 + (mediumLength + PADDING * 2) * 2 + PADDING + Size.XLARGE -> (largeLength + PADDING * 2) * 2 + (mediumLength + PADDING * 2) * 2 + (smallLength + PADDING * 2) * 2 + PADDING + } + + val darkOffset = if (isDarkTheme) { + when (size) { + Size.XLARGE -> 0 + else -> sideLength + PADDING * 2 + } + } else { + 0 + } + + return Rect( + lightOffset + darkOffset, + PADDING, + lightOffset + darkOffset + sideLength, + sideLength + PADDING + ) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/models/Badge.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/models/Badge.kt index 1a8f1e45e..3c9b53369 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/models/Badge.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/models/Badge.kt @@ -2,22 +2,26 @@ package org.thoughtcrime.securesms.badges.models import android.graphics.drawable.Drawable import android.net.Uri -import android.os.Parcel import android.os.Parcelable import android.view.View import android.widget.ImageView import android.widget.TextView import androidx.core.content.ContextCompat import com.bumptech.glide.load.Key +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy import com.bumptech.glide.request.target.CustomViewTarget import com.bumptech.glide.request.transition.Transition +import kotlinx.parcelize.Parcelize import org.signal.core.util.DimensionUnit import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.badges.Badges.selectable +import org.thoughtcrime.securesms.badges.glide.BadgeSpriteTransformation import org.thoughtcrime.securesms.components.settings.PreferenceModel import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.util.MappingAdapter import org.thoughtcrime.securesms.util.MappingViewHolder +import org.thoughtcrime.securesms.util.ThemeUtil import java.security.MessageDigest typealias OnBadgeClicked = (Badge, Boolean) -> Unit @@ -25,42 +29,22 @@ typealias OnBadgeClicked = (Badge, Boolean) -> Unit /** * A Badge that can be collected and displayed by a user. */ +@Parcelize data class Badge( val id: String, val category: Category, - val imageUrl: Uri, val name: String, val description: String, + val imageUrl: Uri, + val imageDensity: String, val expirationTimestamp: Long, - val visible: Boolean + val visible: Boolean, ) : Parcelable, Key { - constructor(parcel: Parcel) : this( - requireNotNull(parcel.readString()), - Category.fromCode(requireNotNull(parcel.readString())), - requireNotNull(parcel.readParcelable(Uri::class.java.classLoader)), - requireNotNull(parcel.readString()), - requireNotNull(parcel.readString()), - parcel.readLong(), - parcel.readByte() == 1.toByte() - ) - - override fun describeContents(): Int { - return 0 - } - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeString(id) - parcel.writeString(category.code) - parcel.writeParcelable(imageUrl, flags) - parcel.writeString(name) - parcel.writeString(description) - parcel.writeLong(expirationTimestamp) - parcel.writeByte(if (visible) 1 else 0) - } - override fun updateDiskCacheKey(messageDigest: MessageDigest) { messageDigest.update(id.toByteArray(Key.CHARSET)) + messageDigest.update(imageUrl.toString().toByteArray(Key.CHARSET)) + messageDigest.update(imageDensity.toByteArray(Key.CHARSET)) } fun resolveDescription(shortName: String): String { @@ -130,6 +114,9 @@ data class Badge( GlideApp.with(badge) .load(model.badge) + .downsample(DownsampleStrategy.NONE) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .transform(BadgeSpriteTransformation(BadgeSpriteTransformation.Size.XLARGE, model.badge.imageDensity, ThemeUtil.isDarkTheme(context))) .into(target) if (model.isSelected) { @@ -170,7 +157,6 @@ data class Badge( val drawable = resource.selectable( DimensionUnit.DP.toPixels(2.5f), ContextCompat.getColor(view.context, R.color.signal_inverse_primary), - ContextCompat.getColor(view.context, R.color.signal_background_primary), animator ) @@ -202,20 +188,34 @@ data class Badge( } } - companion object CREATOR : Parcelable.Creator { + companion object { private val SELECTION_CHANGED = Any() - override fun createFromParcel(parcel: Parcel): Badge { - return Badge(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - fun register(mappingAdapter: MappingAdapter, onBadgeClicked: OnBadgeClicked) { mappingAdapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it, onBadgeClicked) }, R.layout.badge_preference_view)) mappingAdapter.registerFactory(EmptyModel::class.java, MappingAdapter.LayoutFactory({ EmptyViewHolder(it) }, R.layout.badge_preference_view)) } } + + @Parcelize + data class ImageSet( + val ldpi: String, + val mdpi: String, + val hdpi: String, + val xhdpi: String, + val xxhdpi: String, + val xxxhdpi: String + ) : Parcelable { + fun getByDensity(density: String): String { + return when (density) { + "ldpi" -> ldpi + "mdpi" -> mdpi + "hdpi" -> hdpi + "xhdpi" -> xhdpi + "xxhdpi" -> xxhdpi + "xxxhdpi" -> xxxhdpi + else -> xhdpi + } + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/models/FeaturedBadgePreview.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/models/FeaturedBadgePreview.kt index 1c0f6bb7e..1af79b238 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/models/FeaturedBadgePreview.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/models/FeaturedBadgePreview.kt @@ -1,17 +1,10 @@ package org.thoughtcrime.securesms.badges.models -import android.graphics.drawable.Drawable import android.view.View -import android.widget.ImageView -import androidx.core.content.ContextCompat -import com.bumptech.glide.request.target.CustomViewTarget -import com.bumptech.glide.request.transition.Transition -import org.signal.core.util.DimensionUnit import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.badges.Badges.insetWithOutline +import org.thoughtcrime.securesms.badges.BadgeImageView import org.thoughtcrime.securesms.components.AvatarImageView import org.thoughtcrime.securesms.components.settings.PreferenceModel -import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.util.MappingAdapter import org.thoughtcrime.securesms.util.MappingViewHolder @@ -35,40 +28,12 @@ object FeaturedBadgePreview { class ViewHolder(itemView: View) : MappingViewHolder(itemView) { private val avatar: AvatarImageView = itemView.findViewById(R.id.avatar) - private val badge: ImageView = itemView.findViewById(R.id.badge) - private val target: Target = Target(badge) + private val badge: BadgeImageView = itemView.findViewById(R.id.badge) override fun bind(model: Model) { avatar.setRecipient(Recipient.self()) avatar.disableQuickContact() - - if (model.badge != null) { - GlideApp.with(badge) - .load(model.badge) - .into(target) - } else { - GlideApp.with(badge).clear(badge) - badge.setImageDrawable(null) - } - } - } - - private class Target(view: ImageView) : CustomViewTarget(view) { - override fun onLoadFailed(errorDrawable: Drawable?) { - view.setImageDrawable(errorDrawable) - } - - override fun onResourceReady(resource: Drawable, transition: Transition?) { - view.setImageDrawable( - resource.insetWithOutline( - DimensionUnit.DP.toPixels(2.5f), - ContextCompat.getColor(view.context, R.color.signal_background_primary) - ) - ) - } - - override fun onResourceCleared(placeholder: Drawable?) { - view.setImageDrawable(placeholder) + badge.setBadge(model.badge) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/models/LargeBadge.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/models/LargeBadge.kt index c79f6caaf..a7cb1eab4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/models/LargeBadge.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/models/LargeBadge.kt @@ -1,10 +1,9 @@ package org.thoughtcrime.securesms.badges.models import android.view.View -import android.widget.ImageView import android.widget.TextView import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.mms.GlideApp +import org.thoughtcrime.securesms.badges.BadgeImageView import org.thoughtcrime.securesms.util.MappingAdapter import org.thoughtcrime.securesms.util.MappingModel import org.thoughtcrime.securesms.util.MappingViewHolder @@ -35,14 +34,12 @@ data class LargeBadge( class ViewHolder(itemView: View) : MappingViewHolder(itemView) { - private val badge: ImageView = itemView.findViewById(R.id.badge) + private val badge: BadgeImageView = itemView.findViewById(R.id.badge) private val name: TextView = itemView.findViewById(R.id.name) private val description: TextView = itemView.findViewById(R.id.description) override fun bind(model: Model) { - GlideApp.with(badge) - .load(model.largeBadge.badge) - .into(badge) + badge.setBadge(model.largeBadge.badge) name.text = model.largeBadge.badge.name description.text = model.largeBadge.badge.resolveDescription(model.shortName) diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/self/featured/SelectFeaturedBadgeFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/self/featured/SelectFeaturedBadgeFragment.kt index cd3cf0ff6..ae4c9e7c5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/self/featured/SelectFeaturedBadgeFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/self/featured/SelectFeaturedBadgeFragment.kt @@ -79,7 +79,7 @@ class SelectFeaturedBadgeFragment : DSLSettingsFragment( private fun getConfiguration(state: SelectFeaturedBadgeState): DSLConfiguration { return configure { sectionHeaderPref(R.string.SelectFeaturedBadgeFragment__select_a_badge) - displayBadges(state.allUnlockedBadges, state.selectedBadge) + displayBadges(requireContext(), state.allUnlockedBadges, state.selectedBadge) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/badges/self/overview/BadgesOverviewFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/badges/self/overview/BadgesOverviewFragment.kt index db4b812e2..ae9e18afc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/badges/self/overview/BadgesOverviewFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/badges/self/overview/BadgesOverviewFragment.kt @@ -52,7 +52,7 @@ class BadgesOverviewFragment : DSLSettingsFragment( return configure { sectionHeaderPref(R.string.BadgesOverviewFragment__my_badges) - displayBadges(state.allUnlockedBadges) + displayBadges(requireContext(), state.allUnlockedBadges) switchPref( title = DSLSettingsText.from(R.string.BadgesOverviewFragment__display_badges_on_profile), diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt index a44ad57fa..f2dbea95b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt @@ -489,7 +489,7 @@ class ConversationSettingsFragment : DSLSettingsFragment( sectionHeaderPref(R.string.ManageProfileFragment_badges) - displayBadges(state.recipient.badges) + displayBadges(requireContext(), state.recipient.badges) } if (recipientSettingsState.selfHasGroups) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java index de3056376..0dd80fe9e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -1373,9 +1373,10 @@ public class RecipientDatabase extends Database { badges.add(new Badge( protoBadge.getId(), Badge.Category.Companion.fromCode(protoBadge.getCategory()), - Uri.parse(protoBadge.getImageUrl()), protoBadge.getName(), protoBadge.getDescription(), + Uri.parse(protoBadge.getImageUrl()), + protoBadge.getImageDensity(), protoBadge.getExpiration(), protoBadge.getVisible() )); @@ -1691,7 +1692,8 @@ public class RecipientDatabase extends Database { .setExpiration(badge.getExpirationTimestamp()) .setVisible(badge.getVisible()) .setName(badge.getName()) - .setImageUrl(badge.getImageUrl().toString())); + .setImageUrl(badge.getImageUrl().toString()) + .setImageDensity(badge.getImageDensity())); } ContentValues values = new ContentValues(1); diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/BadgeLoader.java b/app/src/main/java/org/thoughtcrime/securesms/glide/BadgeLoader.java index 9b80ba6dc..7e1fced17 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/glide/BadgeLoader.java +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/BadgeLoader.java @@ -29,7 +29,7 @@ import okhttp3.ConnectionSpec; import okhttp3.OkHttpClient; /** - * A simple model loader for fetching media over http/https using OkHttp. + * A loader which will load a sprite sheet for a particular badge at the correct dpi for this device. */ public class BadgeLoader implements ModelLoader { @@ -40,12 +40,12 @@ public class BadgeLoader implements ModelLoader { } @Override - public @Nullable LoadData buildLoadData(@NonNull Badge badge, int width, int height, @NonNull Options options) { - return new LoadData<>(badge, new OkHttpStreamFetcher(client, new GlideUrl(badge.getImageUrl().toString()))); + public @Nullable LoadData buildLoadData(@NonNull Badge request, int width, int height, @NonNull Options options) { + return new LoadData<>(request, new OkHttpStreamFetcher(client, new GlideUrl(request.getImageUrl().toString()))); } @Override - public boolean handles(@NonNull Badge badge) { + public boolean handles(@NonNull Badge badgeSpriteSheetRequest) { return true; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java index 02d0a9793..08546783e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshOwnProfileJob.java @@ -9,6 +9,7 @@ import androidx.annotation.Nullable; import org.signal.core.util.logging.Log; import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKeyCredential; +import org.thoughtcrime.securesms.BuildConfig; import org.thoughtcrime.securesms.badges.models.Badge; import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; import org.thoughtcrime.securesms.database.DatabaseFactory; @@ -21,8 +22,10 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.profiles.ProfileName; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.ProfileUtil; +import org.thoughtcrime.securesms.util.ScreenDensity; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; +import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException; import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; @@ -177,12 +180,14 @@ public class RefreshOwnProfileJob extends BaseJob { } private static Badge adaptFromServiceBadge(@NonNull SignalServiceProfile.Badge serviceBadge) { + Pair uriAndDensity = RetrieveProfileJob.getBestBadgeImageUriForDevice(serviceBadge); return new Badge( serviceBadge.getId(), Badge.Category.Companion.fromCode(serviceBadge.getCategory()), - Uri.parse(serviceBadge.getImageUrl()), serviceBadge.getName(), serviceBadge.getDescription(), + uriAndDensity.first(), + uriAndDensity.second(), getTimestamp(serviceBadge.getExpiration()), serviceBadge.isVisible() ); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java index 54994e78c..b26b99bb5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java @@ -16,6 +16,7 @@ import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.Log; import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKeyCredential; +import org.thoughtcrime.securesms.BuildConfig; import org.thoughtcrime.securesms.badges.models.Badge; import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; import org.thoughtcrime.securesms.database.DatabaseFactory; @@ -34,9 +35,9 @@ import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.util.Base64; -import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.IdentityUtil; import org.thoughtcrime.securesms.util.ProfileUtil; +import org.thoughtcrime.securesms.util.ScreenDensity; import org.thoughtcrime.securesms.util.SetUtil; import org.thoughtcrime.securesms.util.Stopwatch; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -357,17 +358,45 @@ public class RetrieveProfileJob extends BaseJob { } private static Badge adaptFromServiceBadge(@NonNull SignalServiceProfile.Badge serviceBadge) { + Pair uriAndDensity = RetrieveProfileJob.getBestBadgeImageUriForDevice(serviceBadge); return new Badge( serviceBadge.getId(), Badge.Category.Companion.fromCode(serviceBadge.getCategory()), - Uri.parse(serviceBadge.getImageUrl()), serviceBadge.getName(), serviceBadge.getDescription(), + uriAndDensity.first(), + uriAndDensity.second(), 0L, true ); } + + public static @NonNull Pair getBestBadgeImageUriForDevice(@NonNull SignalServiceProfile.Badge serviceBadge) { + String bestDensity = ScreenDensity.getBestDensityBucketForDevice(); + + switch (bestDensity) { + case "ldpi": + return new Pair<>(getBadgeImageUri(serviceBadge.getLdpiUri()), "ldpi"); + case "mdpi": + return new Pair<>(getBadgeImageUri(serviceBadge.getMdpiUri()), "mdpi"); + case "hdpi": + return new Pair<>(getBadgeImageUri(serviceBadge.getHdpiUri()), "hdpi"); + case "xxhdpi": + return new Pair<>(getBadgeImageUri(serviceBadge.getXxhdpiUri()), "xxhdpi"); + case "xxxhdpi": + return new Pair<>(getBadgeImageUri(serviceBadge.getXxxhdpiUri()), "xxxhdpi"); + default: + return new Pair<>(getBadgeImageUri(serviceBadge.getXhdpiUri()), "xdpi"); + } + } + + private static @NonNull Uri getBadgeImageUri(@NonNull String densityPath) { + return Uri.parse(BuildConfig.BADGE_STATIC_ROOT).buildUpon() + .appendPath(densityPath) + .build(); + } + private void setProfileKeyCredential(@NonNull Recipient recipient, @NonNull ProfileKey recipientProfileKey, @NonNull ProfileKeyCredential credential) diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ScreenDensity.java b/app/src/main/java/org/thoughtcrime/securesms/util/ScreenDensity.java index 8e1a6d2bf..9cb223ed7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ScreenDensity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ScreenDensity.java @@ -5,6 +5,8 @@ import android.util.DisplayMetrics; import androidx.annotation.NonNull; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; + import java.util.LinkedHashMap; import java.util.Map; @@ -51,6 +53,16 @@ public final class ScreenDensity { return new ScreenDensity(bucket, density); } + public static @NonNull String getBestDensityBucketForDevice() { + ScreenDensity density = get(ApplicationDependencies.getApplication()); + + if (density.isKnownDensity()) { + return density.bucket; + } else { + return "xhdpi"; + } + } + public String getBucket() { return bucket; } diff --git a/app/src/main/proto/Database.proto b/app/src/main/proto/Database.proto index c50dbf33a..1dd1a0728 100644 --- a/app/src/main/proto/Database.proto +++ b/app/src/main/proto/Database.proto @@ -25,13 +25,14 @@ message ReactionList { message BadgeList { message Badge { - string id = 1; - string category = 2; - string name = 3; - string description = 4; - string imageUrl = 5; - uint64 expiration = 6; - bool visible = 7; + string id = 1; + string category = 2; + string name = 3; + string description = 4; + string imageUrl = 5; + uint64 expiration = 6; + bool visible = 7; + string imageDensity = 8; } repeated Badge badges = 1; diff --git a/app/src/main/res/layout/bio_preference_item.xml b/app/src/main/res/layout/bio_preference_item.xml index e9905b9fb..cd7ab7a98 100644 --- a/app/src/main/res/layout/bio_preference_item.xml +++ b/app/src/main/res/layout/bio_preference_item.xml @@ -21,14 +21,13 @@ diff --git a/app/src/main/res/layout/contact_selection_list_item.xml b/app/src/main/res/layout/contact_selection_list_item.xml index 63b5088d4..855608f59 100644 --- a/app/src/main/res/layout/contact_selection_list_item.xml +++ b/app/src/main/res/layout/contact_selection_list_item.xml @@ -25,14 +25,13 @@ diff --git a/app/src/main/res/layout/conversation_banner_view.xml b/app/src/main/res/layout/conversation_banner_view.xml index 732a14faf..bf71f24e2 100644 --- a/app/src/main/res/layout/conversation_banner_view.xml +++ b/app/src/main/res/layout/conversation_banner_view.xml @@ -18,14 +18,13 @@ diff --git a/app/src/main/res/layout/conversation_list_item_view.xml b/app/src/main/res/layout/conversation_list_item_view.xml index 332404393..07401c80f 100644 --- a/app/src/main/res/layout/conversation_list_item_view.xml +++ b/app/src/main/res/layout/conversation_list_item_view.xml @@ -191,14 +191,13 @@ diff --git a/app/src/main/res/layout/conversation_settings_avatar_preference_item.xml b/app/src/main/res/layout/conversation_settings_avatar_preference_item.xml index 90bcf11bc..407fa631b 100644 --- a/app/src/main/res/layout/conversation_settings_avatar_preference_item.xml +++ b/app/src/main/res/layout/conversation_settings_avatar_preference_item.xml @@ -23,14 +23,13 @@ diff --git a/app/src/main/res/layout/conversation_title_view.xml b/app/src/main/res/layout/conversation_title_view.xml index 76d4abbfc..fa7d63efb 100644 --- a/app/src/main/res/layout/conversation_title_view.xml +++ b/app/src/main/res/layout/conversation_title_view.xml @@ -31,16 +31,15 @@ diff --git a/app/src/main/res/layout/featured_badge_preview_preference.xml b/app/src/main/res/layout/featured_badge_preview_preference.xml index b3ec0e990..e805a4dff 100644 --- a/app/src/main/res/layout/featured_badge_preview_preference.xml +++ b/app/src/main/res/layout/featured_badge_preview_preference.xml @@ -22,11 +22,12 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> - diff --git a/app/src/main/res/layout/recipient_bottom_sheet.xml b/app/src/main/res/layout/recipient_bottom_sheet.xml index 2a103f8c1..319466d8b 100644 --- a/app/src/main/res/layout/recipient_bottom_sheet.xml +++ b/app/src/main/res/layout/recipient_bottom_sheet.xml @@ -23,14 +23,13 @@ diff --git a/app/src/main/res/layout/view_badge_bottom_sheet_dialog_fragment_page.xml b/app/src/main/res/layout/view_badge_bottom_sheet_dialog_fragment_page.xml index e38b0ab9f..920df09f1 100644 --- a/app/src/main/res/layout/view_badge_bottom_sheet_dialog_fragment_page.xml +++ b/app/src/main/res/layout/view_badge_bottom_sheet_dialog_fragment_page.xml @@ -3,14 +3,16 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + xmlns:app="http://schemas.android.com/apk/res-auto" android:orientation="vertical"> - 34dp 32dp + + 4 \ No newline at end of file diff --git a/app/src/main/res/values-sw480dp/dimens.xml b/app/src/main/res/values-sw480dp/dimens.xml index 80c9e9b46..5882581c1 100644 --- a/app/src/main/res/values-sw480dp/dimens.xml +++ b/app/src/main/res/values-sw480dp/dimens.xml @@ -2,4 +2,6 @@ 350dp 300dp + + 5 \ 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 9df5021d0..5c237453f 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -328,7 +328,11 @@ - - + + + + + + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 86fc5a0be..197da5d34 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -205,9 +205,8 @@ 28dp 26dp 48dp - 25dp 16dp - 18dp + 3 diff --git a/app/src/test/java/org/thoughtcrime/securesms/badges/glide/BadgeSpriteTransformationTest__mdpi.kt b/app/src/test/java/org/thoughtcrime/securesms/badges/glide/BadgeSpriteTransformationTest__mdpi.kt new file mode 100644 index 000000000..38cce0d7b --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/badges/glide/BadgeSpriteTransformationTest__mdpi.kt @@ -0,0 +1,134 @@ +package org.thoughtcrime.securesms.badges.glide + +import android.app.Application +import android.graphics.Rect +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@Suppress("ClassName") +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, application = Application::class) +class BadgeSpriteTransformationTest__mdpi { + + @Test + fun `Given request for large mdpi in light theme, when I getInBounds, then I expect 18x18@1,1`() { + // GIVEN + val density = "mdpi" + val size = BadgeSpriteTransformation.Size.LARGE + val isDarkTheme = false + + // WHEN + val inBounds = BadgeSpriteTransformation.getInBounds(density, size, isDarkTheme) + + // THEN + assertRectMatches(inBounds, 1, 1, 18, 18) + } + + @Test + fun `Given request for large mdpi in dark theme, when I getInBounds, then I expect 18x18@21,1`() { + // GIVEN + val density = "mdpi" + val size = BadgeSpriteTransformation.Size.LARGE + val isDarkTheme = true + + // WHEN + val inBounds = BadgeSpriteTransformation.getInBounds(density, size, isDarkTheme) + + // THEN + assertRectMatches(inBounds, 21, 1, 18, 18) + } + + @Test + fun `Given request for medium mdpi in light theme, when I getInBounds, then I expect 12x12@41,1`() { + // GIVEN + val density = "mdpi" + val size = BadgeSpriteTransformation.Size.MEDIUM + val isDarkTheme = false + + // WHEN + val inBounds = BadgeSpriteTransformation.getInBounds(density, size, isDarkTheme) + + // THEN + assertRectMatches(inBounds, 41, 1, 12, 12) + } + + @Test + fun `Given request for medium mdpi in dark theme, when I getInBounds, then I expect 12x12@55,1`() { + // GIVEN + val density = "mdpi" + val size = BadgeSpriteTransformation.Size.MEDIUM + val isDarkTheme = true + + // WHEN + val inBounds = BadgeSpriteTransformation.getInBounds(density, size, isDarkTheme) + + // THEN + assertRectMatches(inBounds, 55, 1, 12, 12) + } + + @Test + fun `Given request for small mdpi in light theme, when I getInBounds, then I expect 8x8@69,1`() { + // GIVEN + val density = "mdpi" + val size = BadgeSpriteTransformation.Size.SMALL + val isDarkTheme = false + + // WHEN + val inBounds = BadgeSpriteTransformation.getInBounds(density, size, isDarkTheme) + + // THEN + assertRectMatches(inBounds, 69, 1, 8, 8) + } + + @Test + fun `Given request for small mdpi in dark theme, when I getInBounds, then I expect 8x8@79,1`() { + // GIVEN + val density = "mdpi" + val size = BadgeSpriteTransformation.Size.SMALL + val isDarkTheme = true + + // WHEN + val inBounds = BadgeSpriteTransformation.getInBounds(density, size, isDarkTheme) + + // THEN + assertRectMatches(inBounds, 79, 1, 8, 8) + } + + @Test + fun `Given request for xlarge mdpi in light theme, when I getInBounds, then I expect 80x80@89,1`() { + // GIVEN + val density = "mdpi" + val size = BadgeSpriteTransformation.Size.XLARGE + val isDarkTheme = false + + // WHEN + val inBounds = BadgeSpriteTransformation.getInBounds(density, size, isDarkTheme) + + // THEN + assertRectMatches(inBounds, 89, 1, 80, 80) + } + + @Test + fun `Given request for xlarge mdpi in dark theme, when I getInBounds, then I expect 80x80@89,1`() { + // GIVEN + val density = "mdpi" + val size = BadgeSpriteTransformation.Size.XLARGE + val isDarkTheme = true + + // WHEN + val inBounds = BadgeSpriteTransformation.getInBounds(density, size, isDarkTheme) + + // THEN + assertRectMatches(inBounds, 89, 1, 80, 80) + } + + private fun assertRectMatches(rect: Rect, x: Int, y: Int, width: Int, height: Int) { + assertEquals("Rect has wrong x value", x, rect.left) + assertEquals("Rect has wrong y value", rect.top, y) + assertEquals("Rect has wrong width", width, rect.width()) + assertEquals("Rect has wrong height", height, rect.height()) + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/badges/glide/BadgeSpriteTransformationTest__xxxhdpi.kt b/app/src/test/java/org/thoughtcrime/securesms/badges/glide/BadgeSpriteTransformationTest__xxxhdpi.kt new file mode 100644 index 000000000..cfa991147 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/badges/glide/BadgeSpriteTransformationTest__xxxhdpi.kt @@ -0,0 +1,134 @@ +package org.thoughtcrime.securesms.badges.glide + +import android.app.Application +import android.graphics.Rect +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@Suppress("ClassName") +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, application = Application::class) +class BadgeSpriteTransformationTest__xxxhdpi { + + @Test + fun `Given request for large xxxhdpi in light theme, when I getInBounds, then I expect 72x72@1,1`() { + // GIVEN + val density = "xxxhdpi" + val size = BadgeSpriteTransformation.Size.LARGE + val isDarkTheme = false + + // WHEN + val inBounds = BadgeSpriteTransformation.getInBounds(density, size, isDarkTheme) + + // THEN + assertRectMatches(inBounds, 1, 1, 72, 72) + } + + @Test + fun `Given request for large xxxhdpi in dark theme, when I getInBounds, then I expect 72x72@21,1`() { + // GIVEN + val density = "xxxhdpi" + val size = BadgeSpriteTransformation.Size.LARGE + val isDarkTheme = true + + // WHEN + val inBounds = BadgeSpriteTransformation.getInBounds(density, size, isDarkTheme) + + // THEN + assertRectMatches(inBounds, 75, 1, 72, 72) + } + + @Test + fun `Given request for medium xxxhdpi in light theme, when I getInBounds, then I expect 48x48@149,1`() { + // GIVEN + val density = "xxxhdpi" + val size = BadgeSpriteTransformation.Size.MEDIUM + val isDarkTheme = false + + // WHEN + val inBounds = BadgeSpriteTransformation.getInBounds(density, size, isDarkTheme) + + // THEN + assertRectMatches(inBounds, 149, 1, 48, 48) + } + + @Test + fun `Given request for medium xxxhdpi in dark theme, when I getInBounds, then I expect 48x48@199,1`() { + // GIVEN + val density = "xxxhdpi" + val size = BadgeSpriteTransformation.Size.MEDIUM + val isDarkTheme = true + + // WHEN + val inBounds = BadgeSpriteTransformation.getInBounds(density, size, isDarkTheme) + + // THEN + assertRectMatches(inBounds, 199, 1, 48, 48) + } + + @Test + fun `Given request for small xxxhdpi in light theme, when I getInBounds, then I expect 32x32@249,1`() { + // GIVEN + val density = "xxxhdpi" + val size = BadgeSpriteTransformation.Size.SMALL + val isDarkTheme = false + + // WHEN + val inBounds = BadgeSpriteTransformation.getInBounds(density, size, isDarkTheme) + + // THEN + assertRectMatches(inBounds, 249, 1, 32, 32) + } + + @Test + fun `Given request for small xxxhdpi in dark theme, when I getInBounds, then I expect 32x32@283,1`() { + // GIVEN + val density = "xxxhdpi" + val size = BadgeSpriteTransformation.Size.SMALL + val isDarkTheme = true + + // WHEN + val inBounds = BadgeSpriteTransformation.getInBounds(density, size, isDarkTheme) + + // THEN + assertRectMatches(inBounds, 283, 1, 32, 32) + } + + @Test + fun `Given request for xlarge xxxhdpi in light theme, when I getInBounds, then I expect 320x320@317,1`() { + // GIVEN + val density = "xxxhdpi" + val size = BadgeSpriteTransformation.Size.XLARGE + val isDarkTheme = false + + // WHEN + val inBounds = BadgeSpriteTransformation.getInBounds(density, size, isDarkTheme) + + // THEN + assertRectMatches(inBounds, 317, 1, 320, 320) + } + + @Test + fun `Given request for xlarge xxxhdpi in dark theme, when I getInBounds, then I expect 320x320@317,1`() { + // GIVEN + val density = "xxxhdpi" + val size = BadgeSpriteTransformation.Size.XLARGE + val isDarkTheme = true + + // WHEN + val inBounds = BadgeSpriteTransformation.getInBounds(density, size, isDarkTheme) + + // THEN + assertRectMatches(inBounds, 317, 1, 320, 320) + } + + private fun assertRectMatches(rect: Rect, x: Int, y: Int, width: Int, height: Int) { + assertEquals("Rect has wrong x value", x, rect.left) + assertEquals("Rect has wrong y value", rect.top, y) + assertEquals("Rect has wrong width", width, rect.width()) + assertEquals("Rect has wrong height", height, rect.height()) + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java index adc312b8d..6ec6919b7 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/profiles/SignalServiceProfile.java @@ -128,15 +128,30 @@ public class SignalServiceProfile { @JsonProperty private String category; - @JsonProperty - private String imageUrl; - @JsonProperty private String name; @JsonProperty private String description; + @JsonProperty + private String ldpi; + + @JsonProperty + private String mdpi; + + @JsonProperty + private String hdpi; + + @JsonProperty + private String xhdpi; + + @JsonProperty + private String xxhdpi; + + @JsonProperty + private String xxxhdpi; + @JsonProperty private BigDecimal expiration; @@ -159,12 +174,32 @@ public class SignalServiceProfile { return description; } - public BigDecimal getExpiration() { - return expiration; + public String getLdpiUri() { + return ldpi; } - public String getImageUrl() { - return imageUrl; + public String getMdpiUri() { + return mdpi; + } + + public String getHdpiUri() { + return hdpi; + } + + public String getXhdpiUri() { + return xhdpi; + } + + public String getXxhdpiUri() { + return xxhdpi; + } + + public String getXxxhdpiUri() { + return xxxhdpi; + } + + public BigDecimal getExpiration() { + return expiration; } public boolean isVisible() {