Add support for updated server badge image url formats.

This commit is contained in:
Alex Hart 2021-09-28 16:55:07 -03:00 committed by Greyson Parrelli
parent 6e00920c95
commit 8d0acb277c
32 changed files with 602 additions and 214 deletions

View file

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

View file

@ -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
}
if (badge != null) {
GlideApp
.with(this)
.load(badge)
.downsample(DownsampleStrategy.NONE)
.transform(BadgeSpriteTransformation(BadgeSpriteTransformation.Size.fromInteger(badgeSize), badge.imageDensity, ThemeUtil.isDarkTheme(context)))
.into(this)
}
override fun setImageDrawable(drawable: Drawable?) {
if (drawable == null || outlineWidth == 0f) {
super.setImageDrawable(drawable)
} else {
super.setImageDrawable(
drawable.insetWithOutline(
outlineWidth, outlineColor
)
)
GlideApp
.with(this)
.clear(this)
}
}
}

View file

@ -11,59 +11,32 @@ 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)
val scale = 1 - ((outlineWidth * 2) / wrapped.bounds.width())
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)
}
}
if (animator.shouldInvalidate()) {
invalidateSelf()
@ -71,12 +44,13 @@ object Badges {
}
}
fun DSLConfiguration.displayBadges(badges: List<Badge>, selectedBadge: Badge? = null) {
fun DSLConfiguration.displayBadges(context: Context, badges: List<Badge>, 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())
}

View file

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

View file

@ -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<Badge> {
companion object {
private val SELECTION_CHANGED = Any()
override fun createFromParcel(parcel: Parcel): Badge {
return Badge(parcel)
}
override fun newArray(size: Int): Array<Badge?> {
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
}
}
}
}

View file

@ -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<Model>(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<ImageView, Drawable>(view) {
override fun onLoadFailed(errorDrawable: Drawable?) {
view.setImageDrawable(errorDrawable)
}
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
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)
}
}
}

View file

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

View file

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

View file

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

View file

@ -489,7 +489,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
sectionHeaderPref(R.string.ManageProfileFragment_badges)
displayBadges(state.recipient.badges)
displayBadges(requireContext(), state.recipient.badges)
}
if (recipientSettingsState.selfHasGroups) {

View file

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

View file

@ -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<Badge, InputStream> {
@ -40,12 +40,12 @@ public class BadgeLoader implements ModelLoader<Badge, InputStream> {
}
@Override
public @Nullable LoadData<InputStream> 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<InputStream> 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;
}

View file

@ -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<Uri, String> 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()
);

View file

@ -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<Uri, String> 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<Uri, String> 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)

View file

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

View file

@ -32,6 +32,7 @@ message BadgeList {
string imageUrl = 5;
uint64 expiration = 6;
bool visible = 7;
string imageDensity = 8;
}
repeated Badge badges = 1;

View file

@ -21,14 +21,13 @@
<org.thoughtcrime.securesms.badges.BadgeImageView
android:id="@+id/badge"
android:layout_width="26dp"
android:layout_height="26dp"
android:layout_marginStart="39dp"
android:layout_marginTop="39dp"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="40dp"
android:layout_marginTop="40dp"
android:contentDescription="@string/ImageView__badge"
android:visibility="gone"
app:badge_outline_color="@color/signal_background_primary"
app:badge_outline_width="1dp"
app:badge_size="medium"
app:layout_constraintStart_toStartOf="@id/icon"
app:layout_constraintTop_toTopOf="@id/icon"
tools:visibility="visible" />

View file

@ -25,14 +25,13 @@
<org.thoughtcrime.securesms.badges.BadgeImageView
android:id="@+id/contact_badge"
android:layout_width="@dimen/badge_size_small"
android:layout_height="@dimen/badge_size_small"
android:layout_marginStart="23dp"
android:layout_marginTop="23dp"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:contentDescription="@string/ImageView__badge"
android:visibility="gone"
app:badge_outline_color="@color/signal_background_primary"
app:badge_outline_width="1dp"
app:badge_size="small"
app:layout_constraintStart_toStartOf="@id/contact_photo_image"
app:layout_constraintTop_toTopOf="@id/contact_photo_image"
tools:visibility="visible" />

View file

@ -18,14 +18,13 @@
<org.thoughtcrime.securesms.badges.BadgeImageView
android:id="@+id/message_request_badge"
android:layout_width="34dp"
android:layout_height="34dp"
android:layout_marginStart="46dp"
android:layout_marginTop="47dp"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="47dp"
android:layout_marginTop="48dp"
android:contentDescription="@string/ImageView__badge"
android:visibility="gone"
app:badge_outline_color="@color/signal_background_primary"
app:badge_outline_width="1dp"
app:badge_size="large"
app:layout_constraintStart_toStartOf="@id/message_request_avatar"
app:layout_constraintTop_toTopOf="@id/message_request_avatar"
tools:visibility="visible" />

View file

