Add support for remote feature flags.

master
Greyson Parrelli 2019-12-19 17:41:21 -05:00
parent b8602ee004
commit 55e9f8722f
33 changed files with 403 additions and 58 deletions

View File

@ -49,6 +49,7 @@ import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.AndroidLogger;
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
import org.thoughtcrime.securesms.logging.Log;
@ -71,6 +72,7 @@ import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
import org.thoughtcrime.securesms.stickers.BlessedPacks;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
@ -133,6 +135,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
initializeBlobProvider();
initializeCleanup();
initializeCameraX();
FeatureFlags.init();
NotificationChannels.create(this);
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);

View File

@ -42,7 +42,6 @@ import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil;

View File

@ -113,7 +113,7 @@ public class GroupMembersDialog extends AsyncTask<Void, Void, List<Recipient>> {
}
private String getRecipientName(Recipient recipient) {
if (FeatureFlags.PROFILE_DISPLAY) return recipient.getDisplayName(context);
if (FeatureFlags.profileDisplay()) return recipient.getDisplayName(context);
String name = recipient.toShortString(context);

View File

@ -434,7 +434,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
colorPreference.setColors(MaterialColors.CONVERSATION_PALETTE.asConversationColorArray(requireActivity()));
colorPreference.setColor(recipient.getColor().toActionBarColor(requireActivity()));
if (FeatureFlags.PROFILE_DISPLAY) {
if (FeatureFlags.profileDisplay()) {
aboutPreference.setTitle(recipient.getDisplayName(requireContext()));
aboutPreference.setSummary(recipient.resolve().getE164().or(""));
} else {

View File

@ -275,7 +275,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
byte[] localId;
byte[] remoteId;
if (FeatureFlags.UUIDS && recipient.resolve().getUuid().isPresent()) {
if (FeatureFlags.uuids() && recipient.resolve().getUuid().isPresent()) {
Log.i(TAG, "Using UUID (version 2).");
version = 2;
localId = UuidUtil.toByteArray(TextSecurePreferences.getLocalUuid(requireContext()));

View File

@ -61,7 +61,7 @@ public class FromTextView extends EmojiTextView {
if (recipient.isLocalNumber()) {
builder.append(getContext().getString(R.string.note_to_self));
} else if (!FeatureFlags.PROFILE_DISPLAY && recipient.getName(getContext()) == null && !recipient.getProfileName().isEmpty()) {
} else if (!FeatureFlags.profileDisplay() && recipient.getName(getContext()) == null && !recipient.getProfileName().isEmpty()) {
SpannableString profileName = new SpannableString(" (~" + recipient.getProfileName().toString() + ") ");
profileName.setSpan(new CenterAlignedRelativeSizeSpan(0.75f), 0, profileName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
profileName.setSpan(new TypefaceSpan("sans-serif-light"), 0, profileName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

View File

@ -370,7 +370,7 @@ public class WebRtcCallScreen extends FrameLayout implements RecipientForeverObs
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(this.photo);
if (FeatureFlags.PROFILE_DISPLAY) {
if (FeatureFlags.profileDisplay()) {
this.name.setText(recipient.getDisplayName(getContext()));
if (recipient.getE164().isPresent()) {

View File

@ -145,15 +145,15 @@ public class ContactsCursorLoader extends CursorLoader {
cursorList.addAll(getContactsCursors());
}
if (FeatureFlags.USERNAMES && NumberUtil.isVisuallyValidNumberOrEmail(filter)) {
if (FeatureFlags.usernames() && NumberUtil.isVisuallyValidNumberOrEmail(filter)) {
cursorList.add(getPhoneNumberSearchHeaderCursor());
cursorList.add(getNewNumberCursor());
} else if (!FeatureFlags.USERNAMES && NumberUtil.isValidSmsOrEmail(filter)){
} else if (!FeatureFlags.usernames() && NumberUtil.isValidSmsOrEmail(filter)){
cursorList.add(getContactsHeaderCursor());
cursorList.add(getNewNumberCursor());
}
if (FeatureFlags.USERNAMES && UsernameUtil.isValidUsernameForSearch(filter)) {
if (FeatureFlags.usernames() && UsernameUtil.isValidUsernameForSearch(filter)) {
cursorList.add(getUsernameSearchHeaderCursor());
cursorList.add(getUsernameSearchCursor());
}

View File

@ -17,14 +17,14 @@ public class DirectoryHelper {
@WorkerThread
public static void refreshDirectory(@NonNull Context context, boolean notifyOfNewUsers) throws IOException {
if (FeatureFlags.UUIDS) {
if (FeatureFlags.uuids()) {
// TODO [greyson] Create a DirectoryHelperV2 when appropriate.
DirectoryHelperV1.refreshDirectory(context, notifyOfNewUsers);
} else {
DirectoryHelperV1.refreshDirectory(context, notifyOfNewUsers);
}
if (FeatureFlags.STORAGE_SERVICE) {
if (FeatureFlags.storageService()) {
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
}
}
@ -34,14 +34,14 @@ public class DirectoryHelper {
RegisteredState originalRegisteredState = recipient.resolve().getRegistered();
RegisteredState newRegisteredState = null;
if (FeatureFlags.UUIDS) {
if (FeatureFlags.uuids()) {
// TODO [greyson] Create a DirectoryHelperV2 when appropriate.
newRegisteredState = DirectoryHelperV1.refreshDirectoryFor(context, recipient, notifyOfNewUsers);
} else {
newRegisteredState = DirectoryHelperV1.refreshDirectoryFor(context, recipient, notifyOfNewUsers);
}
if (FeatureFlags.STORAGE_SERVICE && newRegisteredState != originalRegisteredState) {
if (FeatureFlags.storageService() && newRegisteredState != originalRegisteredState) {
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
}

View File

@ -2014,7 +2014,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
private void setGroupShareProfileReminder(@NonNull Recipient recipient) {
if (!FeatureFlags.MESSAGE_REQUESTS && recipient.isPushGroup() && !recipient.isProfileSharing()) {
if (!FeatureFlags.messageRequests() && recipient.isPushGroup() && !recipient.isProfileSharing()) {
groupShareProfileView.get().setRecipient(recipient);
groupShareProfileView.get().setVisibility(View.VISIBLE);
} else if (groupShareProfileView.resolved()) {

View File

@ -713,7 +713,7 @@ public class ConversationFragment extends Fragment
setLastSeen(loader.getLastSeen());
}
if (FeatureFlags.MESSAGE_REQUESTS) {
if (FeatureFlags.messageRequests()) {
if (!loader.hasSent() && !recipient.get().isSystemContact() && !recipient.get().isProfileSharing() && !recipient.get().isBlocked() && recipient.get().isRegistered()) {
listener.onMessageRequest();
} else {
@ -994,8 +994,8 @@ public class ConversationFragment extends Fragment
if (actionMode != null) return;
if (FeatureFlags.REACTION_SENDING &&
messageRecord.isSecure() &&
if (FeatureFlags.reactionSending() &&
messageRecord.isSecure() &&
((ConversationAdapter) list.getAdapter()).getSelectedItems().isEmpty())
{
isReacting = true;

View File

@ -960,7 +960,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
private void setGroupMessageStatus(MessageRecord messageRecord, Recipient recipient) {
if (groupThread && !messageRecord.isOutgoing()) {
if (FeatureFlags.PROFILE_DISPLAY) {
if (FeatureFlags.profileDisplay()) {
this.groupSender.setText(recipient.getDisplayName(getContext()));
this.groupSenderProfileName.setVisibility(View.GONE);
} else {

View File

@ -126,7 +126,7 @@ public class ConversationTitleView extends RelativeLayout {
}
private void setRecipientTitle(Recipient recipient) {
if (FeatureFlags.PROFILE_DISPLAY) {
if (FeatureFlags.profileDisplay()) {
if (recipient.isGroup()) setGroupRecipientTitle(recipient);
else if (recipient.isLocalNumber()) setSelfTitle();
else setIndividualRecipientTitle(recipient);
@ -166,7 +166,7 @@ public class ConversationTitleView extends RelativeLayout {
private void setGroupRecipientTitle(Recipient recipient) {
String localNumber = TextSecurePreferences.getLocalNumber(getContext());
if (FeatureFlags.PROFILE_DISPLAY) {
if (FeatureFlags.profileDisplay()) {
this.title.setText(recipient.getDisplayName(getContext()));
} else {
this.title.setText(recipient.getName(getContext()));

View File

@ -1209,7 +1209,7 @@ public class RecipientDatabase extends Database {
}
void markDirty(@NonNull RecipientId recipientId, @NonNull DirtyState dirtyState) {
if (!FeatureFlags.STORAGE_SERVICE) return;
if (!FeatureFlags.storageService()) return;
ContentValues contentValues = new ContentValues(1);
contentValues.put(DIRTY, dirtyState.getId());

View File

@ -69,7 +69,7 @@ public class ApplicationDependencies {
}
public static synchronized @NonNull KeyBackupService getKeyBackupService() {
if (!FeatureFlags.KBS) throw new AssertionError();
if (!FeatureFlags.kbs()) throw new AssertionError();
return getSignalServiceAccountManager().getKeyBackupService(IasKeyStore.getIasKeyStore(application),
BuildConfig.KEY_BACKUP_ENCLAVE_NAME,
BuildConfig.KEY_BACKUP_MRENCLAVE,

View File

@ -75,6 +75,7 @@ public final class JobManagerFactories {
put(RefreshAttributesJob.KEY, new RefreshAttributesJob.Factory());
put(RefreshOwnProfileJob.KEY, new RefreshOwnProfileJob.Factory());
put(RefreshPreKeysJob.KEY, new RefreshPreKeysJob.Factory());
put(RemoteConfigRefreshJob.KEY, new RemoteConfigRefreshJob.Factory());
put(RequestGroupInfoJob.KEY, new RequestGroupInfoJob.Factory());
put(RetrieveProfileAvatarJob.KEY, new RetrieveProfileAvatarJob.Factory());
put(RetrieveProfileJob.KEY, new RetrieveProfileJob.Factory());

View File

@ -0,0 +1,66 @@
package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class RemoteConfigRefreshJob extends BaseJob {
public static final String KEY = "RemoteConfigRefreshJob";
public RemoteConfigRefreshJob() {
this(new Job.Parameters.Builder()
.setQueue("RemoteConfigRefreshJob")
.setMaxInstances(1)
.setMaxAttempts(Parameters.UNLIMITED)
.setLifespan(TimeUnit.DAYS.toMillis(1))
.build());
}
private RemoteConfigRefreshJob(@NonNull Parameters parameters) {
super(parameters);
}
@Override
public @NonNull Data serialize() {
return Data.EMPTY;
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
protected void onRun() throws Exception {
Map<String, Boolean> config = ApplicationDependencies.getSignalServiceAccountManager().getRemoteConfig();
FeatureFlags.updateDiskCache(config);
SignalStore.setRemoteConfigLastFetchTime(System.currentTimeMillis());
}
@Override
protected boolean onShouldRetry(@NonNull Exception e) {
return e instanceof PushNetworkException;
}
@Override
public void onFailure() {
}
public static final class Factory implements Job.Factory<RemoteConfigRefreshJob> {
@Override
public @NonNull RemoteConfigRefreshJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new RemoteConfigRefreshJob(parameters);
}
}
}

View File

@ -102,7 +102,7 @@ public class RetrieveProfileJob extends BaseJob {
setProfileName(recipient, profile.getName());
setProfileAvatar(recipient, profile.getAvatar());
if (FeatureFlags.USERNAMES) setUsername(recipient, profile.getUsername());
if (FeatureFlags.usernames()) setUsername(recipient, profile.getUsername());
setProfileCapabilities(recipient, profile.getCapabilities());
setIdentityKey(recipient, profile.getIdentityKey());
setUnidentifiedAccessMode(recipient, profile.getUnidentifiedAccess(), profile.isUnrestrictedUnidentifiedAccess());

View File

@ -67,7 +67,7 @@ public class StorageForcePushJob extends BaseJob {
@Override
protected void onRun() throws IOException, RetryLaterException {
if (!FeatureFlags.STORAGE_SERVICE) throw new AssertionError();
if (!FeatureFlags.storageService()) throw new AssertionError();
MasterKey kbsMasterKey = SignalStore.kbsValues().getPinBackedMasterKey();

View File

@ -79,7 +79,7 @@ public class StorageSyncJob extends BaseJob {
@Override
protected void onRun() throws IOException, RetryLaterException {
if (!FeatureFlags.STORAGE_SERVICE) throw new AssertionError();
if (!FeatureFlags.storageService()) throw new AssertionError();
try {
boolean needsMultiDeviceSync = performSync();

View File

@ -1,5 +1,7 @@
package org.thoughtcrime.securesms.keyvalue;
import android.content.Context;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
@ -10,12 +12,32 @@ import org.thoughtcrime.securesms.logging.SignalUncaughtExceptionHandler;
*/
public final class SignalStore {
private static final String REMOTE_CONFIG = "remote_config";
private static final String REMOTE_CONFIG_LAST_FETCH_TIME = "remote_config_last_fetch_time";
private SignalStore() {}
public static KbsValues kbsValues() {
return new KbsValues(getStore());
}
public static String getRemoteConfig() {
return getStore().getString(REMOTE_CONFIG, null);
}
public static void setRemoteConfig(String value) {
putString(REMOTE_CONFIG, value);
}
public static long getRemoteConfigLastFetchTime() {
return getStore().getLong(REMOTE_CONFIG_LAST_FETCH_TIME, 0);
}
public static void setRemoteConfigLastFetchTime(long time) {
putLong(REMOTE_CONFIG_LAST_FETCH_TIME, time);
}
/**
* Ensures any pending writes are finished. Only intended to be called by
* {@link SignalUncaughtExceptionHandler}.

View File

@ -126,7 +126,7 @@ public final class RegistrationLockDialog {
dialog.dismiss();
RegistrationLockReminders.scheduleReminder(context, true);
if (FeatureFlags.KBS) {
if (FeatureFlags.kbs()) {
Log.i(TAG, "Pin V1 successfully remembered, scheduling a migration to V2");
ApplicationDependencies.getJobManager().add(new RegistrationPinV2MigrationJob());
}
@ -201,7 +201,7 @@ public final class RegistrationLockDialog {
@Override
protected Boolean doInBackground(Void... voids) {
try {
if (!FeatureFlags.KBS) {
if (!FeatureFlags.kbs()) {
Log.i(TAG, "Setting V1 pin");
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
accountManager.setPin(pinValue);
@ -282,7 +282,7 @@ public final class RegistrationLockDialog {
@Override
protected Boolean doInBackground(Void... voids) {
try {
if (!FeatureFlags.KBS) {
if (!FeatureFlags.kbs()) {
Log.i(TAG, "Removing v1 registration lock pin from server");
ApplicationDependencies.getSignalServiceAccountManager().removeV1Pin();
} else {

View File

@ -52,6 +52,8 @@ import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.annimon.stream.Stream;
import org.json.JSONException;
import org.json.JSONObject;
import org.thoughtcrime.securesms.ApplicationContext;
@ -61,6 +63,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.logsubmit.util.Scrubber;
import org.thoughtcrime.securesms.util.BucketInfo;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.FrameRateTracker;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
@ -111,6 +114,7 @@ public class SubmitLogFragment extends Fragment {
private static final String HEADER_POWER = "========== POWER ==========";
private static final String HEADER_THREADS = "===== BLOCKED THREADS =====";
private static final String HEADER_PERMISSIONS = "======= PERMISSIONS =======";
private static final String HEADER_FLAGS = "====== FEATURE FLAGS ======";
private static final String HEADER_LOGCAT = "========== LOGCAT =========";
private static final String HEADER_LOGGER = "========== LOGGER =========";
@ -411,6 +415,11 @@ public class SubmitLogFragment extends Fragment {
.append(buildBlockedThreads())
.append("\n\n\n");
stringBuilder.append(HEADER_FLAGS)
.append("\n\n")
.append(buildFlags())
.append("\n\n\n");
stringBuilder.append(HEADER_PERMISSIONS)
.append("\n\n")
.append(buildPermissions(context))
@ -628,6 +637,28 @@ public class SubmitLogFragment extends Fragment {
return out;
}
private static CharSequence buildFlags() {
StringBuilder out = new StringBuilder();
Map<String, Boolean> remote = FeatureFlags.getRemoteValues();
Map<String, Boolean> forced = FeatureFlags.getForcedValues();
int remoteLength = Stream.of(remote.keySet()).map(String::length).max(Integer::compareTo).orElse(0);
int forcedLength = Stream.of(forced.keySet()).map(String::length).max(Integer::compareTo).orElse(0);
out.append("-- Remote\n");
for (Map.Entry<String, Boolean> entry : remote.entrySet()) {
out.append(Util.rightPad(entry.getKey(), remoteLength)).append(": ").append(entry.getValue()).append("\n");
}
out.append("\n");
out.append("-- Forced\n");
for (Map.Entry<String, Boolean> entry : forced.entrySet()) {
out.append(Util.rightPad(entry.getKey(), forcedLength)).append(": ").append(entry.getValue()).append("\n");
}
return out;
}
private static Iterable<String> getSupportedAbis() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return Arrays.asList(Build.SUPPORTED_ABIS);

View File

@ -55,7 +55,7 @@ public final class RegistrationPinV2MigrationJob extends BaseJob {
@Override
protected void onRun() throws IOException, UnauthenticatedResponseException, KeyBackupServicePinException {
if (!FeatureFlags.KBS) {
if (!FeatureFlags.kbs()) {
Log.i(TAG, "Not migrating pin to KBS");
return;
}

View File

@ -203,7 +203,7 @@ public class EditProfileFragment extends Fragment {
this.usernameLabel = view.findViewById(R.id.profile_overview_username_label);
this.nextIntent = getArguments().getParcelable(NEXT_INTENT);
if (FeatureFlags.USERNAMES && getArguments().getBoolean(DISPLAY_USERNAME, false)) {
if (FeatureFlags.usernames() && getArguments().getBoolean(DISPLAY_USERNAME, false)) {
username.setVisibility(View.VISIBLE);
usernameEditButton.setVisibility(View.VISIBLE);
usernameLabel.setVisibility(View.VISIBLE);

View File

@ -167,7 +167,7 @@ public class Recipient {
} else if (!recipient.isRegistered()) {
db.markRegistered(recipient.getId());
if (FeatureFlags.UUIDS) {
if (FeatureFlags.uuids()) {
Log.i(TAG, "No UUID! Scheduling a fetch.");
ApplicationDependencies.getJobManager().add(new DirectoryRefreshJob(recipient, false));
}
@ -175,7 +175,7 @@ public class Recipient {
return resolved(recipient.getId());
} else if (uuid != null) {
if (FeatureFlags.UUIDS || e164 != null) {
if (FeatureFlags.uuids() || e164 != null) {
RecipientId id = db.getOrInsertFromUuid(uuid);
db.markRegistered(id, uuid);
@ -193,7 +193,7 @@ public class Recipient {
if (!recipient.isRegistered()) {
db.markRegistered(recipient.getId());
if (FeatureFlags.UUIDS) {
if (FeatureFlags.uuids()) {
Log.i(TAG, "No UUID! Scheduling a fetch.");
ApplicationDependencies.getJobManager().add(new DirectoryRefreshJob(recipient, false));
}
@ -247,7 +247,7 @@ public class Recipient {
if (UuidUtil.isUuid(identifier)) {
UUID uuid = UuidUtil.parseOrThrow(identifier);
if (FeatureFlags.UUIDS) {
if (FeatureFlags.uuids()) {
id = db.getOrInsertFromUuid(uuid);
} else {
Optional<RecipientId> possibleId = db.getByUuid(uuid);
@ -383,8 +383,8 @@ public class Recipient {
*/
@Deprecated
public @NonNull String toShortString(@NonNull Context context) {
if (FeatureFlags.PROFILE_DISPLAY) return getDisplayName(context);
else return Optional.fromNullable(getName(context)).or(getSmsAddress()).or("");
if (FeatureFlags.profileDisplay()) return getDisplayName(context);
else return Optional.fromNullable(getName(context)).or(getSmsAddress()).or("");
}
public @NonNull String getDisplayName(@NonNull Context context) {
@ -408,7 +408,7 @@ public class Recipient {
}
public @NonNull Optional<String> getUsername() {
if (FeatureFlags.USERNAMES) {
if (FeatureFlags.usernames()) {
return Optional.fromNullable(username);
} else {
return Optional.absent();
@ -529,7 +529,7 @@ public class Recipient {
}
public @Nullable String getCustomLabel() {
if (FeatureFlags.PROFILE_DISPLAY) throw new AssertionError("This method should never be called if PROFILE_DISPLAY is enabled.");
if (FeatureFlags.profileDisplay()) throw new AssertionError("This method should never be called if PROFILE_DISPLAY is enabled.");
return customLabel;
}
@ -655,10 +655,10 @@ public class Recipient {
* @return True if this recipient can support receiving UUID-only messages, otherwise false.
*/
public boolean isUuidSupported() {
if (FeatureFlags.USERNAMES) {
if (FeatureFlags.usernames()) {
return true;
} else {
return FeatureFlags.UUIDS && uuidSupported;
return FeatureFlags.uuids() && uuidSupported;
}
}

View File

@ -44,7 +44,7 @@ public class RecipientUtil {
throw new AssertionError(recipient.getId() + " - No UUID or phone number!");
}
if (FeatureFlags.UUIDS && !recipient.getUuid().isPresent()) {
if (FeatureFlags.uuids() && !recipient.getUuid().isPresent()) {
Log.i(TAG, recipient.getId() + " is missing a UUID...");
try {
RegisteredState state = DirectoryHelper.refreshDirectoryFor(context, recipient, false);
@ -110,7 +110,7 @@ public class RecipientUtil {
@WorkerThread
public static boolean isRecipientMessageRequestAccepted(@NonNull Context context, @Nullable Recipient recipient) {
if (recipient == null || !FeatureFlags.MESSAGE_REQUESTS) return true;
if (recipient == null || !FeatureFlags.messageRequests()) return true;
Recipient resolved = recipient.resolve();

View File

@ -55,7 +55,7 @@ public final class CodeVerificationRequest {
static TokenResponse getToken(@Nullable String basicStorageCredentials) throws IOException {
if (basicStorageCredentials == null) return null;
if (!FeatureFlags.KBS) return null;
if (!FeatureFlags.kbs()) return null;
return ApplicationDependencies.getKeyBackupService().getToken(basicStorageCredentials);
}
@ -214,7 +214,7 @@ public final class CodeVerificationRequest {
//noinspection deprecation Only acceptable place to write the old pin enabled state.
TextSecurePreferences.setV1RegistrationLockEnabled(context, pin != null);
if (pin != null) {
if (FeatureFlags.KBS) {
if (FeatureFlags.kbs()) {
Log.i(TAG, "Pin V1 successfully entered during registration, scheduling a migration to Pin V2");
ApplicationDependencies.getJobManager().add(new RegistrationPinV2MigrationJob());
}
@ -230,7 +230,7 @@ public final class CodeVerificationRequest {
}
private static void repostPinToResetTries(@NonNull Context context, @Nullable String pin, @NonNull RegistrationLockData kbsData) {
if (!FeatureFlags.KBS) return;
if (!FeatureFlags.kbs()) return;
if (pin == null) return;
@ -264,7 +264,7 @@ public final class CodeVerificationRequest {
return null;
}
if (!FeatureFlags.KBS) {
if (!FeatureFlags.kbs()) {
Log.w(TAG, "User appears to have a KBS pin, but this build has KBS off.");
return null;
}

View File

@ -1,28 +1,192 @@
package org.thoughtcrime.securesms.util;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import org.json.JSONException;
import org.json.JSONObject;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.RemoteConfigRefreshJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
/**
* A location for constants that allows us to turn features on and off during development.
* After a feature has been launched, the flag should be removed.
* A location for flags that can be set locally and remotely. These flags can guard features that
* are not yet ready to be activated.
*
* When creating a new flag:
* - Create a new string constant using {@link #generateKey(String)})
* - Add a method to retrieve the value using {@link #getValue(String, boolean)}. You can also add
* other checks here, like requiring other flags.
* - If you would like to force a value for testing, place an entry in {@link #FORCED_VALUES}. When
* launching a feature that is planned to be updated via a remote config, do not forget to
* remove the entry!
*/
public class FeatureFlags {
public final class FeatureFlags {
private static final String TAG = Log.tag(FeatureFlags.class);
private static final String PREFIX = "android.";
private static final long FETCH_INTERVAL = TimeUnit.HOURS.toMillis(2);
private static final String UUIDS = generateKey("uuids");
private static final String PROFILE_DISPLAY = generateKey("profileDisplay");
private static final String MESSAGE_REQUESTS = generateKey("messageRequests");
private static final String USERNAMES = generateKey("usernames");
private static final String KBS = generateKey("kbs");
private static final String STORAGE_SERVICE = generateKey("storageService");
private static final String REACTION_SENDING = generateKey("reactionSending");
/**
* Values in this map will take precedence over any value. If you do not wish to have any sort of
* override, simply don't put a value in this map. You should never commit additions to this map
* for flags that you plan on updating remotely.
*/
private static final Map<String, Boolean> FORCED_VALUES = new HashMap<String, Boolean>() {{
put(UUIDS, false);
put(PROFILE_DISPLAY, false);
put(MESSAGE_REQUESTS, false);
put(USERNAMES, false);
put(KBS, false);
put(STORAGE_SERVICE, false);
put(REACTION_SENDING, false);
}};
private static final Map<String, Boolean> REMOTE_VALUES = new HashMap<>();
private FeatureFlags() {}
public static void init() {
scheduleFetchIfNecessary();
REMOTE_VALUES.putAll(parseStoredConfig());
}
public static void updateDiskCache(@NonNull Map<String, Boolean> config) {
try {
JSONObject filtered = new JSONObject();
for (Map.Entry<String, Boolean> entry : config.entrySet()) {
if (entry.getKey().startsWith(PREFIX)) {
filtered.put(entry.getKey(), (boolean) entry.getValue());
}
}
SignalStore.setRemoteConfig(filtered.toString());
} catch (JSONException e) {
throw new AssertionError(e);
}
}
/** UUID-related stuff that shouldn't be activated until the user-facing launch. */
public static final boolean UUIDS = false;
public static boolean uuids() {
return getValue(UUIDS, false);
}
/** Favoring profile names when displaying contacts. */
public static final boolean PROFILE_DISPLAY = UUIDS;
public static boolean profileDisplay() {
return getValue(PROFILE_DISPLAY, false);
}
/** MessageRequest stuff */
public static final boolean MESSAGE_REQUESTS = UUIDS;
public static boolean messageRequests() {
return getValue(MESSAGE_REQUESTS, false);
}
/** Creating usernames, sending messages by username. Requires {@link #UUIDS}. */
public static final boolean USERNAMES = false;
/** Creating usernames, sending messages by username. Requires {@link #uuids()}. */
public static boolean usernames() {
boolean value = getValue(USERNAMES, false);
if (value && !uuids()) throw new MissingFlagRequirementError();
return value;
}
/** Set or migrate PIN to KBS */
public static final boolean KBS = false;
public static boolean kbs() {
return getValue(KBS, false);
}
/** Storage service. Requires {@link #KBS}. */
public static final boolean STORAGE_SERVICE = false;
/** Storage service. Requires {@link #kbs()}. */
public static boolean storageService() {
boolean value = getValue(STORAGE_SERVICE, false);
if (value && !kbs()) throw new MissingFlagRequirementError();
return value;
}
/** Send support for reactions. */
public static final boolean REACTION_SENDING = false;
public static boolean reactionSending() {
return getValue(REACTION_SENDING, false);
}
/** Only for rendering debug info. */
public static @NonNull Map<String, Boolean> getRemoteValues() {
return new TreeMap<>(REMOTE_VALUES);
}
/** Only for rendering debug info. */
public static @NonNull Map<String, Boolean> getForcedValues() {
return new TreeMap<>(FORCED_VALUES);
}
private static @NonNull String generateKey(@NonNull String key) {
return PREFIX + key;
}
private static boolean getValue(@NonNull String key, boolean defaultValue) {
Boolean forced = FORCED_VALUES.get(key);
if (forced != null) {
return forced;
}
Boolean remote = REMOTE_VALUES.get(key);
if (remote != null) {
return remote;
}
return defaultValue;
}
private static void scheduleFetchIfNecessary() {
long timeSinceLastFetch = System.currentTimeMillis() - SignalStore.getRemoteConfigLastFetchTime();
if (timeSinceLastFetch > FETCH_INTERVAL) {
Log.i(TAG, "Scheduling remote config refresh.");
ApplicationDependencies.getJobManager().add(new RemoteConfigRefreshJob());
} else {
Log.i(TAG, "Skipping remote config refresh. Refreshed " + timeSinceLastFetch + " ms ago.");
}
}
private static Map<String, Boolean> parseStoredConfig() {
Map<String, Boolean> parsed = new HashMap<>();
String stored = SignalStore.getRemoteConfig();
if (TextUtils.isEmpty(stored)) {
Log.i(TAG, "No remote config stored. Skipping.");
return parsed;
}
try {
JSONObject root = new JSONObject(stored);
Iterator<String> iter = root.keys();
while (iter.hasNext()) {
String key = iter.next();
parsed.put(key, root.getBoolean(key));
}
} catch (JSONException e) {
SignalStore.setRemoteConfig(null);
throw new AssertionError("Failed to parse! Cleared storage.");
}
return parsed;
}
private static final class MissingFlagRequirementError extends Error {
}
}

View File

@ -122,6 +122,19 @@ public class Util {
return sb.toString();
}
public static String rightPad(String value, int length) {
if (value.length() >= length) {
return value;
}
StringBuilder out = new StringBuilder(value);
while (out.length() < length) {
out.append(" ");
}
return out.toString();
}
public static ExecutorService newSingleThreadedLifoExecutor() {
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingLifoQueue<Runnable>());

View File

@ -43,6 +43,7 @@ import org.whispersystems.signalservice.internal.crypto.ProvisioningCipher;
import org.whispersystems.signalservice.internal.push.ProfileAvatarData;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import org.whispersystems.signalservice.internal.push.RemoteAttestationUtil;
import org.whispersystems.signalservice.internal.push.RemoteConfigResponse;
import org.whispersystems.signalservice.internal.push.http.ProfileCipherOutputStreamFactory;
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
import org.whispersystems.signalservice.internal.storage.protos.ReadOperation;
@ -501,6 +502,16 @@ public class SignalServiceAccountManager {
}
}
public Map<String, Boolean> getRemoteConfig() throws IOException {
RemoteConfigResponse response = this.pushServiceSocket.getRemoteConfig();
Map<String, Boolean> out = new HashMap<>();
for (RemoteConfigResponse.Config config : response.getConfig()) {
out.put(config.getName(), config.isEnabled());
}
return out;
}
public String getNewDeviceVerificationCode() throws IOException {

View File

@ -731,6 +731,11 @@ public class PushServiceSocket {
}
}
public RemoteConfigResponse getRemoteConfig() throws IOException {
String response = makeServiceRequest("/v1/config", "GET", null);
return JsonUtil.fromJson(response, RemoteConfigResponse.class);
}
public void setSoTimeoutMillis(long soTimeoutMillis) {
this.soTimeoutMillis = soTimeoutMillis;
}

View File

@ -0,0 +1,30 @@
package org.whispersystems.signalservice.internal.push;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public class RemoteConfigResponse {
@JsonProperty
private List<Config> config;
public List<Config> getConfig() {
return config;
}
public static class Config {
@JsonProperty
private String name;
@JsonProperty
private boolean enabled;
public String getName() {
return name;
}
public boolean isEnabled() {
return enabled;
}
}
}