diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java index 15d8d38893..ebd3428bd9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java @@ -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 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 records) { long currentTime = System.currentTimeMillis(); - List megaphones = Stream.of(buildDisplayOrder(context)) + List 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 buildDisplayOrder(@NonNull Context context) { + private static Map buildDisplayOrder(@NonNull Context context, @NonNull Map records) { return new LinkedHashMap() {{ 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 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 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 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; diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index d743b4f290..749e9dc7db 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/LocaleFeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/LocaleFeatureFlags.java index fa4342be2a..f7d0f9e840 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/LocaleFeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/LocaleFeatureFlags.java @@ -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 getMediaQualityLevel() { Map countryValues = parseCountryValues(FeatureFlags.getMediaQualityLevels(), NOT_FOUND); int level = getCountryValue(countryValues, Recipient.self().getE164().or(""), NOT_FOUND); diff --git a/app/src/main/res/drawable/ic_valentines_donor_megaphone_64.xml b/app/src/main/res/drawable/ic_valentines_donor_megaphone_64.xml new file mode 100644 index 0000000000..f76b77ef22 --- /dev/null +++ b/app/src/main/res/drawable/ic_valentines_donor_megaphone_64.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a93f76b81e..caf7aa6dd2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1445,6 +1445,12 @@ The number you dialed does not support secure voice! Got it + + + Happy 💜 Day! + + Show your affection by becoming a Signal sustainer. + Tap here to turn on your video To call %1$s, Signal needs access to your camera