From c4aef08a63b4c34226c37135f55fe756790a31b9 Mon Sep 17 00:00:00 2001 From: Infi Date: Fri, 8 Nov 2024 16:43:33 +0100 Subject: [PATCH] feat(debug): chucker Signed-off-by: Infi --- app/build.gradle.kts | 4 ++ .../main/java/chat/revolt/api/RevoltAPI.kt | 19 +++++ .../dialogs/NotificationRationaleDialog.kt | 17 +++++ .../revolt/screens/chat/ChatRouterScreen.kt | 72 +++++++++++++++++++ .../screens/login/LoginGreetingScreen.kt | 13 ++++ 5 files changed, 125 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6bd19b2f..1b24df5a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -338,6 +338,10 @@ dependencies { // Shimmer - loading animations implementation("com.valentinilk.shimmer:compose-shimmer:1.3.1") + // Chucker - HTTP inspector + debugImplementation("com.github.chuckerteam.chucker:library:4.0.0") + releaseImplementation("com.github.chuckerteam.chucker:library-no-op:4.0.0") + // Testing androidTestImplementation("androidx.test:runner:$androidXTestVersion") androidTestImplementation("androidx.test:rules:$androidXTestVersion") diff --git a/app/src/main/java/chat/revolt/api/RevoltAPI.kt b/app/src/main/java/chat/revolt/api/RevoltAPI.kt index 7fa7ec1d..b494da9f 100644 --- a/app/src/main/java/chat/revolt/api/RevoltAPI.kt +++ b/app/src/main/java/chat/revolt/api/RevoltAPI.kt @@ -5,6 +5,7 @@ import android.os.Looper import android.util.Log import androidx.compose.runtime.mutableStateMapOf import chat.revolt.BuildConfig +import chat.revolt.RevoltApplication import chat.revolt.api.internals.Members import chat.revolt.api.realtime.DisconnectionState import chat.revolt.api.realtime.RealtimeSocket @@ -18,6 +19,9 @@ import chat.revolt.api.schemas.User import chat.revolt.api.unreads.Unreads import chat.revolt.persistence.Database import chat.revolt.persistence.SqlStorage +import com.chuckerteam.chucker.api.ChuckerCollector +import com.chuckerteam.chucker.api.ChuckerInterceptor +import com.chuckerteam.chucker.api.RetentionManager import io.ktor.client.HttpClient import io.ktor.client.engine.okhttp.OkHttp import io.ktor.client.plugins.DefaultRequest @@ -103,6 +107,20 @@ val RevoltHttp = HttpClient(OkHttp) { install(Logging) { level = LogLevel.INFO } + val chuckerCollector = ChuckerCollector( + context = RevoltApplication.instance, + showNotification = true, + retentionPeriod = RetentionManager.Period.ONE_DAY + ) + + val chuckerInterceptor = ChuckerInterceptor.Builder(RevoltApplication.instance) + .collector(chuckerCollector) + .maxContentLength(250_000L) + .redactHeaders(RevoltAPI.TOKEN_HEADER_NAME) + .alwaysReadResponseBody(true) + .createShortcut(false) + .build() + engine { addInterceptor { chain -> val request = chain.request().newBuilder() @@ -114,6 +132,7 @@ val RevoltHttp = HttpClient(OkHttp) { .build() chain.proceed(request) } + addInterceptor(chuckerInterceptor) } defaultRequest { diff --git a/app/src/main/java/chat/revolt/dialogs/NotificationRationaleDialog.kt b/app/src/main/java/chat/revolt/dialogs/NotificationRationaleDialog.kt index cab125df..4e1ac34e 100644 --- a/app/src/main/java/chat/revolt/dialogs/NotificationRationaleDialog.kt +++ b/app/src/main/java/chat/revolt/dialogs/NotificationRationaleDialog.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.widthIn import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -14,7 +15,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import chat.revolt.BuildConfig import chat.revolt.R @Composable @@ -42,6 +47,18 @@ fun NotificationRationaleDialog( Text( text = stringResource(id = R.string.spark_notifications_rationale_description) ) + if (BuildConfig.DEBUG) { + Text( + text = buildAnnotatedString { + pushStyle(SpanStyle(fontWeight = FontWeight.Bold)) + append("Debug build:") + pop() + append(" Required to open Chucker for network debugging. ") + append("You can show this dialogue again from Settings -> Debug.") + }, + color = MaterialTheme.colorScheme.error + ) + } } }, dismissButton = { diff --git a/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt b/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt index 1a57cad7..c964ccf6 100644 --- a/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt +++ b/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt @@ -2,9 +2,13 @@ package chat.revolt.screens.chat import android.annotation.SuppressLint import android.content.Context +import android.os.Build +import android.util.Log import android.view.accessibility.AccessibilityManager import android.view.inputmethod.InputMethodManager import androidx.activity.compose.BackHandler +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -46,20 +50,24 @@ import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import androidx.core.app.NotificationManagerCompat import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.NavController +import chat.revolt.BuildConfig import chat.revolt.R import chat.revolt.api.RevoltAPI import chat.revolt.api.internals.DirectMessages import chat.revolt.api.realtime.DisconnectionState import chat.revolt.api.realtime.RealtimeSocket +import chat.revolt.api.routes.push.subscribePush import chat.revolt.callbacks.Action import chat.revolt.callbacks.ActionChannel import chat.revolt.components.chat.DisconnectedNotice import chat.revolt.components.screens.chat.drawer.ChannelSideDrawer import chat.revolt.components.screens.voice.VoiceChannelOverlay +import chat.revolt.dialogs.NotificationRationaleDialog import chat.revolt.internals.Changelogs import chat.revolt.internals.extensions.zero import chat.revolt.persistence.KVStorage @@ -84,8 +92,11 @@ import com.airbnb.lottie.compose.LottieAnimation import com.airbnb.lottie.compose.LottieCompositionSpec import com.airbnb.lottie.compose.animateLottieCompositionAsState import com.airbnb.lottie.compose.rememberLottieComposition +import com.google.android.gms.tasks.OnCompleteListener +import com.google.firebase.messaging.FirebaseMessaging import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import io.sentry.Sentry import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import javax.inject.Inject @@ -137,6 +148,7 @@ class ChatRouterViewModel @Inject constructor( var latestChangelogRead by mutableStateOf(true) var latestChangelog by mutableStateOf("") var latestChangelogBody by mutableStateOf("") + var showNotificationRationale by mutableStateOf(false) private val changelogs = Changelogs(context, kvStorage) @@ -158,6 +170,13 @@ class ChatRouterViewModel @Inject constructor( if (!latestChangelogRead) { changelogs.markAsSeen() } + + val hasNotificationPermission = + NotificationManagerCompat.from(context).areNotificationsEnabled() + // right now we only show this in debug builds so Chucker can show its notification + if (!hasNotificationPermission && BuildConfig.DEBUG) { + showNotificationRationale = true + } } } @@ -181,6 +200,32 @@ class ChatRouterViewModel @Inject constructor( sidebarSparkDisplayed = true } + fun setRegisterForNotifications() { + showNotificationRationale = false + FirebaseMessaging.getInstance().token.addOnCompleteListener( + OnCompleteListener { task -> + if (!task.isSuccessful) { + Log.w("FCM", "Fetching FCM registration token failed", task.exception) + task.exception?.let { Sentry.captureException(it) } + return@OnCompleteListener + } + + val token = task.result + viewModelScope.launch { + kvStorage.set("fcmToken", token) + subscribePush(auth = token) + } + } + ) + } + + fun markNotificationsRejected() { + showNotificationRationale = false + viewModelScope.launch { + kvStorage.set("pushNotificationsRejected", true) + } + } + fun navigateToServer(serverId: String) { viewModelScope.launch { val savedLastChannel = kvStorage.get("lastChannel/$serverId") @@ -692,6 +737,33 @@ fun ChatRouterScreen( } } + val askNotificationsPermission = + rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + viewModel.setRegisterForNotifications() + } else { + viewModel.showNotificationRationale = false + } + } + if (viewModel.showNotificationRationale) { + NotificationRationaleDialog( + onDismiss = { + viewModel.showNotificationRationale = false + }, + onSelected = { accepted -> + if (accepted) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + askNotificationsPermission.launch(android.Manifest.permission.POST_NOTIFICATIONS) + } else { + viewModel.setRegisterForNotifications() + } + } else { + viewModel.markNotificationsRejected() + } + } + ) + } + Column( modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/chat/revolt/screens/login/LoginGreetingScreen.kt b/app/src/main/java/chat/revolt/screens/login/LoginGreetingScreen.kt index d287f9d6..a2c2756f 100644 --- a/app/src/main/java/chat/revolt/screens/login/LoginGreetingScreen.kt +++ b/app/src/main/java/chat/revolt/screens/login/LoginGreetingScreen.kt @@ -38,9 +38,12 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController +import chat.revolt.BuildConfig import chat.revolt.R import chat.revolt.api.REVOLT_MARKETING +import chat.revolt.components.generic.AnyLink import chat.revolt.components.generic.Weblink +import com.chuckerteam.chucker.api.Chucker @Composable fun LoginGreetingScreen(navController: NavController) { @@ -159,6 +162,16 @@ fun LoginGreetingScreen(navController: NavController) { text = stringResource(R.string.community_guidelines), url = "$REVOLT_MARKETING/aup" ) + if (BuildConfig.DEBUG) { + AnyLink( + text = "Debug: Chucker", + action = { + Chucker.getLaunchIntent(context).apply { + context.startActivity(this) + } + } + ) + } } } }