diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/MmsDatabaseTest_stories.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/MmsDatabaseTest_stories.kt index 50cd3b4355..546c3d62ac 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/MmsDatabaseTest_stories.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/database/MmsDatabaseTest_stories.kt @@ -259,7 +259,7 @@ class MmsDatabaseTest_stories { ) // WHEN - val result = mms.hasSelfReplyInStory(groupStoryId) + val result = mms.hasGroupReplyOrReactionInStory(groupStoryId) // THEN assertFalse(result) @@ -284,7 +284,7 @@ class MmsDatabaseTest_stories { ) // WHEN - val result = mms.hasSelfReplyInStory(groupStoryId) + val result = mms.hasGroupReplyOrReactionInStory(groupStoryId) // THEN assertTrue(result) @@ -309,7 +309,7 @@ class MmsDatabaseTest_stories { ) // WHEN - val result = mms.hasSelfReplyInStory(groupStoryId) + val result = mms.hasGroupReplyOrReactionInStory(groupStoryId) // THEN assertFalse(result) @@ -337,7 +337,7 @@ class MmsDatabaseTest_stories { ) // WHEN - val result = mms.hasSelfReplyInStory(groupStoryId) + val result = mms.hasGroupReplyOrReactionInStory(groupStoryId) // THEN assertFalse(result) diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest.kt index 4c721a7380..5ff5df98a2 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest.kt @@ -18,6 +18,77 @@ class RecipientDatabaseTest { @get:Rule val harness = SignalActivityRule() + @Test + fun givenAHiddenRecipient_whenIQueryAllContacts_thenIDoNotExpectHiddenToBeReturned() { + val hiddenRecipient = harness.others[0] + SignalDatabase.recipients.setProfileName(hiddenRecipient, ProfileName.fromParts("Hidden", "Person")) + SignalDatabase.recipients.markHidden(hiddenRecipient) + + val results = SignalDatabase.recipients.queryAllContacts("Hidden")!! + + assertEquals(0, results.count) + } + + @Test + fun givenAHiddenRecipient_whenIGetSignalContacts_thenIDoNotExpectHiddenToBeReturned() { + val hiddenRecipient = harness.others[0] + SignalDatabase.recipients.setProfileName(hiddenRecipient, ProfileName.fromParts("Hidden", "Person")) + SignalDatabase.recipients.markHidden(hiddenRecipient) + + val results: MutableList = SignalDatabase.recipients.getSignalContacts(false)?.use { + val ids = mutableListOf() + while (it.moveToNext()) { + ids.add(RecipientId.from(CursorUtil.requireLong(it, RecipientDatabase.ID))) + } + + ids + }!! + + assertNotEquals(0, results.size) + assertFalse(hiddenRecipient in results) + } + + @Test + fun givenAHiddenRecipient_whenIQuerySignalContacts_thenIDoNotExpectHiddenToBeReturned() { + val hiddenRecipient = harness.others[0] + SignalDatabase.recipients.setProfileName(hiddenRecipient, ProfileName.fromParts("Hidden", "Person")) + SignalDatabase.recipients.markHidden(hiddenRecipient) + + val results = SignalDatabase.recipients.querySignalContacts("Hidden", false)!! + + assertEquals(0, results.count) + } + + @Test + fun givenAHiddenRecipient_whenIQueryNonGroupContacts_thenIDoNotExpectHiddenToBeReturned() { + val hiddenRecipient = harness.others[0] + SignalDatabase.recipients.setProfileName(hiddenRecipient, ProfileName.fromParts("Hidden", "Person")) + SignalDatabase.recipients.markHidden(hiddenRecipient) + + val results = SignalDatabase.recipients.queryNonGroupContacts("Hidden", false)!! + + assertEquals(0, results.count) + } + + @Test + fun givenAHiddenRecipient_whenIGetNonGroupContacts_thenIDoNotExpectHiddenToBeReturned() { + val hiddenRecipient = harness.others[0] + SignalDatabase.recipients.setProfileName(hiddenRecipient, ProfileName.fromParts("Hidden", "Person")) + SignalDatabase.recipients.markHidden(hiddenRecipient) + + val results: MutableList = SignalDatabase.recipients.getNonGroupContacts(false)?.use { + val ids = mutableListOf() + while (it.moveToNext()) { + ids.add(RecipientId.from(CursorUtil.requireLong(it, RecipientDatabase.ID))) + } + + ids + }!! + + assertNotEquals(0, results.size) + assertFalse(hiddenRecipient in results) + } + @Test fun givenABlockedRecipient_whenIQueryAllContacts_thenIDoNotExpectBlockedToBeReturned() { val blockedRecipient = harness.others[0] diff --git a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java index 787d8d5afd..8e165fe7d1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ContactSelectionListFragment.java @@ -144,20 +144,20 @@ public final class ContactSelectionListFragment extends LoggingFragment private MappingAdapter contactChipAdapter; private ContactChipViewModel contactChipViewModel; private LifecycleDisposable lifecycleDisposable; - private HeaderActionProvider headerActionProvider; private TextView headerActionView; - @Nullable private FixedViewsAdapter headerAdapter; - @Nullable private FixedViewsAdapter footerAdapter; - @Nullable private ListCallback listCallback; - @Nullable private ScrollCallback scrollCallback; - private GlideRequests glideRequests; - private SelectionLimits selectionLimit = SelectionLimits.NO_LIMITS; - private Set currentSelection; - private boolean isMulti; - private boolean hideCount; - private boolean canSelectSelf; + @Nullable private FixedViewsAdapter headerAdapter; + @Nullable private FixedViewsAdapter footerAdapter; + @Nullable private ListCallback listCallback; + @Nullable private ScrollCallback scrollCallback; + @Nullable private OnItemLongClickListener onItemLongClickListener; + private GlideRequests glideRequests; + private SelectionLimits selectionLimit = SelectionLimits.NO_LIMITS; + private Set currentSelection; + private boolean isMulti; + private boolean hideCount; + private boolean canSelectSelf; @Override public void onAttach(@NonNull Context context) { @@ -206,6 +206,14 @@ public final class ContactSelectionListFragment extends LoggingFragment if (getParentFragment() instanceof HeaderActionProvider) { headerActionProvider = (HeaderActionProvider) getParentFragment(); } + + if (context instanceof OnItemLongClickListener) { + onItemLongClickListener = (OnItemLongClickListener) context; + } + + if (getParentFragment() instanceof OnItemLongClickListener) { + onItemLongClickListener = (OnItemLongClickListener) getParentFragment(); + } } @Override @@ -720,6 +728,15 @@ public final class ContactSelectionListFragment extends LoggingFragment } } } + + @Override + public boolean onItemLongClick(ContactSelectionListItem item) { + if (onItemLongClickListener != null) { + return onItemLongClickListener.onLongClick(item); + } else { + return false; + } + } } private boolean selectionHardLimitReached() { @@ -850,6 +867,10 @@ public final class ContactSelectionListFragment extends LoggingFragment @NonNull HeaderAction getHeaderAction(); } + public interface OnItemLongClickListener { + boolean onLongClick(ContactSelectionListItem contactSelectionListItem); + } + public interface AbstractContactsCursorLoaderFactoryProvider { @NonNull AbstractContactsCursorLoader.Factory get(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/NewConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/NewConversationActivity.java index 163295aaca..2aa8da7107 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/NewConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/NewConversationActivity.java @@ -20,11 +20,27 @@ import android.content.Intent; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; +import android.view.ViewGroup; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; import androidx.appcompat.app.AlertDialog; +import androidx.lifecycle.ViewModelProvider; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.snackbar.Snackbar; + +import org.signal.core.util.DimensionUnit; +import org.signal.core.util.concurrent.SimpleTask; import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.components.menu.ActionItem; +import org.thoughtcrime.securesms.components.menu.SignalContextMenu; +import org.thoughtcrime.securesms.contacts.ContactSelectionListItem; +import org.thoughtcrime.securesms.contacts.management.ContactsManagementRepository; +import org.thoughtcrime.securesms.contacts.management.ContactsManagementViewModel; import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery; import org.thoughtcrime.securesms.conversation.ConversationIntents; import org.thoughtcrime.securesms.database.SignalDatabase; @@ -33,32 +49,57 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; -import org.signal.core.util.concurrent.SimpleTask; +import org.thoughtcrime.securesms.util.CommunicationActions; +import org.thoughtcrime.securesms.util.FeatureFlags; +import org.thoughtcrime.securesms.util.LifecycleDisposable; import org.thoughtcrime.securesms.util.views.SimpleProgressDialog; import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * Activity container for starting a new conversation. * * @author Moxie Marlinspike - * */ public class NewConversationActivity extends ContactSelectionActivity - implements ContactSelectionListFragment.ListCallback + implements ContactSelectionListFragment.ListCallback, ContactSelectionListFragment.OnItemLongClickListener { @SuppressWarnings("unused") private static final String TAG = Log.tag(NewConversationActivity.class); + private ContactsManagementViewModel viewModel; + private ActivityResultLauncher contactLauncher; + + private final LifecycleDisposable disposables = new LifecycleDisposable(); + @Override public void onCreate(Bundle bundle, boolean ready) { super.onCreate(bundle, ready); assert getSupportActionBar() != null; getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setTitle(R.string.NewConversationActivity__new_message); + + disposables.bindTo(this); + + ContactsManagementRepository repository = new ContactsManagementRepository(this); + ContactsManagementViewModel.Factory factory = new ContactsManagementViewModel.Factory(repository); + + contactLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), activityResult -> { + if (activityResult.getResultCode() == RESULT_OK) { + handleManualRefresh(); + } + }); + + viewModel = new ViewModelProvider(this, factory).get(ContactsManagementViewModel.class); } @Override @@ -120,10 +161,18 @@ public class NewConversationActivity extends ContactSelectionActivity super.onOptionsItemSelected(item); switch (item.getItemId()) { - case android.R.id.home: super.onBackPressed(); return true; - case R.id.menu_refresh: handleManualRefresh(); return true; - case R.id.menu_new_group: handleCreateGroup(); return true; - case R.id.menu_invite: handleInvite(); return true; + case android.R.id.home: + super.onBackPressed(); + return true; + case R.id.menu_refresh: + handleManualRefresh(); + return true; + case R.id.menu_new_group: + handleCreateGroup(); + return true; + case R.id.menu_invite: + handleInvite(); + return true; } return false; @@ -162,4 +211,143 @@ public class NewConversationActivity extends ContactSelectionActivity handleCreateGroup(); finish(); } + + @Override + public boolean onLongClick(ContactSelectionListItem contactSelectionListItem) { + RecipientId recipientId = contactSelectionListItem.getRecipientId().orElse(null); + if (recipientId == null) { + return false; + } + + List actions = generateContextualActionsForRecipient(recipientId); + if (actions.isEmpty()) { + return false; + } + + new SignalContextMenu.Builder(contactSelectionListItem, (ViewGroup) contactSelectionListItem.getRootView()) + .preferredVerticalPosition(SignalContextMenu.VerticalPosition.BELOW) + .preferredHorizontalPosition(SignalContextMenu.HorizontalPosition.START) + .offsetX((int) DimensionUnit.DP.toPixels(12)) + .offsetY((int) DimensionUnit.DP.toPixels(12)) + .show(actions); + + return true; + } + + private @NonNull List generateContextualActionsForRecipient(@NonNull RecipientId recipientId) { + Recipient recipient = Recipient.resolved(recipientId); + + return Stream.of( + createMessageActionItem(recipient), + createAudioCallActionItem(recipient), + createVideoCallActionItem(recipient), + createRemoveActionItem(recipient), + createBlockActionItem(recipient) + ).filter(Objects::nonNull).collect(Collectors.toList()); + } + + private @NonNull ActionItem createMessageActionItem(@NonNull Recipient recipient) { + return new ActionItem( + R.drawable.ic_message_24, + getString(R.string.NewConversationActivity__message), + R.color.signal_colorOnSurface, + () -> startActivity(ConversationIntents.createBuilder(this, recipient.getId(), -1L).build()) + ); + } + + private @Nullable ActionItem createAudioCallActionItem(@NonNull Recipient recipient) { + if (recipient.isSelf() || recipient.isGroup()) { + return null; + } + + return new ActionItem( + R.drawable.ic_phone_right_24, + getString(R.string.NewConversationActivity__audio_call), + R.color.signal_colorOnSurface, + () -> CommunicationActions.startVoiceCall(this, recipient) + ); + } + + private @Nullable ActionItem createVideoCallActionItem(@NonNull Recipient recipient) { + if (recipient.isSelf() || recipient.isMmsGroup()) { + return null; + } + + return new ActionItem( + R.drawable.ic_video_call_24, + getString(R.string.NewConversationActivity__video_call), + R.color.signal_colorOnSurface, + () -> CommunicationActions.startVideoCall(this, recipient) + ); + } + + private @Nullable ActionItem createRemoveActionItem(@NonNull Recipient recipient) { + if (!FeatureFlags.hideContacts() || recipient.isSelf() || recipient.isGroup()) { + return null; + } + + return new ActionItem( + R.drawable.ic_minus_circle_20, // TODO [alex] -- correct asset + getString(R.string.NewConversationActivity__remove), + R.color.signal_colorOnSurface, + () -> { + if (recipient.isSystemContact()) { + displayIsInSystemContactsDialog(recipient); + } else { + displayRemovalDialog(recipient); + } + } + ); + } + + @SuppressWarnings("CodeBlock2Expr") + private @Nullable ActionItem createBlockActionItem(@NonNull Recipient recipient) { + if (recipient.isSelf()) { + return null; + } + + return new ActionItem( + R.drawable.ic_block_tinted_24, + getString(R.string.NewConversationActivity__block), + R.color.signal_colorError, + () -> BlockUnblockDialog.showBlockFor(this, + this.getLifecycle(), + recipient, + () -> { + disposables.add(viewModel.blockContact(recipient).subscribe(() -> { + displaySnackbar(R.string.NewConversationActivity__s_has_been_removed); + })); + }) + ); + } + + private void displayIsInSystemContactsDialog(@NonNull Recipient recipient) { + new MaterialAlertDialogBuilder(this) + .setTitle(getString(R.string.NewConversationActivity__unable_to_remove_s, recipient.getShortDisplayName(this))) + .setMessage(R.string.NewConversationActivity__this_person_is_saved_to_your) + .setPositiveButton(R.string.NewConversationActivity__view_contact, + (dialog, which) -> contactLauncher.launch(new Intent(Intent.ACTION_VIEW, recipient.getContactUri())) + ) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } + + private void displayRemovalDialog(@NonNull Recipient recipient) { + new MaterialAlertDialogBuilder(this) + .setTitle(getString(R.string.NewConversationActivity__remove_s, recipient.getShortDisplayName(this))) + .setMessage(R.string.NewConversationActivity__you_wont_see_this_person) + .setPositiveButton(R.string.NewConversationActivity__remove, + (dialog, which) -> { + disposables.add(viewModel.hideContact(recipient).subscribe(() -> { + displaySnackbar(R.string.NewConversationActivity__s_has_been_removed); + })); + } + ) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } + + private void displaySnackbar(@StringRes int message) { + Snackbar.make(findViewById(android.R.id.content), message, Snackbar.LENGTH_SHORT).show(); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java index 486e46c9ab..b106630380 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java @@ -130,6 +130,14 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter { if (clickListener != null) clickListener.onItemClick(getView()); }); + + itemView.setOnLongClickListener(v -> { + if (clickListener != null) { + return clickListener.onItemLongClick(getView()); + } else { + return false; + } + }); } public ContactSelectionListItem getView() { @@ -435,5 +443,6 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter create(modelClass: Class): T { + return modelClass.cast(ContactsManagementViewModel(repository)) as T + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt index 1b4ec99669..ed34f31812 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt @@ -184,6 +184,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : private const val IDENTITY_KEY = "identity_key" private const val NEEDS_PNI_SIGNATURE = "needs_pni_signature" private const val UNREGISTERED_TIMESTAMP = "unregistered_timestamp" + private const val HIDDEN = "hidden" @JvmField val CREATE_TABLE = @@ -243,7 +244,8 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : $PNI_COLUMN TEXT DEFAULT NULL, $DISTRIBUTION_LIST_ID INTEGER DEFAULT NULL, $NEEDS_PNI_SIGNATURE INTEGER DEFAULT 0, - $UNREGISTERED_TIMESTAMP INTEGER DEFAULT 0 + $UNREGISTERED_TIMESTAMP INTEGER DEFAULT 0, + $HIDDEN INTEGER DEFAULT 0 ) """.trimIndent() @@ -304,7 +306,8 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : CUSTOM_CHAT_COLORS_ID, BADGES, DISTRIBUTION_LIST_ID, - NEEDS_PNI_SIGNATURE + NEEDS_PNI_SIGNATURE, + HIDDEN ) private val ID_PROJECTION = arrayOf(ID) @@ -386,7 +389,8 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : $TABLE_NAME.$REGISTERED = ${RegisteredState.NOT_REGISTERED.id} AND $TABLE_NAME.$SEEN_INVITE_REMINDER < ${InsightsBannerTier.TIER_TWO.id} AND ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.HAS_SENT} AND - ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.DATE} > ? + ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.DATE} > ? AND + $TABLE_NAME.$HIDDEN = 0 ORDER BY ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.DATE} DESC LIMIT 50 """ } @@ -1820,8 +1824,8 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : fun getSimilarRecipientIds(recipient: Recipient): List { val projection = SqlUtil.buildArgs(ID, "COALESCE(NULLIF($SYSTEM_JOINED_NAME, ''), NULLIF($PROFILE_JOINED_NAME, '')) AS checked_name") - val where = "checked_name = ?" - val arguments = SqlUtil.buildArgs(recipient.profileName.toString()) + val where = "checked_name = ? AND $HIDDEN = ?" + val arguments = SqlUtil.buildArgs(recipient.profileName.toString(), 0) readableDatabase.query(TABLE_NAME, projection, where, arguments, null, null, null).use { cursor -> if (cursor == null || cursor.count == 0) { @@ -1881,10 +1885,31 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : } } + fun markHidden(id: RecipientId) { + val contentValues = contentValuesOf( + HIDDEN to 1, + PROFILE_SHARING to 0 + ) + + val updated = writableDatabase.update(TABLE_NAME, contentValues, "$ID_WHERE AND $GROUP_TYPE = ?", SqlUtil.buildArgs(id, GroupType.NONE.id)) > 0 + if (updated) { + rotateStorageId(id) + ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id) + StorageSyncHelper.scheduleSyncForDataChange() + } else { + Log.w(TAG, "Failed to hide recipient $id") + } + } + fun setProfileSharing(id: RecipientId, enabled: Boolean) { val contentValues = ContentValues(1).apply { put(PROFILE_SHARING, if (enabled) 1 else 0) } + + if (enabled) { + contentValues.put(HIDDEN, 0) + } + val profiledUpdated = update(id, contentValues) if (profiledUpdated && enabled) { @@ -2961,7 +2986,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : fun getRegistered(): List { val results: MutableList = LinkedList() - readableDatabase.query(TABLE_NAME, ID_PROJECTION, "$REGISTERED = ?", arrayOf("1"), null, null, null).use { cursor -> + readableDatabase.query(TABLE_NAME, ID_PROJECTION, "$REGISTERED = ? and $HIDDEN = ?", arrayOf("1", "0"), null, null, null).use { cursor -> while (cursor != null && cursor.moveToNext()) { results.add(RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ID)))) } @@ -3127,7 +3152,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : val query = SqlUtil.buildCaseInsensitiveGlobPattern(inputQuery) val selection = """ - $BLOCKED = ? AND + $BLOCKED = ? AND $HIDDEN = ? AND ( $SORT_NAME GLOB ? OR $USERNAME GLOB ? OR @@ -3135,7 +3160,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : $EMAIL GLOB ? ) """.trimIndent() - val args = SqlUtil.buildArgs("0", query, query, query, query) + val args = SqlUtil.buildArgs(0, 0, query, query, query, query) return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, null) } @@ -3323,9 +3348,11 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : if (Util.hasItems(idsToUpdate)) { val query = SqlUtil.buildSingleCollectionQuery(ID, idsToUpdate) - val values = ContentValues(1).apply { - put(PROFILE_SHARING, 1) - } + + val values = contentValuesOf( + PROFILE_SHARING to 1, + HIDDEN to 0 + ) writableDatabase.update(TABLE_NAME, values, query.where, query.whereArgs) @@ -3588,6 +3615,10 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : MENTION_SETTING to if (primaryRecord.mentionSetting != MentionSetting.ALWAYS_NOTIFY) primaryRecord.mentionSetting.id else secondaryRecord.mentionSetting.id ) + if (primaryRecord.profileSharing || secondaryRecord.profileSharing) { + uuidValues.put(HIDDEN, 0) + } + if (primaryRecord.profileKey != null) { updateProfileValuesForMerge(uuidValues, primaryRecord) } else if (secondaryRecord.profileKey != null) { @@ -3657,6 +3688,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : put(BLOCKED, if (contact.isBlocked) "1" else "0") put(MUTE_UNTIL, contact.muteUntil) put(STORAGE_SERVICE_ID, Base64.encodeBytes(contact.id.raw)) + put(HIDDEN, contact.isHidden) if (contact.hasUnknownFields()) { put(STORAGE_PROTO, Base64.encodeBytes(Objects.requireNonNull(contact.serializeUnknownFields()))) @@ -3908,7 +3940,8 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : extras = getExtras(cursor), hasGroupsInCommon = cursor.requireBoolean(GROUPS_IN_COMMON), badges = parseBadgeList(cursor.requireBlob(BADGES)), - needsPniSignature = cursor.requireBoolean(NEEDS_PNI_SIGNATURE) + needsPniSignature = cursor.requireBoolean(NEEDS_PNI_SIGNATURE), + isHidden = cursor.requireBoolean(HIDDEN) ) } @@ -4187,6 +4220,9 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : stringBuilder.append(FILTER_BLOCKED) args.add(0) + stringBuilder.append(FILTER_HIDDEN) + args.add(0) + if (excludeGroups) { stringBuilder.append(FILTER_GROUPS) } @@ -4204,6 +4240,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : const val FILTER_GROUPS = " AND $GROUP_ID IS NULL" const val FILTER_ID = " AND $ID != ?" const val FILTER_BLOCKED = " AND $BLOCKED = ?" + const val FILTER_HIDDEN = " AND $HIDDEN = ?" const val NON_SIGNAL_CONTACT = "$REGISTERED != ? AND $SYSTEM_CONTACT_URI NOT NULL AND ($PHONE NOT NULL OR $EMAIL NOT NULL)" const val QUERY_NON_SIGNAL_CONTACT = "$NON_SIGNAL_CONTACT AND ($PHONE GLOB ? OR $EMAIL GLOB ? OR $SYSTEM_JOINED_NAME GLOB ?)" const val SIGNAL_CONTACT = "$REGISTERED = ? AND (NULLIF($SYSTEM_JOINED_NAME, '') NOT NULL OR $PROFILE_SHARING = ?) AND ($SORT_NAME NOT NULL OR $USERNAME NOT NULL)" @@ -4217,7 +4254,8 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : */ internal object Capabilities { const val BIT_LENGTH = 2 -// const val GROUPS_V2 = 0 + + // const val GROUPS_V2 = 0 const val GROUPS_V1_MIGRATION = 1 const val SENDER_KEY = 2 const val ANNOUNCEMENT_GROUPS = 3 diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt index 7605152aa0..753887e22d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt @@ -11,13 +11,14 @@ import org.thoughtcrime.securesms.database.helpers.migration.V153_MyStoryMigrati import org.thoughtcrime.securesms.database.helpers.migration.V154_PniSignaturesMigration import org.thoughtcrime.securesms.database.helpers.migration.V155_SmsExporterMigration import org.thoughtcrime.securesms.database.helpers.migration.V156_RecipientUnregisteredTimestampMigration +import org.thoughtcrime.securesms.database.helpers.migration.V157_RecipeintHiddenMigration /** * Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness. */ object SignalDatabaseMigrations { - const val DATABASE_VERSION = 156 + const val DATABASE_VERSION = 157 @JvmStatic fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { @@ -52,6 +53,10 @@ object SignalDatabaseMigrations { if (oldVersion < 156) { V156_RecipientUnregisteredTimestampMigration.migrate(context, db, oldVersion, newVersion) } + + if (oldVersion < 157) { + V157_RecipeintHiddenMigration.migrate(context, db, oldVersion, newVersion) + } } @JvmStatic diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V157_RecipeintHiddenMigration.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V157_RecipeintHiddenMigration.kt new file mode 100644 index 0000000000..7dfec71f62 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V157_RecipeintHiddenMigration.kt @@ -0,0 +1,10 @@ +package org.thoughtcrime.securesms.database.helpers.migration + +import android.app.Application +import net.zetetic.database.sqlcipher.SQLiteDatabase + +object V157_RecipeintHiddenMigration : SignalDatabaseMigration { + override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + db.execSQL("ALTER TABLE recipient ADD COLUMN hidden INTEGER DEFAULT 0") + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt index 7f944317e8..80a23c8497 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt @@ -85,7 +85,8 @@ data class RecipientRecord( val hasGroupsInCommon: Boolean, val badges: List, @get:JvmName("needsPniSignature") - val needsPniSignature: Boolean + val needsPniSignature: Boolean, + val isHidden: Boolean ) { fun getDefaultSubscriptionId(): Optional { diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/ContactRecordProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/storage/ContactRecordProcessor.java index 050c1b1bd9..95f04f6361 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/ContactRecordProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/ContactRecordProcessor.java @@ -197,8 +197,9 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor