Add support for remote feature flags.
parent
b8602ee004
commit
55e9f8722f
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()));
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()));
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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}.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>());
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue