From ad81b310e37900ecee62515e9aa8a62a6120eb72 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Fri, 23 Apr 2021 14:42:51 -0400 Subject: [PATCH] Blur avatar photos from unknown senders when in message request state. --- .../database/FlipperSqlCipherAdapter.java | 8 +- .../securesms/components/AvatarImageView.java | 35 +++- .../conversation/ConversationBannerView.java | 23 +++ .../securesms/database/GroupDatabase.java | 21 ++- .../securesms/database/RecipientDatabase.java | 160 ++++++++++++++++-- .../database/helpers/SQLCipherOpenHelper.java | 22 ++- .../securesms/jobs/JobManagerFactories.java | 2 + .../migrations/ApplicationMigrations.java | 65 +++---- .../ProfileSharingUpdateMigrationJob.java | 52 ++++++ .../v2/NotificationExtensions.kt | 13 +- .../securesms/recipients/Recipient.java | 56 +++++- .../recipients/RecipientDetails.java | 92 +++++----- .../securesms/util/AvatarUtil.java | 25 ++- .../securesms/util/BlurTransformation.java | 4 +- app/src/main/proto/Database.proto | 4 + .../main/res/drawable/ic_tap_outline_24.xml | 34 ++++ .../res/layout/conversation_banner_view.xml | 32 +++- app/src/main/res/values/strings.xml | 1 + .../securesms/util/SqlUtilTest.java | 10 ++ 19 files changed, 546 insertions(+), 113 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/migrations/ProfileSharingUpdateMigrationJob.java create mode 100644 app/src/main/res/drawable/ic_tap_outline_24.xml diff --git a/app/src/flipper/java/org/thoughtcrime/securesms/database/FlipperSqlCipherAdapter.java b/app/src/flipper/java/org/thoughtcrime/securesms/database/FlipperSqlCipherAdapter.java index dd9d0dde1..fcb38a49a 100644 --- a/app/src/flipper/java/org/thoughtcrime/securesms/database/FlipperSqlCipherAdapter.java +++ b/app/src/flipper/java/org/thoughtcrime/securesms/database/FlipperSqlCipherAdapter.java @@ -17,6 +17,7 @@ import net.sqlcipher.database.SQLiteStatement; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; +import org.thoughtcrime.securesms.util.Hex; import java.lang.reflect.Field; import java.util.ArrayList; @@ -237,7 +238,12 @@ public class FlipperSqlCipherAdapter extends DatabaseDriver 32) { + bytes += "..."; + } + return bytes; case Cursor.FIELD_TYPE_STRING: default: return cursor.getString(column); 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 e8a44c2aa..208941e11 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/AvatarImageView.java @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components; import android.content.Context; import android.content.res.TypedArray; +import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; @@ -14,7 +15,11 @@ import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatImageView; import androidx.fragment.app.FragmentActivity; +import com.bumptech.glide.load.MultiTransformation; +import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.load.resource.bitmap.CircleCrop; +import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.R; @@ -23,6 +28,7 @@ import org.thoughtcrime.securesms.contacts.avatars.ContactColors; import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto; import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto; import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.ui.managegroup.ManageGroupActivity; import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideRequests; @@ -30,9 +36,12 @@ import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment; import org.thoughtcrime.securesms.recipients.ui.managerecipient.ManageRecipientActivity; import org.thoughtcrime.securesms.util.AvatarUtil; +import org.thoughtcrime.securesms.util.BlurTransformation; import org.thoughtcrime.securesms.util.ThemeUtil; import org.thoughtcrime.securesms.util.Util; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; public final class AvatarImageView extends AppCompatImageView { @@ -63,6 +72,7 @@ public final class AvatarImageView extends AppCompatImageView { private Paint outlinePaint; private OnClickListener listener; private Recipient.FallbackPhotoProvider fallbackPhotoProvider; + private boolean blurred; private @Nullable RecipientContactPhoto recipientContactPhoto; private @NonNull Drawable unknownRecipientDrawable; @@ -90,15 +100,16 @@ public final class AvatarImageView extends AppCompatImageView { outlinePaint = ThemeUtil.isDarkTheme(getContext()) ? DARK_THEME_OUTLINE_PAINT : LIGHT_THEME_OUTLINE_PAINT; unknownRecipientDrawable = new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20).asDrawable(getContext(), ContactColors.UNKNOWN_COLOR.toConversationColor(getContext()), inverted); + blurred = false; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); - float width = getWidth() - getPaddingRight() - getPaddingLeft(); + float width = getWidth() - getPaddingRight() - getPaddingLeft(); float height = getHeight() - getPaddingBottom() - getPaddingTop(); - float cx = width / 2f; + float cx = width / 2f; float cy = height / 2f; float radius = Math.min(cx, cy) - (outlinePaint.getStrokeWidth() / 2f); @@ -160,20 +171,30 @@ public final class AvatarImageView extends AppCompatImageView { Recipient.self().getProfileAvatar())) : new RecipientContactPhoto(recipient); - if (!photo.equals(recipientContactPhoto)) { + boolean shouldBlur = recipient.shouldBlurAvatar(); + + if (!photo.equals(recipientContactPhoto) || shouldBlur != blurred) { requestManager.clear(this); recipientContactPhoto = photo; - Drawable fallbackContactPhotoDrawable = size == SIZE_SMALL - ? photo.recipient.getSmallFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider) - : photo.recipient.getFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider); + Drawable fallbackContactPhotoDrawable = size == SIZE_SMALL ? photo.recipient.getSmallFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider) + : photo.recipient.getFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider); if (photo.contactPhoto != null) { + + List> transforms = new ArrayList<>(); + if (shouldBlur) { + transforms.add(new BlurTransformation(ApplicationDependencies.getApplication(), 0.25f, BlurTransformation.MAX_RADIUS)); + } + transforms.add(new CircleCrop()); + blurred = shouldBlur; + requestManager.load(photo.contactPhoto) .fallback(fallbackContactPhotoDrawable) .error(fallbackContactPhotoDrawable) .diskCacheStrategy(DiskCacheStrategy.ALL) - .circleCrop() + .downsample(DownsampleStrategy.CENTER_INSIDE) + .transform(new MultiTransformation<>(transforms)) .into(this); } else { setImageDrawable(fallbackContactPhotoDrawable); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationBannerView.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationBannerView.java index d0f543346..58638c850 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationBannerView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationBannerView.java @@ -1,6 +1,8 @@ package org.thoughtcrime.securesms.conversation; import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.PorterDuff; import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; @@ -9,11 +11,15 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.core.content.ContextCompat; +import androidx.core.widget.ImageViewCompat; +import org.signal.core.util.concurrent.SignalExecutors; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.AvatarImageView; import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto; import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto; +import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.recipients.Recipient; @@ -24,6 +30,7 @@ public class ConversationBannerView extends ConstraintLayout { private TextView contactAbout; private TextView contactSubtitle; private TextView contactDescription; + private View tapToView; public ConversationBannerView(Context context) { this(context, null); @@ -43,12 +50,28 @@ public class ConversationBannerView extends ConstraintLayout { contactAbout = findViewById(R.id.message_request_about); contactSubtitle = findViewById(R.id.message_request_subtitle); contactDescription = findViewById(R.id.message_request_description); + tapToView = findViewById(R.id.message_request_avatar_tap_to_view); contactAvatar.setFallbackPhotoProvider(new FallbackPhotoProvider()); } public void setAvatar(@NonNull GlideRequests requests, @Nullable Recipient recipient) { contactAvatar.setAvatar(requests, recipient, false); + + if (recipient.shouldBlurAvatar() && recipient.getContactPhoto() != null) { + tapToView.setVisibility(VISIBLE); + tapToView.setOnClickListener(v -> { + SignalExecutors.BOUNDED.execute(() -> DatabaseFactory.getRecipientDatabase(getContext().getApplicationContext()) + .manuallyShowAvatar(recipient.getId())); + }); + ImageViewCompat.setImageTintList(contactAvatar, ColorStateList.valueOf(ContextCompat.getColor(getContext(), R.color.transparent_black_40))); + ImageViewCompat.setImageTintMode(contactAvatar, PorterDuff.Mode.SRC_ATOP); + } else { + tapToView.setVisibility(GONE); + tapToView.setOnClickListener(null); + ImageViewCompat.setImageTintList(contactAvatar, null); + ImageViewCompat.setImageTintMode(contactAvatar, PorterDuff.Mode.SRC_IN); + } } public void setTitle(@Nullable CharSequence title) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java index e3c0a00b5..bb05d37ba 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -15,11 +15,9 @@ import com.google.protobuf.InvalidProtocolBufferException; import org.signal.core.util.logging.Log; import org.signal.storageservice.protos.groups.AccessControl; -import org.signal.storageservice.protos.groups.GroupChange; import org.signal.storageservice.protos.groups.Member; import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; -import org.signal.storageservice.protos.groups.local.DecryptedMember; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.groups.GroupMasterKey; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; @@ -447,15 +445,17 @@ public final class GroupDatabase extends Database { contentValues.put(MMS, groupId.isMms()); + List groupMembers = members; if (groupMasterKey != null) { if (groupState == null) { throw new AssertionError("V2 master key but no group state"); } groupId.requireV2(); + groupMembers = getV2GroupMembers(groupState); contentValues.put(V2_MASTER_KEY, groupMasterKey.serialize()); contentValues.put(V2_REVISION, groupState.getRevision()); contentValues.put(V2_DECRYPTED_GROUP, groupState.toByteArray()); - contentValues.put(MEMBERS, serializeV2GroupMembers(groupState)); + contentValues.put(MEMBERS, RecipientId.toSerializedList(groupMembers)); } else { if (groupId.isV2()) { throw new AssertionError("V2 group id but no master key"); @@ -468,6 +468,10 @@ public final class GroupDatabase extends Database { recipientDatabase.setExpireMessages(groupRecipientId, groupState.getDisappearingMessagesTimer().getDuration()); } + if (groupMembers != null && (groupId.isMms() || Recipient.resolved(groupRecipientId).isProfileSharing())) { + recipientDatabase.setHasGroupsInCommon(groupMembers); + } + Recipient.live(groupRecipientId).refresh(); notifyConversationListListeners(); @@ -585,10 +589,11 @@ public final class GroupDatabase extends Database { contentValues.put(UNMIGRATED_V1_MEMBERS, unmigratedV1Members.isEmpty() ? null : RecipientId.toSerializedList(unmigratedV1Members)); } + List groupMembers = getV2GroupMembers(decryptedGroup); contentValues.put(TITLE, title); contentValues.put(V2_REVISION, decryptedGroup.getRevision()); contentValues.put(V2_DECRYPTED_GROUP, decryptedGroup.toByteArray()); - contentValues.put(MEMBERS, serializeV2GroupMembers(decryptedGroup)); + contentValues.put(MEMBERS, RecipientId.toSerializedList(groupMembers)); contentValues.put(ACTIVE, gv2GroupActive(decryptedGroup) ? 1 : 0); databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, @@ -599,6 +604,10 @@ public final class GroupDatabase extends Database { recipientDatabase.setExpireMessages(groupRecipientId, decryptedGroup.getDisappearingMessagesTimer().getDuration()); } + if (groupMembers != null && (groupId.isMms() || Recipient.resolved(groupRecipientId).isProfileSharing())) { + recipientDatabase.setHasGroupsInCommon(groupMembers); + } + Recipient.live(groupRecipientId).refresh(); notifyConversationListListeners(); @@ -742,11 +751,11 @@ public final class GroupDatabase extends Database { return groupMembers; } - private static String serializeV2GroupMembers(@NonNull DecryptedGroup decryptedGroup) { + private static List getV2GroupMembers(@NonNull DecryptedGroup decryptedGroup) { List uuids = DecryptedGroupUtil.membersToUuidList(decryptedGroup.getMembersList()); List recipientIds = uuidsToRecipientIds(uuids); - return RecipientId.toSerializedList(recipientIds); + return recipientIds; } public @NonNull List getAllGroupV2Ids() { 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 640aed18b..ef0ffeb20 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -8,6 +8,7 @@ import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.arch.core.util.Function; import com.annimon.stream.Stream; import com.google.protobuf.ByteString; @@ -31,6 +32,7 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.database.model.databaseprotos.DeviceLastResetTime; import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileKeyCredentialColumnData; +import org.thoughtcrime.securesms.database.model.databaseprotos.RecipientExtras; import org.thoughtcrime.securesms.database.model.databaseprotos.Wallpaper; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.GroupId; @@ -43,8 +45,8 @@ import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.profiles.ProfileName; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; -import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.storage.StorageRecordUpdate; +import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.storage.StorageSyncModels; import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.Bitmask; @@ -68,8 +70,6 @@ import org.whispersystems.signalservice.api.storage.SignalAccountRecord; import org.whispersystems.signalservice.api.storage.SignalContactRecord; import org.whispersystems.signalservice.api.storage.SignalGroupV1Record; import org.whispersystems.signalservice.api.storage.SignalGroupV2Record; -import org.whispersystems.signalservice.api.storage.SignalRecord; -import org.whispersystems.signalservice.api.storage.SignalStorageRecord; import org.whispersystems.signalservice.api.storage.StorageId; import org.whispersystems.signalservice.api.util.UuidUtil; @@ -142,8 +142,10 @@ public class RecipientDatabase extends Database { private static final String LAST_SESSION_RESET = "last_session_reset"; private static final String WALLPAPER = "wallpaper"; private static final String WALLPAPER_URI = "wallpaper_file"; - public static final String ABOUT = "about"; - public static final String ABOUT_EMOJI = "about_emoji"; + public static final String ABOUT = "about"; + public static final String ABOUT_EMOJI = "about_emoji"; + private static final String EXTRAS = "extras"; + private static final String GROUPS_IN_COMMON = "groups_in_common"; public static final String SEARCH_PROFILE_NAME = "search_signal_profile"; private static final String SORT_NAME = "sort_name"; @@ -170,12 +172,13 @@ public class RecipientDatabase extends Database { STORAGE_SERVICE_ID, DIRTY, MENTION_SETTING, WALLPAPER, WALLPAPER_URI, MENTION_SETTING, - ABOUT, ABOUT_EMOJI + ABOUT, ABOUT_EMOJI, + EXTRAS, GROUPS_IN_COMMON }; private static final String[] ID_PROJECTION = new String[]{ID}; - private static final String[] SEARCH_PROJECTION = new String[]{ID, SYSTEM_JOINED_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, ABOUT, ABOUT_EMOJI, "COALESCE(" + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ") AS " + SEARCH_PROFILE_NAME, "COALESCE(" + nullIfEmpty(SYSTEM_JOINED_NAME) + ", " + nullIfEmpty(SYSTEM_GIVEN_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ") AS " + SORT_NAME}; - public static final String[] SEARCH_PROJECTION_NAMES = new String[]{ID, SYSTEM_JOINED_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, ABOUT, ABOUT_EMOJI, SEARCH_PROFILE_NAME, SORT_NAME}; + private static final String[] SEARCH_PROJECTION = new String[]{ID, SYSTEM_JOINED_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, ABOUT, ABOUT_EMOJI, EXTRAS, GROUPS_IN_COMMON, "COALESCE(" + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ") AS " + SEARCH_PROFILE_NAME, "COALESCE(" + nullIfEmpty(SYSTEM_JOINED_NAME) + ", " + nullIfEmpty(SYSTEM_GIVEN_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ") AS " + SORT_NAME}; + public static final String[] SEARCH_PROJECTION_NAMES = new String[]{ID, SYSTEM_JOINED_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, ABOUT, ABOUT_EMOJI, EXTRAS, GROUPS_IN_COMMON, SEARCH_PROFILE_NAME, SORT_NAME}; private static final String[] TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION) .map(columnName -> TABLE_NAME + "." + columnName) .toList().toArray(new String[0]); @@ -371,7 +374,9 @@ public class RecipientDatabase extends Database { WALLPAPER + " BLOB DEFAULT NULL, " + WALLPAPER_URI + " TEXT DEFAULT NULL, " + ABOUT + " TEXT DEFAULT NULL, " + - ABOUT_EMOJI + " TEXT DEFAULT NULL);"; + ABOUT_EMOJI + " TEXT DEFAULT NULL, " + + EXTRAS + " BLOB DEFAULT NULL, " + + GROUPS_IN_COMMON + " INTEGER DEFAULT 0);"; private static final String INSIGHTS_INVITEE_LIST = "SELECT " + TABLE_NAME + "." + ID + " FROM " + TABLE_NAME + @@ -1461,6 +1466,7 @@ public class RecipientDatabase extends Database { byte[] wallpaper = CursorUtil.requireBlob(cursor, WALLPAPER); String about = CursorUtil.requireString(cursor, ABOUT); String aboutEmoji = CursorUtil.requireString(cursor, ABOUT_EMOJI); + boolean hasGroupsInCommon = CursorUtil.requireBoolean(cursor, GROUPS_IN_COMMON); MaterialColor color; byte[] profileKey = null; @@ -1549,7 +1555,9 @@ public class RecipientDatabase extends Database { chatWallpaper, about, aboutEmoji, - getSyncExtras(cursor)); + getSyncExtras(cursor), + getExtras(cursor), + hasGroupsInCommon); } private static @NonNull RecipientSettings.SyncExtras getSyncExtras(@NonNull Cursor cursor) { @@ -1565,6 +1573,23 @@ public class RecipientDatabase extends Database { return new RecipientSettings.SyncExtras(storageProto, groupMasterKey, identityKey, identityStatus, archived, forcedUnread); } + private static @Nullable Recipient.Extras getExtras(@NonNull Cursor cursor) { + return Recipient.Extras.from(getRecipientExtras(cursor)); + } + + private static @Nullable RecipientExtras getRecipientExtras(@NonNull Cursor cursor) { + final Optional blob = CursorUtil.getBlob(cursor, EXTRAS); + + return blob.transform(b -> { + try { + return RecipientExtras.parseFrom(b); + } catch (InvalidProtocolBufferException e) { + Log.w(TAG, e); + throw new AssertionError(e); + } + }).orNull(); + } + public BulkOperationsHandle beginBulkSystemContactUpdate() { SQLiteDatabase database = databaseHelper.getWritableDatabase(); database.beginTransaction(); @@ -1986,6 +2011,14 @@ public class RecipientDatabase extends Database { boolean profiledUpdated = update(id, contentValues); boolean colorUpdated = enabled && setColorIfNotSetInternal(id, ContactColors.generateFor(Recipient.resolved(id).getDisplayName(context))); + if (profiledUpdated && enabled) { + Optional group = DatabaseFactory.getGroupDatabase(context).getGroup(id); + + if (group.isPresent()) { + setHasGroupsInCommon(group.get().getMembers()); + } + } + if (profiledUpdated || colorUpdated) { markDirty(id, DirtyState.UPDATE); Recipient.live(id).refresh(); @@ -2841,6 +2874,93 @@ public class RecipientDatabase extends Database { } } + public void markPreMessageRequestRecipientsAsProfileSharingEnabled(long messageRequestEnableTime) { + String[] whereArgs = SqlUtil.buildArgs(messageRequestEnableTime, messageRequestEnableTime); + + String select = "SELECT r." + ID + " FROM " + TABLE_NAME + " AS r " + + "INNER JOIN " + ThreadDatabase.TABLE_NAME + " AS t ON t." + ThreadDatabase.RECIPIENT_ID + " = r." + ID + " WHERE " + + "r." + PROFILE_SHARING + " = 0 AND " + + "(" + + "EXISTS(SELECT 1 FROM " + SmsDatabase.TABLE_NAME + " WHERE " + SmsDatabase.THREAD_ID + " = t." + ThreadDatabase.ID + " AND " + SmsDatabase.DATE_RECEIVED + " < ?) " + + "OR " + + "EXISTS(SELECT 1 FROM " + MmsDatabase.TABLE_NAME + " WHERE " + MmsDatabase.THREAD_ID + " = t." + ThreadDatabase.ID + " AND " + MmsDatabase.DATE_RECEIVED + " < ?) " + + ")"; + + List idsToUpdate = new ArrayList<>(); + try (Cursor cursor = databaseHelper.getReadableDatabase().rawQuery(select, whereArgs)) { + while (cursor.moveToNext()) { + idsToUpdate.add(CursorUtil.requireLong(cursor, ID)); + } + } + + if (Util.hasItems(idsToUpdate)) { + SqlUtil.Query query = SqlUtil.buildCollectionQuery(ID, idsToUpdate); + ContentValues values = new ContentValues(1); + values.put(PROFILE_SHARING, 1); + databaseHelper.getWritableDatabase().update(TABLE_NAME, values, query.getWhere(), query.getWhereArgs()); + + for (long id : idsToUpdate) { + Recipient.live(RecipientId.from(id)).refresh(); + } + } + } + + public void setHasGroupsInCommon(@NonNull List recipientIds) { + SqlUtil.Query query = SqlUtil.buildCollectionQuery(ID, recipientIds); + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + try (Cursor cursor = db.query(TABLE_NAME, + new String[]{ID}, + query.getWhere() + " AND " + GROUPS_IN_COMMON + " = 0", + query.getWhereArgs(), + null, + null, + null)) + { + List idsToUpdate = new ArrayList<>(cursor.getCount()); + while (cursor.moveToNext()) { + idsToUpdate.add(CursorUtil.requireLong(cursor, ID)); + } + + if (Util.hasItems(idsToUpdate)) { + query = SqlUtil.buildCollectionQuery(ID, idsToUpdate); + ContentValues values = new ContentValues(); + values.put(GROUPS_IN_COMMON, 1); + int count = db.update(TABLE_NAME, values, query.getWhere(), query.getWhereArgs()); + if (count > 0) { + for (long id : idsToUpdate) { + Recipient.live(RecipientId.from(id)).refresh(); + } + } + } + } + } + + public void manuallyShowAvatar(@NonNull RecipientId recipientId) { + updateExtras(recipientId, b -> b.setManuallyShownAvatar(true)); + } + + private void updateExtras(@NonNull RecipientId recipientId, @NonNull Function updater) { + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + db.beginTransaction(); + try { + try (Cursor cursor = db.query(TABLE_NAME, new String[]{ID, EXTRAS}, ID_WHERE, SqlUtil.buildArgs(recipientId), null, null, null)) { + if (cursor.moveToNext()) { + RecipientExtras state = getRecipientExtras(cursor); + RecipientExtras.Builder builder = state != null ? state.toBuilder() : RecipientExtras.newBuilder(); + byte[] updatedState = updater.apply(builder).build().toByteArray(); + ContentValues values = new ContentValues(1); + values.put(EXTRAS, updatedState); + db.update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(CursorUtil.requireLong(cursor, ID))); + } + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + Recipient.live(recipientId).refresh(); + } + void markDirty(@NonNull RecipientId recipientId, @NonNull DirtyState dirtyState) { Log.d(TAG, "Attempting to mark " + recipientId + " with dirty state " + dirtyState); @@ -3232,7 +3352,9 @@ public class RecipientDatabase extends Database { private final ChatWallpaper wallpaper; private final String about; private final String aboutEmoji; - private final SyncExtras syncExtras; + private final SyncExtras syncExtras; + private final Recipient.Extras extras; + private final boolean hasGroupsInCommon; RecipientSettings(@NonNull RecipientId id, @Nullable UUID uuid, @@ -3273,7 +3395,9 @@ public class RecipientDatabase extends Database { @Nullable ChatWallpaper wallpaper, @Nullable String about, @Nullable String aboutEmoji, - @NonNull SyncExtras syncExtras) + @NonNull SyncExtras syncExtras, + @Nullable Recipient.Extras extras, + boolean hasGroupsInCommon) { this.id = id; this.uuid = uuid; @@ -3316,7 +3440,9 @@ public class RecipientDatabase extends Database { this.wallpaper = wallpaper; this.about = about; this.aboutEmoji = aboutEmoji; - this.syncExtras = syncExtras; + this.syncExtras = syncExtras; + this.extras = extras; + this.hasGroupsInCommon = hasGroupsInCommon; } public RecipientId getId() { @@ -3483,6 +3609,14 @@ public class RecipientDatabase extends Database { return syncExtras; } + public @Nullable Recipient.Extras getExtras() { + return extras; + } + + public boolean hasGroupsInCommon() { + return hasGroupsInCommon; + } + long getCapabilities() { return capabilities; } 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 d557a199d..fab921baf 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 @@ -43,8 +43,8 @@ import org.thoughtcrime.securesms.database.SignedPreKeyDatabase; import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.SqlCipherDatabaseHook; import org.thoughtcrime.securesms.database.StickerDatabase; -import org.thoughtcrime.securesms.database.UnknownStorageIdDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase; +import org.thoughtcrime.securesms.database.UnknownStorageIdDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob; @@ -172,8 +172,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab private static final int PAYMENTS = 91; private static final int CLEAN_STORAGE_IDS = 92; private static final int MP4_GIF_SUPPORT = 93; + private static final int BLUR_AVATARS = 94; - private static final int DATABASE_VERSION = 93; + private static final int DATABASE_VERSION = 94; private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -1304,6 +1305,23 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab db.execSQL("ALTER TABLE part ADD COLUMN video_gif INTEGER DEFAULT 0"); } + if (oldVersion < BLUR_AVATARS) { + db.execSQL("ALTER TABLE recipient ADD COLUMN extras BLOB DEFAULT NULL"); + db.execSQL("ALTER TABLE recipient ADD COLUMN groups_in_common INTEGER DEFAULT 0"); + + String secureOutgoingSms = "EXISTS(SELECT 1 FROM sms WHERE thread_id = t._id AND (type & 31) = 23 AND (type & 10485760) AND (type & 131072 = 0))"; + String secureOutgoingMms = "EXISTS(SELECT 1 FROM mms WHERE thread_id = t._id AND (msg_box & 31) = 23 AND (msg_box & 10485760) AND (msg_box & 131072 = 0))"; + + String selectIdsToUpdateProfileSharing = "SELECT r._id FROM recipient AS r INNER JOIN thread AS t ON r._id = t.recipient_ids WHERE profile_sharing = 0 AND (" + secureOutgoingSms + " OR " + secureOutgoingMms + ")"; + + db.rawExecSQL("UPDATE recipient SET profile_sharing = 1 WHERE _id IN (" + selectIdsToUpdateProfileSharing + ")"); + + String selectIdsWithGroupsInCommon = "SELECT r._id FROM recipient AS r WHERE EXISTS(" + + "SELECT 1 FROM groups AS g INNER JOIN recipient AS gr ON (g.recipient_id = gr._id AND gr.profile_sharing = 1) WHERE g.active = 1 AND (g.members LIKE r._id || ',%' OR g.members LIKE '%,' || r._id || ',%' OR g.members LIKE '%,' || r._id)" + + ")"; + db.rawExecSQL("UPDATE recipient SET groups_in_common = 1 WHERE _id IN (" + selectIdsWithGroupsInCommon + ")"); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 560585e3f..597df9ac1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -43,6 +43,7 @@ import org.thoughtcrime.securesms.migrations.PassingMigrationJob; import org.thoughtcrime.securesms.migrations.PinOptOutMigration; import org.thoughtcrime.securesms.migrations.PinReminderMigrationJob; import org.thoughtcrime.securesms.migrations.ProfileMigrationJob; +import org.thoughtcrime.securesms.migrations.ProfileSharingUpdateMigrationJob; import org.thoughtcrime.securesms.migrations.RecipientSearchMigrationJob; import org.thoughtcrime.securesms.migrations.RegistrationPinV2MigrationJob; import org.thoughtcrime.securesms.migrations.StickerAdditionMigrationJob; @@ -167,6 +168,7 @@ public final class JobManagerFactories { put(PinOptOutMigration.KEY, new PinOptOutMigration.Factory()); put(PinReminderMigrationJob.KEY, new PinReminderMigrationJob.Factory()); put(ProfileMigrationJob.KEY, new ProfileMigrationJob.Factory()); + put(ProfileSharingUpdateMigrationJob.KEY, new ProfileSharingUpdateMigrationJob.Factory()); put(RecipientSearchMigrationJob.KEY, new RecipientSearchMigrationJob.Factory()); put(RegistrationPinV2MigrationJob.KEY, new RegistrationPinV2MigrationJob.Factory()); put(StickerLaunchMigrationJob.KEY, new StickerLaunchMigrationJob.Factory()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java index a4659c58f..aead2e9aa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java @@ -40,39 +40,40 @@ public class ApplicationMigrations { private static final int LEGACY_CANONICAL_VERSION = 455; - public static final int CURRENT_VERSION = 31; + public static final int CURRENT_VERSION = 32; private static final class Version { - static final int LEGACY = 1; - static final int RECIPIENT_ID = 2; - static final int RECIPIENT_SEARCH = 3; - static final int RECIPIENT_CLEANUP = 4; - static final int AVATAR_MIGRATION = 5; - static final int UUIDS = 6; - static final int CACHED_ATTACHMENTS = 7; - static final int STICKERS_LAUNCH = 8; - //static final int TEST_ARGON2 = 9; - static final int SWOON_STICKERS = 10; - static final int STORAGE_SERVICE = 11; - //static final int STORAGE_KEY_ROTATE = 12; - static final int REMOVE_AVATAR_ID = 13; - static final int STORAGE_CAPABILITY = 14; - static final int PIN_REMINDER = 15; - static final int VERSIONED_PROFILE = 16; - static final int PIN_OPT_OUT = 17; - static final int TRIM_SETTINGS = 18; - static final int THUMBNAIL_CLEANUP = 19; - static final int GV2 = 20; - static final int GV2_2 = 21; - static final int CDS = 22; - static final int BACKUP_NOTIFICATION = 23; - static final int GV1_MIGRATION = 24; - static final int USER_NOTIFICATION = 25; - static final int DAY_BY_DAY_STICKERS = 26; - static final int BLOB_LOCATION = 27; - static final int SYSTEM_NAME_SPLIT = 28; + static final int LEGACY = 1; + static final int RECIPIENT_ID = 2; + static final int RECIPIENT_SEARCH = 3; + static final int RECIPIENT_CLEANUP = 4; + static final int AVATAR_MIGRATION = 5; + static final int UUIDS = 6; + static final int CACHED_ATTACHMENTS = 7; + static final int STICKERS_LAUNCH = 8; + //static final int TEST_ARGON2 = 9; + static final int SWOON_STICKERS = 10; + static final int STORAGE_SERVICE = 11; + //static final int STORAGE_KEY_ROTATE = 12; + static final int REMOVE_AVATAR_ID = 13; + static final int STORAGE_CAPABILITY = 14; + static final int PIN_REMINDER = 15; + static final int VERSIONED_PROFILE = 16; + static final int PIN_OPT_OUT = 17; + static final int TRIM_SETTINGS = 18; + static final int THUMBNAIL_CLEANUP = 19; + static final int GV2 = 20; + static final int GV2_2 = 21; + static final int CDS = 22; + static final int BACKUP_NOTIFICATION = 23; + static final int GV1_MIGRATION = 24; + static final int USER_NOTIFICATION = 25; + static final int DAY_BY_DAY_STICKERS = 26; + static final int BLOB_LOCATION = 27; + static final int SYSTEM_NAME_SPLIT = 28; // Versions 29, 30 accidentally skipped - static final int MUTE_SYNC = 31; + static final int MUTE_SYNC = 31; + static final int PROFILE_SHARING_UPDATE = 32; } /** @@ -307,6 +308,10 @@ public class ApplicationMigrations { jobs.put(Version.MUTE_SYNC, new StorageServiceMigrationJob()); } + if (lastSeenVersion < Version.PROFILE_SHARING_UPDATE) { + jobs.put(Version.PROFILE_SHARING_UPDATE, new ProfileSharingUpdateMigrationJob()); + } + return jobs; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/ProfileSharingUpdateMigrationJob.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/ProfileSharingUpdateMigrationJob.java new file mode 100644 index 000000000..d1309d364 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ProfileSharingUpdateMigrationJob.java @@ -0,0 +1,52 @@ +package org.thoughtcrime.securesms.migrations; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.keyvalue.SignalStore; + +/** + * Updates profile sharing flag to true if conversation is pre-message request enable time. + */ +public class ProfileSharingUpdateMigrationJob extends MigrationJob { + + public static final String KEY = "ProfileSharingUpdateMigrationJob"; + + ProfileSharingUpdateMigrationJob() { + this(new Parameters.Builder().build()); + } + + private ProfileSharingUpdateMigrationJob(@NonNull Parameters parameters) { + super(parameters); + } + + @Override + public boolean isUiBlocking() { + return true; + } + + @Override + public @NonNull String getFactoryKey() { + return KEY; + } + + @Override + public void performMigration() { + long messageRequestEnableTime = SignalStore.misc().getMessageRequestEnableTime(); + DatabaseFactory.getRecipientDatabase(context).markPreMessageRequestRecipientsAsProfileSharingEnabled(messageRequestEnableTime); + } + + @Override + boolean shouldRetry(@NonNull Exception e) { + return false; + } + + public static class Factory implements Job.Factory { + @Override + public @NonNull ProfileSharingUpdateMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new ProfileSharingUpdateMigrationJob(parameters); + } + } +} 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 7847900cb..9d3e9b99e 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 @@ -5,16 +5,21 @@ import android.content.Intent import android.graphics.Bitmap import android.graphics.drawable.Drawable import android.net.Uri +import com.bumptech.glide.load.MultiTransformation +import com.bumptech.glide.load.Transformation import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.load.resource.bitmap.CircleCrop import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.util.BitmapUtil +import org.thoughtcrime.securesms.util.BlurTransformation import java.util.concurrent.ExecutionException fun Drawable?.toLargeBitmap(context: Context): Bitmap? { @@ -32,10 +37,16 @@ fun Recipient.getContactDrawable(context: Context): Drawable? { val fallbackContactPhoto: FallbackContactPhoto = if (isSelf) getFallback(context) else fallbackContactPhoto return if (contactPhoto != null) { try { + val transforms: MutableList> = mutableListOf() + if (shouldBlurAvatar()) { + transforms += BlurTransformation(ApplicationDependencies.getApplication(), 0.25f, BlurTransformation.MAX_RADIUS) + } + transforms += CircleCrop() + GlideApp.with(context.applicationContext) .load(contactPhoto) .diskCacheStrategy(DiskCacheStrategy.ALL) - .circleCrop() + .transform(MultiTransformation(transforms)) .submit( context.resources.getDimensionPixelSize(android.R.dimen.notification_large_icon_width), context.resources.getDimensionPixelSize(android.R.dimen.notification_large_icon_height) 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 5f99b3a65..4f47e1bb8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java @@ -32,6 +32,7 @@ import org.thoughtcrime.securesms.database.RecipientDatabase.MentionSetting; import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState; import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode; import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState; +import org.thoughtcrime.securesms.database.model.databaseprotos.RecipientExtras; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.keyvalue.SignalStore; @@ -111,7 +112,8 @@ public class Recipient { private final String aboutEmoji; private final ProfileName systemProfileName; private final String systemContactName; - + private final Optional extras; + private final boolean hasGroupsInCommon; /** * Returns a {@link LiveRecipient}, which contains a {@link Recipient} that may or may not be @@ -349,6 +351,8 @@ public class Recipient { this.aboutEmoji = null; this.systemProfileName = ProfileName.EMPTY; this.systemContactName = null; + this.extras = Optional.absent(); + this.hasGroupsInCommon = false; } public Recipient(@NonNull RecipientId id, @NonNull RecipientDetails details, boolean resolved) { @@ -396,6 +400,8 @@ public class Recipient { this.aboutEmoji = details.aboutEmoji; this.systemProfileName = details.systemProfileName; this.systemContactName = details.systemContactName; + this.extras = details.extras; + this.hasGroupsInCommon = details.hasGroupsInCommon; } public @NonNull RecipientId getId() { @@ -929,6 +935,18 @@ public class Recipient { } } + public boolean shouldBlurAvatar() { + boolean showOverride = false; + if (extras.isPresent()) { + showOverride = extras.get().manuallyShownAvatar(); + } + return !showOverride && !isSelf() && !isProfileSharing() && !isSystemContact() && !hasGroupsInCommon && isRegistered(); + } + + public boolean hasGroupsInCommon() { + return hasGroupsInCommon; + } + /** * If this recipient is missing crucial data, this will return a populated copy. Otherwise it * returns itself. @@ -1003,6 +1021,39 @@ public class Recipient { } } + public static final class Extras { + private final RecipientExtras recipientExtras; + + public static @Nullable Extras from(@Nullable RecipientExtras recipientExtras) { + if (recipientExtras != null) { + return new Extras(recipientExtras); + } else { + return null; + } + } + + private Extras(@NonNull RecipientExtras extras) { + this.recipientExtras = extras; + } + + public boolean manuallyShownAvatar() { + return recipientExtras.getManuallyShownAvatar(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final Extras that = (Extras) o; + return manuallyShownAvatar() == that.manuallyShownAvatar(); + } + + @Override + public int hashCode() { + return Objects.hash(manuallyShownAvatar()); + } + } + public boolean hasSameContent(@NonNull Recipient other) { return Objects.equals(id, other.id) && resolving == other.resolving && @@ -1047,7 +1098,8 @@ public class Recipient { mentionSetting == other.mentionSetting && Objects.equals(wallpaper, other.wallpaper) && Objects.equals(about, other.about) && - Objects.equals(aboutEmoji, other.aboutEmoji); + Objects.equals(aboutEmoji, other.aboutEmoji) && + Objects.equals(extras, other.extras); } private static boolean allContentsAreTheSame(@NonNull List a, @NonNull List b) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java index a328f67ad..9702358eb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java @@ -27,49 +27,51 @@ import java.util.UUID; public class RecipientDetails { - final UUID uuid; - final String username; - final String e164; - final String email; - final GroupId groupId; - final String groupName; - final String systemContactName; - final String customLabel; - final Uri systemContactPhoto; - final Uri contactUri; - final Optional groupAvatarId; - final MaterialColor color; - final Uri messageRingtone; - final Uri callRingtone; - final long mutedUntil; - final VibrateState messageVibrateState; - final VibrateState callVibrateState; - final boolean blocked; - final int expireMessages; - final List participants; - final ProfileName profileName; - final Optional defaultSubscriptionId; - final RegisteredState registered; - final byte[] profileKey; - final ProfileKeyCredential profileKeyCredential; - final String profileAvatar; - final boolean hasProfileImage; - final boolean profileSharing; - final long lastProfileFetch; - final boolean systemContact; - final boolean isSelf; - final String notificationChannel; - final UnidentifiedAccessMode unidentifiedAccessMode; - final boolean forceSmsSelection; - final Recipient.Capability groupsV2Capability; - final Recipient.Capability groupsV1MigrationCapability; - final InsightsBannerTier insightsBannerTier; - final byte[] storageId; - final MentionSetting mentionSetting; - final ChatWallpaper wallpaper; - final String about; - final String aboutEmoji; - final ProfileName systemProfileName; + final UUID uuid; + final String username; + final String e164; + final String email; + final GroupId groupId; + final String groupName; + final String systemContactName; + final String customLabel; + final Uri systemContactPhoto; + final Uri contactUri; + final Optional groupAvatarId; + final MaterialColor color; + final Uri messageRingtone; + final Uri callRingtone; + final long mutedUntil; + final VibrateState messageVibrateState; + final VibrateState callVibrateState; + final boolean blocked; + final int expireMessages; + final List participants; + final ProfileName profileName; + final Optional defaultSubscriptionId; + final RegisteredState registered; + final byte[] profileKey; + final ProfileKeyCredential profileKeyCredential; + final String profileAvatar; + final boolean hasProfileImage; + final boolean profileSharing; + final long lastProfileFetch; + final boolean systemContact; + final boolean isSelf; + final String notificationChannel; + final UnidentifiedAccessMode unidentifiedAccessMode; + final boolean forceSmsSelection; + final Recipient.Capability groupsV2Capability; + final Recipient.Capability groupsV1MigrationCapability; + final InsightsBannerTier insightsBannerTier; + final byte[] storageId; + final MentionSetting mentionSetting; + final ChatWallpaper wallpaper; + final String about; + final String aboutEmoji; + final ProfileName systemProfileName; + final Optional extras; + final boolean hasGroupsInCommon; public RecipientDetails(@Nullable String groupName, @Nullable String systemContactName, @@ -122,6 +124,8 @@ public class RecipientDetails { this.systemProfileName = settings.getSystemProfileName(); this.groupName = groupName; this.systemContactName = systemContactName; + this.extras = Optional.fromNullable(settings.getExtras()); + this.hasGroupsInCommon = settings.hasGroupsInCommon(); } /** @@ -171,6 +175,8 @@ public class RecipientDetails { this.aboutEmoji = null; this.systemProfileName = ProfileName.EMPTY; this.systemContactName = null; + this.extras = Optional.absent(); + this.hasGroupsInCommon = false; } public static @NonNull RecipientDetails forIndividual(@NonNull Context context, @NonNull RecipientSettings settings) { 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 e5881e80f..cab548580 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/AvatarUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/AvatarUtil.java @@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.util; import android.content.Context; import android.graphics.Bitmap; -import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.text.TextUtils; @@ -108,7 +107,11 @@ public final class AvatarUtil { @WorkerThread public static @NonNull Icon getIconForShortcut(@NonNull Context context, @NonNull Recipient recipient) { try { - return Icon.createWithAdaptiveBitmap(GlideApp.with(context).asBitmap().load(new ConversationShortcutPhoto(recipient)).submit().get()); + GlideRequest glideRequest = GlideApp.with(context).asBitmap().load(new ConversationShortcutPhoto(recipient)); + if (recipient.shouldBlurAvatar()) { + glideRequest = glideRequest.transform(new BlurTransformation(context, 0.25f, BlurTransformation.MAX_RADIUS)); + } + return Icon.createWithAdaptiveBitmap(glideRequest.submit().get()); } catch (ExecutionException | InterruptedException e) { throw new AssertionError("This call should not fail."); } @@ -117,7 +120,11 @@ public final class AvatarUtil { @WorkerThread public static @NonNull IconCompat getIconCompatForShortcut(@NonNull Context context, @NonNull Recipient recipient) { try { - return IconCompat.createWithAdaptiveBitmap(GlideApp.with(context).asBitmap().load(new ConversationShortcutPhoto(recipient)).submit().get()); + GlideRequest glideRequest = GlideApp.with(context).asBitmap().load(new ConversationShortcutPhoto(recipient)); + if (recipient.shouldBlurAvatar()) { + glideRequest = glideRequest.transform(new BlurTransformation(context, 0.25f, BlurTransformation.MAX_RADIUS)); + } + return IconCompat.createWithAdaptiveBitmap(glideRequest.submit().get()); } catch (ExecutionException | InterruptedException e) { throw new AssertionError("This call should not fail."); } @@ -152,9 +159,15 @@ public final class AvatarUtil { photo = recipient.getContactPhoto(); } - return glideRequest.load(photo) - .error(getFallback(context, recipient)) - .diskCacheStrategy(DiskCacheStrategy.ALL); + final GlideRequest request = glideRequest.load(photo) + .error(getFallback(context, recipient)) + .diskCacheStrategy(DiskCacheStrategy.ALL); + + if (recipient.shouldBlurAvatar()) { + return request.transform(new BlurTransformation(context, 0.25f, BlurTransformation.MAX_RADIUS)); + } else { + return request; + } } private static Drawable getFallback(@NonNull Context context, @NonNull Recipient recipient) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/BlurTransformation.java b/app/src/main/java/org/thoughtcrime/securesms/util/BlurTransformation.java index c5f21c3c3..27050d3ac 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/BlurTransformation.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/BlurTransformation.java @@ -41,7 +41,9 @@ public final class BlurTransformation extends BitmapTransformation { Matrix scaleMatrix = new Matrix(); scaleMatrix.setScale(bitmapScaleFactor, bitmapScaleFactor); - Bitmap blurredBitmap = Bitmap.createBitmap(toTransform, 0, 0, outWidth, outHeight, scaleMatrix, true); + int targetWidth = Math.min(outWidth, toTransform.getWidth()); + int targetHeight = Math.min(outHeight, toTransform.getHeight()); + Bitmap blurredBitmap = Bitmap.createBitmap(toTransform, 0, 0, targetWidth, targetHeight, scaleMatrix, true); Allocation input = Allocation.createFromBitmap(rs, blurredBitmap, Allocation.MipmapControl.MIPMAP_FULL, Allocation.USAGE_SHARED); Allocation output = Allocation.createTyped(rs, input.getType()); ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)); diff --git a/app/src/main/proto/Database.proto b/app/src/main/proto/Database.proto index 928e138c9..f182467b0 100644 --- a/app/src/main/proto/Database.proto +++ b/app/src/main/proto/Database.proto @@ -124,4 +124,8 @@ message Wallpaper { } float dimLevelInDarkTheme = 4; +} + +message RecipientExtras { + bool manuallyShownAvatar = 1; } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_tap_outline_24.xml b/app/src/main/res/drawable/ic_tap_outline_24.xml new file mode 100644 index 000000000..db3558501 --- /dev/null +++ b/app/src/main/res/drawable/ic_tap_outline_24.xml @@ -0,0 +1,34 @@ + + + + + + diff --git a/app/src/main/res/layout/conversation_banner_view.xml b/app/src/main/res/layout/conversation_banner_view.xml index 478cc6878..934ce1901 100644 --- a/app/src/main/res/layout/conversation_banner_view.xml +++ b/app/src/main/res/layout/conversation_banner_view.xml @@ -13,7 +13,37 @@ android:layout_height="112dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toTopOf="parent" + tools:srcCompat="@tools:sample/avatars" /> + + + + + + + + Join this group? They won’t know you’ve seen their messages until you accept. Unblock this group and share your name and photo with its members? You won\'t receive any messages until you unblock them. https://support.signal.org/hc/articles/360007459591 + View Member of %1$s Member of %1$s and %2$s Member of %1$s, %2$s, and %3$s diff --git a/app/src/test/java/org/thoughtcrime/securesms/util/SqlUtilTest.java b/app/src/test/java/org/thoughtcrime/securesms/util/SqlUtilTest.java index ba8f209f2..f6dcd577a 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/util/SqlUtilTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/util/SqlUtilTest.java @@ -7,6 +7,8 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientId; import java.util.Arrays; import java.util.Collections; @@ -111,6 +113,14 @@ public final class SqlUtilTest { assertArrayEquals(new String[] { "1", "2", "3" }, updateQuery.getWhereArgs()); } + @Test + public void buildCollectionQuery_multipleRecipientIds() { + SqlUtil.Query updateQuery = SqlUtil.buildCollectionQuery("a", Arrays.asList(RecipientId.from(1), RecipientId.from(2), RecipientId.from(3))); + + assertEquals("a IN (?, ?, ?)", updateQuery.getWhere()); + assertArrayEquals(new String[] { "1", "2", "3" }, updateQuery.getWhereArgs()); + } + @Test(expected = IllegalArgumentException.class) public void buildCollectionQuery_none() { SqlUtil.buildCollectionQuery("a", Collections.emptyList());