Add a megaphone to celebrate Valentine's Day.

This commit is contained in:
Greyson Parrelli 2022-02-09 19:16:19 -05:00
parent 65af5f0849
commit 597cf3f576
5 changed files with 142 additions and 18 deletions

View file

@ -7,6 +7,7 @@ import android.provider.Settings;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Stream;
@ -15,7 +16,6 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.badges.models.Badge;
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
import org.thoughtcrime.securesms.conversationlist.ConversationListFragment;
import org.thoughtcrime.securesms.database.model.MegaphoneRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
@ -24,37 +24,38 @@ import org.thoughtcrime.securesms.lock.SignalPinReminderDialog;
import org.thoughtcrime.securesms.lock.SignalPinReminders;
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
import org.thoughtcrime.securesms.lock.v2.KbsMigrationActivity;
import org.thoughtcrime.securesms.messagerequests.MessageRequestMegaphoneActivity;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.profiles.manage.ManageProfileActivity;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.LocaleFeatureFlags;
import org.thoughtcrime.securesms.util.PlayServicesUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.SetUtil;
import org.thoughtcrime.securesms.util.VersionTracker;
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaperActivity;
import java.time.LocalDateTime;
import java.time.Month;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* Creating a new megaphone:
* - Add an enum to {@link Event}
* - Return a megaphone in {@link #forRecord(Context, MegaphoneRecord)}
* - Include the event in {@link #buildDisplayOrder(Context)}
* - Include the event in {@link #buildDisplayOrder(Context, Map)}
*
* Common patterns:
* - For events that have a snooze-able recurring display schedule, use a {@link RecurringSchedule}.
* - For events guarded by feature flags, set a {@link ForeverSchedule} with false in
* {@link #buildDisplayOrder(Context)}.
* {@link #buildDisplayOrder(Context, Map)}.
* - For events that change, return different megaphones in {@link #forRecord(Context, MegaphoneRecord)}
* based on whatever properties you're interested in.
*/
@ -65,12 +66,16 @@ public final class Megaphones {
private static final MegaphoneSchedule ALWAYS = new ForeverSchedule(true);
private static final MegaphoneSchedule NEVER = new ForeverSchedule(false);
private static final Set<Event> DONATE_EVENTS = SetUtil.newHashSet(Event.VALENTINES_DONATIONS_2022, Event.BECOME_A_SUSTAINER);
private static final long MIN_TIME_BETWEEN_DONATE_MEGAPHONES = TimeUnit.DAYS.toMillis(30);
private Megaphones() {}
@WorkerThread
static @Nullable Megaphone getNextMegaphone(@NonNull Context context, @NonNull Map<Event, MegaphoneRecord> records) {
long currentTime = System.currentTimeMillis();
List<Megaphone> megaphones = Stream.of(buildDisplayOrder(context))
List<Megaphone> megaphones = Stream.of(buildDisplayOrder(context, records))
.filter(e -> {
MegaphoneRecord record = Objects.requireNonNull(records.get(e.getKey()));
MegaphoneSchedule schedule = e.getValue();
@ -95,13 +100,14 @@ public final class Megaphones {
*
* This is also when you would hide certain megaphones based on things like {@link FeatureFlags}.
*/
private static Map<Event, MegaphoneSchedule> buildDisplayOrder(@NonNull Context context) {
private static Map<Event, MegaphoneSchedule> buildDisplayOrder(@NonNull Context context, @NonNull Map<Event, MegaphoneRecord> records) {
return new LinkedHashMap<Event, MegaphoneSchedule>() {{
put(Event.PINS_FOR_ALL, new PinsForAllSchedule());
put(Event.CLIENT_DEPRECATED, SignalStore.misc().isClientDeprecated() ? ALWAYS : NEVER);
put(Event.NOTIFICATIONS, shouldShowNotificationsMegaphone(context) ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(30)) : NEVER);
put(Event.ONBOARDING, shouldShowOnboardingMegaphone(context) ? ALWAYS : NEVER);
put(Event.BECOME_A_SUSTAINER, shouldShowDonateMegaphone(context) ? ShowForDurationSchedule.showForDays(7) : NEVER);
put(Event.BECOME_A_SUSTAINER, shouldShowDonateMegaphone(context, records) ? ShowForDurationSchedule.showForDays(7) : NEVER);
put(Event.VALENTINES_DONATIONS_2022, shouldShowValentinesDonationsMegaphone(context, records) ? ShowForDurationSchedule.showForDays(1) : NEVER);
put(Event.PIN_REMINDER, new SignalPinReminderSchedule());
// Feature-introduction megaphones should *probably* be added below this divider
@ -129,6 +135,8 @@ public final class Megaphones {
return buildAddAProfilePhotoMegaphone(context);
case BECOME_A_SUSTAINER:
return buildBecomeASustainerMegaphone(context);
case VALENTINES_DONATIONS_2022:
return buildValentinesDonationsMegaphone(context);
case NOTIFICATION_PROFILES:
return buildNotificationProfilesMegaphone(context);
default:
@ -275,6 +283,21 @@ public final class Megaphones {
.build();
}
private static @NonNull Megaphone buildValentinesDonationsMegaphone(@NonNull Context context) {
return new Megaphone.Builder(Event.VALENTINES_DONATIONS_2022, Megaphone.Style.BASIC)
.setTitle(R.string.ValentinesDayMegaphone_happy_heart_day)
.setImage(R.drawable.ic_valentines_donor_megaphone_64)
.setBody(R.string.ValentinesDayMegaphone_show_your_affection)
.setActionButton(R.string.BecomeASustainerMegaphone__contribute, (megaphone, listener) -> {
listener.onMegaphoneNavigationRequested(AppSettingsActivity.subscriptions(context));
listener.onMegaphoneCompleted(Event.VALENTINES_DONATIONS_2022);
})
.setSecondaryButton(R.string.BecomeASustainerMegaphone__no_thanks, (megaphone, listener) -> {
listener.onMegaphoneCompleted(Event.VALENTINES_DONATIONS_2022);
})
.build();
}
private static @NonNull Megaphone buildNotificationProfilesMegaphone(@NonNull Context context) {
return new Megaphone.Builder(Event.NOTIFICATION_PROFILES, Megaphone.Style.BASIC)
.setTitle(R.string.NotificationProfilesMegaphone__notification_profiles)
@ -290,15 +313,36 @@ public final class Megaphones {
.build();
}
private static boolean shouldShowDonateMegaphone(@NonNull Context context) {
return VersionTracker.getDaysSinceFirstInstalled(context) >= 7 &&
private static boolean shouldShowDonateMegaphone(@NonNull Context context, @NonNull Map<Event, MegaphoneRecord> records) {
long timeSinceLastDonatePrompt = timeSinceLastDonatePrompt(records);
return timeSinceLastDonatePrompt > MIN_TIME_BETWEEN_DONATE_MEGAPHONES &&
VersionTracker.getDaysSinceFirstInstalled(context) >= 7 &&
LocaleFeatureFlags.isInDonateMegaphone() &&
PlayServicesUtil.getPlayServicesStatus(context) == PlayServicesUtil.PlayServicesStatus.SUCCESS &&
Recipient.self()
.getBadges()
.stream()
.filter(Objects::nonNull)
.noneMatch(badge -> badge.getCategory() == Badge.Category.Donor);
.getBadges()
.stream()
.filter(Objects::nonNull)
.noneMatch(badge -> badge.getCategory() == Badge.Category.Donor);
}
private static boolean shouldShowValentinesDonationsMegaphone(@NonNull Context context, @NonNull Map<Event, MegaphoneRecord> records) {
LocalDateTime now = LocalDateTime.now();
long timeSinceLastDonatePrompt = timeSinceLastDonatePrompt(records);
return timeSinceLastDonatePrompt > MIN_TIME_BETWEEN_DONATE_MEGAPHONES &&
VersionTracker.getDaysSinceFirstInstalled(context) >= 7 &&
LocaleFeatureFlags.isInValentinesDonateMegaphone() &&
now.getMonth() == Month.FEBRUARY &&
now.getDayOfMonth() == 14 &&
now.getYear() == 2022 &&
PlayServicesUtil.getPlayServicesStatus(context) == PlayServicesUtil.PlayServicesStatus.SUCCESS &&
Recipient.self()
.getBadges()
.stream()
.filter(Objects::nonNull)
.noneMatch(badge -> badge.getCategory() == Badge.Category.Donor);
}
private static boolean shouldShowOnboardingMegaphone(@NonNull Context context) {
@ -316,7 +360,8 @@ public final class Megaphones {
.textExistsInUsersLanguage(R.string.NotificationsMegaphone_turn_on_notifications,
R.string.NotificationsMegaphone_never_miss_a_message,
R.string.NotificationsMegaphone_turn_on,
R.string.NotificationsMegaphone_not_now)) {
R.string.NotificationsMegaphone_not_now))
{
Log.i(TAG, "Would show NotificationsMegaphone but is not yet translated in " + locale);
return false;
}
@ -338,6 +383,23 @@ public final class Megaphones {
return true;
}
/**
* Unfortunately lastSeen is only set today upon snoozing, which never happens to donate prompts.
* So we use firstVisible as a proxy.
*/
private static long timeSinceLastDonatePrompt(@NonNull Map<Event, MegaphoneRecord> records) {
long lastSeenDonatePrompt = records.entrySet()
.stream()
.filter(e -> DONATE_EVENTS.contains(e.getKey()))
.map(e -> e.getValue().getFirstVisible())
.filter(t -> t > 0)
.sorted()
.findFirst()
.orElse(0L);
return System.currentTimeMillis() - lastSeenDonatePrompt;
}
public enum Event {
PINS_FOR_ALL("pins_for_all"),
PIN_REMINDER("pin_reminder"),
@ -347,6 +409,7 @@ public final class Megaphones {
CHAT_COLORS("chat_colors"),
ADD_A_PROFILE_PHOTO("add_a_profile_photo"),
BECOME_A_SUSTAINER("become_a_sustainer"),
VALENTINES_DONATIONS_2022("valentines_donations_2022"),
NOTIFICATION_PROFILES("notification_profiles");
private final String key;

View file

@ -63,6 +63,7 @@ public final class FeatureFlags {
private static final String PHONE_NUMBER_PRIVACY_VERSION = "android.phoneNumberPrivacyVersion";
private static final String CLIENT_EXPIRATION = "android.clientExpiration";
public static final String DONATE_MEGAPHONE = "android.donate.2";
public static final String VALENTINES_DONATE_MEGAPHONE = "android.donate.valentines.2022";
private static final String CUSTOM_VIDEO_MUXER = "android.customVideoMuxer";
private static final String CDS_REFRESH_INTERVAL = "cds.syncInterval.seconds";
private static final String AUTOMATIC_SESSION_RESET = "android.automaticSessionReset.2";
@ -132,7 +133,8 @@ public final class FeatureFlags {
DONOR_BADGES_DISPLAY,
CHANGE_NUMBER_ENABLED,
HARDWARE_AEC_MODELS,
FORCE_DEFAULT_AEC
FORCE_DEFAULT_AEC,
VALENTINES_DONATE_MEGAPHONE
);
@VisibleForTesting
@ -187,7 +189,8 @@ public final class FeatureFlags {
SENDER_KEY_MAX_AGE,
DONOR_BADGES_DISPLAY,
DONATE_MEGAPHONE,
FORCE_DEFAULT_AEC
FORCE_DEFAULT_AEC,
VALENTINES_DONATE_MEGAPHONE
);
/**
@ -303,6 +306,11 @@ public final class FeatureFlags {
return getString(DONATE_MEGAPHONE, "");
}
/** The raw valentine's day donate megaphone CSV string */
public static String valentinesDonateMegaphone() {
return getString(VALENTINES_DONATE_MEGAPHONE, "");
}
/**
* Whether the user can choose phone number privacy settings, and;
* Whether to fetch and store the secondary certificate

View file

@ -37,6 +37,13 @@ public final class LocaleFeatureFlags {
return isEnabled(FeatureFlags.DONATE_MEGAPHONE, FeatureFlags.donateMegaphone());
}
/**
* In valentines donation megaphone group for given country code
*/
public static boolean isInValentinesDonateMegaphone() {
return isEnabled(FeatureFlags.VALENTINES_DONATE_MEGAPHONE, FeatureFlags.valentinesDonateMegaphone());
}
public static @NonNull Optional<PushMediaConstraints.MediaConfig> getMediaQualityLevel() {
Map<String, Integer> countryValues = parseCountryValues(FeatureFlags.getMediaQualityLevels(), NOT_FOUND);
int level = getCountryValue(countryValues, Recipient.self().getE164().or(""), NOT_FOUND);

File diff suppressed because one or more lines are too long

View file

@ -1445,6 +1445,12 @@
<string name="RedPhone_the_number_you_dialed_does_not_support_secure_voice">The number you dialed does not support secure voice!</string>
<string name="RedPhone_got_it">Got it</string>
<!-- Valentine's Day Megaphone -->
<!-- Title text for the Valentine's Day donation megaphone. The placeholder will always be a heart emoji. Needs to be a placeholder for Android reasons. -->
<string name="ValentinesDayMegaphone_happy_heart_day">Happy 💜 Day!</string>
<!-- Body text for the Valentine's Day donation megaphone. -->
<string name="ValentinesDayMegaphone_show_your_affection">Show your affection by becoming a Signal sustainer.</string>
<!-- WebRtcCallActivity -->
<string name="WebRtcCallActivity__tap_here_to_turn_on_your_video">Tap here to turn on your video</string>
<string name="WebRtcCallActivity__to_call_s_signal_needs_access_to_your_camera">To call %1$s, Signal needs access to your camera</string>