@ -191,14 +191,13 @@
<org.thoughtcrime.securesms.badges.BadgeImageView
android:id="@+id/conversation_list_item_badge"
android:layout_width="26dp"
android:layout_height="26dp"
android:layout_marginStart="@dimen/conversation_list_badge_offset"
android:layout_marginTop="@dimen/conversation_list_badge_offset"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="26dp"
android:layout_marginTop="26dp"
android:contentDescription="@string/ImageView__badge"
android:visibility="gone"
app:badge_outline_color="@color/signal_background_primary"
app:badge_outline_width="1dp"
app:badge_size="medium"
app:layout_constraintStart_toStartOf="@id/conversation_list_item_avatar"
app:layout_constraintTop_toTopOf="@id/conversation_list_item_avatar"
tools:visibility="visible" />

View file

@ -23,14 +23,13 @@
<org.thoughtcrime.securesms.badges.BadgeImageView
android:id="@+id/bio_preference_badge"
android:layout_width="34dp"
android:layout_height="34dp"
android:layout_marginStart="46dp"
android:layout_marginTop="47dp"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="47dp"
android:layout_marginTop="48dp"
android:contentDescription="@string/ImageView__badge"
android:visibility="gone"
app:badge_outline_color="@color/signal_background_primary"
app:badge_outline_width="1dp"
app:badge_size="large"
app:layout_constraintStart_toStartOf="@id/bio_preference_avatar"
app:layout_constraintTop_toTopOf="@id/bio_preference_avatar"
tools:visibility="visible" />

View file

@ -31,16 +31,15 @@
<org.thoughtcrime.securesms.badges.BadgeImageView
android:id="@+id/badge"
android:layout_width="@dimen/badge_size_small"
android:layout_height="@dimen/badge_size_small"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_alignStart="@id/contact_photo_image"
android:layout_alignTop="@id/contact_photo_image"
android:layout_marginStart="21dp"
android:layout_marginTop="21dp"
android:layout_marginStart="22dp"
android:layout_marginTop="22dp"
android:contentDescription="@string/ImageView__badge"
android:visibility="gone"
app:badge_outline_color="@color/signal_background_primary"
app:badge_outline_width="1dp"
app:badge_size="small"
tools:visibility="visible" />
</RelativeLayout>

View file

@ -22,11 +22,12 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
<org.thoughtcrime.securesms.badges.BadgeImageView
android:id="@+id/badge"
android:layout_width="33dp"
android:layout_height="33dp"
android:layout_width="32dp"
android:layout_height="32dp"
android:contentDescription="@string/BadgesOverviewFragment__featured_badge"
app:badge_size="large"
app:layout_constraintBottom_toBottomOf="@id/avatar"
app:layout_constraintEnd_toEndOf="@id/avatar" />

View file

@ -23,14 +23,13 @@
<org.thoughtcrime.securesms.badges.BadgeImageView
android:id="@+id/rbs_badge"
android:layout_width="34dp"
android:layout_height="34dp"
android:layout_marginStart="46dp"
android:layout_marginTop="47dp"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="47dp"
android:layout_marginTop="48dp"
android:contentDescription="@string/ImageView__badge"
android:visibility="gone"
app:badge_outline_color="@color/signal_background_primary"
app:badge_outline_width="1dp"
app:badge_size="large"
app:layout_constraintStart_toStartOf="@id/rbs_recipient_avatar"
app:layout_constraintTop_toTopOf="@id/rbs_recipient_avatar"
tools:visibility="visible" />

View file

@ -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">
<ImageView
<org.thoughtcrime.securesms.badges.BadgeImageView
android:id="@+id/badge"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center_horizontal"
android:contentDescription="@string/BadgesOverviewFragment__featured_badge"
app:badge_size="xlarge"
tools:src="@drawable/test_gradient" />
<TextView

View file

@ -33,4 +33,6 @@
<dimen name="toolbar_avatar_margin">34dp</dimen>
<dimen name="verify_identity_vertical_margin">32dp</dimen>
<integer name="badge_columns">4</integer>
</resources>

View file

@ -2,4 +2,6 @@
<resources>
<dimen name="media_bubble_max_width">350dp</dimen>
<dimen name="media_bubble_max_height">300dp</dimen>
<integer name="badge_columns">5</integer>
</resources>

View file

@ -328,7 +328,11 @@
</declare-styleable>
<declare-styleable name="BadgeImageView">
<attr name="badge_outline_width" format="dimension" />
<attr name="badge_outline_color" format="color" />
<attr name="badge_size" format="enum">
<enum name="small" value="0" />
<enum name="medium" value="1" />
<enum name="large" value="2" />
<enum name="xlarge" value="3" />
</attr>
</declare-styleable>
</resources>

View file

@ -205,9 +205,8 @@
<dimen name="toolbar_avatar_size">28dp</dimen>
<dimen name="toolbar_avatar_margin">26dp</dimen>
<dimen name="conversation_list_avatar_size">48dp</dimen>
<dimen name="conversation_list_badge_offset">25dp</dimen>
<dimen name="verify_identity_vertical_margin">16dp</dimen>
<dimen name="badge_size_small">18dp</dimen>
<integer name="badge_columns">3</integer>
</resources>

View file

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

View file

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

View file

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