Add Research Megaphone.
parent
9dbb77c10a
commit
ca442970a3
|
@ -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.conversation.ui.mentions.MentionsPickerRepository.MentionQuery;
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
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.LiveRecipient;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
|
@ -31,12 +29,8 @@ public class MentionsPickerViewModel extends ViewModel {
|
||||||
private final MutableLiveData<LiveRecipient> liveRecipient;
|
private final MutableLiveData<LiveRecipient> liveRecipient;
|
||||||
private final MutableLiveData<Query> liveQuery;
|
private final MutableLiveData<Query> liveQuery;
|
||||||
private final MutableLiveData<Boolean> isShowing;
|
private final MutableLiveData<Boolean> isShowing;
|
||||||
private final MegaphoneRepository megaphoneRepository;
|
|
||||||
|
|
||||||
MentionsPickerViewModel(@NonNull MentionsPickerRepository mentionsPickerRepository,
|
MentionsPickerViewModel(@NonNull MentionsPickerRepository mentionsPickerRepository) {
|
||||||
@NonNull MegaphoneRepository megaphoneRepository)
|
|
||||||
{
|
|
||||||
this.megaphoneRepository = megaphoneRepository;
|
|
||||||
this.liveRecipient = new MutableLiveData<>();
|
this.liveRecipient = new MutableLiveData<>();
|
||||||
this.liveQuery = new MutableLiveData<>();
|
this.liveQuery = new MutableLiveData<>();
|
||||||
this.selectedRecipient = new SingleLiveEvent<>();
|
this.selectedRecipient = new SingleLiveEvent<>();
|
||||||
|
@ -56,7 +50,6 @@ public class MentionsPickerViewModel extends ViewModel {
|
||||||
|
|
||||||
void onSelectionChange(@NonNull Recipient recipient) {
|
void onSelectionChange(@NonNull Recipient recipient) {
|
||||||
selectedRecipient.setValue(recipient);
|
selectedRecipient.setValue(recipient);
|
||||||
megaphoneRepository.markFinished(Megaphones.Event.MENTIONS);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void setIsShowing(boolean isShowing) {
|
void setIsShowing(boolean isShowing) {
|
||||||
|
@ -119,8 +112,7 @@ public class MentionsPickerViewModel extends ViewModel {
|
||||||
@Override
|
@Override
|
||||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||||
//noinspection ConstantConditions
|
//noinspection ConstantConditions
|
||||||
return modelClass.cast(new MentionsPickerViewModel(new MentionsPickerRepository(ApplicationDependencies.getApplication()),
|
return modelClass.cast(new MentionsPickerViewModel(new MentionsPickerRepository(ApplicationDependencies.getApplication())));
|
||||||
ApplicationDependencies.getMegaphoneRepository()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,6 +55,7 @@ import androidx.appcompat.widget.Toolbar;
|
||||||
import androidx.appcompat.widget.TooltipCompat;
|
import androidx.appcompat.widget.TooltipCompat;
|
||||||
import androidx.core.content.res.ResourcesCompat;
|
import androidx.core.content.res.ResourcesCompat;
|
||||||
import androidx.core.view.ViewCompat;
|
import androidx.core.view.ViewCompat;
|
||||||
|
import androidx.fragment.app.DialogFragment;
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver;
|
import androidx.lifecycle.DefaultLifecycleObserver;
|
||||||
import androidx.lifecycle.LifecycleObserver;
|
import androidx.lifecycle.LifecycleObserver;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
|
@ -423,6 +424,11 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
||||||
viewModel.onMegaphoneCompleted(event);
|
viewModel.onMegaphoneCompleted(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMegaphoneDialogFragmentRequested(@NonNull DialogFragment dialogFragment) {
|
||||||
|
dialogFragment.show(getChildFragmentManager(), "megaphone_dialog");
|
||||||
|
}
|
||||||
|
|
||||||
private void onReminderAction(@IdRes int reminderActionId) {
|
private void onReminderAction(@IdRes int reminderActionId) {
|
||||||
if (reminderActionId == R.id.reminder_action_update_now) {
|
if (reminderActionId == R.id.reminder_action_update_now) {
|
||||||
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext());
|
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext());
|
||||||
|
|
|
@ -14,11 +14,11 @@ import org.thoughtcrime.securesms.R;
|
||||||
|
|
||||||
public class BasicMegaphoneView extends FrameLayout {
|
public class BasicMegaphoneView extends FrameLayout {
|
||||||
|
|
||||||
private ImageView image;
|
private ImageView image;
|
||||||
private TextView titleText;
|
private TextView titleText;
|
||||||
private TextView bodyText;
|
private TextView bodyText;
|
||||||
private Button actionButton;
|
private Button actionButton;
|
||||||
private Button snoozeButton;
|
private Button secondaryButton;
|
||||||
|
|
||||||
private Megaphone megaphone;
|
private Megaphone megaphone;
|
||||||
private MegaphoneActionController megaphoneListener;
|
private MegaphoneActionController megaphoneListener;
|
||||||
|
@ -36,11 +36,11 @@ public class BasicMegaphoneView extends FrameLayout {
|
||||||
private void init(@NonNull Context context) {
|
private void init(@NonNull Context context) {
|
||||||
inflate(context, R.layout.basic_megaphone_view, this);
|
inflate(context, R.layout.basic_megaphone_view, this);
|
||||||
|
|
||||||
this.image = findViewById(R.id.basic_megaphone_image);
|
this.image = findViewById(R.id.basic_megaphone_image);
|
||||||
this.titleText = findViewById(R.id.basic_megaphone_title);
|
this.titleText = findViewById(R.id.basic_megaphone_title);
|
||||||
this.bodyText = findViewById(R.id.basic_megaphone_body);
|
this.bodyText = findViewById(R.id.basic_megaphone_body);
|
||||||
this.actionButton = findViewById(R.id.basic_megaphone_action);
|
this.actionButton = findViewById(R.id.basic_megaphone_action);
|
||||||
this.snoozeButton = findViewById(R.id.basic_megaphone_snooze);
|
this.secondaryButton = findViewById(R.id.basic_megaphone_secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -89,17 +89,27 @@ public class BasicMegaphoneView extends FrameLayout {
|
||||||
actionButton.setVisibility(GONE);
|
actionButton.setVisibility(GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (megaphone.canSnooze()) {
|
if (megaphone.canSnooze() || megaphone.hasSecondaryButton()) {
|
||||||
snoozeButton.setVisibility(VISIBLE);
|
secondaryButton.setVisibility(VISIBLE);
|
||||||
snoozeButton.setOnClickListener(v -> {
|
|
||||||
megaphoneListener.onMegaphoneSnooze(megaphone.getEvent());
|
|
||||||
|
|
||||||
if (megaphone.getSnoozeListener() != null) {
|
if (megaphone.canSnooze()) {
|
||||||
megaphone.getSnoozeListener().onEvent(megaphone, megaphoneListener);
|
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 {
|
} else {
|
||||||
snoozeButton.setVisibility(GONE);
|
secondaryButton.setVisibility(GONE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,20 +28,24 @@ public class Megaphone {
|
||||||
private final int buttonTextRes;
|
private final int buttonTextRes;
|
||||||
private final EventListener buttonListener;
|
private final EventListener buttonListener;
|
||||||
private final EventListener snoozeListener;
|
private final EventListener snoozeListener;
|
||||||
|
private final int secondaryButtonTextRes;
|
||||||
|
private final EventListener secondaryButtonListener;
|
||||||
private final EventListener onVisibleListener;
|
private final EventListener onVisibleListener;
|
||||||
|
|
||||||
private Megaphone(@NonNull Builder builder) {
|
private Megaphone(@NonNull Builder builder) {
|
||||||
this.event = builder.event;
|
this.event = builder.event;
|
||||||
this.style = builder.style;
|
this.style = builder.style;
|
||||||
this.priority = builder.priority;
|
this.priority = builder.priority;
|
||||||
this.canSnooze = builder.canSnooze;
|
this.canSnooze = builder.canSnooze;
|
||||||
this.titleRes = builder.titleRes;
|
this.titleRes = builder.titleRes;
|
||||||
this.bodyRes = builder.bodyRes;
|
this.bodyRes = builder.bodyRes;
|
||||||
this.imageRequest = builder.imageRequest;
|
this.imageRequest = builder.imageRequest;
|
||||||
this.buttonTextRes = builder.buttonTextRes;
|
this.buttonTextRes = builder.buttonTextRes;
|
||||||
this.buttonListener = builder.buttonListener;
|
this.buttonListener = builder.buttonListener;
|
||||||
this.snoozeListener = builder.snoozeListener;
|
this.snoozeListener = builder.snoozeListener;
|
||||||
this.onVisibleListener = builder.onVisibleListener;
|
this.secondaryButtonTextRes = builder.secondaryButtonTextRes;
|
||||||
|
this.secondaryButtonListener = builder.secondaryButtonListener;
|
||||||
|
this.onVisibleListener = builder.onVisibleListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
public @NonNull Event getEvent() {
|
public @NonNull Event getEvent() {
|
||||||
|
@ -88,6 +92,18 @@ public class Megaphone {
|
||||||
return snoozeListener;
|
return snoozeListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public @StringRes int getSecondaryButtonText() {
|
||||||
|
return secondaryButtonTextRes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasSecondaryButton() {
|
||||||
|
return secondaryButtonTextRes != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Nullable EventListener getSecondaryButtonClickListener() {
|
||||||
|
return secondaryButtonListener;
|
||||||
|
}
|
||||||
|
|
||||||
public @Nullable EventListener getOnVisibleListener() {
|
public @Nullable EventListener getOnVisibleListener() {
|
||||||
return onVisibleListener;
|
return onVisibleListener;
|
||||||
}
|
}
|
||||||
|
@ -105,6 +121,8 @@ public class Megaphone {
|
||||||
private int buttonTextRes;
|
private int buttonTextRes;
|
||||||
private EventListener buttonListener;
|
private EventListener buttonListener;
|
||||||
private EventListener snoozeListener;
|
private EventListener snoozeListener;
|
||||||
|
private int secondaryButtonTextRes;
|
||||||
|
private EventListener secondaryButtonListener;
|
||||||
private EventListener onVisibleListener;
|
private EventListener onVisibleListener;
|
||||||
|
|
||||||
|
|
||||||
|
@ -159,6 +177,12 @@ public class Megaphone {
|
||||||
return this;
|
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) {
|
public @NonNull Builder setOnVisibleListener(@Nullable EventListener listener) {
|
||||||
this.onVisibleListener = listener;
|
this.onVisibleListener = listener;
|
||||||
return this;
|
return this;
|
||||||
|
|
|
@ -5,6 +5,7 @@ import android.content.Intent;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.StringRes;
|
import androidx.annotation.StringRes;
|
||||||
|
import androidx.fragment.app.DialogFragment;
|
||||||
|
|
||||||
public interface MegaphoneActionController {
|
public interface MegaphoneActionController {
|
||||||
/**
|
/**
|
||||||
|
@ -36,4 +37,9 @@ public interface MegaphoneActionController {
|
||||||
* Called when a megaphone completed its goal.
|
* Called when a megaphone completed its goal.
|
||||||
*/
|
*/
|
||||||
void onMegaphoneCompleted(@NonNull Megaphones.Event event);
|
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 android.content.Context;
|
||||||
|
|
||||||
import androidx.annotation.AnyThread;
|
import androidx.annotation.AnyThread;
|
||||||
import androidx.annotation.MainThread;
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.WorkerThread;
|
import androidx.annotation.WorkerThread;
|
||||||
|
@ -53,7 +52,7 @@ public class MegaphoneRepository {
|
||||||
executor.execute(() -> {
|
executor.execute(() -> {
|
||||||
database.markFinished(Event.REACTIONS);
|
database.markFinished(Event.REACTIONS);
|
||||||
database.markFinished(Event.MESSAGE_REQUESTS);
|
database.markFinished(Event.MESSAGE_REQUESTS);
|
||||||
database.markFinished(Event.MENTIONS);
|
database.markFinished(Event.RESEARCH);
|
||||||
resetDatabaseCache();
|
resetDatabaseCache();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.messagerequests.MessageRequestMegaphoneActivit
|
||||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||||
|
import org.thoughtcrime.securesms.util.ResearchMegaphone;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
@ -85,9 +86,9 @@ public final class Megaphones {
|
||||||
put(Event.PINS_FOR_ALL, new PinsForAllSchedule());
|
put(Event.PINS_FOR_ALL, new PinsForAllSchedule());
|
||||||
put(Event.PIN_REMINDER, new SignalPinReminderSchedule());
|
put(Event.PIN_REMINDER, new SignalPinReminderSchedule());
|
||||||
put(Event.MESSAGE_REQUESTS, shouldShowMessageRequestsMegaphone() ? ALWAYS : NEVER);
|
put(Event.MESSAGE_REQUESTS, shouldShowMessageRequestsMegaphone() ? ALWAYS : NEVER);
|
||||||
put(Event.MENTIONS, shouldShowMentionsMegaphone() ? ALWAYS : NEVER);
|
|
||||||
put(Event.LINK_PREVIEWS, shouldShowLinkPreviewsMegaphone(context) ? ALWAYS : NEVER);
|
put(Event.LINK_PREVIEWS, shouldShowLinkPreviewsMegaphone(context) ? ALWAYS : NEVER);
|
||||||
put(Event.CLIENT_DEPRECATED, SignalStore.misc().isClientDeprecated() ? 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);
|
return buildPinReminderMegaphone(context);
|
||||||
case MESSAGE_REQUESTS:
|
case MESSAGE_REQUESTS:
|
||||||
return buildMessageRequestsMegaphone(context);
|
return buildMessageRequestsMegaphone(context);
|
||||||
case MENTIONS:
|
|
||||||
return buildMentionsMegaphone();
|
|
||||||
case LINK_PREVIEWS:
|
case LINK_PREVIEWS:
|
||||||
return buildLinkPreviewsMegaphone();
|
return buildLinkPreviewsMegaphone();
|
||||||
case CLIENT_DEPRECATED:
|
case CLIENT_DEPRECATED:
|
||||||
return buildClientDeprecatedMegaphone(context);
|
return buildClientDeprecatedMegaphone(context);
|
||||||
|
case RESEARCH:
|
||||||
|
return buildResearchMegaphone(context);
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Event not handled!");
|
throw new IllegalArgumentException("Event not handled!");
|
||||||
}
|
}
|
||||||
|
@ -189,14 +190,6 @@ public final class Megaphones {
|
||||||
.build();
|
.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() {
|
private static @NonNull Megaphone buildLinkPreviewsMegaphone() {
|
||||||
return new Megaphone.Builder(Event.LINK_PREVIEWS, Megaphone.Style.LINK_PREVIEWS)
|
return new Megaphone.Builder(Event.LINK_PREVIEWS, Megaphone.Style.LINK_PREVIEWS)
|
||||||
.setPriority(Megaphone.Priority.HIGH)
|
.setPriority(Megaphone.Priority.HIGH)
|
||||||
|
@ -207,9 +200,22 @@ public final class Megaphones {
|
||||||
return new Megaphone.Builder(Event.CLIENT_DEPRECATED, Megaphone.Style.FULLSCREEN)
|
return new Megaphone.Builder(Event.CLIENT_DEPRECATED, Megaphone.Style.FULLSCREEN)
|
||||||
.disableSnooze()
|
.disableSnooze()
|
||||||
.setPriority(Megaphone.Priority.HIGH)
|
.setPriority(Megaphone.Priority.HIGH)
|
||||||
.setOnVisibleListener((megaphone, listener) -> {
|
.setOnVisibleListener((megaphone, listener) -> listener.onMegaphoneNavigationRequested(new Intent(context, ClientDeprecatedActivity.class)))
|
||||||
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();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -217,9 +223,8 @@ public final class Megaphones {
|
||||||
return Recipient.self().getProfileName() == ProfileName.EMPTY;
|
return Recipient.self().getProfileName() == ProfileName.EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean shouldShowMentionsMegaphone() {
|
private static boolean shouldShowResearchMegaphone() {
|
||||||
return false;
|
return ResearchMegaphone.isInResearchMegaphone();
|
||||||
// return FeatureFlags.mentions();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean shouldShowLinkPreviewsMegaphone(@NonNull Context context) {
|
private static boolean shouldShowLinkPreviewsMegaphone(@NonNull Context context) {
|
||||||
|
@ -231,9 +236,9 @@ public final class Megaphones {
|
||||||
PINS_FOR_ALL("pins_for_all"),
|
PINS_FOR_ALL("pins_for_all"),
|
||||||
PIN_REMINDER("pin_reminder"),
|
PIN_REMINDER("pin_reminder"),
|
||||||
MESSAGE_REQUESTS("message_requests"),
|
MESSAGE_REQUESTS("message_requests"),
|
||||||
MENTIONS("mentions"),
|
|
||||||
LINK_PREVIEWS("link_previews"),
|
LINK_PREVIEWS("link_previews"),
|
||||||
CLIENT_DEPRECATED("client_deprecated");
|
CLIENT_DEPRECATED("client_deprecated"),
|
||||||
|
RESEARCH("research");
|
||||||
|
|
||||||
private final String key;
|
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.annimon.stream.Stream;
|
||||||
import com.google.android.collect.Sets;
|
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.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
@ -17,7 +20,10 @@ import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob;
|
||||||
import org.thoughtcrime.securesms.jobs.RemoteConfigRefreshJob;
|
import org.thoughtcrime.securesms.jobs.RemoteConfigRefreshJob;
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
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.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
@ -65,6 +71,7 @@ public final class FeatureFlags {
|
||||||
private static final String VERIFY_V2 = "android.verifyV2";
|
private static final String VERIFY_V2 = "android.verifyV2";
|
||||||
private static final String PHONE_NUMBER_PRIVACY_VERSION = "android.phoneNumberPrivacyVersion";
|
private static final String PHONE_NUMBER_PRIVACY_VERSION = "android.phoneNumberPrivacyVersion";
|
||||||
private static final String CLIENT_EXPIRATION = "android.clientExpiration";
|
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
|
* 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,
|
USERNAMES,
|
||||||
MENTIONS,
|
MENTIONS,
|
||||||
VERIFY_V2,
|
VERIFY_V2,
|
||||||
CLIENT_EXPIRATION
|
CLIENT_EXPIRATION,
|
||||||
|
RESEARCH_MEGAPHONE_1
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -283,6 +291,11 @@ public final class FeatureFlags {
|
||||||
return getString(CLIENT_EXPIRATION, null);
|
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 the user can choose phone number privacy settings, and;
|
||||||
* Whether to fetch and store the secondary certificate
|
* 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
|
* 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
|
* useful for when exceptions are thrown inside of asynchronous systems (like runnables in an
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 8.7 KiB |
Binary file not shown.
After Width: | Height: | Size: 5.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
|
@ -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"
|
android:scaleType="centerInside"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:src="@drawable/profile_splash"/>
|
tools:src="@tools:sample/avatars"/>
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||||
android:id="@+id/basic_megaphone_title"
|
android:id="@+id/basic_megaphone_title"
|
||||||
|
@ -63,14 +63,14 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
style="@style/Button.Borderless"
|
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_constraintTop_toBottomOf="@id/basic_megaphone_content_barrier"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
tools:text="*sigh*"
|
tools:text="*sigh*"
|
||||||
tools:visibility="visible"/>
|
tools:visibility="visible"/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/basic_megaphone_snooze"
|
android:id="@+id/basic_megaphone_secondary"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
File diff suppressed because one or more lines are too long
|
@ -40,6 +40,19 @@
|
||||||
<item name="android:textColor">@null</item>
|
<item name="android:textColor">@null</item>
|
||||||
</style>
|
</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 -->
|
<!-- ActionBar styles -->
|
||||||
<style name="TextSecure.DarkActionBar"
|
<style name="TextSecure.DarkActionBar"
|
||||||
parent="@style/Widget.AppCompat.ActionBar">
|
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…
Reference in New Issue