Observe service outages in a lifecycle-aware fashion.

This commit is contained in:
Nicholas Tinsley
2024-08-19 13:52:15 -04:00
committed by mtang-signal
parent fd31bc60b2
commit 4002dea05d
6 changed files with 127 additions and 13 deletions

View File

@@ -9,15 +9,18 @@ import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.banner.Banner
import org.thoughtcrime.securesms.banner.ui.compose.DefaultBanner
import org.thoughtcrime.securesms.banner.ui.compose.Importance
import org.thoughtcrime.securesms.util.TextSecurePreferences
class ServiceOutageBanner(context: Context) : Banner() {
class ServiceOutageBanner(outageInProgress: Boolean) : Banner() {
override val enabled = TextSecurePreferences.getServiceOutage(context)
constructor(context: Context) : this(TextSecurePreferences.getServiceOutage(context))
override val enabled = outageInProgress
@Composable
override fun DisplayBanner() {
@@ -31,8 +34,14 @@ class ServiceOutageBanner(context: Context) : Banner() {
companion object {
@JvmStatic
fun createFlow(context: Context): Flow<ServiceOutageBanner> = createAndEmit {
fun createOneShotFlow(context: Context): Flow<ServiceOutageBanner> = createAndEmit {
ServiceOutageBanner(context)
}
/**
* Take a [Flow] of [Boolean] values representing the service status and map it into a [Flow] of [ServiceOutageBanner]
*/
@JvmStatic
fun fromFlow(statusFlow: Flow<Boolean>): Flow<ServiceOutageBanner> = statusFlow.map { ServiceOutageBanner(it) }
}
}

View File

@@ -308,6 +308,7 @@ import org.thoughtcrime.securesms.util.MessageConstraintsUtil.isValidEditMessage
import org.thoughtcrime.securesms.util.PlayStoreUtil
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.SaveAttachmentUtil
import org.thoughtcrime.securesms.util.ServiceOutageObserver
import org.thoughtcrime.securesms.util.SignalLocalMetrics
import org.thoughtcrime.securesms.util.StorageUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
@@ -1020,8 +1021,11 @@ class ConversationFragment :
val conversationBannerListener = ConversationBannerListener()
binding.conversationBanner.listener = conversationBannerListener
if (RemoteConfig.newBannerUi) {
val serviceOutageObserver = ServiceOutageObserver(requireContext())
val bannerFlows = viewModel.getBannerFlows(
context = requireContext(),
serviceOutageStatusFlow = serviceOutageObserver.flow,
groupJoinClickListener = conversationBannerListener::reviewJoinRequestsAction,
onAddMembers = {
conversationGroupViewModel.groupRecordSnapshot?.let { groupRecord ->
@@ -1033,10 +1037,6 @@ class ConversationFragment :
)
binding.conversationBanner.collectAndShowBanners(bannerFlows)
if (TextSecurePreferences.getServiceOutage(context)) {
AppDependencies.jobManager.add(ServiceOutageDetectionJob())
}
} else {
viewModel
.reminder

View File

@@ -323,7 +323,7 @@ class ConversationViewModel(
}
@OptIn(ExperimentalCoroutinesApi::class)
fun getBannerFlows(context: Context, groupJoinClickListener: () -> Unit, onAddMembers: () -> Unit, onNoThanks: () -> Unit, bubbleClickListener: (Boolean) -> Unit): List<Flow<Banner>> {
fun getBannerFlows(context: Context, serviceOutageStatusFlow: Flow<Boolean>, groupJoinClickListener: () -> Unit, onAddMembers: () -> Unit, onNoThanks: () -> Unit, bubbleClickListener: (Boolean) -> Unit): List<Flow<Banner>> {
val pendingGroupJoinFlow: Flow<PendingGroupJoinRequestsBanner> = merge(
flow {
emit(PendingGroupJoinRequestsBanner(false, 0, {}, {}))
@@ -341,7 +341,7 @@ class ConversationViewModel(
return listOf(
OutdatedBuildBanner.createFlow(context, OutdatedBuildBanner.ExpiryStatus.EXPIRED_ONLY),
UnauthorizedBanner.createFlow(context),
ServiceOutageBanner.createFlow(context),
ServiceOutageBanner.fromFlow(serviceOutageStatusFlow),
pendingGroupJoinFlow,
groupV1SuggestionsFlow,
BubbleOptOutBanner.createFlow(inBubble = repository.isInBubble, bubbleClickListener)

View File

@@ -183,6 +183,7 @@ import org.thoughtcrime.securesms.util.CachedInflater;
import org.thoughtcrime.securesms.util.ConversationUtil;
import org.thoughtcrime.securesms.util.PlayStoreUtil;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.ServiceOutageObserver;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
import org.thoughtcrime.securesms.util.SignalProxyUtil;
@@ -892,9 +893,13 @@ public class ConversationListFragment extends MainFragment implements ActionMode
}
private void initializeBanners() {
final ServiceOutageObserver serviceOutageObserver = new ServiceOutageObserver(requireContext());
getLifecycle().addObserver(serviceOutageObserver);
final List<Flow<? extends Banner>> bannerRepositories = List.of(OutdatedBuildBanner.createFlow(requireContext(), OutdatedBuildBanner.ExpiryStatus.EXPIRED_ONLY),
UnauthorizedBanner.createFlow(requireContext()),
ServiceOutageBanner.createFlow(requireContext()),
ServiceOutageBanner.fromFlow(serviceOutageObserver.getFlow()),
OutdatedBuildBanner.createFlow(requireContext(), OutdatedBuildBanner.ExpiryStatus.OUTDATED_ONLY),
DozeBanner.createFlow(requireContext()),
CdsTemporaryErrorBanner.createFlow(getChildFragmentManager()),

View File

@@ -21,9 +21,9 @@ public class ServiceOutageDetectionJob extends BaseJob {
private static final String TAG = Log.tag(ServiceOutageDetectionJob.class);
private static final String IP_SUCCESS = "127.0.0.1";
private static final String IP_FAILURE = "127.0.0.2";
private static final long CHECK_TIME = 1000 * 60;
public static final String IP_SUCCESS = "127.0.0.1";
public static final String IP_FAILURE = "127.0.0.2";
public static final long CHECK_TIME = 1000 * 60;
public ServiceOutageDetectionJob() {
this(new Job.Parameters.Builder()

View File

@@ -0,0 +1,100 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.util
import android.content.Context
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob
import java.net.InetAddress
import java.net.UnknownHostException
/**
* A lifecycle aware observer that can be instantiated to monitor the service for outages.
*
* @see [ServiceOutageDetectionJob]
*/
class ServiceOutageObserver(private val context: Context) : DefaultLifecycleObserver {
companion object {
val TAG = Log.tag(ServiceOutageObserver::class)
}
private var observing = false
override fun onResume(owner: LifecycleOwner) {
observing = true
}
override fun onStop(owner: LifecycleOwner) {
observing = false
}
val flow: Flow<Boolean> = flow {
emit(TextSecurePreferences.getServiceOutage(context))
TextSecurePreferences.setLastOutageCheckTime(context, System.currentTimeMillis())
while (true) {
if (observing && getNextCheckTime(context) <= System.currentTimeMillis()) {
when (queryAvailability()) {
Result.SUCCESS -> {
TextSecurePreferences.setServiceOutage(context, false)
emit(false)
}
Result.FAILURE -> {
Log.w(TAG, "Service is down.")
TextSecurePreferences.setServiceOutage(context, true)
emit(true)
}
Result.RETRY_LATER -> {
Log.w(TAG, "Service status check returned an unrecognized IP address. Could be a weird network state. Prompting retry.")
}
}
}
val nextCheckTime = getNextCheckTime(context)
val now = System.currentTimeMillis()
val delay = nextCheckTime - now
if (delay > 0) {
delay(delay)
}
}
}
private fun getNextCheckTime(context: Context): Long = TextSecurePreferences.getLastOutageCheckTime(context) + ServiceOutageDetectionJob.CHECK_TIME
private suspend fun queryAvailability(): Result = withContext(Dispatchers.IO) {
try {
val address = InetAddress.getByName(BuildConfig.SIGNAL_SERVICE_STATUS_URL)
val now = System.currentTimeMillis()
TextSecurePreferences.setLastOutageCheckTime(context, now)
if (ServiceOutageDetectionJob.IP_SUCCESS == address.hostAddress) {
Result.SUCCESS
} else if (ServiceOutageDetectionJob.IP_FAILURE == address.hostAddress) {
Result.FAILURE
} else {
Result.RETRY_LATER
}
} catch (e: UnknownHostException) {
Log.i(TAG, "Received UnknownHostException!", e)
Result.RETRY_LATER
}
}
private enum class Result {
SUCCESS, FAILURE, RETRY_LATER
}
}