Add Research Megaphone.

fork-5.53.8
Cody Henthorne 2020-09-18 17:32:56 -04:00 zatwierdzone przez Greyson Parrelli
rodzic 9dbb77c10a
commit ca442970a3
28 zmienionych plików z 685 dodań i 67 usunięć

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 8.7 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 5.4 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 12 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 21 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 29 KiB

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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