Add Research Megaphone.
This commit is contained in:
parent
9dbb77c10a
commit
ca442970a3
28 changed files with 685 additions and 67 deletions
|
@ -0,0 +1,51 @@
|
|||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.LayoutRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
|
||||
/**
|
||||
* Base dialog fragment for rendering as a full screen dialog with animation
|
||||
* transitions.
|
||||
*/
|
||||
public abstract class FullScreenDialogFragment extends DialogFragment {
|
||||
|
||||
protected Toolbar toolbar;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setStyle(STYLE_NO_FRAME, ThemeUtil.isDarkTheme(requireActivity()) ? R.style.TextSecure_DarkTheme_FullScreenDialog
|
||||
: R.style.TextSecure_LightTheme_FullScreenDialog);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.full_screen_dialog_fragment, container, false);
|
||||
inflater.inflate(getDialogLayoutResource(), view.findViewById(R.id.full_screen_dialog_content), true);
|
||||
toolbar = view.findViewById(R.id.full_screen_dialog_toolbar);
|
||||
toolbar.setTitle(getTitle());
|
||||
toolbar.setNavigationOnClickListener(v -> onNavigateUp());
|
||||
return view;
|
||||
}
|
||||
|
||||
protected void onNavigateUp() {
|
||||
dismissAllowingStateLoss();
|
||||
}
|
||||
|
||||
protected abstract @StringRes int getTitle();
|
||||
|
||||
protected abstract @LayoutRes int getDialogLayoutResource();
|
||||
}
|
|
@ -12,8 +12,6 @@ import com.annimon.stream.Stream;
|
|||
|
||||
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerRepository.MentionQuery;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphones;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
@ -31,12 +29,8 @@ public class MentionsPickerViewModel extends ViewModel {
|
|||
private final MutableLiveData<LiveRecipient> liveRecipient;
|
||||
private final MutableLiveData<Query> liveQuery;
|
||||
private final MutableLiveData<Boolean> isShowing;
|
||||
private final MegaphoneRepository megaphoneRepository;
|
||||
|
||||
MentionsPickerViewModel(@NonNull MentionsPickerRepository mentionsPickerRepository,
|
||||
@NonNull MegaphoneRepository megaphoneRepository)
|
||||
{
|
||||
this.megaphoneRepository = megaphoneRepository;
|
||||
MentionsPickerViewModel(@NonNull MentionsPickerRepository mentionsPickerRepository) {
|
||||
this.liveRecipient = new MutableLiveData<>();
|
||||
this.liveQuery = new MutableLiveData<>();
|
||||
this.selectedRecipient = new SingleLiveEvent<>();
|
||||
|
@ -56,7 +50,6 @@ public class MentionsPickerViewModel extends ViewModel {
|
|||
|
||||
void onSelectionChange(@NonNull Recipient recipient) {
|
||||
selectedRecipient.setValue(recipient);
|
||||
megaphoneRepository.markFinished(Megaphones.Event.MENTIONS);
|
||||
}
|
||||
|
||||
void setIsShowing(boolean isShowing) {
|
||||
|
@ -119,8 +112,7 @@ public class MentionsPickerViewModel extends ViewModel {
|
|||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection ConstantConditions
|
||||
return modelClass.cast(new MentionsPickerViewModel(new MentionsPickerRepository(ApplicationDependencies.getApplication()),
|
||||
ApplicationDependencies.getMegaphoneRepository()));
|
||||
return modelClass.cast(new MentionsPickerViewModel(new MentionsPickerRepository(ApplicationDependencies.getApplication())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,6 +55,7 @@ import androidx.appcompat.widget.Toolbar;
|
|||
import androidx.appcompat.widget.TooltipCompat;
|
||||
import androidx.core.content.res.ResourcesCompat;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
|
@ -423,6 +424,11 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
viewModel.onMegaphoneCompleted(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMegaphoneDialogFragmentRequested(@NonNull DialogFragment dialogFragment) {
|
||||
dialogFragment.show(getChildFragmentManager(), "megaphone_dialog");
|
||||
}
|
||||
|
||||
private void onReminderAction(@IdRes int reminderActionId) {
|
||||
if (reminderActionId == R.id.reminder_action_update_now) {
|
||||
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext());
|
||||
|
|
|
@ -14,11 +14,11 @@ import org.thoughtcrime.securesms.R;
|
|||
|
||||
public class BasicMegaphoneView extends FrameLayout {
|
||||
|
||||
private ImageView image;
|
||||
private TextView titleText;
|
||||
private TextView bodyText;
|
||||
private Button actionButton;
|
||||
private Button snoozeButton;
|
||||
private ImageView image;
|
||||
private TextView titleText;
|
||||
private TextView bodyText;
|
||||
private Button actionButton;
|
||||
private Button secondaryButton;
|
||||
|
||||
private Megaphone megaphone;
|
||||
private MegaphoneActionController megaphoneListener;
|
||||
|
@ -36,11 +36,11 @@ public class BasicMegaphoneView extends FrameLayout {
|
|||
private void init(@NonNull Context context) {
|
||||
inflate(context, R.layout.basic_megaphone_view, this);
|
||||
|
||||
this.image = findViewById(R.id.basic_megaphone_image);
|
||||
this.titleText = findViewById(R.id.basic_megaphone_title);
|
||||
this.bodyText = findViewById(R.id.basic_megaphone_body);
|
||||
this.actionButton = findViewById(R.id.basic_megaphone_action);
|
||||
this.snoozeButton = findViewById(R.id.basic_megaphone_snooze);
|
||||
this.image = findViewById(R.id.basic_megaphone_image);
|
||||
this.titleText = findViewById(R.id.basic_megaphone_title);
|
||||
this.bodyText = findViewById(R.id.basic_megaphone_body);
|
||||
this.actionButton = findViewById(R.id.basic_megaphone_action);
|
||||
this.secondaryButton = findViewById(R.id.basic_megaphone_secondary);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -89,17 +89,27 @@ public class BasicMegaphoneView extends FrameLayout {
|
|||
actionButton.setVisibility(GONE);
|
||||
}
|
||||
|
||||
if (megaphone.canSnooze()) {
|
||||
snoozeButton.setVisibility(VISIBLE);
|
||||
snoozeButton.setOnClickListener(v -> {
|
||||
megaphoneListener.onMegaphoneSnooze(megaphone.getEvent());
|
||||
if (megaphone.canSnooze() || megaphone.hasSecondaryButton()) {
|
||||
secondaryButton.setVisibility(VISIBLE);
|
||||
|
||||
if (megaphone.getSnoozeListener() != null) {
|
||||
megaphone.getSnoozeListener().onEvent(megaphone, megaphoneListener);
|
||||
}
|
||||
});
|
||||
if (megaphone.canSnooze()) {
|
||||
secondaryButton.setOnClickListener(v -> {
|
||||
megaphoneListener.onMegaphoneSnooze(megaphone.getEvent());
|
||||
|
||||
if (megaphone.getSnoozeListener() != null) {
|
||||
megaphone.getSnoozeListener().onEvent(megaphone, megaphoneListener);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
secondaryButton.setText(megaphone.getSecondaryButtonText());
|
||||
secondaryButton.setOnClickListener(v -> {
|
||||
if (megaphone.getSecondaryButtonClickListener() != null) {
|
||||
megaphone.getSecondaryButtonClickListener().onEvent(megaphone, megaphoneListener);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
snoozeButton.setVisibility(GONE);
|
||||
secondaryButton.setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,20 +28,24 @@ public class Megaphone {
|
|||
private final int buttonTextRes;
|
||||
private final EventListener buttonListener;
|
||||
private final EventListener snoozeListener;
|
||||
private final int secondaryButtonTextRes;
|
||||
private final EventListener secondaryButtonListener;
|
||||
private final EventListener onVisibleListener;
|
||||
|
||||
private Megaphone(@NonNull Builder builder) {
|
||||
this.event = builder.event;
|
||||
this.style = builder.style;
|
||||
this.priority = builder.priority;
|
||||
this.canSnooze = builder.canSnooze;
|
||||
this.titleRes = builder.titleRes;
|
||||
this.bodyRes = builder.bodyRes;
|
||||
this.imageRequest = builder.imageRequest;
|
||||
this.buttonTextRes = builder.buttonTextRes;
|
||||
this.buttonListener = builder.buttonListener;
|
||||
this.snoozeListener = builder.snoozeListener;
|
||||
this.onVisibleListener = builder.onVisibleListener;
|
||||
this.event = builder.event;
|
||||
this.style = builder.style;
|
||||
this.priority = builder.priority;
|
||||
this.canSnooze = builder.canSnooze;
|
||||
this.titleRes = builder.titleRes;
|
||||
this.bodyRes = builder.bodyRes;
|
||||
this.imageRequest = builder.imageRequest;
|
||||
this.buttonTextRes = builder.buttonTextRes;
|
||||
this.buttonListener = builder.buttonListener;
|
||||
this.snoozeListener = builder.snoozeListener;
|
||||
this.secondaryButtonTextRes = builder.secondaryButtonTextRes;
|
||||
this.secondaryButtonListener = builder.secondaryButtonListener;
|
||||
this.onVisibleListener = builder.onVisibleListener;
|
||||
}
|
||||
|
||||
public @NonNull Event getEvent() {
|
||||
|
@ -88,6 +92,18 @@ public class Megaphone {
|
|||
return snoozeListener;
|
||||
}
|
||||
|
||||
public @StringRes int getSecondaryButtonText() {
|
||||
return secondaryButtonTextRes;
|
||||
}
|
||||
|
||||
public boolean hasSecondaryButton() {
|
||||
return secondaryButtonTextRes != 0;
|
||||
}
|
||||
|
||||
public @Nullable EventListener getSecondaryButtonClickListener() {
|
||||
return secondaryButtonListener;
|
||||
}
|
||||
|
||||
public @Nullable EventListener getOnVisibleListener() {
|
||||
return onVisibleListener;
|
||||
}
|
||||
|
@ -105,6 +121,8 @@ public class Megaphone {
|
|||
private int buttonTextRes;
|
||||
private EventListener buttonListener;
|
||||
private EventListener snoozeListener;
|
||||
private int secondaryButtonTextRes;
|
||||
private EventListener secondaryButtonListener;
|
||||
private EventListener onVisibleListener;
|
||||
|
||||
|
||||
|
@ -159,6 +177,12 @@ public class Megaphone {
|
|||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder setSecondaryButton(@StringRes int secondaryButtonTextRes, @NonNull EventListener listener) {
|
||||
this.secondaryButtonTextRes = secondaryButtonTextRes;
|
||||
this.secondaryButtonListener = listener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder setOnVisibleListener(@Nullable EventListener listener) {
|
||||
this.onVisibleListener = listener;
|
||||
return this;
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.content.Intent;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
public interface MegaphoneActionController {
|
||||
/**
|
||||
|
@ -36,4 +37,9 @@ public interface MegaphoneActionController {
|
|||
* Called when a megaphone completed its goal.
|
||||
*/
|
||||
void onMegaphoneCompleted(@NonNull Megaphones.Event event);
|
||||
|
||||
/**
|
||||
* When a megaphone wnats to show a dialog fragment.
|
||||
*/
|
||||
void onMegaphoneDialogFragmentRequested(@NonNull DialogFragment dialogFragment);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.megaphone;
|
|||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
@ -53,7 +52,7 @@ public class MegaphoneRepository {
|
|||
executor.execute(() -> {
|
||||
database.markFinished(Event.REACTIONS);
|
||||
database.markFinished(Event.MESSAGE_REQUESTS);
|
||||
database.markFinished(Event.MENTIONS);
|
||||
database.markFinished(Event.RESEARCH);
|
||||
resetDatabaseCache();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.messagerequests.MessageRequestMegaphoneActivit
|
|||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.ResearchMegaphone;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
|
@ -85,9 +86,9 @@ public final class Megaphones {
|
|||
put(Event.PINS_FOR_ALL, new PinsForAllSchedule());
|
||||
put(Event.PIN_REMINDER, new SignalPinReminderSchedule());
|
||||
put(Event.MESSAGE_REQUESTS, shouldShowMessageRequestsMegaphone() ? ALWAYS : NEVER);
|
||||
put(Event.MENTIONS, shouldShowMentionsMegaphone() ? ALWAYS : NEVER);
|
||||
put(Event.LINK_PREVIEWS, shouldShowLinkPreviewsMegaphone(context) ? ALWAYS : NEVER);
|
||||
put(Event.CLIENT_DEPRECATED, SignalStore.misc().isClientDeprecated() ? ALWAYS : NEVER);
|
||||
put(Event.RESEARCH, shouldShowResearchMegaphone() ? ShowForDurationSchedule.showForDays(7) : NEVER);
|
||||
}};
|
||||
}
|
||||
|
||||
|
@ -101,12 +102,12 @@ public final class Megaphones {
|
|||
return buildPinReminderMegaphone(context);
|
||||
case MESSAGE_REQUESTS:
|
||||
return buildMessageRequestsMegaphone(context);
|
||||
case MENTIONS:
|
||||
return buildMentionsMegaphone();
|
||||
case LINK_PREVIEWS:
|
||||
return buildLinkPreviewsMegaphone();
|
||||
case CLIENT_DEPRECATED:
|
||||
return buildClientDeprecatedMegaphone(context);
|
||||
case RESEARCH:
|
||||
return buildResearchMegaphone(context);
|
||||
default:
|
||||
throw new IllegalArgumentException("Event not handled!");
|
||||
}
|
||||
|
@ -189,14 +190,6 @@ public final class Megaphones {
|
|||
.build();
|
||||
}
|
||||
|
||||
private static Megaphone buildMentionsMegaphone() {
|
||||
return new Megaphone.Builder(Event.MENTIONS, Megaphone.Style.POPUP)
|
||||
.setTitle(R.string.MentionsMegaphone__introducing_mentions)
|
||||
.setBody(R.string.MentionsMegaphone__get_someones_attention_in_a_group_by_typing)
|
||||
.setImage(R.drawable.mention_megaphone)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static @NonNull Megaphone buildLinkPreviewsMegaphone() {
|
||||
return new Megaphone.Builder(Event.LINK_PREVIEWS, Megaphone.Style.LINK_PREVIEWS)
|
||||
.setPriority(Megaphone.Priority.HIGH)
|
||||
|
@ -207,9 +200,22 @@ public final class Megaphones {
|
|||
return new Megaphone.Builder(Event.CLIENT_DEPRECATED, Megaphone.Style.FULLSCREEN)
|
||||
.disableSnooze()
|
||||
.setPriority(Megaphone.Priority.HIGH)
|
||||
.setOnVisibleListener((megaphone, listener) -> {
|
||||
listener.onMegaphoneNavigationRequested(new Intent(context, ClientDeprecatedActivity.class));
|
||||
.setOnVisibleListener((megaphone, listener) -> listener.onMegaphoneNavigationRequested(new Intent(context, ClientDeprecatedActivity.class)))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static @NonNull Megaphone buildResearchMegaphone(@NonNull Context context) {
|
||||
return new Megaphone.Builder(Event.RESEARCH, Megaphone.Style.BASIC)
|
||||
.disableSnooze()
|
||||
.setTitle(R.string.ResearchMegaphone_tell_signal_what_you_think)
|
||||
.setBody(R.string.ResearchMegaphone_to_make_signal_the_best_messaging_app_on_the_planet)
|
||||
.setImage(R.drawable.ic_research_megaphone)
|
||||
.setActionButton(R.string.ResearchMegaphone_learn_more, (megaphone, controller) -> {
|
||||
controller.onMegaphoneCompleted(megaphone.getEvent());
|
||||
controller.onMegaphoneDialogFragmentRequested(new ResearchMegaphoneDialog());
|
||||
})
|
||||
.setSecondaryButton(R.string.ResearchMegaphone_dismiss, (megaphone, controller) -> controller.onMegaphoneCompleted(megaphone.getEvent()))
|
||||
.setPriority(Megaphone.Priority.DEFAULT)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
@ -217,9 +223,8 @@ public final class Megaphones {
|
|||
return Recipient.self().getProfileName() == ProfileName.EMPTY;
|
||||
}
|
||||
|
||||
private static boolean shouldShowMentionsMegaphone() {
|
||||
return false;
|
||||
// return FeatureFlags.mentions();
|
||||
private static boolean shouldShowResearchMegaphone() {
|
||||
return ResearchMegaphone.isInResearchMegaphone();
|
||||
}
|
||||
|
||||
private static boolean shouldShowLinkPreviewsMegaphone(@NonNull Context context) {
|
||||
|
@ -231,9 +236,9 @@ public final class Megaphones {
|
|||
PINS_FOR_ALL("pins_for_all"),
|
||||
PIN_REMINDER("pin_reminder"),
|
||||
MESSAGE_REQUESTS("message_requests"),
|
||||
MENTIONS("mentions"),
|
||||
LINK_PREVIEWS("link_previews"),
|
||||
CLIENT_DEPRECATED("client_deprecated");
|
||||
CLIENT_DEPRECATED("client_deprecated"),
|
||||
RESEARCH("research");
|
||||
|
||||
private final String key;
|
||||
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
package org.thoughtcrime.securesms.megaphone;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.Html;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.FullScreenDialogFragment;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
|
||||
public class ResearchMegaphoneDialog extends FullScreenDialogFragment {
|
||||
|
||||
private static final String SURVEY_URL = "https://surveys.signalusers.org/s3";
|
||||
|
||||
@Override
|
||||
public @NonNull View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View view = super.onCreateView(inflater, container, savedInstanceState);
|
||||
|
||||
TextView content = view.findViewById(R.id.research_megaphone_content);
|
||||
content.setText(Html.fromHtml(requireContext().getString(R.string.ResearchMegaphoneDialog_we_believe_in_privacy)));
|
||||
|
||||
view.findViewById(R.id.research_megaphone_dialog_take_the_survey)
|
||||
.setOnClickListener(v -> CommunicationActions.openBrowserLink(requireContext(), SURVEY_URL));
|
||||
|
||||
view.findViewById(R.id.research_megaphone_dialog_no_thanks)
|
||||
.setOnClickListener(v -> dismissAllowingStateLoss());
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @StringRes int getTitle() {
|
||||
return R.string.ResearchMegaphoneDialog_signal_research;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getDialogLayoutResource() {
|
||||
return R.layout.research_megaphone_dialog;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package org.thoughtcrime.securesms.megaphone;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Megaphone schedule that will always show for some duration after the first
|
||||
* time the user sees it.
|
||||
*/
|
||||
public class ShowForDurationSchedule implements MegaphoneSchedule {
|
||||
|
||||
private final long duration;
|
||||
|
||||
public static MegaphoneSchedule showForDays(int days) {
|
||||
return new ShowForDurationSchedule(TimeUnit.DAYS.toMillis(days));
|
||||
}
|
||||
|
||||
public ShowForDurationSchedule(long duration) {
|
||||
this.duration = duration;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldDisplay(int seenCount, long lastSeen, long firstVisible, long currentTime) {
|
||||
return firstVisible == 0 || currentTime < firstVisible + duration;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Logic to bucket a user for a given feature flag based on their UUID.
|
||||
*/
|
||||
public final class BucketingUtil {
|
||||
|
||||
private BucketingUtil() {}
|
||||
|
||||
/**
|
||||
* Calculate a user bucket for a given feature flag, uuid, and part per modulus.
|
||||
*
|
||||
* @param key Feature flag key (e.g., "research.megaphone.1")
|
||||
* @param uuid Current user's UUID (see {@link Recipient#getUuid()})
|
||||
* @param modulus Drives the bucketing parts per N (e.g., passing 1,000,000 indicates bucketing into parts per million)
|
||||
*/
|
||||
public static long bucket(@NonNull String key, @NonNull UUID uuid, long modulus) {
|
||||
MessageDigest digest;
|
||||
try {
|
||||
digest = MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
||||
digest.update(key.getBytes());
|
||||
digest.update(".".getBytes());
|
||||
|
||||
ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[16]);
|
||||
byteBuffer.order(ByteOrder.BIG_ENDIAN);
|
||||
byteBuffer.putLong(uuid.getMostSignificantBits());
|
||||
byteBuffer.putLong(uuid.getLeastSignificantBits());
|
||||
|
||||
digest.update(byteBuffer.array());
|
||||
|
||||
return new BigInteger(Arrays.copyOfRange(digest.digest(), 0, 8)).mod(BigInteger.valueOf(modulus)).longValue();
|
||||
}
|
||||
}
|
|
@ -7,6 +7,9 @@ import androidx.annotation.VisibleForTesting;
|
|||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.collect.Sets;
|
||||
import com.google.i18n.phonenumbers.NumberParseException;
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||
import com.google.i18n.phonenumbers.Phonenumber;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
@ -17,7 +20,10 @@ import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob;
|
|||
import org.thoughtcrime.securesms.jobs.RemoteConfigRefreshJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
|
@ -65,6 +71,7 @@ public final class FeatureFlags {
|
|||
private static final String VERIFY_V2 = "android.verifyV2";
|
||||
private static final String PHONE_NUMBER_PRIVACY_VERSION = "android.phoneNumberPrivacyVersion";
|
||||
private static final String CLIENT_EXPIRATION = "android.clientExpiration";
|
||||
public static final String RESEARCH_MEGAPHONE_1 = "research.megaphone.1";
|
||||
|
||||
/**
|
||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||
|
@ -83,7 +90,8 @@ public final class FeatureFlags {
|
|||
USERNAMES,
|
||||
MENTIONS,
|
||||
VERIFY_V2,
|
||||
CLIENT_EXPIRATION
|
||||
CLIENT_EXPIRATION,
|
||||
RESEARCH_MEGAPHONE_1
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -283,6 +291,11 @@ public final class FeatureFlags {
|
|||
return getString(CLIENT_EXPIRATION, null);
|
||||
}
|
||||
|
||||
/** The raw research megaphone CSV string */
|
||||
public static String researchMegaphone() {
|
||||
return getString(RESEARCH_MEGAPHONE_1, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the user can choose phone number privacy settings, and;
|
||||
* Whether to fetch and store the secondary certificate
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.google.i18n.phonenumbers.NumberParseException;
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Parses a comma-separated list of country codes colon-separated from how many buckets out of 1 million
|
||||
* should be enabled to see this megaphone in that country code. At the end of the list, an optional
|
||||
* element saying how many buckets out of a million should be enabled for all countries not listed previously
|
||||
* in the list. For example, "1:20000,*:40000" would mean 2% of the NANPA phone numbers and 4% of the rest of
|
||||
* the world should see the megaphone.
|
||||
*/
|
||||
public final class ResearchMegaphone {
|
||||
|
||||
private static final String TAG = Log.tag(ResearchMegaphone.class);
|
||||
|
||||
private static final String COUNTRY_WILDCARD = "*";
|
||||
|
||||
/**
|
||||
* In research megaphone group for given country code
|
||||
*/
|
||||
public static boolean isInResearchMegaphone() {
|
||||
Map<String, Integer> countryCountEnabled = parseCountryCounts(FeatureFlags.researchMegaphone());
|
||||
Recipient self = Recipient.self();
|
||||
|
||||
if (countryCountEnabled.isEmpty() || !self.getE164().isPresent() || !self.getUuid().isPresent()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
long countEnabled = determineCountEnabled(countryCountEnabled, self.getE164().or(""));
|
||||
long currentUserBucket = BucketingUtil.bucket(FeatureFlags.RESEARCH_MEGAPHONE_1, self.requireUuid(), 1_000_000);
|
||||
|
||||
return countEnabled > currentUserBucket;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static @NonNull Map<String, Integer> parseCountryCounts(@NonNull String buckets) {
|
||||
Map<String, Integer> countryCountEnabled = new HashMap<>();
|
||||
|
||||
for (String bucket : buckets.split(",")) {
|
||||
String[] parts = bucket.split(":");
|
||||
if (parts.length == 2 && !parts[0].isEmpty()) {
|
||||
countryCountEnabled.put(parts[0], Util.parseInt(parts[1], 0));
|
||||
}
|
||||
}
|
||||
return countryCountEnabled;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static long determineCountEnabled(@NonNull Map<String, Integer> countryCountEnabled, @NonNull String e164) {
|
||||
Integer countEnabled = countryCountEnabled.get(COUNTRY_WILDCARD);
|
||||
try {
|
||||
String countryCode = String.valueOf(PhoneNumberUtil.getInstance().parse(e164, "").getCountryCode());
|
||||
if (countryCountEnabled.containsKey(countryCode)) {
|
||||
countEnabled = countryCountEnabled.get(countryCode);
|
||||
}
|
||||
} catch (NumberParseException e) {
|
||||
Log.d(TAG, "Unable to determine country code for bucketing.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
return countEnabled != null ? countEnabled : 0;
|
||||
}
|
||||
}
|
|
@ -664,6 +664,14 @@ public class Util {
|
|||
}
|
||||
}
|
||||
|
||||
public static int parseInt(String integer, int defaultValue) {
|
||||
try {
|
||||
return Integer.parseInt(integer);
|
||||
} catch (NumberFormatException e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends the stack trace of the provided throwable onto the provided primary exception. This is
|
||||
* useful for when exceptions are thrown inside of asynchronous systems (like runnables in an
|
||||
|
|
BIN
app/src/main/res/drawable-hdpi/signal_research.webp
Normal file
BIN
app/src/main/res/drawable-hdpi/signal_research.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.7 KiB |
BIN
app/src/main/res/drawable-mdpi/signal_research.webp
Normal file
BIN
app/src/main/res/drawable-mdpi/signal_research.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.4 KiB |
BIN
app/src/main/res/drawable-xhdpi/signal_research.webp
Normal file
BIN
app/src/main/res/drawable-xhdpi/signal_research.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
app/src/main/res/drawable-xxhdpi/signal_research.webp
Normal file
BIN
app/src/main/res/drawable-xxhdpi/signal_research.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/signal_research.webp
Normal file
BIN
app/src/main/res/drawable-xxxhdpi/signal_research.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
30
app/src/main/res/drawable/ic_research_megaphone.xml
Normal file
30
app/src/main/res/drawable/ic_research_megaphone.xml
Normal file
|
@ -0,0 +1,30 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="60dp"
|
||||
android:height="60dp"
|
||||
android:viewportWidth="60"
|
||||
android:viewportHeight="60">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M6,0L54,0A6,6 0,0 1,60 6L60,54A6,6 0,0 1,54 60L6,60A6,6 0,0 1,0 54L0,6A6,6 0,0 1,6 0z"/>
|
||||
<path
|
||||
android:pathData="M6,0L54,0A6,6 0,0 1,60 6L60,54A6,6 0,0 1,54 60L6,60A6,6 0,0 1,0 54L0,6A6,6 0,0 1,6 0z"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M0,0h60v60h-60z"
|
||||
android:fillColor="#DFE9FD"/>
|
||||
<path
|
||||
android:pathData="M-6,13L5,13A8,8 0,0 1,13 21L13,38A8,8 0,0 1,5 46L-6,46A8,8 0,0 1,-14 38L-14,21A8,8 0,0 1,-6 13z"
|
||||
android:fillColor="#2C6BED"/>
|
||||
<path
|
||||
android:pathData="M28,13L83,13A8,8 0,0 1,91 21L91,38A8,8 0,0 1,83 46L28,46A8,8 0,0 1,20 38L20,21A8,8 0,0 1,28 13z"
|
||||
android:fillColor="#6191F3"/>
|
||||
<path
|
||||
android:pathData="M33.81,51.3213L30.0808,24.8698C30.0493,24.6463 30.2956,24.4897 30.4846,24.6131L53.8863,39.8865C54.0758,40.0102 54.0308,40.2996 53.8126,40.3598L46.2009,42.4617C46.031,42.5086 45.9546,42.7061 46.0486,42.8552L52.9586,53.8078C53.0337,53.9269 53.0017,54.084 52.8859,54.1641L48.3603,57.2976C48.2386,57.3818 48.0714,57.349 47.9905,57.225L40.4311,45.6269C40.3393,45.4861 40.1409,45.4664 40.0233,45.5864L34.2579,51.4685C34.1053,51.6242 33.8404,51.5371 33.81,51.3213Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M33.81,51.3213L30.0808,24.8698C30.0493,24.6463 30.2956,24.4897 30.4846,24.6131L53.8863,39.8865C54.0758,40.0102 54.0308,40.2996 53.8126,40.3598L46.2009,42.4617C46.031,42.5086 45.9546,42.7061 46.0486,42.8552L52.9586,53.8078C53.0337,53.9269 53.0017,54.084 52.8859,54.1641L48.3603,57.2976C48.2386,57.3818 48.0714,57.349 47.9905,57.225L40.4311,45.6269C40.3393,45.4861 40.1409,45.4664 40.0233,45.5864L34.2579,51.4685C34.1053,51.6242 33.8404,51.5371 33.81,51.3213Z"
|
||||
android:strokeWidth="1.57676"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"/>
|
||||
</group>
|
||||
</vector>
|
|
@ -23,7 +23,7 @@
|
|||
android:scaleType="centerInside"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@drawable/profile_splash"/>
|
||||
tools:src="@tools:sample/avatars"/>
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/basic_megaphone_title"
|
||||
|
@ -63,14 +63,14 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
style="@style/Button.Borderless"
|
||||
app:layout_constraintStart_toEndOf="@id/basic_megaphone_snooze"
|
||||
app:layout_constraintStart_toEndOf="@id/basic_megaphone_secondary"
|
||||
app:layout_constraintTop_toBottomOf="@id/basic_megaphone_content_barrier"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
tools:text="*sigh*"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/basic_megaphone_snooze"
|
||||
android:id="@+id/basic_megaphone_secondary"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
|
|
22
app/src/main/res/layout/full_screen_dialog_fragment.xml
Normal file
22
app/src/main/res/layout/full_screen_dialog_fragment.xml
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/full_screen_dialog_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:navigationIcon="@drawable/ic_arrow_left_24"
|
||||
app:titleTextAppearance="@style/TextSecure.TitleTextStyle"
|
||||
tools:title="Dialog Title" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/full_screen_dialog_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
</LinearLayout>
|
67
app/src/main/res/layout/research_megaphone_dialog.xml
Normal file
67
app/src/main/res/layout/research_megaphone_dialog.xml
Normal file
|
@ -0,0 +1,67 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="160dp"
|
||||
android:background="@color/blue_100"
|
||||
app:srcCompat="@drawable/signal_research" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/research_megaphone_content"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginTop="22dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:text="@string/ResearchMegaphoneDialog_we_believe_in_privacy" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/research_megaphone_dialog_take_the_survey"
|
||||
style="@style/Widget.Signal.Button.Flat"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/ResearchMegaphoneDialog_take_the_survey"
|
||||
android:textColor="@color/core_white"
|
||||
app:backgroundTint="?attr/colorAccent"
|
||||
app:icon="@drawable/ic_open_20"
|
||||
app:iconGravity="textEnd"
|
||||
app:iconTint="@color/core_white" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/research_megaphone_dialog_no_thanks"
|
||||
style="@style/Widget.Signal.Button.Flat"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:text="@string/ResearchMegaphoneDialog_no_thanks"
|
||||
android:textColor="?safety_number_change_dialog_button_text_color"
|
||||
app:backgroundTint="?safety_number_change_dialog_button_background" />
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance.Signal.Caption"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/ResearchMegaphoneDialog_the_survey_is_hosted_by_surveygizmo_at_the_secure_domain" />
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
|
@ -2485,9 +2485,17 @@
|
|||
<string name="KbsMegaphone__well_remind_you_later_creating_a_pin">We\'ll remind you later. Creating a PIN will become mandatory in %1$d days.</string>
|
||||
<string name="KbsMegaphone__well_remind_you_later_confirming_your_pin">We\'ll remind you later. Confirming your PIN will become mandatory in %1$d days.</string>
|
||||
|
||||
<!-- Mention Megaphone -->
|
||||
<string name="MentionsMegaphone__introducing_mentions">Introducing @Mentions</string>
|
||||
<string name="MentionsMegaphone__get_someones_attention_in_a_group_by_typing">Get someone\'s attention in a \"New Group\" by typing @</string>
|
||||
<!-- Research Megaphone -->
|
||||
<string name="ResearchMegaphone_tell_signal_what_you_think">Tell Signal what you think</string>
|
||||
<string name="ResearchMegaphone_to_make_signal_the_best_messaging_app_on_the_planet">To make Signal the best messaging app on the planet, we\'d love to hear your feedback.</string>
|
||||
<string name="ResearchMegaphone_learn_more">Learn more</string>
|
||||
<string name="ResearchMegaphone_dismiss">Dismiss</string>
|
||||
|
||||
<string name="ResearchMegaphoneDialog_signal_research">Signal Research</string>
|
||||
<string name="ResearchMegaphoneDialog_we_believe_in_privacy"><![CDATA[<p><b>We believe in privacy.</b></p><p>Signal doesn\'t track you or collect your data. To improve Signal for everyone, we rely on user feedback, <b>and we\'d love yours.</b></p><p>We\'re running a survey to understand how you use Signal. Our survey doesn\'t collect any data that will identify you. If you’re interested in sharing additional feedback, you\'ll have the option to provide contact information.</p><p>If you have a few minutes and feedback to offer, we\'d love to hear from you.</p>]]></string>
|
||||
<string name="ResearchMegaphoneDialog_take_the_survey">Take the survey</string>
|
||||
<string name="ResearchMegaphoneDialog_no_thanks">No thanks</string>
|
||||
<string name="ResearchMegaphoneDialog_the_survey_is_hosted_by_surveygizmo_at_the_secure_domain">The survey is hosted by Surveygizmo at the secure domain surveys.signalusers.org</string>
|
||||
|
||||
<!-- transport_selection_list_item -->
|
||||
<string name="transport_selection_list_item__transport_icon">Transport icon</string>
|
||||
|
|
|
@ -40,6 +40,19 @@
|
|||
<item name="android:textColor">@null</item>
|
||||
</style>
|
||||
|
||||
<style name="TextSecure.DarkTheme.FullScreenDialog">
|
||||
<item name="android:windowAnimationStyle">@style/TextSecure.Animation.FullScreenDialog</item>
|
||||
</style>
|
||||
|
||||
<style name="TextSecure.LightTheme.FullScreenDialog">
|
||||
<item name="android:windowAnimationStyle">@style/TextSecure.Animation.FullScreenDialog</item>
|
||||
</style>
|
||||
|
||||
<style name="TextSecure.Animation.FullScreenDialog" parent="@android:style/Animation.Activity">
|
||||
<item name="android:windowEnterAnimation">@anim/fade_scale_in</item>
|
||||
<item name="android:windowExitAnimation">@anim/fade_scale_out</item>
|
||||
</style>
|
||||
|
||||
<!-- ActionBar styles -->
|
||||
<style name="TextSecure.DarkActionBar"
|
||||
parent="@style/Widget.AppCompat.ActionBar">
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.signal.zkgroup.util.UUIDUtil;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class BucketingUtilTest {
|
||||
|
||||
@Test
|
||||
public void bucket() {
|
||||
// GIVEN
|
||||
String key = "research.megaphone.1";
|
||||
UUID uuid = UuidUtil.parseOrThrow("15b9729c-51ea-4ddb-b516-652befe78062");
|
||||
long partPer = 1_000_000;
|
||||
|
||||
// WHEN
|
||||
long countEnabled = BucketingUtil.bucket(key, uuid, partPer);
|
||||
|
||||
// THEN
|
||||
assertEquals(243315, countEnabled);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.protobuf.Empty;
|
||||
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.testutil.EmptyLogger;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public class ResearchMegaphoneTest_determineCountEnabled {
|
||||
|
||||
private final String phoneNumber;
|
||||
private final Map<String, Integer> countryCounts;
|
||||
private final long output;
|
||||
|
||||
@Parameterized.Parameters
|
||||
public static Collection<Object[]> data() {
|
||||
return Arrays.asList(new Object[][]{
|
||||
{"+1 555 555 5555", new HashMap<String, Integer>() {{
|
||||
put("1", 10000);
|
||||
put("*", 400);
|
||||
}}, 10000},
|
||||
{"+1 555 555 5555", new HashMap<String, Integer>() {{
|
||||
put("011", 1000);
|
||||
put("1", 20000);
|
||||
}}, 20000},
|
||||
{"+1 555 555 5555", new HashMap<String, Integer>() {{
|
||||
put("011", 1000);
|
||||
put("a", 123);
|
||||
put("abba", 0);
|
||||
}}, 0},
|
||||
{"+1 555", new HashMap<String, Integer>() {{
|
||||
put("011", 1000);
|
||||
put("1", 1000);
|
||||
}}, 1000},
|
||||
{"+81 555 555 5555", new HashMap<String, Integer>() {{
|
||||
put("81", 6000);
|
||||
put("1", 1000);
|
||||
put("*", 2000);
|
||||
}}, 6000},
|
||||
{"+81 555 555 5555", new HashMap<String, Integer>() {{
|
||||
put("0011", 6000);
|
||||
put("1", 1000);
|
||||
put("*", 2000);
|
||||
}}, 2000},
|
||||
{"+49 555 555 5555", new HashMap<String, Integer>() {{
|
||||
put("0011", 6000);
|
||||
put("1", 1000);
|
||||
put("*", 2000);
|
||||
}}, 2000}
|
||||
});
|
||||
}
|
||||
|
||||
@BeforeClass
|
||||
public static void setup() {
|
||||
Log.initialize(new EmptyLogger());
|
||||
}
|
||||
|
||||
public ResearchMegaphoneTest_determineCountEnabled(@NonNull String phoneNumber,
|
||||
@NonNull Map<String, Integer> countryCounts,
|
||||
long output)
|
||||
{
|
||||
this.phoneNumber = phoneNumber;
|
||||
this.countryCounts = countryCounts;
|
||||
this.output = output;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void determineCountEnabled() {
|
||||
assertEquals(output, ResearchMegaphone.determineCountEnabled(countryCounts, phoneNumber));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public class ResearchMegaphoneTest_parseCountryCounts {
|
||||
|
||||
private final String input;
|
||||
private final Map<String, Integer> output;
|
||||
|
||||
@Parameterized.Parameters
|
||||
public static Collection<Object[]> data() {
|
||||
return Arrays.asList(new Object[][]{
|
||||
{"1:10000,*:400", new HashMap<String, Integer>() {{
|
||||
put("1", 10000);
|
||||
put("*", 400);
|
||||
}}},
|
||||
{"011:1000,1:1000", new HashMap<String, Integer>() {{
|
||||
put("011", 1000);
|
||||
put("1", 1000);
|
||||
}}},
|
||||
{"011:1000,1:1000,a:123,abba:abba", new HashMap<String, Integer>() {{
|
||||
put("011", 1000);
|
||||
put("1", 1000);
|
||||
put("a", 123);
|
||||
put("abba", 0);
|
||||
}}},
|
||||
{":,011:1000,1:1000,1:,:1,1:1:1", new HashMap<String, Integer>() {{
|
||||
put("011", 1000);
|
||||
put("1", 1000);
|
||||
}}},
|
||||
{"asdf", new HashMap<String, Integer>()},
|
||||
{"asdf:", new HashMap<String, Integer>()},
|
||||
{":,:,:", new HashMap<String, Integer>()},
|
||||
{",,", new HashMap<String, Integer>()},
|
||||
{"", new HashMap<String, Integer>()}
|
||||
});
|
||||
}
|
||||
|
||||
public ResearchMegaphoneTest_parseCountryCounts(String input, Map<String, Integer> output) {
|
||||
this.input = input;
|
||||
this.output = output;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseCountryCounts() {
|
||||
assertEquals(output, ResearchMegaphone.parseCountryCounts(input));
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Reference in a new issue