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

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