Add a megaphone to celebrate Valentine's Day.

fork-5.53.8
Greyson Parrelli 2022-02-09 19:16:19 -05:00
rodzic 65af5f0849
commit 597cf3f576
5 zmienionych plików z 142 dodań i 18 usunięć

Wyświetl plik

@ -7,6 +7,7 @@ import android.provider.Settings;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Stream; 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.R;
import org.thoughtcrime.securesms.badges.models.Badge; import org.thoughtcrime.securesms.badges.models.Badge;
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity; 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.database.model.MegaphoneRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; 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.SignalPinReminders;
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity; import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
import org.thoughtcrime.securesms.lock.v2.KbsMigrationActivity; import org.thoughtcrime.securesms.lock.v2.KbsMigrationActivity;
import org.thoughtcrime.securesms.messagerequests.MessageRequestMegaphoneActivity;
import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.profiles.manage.ManageProfileActivity; import org.thoughtcrime.securesms.profiles.manage.ManageProfileActivity;
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.LocaleFeatureFlags; import org.thoughtcrime.securesms.util.LocaleFeatureFlags;
import org.thoughtcrime.securesms.util.PlayServicesUtil; 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.VersionTracker;
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper; import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaperActivity; import org.thoughtcrime.securesms.wallpaper.ChatWallpaperActivity;
import java.time.LocalDateTime;
import java.time.Month;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
* Creating a new megaphone: * Creating a new megaphone:
* - Add an enum to {@link Event} * - Add an enum to {@link Event}
* - Return a megaphone in {@link #forRecord(Context, MegaphoneRecord)} * - 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: * Common patterns:
* - For events that have a snooze-able recurring display schedule, use a {@link RecurringSchedule}. * - 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 * - 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)} * - For events that change, return different megaphones in {@link #forRecord(Context, MegaphoneRecord)}
* based on whatever properties you're interested in. * 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 ALWAYS = new ForeverSchedule(true);
private static final MegaphoneSchedule NEVER = new ForeverSchedule(false); 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() {} private Megaphones() {}
@WorkerThread
static @Nullable Megaphone getNextMegaphone(@NonNull Context context, @NonNull Map<Event, MegaphoneRecord> records) { static @Nullable Megaphone getNextMegaphone(@NonNull Context context, @NonNull Map<Event, MegaphoneRecord> records) {
long currentTime = System.currentTimeMillis(); long currentTime = System.currentTimeMillis();
List<Megaphone> megaphones = Stream.of(buildDisplayOrder(context)) List<Megaphone> megaphones = Stream.of(buildDisplayOrder(context, records))
.filter(e -> { .filter(e -> {
MegaphoneRecord record = Objects.requireNonNull(records.get(e.getKey())); MegaphoneRecord record = Objects.requireNonNull(records.get(e.getKey()));
MegaphoneSchedule schedule = e.getValue(); 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}. * 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>() {{ return new LinkedHashMap<Event, MegaphoneSchedule>() {{
put(Event.PINS_FOR_ALL, new PinsForAllSchedule()); put(Event.PINS_FOR_ALL, new PinsForAllSchedule());
put(Event.CLIENT_DEPRECATED, SignalStore.misc().isClientDeprecated() ? ALWAYS : NEVER); put(Event.CLIENT_DEPRECATED, SignalStore.misc().isClientDeprecated() ? ALWAYS : NEVER);
put(Event.NOTIFICATIONS, shouldShowNotificationsMegaphone(context) ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(30)) : NEVER); put(Event.NOTIFICATIONS, shouldShowNotificationsMegaphone(context) ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(30)) : NEVER);
put(Event.ONBOARDING, shouldShowOnboardingMegaphone(context) ? ALWAYS : 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()); put(Event.PIN_REMINDER, new SignalPinReminderSchedule());
// Feature-introduction megaphones should *probably* be added below this divider // Feature-introduction megaphones should *probably* be added below this divider
@ -129,6 +135,8 @@ public final class Megaphones {
return buildAddAProfilePhotoMegaphone(context); return buildAddAProfilePhotoMegaphone(context);
case BECOME_A_SUSTAINER: case BECOME_A_SUSTAINER:
return buildBecomeASustainerMegaphone(context); return buildBecomeASustainerMegaphone(context);
case VALENTINES_DONATIONS_2022:
return buildValentinesDonationsMegaphone(context);
case NOTIFICATION_PROFILES: case NOTIFICATION_PROFILES:
return buildNotificationProfilesMegaphone(context); return buildNotificationProfilesMegaphone(context);
default: default:
@ -275,6 +283,21 @@ public final class Megaphones {
.build(); .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) { private static @NonNull Megaphone buildNotificationProfilesMegaphone(@NonNull Context context) {
return new Megaphone.Builder(Event.NOTIFICATION_PROFILES, Megaphone.Style.BASIC) return new Megaphone.Builder(Event.NOTIFICATION_PROFILES, Megaphone.Style.BASIC)
.setTitle(R.string.NotificationProfilesMegaphone__notification_profiles) .setTitle(R.string.NotificationProfilesMegaphone__notification_profiles)
@ -290,8 +313,11 @@ public final class Megaphones {
.build(); .build();
} }
private static boolean shouldShowDonateMegaphone(@NonNull Context context) { private static boolean shouldShowDonateMegaphone(@NonNull Context context, @NonNull Map<Event, MegaphoneRecord> records) {
return VersionTracker.getDaysSinceFirstInstalled(context) >= 7 && long timeSinceLastDonatePrompt = timeSinceLastDonatePrompt(records);
return timeSinceLastDonatePrompt > MIN_TIME_BETWEEN_DONATE_MEGAPHONES &&
VersionTracker.getDaysSinceFirstInstalled(context) >= 7 &&
LocaleFeatureFlags.isInDonateMegaphone() && LocaleFeatureFlags.isInDonateMegaphone() &&
PlayServicesUtil.getPlayServicesStatus(context) == PlayServicesUtil.PlayServicesStatus.SUCCESS && PlayServicesUtil.getPlayServicesStatus(context) == PlayServicesUtil.PlayServicesStatus.SUCCESS &&
Recipient.self() Recipient.self()
@ -301,6 +327,24 @@ public final class Megaphones {
.noneMatch(badge -> badge.getCategory() == Badge.Category.Donor); .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) { private static boolean shouldShowOnboardingMegaphone(@NonNull Context context) {
return SignalStore.onboarding().hasOnboarding(context); return SignalStore.onboarding().hasOnboarding(context);
} }
@ -316,7 +360,8 @@ public final class Megaphones {
.textExistsInUsersLanguage(R.string.NotificationsMegaphone_turn_on_notifications, .textExistsInUsersLanguage(R.string.NotificationsMegaphone_turn_on_notifications,
R.string.NotificationsMegaphone_never_miss_a_message, R.string.NotificationsMegaphone_never_miss_a_message,
R.string.NotificationsMegaphone_turn_on, 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); Log.i(TAG, "Would show NotificationsMegaphone but is not yet translated in " + locale);
return false; return false;
} }
@ -338,6 +383,23 @@ public final class Megaphones {
return true; 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 { public enum Event {
PINS_FOR_ALL("pins_for_all"), PINS_FOR_ALL("pins_for_all"),
PIN_REMINDER("pin_reminder"), PIN_REMINDER("pin_reminder"),
@ -347,6 +409,7 @@ public final class Megaphones {
CHAT_COLORS("chat_colors"), CHAT_COLORS("chat_colors"),
ADD_A_PROFILE_PHOTO("add_a_profile_photo"), ADD_A_PROFILE_PHOTO("add_a_profile_photo"),
BECOME_A_SUSTAINER("become_a_sustainer"), BECOME_A_SUSTAINER("become_a_sustainer"),
VALENTINES_DONATIONS_2022("valentines_donations_2022"),
NOTIFICATION_PROFILES("notification_profiles"); NOTIFICATION_PROFILES("notification_profiles");
private final String key; private final String key;

Wyświetl plik

@ -63,6 +63,7 @@ public final class FeatureFlags {
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 DONATE_MEGAPHONE = "android.donate.2"; 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 CUSTOM_VIDEO_MUXER = "android.customVideoMuxer";
private static final String CDS_REFRESH_INTERVAL = "cds.syncInterval.seconds"; private static final String CDS_REFRESH_INTERVAL = "cds.syncInterval.seconds";
private static final String AUTOMATIC_SESSION_RESET = "android.automaticSessionReset.2"; private static final String AUTOMATIC_SESSION_RESET = "android.automaticSessionReset.2";
@ -132,7 +133,8 @@ public final class FeatureFlags {
DONOR_BADGES_DISPLAY, DONOR_BADGES_DISPLAY,
CHANGE_NUMBER_ENABLED, CHANGE_NUMBER_ENABLED,
HARDWARE_AEC_MODELS, HARDWARE_AEC_MODELS,
FORCE_DEFAULT_AEC FORCE_DEFAULT_AEC,
VALENTINES_DONATE_MEGAPHONE
); );
@VisibleForTesting @VisibleForTesting
@ -187,7 +189,8 @@ public final class FeatureFlags {
SENDER_KEY_MAX_AGE, SENDER_KEY_MAX_AGE,
DONOR_BADGES_DISPLAY, DONOR_BADGES_DISPLAY,
DONATE_MEGAPHONE, DONATE_MEGAPHONE,
FORCE_DEFAULT_AEC FORCE_DEFAULT_AEC,
VALENTINES_DONATE_MEGAPHONE
); );
/** /**
@ -303,6 +306,11 @@ public final class FeatureFlags {
return getString(DONATE_MEGAPHONE, ""); 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 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

Wyświetl plik

@ -37,6 +37,13 @@ public final class LocaleFeatureFlags {
return isEnabled(FeatureFlags.DONATE_MEGAPHONE, FeatureFlags.donateMegaphone()); 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() { public static @NonNull Optional<PushMediaConstraints.MediaConfig> getMediaQualityLevel() {
Map<String, Integer> countryValues = parseCountryValues(FeatureFlags.getMediaQualityLevels(), NOT_FOUND); Map<String, Integer> countryValues = parseCountryValues(FeatureFlags.getMediaQualityLevels(), NOT_FOUND);
int level = getCountryValue(countryValues, Recipient.self().getE164().or(""), NOT_FOUND); int level = getCountryValue(countryValues, Recipient.self().getE164().or(""), NOT_FOUND);

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -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_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> <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 --> <!-- WebRtcCallActivity -->
<string name="WebRtcCallActivity__tap_here_to_turn_on_your_video">Tap here to turn on your video</string> <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> <string name="WebRtcCallActivity__to_call_s_signal_needs_access_to_your_camera">To call %1$s, Signal needs access to your camera</string>