Add ability to hide contacts behind a feature flag.

This commit is contained in:
Alex Hart
2022-09-27 16:40:27 -03:00
committed by Cody Henthorne
parent a8a773db43
commit 04eeb434c9
19 changed files with 511 additions and 45 deletions

View File

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

View File

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

View File

@@ -130,6 +130,14 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
itemView.setOnClickListener(v -> {
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<ViewH
public interface ItemClickListener {
void onItemClick(ContactSelectionListItem item);
boolean onItemLongClick(ContactSelectionListItem item);
}
}

View File

@@ -0,0 +1,37 @@
package org.thoughtcrime.securesms.contacts.management
import android.content.Context
import androidx.annotation.CheckResult
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.schedulers.Schedulers
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientUtil
class ContactsManagementRepository(context: Context) {
private val context = context.applicationContext
@CheckResult
fun blockContact(recipient: Recipient): Completable {
return Completable.fromAction {
if (recipient.isDistributionList) {
error("Blocking a distribution list makes no sense")
} else if (recipient.isGroup) {
RecipientUtil.block(context, recipient)
} else {
RecipientUtil.blockNonGroup(context, recipient)
}
}.subscribeOn(Schedulers.io())
}
@CheckResult
fun hideContact(recipient: Recipient): Completable {
return Completable.fromAction {
if (recipient.isGroup || recipient.isDistributionList || recipient.isSelf) {
error("Cannot hide groups, self, or distribution lists.")
}
SignalDatabase.recipients.markHidden(recipient.id)
}
}
}

View File

@@ -0,0 +1,27 @@
package org.thoughtcrime.securesms.contacts.management
import androidx.annotation.CheckResult
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Completable
import org.thoughtcrime.securesms.recipients.Recipient
class ContactsManagementViewModel(private val repository: ContactsManagementRepository) : ViewModel() {
@CheckResult
fun hideContact(recipient: Recipient): Completable {
return repository.hideContact(recipient).observeOn(AndroidSchedulers.mainThread())
}
@CheckResult
fun blockContact(recipient: Recipient): Completable {
return repository.blockContact(recipient).observeOn(AndroidSchedulers.mainThread())
}
class Factory(private val repository: ContactsManagementRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.cast(ContactsManagementViewModel(repository)) as T
}
}
}

View File

@@ -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<RecipientId> {
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<RecipientId> {
val results: MutableList<RecipientId> = 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

View File

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

View File

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

View File

@@ -85,7 +85,8 @@ data class RecipientRecord(
val hasGroupsInCommon: Boolean,
val badges: List<Badge>,
@get:JvmName("needsPniSignature")
val needsPniSignature: Boolean
val needsPniSignature: Boolean,
val isHidden: Boolean
) {
fun getDefaultSubscriptionId(): Optional<Int> {

View File

@@ -197,8 +197,9 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
long muteUntil = remote.getMuteUntil();
boolean hideStory = remote.shouldHideStory();
long unregisteredTimestamp = remote.getUnregisteredTimestamp();
boolean matchesRemote = doParamsMatch(remote, unknownFields, serviceId, pni, e164, givenName, familyName, profileKey, username, identityState, identityKey, blocked, profileSharing, archived, forcedUnread, muteUntil, hideStory, unregisteredTimestamp);
boolean matchesLocal = doParamsMatch(local, unknownFields, serviceId, pni, e164, givenName, familyName, profileKey, username, identityState, identityKey, blocked, profileSharing, archived, forcedUnread, muteUntil, hideStory, unregisteredTimestamp);
boolean hidden = remote.isHidden();
boolean matchesRemote = doParamsMatch(remote, unknownFields, serviceId, pni, e164, givenName, familyName, profileKey, username, identityState, identityKey, blocked, profileSharing, archived, forcedUnread, muteUntil, hideStory, unregisteredTimestamp, hidden);
boolean matchesLocal = doParamsMatch(local, unknownFields, serviceId, pni, e164, givenName, familyName, profileKey, username, identityState, identityKey, blocked, profileSharing, archived, forcedUnread, muteUntil, hideStory, unregisteredTimestamp, hidden);
if (matchesRemote) {
return remote;
@@ -221,6 +222,7 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
.setMuteUntil(muteUntil)
.setHideStory(hideStory)
.setUnregisteredTimestamp(unregisteredTimestamp)
.setHidden(hidden)
.build();
}
}
@@ -264,7 +266,8 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
boolean forcedUnread,
long muteUntil,
boolean hideStory,
long unregisteredTimestamp)
long unregisteredTimestamp,
boolean hidden)
{
return Arrays.equals(contact.serializeUnknownFields(), unknownFields) &&
Objects.equals(contact.getServiceId(), serviceId) &&
@@ -282,6 +285,7 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor<Signal
contact.isForcedUnread() == forcedUnread &&
contact.getMuteUntil() == muteUntil &&
contact.shouldHideStory() == hideStory &&
contact.getUnregisteredTimestamp() == unregisteredTimestamp;
contact.getUnregisteredTimestamp() == unregisteredTimestamp &&
contact.isHidden() == hidden;
}
}

View File

@@ -127,6 +127,7 @@ public final class StorageSyncModels {
.setMuteUntil(recipient.getMuteUntil())
.setHideStory(hideStory)
.setUnregisteredTimestamp(recipient.getSyncExtras().getUnregisteredTimestamp())
.setHidden(recipient.isHidden())
.build();
}

View File

@@ -106,6 +106,7 @@ public final class FeatureFlags {
private static final String SMS_EXPORTER = "android.sms.exporter";
private static final String CDS_V2_COMPAT = "android.cdsV2Compat.3";
public static final String STORIES_LOCALE = "android.stories.locale";
private static final String HIDE_CONTACTS = "android.hide.contacts";
/**
* We will only store remote values for flags in this set. If you want a flag to be controllable
@@ -163,7 +164,8 @@ public final class FeatureFlags {
CDS_V2_LOAD_TEST,
SMS_EXPORTER,
CDS_V2_COMPAT,
STORIES_LOCALE
STORIES_LOCALE,
HIDE_CONTACTS
);
@VisibleForTesting
@@ -588,6 +590,16 @@ public final class FeatureFlags {
return getBoolean(CDS_V2_COMPAT, false);
}
/**
* Whether or not users can hide contacts.
*
* WARNING: This feature is intended to be enabled in tandem with other clients, as it modifies contact records.
* Here be dragons.
*/
public static boolean hideContacts() {
return getBoolean(HIDE_CONTACTS, false);
}
/** Only for rendering debug info. */
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES);