Skip to content

Commit

Permalink
Add FCM or WebSocket option in notification settings
Browse files Browse the repository at this point in the history
  • Loading branch information
valldrac committed Oct 14, 2024
1 parent 00497b1 commit 4fe5ff4
Show file tree
Hide file tree
Showing 14 changed files with 183 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;

import com.bumptech.glide.Glide;
Expand Down Expand Up @@ -89,6 +90,7 @@
import org.thoughtcrime.securesms.mms.SignalGlideModule;
import org.thoughtcrime.securesms.net.NetworkManager;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.notifications.NotificationIds;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.ratelimit.RateLimitUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
Expand All @@ -111,7 +113,6 @@
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.FileUtils;
import org.thoughtcrime.securesms.util.PlayServicesUtil;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler;
import org.thoughtcrime.securesms.util.StorageUtil;
Expand Down Expand Up @@ -201,7 +202,7 @@ private void onCreateUnlock() {
.addBlocking("scrubber", () -> Scrubber.setIdentifierHmacKeyProvider(() -> SignalStore.svr().getOrCreateMasterKey().deriveLoggingKey()))
.addBlocking("network-settings", this::initializeNetworkSettings)
.addBlocking("first-launch", this::initializeFirstEverAppLaunch)
.addBlocking("gcm-check", this::initializeFcmCheck)
.addBlocking("push", this::updatePushNotificationServices)
.addBlocking("app-migrations", this::initializeApplicationMigrations)
.addBlocking("lifecycle-observer", () -> AppForegroundObserver.addListener(this))
.addBlocking("message-retriever", this::initializeMessageRetrieval)
Expand Down Expand Up @@ -427,11 +428,11 @@ private void initializeApplicationMigrations() {
ApplicationMigrations.onApplicationCreate(this, AppDependencies.getJobManager());
}

public void initializeMessageRetrieval() {
private void initializeMessageRetrieval() {
AppDependencies.getIncomingMessageObserver();
}

public void finalizeMessageRetrieval() {
private void finalizeMessageRetrieval() {
AppDependencies.resetNetwork(false);
}

Expand Down Expand Up @@ -495,31 +496,29 @@ private void initializeNetworkSettings() {
}
}

private void initializeFcmCheck() {
@MainThread
public void updatePushNotificationServices() {
if (!SignalStore.account().isRegistered()) {
return;
}

PlayServicesUtil.PlayServicesStatus fcmStatus = PlayServicesUtil.getPlayServicesStatus(this);

if (fcmStatus == PlayServicesUtil.PlayServicesStatus.DISABLED) {
if (SignalStore.account().isFcmEnabled()) {
boolean fcmEnabled = SignalStore.account().isFcmEnabled();
boolean forceWebSocket = SignalStore.internal().isWebsocketModeForced();

if (forceWebSocket || fcmStatus == PlayServicesUtil.PlayServicesStatus.DISABLED) {
if (fcmEnabled) {
Log.i(TAG, "Play Services are disabled. Disabling FCM.");
SignalStore.account().setFcmEnabled(false);
SignalStore.account().setFcmToken(null);
SignalStore.account().setFcmTokenLastSetTime(-1);
AppDependencies.getJobManager().add(new RefreshAttributesJob());
updateFcmStatus(false);
} else {
Log.d(TAG, "FCM is disabled.");
SignalStore.account().setFcmTokenLastSetTime(-1);
}
} else if (fcmStatus == PlayServicesUtil.PlayServicesStatus.SUCCESS &&
!SignalStore.account().isFcmEnabled() &&
} else if (fcmStatus == PlayServicesUtil.PlayServicesStatus.SUCCESS && !fcmEnabled &&
SignalStore.account().getFcmTokenLastSetTime() < 0) {
Log.i(TAG, "Play Services are newly-available. Updating to use FCM.");
SignalStore.account().setFcmEnabled(true);
AppDependencies.getJobManager().startChain(new FcmRefreshJob())
.then(new RefreshAttributesJob())
.enqueue();
updateFcmStatus(true);
} else {
long lastSetTime = SignalStore.account().getFcmTokenLastSetTime();
long nextSetTime = lastSetTime + TimeUnit.HOURS.toMillis(6);
Expand All @@ -537,6 +536,16 @@ private void initializeFcmCheck() {
}
}

private void updateFcmStatus(boolean fcmEnabled) {
SignalStore.account().setFcmEnabled(fcmEnabled);
if (!fcmEnabled) {
NotificationManagerCompat.from(this).cancel(NotificationIds.FCM_FAILURE);
}
AppDependencies.getJobManager().startChain(new FcmRefreshJob())
.then(new RefreshAttributesJob())
.enqueue();
}

private void initializeExpiringMessageManager() {
AppDependencies.getExpiringMessageManager().checkSchedule();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,6 @@ import java.util.UUID
import java.util.concurrent.TimeUnit
import kotlin.math.max
import kotlin.random.Random
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds

class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__internal_preferences) {

Expand Down Expand Up @@ -340,25 +338,6 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter

sectionHeaderPref(DSLSettingsText.from("Network"))

switchPref(
title = DSLSettingsText.from("Force websocket mode"),
summary = DSLSettingsText.from("Pretend you have no Play Services. Ignores websocket messages and keeps the websocket open in a foreground service. You have to manually force-stop the app for changes to take effect."),
isChecked = state.forceWebsocketMode,
onClick = {
viewModel.setForceWebsocketMode(!state.forceWebsocketMode)
SimpleTask.run({
val jobState = AppDependencies.jobManager.runSynchronously(RefreshAttributesJob(), 10.seconds.inWholeMilliseconds)
return@run jobState.isPresent && jobState.get().isComplete
}, { success ->
if (success) {
Toast.makeText(context, "Successfully refreshed attributes. Force-stop the app for changes to take effect.", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "Failed to refresh attributes.", Toast.LENGTH_SHORT).show()
}
})
}
)

switchPref(
title = DSLSettingsText.from("Allow censorship circumvention toggle"),
summary = DSLSettingsText.from("Allow changing the censorship circumvention toggle regardless of network connectivity."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,6 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
refresh()
}

fun setForceWebsocketMode(enabled: Boolean) {
preferenceDataStore.putBoolean(InternalValues.FORCE_WEBSOCKET_MODE, enabled)
refresh()
}

fun setUseBuiltInEmoji(enabled: Boolean) {
preferenceDataStore.putBoolean(InternalValues.FORCE_BUILT_IN_EMOJI, enabled)
refresh()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,32 @@ import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.signal.core.util.getParcelableExtraCompat
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.PromptBatterySaverDialogFragment
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.PreferenceModel
import org.thoughtcrime.securesms.components.settings.PreferenceViewHolder
import org.thoughtcrime.securesms.components.settings.RadioListPreference
import org.thoughtcrime.securesms.components.settings.RadioListPreferenceViewHolder
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.components.settings.models.Banner
import org.thoughtcrime.securesms.conversation.v2.registerForLifecycle
import org.thoughtcrime.securesms.events.PushServiceEvent
import org.thoughtcrime.securesms.keyvalue.SettingsValues.NotificationDeliveryMethod
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.notifications.TurnOnNotificationsBottomSheet
import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.PlayServicesUtil
import org.thoughtcrime.securesms.util.RingtoneUtil
import org.thoughtcrime.securesms.util.SecurePreferenceManager
import org.thoughtcrime.securesms.util.ViewUtil
Expand Down Expand Up @@ -64,6 +73,11 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__
private val ledBlinkValues by lazy { resources.getStringArray(R.array.pref_led_blink_pattern_values) }
private val ledBlinkLabels by lazy { resources.getStringArray(R.array.pref_led_blink_pattern_entries) }

private val notificationMethodValues = NotificationDeliveryMethod.entries.filterNot { method ->
method == NotificationDeliveryMethod.FCM && !BuildConfig.USE_PLAY_SERVICES
}
private val notificationMethodLabels by lazy { notificationMethodValues.map { resources.getString(it.stringId) }.toTypedArray() }

private lateinit var viewModel: NotificationsSettingsViewModel

override fun onResume() {
Expand Down Expand Up @@ -97,6 +111,13 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__
viewModel.state.observe(viewLifecycleOwner) {
adapter.submitList(getConfiguration(it).toMappingModelList())
}

EventBus.getDefault().registerForLifecycle(subscriber = this, lifecycleOwner = viewLifecycleOwner)
}

@Subscribe(threadMode = ThreadMode.MAIN)
fun onPushServiceEvent(event: PushServiceEvent) {
viewModel.refresh()
}

private fun getConfiguration(state: NotificationsSettingsState): DSLConfiguration {
Expand Down Expand Up @@ -314,6 +335,54 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__
viewModel.setNotifyWhenContactJoinsSignal(!state.notifyWhenContactJoinsSignal)
}
)

dividerPref()

sectionHeaderPref(R.string.NotificationsSettingsFragment__push_notifications)

textPref(
summary = DSLSettingsText.from(R.string.NotificationsSettingsFragment__preferred_method_for_receiving_notifications_from_the_signal_service)
)

val showAlertIcon = when (state.preferredNotificationMethod) {
NotificationDeliveryMethod.FCM -> !state.canReceiveFcm
NotificationDeliveryMethod.WEBSOCKET -> false
}
radioListPref(
title = DSLSettingsText.from(R.string.NotificationsSettingsFragment__delivery_method),
listItems = notificationMethodLabels,
selected = notificationMethodValues.indexOf(state.preferredNotificationMethod),
isEnabled = !state.isLinkedDevice, // MOLLY: TODO
iconEnd = if (showAlertIcon) DSLSettingsIcon.from(R.drawable.ic_alert) else null,
onSelected = {
onNotificationMethodChanged(notificationMethodValues[it], state.preferredNotificationMethod)
}
)
}
}

private fun onNotificationMethodChanged(
method: NotificationDeliveryMethod,
previousMethod: NotificationDeliveryMethod
) {
when (method) {
NotificationDeliveryMethod.FCM -> {
viewModel.setPreferredNotificationMethod(method)
val msgId = when (viewModel.fcmState) {
PlayServicesUtil.PlayServicesStatus.SUCCESS -> null
PlayServicesUtil.PlayServicesStatus.DISABLED -> R.string.RegistrationActivity_missing_google_play_services
PlayServicesUtil.PlayServicesStatus.MISSING -> R.string.RegistrationActivity_missing_google_play_services
PlayServicesUtil.PlayServicesStatus.NEEDS_UPDATE -> R.string.RegistrationActivity_google_play_services_is_updating_or_unavailable
PlayServicesUtil.PlayServicesStatus.TRANSIENT_ERROR -> R.string.RegistrationActivity_play_services_error
}
if (msgId != null) {
Toast.makeText(requireContext(), getString(msgId), Toast.LENGTH_LONG).show()
}
}

NotificationDeliveryMethod.WEBSOCKET -> {
viewModel.setPreferredNotificationMethod(method)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package org.thoughtcrime.securesms.components.settings.app.notifications

import android.net.Uri
import org.thoughtcrime.securesms.keyvalue.SettingsValues.NotificationDeliveryMethod

data class NotificationsSettingsState(
val messageNotificationsState: MessageNotificationsState,
val callNotificationsState: CallNotificationsState,
val notifyWhileLocked: Boolean,
val canEnableNotifyWhileLocked: Boolean,
val notifyWhenContactJoinsSignal: Boolean
val notifyWhenContactJoinsSignal: Boolean,
val isLinkedDevice: Boolean,
val preferredNotificationMethod: NotificationDeliveryMethod,
val canReceiveFcm: Boolean
)

data class MessageNotificationsState(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ import android.os.Build
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider

import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SettingsValues.NotificationDeliveryMethod
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.notifications.DeviceSpecificNotificationConfig
import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.notifications.SlowNotificationHeuristics
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference
import org.thoughtcrime.securesms.util.PlayServicesUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.livedata.Store

Expand Down Expand Up @@ -108,6 +112,15 @@ class NotificationsSettingsViewModel(private val sharedPreferences: SharedPrefer
refresh()
}

fun setPreferredNotificationMethod(method: NotificationDeliveryMethod) {
SignalStore.settings.preferredNotificationMethod = method
ApplicationContext.getInstance().updatePushNotificationServices()
AppDependencies.resetNetwork(true)
refresh()
}

val fcmState get() = PlayServicesUtil.getPlayServicesStatus(AppDependencies.application)

/**
* @param currentState If provided and [calculateSlowNotifications] = false, then we will copy the slow notification state from it
* @param calculateSlowNotifications If true, calculate the true slow notification state (this is not main-thread safe). Otherwise, it will copy from
Expand Down Expand Up @@ -142,7 +155,10 @@ class NotificationsSettingsViewModel(private val sharedPreferences: SharedPrefer
),
notifyWhileLocked = TextSecurePreferences.isPassphraseLockNotificationsEnabled(AppDependencies.application) && SignalStore.account.pushAvailable,
canEnableNotifyWhileLocked = SignalStore.account.pushAvailable,
notifyWhenContactJoinsSignal = SignalStore.settings.isNotifyWhenContactJoinsSignal
notifyWhenContactJoinsSignal = SignalStore.settings.isNotifyWhenContactJoinsSignal,
isLinkedDevice = SignalStore.account.isLinkedDevice,
preferredNotificationMethod = SignalStore.settings.preferredNotificationMethod,
canReceiveFcm = SignalStore.account.canReceiveFcm,
)

private fun canEnableNotifications(): Boolean {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package org.thoughtcrime.securesms.events

data object PushServiceEvent
Loading

0 comments on commit 4fe5ff4

Please sign in to comment.