diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java index 03a94645cd..bd3f19d498 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java @@ -66,9 +66,10 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences; public class ApplicationPreferencesActivity extends PassphraseRequiredActivity implements SharedPreferences.OnSharedPreferenceChangeListener { - public static final String LAUNCH_TO_BACKUPS_FRAGMENT = "launch.to.backups.fragment"; - public static final String LAUNCH_TO_HELP_FRAGMENT = "launch.to.help.fragment"; - public static final String LAUNCH_TO_PROXY_FRAGMENT = "launch.to.proxy.fragment"; + public static final String LAUNCH_TO_BACKUPS_FRAGMENT = "launch.to.backups.fragment"; + public static final String LAUNCH_TO_HELP_FRAGMENT = "launch.to.help.fragment"; + public static final String LAUNCH_TO_PROXY_FRAGMENT = "launch.to.proxy.fragment"; + public static final String LAUNCH_TO_NOTIFICATIONS_FRAGMENT = "launch.to.notifications.fragment"; @SuppressWarnings("unused") private static final String TAG = ApplicationPreferencesActivity.class.getSimpleName(); @@ -112,6 +113,8 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity initFragment(android.R.id.content, new HelpFragment()); } else if (getIntent() != null && getIntent().getBooleanExtra(LAUNCH_TO_PROXY_FRAGMENT, false)) { initFragment(android.R.id.content, EditProxyFragment.newInstance()); + } else if (getIntent() != null && getIntent().getBooleanExtra(LAUNCH_TO_NOTIFICATIONS_FRAGMENT, false)) { + initFragment(android.R.id.content, new NotificationsPreferenceFragment()); } else if (icicle == null) { initFragment(android.R.id.content, new ApplicationPreferenceFragment()); } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/BasicMegaphoneView.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/BasicMegaphoneView.java index 0de9510e63..28ffd79f6d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/BasicMegaphoneView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/BasicMegaphoneView.java @@ -56,7 +56,10 @@ public class BasicMegaphoneView extends FrameLayout { this.megaphone = megaphone; this.megaphoneListener = megaphoneListener; - if (megaphone.getImageRequest() != null) { + if (megaphone.getImageRes() != 0) { + image.setVisibility(VISIBLE); + image.setImageResource(megaphone.getImageRes()); + } else if (megaphone.getImageRequest() != null) { image.setVisibility(VISIBLE); megaphone.getImageRequest().into(image); } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphone.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphone.java index a6cdd69b8f..d3e8df29b4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphone.java +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphone.java @@ -24,6 +24,7 @@ public class Megaphone { private final boolean canSnooze; private final int titleRes; private final int bodyRes; + private final int imageRes; private final GlideRequest imageRequest; private final int buttonTextRes; private final EventListener buttonListener; @@ -39,6 +40,7 @@ public class Megaphone { this.canSnooze = builder.canSnooze; this.titleRes = builder.titleRes; this.bodyRes = builder.bodyRes; + this.imageRes = builder.imageRes; this.imageRequest = builder.imageRequest; this.buttonTextRes = builder.buttonTextRes; this.buttonListener = builder.buttonListener; @@ -72,6 +74,10 @@ public class Megaphone { return bodyRes; } + public @DrawableRes int getImageRes() { + return imageRes; + } + public @Nullable GlideRequest getImageRequest() { return imageRequest; } @@ -117,6 +123,7 @@ public class Megaphone { private boolean canSnooze; private int titleRes; private int bodyRes; + private int imageRes; private GlideRequest imageRequest; private int buttonTextRes; private EventListener buttonListener; @@ -163,7 +170,8 @@ public class Megaphone { } public @NonNull Builder setImage(@DrawableRes int imageRes) { - return setImageRequest(GlideApp.with(ApplicationDependencies.getApplication()).load(imageRes)); + this.imageRes = imageRes; + return this; } public @NonNull Builder setImageRequest(@Nullable GlideRequest imageRequest) { 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 3467a94278..5f7113595e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/Megaphones.java @@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.megaphone; import android.content.Context; import android.content.Intent; +import android.os.Build; +import android.provider.Settings; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -9,6 +11,7 @@ import androidx.annotation.Nullable; import com.annimon.stream.Stream; import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.ApplicationPreferencesActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.conversationlist.ConversationListFragment; import org.thoughtcrime.securesms.database.model.MegaphoneRecord; @@ -20,6 +23,7 @@ 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.ProfileName; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.CommunicationActions; @@ -32,6 +36,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.TimeUnit; /** * Creating a new megaphone: @@ -94,6 +99,7 @@ public final class Megaphones { put(Event.DONATE, shouldShowDonateMegaphone(context) ? ShowForDurationSchedule.showForDays(7) : NEVER); put(Event.GROUP_CALLING, shouldShowGroupCallingMegaphone() ? ALWAYS : NEVER); put(Event.ONBOARDING, shouldShowOnboardingMegaphone(context) ? ALWAYS : NEVER); + put(Event.NOTIFICATIONS, shouldShowNotificationsMegaphone(context) ? RecurringSchedule.every(TimeUnit.DAYS.toMillis(30)) : NEVER); }}; } @@ -119,6 +125,8 @@ public final class Megaphones { return buildGroupCallingMegaphone(context); case ONBOARDING: return buildOnboardingMegaphone(); + case NOTIFICATIONS: + return buildNotificationsMegaphone(context); default: throw new IllegalArgumentException("Event not handled!"); } @@ -264,6 +272,34 @@ public final class Megaphones { .build(); } + private static @NonNull Megaphone buildNotificationsMegaphone(@NonNull Context context) { + return new Megaphone.Builder(Event.NOTIFICATIONS, Megaphone.Style.BASIC) + .setTitle(R.string.NotificationsMegaphone_turn_on_notifications) + .setBody(R.string.NotificationsMegaphone_never_miss_a_message) + .setImage(R.drawable.megaphone_notifications_64) + .setActionButton(R.string.NotificationsMegaphone_turn_on, (megaphone, controller) -> { + controller.onMegaphoneSnooze(Event.NOTIFICATIONS); + + if (Build.VERSION.SDK_INT >= 26 && !NotificationChannels.isMessageChannelEnabled(context)) { + Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS); + intent.putExtra(Settings.EXTRA_CHANNEL_ID, NotificationChannels.getMessagesChannel(context)); + intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName()); + controller.onMegaphoneNavigationRequested(intent); + } else if (Build.VERSION.SDK_INT >= 26 && !NotificationChannels.areNotificationsEnabled(context)) { + Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS); + intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName()); + controller.onMegaphoneNavigationRequested(intent); + } else { + Intent intent = new Intent(context, ApplicationPreferencesActivity.class); + intent.putExtra(ApplicationPreferencesActivity.LAUNCH_TO_NOTIFICATIONS_FRAGMENT, true); + controller.onMegaphoneNavigationRequested(intent); + } + }) + .setSecondaryButton(R.string.NotificationsMegaphone_not_now, (megaphone, controller) -> controller.onMegaphoneSnooze(Event.NOTIFICATIONS)) + .setPriority(Megaphone.Priority.DEFAULT) + .build(); + } + private static boolean shouldShowMessageRequestsMegaphone() { return Recipient.self().getProfileName() == ProfileName.EMPTY; } @@ -288,6 +324,12 @@ public final class Megaphones { return SignalStore.onboarding().hasOnboarding(context); } + private static boolean shouldShowNotificationsMegaphone(@NonNull Context context) { + return !TextSecurePreferences.isNotificationsEnabled(context) || + !NotificationChannels.isMessageChannelEnabled(context) || + !NotificationChannels.areNotificationsEnabled(context); + } + public enum Event { REACTIONS("reactions"), PINS_FOR_ALL("pins_for_all"), @@ -298,7 +340,8 @@ public final class Megaphones { RESEARCH("research"), DONATE("donate"), GROUP_CALLING("group_calling"), - ONBOARDING("onboarding"); + ONBOARDING("onboarding"), + NOTIFICATIONS("notifications"); private final String key; diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/RecurringSchedule.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/RecurringSchedule.java index 90fe17dd5f..d9a957ff4a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/RecurringSchedule.java +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/RecurringSchedule.java @@ -1,13 +1,34 @@ package org.thoughtcrime.securesms.megaphone; +import androidx.annotation.NonNull; + +/** + * A schedule that provides a high level of control, allowing you to specify an amount of time to + * wait based on how many times a user has seen the megaphone. + */ class RecurringSchedule implements MegaphoneSchedule { private final long[] gaps; + /** + * How long to wait after each time a user has seen the megaphone. Index 0 corresponds to how long + * to wait to show it again after the user has seen it once, index 1 is for after the user has + * seen it twice, etc. If the seen count is greater than the number of provided intervals, it will + * continue to use the last interval provided indefinitely. + * + * The schedule will always show the megaphone if the user has never seen it. + */ RecurringSchedule(long... durationGaps) { this.gaps = durationGaps; } + /** + * Shortcut for a recurring schedule with a single interval. + */ + public static @NonNull MegaphoneSchedule every(long interval) { + return new RecurringSchedule(interval); + } + @Override public boolean shouldDisplay(int seenCount, long lastSeen, long firstVisible, long currentTime) { if (seenCount == 0) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationChannels.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationChannels.java index 2a9cb66e3c..de9fd1e31d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationChannels.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/NotificationChannels.java @@ -376,6 +376,36 @@ public class NotificationChannels { ensureCustomChannelConsistency(context); } + /** + * Whether or not the default messages notification channel is enabled. Note that "enabled" just + * means receiving notifications in some capacity -- a user could have it enabled, but set it to a + * lower importance. + * + * This could also return true if the specific channnel is enabled, but notifications *overall* + * are disabled. Check {@link #areNotificationsEnabled(Context)} to be safe. + */ + public static synchronized boolean isMessageChannelEnabled(@NonNull Context context) { + if (!supported()) { + return true; + } + + NotificationManager notificationManager = ServiceUtil.getNotificationManager(context); + NotificationChannel channel = notificationManager.getNotificationChannel(getMessagesChannel(context)); + + return channel != null && channel.getImportance() != NotificationManager.IMPORTANCE_NONE; + } + + /** + * Whether or not notifications for the entire app are enabled. + */ + public static synchronized boolean areNotificationsEnabled(@NonNull Context context) { + if (Build.VERSION.SDK_INT >= 24) { + return ServiceUtil.getNotificationManager(context).areNotificationsEnabled(); + } else { + return true; + } + } + /** * Updates the name of an existing channel to match the recipient's current name. Will have no * effect if the recipient doesn't have an existing valid channel. diff --git a/app/src/main/res/drawable-hdpi/megaphone_notifications_64.webp b/app/src/main/res/drawable-hdpi/megaphone_notifications_64.webp new file mode 100644 index 0000000000..fc819496fb Binary files /dev/null and b/app/src/main/res/drawable-hdpi/megaphone_notifications_64.webp differ diff --git a/app/src/main/res/drawable-mdpi/megaphone_notifications_64.webp b/app/src/main/res/drawable-mdpi/megaphone_notifications_64.webp new file mode 100644 index 0000000000..61dde0217c Binary files /dev/null and b/app/src/main/res/drawable-mdpi/megaphone_notifications_64.webp differ diff --git a/app/src/main/res/drawable-xhdpi/megaphone_notifications_64.webp b/app/src/main/res/drawable-xhdpi/megaphone_notifications_64.webp new file mode 100644 index 0000000000..abb39ff0c6 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/megaphone_notifications_64.webp differ diff --git a/app/src/main/res/drawable-xxhdpi/megaphone_notifications_64.webp b/app/src/main/res/drawable-xxhdpi/megaphone_notifications_64.webp new file mode 100644 index 0000000000..f168031e7d Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/megaphone_notifications_64.webp differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8818ccd7ca..ac74af3449 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -973,6 +973,12 @@ End call Cancel call + + Turn on Notifications? + Never miss a message from your contacts and groups. + Turn on + Not now + Multimedia message Downloading MMS message