Add Research Megaphone.

This commit is contained in:
Cody Henthorne
2020-09-18 17:32:56 -04:00
committed by Greyson Parrelli
parent 9dbb77c10a
commit ca442970a3
28 changed files with 685 additions and 67 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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